mirror of
https://github.com/AutismDevelopment/Autism-Client.git
synced 2026-10-28 17:29:46 +00:00
Refactor: extract MacroChatText from MacroExecutor (upstream #4)
Move 31 pure chat-text utilities (sanitize/normalize/fuzzy-match, levenshtein, component literalize/serialize) verbatim into MacroChatText. MacroExecutor keeps one-line public delegates so the public API is unchanged. All moved bodies verified byte-identical against HEAD; only 2 private->public visibility widenings.
This commit is contained in:
parent
cb6910b424
commit
db8fbb763e
2 changed files with 543 additions and 497 deletions
521
src/main/java/autismclient/util/macro/MacroChatText.java
Normal file
521
src/main/java/autismclient/util/macro/MacroChatText.java
Normal file
|
|
@ -0,0 +1,521 @@
|
||||||
|
package autismclient.util.macro;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import com.mojang.serialization.JsonOps;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.network.chat.ComponentSerialization;
|
||||||
|
import net.minecraft.network.chat.MutableComponent;
|
||||||
|
import net.minecraft.network.chat.Style;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.RecordComponent;
|
||||||
|
import java.text.Normalizer;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
final class MacroChatText {
|
||||||
|
private MacroChatText() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Gson GSON = new Gson();
|
||||||
|
public static String sanitizeChatText(String text) {
|
||||||
|
if (text == null) return "";
|
||||||
|
String stripped = stripLegacyFormatting(text).replace('\n', ' ').replace('\r', ' ');
|
||||||
|
StringBuilder out = new StringBuilder(stripped.length());
|
||||||
|
for (int offset = 0; offset < stripped.length();) {
|
||||||
|
int cp = stripped.codePointAt(offset);
|
||||||
|
offset += Character.charCount(cp);
|
||||||
|
int type = Character.getType(cp);
|
||||||
|
if (type == Character.FORMAT
|
||||||
|
|| type == Character.CONTROL
|
||||||
|
|| cp == 0x200B
|
||||||
|
|| cp == 0x200C
|
||||||
|
|| cp == 0x200D
|
||||||
|
|| cp == 0x2060
|
||||||
|
|| cp == 0xFE0E
|
||||||
|
|| cp == 0xFE0F) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.appendCodePoint(cp);
|
||||||
|
}
|
||||||
|
return out.toString().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String normalizeManualText(String text) {
|
||||||
|
if (text == null || text.isBlank()) return "";
|
||||||
|
String stripped = stripLegacyFormatting(text).trim();
|
||||||
|
String flattened = flattenManualTextComponentJson(stripped);
|
||||||
|
return sanitizeChatText(flattened != null ? flattened : stripped);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String flattenManualTextComponentJson(String text) {
|
||||||
|
if (text == null || text.isBlank()) return null;
|
||||||
|
String trimmed = text.trim();
|
||||||
|
if (!(trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith("\""))) return null;
|
||||||
|
try {
|
||||||
|
com.google.gson.JsonElement element = JsonParser.parseString(trimmed);
|
||||||
|
if (element == null || element.isJsonNull()) return "";
|
||||||
|
if (element.isJsonArray()) {
|
||||||
|
StringBuilder out = new StringBuilder();
|
||||||
|
for (com.google.gson.JsonElement child : element.getAsJsonArray()) {
|
||||||
|
Component component = parseTextComponentElement(child);
|
||||||
|
if (component != null) out.append(component.getString());
|
||||||
|
}
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
Component component = parseTextComponentElement(element);
|
||||||
|
return component == null ? null : component.getString();
|
||||||
|
} catch (RuntimeException ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Component parseTextComponentElement(com.google.gson.JsonElement element) {
|
||||||
|
try {
|
||||||
|
Component component = ComponentSerialization.CODEC.parse(JsonOps.INSTANCE, element).getOrThrow();
|
||||||
|
return literalizeTextComponent(component);
|
||||||
|
} catch (RuntimeException ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String stripLegacyFormatting(String text) {
|
||||||
|
if (text == null || text.isEmpty()) return "";
|
||||||
|
StringBuilder out = new StringBuilder(text.length());
|
||||||
|
for (int i = 0; i < text.length(); i++) {
|
||||||
|
char ch = text.charAt(i);
|
||||||
|
if ((ch == '\u00A7' || ch == '&') && i + 1 < text.length()) {
|
||||||
|
char next = Character.toLowerCase(text.charAt(i + 1));
|
||||||
|
if ((next >= '0' && next <= '9')
|
||||||
|
|| (next >= 'a' && next <= 'f')
|
||||||
|
|| (next >= 'k' && next <= 'o')
|
||||||
|
|| next == 'r'
|
||||||
|
|| next == 'x') {
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.append(ch);
|
||||||
|
}
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isLikelyPlayerSender(String sender) {
|
||||||
|
String cleanSender = sanitizeChatText(sender);
|
||||||
|
if (cleanSender.isBlank()) return false;
|
||||||
|
try {
|
||||||
|
Minecraft mc = Minecraft.getInstance();
|
||||||
|
if (mc.getConnection() != null) {
|
||||||
|
for (var entry : mc.getConnection().getListedOnlinePlayers()) {
|
||||||
|
String playerName = entry != null && entry.getProfile() != null ? extractTextValue(entry.getProfile()) : null;
|
||||||
|
if (playerName != null && cleanSender.equalsIgnoreCase(playerName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
try {
|
||||||
|
Minecraft mc = Minecraft.getInstance();
|
||||||
|
if (mc.level != null) {
|
||||||
|
for (var player : mc.level.players()) {
|
||||||
|
String playerName = player != null && player.getGameProfile() != null ? extractTextValue(player.getGameProfile()) : null;
|
||||||
|
if (playerName != null && cleanSender.equalsIgnoreCase(playerName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean fuzzyChatMatch(String pattern, String candidate, int thresholdPercent) {
|
||||||
|
String normalizedPattern = normalizeChatText(pattern);
|
||||||
|
String normalizedCandidate = normalizeChatText(candidate);
|
||||||
|
if (normalizedPattern.isBlank()) return true;
|
||||||
|
if (normalizedCandidate.isBlank()) return false;
|
||||||
|
if (normalizedCandidate.contains(normalizedPattern)) return true;
|
||||||
|
|
||||||
|
List<String> patternTokens = tokenizeChat(normalizedPattern);
|
||||||
|
List<String> candidateTokens = tokenizeChat(normalizedCandidate);
|
||||||
|
if (patternTokens.isEmpty()) return true;
|
||||||
|
|
||||||
|
if (thresholdPercent >= 100) {
|
||||||
|
return allPatternTokensPresent(patternTokens, normalizedCandidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
double tokenScore = computeTokenCoverage(patternTokens, candidateTokens);
|
||||||
|
double charScore = similarityRatio(normalizedPattern.replace(" ", ""), normalizedCandidate.replace(" ", ""));
|
||||||
|
double score = Math.max(tokenScore, tokenScore * 0.7 + charScore * 0.3);
|
||||||
|
return score + 1.0e-9 >= (Math.max(40, Math.min(100, thresholdPercent)) / 100.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeChatForCompare(String text) {
|
||||||
|
if (text == null) return "";
|
||||||
|
String normalized = Normalizer.normalize(text, Normalizer.Form.NFKC).toLowerCase(Locale.ROOT);
|
||||||
|
StringBuilder out = new StringBuilder(normalized.length());
|
||||||
|
for (int i = 0; i < normalized.length(); i++) {
|
||||||
|
char ch = normalized.charAt(i);
|
||||||
|
if (Character.isLetterOrDigit(ch)) out.append(ch);
|
||||||
|
else if (Character.isWhitespace(ch)) out.append(' ');
|
||||||
|
else if (ch == '\'' || ch == '"' || ch == '`' || ch == '-' || ch == '_' || ch == '\u2019') {
|
||||||
|
|
||||||
|
} else {
|
||||||
|
out.append(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out.toString().trim().replaceAll("\\s+", " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String normalizeChatText(String text) {
|
||||||
|
if (text == null) return "";
|
||||||
|
String normalized = Normalizer.normalize(text, Normalizer.Form.NFKD).toLowerCase(Locale.ROOT);
|
||||||
|
StringBuilder out = new StringBuilder(normalized.length());
|
||||||
|
boolean lastWasSpace = true;
|
||||||
|
for (int offset = 0; offset < normalized.length();) {
|
||||||
|
int cp = normalized.codePointAt(offset);
|
||||||
|
offset += Character.charCount(cp);
|
||||||
|
|
||||||
|
cp = foldStyledLatinCodePoint(cp);
|
||||||
|
int type = Character.getType(cp);
|
||||||
|
if (type == Character.NON_SPACING_MARK
|
||||||
|
|| type == Character.COMBINING_SPACING_MARK
|
||||||
|
|| type == Character.ENCLOSING_MARK
|
||||||
|
|| type == Character.FORMAT
|
||||||
|
|| type == Character.CONTROL
|
||||||
|
|| type == Character.PRIVATE_USE
|
||||||
|
|| type == Character.SURROGATE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Character.isLetterOrDigit(cp)) {
|
||||||
|
out.appendCodePoint(cp);
|
||||||
|
lastWasSpace = false;
|
||||||
|
} else if (Character.isWhitespace(cp)) {
|
||||||
|
if (!lastWasSpace) out.append(' ');
|
||||||
|
lastWasSpace = true;
|
||||||
|
} else if (isJoinerPunctuation(cp)) {
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (!lastWasSpace) out.append(' ');
|
||||||
|
lastWasSpace = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int len = out.length();
|
||||||
|
if (len > 0 && out.charAt(len - 1) == ' ') out.setLength(len - 1);
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isJoinerPunctuation(int cp) {
|
||||||
|
return switch (cp) {
|
||||||
|
case '\'', '"', '`', '-', '_', '\u2019', '\u2018', '\u201B', '\u2032', '\u02BC', '\uFF07',
|
||||||
|
'\u2010', '\u2011', '\u2012', '\u2013', '\u2014', '\u2015', '\u2212' -> true;
|
||||||
|
default -> false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int foldStyledLatinCodePoint(int cp) {
|
||||||
|
return switch (cp) {
|
||||||
|
case '\u1D00' -> 'a';
|
||||||
|
case '\u0299' -> 'b';
|
||||||
|
case '\u1D04' -> 'c';
|
||||||
|
case '\u1D05' -> 'd';
|
||||||
|
case '\u1D07' -> 'e';
|
||||||
|
case '\uA730' -> 'f';
|
||||||
|
case '\u0262' -> 'g';
|
||||||
|
case '\u029C' -> 'h';
|
||||||
|
case '\u026A' -> 'i';
|
||||||
|
case '\u1D0A' -> 'j';
|
||||||
|
case '\u1D0B' -> 'k';
|
||||||
|
case '\u029F' -> 'l';
|
||||||
|
case '\u1D0D' -> 'm';
|
||||||
|
case '\u0274' -> 'n';
|
||||||
|
case '\u1D0F' -> 'o';
|
||||||
|
case '\u1D18' -> 'p';
|
||||||
|
case '\u01EB' -> 'q';
|
||||||
|
case '\u0280' -> 'r';
|
||||||
|
case '\uA731' -> 's';
|
||||||
|
case '\u1D1B' -> 't';
|
||||||
|
case '\u1D1C' -> 'u';
|
||||||
|
case '\u1D20' -> 'v';
|
||||||
|
case '\u1D21' -> 'w';
|
||||||
|
case '\u02E3' -> 'x';
|
||||||
|
case '\u028F' -> 'y';
|
||||||
|
case '\u1D22' -> 'z';
|
||||||
|
case '\u00DF' -> 's';
|
||||||
|
case '\u00E6' -> 'a';
|
||||||
|
case '\u0153' -> 'o';
|
||||||
|
default -> cp;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> tokenizeChat(String normalized) {
|
||||||
|
if (normalized == null || normalized.isBlank()) return List.of();
|
||||||
|
return List.of(normalized.split(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean allPatternTokensPresent(List<String> patternTokens, String normalizedCandidate) {
|
||||||
|
for (String token : patternTokens) {
|
||||||
|
if (!normalizedCandidate.contains(token)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double computeTokenCoverage(List<String> patternTokens, List<String> candidateTokens) {
|
||||||
|
if (patternTokens.isEmpty()) return 1.0;
|
||||||
|
if (candidateTokens.isEmpty()) return 0.0;
|
||||||
|
double total = 0.0;
|
||||||
|
for (String patternToken : patternTokens) {
|
||||||
|
double best = 0.0;
|
||||||
|
for (String candidateToken : candidateTokens) {
|
||||||
|
best = Math.max(best, tokenSimilarity(patternToken, candidateToken));
|
||||||
|
if (best >= 1.0) break;
|
||||||
|
}
|
||||||
|
total += best;
|
||||||
|
}
|
||||||
|
return total / patternTokens.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double tokenSimilarity(String left, String right) {
|
||||||
|
if (left.equals(right)) return 1.0;
|
||||||
|
if (left.isBlank() || right.isBlank()) return 0.0;
|
||||||
|
if (left.contains(right) || right.contains(left)) {
|
||||||
|
return (double) Math.min(left.length(), right.length()) / Math.max(left.length(), right.length());
|
||||||
|
}
|
||||||
|
return similarityRatio(left, right);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double similarityRatio(String left, String right) {
|
||||||
|
if (left.equals(right)) return 1.0;
|
||||||
|
int maxLen = Math.max(left.length(), right.length());
|
||||||
|
if (maxLen == 0) return 1.0;
|
||||||
|
int distance = levenshteinDistance(left, right);
|
||||||
|
return Math.max(0.0, 1.0 - (distance / (double) maxLen));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int levenshteinDistance(String left, String right) {
|
||||||
|
int[] previous = new int[right.length() + 1];
|
||||||
|
int[] current = new int[right.length() + 1];
|
||||||
|
for (int j = 0; j <= right.length(); j++) previous[j] = j;
|
||||||
|
for (int i = 1; i <= left.length(); i++) {
|
||||||
|
current[0] = i;
|
||||||
|
for (int j = 1; j <= right.length(); j++) {
|
||||||
|
int cost = left.charAt(i - 1) == right.charAt(j - 1) ? 0 : 1;
|
||||||
|
current[j] = Math.min(Math.min(current[j - 1] + 1, previous[j] + 1), previous[j - 1] + cost);
|
||||||
|
}
|
||||||
|
int[] temp = previous;
|
||||||
|
previous = current;
|
||||||
|
current = temp;
|
||||||
|
}
|
||||||
|
return previous[right.length()];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractChatName(Object value) {
|
||||||
|
Object direct = firstNonNull(
|
||||||
|
invokeFirstNoArg(value, "name", "getName", "targetName", "getTargetName"),
|
||||||
|
getRecordComponentValue(value, 0),
|
||||||
|
findRecordComponentByMethodNames(value, 2, "name", "targetName", "type")
|
||||||
|
);
|
||||||
|
return sanitizeChatText(extractTextValue(direct));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String resolvePlayerName(UUID uuid) {
|
||||||
|
if (uuid == null) return "";
|
||||||
|
try {
|
||||||
|
if (Minecraft.getInstance().getConnection() != null) {
|
||||||
|
var entry = Minecraft.getInstance().getConnection().getPlayerInfo(uuid);
|
||||||
|
if (entry != null && entry.getProfile() != null) {
|
||||||
|
String profileName = extractTextValue(entry.getProfile());
|
||||||
|
if (profileName != null && !profileName.isBlank()) return profileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
try {
|
||||||
|
if (Minecraft.getInstance().level != null) {
|
||||||
|
for (var player : Minecraft.getInstance().level.players()) {
|
||||||
|
if (uuid.equals(player.getUUID()) && player.getGameProfile() != null) {
|
||||||
|
String profileName = extractTextValue(player.getGameProfile());
|
||||||
|
if (profileName != null && !profileName.isBlank()) return profileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object firstNonNull(Object... values) {
|
||||||
|
if (values == null) return null;
|
||||||
|
for (Object value : values) {
|
||||||
|
if (value != null) return value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object invokeFirstNoArg(Object target, String... names) {
|
||||||
|
if (target == null || names == null) return null;
|
||||||
|
Class<?> type = target.getClass();
|
||||||
|
for (String name : names) {
|
||||||
|
try {
|
||||||
|
Method method = type.getMethod(name);
|
||||||
|
method.setAccessible(true);
|
||||||
|
return method.invoke(target);
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object getRecordComponentValue(Object target, int index) {
|
||||||
|
if (target == null) return null;
|
||||||
|
try {
|
||||||
|
RecordComponent[] components = target.getClass().getRecordComponents();
|
||||||
|
if (components == null || index < 0 || index >= components.length) return null;
|
||||||
|
Method accessor = components[index].getAccessor();
|
||||||
|
accessor.setAccessible(true);
|
||||||
|
return accessor.invoke(target);
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object findRecordComponentByValueType(Object target, Class<?> type) {
|
||||||
|
if (target == null || type == null) return null;
|
||||||
|
try {
|
||||||
|
RecordComponent[] components = target.getClass().getRecordComponents();
|
||||||
|
if (components == null) return null;
|
||||||
|
for (RecordComponent component : components) {
|
||||||
|
if (type.isAssignableFrom(component.getType())) {
|
||||||
|
Method accessor = component.getAccessor();
|
||||||
|
accessor.setAccessible(true);
|
||||||
|
return accessor.invoke(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object findRecordComponentByMethodNames(Object target, int fallbackIndex, String... names) {
|
||||||
|
Object direct = invokeFirstNoArg(target, names);
|
||||||
|
return direct != null ? direct : getRecordComponentValue(target, fallbackIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object findRecordTextComponent(Object target, Object... excludedValues) {
|
||||||
|
if (target == null) return null;
|
||||||
|
try {
|
||||||
|
RecordComponent[] components = target.getClass().getRecordComponents();
|
||||||
|
if (components == null) return null;
|
||||||
|
outer:
|
||||||
|
for (RecordComponent component : components) {
|
||||||
|
Method accessor = component.getAccessor();
|
||||||
|
accessor.setAccessible(true);
|
||||||
|
Object value = accessor.invoke(target);
|
||||||
|
if (value == null) continue;
|
||||||
|
for (Object excluded : excludedValues) {
|
||||||
|
if (excluded == value) continue outer;
|
||||||
|
}
|
||||||
|
if (value instanceof Component || value instanceof CharSequence || value instanceof Optional<?>) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Component extractTextComponent(Object value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (value instanceof Optional<?> optional) {
|
||||||
|
return optional.isPresent() ? extractTextComponent(optional.get()) : null;
|
||||||
|
}
|
||||||
|
if (value instanceof Component text) return literalizeTextComponent(text);
|
||||||
|
if (value instanceof CharSequence chars) return Component.literal(chars.toString());
|
||||||
|
if (value instanceof UUID uuid) return Component.literal(resolvePlayerName(uuid));
|
||||||
|
try {
|
||||||
|
Method getComponent = value.getClass().getMethod("getText");
|
||||||
|
getComponent.setAccessible(true);
|
||||||
|
Object result = getComponent.invoke(value);
|
||||||
|
Component text = extractTextComponent(result);
|
||||||
|
if (text != null) return text;
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
try {
|
||||||
|
Method getString = value.getClass().getMethod("getString");
|
||||||
|
getString.setAccessible(true);
|
||||||
|
Object result = getString.invoke(value);
|
||||||
|
if (result != null) return Component.literal(String.valueOf(result));
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Component literalizeTextComponent(Component text) {
|
||||||
|
if (text == null) return null;
|
||||||
|
MutableComponent literalized = Component.empty();
|
||||||
|
try {
|
||||||
|
text.visit((style, string) -> {
|
||||||
|
if (string != null && !string.isEmpty()) {
|
||||||
|
literalized.append(Component.literal(string).setStyle(copyVisualStyle(style)));
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}, Style.EMPTY);
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
if (!literalized.getString().isEmpty()) {
|
||||||
|
return literalized;
|
||||||
|
}
|
||||||
|
return Component.literal(text.getString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Style copyVisualStyle(Style style) {
|
||||||
|
if (style == null || style.isEmpty()) return Style.EMPTY;
|
||||||
|
Style safe = Style.EMPTY;
|
||||||
|
if (style.getFont() != null) safe = safe.withFont(style.getFont());
|
||||||
|
if (style.getColor() != null) safe = safe.withColor(style.getColor());
|
||||||
|
if (style.isBold()) safe = safe.withBold(true);
|
||||||
|
if (style.isItalic()) safe = safe.withItalic(true);
|
||||||
|
if (style.isUnderlined()) safe = safe.withUnderlined(true);
|
||||||
|
if (style.isStrikethrough()) safe = safe.withStrikethrough(true);
|
||||||
|
if (style.isObfuscated()) safe = safe.withObfuscated(true);
|
||||||
|
return safe;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String serializeTextComponent(Component text) {
|
||||||
|
if (text == null) return "";
|
||||||
|
try {
|
||||||
|
Component safeComponent = literalizeTextComponent(text);
|
||||||
|
return safeComponent == null ? "" : GSON.toJson(ComponentSerialization.CODEC.encodeStart(JsonOps.INSTANCE, safeComponent).getOrThrow());
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Component deserializeTextComponent(String json) {
|
||||||
|
if (json == null || json.isBlank()) return null;
|
||||||
|
try {
|
||||||
|
return literalizeTextComponent(ComponentSerialization.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseString(json)).getOrThrow().copy());
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractTextValue(Object value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (value instanceof Optional<?> optional) {
|
||||||
|
return optional.isPresent() ? extractTextValue(optional.get()) : null;
|
||||||
|
}
|
||||||
|
if (value instanceof Component text) return text.getString();
|
||||||
|
if (value instanceof CharSequence chars) return chars.toString();
|
||||||
|
if (value instanceof UUID uuid) return resolvePlayerName(uuid);
|
||||||
|
try {
|
||||||
|
Method getString = value.getClass().getMethod("getString");
|
||||||
|
getString.setAccessible(true);
|
||||||
|
Object result = getString.invoke(value);
|
||||||
|
if (result != null) return String.valueOf(result);
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
try {
|
||||||
|
Method getName = value.getClass().getMethod("getName");
|
||||||
|
getName.setAccessible(true);
|
||||||
|
Object result = getName.invoke(value);
|
||||||
|
if (result instanceof CharSequence chars) return chars.toString();
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package autismclient.util.macro;
|
package autismclient.util.macro;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import com.google.gson.JsonParser;
|
import com.google.gson.JsonParser;
|
||||||
import com.mojang.serialization.JsonOps;
|
import com.mojang.serialization.JsonOps;
|
||||||
import autismclient.util.macro.MacroActionResources.SharedClientResource;
|
import autismclient.util.macro.MacroActionResources.SharedClientResource;
|
||||||
|
|
@ -80,7 +79,6 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
public class MacroExecutor {
|
public class MacroExecutor {
|
||||||
private static final Gson GSON = new Gson();
|
|
||||||
|
|
||||||
private static final long MACRO_WAIT_POLL_NANOS = 500_000L;
|
private static final long MACRO_WAIT_POLL_NANOS = 500_000L;
|
||||||
|
|
||||||
|
|
@ -2144,7 +2142,7 @@ public class MacroExecutor {
|
||||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||||
activeChatFuture = future;
|
activeChatFuture = future;
|
||||||
isWaitingForChat.set(true);
|
isWaitingForChat.set(true);
|
||||||
waitingChatPattern.set(normalizeManualText(wca.pattern));
|
waitingChatPattern.set(MacroChatText.normalizeManualText(wca.pattern));
|
||||||
waitingChatPatternJson.set(wca.patternJson == null ? "" : wca.patternJson);
|
waitingChatPatternJson.set(wca.patternJson == null ? "" : wca.patternJson);
|
||||||
waitingChatIsRegex.set(wca.useRegex);
|
waitingChatIsRegex.set(wca.useRegex);
|
||||||
waitingChatFuzzyPercent.set(WaitForChatAction.clampFuzzyPercent(wca.fuzzyPercent));
|
waitingChatFuzzyPercent.set(WaitForChatAction.clampFuzzyPercent(wca.fuzzyPercent));
|
||||||
|
|
@ -8093,10 +8091,10 @@ public class MacroExecutor {
|
||||||
private static boolean matchesWaitingChat(ChatCapture capture) {
|
private static boolean matchesWaitingChat(ChatCapture capture) {
|
||||||
String patternJson = waitingChatPatternJson.get();
|
String patternJson = waitingChatPatternJson.get();
|
||||||
if (!waitingChatIsRegex.get() && patternJson != null && !patternJson.isBlank() && capture.displayComponent() != null) {
|
if (!waitingChatIsRegex.get() && patternJson != null && !patternJson.isBlank() && capture.displayComponent() != null) {
|
||||||
String captureJson = serializeTextComponent(capture.displayComponent());
|
String captureJson = MacroChatText.serializeTextComponent(capture.displayComponent());
|
||||||
if (patternJson.equals(captureJson)) return true;
|
if (patternJson.equals(captureJson)) return true;
|
||||||
}
|
}
|
||||||
String pattern = normalizeManualText(waitingChatPattern.get());
|
String pattern = MacroChatText.normalizeManualText(waitingChatPattern.get());
|
||||||
if (pattern == null || pattern.isBlank()) return true;
|
if (pattern == null || pattern.isBlank()) return true;
|
||||||
if (waitingChatIsRegex.get()) {
|
if (waitingChatIsRegex.get()) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -8107,8 +8105,8 @@ public class MacroExecutor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
int threshold = waitingChatFuzzyPercent.get();
|
int threshold = waitingChatFuzzyPercent.get();
|
||||||
return fuzzyChatMatch(pattern, capture.message(), threshold)
|
return MacroChatText.fuzzyChatMatch(pattern, capture.message(), threshold)
|
||||||
|| fuzzyChatMatch(pattern, capture.displayText(), threshold);
|
|| MacroChatText.fuzzyChatMatch(pattern, capture.displayText(), threshold);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean matchesChatAction(WaitForChatAction action, ChatCapture capture) {
|
private static boolean matchesChatAction(WaitForChatAction action, ChatCapture capture) {
|
||||||
|
|
@ -8124,10 +8122,10 @@ public class MacroExecutor {
|
||||||
if (action == null || capture == null) return java.util.Optional.empty();
|
if (action == null || capture == null) return java.util.Optional.empty();
|
||||||
MacroCapturePattern.Mode mode = action.effectiveMatchMode();
|
MacroCapturePattern.Mode mode = action.effectiveMatchMode();
|
||||||
if (mode == MacroCapturePattern.Mode.MATCH && action.patternJson != null && !action.patternJson.isBlank() && capture.displayComponent() != null) {
|
if (mode == MacroCapturePattern.Mode.MATCH && action.patternJson != null && !action.patternJson.isBlank() && capture.displayComponent() != null) {
|
||||||
String captureJson = serializeTextComponent(capture.displayComponent());
|
String captureJson = MacroChatText.serializeTextComponent(capture.displayComponent());
|
||||||
if (action.patternJson.equals(captureJson)) return java.util.Optional.of(chatValues(action, capture, java.util.Map.of()));
|
if (action.patternJson.equals(captureJson)) return java.util.Optional.of(chatValues(action, capture, java.util.Map.of()));
|
||||||
}
|
}
|
||||||
String pattern = normalizeManualText(action.pattern);
|
String pattern = MacroChatText.normalizeManualText(action.pattern);
|
||||||
if (mode == MacroCapturePattern.Mode.MATCH) {
|
if (mode == MacroCapturePattern.Mode.MATCH) {
|
||||||
MacroTemplate.Resolution patternResolution = MacroTemplate.resolve(pattern, context, Minecraft.getInstance());
|
MacroTemplate.Resolution patternResolution = MacroTemplate.resolve(pattern, context, Minecraft.getInstance());
|
||||||
if (!patternResolution.success()) return java.util.Optional.empty();
|
if (!patternResolution.success()) return java.util.Optional.empty();
|
||||||
|
|
@ -8140,8 +8138,8 @@ public class MacroExecutor {
|
||||||
return result.map(value -> chatValues(action, capture, value.values()));
|
return result.map(value -> chatValues(action, capture, value.values()));
|
||||||
}
|
}
|
||||||
int threshold = WaitForChatAction.clampFuzzyPercent(action.fuzzyPercent);
|
int threshold = WaitForChatAction.clampFuzzyPercent(action.fuzzyPercent);
|
||||||
boolean matched = fuzzyChatMatch(pattern, capture.message(), threshold)
|
boolean matched = MacroChatText.fuzzyChatMatch(pattern, capture.message(), threshold)
|
||||||
|| fuzzyChatMatch(pattern, capture.displayText(), threshold);
|
|| MacroChatText.fuzzyChatMatch(pattern, capture.displayText(), threshold);
|
||||||
return matched ? java.util.Optional.of(chatValues(action, capture, java.util.Map.of())) : java.util.Optional.empty();
|
return matched ? java.util.Optional.of(chatValues(action, capture, java.util.Map.of())) : java.util.Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -8190,7 +8188,7 @@ public class MacroExecutor {
|
||||||
Component decorated = playerChat.chatType().decorate(content);
|
Component decorated = playerChat.chatType().decorate(content);
|
||||||
String sender = playerChat.chatType().name().getString();
|
String sender = playerChat.chatType().name().getString();
|
||||||
if (sender == null || sender.isBlank()) {
|
if (sender == null || sender.isBlank()) {
|
||||||
String byUuid = resolvePlayerName(playerChat.sender());
|
String byUuid = MacroChatText.resolvePlayerName(playerChat.sender());
|
||||||
if (byUuid != null && !byUuid.isBlank()) sender = byUuid;
|
if (byUuid != null && !byUuid.isBlank()) sender = byUuid;
|
||||||
}
|
}
|
||||||
return buildChatCapture(sender, message, null, decorated, ChatSource.PLAYER);
|
return buildChatCapture(sender, message, null, decorated, ChatSource.PLAYER);
|
||||||
|
|
@ -8200,11 +8198,11 @@ public class MacroExecutor {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ChatCapture buildChatCapture(String sender, String message, String displayText, Component displayComponent, ChatSource source) {
|
private static ChatCapture buildChatCapture(String sender, String message, String displayText, Component displayComponent, ChatSource source) {
|
||||||
String cleanMessage = sanitizeChatText(message);
|
String cleanMessage = MacroChatText.sanitizeChatText(message);
|
||||||
if (cleanMessage.isBlank()) return null;
|
if (cleanMessage.isBlank()) return null;
|
||||||
String cleanSender = sanitizeChatText(sender);
|
String cleanSender = MacroChatText.sanitizeChatText(sender);
|
||||||
String cleanDisplay = sanitizeChatText(displayText);
|
String cleanDisplay = MacroChatText.sanitizeChatText(displayText);
|
||||||
Component rendered = literalizeTextComponent(displayComponent);
|
Component rendered = MacroChatText.literalizeTextComponent(displayComponent);
|
||||||
String display = cleanDisplay;
|
String display = cleanDisplay;
|
||||||
if (rendered == null || rendered.getString().isBlank()) {
|
if (rendered == null || rendered.getString().isBlank()) {
|
||||||
rendered = cleanSender.isBlank()
|
rendered = cleanSender.isBlank()
|
||||||
|
|
@ -8212,507 +8210,34 @@ public class MacroExecutor {
|
||||||
: Component.literal("<" + cleanSender + "> ").append(Component.literal(cleanMessage));
|
: Component.literal("<" + cleanSender + "> ").append(Component.literal(cleanMessage));
|
||||||
}
|
}
|
||||||
if (display == null || display.isBlank()) {
|
if (display == null || display.isBlank()) {
|
||||||
display = sanitizeChatText(rendered.getString());
|
display = MacroChatText.sanitizeChatText(rendered.getString());
|
||||||
}
|
}
|
||||||
ChatSource resolvedSource = source != null ? source : ChatSource.PLAYER;
|
ChatSource resolvedSource = source != null ? source : ChatSource.PLAYER;
|
||||||
return new ChatCapture(cleanSender, cleanMessage, sanitizeChatText(display), rendered, resolvedSource);
|
return new ChatCapture(cleanSender, cleanMessage, MacroChatText.sanitizeChatText(display), rendered, resolvedSource);
|
||||||
}
|
|
||||||
|
|
||||||
private static String sanitizeChatText(String text) {
|
|
||||||
if (text == null) return "";
|
|
||||||
String stripped = stripLegacyFormatting(text).replace('\n', ' ').replace('\r', ' ');
|
|
||||||
StringBuilder out = new StringBuilder(stripped.length());
|
|
||||||
for (int offset = 0; offset < stripped.length();) {
|
|
||||||
int cp = stripped.codePointAt(offset);
|
|
||||||
offset += Character.charCount(cp);
|
|
||||||
int type = Character.getType(cp);
|
|
||||||
if (type == Character.FORMAT
|
|
||||||
|| type == Character.CONTROL
|
|
||||||
|| cp == 0x200B
|
|
||||||
|| cp == 0x200C
|
|
||||||
|| cp == 0x200D
|
|
||||||
|| cp == 0x2060
|
|
||||||
|| cp == 0xFE0E
|
|
||||||
|| cp == 0xFE0F) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
out.appendCodePoint(cp);
|
|
||||||
}
|
|
||||||
return out.toString().trim();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String normalizeManualText(String text) {
|
public static String normalizeManualText(String text) {
|
||||||
if (text == null || text.isBlank()) return "";
|
return MacroChatText.normalizeManualText(text);
|
||||||
String stripped = stripLegacyFormatting(text).trim();
|
|
||||||
String flattened = flattenManualTextComponentJson(stripped);
|
|
||||||
return sanitizeChatText(flattened != null ? flattened : stripped);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String flattenManualTextComponentJson(String text) {
|
|
||||||
if (text == null || text.isBlank()) return null;
|
|
||||||
String trimmed = text.trim();
|
|
||||||
if (!(trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith("\""))) return null;
|
|
||||||
try {
|
|
||||||
com.google.gson.JsonElement element = JsonParser.parseString(trimmed);
|
|
||||||
if (element == null || element.isJsonNull()) return "";
|
|
||||||
if (element.isJsonArray()) {
|
|
||||||
StringBuilder out = new StringBuilder();
|
|
||||||
for (com.google.gson.JsonElement child : element.getAsJsonArray()) {
|
|
||||||
Component component = parseTextComponentElement(child);
|
|
||||||
if (component != null) out.append(component.getString());
|
|
||||||
}
|
|
||||||
return out.toString();
|
|
||||||
}
|
|
||||||
Component component = parseTextComponentElement(element);
|
|
||||||
return component == null ? null : component.getString();
|
|
||||||
} catch (RuntimeException ignored) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Component parseTextComponentElement(com.google.gson.JsonElement element) {
|
|
||||||
try {
|
|
||||||
Component component = ComponentSerialization.CODEC.parse(JsonOps.INSTANCE, element).getOrThrow();
|
|
||||||
return literalizeTextComponent(component);
|
|
||||||
} catch (RuntimeException ignored) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String stripLegacyFormatting(String text) {
|
|
||||||
if (text == null || text.isEmpty()) return "";
|
|
||||||
StringBuilder out = new StringBuilder(text.length());
|
|
||||||
for (int i = 0; i < text.length(); i++) {
|
|
||||||
char ch = text.charAt(i);
|
|
||||||
if ((ch == '\u00A7' || ch == '&') && i + 1 < text.length()) {
|
|
||||||
char next = Character.toLowerCase(text.charAt(i + 1));
|
|
||||||
if ((next >= '0' && next <= '9')
|
|
||||||
|| (next >= 'a' && next <= 'f')
|
|
||||||
|| (next >= 'k' && next <= 'o')
|
|
||||||
|| next == 'r'
|
|
||||||
|| next == 'x') {
|
|
||||||
i++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out.append(ch);
|
|
||||||
}
|
|
||||||
return out.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isLikelyPlayerSender(String sender) {
|
|
||||||
String cleanSender = sanitizeChatText(sender);
|
|
||||||
if (cleanSender.isBlank()) return false;
|
|
||||||
try {
|
|
||||||
Minecraft mc = Minecraft.getInstance();
|
|
||||||
if (mc.getConnection() != null) {
|
|
||||||
for (var entry : mc.getConnection().getListedOnlinePlayers()) {
|
|
||||||
String playerName = entry != null && entry.getProfile() != null ? extractTextValue(entry.getProfile()) : null;
|
|
||||||
if (playerName != null && cleanSender.equalsIgnoreCase(playerName)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Throwable ignored) { }
|
|
||||||
try {
|
|
||||||
Minecraft mc = Minecraft.getInstance();
|
|
||||||
if (mc.level != null) {
|
|
||||||
for (var player : mc.level.players()) {
|
|
||||||
String playerName = player != null && player.getGameProfile() != null ? extractTextValue(player.getGameProfile()) : null;
|
|
||||||
if (playerName != null && cleanSender.equalsIgnoreCase(playerName)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Throwable ignored) { }
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean fuzzyChatMatch(String pattern, String candidate, int thresholdPercent) {
|
public static boolean fuzzyChatMatch(String pattern, String candidate, int thresholdPercent) {
|
||||||
String normalizedPattern = normalizeChatText(pattern);
|
return MacroChatText.fuzzyChatMatch(pattern, candidate, thresholdPercent);
|
||||||
String normalizedCandidate = normalizeChatText(candidate);
|
|
||||||
if (normalizedPattern.isBlank()) return true;
|
|
||||||
if (normalizedCandidate.isBlank()) return false;
|
|
||||||
if (normalizedCandidate.contains(normalizedPattern)) return true;
|
|
||||||
|
|
||||||
List<String> patternTokens = tokenizeChat(normalizedPattern);
|
|
||||||
List<String> candidateTokens = tokenizeChat(normalizedCandidate);
|
|
||||||
if (patternTokens.isEmpty()) return true;
|
|
||||||
|
|
||||||
if (thresholdPercent >= 100) {
|
|
||||||
return allPatternTokensPresent(patternTokens, normalizedCandidate);
|
|
||||||
}
|
|
||||||
|
|
||||||
double tokenScore = computeTokenCoverage(patternTokens, candidateTokens);
|
|
||||||
double charScore = similarityRatio(normalizedPattern.replace(" ", ""), normalizedCandidate.replace(" ", ""));
|
|
||||||
double score = Math.max(tokenScore, tokenScore * 0.7 + charScore * 0.3);
|
|
||||||
return score + 1.0e-9 >= (Math.max(40, Math.min(100, thresholdPercent)) / 100.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String normalizeChatForCompare(String text) {
|
|
||||||
if (text == null) return "";
|
|
||||||
String normalized = Normalizer.normalize(text, Normalizer.Form.NFKC).toLowerCase(Locale.ROOT);
|
|
||||||
StringBuilder out = new StringBuilder(normalized.length());
|
|
||||||
for (int i = 0; i < normalized.length(); i++) {
|
|
||||||
char ch = normalized.charAt(i);
|
|
||||||
if (Character.isLetterOrDigit(ch)) out.append(ch);
|
|
||||||
else if (Character.isWhitespace(ch)) out.append(' ');
|
|
||||||
else if (ch == '\'' || ch == '"' || ch == '`' || ch == '-' || ch == '_' || ch == '\u2019') {
|
|
||||||
|
|
||||||
} else {
|
|
||||||
out.append(' ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out.toString().trim().replaceAll("\\s+", " ");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String normalizeChatText(String text) {
|
public static String normalizeChatText(String text) {
|
||||||
if (text == null) return "";
|
return MacroChatText.normalizeChatText(text);
|
||||||
String normalized = Normalizer.normalize(text, Normalizer.Form.NFKD).toLowerCase(Locale.ROOT);
|
|
||||||
StringBuilder out = new StringBuilder(normalized.length());
|
|
||||||
boolean lastWasSpace = true;
|
|
||||||
for (int offset = 0; offset < normalized.length();) {
|
|
||||||
int cp = normalized.codePointAt(offset);
|
|
||||||
offset += Character.charCount(cp);
|
|
||||||
|
|
||||||
cp = foldStyledLatinCodePoint(cp);
|
|
||||||
int type = Character.getType(cp);
|
|
||||||
if (type == Character.NON_SPACING_MARK
|
|
||||||
|| type == Character.COMBINING_SPACING_MARK
|
|
||||||
|| type == Character.ENCLOSING_MARK
|
|
||||||
|| type == Character.FORMAT
|
|
||||||
|| type == Character.CONTROL
|
|
||||||
|| type == Character.PRIVATE_USE
|
|
||||||
|| type == Character.SURROGATE) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Character.isLetterOrDigit(cp)) {
|
|
||||||
out.appendCodePoint(cp);
|
|
||||||
lastWasSpace = false;
|
|
||||||
} else if (Character.isWhitespace(cp)) {
|
|
||||||
if (!lastWasSpace) out.append(' ');
|
|
||||||
lastWasSpace = true;
|
|
||||||
} else if (isJoinerPunctuation(cp)) {
|
|
||||||
|
|
||||||
} else {
|
|
||||||
if (!lastWasSpace) out.append(' ');
|
|
||||||
lastWasSpace = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
int len = out.length();
|
|
||||||
if (len > 0 && out.charAt(len - 1) == ' ') out.setLength(len - 1);
|
|
||||||
return out.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isJoinerPunctuation(int cp) {
|
|
||||||
return switch (cp) {
|
|
||||||
case '\'', '"', '`', '-', '_', '\u2019', '\u2018', '\u201B', '\u2032', '\u02BC', '\uFF07',
|
|
||||||
'\u2010', '\u2011', '\u2012', '\u2013', '\u2014', '\u2015', '\u2212' -> true;
|
|
||||||
default -> false;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int foldStyledLatinCodePoint(int cp) {
|
|
||||||
return switch (cp) {
|
|
||||||
case '\u1D00' -> 'a';
|
|
||||||
case '\u0299' -> 'b';
|
|
||||||
case '\u1D04' -> 'c';
|
|
||||||
case '\u1D05' -> 'd';
|
|
||||||
case '\u1D07' -> 'e';
|
|
||||||
case '\uA730' -> 'f';
|
|
||||||
case '\u0262' -> 'g';
|
|
||||||
case '\u029C' -> 'h';
|
|
||||||
case '\u026A' -> 'i';
|
|
||||||
case '\u1D0A' -> 'j';
|
|
||||||
case '\u1D0B' -> 'k';
|
|
||||||
case '\u029F' -> 'l';
|
|
||||||
case '\u1D0D' -> 'm';
|
|
||||||
case '\u0274' -> 'n';
|
|
||||||
case '\u1D0F' -> 'o';
|
|
||||||
case '\u1D18' -> 'p';
|
|
||||||
case '\u01EB' -> 'q';
|
|
||||||
case '\u0280' -> 'r';
|
|
||||||
case '\uA731' -> 's';
|
|
||||||
case '\u1D1B' -> 't';
|
|
||||||
case '\u1D1C' -> 'u';
|
|
||||||
case '\u1D20' -> 'v';
|
|
||||||
case '\u1D21' -> 'w';
|
|
||||||
case '\u02E3' -> 'x';
|
|
||||||
case '\u028F' -> 'y';
|
|
||||||
case '\u1D22' -> 'z';
|
|
||||||
case '\u00DF' -> 's';
|
|
||||||
case '\u00E6' -> 'a';
|
|
||||||
case '\u0153' -> 'o';
|
|
||||||
default -> cp;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<String> tokenizeChat(String normalized) {
|
|
||||||
if (normalized == null || normalized.isBlank()) return List.of();
|
|
||||||
return List.of(normalized.split(" "));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean allPatternTokensPresent(List<String> patternTokens, String normalizedCandidate) {
|
|
||||||
for (String token : patternTokens) {
|
|
||||||
if (!normalizedCandidate.contains(token)) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double computeTokenCoverage(List<String> patternTokens, List<String> candidateTokens) {
|
|
||||||
if (patternTokens.isEmpty()) return 1.0;
|
|
||||||
if (candidateTokens.isEmpty()) return 0.0;
|
|
||||||
double total = 0.0;
|
|
||||||
for (String patternToken : patternTokens) {
|
|
||||||
double best = 0.0;
|
|
||||||
for (String candidateToken : candidateTokens) {
|
|
||||||
best = Math.max(best, tokenSimilarity(patternToken, candidateToken));
|
|
||||||
if (best >= 1.0) break;
|
|
||||||
}
|
|
||||||
total += best;
|
|
||||||
}
|
|
||||||
return total / patternTokens.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double tokenSimilarity(String left, String right) {
|
|
||||||
if (left.equals(right)) return 1.0;
|
|
||||||
if (left.isBlank() || right.isBlank()) return 0.0;
|
|
||||||
if (left.contains(right) || right.contains(left)) {
|
|
||||||
return (double) Math.min(left.length(), right.length()) / Math.max(left.length(), right.length());
|
|
||||||
}
|
|
||||||
return similarityRatio(left, right);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double similarityRatio(String left, String right) {
|
|
||||||
if (left.equals(right)) return 1.0;
|
|
||||||
int maxLen = Math.max(left.length(), right.length());
|
|
||||||
if (maxLen == 0) return 1.0;
|
|
||||||
int distance = levenshteinDistance(left, right);
|
|
||||||
return Math.max(0.0, 1.0 - (distance / (double) maxLen));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int levenshteinDistance(String left, String right) {
|
|
||||||
int[] previous = new int[right.length() + 1];
|
|
||||||
int[] current = new int[right.length() + 1];
|
|
||||||
for (int j = 0; j <= right.length(); j++) previous[j] = j;
|
|
||||||
for (int i = 1; i <= left.length(); i++) {
|
|
||||||
current[0] = i;
|
|
||||||
for (int j = 1; j <= right.length(); j++) {
|
|
||||||
int cost = left.charAt(i - 1) == right.charAt(j - 1) ? 0 : 1;
|
|
||||||
current[j] = Math.min(Math.min(current[j - 1] + 1, previous[j] + 1), previous[j - 1] + cost);
|
|
||||||
}
|
|
||||||
int[] temp = previous;
|
|
||||||
previous = current;
|
|
||||||
current = temp;
|
|
||||||
}
|
|
||||||
return previous[right.length()];
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String extractChatName(Object value) {
|
|
||||||
Object direct = firstNonNull(
|
|
||||||
invokeFirstNoArg(value, "name", "getName", "targetName", "getTargetName"),
|
|
||||||
getRecordComponentValue(value, 0),
|
|
||||||
findRecordComponentByMethodNames(value, 2, "name", "targetName", "type")
|
|
||||||
);
|
|
||||||
return sanitizeChatText(extractTextValue(direct));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String resolvePlayerName(UUID uuid) {
|
|
||||||
if (uuid == null) return "";
|
|
||||||
try {
|
|
||||||
if (Minecraft.getInstance().getConnection() != null) {
|
|
||||||
var entry = Minecraft.getInstance().getConnection().getPlayerInfo(uuid);
|
|
||||||
if (entry != null && entry.getProfile() != null) {
|
|
||||||
String profileName = extractTextValue(entry.getProfile());
|
|
||||||
if (profileName != null && !profileName.isBlank()) return profileName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Throwable ignored) { }
|
|
||||||
try {
|
|
||||||
if (Minecraft.getInstance().level != null) {
|
|
||||||
for (var player : Minecraft.getInstance().level.players()) {
|
|
||||||
if (uuid.equals(player.getUUID()) && player.getGameProfile() != null) {
|
|
||||||
String profileName = extractTextValue(player.getGameProfile());
|
|
||||||
if (profileName != null && !profileName.isBlank()) return profileName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Throwable ignored) { }
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Object firstNonNull(Object... values) {
|
|
||||||
if (values == null) return null;
|
|
||||||
for (Object value : values) {
|
|
||||||
if (value != null) return value;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Object invokeFirstNoArg(Object target, String... names) {
|
|
||||||
if (target == null || names == null) return null;
|
|
||||||
Class<?> type = target.getClass();
|
|
||||||
for (String name : names) {
|
|
||||||
try {
|
|
||||||
Method method = type.getMethod(name);
|
|
||||||
method.setAccessible(true);
|
|
||||||
return method.invoke(target);
|
|
||||||
} catch (Throwable ignored) { }
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Object getRecordComponentValue(Object target, int index) {
|
|
||||||
if (target == null) return null;
|
|
||||||
try {
|
|
||||||
RecordComponent[] components = target.getClass().getRecordComponents();
|
|
||||||
if (components == null || index < 0 || index >= components.length) return null;
|
|
||||||
Method accessor = components[index].getAccessor();
|
|
||||||
accessor.setAccessible(true);
|
|
||||||
return accessor.invoke(target);
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Object findRecordComponentByValueType(Object target, Class<?> type) {
|
|
||||||
if (target == null || type == null) return null;
|
|
||||||
try {
|
|
||||||
RecordComponent[] components = target.getClass().getRecordComponents();
|
|
||||||
if (components == null) return null;
|
|
||||||
for (RecordComponent component : components) {
|
|
||||||
if (type.isAssignableFrom(component.getType())) {
|
|
||||||
Method accessor = component.getAccessor();
|
|
||||||
accessor.setAccessible(true);
|
|
||||||
return accessor.invoke(target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Throwable ignored) { }
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Object findRecordComponentByMethodNames(Object target, int fallbackIndex, String... names) {
|
|
||||||
Object direct = invokeFirstNoArg(target, names);
|
|
||||||
return direct != null ? direct : getRecordComponentValue(target, fallbackIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Object findRecordTextComponent(Object target, Object... excludedValues) {
|
|
||||||
if (target == null) return null;
|
|
||||||
try {
|
|
||||||
RecordComponent[] components = target.getClass().getRecordComponents();
|
|
||||||
if (components == null) return null;
|
|
||||||
outer:
|
|
||||||
for (RecordComponent component : components) {
|
|
||||||
Method accessor = component.getAccessor();
|
|
||||||
accessor.setAccessible(true);
|
|
||||||
Object value = accessor.invoke(target);
|
|
||||||
if (value == null) continue;
|
|
||||||
for (Object excluded : excludedValues) {
|
|
||||||
if (excluded == value) continue outer;
|
|
||||||
}
|
|
||||||
if (value instanceof Component || value instanceof CharSequence || value instanceof Optional<?>) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Throwable ignored) { }
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Component extractTextComponent(Object value) {
|
|
||||||
if (value == null) return null;
|
|
||||||
if (value instanceof Optional<?> optional) {
|
|
||||||
return optional.isPresent() ? extractTextComponent(optional.get()) : null;
|
|
||||||
}
|
|
||||||
if (value instanceof Component text) return literalizeTextComponent(text);
|
|
||||||
if (value instanceof CharSequence chars) return Component.literal(chars.toString());
|
|
||||||
if (value instanceof UUID uuid) return Component.literal(resolvePlayerName(uuid));
|
|
||||||
try {
|
|
||||||
Method getComponent = value.getClass().getMethod("getText");
|
|
||||||
getComponent.setAccessible(true);
|
|
||||||
Object result = getComponent.invoke(value);
|
|
||||||
Component text = extractTextComponent(result);
|
|
||||||
if (text != null) return text;
|
|
||||||
} catch (Throwable ignored) { }
|
|
||||||
try {
|
|
||||||
Method getString = value.getClass().getMethod("getString");
|
|
||||||
getString.setAccessible(true);
|
|
||||||
Object result = getString.invoke(value);
|
|
||||||
if (result != null) return Component.literal(String.valueOf(result));
|
|
||||||
} catch (Throwable ignored) { }
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Component literalizeTextComponent(Component text) {
|
public static Component literalizeTextComponent(Component text) {
|
||||||
if (text == null) return null;
|
return MacroChatText.literalizeTextComponent(text);
|
||||||
MutableComponent literalized = Component.empty();
|
|
||||||
try {
|
|
||||||
text.visit((style, string) -> {
|
|
||||||
if (string != null && !string.isEmpty()) {
|
|
||||||
literalized.append(Component.literal(string).setStyle(copyVisualStyle(style)));
|
|
||||||
}
|
|
||||||
return Optional.empty();
|
|
||||||
}, Style.EMPTY);
|
|
||||||
} catch (Throwable ignored) { }
|
|
||||||
if (!literalized.getString().isEmpty()) {
|
|
||||||
return literalized;
|
|
||||||
}
|
|
||||||
return Component.literal(text.getString());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Style copyVisualStyle(Style style) {
|
|
||||||
if (style == null || style.isEmpty()) return Style.EMPTY;
|
|
||||||
Style safe = Style.EMPTY;
|
|
||||||
if (style.getFont() != null) safe = safe.withFont(style.getFont());
|
|
||||||
if (style.getColor() != null) safe = safe.withColor(style.getColor());
|
|
||||||
if (style.isBold()) safe = safe.withBold(true);
|
|
||||||
if (style.isItalic()) safe = safe.withItalic(true);
|
|
||||||
if (style.isUnderlined()) safe = safe.withUnderlined(true);
|
|
||||||
if (style.isStrikethrough()) safe = safe.withStrikethrough(true);
|
|
||||||
if (style.isObfuscated()) safe = safe.withObfuscated(true);
|
|
||||||
return safe;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String serializeTextComponent(Component text) {
|
public static String serializeTextComponent(Component text) {
|
||||||
if (text == null) return "";
|
return MacroChatText.serializeTextComponent(text);
|
||||||
try {
|
|
||||||
Component safeComponent = literalizeTextComponent(text);
|
|
||||||
return safeComponent == null ? "" : GSON.toJson(ComponentSerialization.CODEC.encodeStart(JsonOps.INSTANCE, safeComponent).getOrThrow());
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Component deserializeTextComponent(String json) {
|
public static Component deserializeTextComponent(String json) {
|
||||||
if (json == null || json.isBlank()) return null;
|
return MacroChatText.deserializeTextComponent(json);
|
||||||
try {
|
|
||||||
return literalizeTextComponent(ComponentSerialization.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseString(json)).getOrThrow().copy());
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String extractTextValue(Object value) {
|
|
||||||
if (value == null) return null;
|
|
||||||
if (value instanceof Optional<?> optional) {
|
|
||||||
return optional.isPresent() ? extractTextValue(optional.get()) : null;
|
|
||||||
}
|
|
||||||
if (value instanceof Component text) return text.getString();
|
|
||||||
if (value instanceof CharSequence chars) return chars.toString();
|
|
||||||
if (value instanceof UUID uuid) return resolvePlayerName(uuid);
|
|
||||||
try {
|
|
||||||
Method getString = value.getClass().getMethod("getString");
|
|
||||||
getString.setAccessible(true);
|
|
||||||
Object result = getString.invoke(value);
|
|
||||||
if (result != null) return String.valueOf(result);
|
|
||||||
} catch (Throwable ignored) { }
|
|
||||||
try {
|
|
||||||
Method getName = value.getClass().getMethod("getName");
|
|
||||||
getName.setAccessible(true);
|
|
||||||
Object result = getName.invoke(value);
|
|
||||||
if (result instanceof CharSequence chars) return chars.toString();
|
|
||||||
} catch (Throwable ignored) { }
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void onRender(float tickDelta) {
|
public static void onRender(float tickDelta) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue