|
|
|
|
@@ -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<Integer> TOTAL_DEPTH = ThreadLocal.withInitial(() -> 0);
|
|
|
|
|
|
|
|
|
|
private static final int MAX_DEPTH = 512; // same as adventure
|
|
|
|
|
|
|
|
|
|
private static Map<String, String> loadJsonStringMap (final String name) {
|
|
|
|
|
final Map<String, String> 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 -> "<Unhandled component type: " + component.getClass().getSimpleName() + ">")
|
|
|
|
|
.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<String, String> 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<String, String> 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<NamedTextColor, String> 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<String, String> 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<TextDecoration, TextDecoration.State> 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<String, String> 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<Component, String> serializer, final Component message) {
|
|
|
|
|
try {
|
|
|
|
|
return serializer.serialize(message);
|
|
|
|
|
} catch (final Exception e) {
|
|
|
|
|
return guardedStringify(
|
|
|
|
|
serializer,
|
|
|
|
|
Component.translatable(
|
|
|
|
|
"<Failed to parse component: %s>",
|
|
|
|
|
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<Component> consumer) {
|
|
|
|
|
consumer.accept(Component.translatable(KEYBINDINGS.getOrDefault(component.keybind(), component.keybind())));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void mapTranslatable (final TranslatableComponent component, final Consumer<Component> consumer) {
|
|
|
|
|
final String format = getOrReturnFallback(component);
|
|
|
|
|
|
|
|
|
|
final Matcher matcher = ARG_PATTERN.matcher(format);
|
|
|
|
|
|
|
|
|
|
final List<Component> 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|