diff --git a/build-number.txt b/build-number.txt index 3f7edff6..18bc02fc 100644 --- a/build-number.txt +++ b/build-number.txt @@ -1 +1 @@ -3211 \ No newline at end of file +3252 \ No newline at end of file diff --git a/build.gradle b/build.gradle index 674bf735..f37d9f22 100644 --- a/build.gradle +++ b/build.gradle @@ -42,7 +42,9 @@ repositories { dependencies { implementation 'org.geysermc.mcprotocollib:protocol:1.21.5-SNAPSHOT' - implementation 'net.kyori:adventure-text-serializer-legacy:4.19.0' + implementation 'net.kyori:adventure-text-serializer-plain:4.21.0' + implementation 'net.kyori:adventure-text-serializer-legacy:4.21.0' + implementation 'net.kyori:adventure-text-serializer-ansi:4.21.0' implementation 'com.google.code.gson:gson:2.12.1' implementation 'com.google.guava:guava:33.4.0-jre' implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.3' diff --git a/src/main/java/me/chayapak1/chomens_bot/plugins/ChatPlugin.java b/src/main/java/me/chayapak1/chomens_bot/plugins/ChatPlugin.java index 453bf822..cca594ff 100644 --- a/src/main/java/me/chayapak1/chomens_bot/plugins/ChatPlugin.java +++ b/src/main/java/me/chayapak1/chomens_bot/plugins/ChatPlugin.java @@ -388,7 +388,7 @@ public class ChatPlugin implements Listener { if (bot.options.useChat) { if (!targets.equals("@a")) return; // worst fix of all time!1! - final String stringified = ComponentUtilities.stringifySectionSign(component).replace("§", "&"); + final String stringified = ComponentUtilities.stringifyLegacy(component).replace("§", "&"); send(stringified); } else { bot.core.run("minecraft:tellraw " + targets + " " + SNBTUtilities.fromComponent(bot, component)); diff --git a/src/main/java/me/chayapak1/chomens_bot/util/ComponentUtilities.java b/src/main/java/me/chayapak1/chomens_bot/util/ComponentUtilities.java index b223426b..7d4268bf 100644 --- a/src/main/java/me/chayapak1/chomens_bot/util/ComponentUtilities.java +++ b/src/main/java/me/chayapak1/chomens_bot/util/ComponentUtilities.java @@ -5,16 +5,21 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import net.kyori.adventure.text.*; +import net.kyori.adventure.text.flattener.ComponentFlattener; import net.kyori.adventure.text.format.NamedTextColor; -import net.kyori.adventure.text.format.Style; -import net.kyori.adventure.text.format.TextColor; -import net.kyori.adventure.text.format.TextDecoration; +import net.kyori.adventure.text.serializer.ComponentEncoder; +import net.kyori.adventure.text.serializer.ansi.ANSIComponentSerializer; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import net.kyori.ansi.ColorLevel; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; +import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -33,6 +38,14 @@ public class ComponentUtilities { } } + private static final Pattern ARG_PATTERN = Pattern.compile("%(?:(\\d+)\\$)?([A-Za-z%]|$)"); + private static final Pattern DISCORD_ANSI_PATTERN = Pattern.compile("(\\u001b\\[\\d+m)"); + private static final Pattern SECTION_SIGN_PATTERN = Pattern.compile("(§.)"); + + private static final ThreadLocal TOTAL_DEPTH = ThreadLocal.withInitial(() -> 0); + + private static final int MAX_DEPTH = 512; // same as adventure + private static Map loadJsonStringMap (final String name) { final Map map = new Object2ObjectOpenHashMap<>(); @@ -48,6 +61,34 @@ public class ComponentUtilities { return map; } + private static ComponentFlattener getFlattener (final boolean shouldReplaceSectionSignsWithANSI, final boolean isDiscord) { + return ComponentFlattener.builder() + .mapper(TextComponent.class, component -> mapText(component, shouldReplaceSectionSignsWithANSI, isDiscord)) + .complexMapper(KeybindComponent.class, ComponentUtilities::mapKeybind) + .mapper(SelectorComponent.class, SelectorComponent::pattern) + .complexMapper(TranslatableComponent.class, ComponentUtilities::mapTranslatable) + .unknownMapper(component -> "") + .build(); + } + + private static final ANSIComponentSerializer TRUE_COLOR_ANSI_SERIALIZER = ANSIComponentSerializer.builder() + .flattener(getFlattener(true, false)) + .colorLevel(ColorLevel.TRUE_COLOR) + .build(); + + private static final ANSIComponentSerializer DISCORD_ANSI_SERIALIZER = ANSIComponentSerializer.builder() + .flattener(getFlattener(true, true)) + .colorLevel(ColorLevel.INDEXED_8) + .build(); + + private static final LegacyComponentSerializer LEGACY_COMPONENT_SERIALIZER = LegacyComponentSerializer.builder() + .flattener(getFlattener(false, false)) + .build(); + + private static final PlainTextComponentSerializer PLAIN_TEXT_COMPONENT_SERIALIZER = PlainTextComponentSerializer.builder() + .flattener(getFlattener(false, false)) + .build(); + public static String getOrReturnFallback (final TranslatableComponent component) { final String key = component.key(); final String fallback = component.fallback(); @@ -58,485 +99,214 @@ public class ComponentUtilities { ); } - public static String stringify (final Component message) { return new ComponentParser().stringify(message, ComponentParser.ParseType.PLAIN); } - - public static String stringifySectionSign (final Component message) { return new ComponentParser().stringify(message, ComponentParser.ParseType.SECTION_SIGNS); } - - public static String stringifyAnsi (final Component message) { return new ComponentParser().stringify(message, ComponentParser.ParseType.ANSI); } - - public static String stringifyDiscordAnsi (final Component message) { return new ComponentParser().stringify(message, ComponentParser.ParseType.DISCORD_ANSI); } - - public static String deserializeFromDiscordAnsi (final String original) { return new ComponentParser().deserializeFromDiscordAnsi(original); } - - private static class ComponentParser { - public static final Pattern ARG_PATTERN = Pattern.compile("%(?:(\\d+)\\$)?([A-Za-z%]|$)"); - - public static final long MAX_TIME = 25; // Change Clock Time And Break Bot ! - - public static final int MAX_DEPTH = 256; - - public static final Map ANSI_MAP = new Object2ObjectOpenHashMap<>(); - - static { - ANSI_MAP.put("0", "\u001b[38;2;0;0;0m"); - ANSI_MAP.put("1", "\u001b[38;2;0;0;170m"); - ANSI_MAP.put("2", "\u001b[38;2;0;170;0m"); - ANSI_MAP.put("3", "\u001b[38;2;0;170;170m"); - ANSI_MAP.put("4", "\u001b[38;2;170;0;0m"); - ANSI_MAP.put("5", "\u001b[38;2;170;0;170m"); - ANSI_MAP.put("6", "\u001b[38;2;255;170;0m"); - ANSI_MAP.put("7", "\u001b[38;2;170;170;170m"); - ANSI_MAP.put("8", "\u001b[38;2;85;85;85m"); - ANSI_MAP.put("9", "\u001b[38;2;85;85;255m"); - ANSI_MAP.put("a", "\u001b[38;2;85;255;85m"); - ANSI_MAP.put("b", "\u001b[38;2;85;255;255m"); - ANSI_MAP.put("c", "\u001b[38;2;255;85;85m"); - ANSI_MAP.put("d", "\u001b[38;2;255;85;255m"); - ANSI_MAP.put("e", "\u001b[38;2;255;255;85m"); - ANSI_MAP.put("f", "\u001b[38;2;255;255;255m"); - ANSI_MAP.put("l", "\u001b[1m"); - ANSI_MAP.put("o", "\u001b[3m"); - ANSI_MAP.put("n", "\u001b[4m"); - ANSI_MAP.put("m", "\u001b[9m"); - ANSI_MAP.put("k", "\u001b[6m"); - ANSI_MAP.put("r", "\u001b[0m"); - } - - public static final Map DISCORD_ANSI_MAP = new Object2ObjectOpenHashMap<>(); - - static { - // map totallynotskidded™ from https://github.com/PrismarineJS/prismarine-chat/blob/master/index.js#L10 - - // these bright colors have to come first because of deserialization - DISCORD_ANSI_MAP.put("a", "\u001b[32m"); - DISCORD_ANSI_MAP.put("b", "\u001b[36m"); - DISCORD_ANSI_MAP.put("c", "\u001b[31m"); - DISCORD_ANSI_MAP.put("d", "\u001b[35m"); - DISCORD_ANSI_MAP.put("e", "\u001b[33m"); - DISCORD_ANSI_MAP.put("f", "\u001b[37m"); - - DISCORD_ANSI_MAP.put("0", "\u001b[30m"); - DISCORD_ANSI_MAP.put("1", "\u001b[34m"); - DISCORD_ANSI_MAP.put("2", "\u001b[32m"); - DISCORD_ANSI_MAP.put("3", "\u001b[36m"); - DISCORD_ANSI_MAP.put("4", "\u001b[31m"); - DISCORD_ANSI_MAP.put("5", "\u001b[35m"); - DISCORD_ANSI_MAP.put("6", "\u001b[33m"); - DISCORD_ANSI_MAP.put("7", "\u001b[37m"); - DISCORD_ANSI_MAP.put("8", "\u001b[30m"); - DISCORD_ANSI_MAP.put("9", "\u001b[34m"); - - DISCORD_ANSI_MAP.put("l", "\u001b[1m"); - DISCORD_ANSI_MAP.put("o", "\u001b[3m"); - DISCORD_ANSI_MAP.put("n", "\u001b[4m"); - DISCORD_ANSI_MAP.put("m", "\u001b[9m"); - DISCORD_ANSI_MAP.put("k", "\u001b[6m"); - DISCORD_ANSI_MAP.put("r", "\u001b[0m"); - } - - public static final Map NAMED_TEXT_COLOR_MAP = new Object2ObjectOpenHashMap<>(); - - static { - NAMED_TEXT_COLOR_MAP.put(NamedTextColor.BLACK, "0"); - NAMED_TEXT_COLOR_MAP.put(NamedTextColor.DARK_BLUE, "1"); - NAMED_TEXT_COLOR_MAP.put(NamedTextColor.DARK_GREEN, "2"); - NAMED_TEXT_COLOR_MAP.put(NamedTextColor.DARK_AQUA, "3"); - NAMED_TEXT_COLOR_MAP.put(NamedTextColor.DARK_RED, "4"); - NAMED_TEXT_COLOR_MAP.put(NamedTextColor.DARK_PURPLE, "5"); - NAMED_TEXT_COLOR_MAP.put(NamedTextColor.GOLD, "6"); - NAMED_TEXT_COLOR_MAP.put(NamedTextColor.GRAY, "7"); - NAMED_TEXT_COLOR_MAP.put(NamedTextColor.DARK_GRAY, "8"); - NAMED_TEXT_COLOR_MAP.put(NamedTextColor.BLUE, "9"); - NAMED_TEXT_COLOR_MAP.put(NamedTextColor.GREEN, "a"); - NAMED_TEXT_COLOR_MAP.put(NamedTextColor.AQUA, "b"); - NAMED_TEXT_COLOR_MAP.put(NamedTextColor.RED, "c"); - NAMED_TEXT_COLOR_MAP.put(NamedTextColor.LIGHT_PURPLE, "d"); - NAMED_TEXT_COLOR_MAP.put(NamedTextColor.YELLOW, "e"); - NAMED_TEXT_COLOR_MAP.put(NamedTextColor.WHITE, "f"); - } - - // we only focus on the discord ones that we made - private static final Pattern DISCORD_ANSI_PATTERN = Pattern.compile("(\\u001b\\[\\d+m)"); - - private ParseType type; - - private long parseStartTime = System.currentTimeMillis(); - - private int depth = 0; - - private String lastColor = ""; - private String lastStyle = ""; - - private boolean isSubParsing = false; - - public String deserializeFromDiscordAnsi (final String original) { - final Matcher matcher = DISCORD_ANSI_PATTERN.matcher(original); - final StringBuilder builder = new StringBuilder(); - - while (matcher.find()) { - final String match = matcher.group(); - - boolean replaced = false; - - for (final Map.Entry entry : DISCORD_ANSI_MAP.entrySet()) { - if (!entry.getValue().equals(match)) continue; - - matcher.appendReplacement(builder, "§" + entry.getKey()); - - replaced = true; - - break; - } - - if (!replaced) matcher.appendReplacement(builder, match); - } - - matcher.appendTail(builder); - - return builder.toString(); - } - - private String stringify (final Component message, final ParseType type) { - this.type = type; - - if (depth > MAX_DEPTH || System.currentTimeMillis() > parseStartTime + MAX_TIME) return ""; - - try { - final StringBuilder builder = new StringBuilder(); - - final String color = getColor(message.color()); - final String style = getStyle(message.style()); - - final String output = stringifyPartially(message, color, style); - - builder.append(output); - - for (final Component child : message.children()) { - final ComponentParser parser = new ComponentParser(); - parser.lastColor = lastColor + color; - parser.lastStyle = lastStyle + style; - parser.parseStartTime = parseStartTime; - parser.depth = depth; - parser.isSubParsing = true; - builder.append(parser.stringify(child, type)); - } - - if ( - !isSubParsing && - (type == ParseType.ANSI || type == ParseType.DISCORD_ANSI) - ) { - builder.append(ANSI_MAP.get("r")); - } - - return builder.toString(); - } catch (final Exception e) { - LoggerUtilities.error(e); - return ""; - } - } - - public String stringifyPartially (final Component message, final String color, final String style) { - return switch (message) { - case final TextComponent t_component -> stringifyPartially(t_component, color, style); - case final TranslatableComponent t_component -> stringifyPartially(t_component, color, style); - case final SelectorComponent t_component -> stringifyPartially(t_component, color, style); - case final KeybindComponent t_component -> stringifyPartially(t_component, color, style); - default -> String.format("[Component type %s not implemented!]", message.getClass().getSimpleName()); - }; - } - - public String getStyle (final Style textStyle) { - if (textStyle == null) return ""; - - final StringBuilder style = new StringBuilder(); - - for (final Map.Entry decorationEntry : textStyle.decorations().entrySet()) { - final TextDecoration decoration = decorationEntry.getKey(); - final TextDecoration.State state = decorationEntry.getValue(); - - if (state == TextDecoration.State.NOT_SET) continue; - - if (type != ParseType.PLAIN) { - final Map styleMap = - type == ParseType.ANSI ? ANSI_MAP : DISCORD_ANSI_MAP; - - final String key = switch (decoration) { - case BOLD -> "l"; - case ITALIC -> "o"; - case UNDERLINED -> "n"; - case STRIKETHROUGH -> "m"; - case OBFUSCATED -> "k"; - }; - - final String code = type == ParseType.SECTION_SIGNS ? - "§" + key : - styleMap.getOrDefault(key, ""); - - if (state == TextDecoration.State.TRUE) { - style.append(code); - } else { - // state is FALSE, meaning that the component HAS SPECIFIED the style to be empty - - if (!lastStyle.isEmpty()) lastStyle = lastStyle.replace(code, ""); - - if (type == ParseType.SECTION_SIGNS) { - style - .append(getResetCode()) - .append(lastColor) - .append(lastStyle); - } else { - style - .append(getResetCode()) - .append(lastStyle) - .append(lastColor); - } - } - } - } - - return style.toString(); - } - - public String getColor (final TextColor color) { - if (color == null) return ""; - - // map totallynotskidded™ from https://github.com/PrismarineJS/prismarine-chat/blob/master/index.js#L299 - final String code; - if (color instanceof final NamedTextColor named) { - code = NAMED_TEXT_COLOR_MAP.getOrDefault(named, ""); - } else { - code = color.asHexString(); - } - - if (type == ParseType.SECTION_SIGNS) { - return "§" + code; - } else if (type == ParseType.ANSI || type == ParseType.DISCORD_ANSI) { - String ansiCode = type == ParseType.ANSI ? - ANSI_MAP.get(code) : - DISCORD_ANSI_MAP.get(code); - if (ansiCode == null) { - if (type == ParseType.DISCORD_ANSI) { - // gets the closest color to the hex - - final int rgb = Integer.parseInt(code.substring(1), 16); - final String chatColor = Character.toString(ColorUtilities.getClosestChatColor(rgb)); - - ansiCode = DISCORD_ANSI_MAP.get(chatColor); - } else { - ansiCode = "\u001b[38;2;" + - color.red() + - ";" + - color.green() + - ";" + - color.blue() + - "m"; - } - } - - return ansiCode; - } - - return ""; - } - - private String getResetCode () { - return switch (type) { - case SECTION_SIGNS -> "§r"; - case ANSI, DISCORD_ANSI -> ANSI_MAP.get("r"); - default -> ""; - }; - } - - private String getPartialResult (final String originalResult, final String color, final String style, final boolean setLastStyles) { - if (type == ParseType.PLAIN) return originalResult; - - final String orderedStyling = type == ParseType.SECTION_SIGNS - ? lastColor + lastStyle + color + style - : lastStyle + lastColor + style + color; - - final String result = - orderedStyling + - originalResult + - ( - !color.isEmpty() || !style.isEmpty() - ? getResetCode() - : "" - ); - - if (setLastStyles) { - lastColor = color; - lastStyle = style; - } - - return result; - } - - private String stringifyPartially (final String message, final String color, final String style) { - if (type == ParseType.PLAIN) return message; - - final boolean isAllAnsi = type == ParseType.ANSI || type == ParseType.DISCORD_ANSI; - - String replacedContent = message; - // processes section signs - // not processing invalid codes is INTENTIONAL and it is a FEATURE - if (isAllAnsi && replacedContent.contains("§")) { - // is try-catch a great idea for these? - try { - replacedContent = Pattern - .compile("(§.)") - .matcher(message) - .replaceAll(m -> { - final String code = m.group(0).substring(1); - - if (!code.equals("r")) return type == ParseType.ANSI ? - ANSI_MAP.get(code) : - DISCORD_ANSI_MAP.get(code); - else return color; - }); - } catch (final Exception ignored) { } - } - - return getPartialResult(replacedContent, color, style, true); - } - - private String stringifyPartially (final TextComponent message, final String color, final String style) { - return stringifyPartially(message.content(), color, style); - } - - private String stringifyPartially (final TranslatableComponent message, final String color, final String style) { - final String format = getOrReturnFallback(message); - - final Matcher matcher = ARG_PATTERN.matcher(format); - final StringBuilder sb = new StringBuilder(); - - // RIPPED straight from minecraft source code !!! :D - try { - int i = 0; - - int lastIndex = 0; - - while (matcher.find(lastIndex)) { - final int start = matcher.start(); - final int end = matcher.end(); - - if (start > lastIndex) { - final String formatSegment = format.substring(lastIndex, start); - - if (formatSegment.indexOf('%') != -1) { - throw new IllegalArgumentException(); - } - - appendTranslation( - color, - style, - sb, - Component.text(formatSegment) - ); - } - - final String full = format.substring(start, end); - - if (matcher.group().equals("%") && full.equals("%%")) { - sb.append('%'); - } else if (matcher.group(2).equals("s")) { - final String idxStr = matcher.group(1); - - final int idx = idxStr == null ? i++ : (Integer.parseInt(idxStr) - 1); - - if (idx < 0 || idx > message.arguments().size()) throw new IllegalArgumentException(); - - appendTranslation( - color, - style, - sb, - message.arguments() - .get(idx) - .asComponent() - ); - } else { - throw new IllegalArgumentException(); - } - - lastIndex = end; - } - - if (lastIndex < format.length()) { - final String remaining = format.substring(lastIndex); - - if (remaining.indexOf('%') != -1) { - throw new IllegalArgumentException(); - } - - appendTranslation( - color, - style, - sb, - Component.text(remaining) - ); - } - } catch (final Exception e) { - sb.setLength(0); - sb.append(format); - } - - return getPartialResult(sb.toString(), color, style, true); - } - - private void appendTranslation ( - final String color, - final String style, - final StringBuilder sb, - final Component message - ) { - depth++; - - final ComponentParser parser = new ComponentParser(); - - parser.lastColor = lastColor + color; - parser.lastStyle = lastStyle + style; - parser.parseStartTime = parseStartTime; - parser.depth = depth; - parser.isSubParsing = true; - - final String orderedStyling = type == ParseType.SECTION_SIGNS - ? lastColor + lastStyle + color + style - : lastStyle + lastColor + style + color; - - sb.append( - getPartialResult( - parser.stringify( - message, - type - ), - color, - style, - false + private static String guardedStringify (final ComponentEncoder serializer, final Component message) { + try { + return serializer.serialize(message); + } catch (final Exception e) { + return guardedStringify( + serializer, + Component.translatable( + "", + NamedTextColor.RED, + Component.text(e.toString()) ) ); - sb.append(orderedStyling); + } finally { + TOTAL_DEPTH.set(0); + } + } + + public static String stringify (final Component message) { + return guardedStringify(PLAIN_TEXT_COMPONENT_SERIALIZER, message); + } + + public static String stringifyLegacy (final Component message) { + return guardedStringify(LEGACY_COMPONENT_SERIALIZER, message); + } + + public static String stringifyAnsi (final Component message) { + return guardedStringify(TRUE_COLOR_ANSI_SERIALIZER, message); + } + + public static String stringifyDiscordAnsi (final Component message) { + return guardedStringify(DISCORD_ANSI_SERIALIZER, message) + .replace("9", "3"); // we have to downscale because discord's ANSI doesn't have bright colors + } + + public static String deserializeFromDiscordAnsi (final String original) { + final Matcher matcher = DISCORD_ANSI_PATTERN.matcher(original); + final StringBuilder builder = new StringBuilder(); + + while (matcher.find()) { + final String match = matcher.group(); + + boolean replaced = false; + + final ANSIStyle[] values = ANSIStyle.values(); + + for (final ANSIStyle value : values) { + if (!value.discordAnsiCode.equals(match)) continue; + matcher.appendReplacement(builder, "§" + value.legacyCode); + replaced = true; + break; + } + + if (!replaced) matcher.appendReplacement(builder, match); } - // on the client side, this acts just like TextComponent - // and does NOT process any players stuff - private String stringifyPartially (final SelectorComponent message, final String color, final String style) { - return stringifyPartially(message.pattern(), style, color); + matcher.appendTail(builder); + + return builder.toString(); + } + + private static String mapText ( + final TextComponent component, + final boolean shouldReplaceSectionSignsWithANSI, + final boolean isDiscord + ) { + final String content = component.content(); + + if (!shouldReplaceSectionSignsWithANSI || !content.contains("§")) { + return component.content(); + } else { + try { + return SECTION_SIGN_PATTERN + .matcher(component.content()) + .replaceAll(match -> { + final String code = match.group(0).substring(1); + + final ANSIStyle[] values = ANSIStyle.values(); + + for (final ANSIStyle value : values) { + if (!code.equals(value.legacyCode)) continue; + + return isDiscord + ? value.discordAnsiCode + : value.ansiCode; + } + + return match.group(0); + }); + } catch (final Exception e) { + return component.content(); + } + } + } + + private static void mapKeybind (final KeybindComponent component, final Consumer consumer) { + consumer.accept(Component.translatable(KEYBINDINGS.getOrDefault(component.keybind(), component.keybind()))); + } + + private static void mapTranslatable (final TranslatableComponent component, final Consumer consumer) { + final String format = getOrReturnFallback(component); + + final Matcher matcher = ARG_PATTERN.matcher(format); + + final List result = new ArrayList<>(); + + // RIPPED straight from minecraft source code !!! :D + try { + int i = 0; + + int lastIndex = 0; + + while (matcher.find(lastIndex)) { + final int start = matcher.start(); + final int end = matcher.end(); + + if (start > lastIndex) { + final String formatSegment = format.substring(lastIndex, start); + + if (formatSegment.indexOf('%') != -1) { + throw new IllegalArgumentException(); + } + + result.add(Component.text(formatSegment)); + } + + final String full = format.substring(start, end); + + if (matcher.group().equals("%") && full.equals("%%")) { + result.add(Component.text('%')); + } else if (matcher.group(2).equals("s")) { + final String idxStr = matcher.group(1); + + final int idx = idxStr == null ? i++ : (Integer.parseInt(idxStr) - 1); + + if (idx < 0 || idx > component.arguments().size()) + throw new IllegalArgumentException(); + + final int currentTotalDepth = TOTAL_DEPTH.get(); + + if (currentTotalDepth > MAX_DEPTH) return; + + TOTAL_DEPTH.set(currentTotalDepth + 1); + + result.add( + component.arguments() + .get(idx) + .asComponent() + ); + } else { + throw new IllegalArgumentException(); + } + + lastIndex = end; + } + + if (lastIndex < format.length()) { + final String remaining = format.substring(lastIndex); + + if (remaining.indexOf('%') != -1) { + throw new IllegalArgumentException(); + } + + result.add(Component.text(remaining)); + } + } catch (final Exception e) { + result.clear(); + result.add(Component.text(format)); } - public String stringifyPartially (final KeybindComponent message, final String color, final String style) { - final String keybind = message.keybind(); + consumer.accept(Component.join(JoinConfiguration.noSeparators(), result)); + } - // this is indeed the correct way to process keybindings - // inside the minecraft code, silly past me was wrong :D - final Component component = Component.translatable(KEYBINDINGS.getOrDefault(keybind, keybind)); + public enum ANSIStyle { + GREEN("a", "\u001b[38;2;85;255;85m", "\u001b[32m"), + AQUA("b", "\u001b[38;2;85;255;255m", "\u001b[36m"), + RED("c", "\u001b[38;2;255;85;85m", "\u001b[31m"), + LIGHT_PURPLE("d", "\u001b[38;2;255;85;255m", "\u001b[35m"), + YELLOW("e", "\u001b[38;2;255;255;85m", "\u001b[33m"), + WHITE("f", "\u001b[38;2;255;255;255m", "\u001b[37m"), + BLACK("0", "\u001b[38;2;0;0;0m", "\u001b[30m"), + DARK_RED("1", "\u001b[38;2;0;0;170m", "\u001b[34m"), + DARK_GREEN("2", "\u001b[38;2;0;170;0m", "\u001b[32m"), + GOLD("3", "\u001b[38;2;0;170;170m", "\u001b[36m"), + DARK_BLUE("4", "\u001b[38;2;170;0;0m", "\u001b[31m"), + DARK_PURPLE("5", "\u001b[38;2;170;0;170m", "\u001b[35m"), + DARK_AQUA("6", "\u001b[38;2;255;170;0m", "\u001b[33m"), + GRAY("7", "\u001b[38;2;170;170;170m", "\u001b[37m"), + DARK_GRAY("8", "\u001b[38;2;85;85;85m", "\u001b[30m"), + BLUE("9", "\u001b[38;2;85;85;255m", "\u001b[34m"), + BOLD("l", "\u001b[1m", "\u001b[1m"), + ITALIC("o", "\u001b[3m", "\u001b[3m"), + UNDERLINED("n", "\u001b[4m", "\u001b[4m"), + STRIKETHROUGH("m", "\u001b[9m", "\u001b[9m"), + OBFUSCATED("k", "\u001b[6m", "\u001b[6m"), + RESET("r", "\u001b[0m", "\u001b[0m"); - return stringifyPartially(component, color, style); - } + private final String legacyCode; + private final String ansiCode; + private final String discordAnsiCode; - public enum ParseType { - PLAIN, - SECTION_SIGNS, - ANSI, - DISCORD_ANSI + ANSIStyle ( + final String legacyCode, + final String ansiCode, + final String discordAnsiCode + ) { + this.legacyCode = legacyCode; + this.ansiCode = ansiCode; + this.discordAnsiCode = discordAnsiCode; } } }