Skip to content

Commit acb2407

Browse files
committed
♻️ refactor: overhaul text replacement logic
completely revamps text replacement by not relying on legacy formatting & regexes and instead directly working with text objects. also fixes #7. code is still a mess and could functionally be improved, but it works. due to many edge cases (e.g. chat rendering vs tab/nametag/width calculations), many checks, albeit mostly redundant, had to be implemented.
1 parent 41bb345 commit acb2407

12 files changed

Lines changed: 193 additions & 121 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ A mod to spoof your Hypixel Rank.
66

77
This is the modern branch (`v2`), made for the latest versions of Minecraft. To see the legacy version (`v1`), made for 1.8.9, check out the [`legacy` branch](https://github.com/cxntered/RankSpoof/tree/legacy).
88

9-
Based on [`CustomRankUpdated`](https://www.chattriggers.com/modules/v/CustomRankUpdated) by Alexandra for ChatTriggers (which is an updated version of [`CustomRank`](https://www.chattriggers.com/modules/v/CustomRank) by Heknon).
9+
Originally inspired by [`CustomRankUpdated`](https://www.chattriggers.com/modules/v/CustomRankUpdated) by Alexandra for ChatTriggers (which is an updated version of [`CustomRank`](https://www.chattriggers.com/modules/v/CustomRank) by Heknon).
1010

1111
</div>

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ loader_version=0.16.14
1010
loom_version=1.10-SNAPSHOT
1111

1212
# Mod Properties
13-
mod_version=2.1.0
13+
mod_version=2.2.0
1414
maven_group=dev.cxntered.rankspoof
1515
archives_base_name=RankSpoof
1616

src/main/java/dev/cxntered/rankspoof/RankSpoof.java

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,58 +2,10 @@
22

33
import dev.cxntered.rankspoof.config.ModConfig;
44
import net.fabricmc.api.ModInitializer;
5-
import net.minecraft.client.MinecraftClient;
6-
7-
import java.util.regex.Matcher;
8-
import java.util.regex.Pattern;
95

106
public class RankSpoof implements ModInitializer {
11-
private static final Pattern TEAM_REGEX = Pattern.compile("(?:§r)?§[a-z0-9]§l[A-Z](?:§r)? .*"); // matches team prefixes, e.g. "§r§c§lR"
12-
private static final Pattern LAST_FORMAT_PATTERN = Pattern.compile("[§a-f0-9rblomn]{2}"); // matches last format, e.g. "§b" or "§l"
13-
14-
private static String cachedUsername;
15-
private static Pattern rankPattern;
16-
private static Pattern noRankPattern;
17-
private static Pattern playerPattern;
18-
197
@Override
208
public void onInitialize() {
219
ModConfig.CONFIG.load();
2210
}
23-
24-
public static String getSpoofedText(String text) {
25-
String username = MinecraftClient.getInstance().getSession().getUsername();
26-
if (!text.contains(username) || TEAM_REGEX.matcher(text).find()) return text;
27-
28-
if (!username.equals(cachedUsername)) {
29-
cachedUsername = username;
30-
updatePatterns(username);
31-
}
32-
33-
String rank = ModConfig.CONFIG.instance().spoofedRank.replace('&', '§');
34-
Matcher rankMatcher = rankPattern.matcher(text);
35-
Matcher noRankMatcher = noRankPattern.matcher(text);
36-
37-
if (rankMatcher.find()) {
38-
return rankMatcher.replaceFirst(rank + " " + username);
39-
} else if (noRankMatcher.find()) {
40-
return noRankMatcher.replaceFirst(rank + " " + username);
41-
} else {
42-
Matcher lastFormatMatcher = LAST_FORMAT_PATTERN.matcher(rank);
43-
String lastFormat = "";
44-
45-
while (lastFormatMatcher.find()) {
46-
lastFormat = lastFormatMatcher.group();
47-
}
48-
49-
return playerPattern.matcher(text).replaceFirst(lastFormat + username);
50-
}
51-
}
52-
53-
private static void updatePatterns(String username) {
54-
String quotedUsername = Pattern.quote(username);
55-
rankPattern = Pattern.compile("\\[[A-Za-z§0-9+]+] " + quotedUsername); // matches rank & username, e.g. "§b[MVP§3+§b] cxntered"
56-
noRankPattern = Pattern.compile("(§r§7|§7)" + quotedUsername); // matches username without rank, e.g. "§7cxntered"
57-
playerPattern = Pattern.compile("[a-f0-9§]{2}" + quotedUsername); // matches username with only rank color, e.g. "§bcxntered"
58-
}
5911
}

src/main/java/dev/cxntered/rankspoof/config/RankPreview.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package dev.cxntered.rankspoof.config;
22

3+
import dev.cxntered.rankspoof.text.LegacyFormatting;
34
import dev.isxander.yacl3.gui.image.ImageRenderer;
45
import net.minecraft.client.MinecraftClient;
56
import net.minecraft.client.font.TextRenderer;
@@ -19,9 +20,9 @@ public int render(DrawContext drawContext, int x, int y, int width, float v) {
1920
isRendering = true;
2021

2122
TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer;
22-
String rankPreview = (rank.isEmpty() ? "" : rank.replace('&', '§') + " ") + MinecraftClient.getInstance().getSession().getUsername();
23+
Text rankPreview = LegacyFormatting.fromLegacy(rank.replace('&', '§') + " " + MinecraftClient.getInstance().getSession().getUsername());
2324

24-
List<OrderedText> lines = textRenderer.wrapLines(Text.literal(rankPreview), width - 10);
25+
List<OrderedText> lines = textRenderer.wrapLines(rankPreview, width - 10);
2526
int textHeight = lines.size() * textRenderer.fontHeight;
2627
int totalHeight = textHeight + 10;
2728

src/main/java/dev/cxntered/rankspoof/mixin/ChatMessagesMixin.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package dev.cxntered.rankspoof.mixin;
22

3-
import dev.cxntered.rankspoof.RankSpoof;
43
import dev.cxntered.rankspoof.config.ModConfig;
5-
import dev.cxntered.rankspoof.text.LegacyFormatting;
4+
import dev.cxntered.rankspoof.text.RankTextModifier;
5+
import dev.cxntered.rankspoof.text.TextConverter;
66
import net.minecraft.client.util.ChatMessages;
77
import net.minecraft.text.StringVisitable;
88
import org.spongepowered.asm.mixin.Mixin;
@@ -20,10 +20,8 @@ public abstract class ChatMessagesMixin {
2020
index = 0
2121
)
2222
private static StringVisitable spoofChatMessageLine(StringVisitable stringVisitable) {
23-
if (ModConfig.CONFIG.instance().enabled) {
24-
String message = LegacyFormatting.toLegacy(stringVisitable);
25-
return LegacyFormatting.fromLegacy(RankSpoof.getSpoofedText(message));
26-
}
23+
if (ModConfig.CONFIG.instance().enabled)
24+
return RankTextModifier.replaceRank(TextConverter.fromStringVisitable(stringVisitable));
2725
return stringVisitable;
2826
}
2927
}

src/main/java/dev/cxntered/rankspoof/mixin/InGameHudMixin.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package dev.cxntered.rankspoof.mixin;
22

33
import dev.cxntered.rankspoof.config.ModConfig;
4+
import dev.cxntered.rankspoof.text.LegacyFormatting;
45
import net.minecraft.client.gui.hud.InGameHud;
56
import net.minecraft.text.Text;
67
import org.spongepowered.asm.mixin.Mixin;
@@ -20,7 +21,7 @@ private Text spoofScoreboardRank(Text text) {
2021
.replace('&', '§')
2122
.replace("[", "")
2223
.replace("]", "");
23-
return Text.literal("Rank: " + rank);
24+
return Text.literal("Rank: ").append(LegacyFormatting.fromLegacy(rank));
2425
}
2526
return text;
2627
}

src/main/java/dev/cxntered/rankspoof/mixin/OrderedTextTooltipComponentMixin.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@ public abstract class OrderedTextTooltipComponentMixin {
2323
@Inject(method = "<init>", at = @At("RETURN"))
2424
private void spoofTooltipRank(OrderedText text, CallbackInfo ci) {
2525
if (ModConfig.CONFIG.instance().enabled) {
26+
// TODO: remove dependency on LegacyFormatting
2627
String string = LegacyFormatting.toLegacy(text);
2728
if (string.startsWith("§7Rank: ")) {
2829
String rank = ModConfig.CONFIG.instance().spoofedRank
2930
.replace('&', '§')
3031
.replace("[", "")
3132
.replace("]", "");
32-
this.text = Text.literal("§7Rank: §r" + rank).asOrderedText();
33+
this.text = Text.literal("§7Rank: §r").append(LegacyFormatting.fromLegacy(rank)).asOrderedText();
3334
}
3435
}
3536
}

src/main/java/dev/cxntered/rankspoof/mixin/TextRendererMixin.java

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,26 @@
22

33
import dev.cxntered.rankspoof.config.ModConfig;
44
import dev.cxntered.rankspoof.config.RankPreview;
5-
import dev.cxntered.rankspoof.text.SpoofedOrderedText;
5+
import dev.cxntered.rankspoof.text.TextConverter;
6+
import dev.cxntered.rankspoof.text.RankTextModifier;
67
import net.minecraft.client.font.TextRenderer;
78
import net.minecraft.text.OrderedText;
89
import net.minecraft.text.StringVisitable;
9-
import net.minecraft.text.Text;
1010
import org.spongepowered.asm.mixin.Mixin;
11-
import org.spongepowered.asm.mixin.Shadow;
1211
import org.spongepowered.asm.mixin.injection.At;
13-
import org.spongepowered.asm.mixin.injection.Inject;
1412
import org.spongepowered.asm.mixin.injection.ModifyVariable;
15-
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
1613

1714
@Mixin(TextRenderer.class)
1815
public abstract class TextRendererMixin {
19-
@Shadow
20-
public abstract int getWidth(OrderedText text);
21-
2216
@ModifyVariable(
2317
method = "prepare(Lnet/minecraft/text/OrderedText;FFIZI)Lnet/minecraft/client/font/TextRenderer$GlyphDrawable;",
2418
at = @At(value = "HEAD"),
2519
argsOnly = true
2620
)
2721
private OrderedText spoofPrepare(OrderedText orderedText) {
28-
if (ModConfig.CONFIG.instance().enabled && !RankPreview.isRendering) return new SpoofedOrderedText(orderedText);
22+
if (ModConfig.CONFIG.instance().enabled && !RankPreview.isRendering)
23+
return RankTextModifier.replaceRank(TextConverter.fromOrderedText(orderedText)).asOrderedText();
24+
2925
return orderedText;
3026
}
3127

@@ -35,17 +31,19 @@ private OrderedText spoofPrepare(OrderedText orderedText) {
3531
argsOnly = true
3632
)
3733
private OrderedText spoofGetWidthOrderedText(OrderedText orderedText) {
38-
if (ModConfig.CONFIG.instance().enabled && !RankPreview.isRendering) return new SpoofedOrderedText(orderedText);
34+
if (ModConfig.CONFIG.instance().enabled && !RankPreview.isRendering)
35+
return RankTextModifier.replaceRank(TextConverter.fromOrderedText(orderedText)).asOrderedText();
3936
return orderedText;
4037
}
4138

42-
@Inject(
39+
@ModifyVariable(
4340
method = "getWidth(Lnet/minecraft/text/StringVisitable;)I",
4441
at = @At(value = "HEAD"),
45-
cancellable = true
42+
argsOnly = true
4643
)
47-
private void spoofGetWidthStringVisitable(StringVisitable stringVisitable, CallbackInfoReturnable<Integer> cir) {
48-
if (ModConfig.CONFIG.instance().enabled && stringVisitable instanceof Text text)
49-
cir.setReturnValue(this.getWidth(new SpoofedOrderedText(text.asOrderedText())));
44+
private StringVisitable spoofGetWidthStringVisitable(StringVisitable stringVisitable) {
45+
if (ModConfig.CONFIG.instance().enabled && !RankPreview.isRendering)
46+
return RankTextModifier.replaceRank(TextConverter.fromStringVisitable(stringVisitable));
47+
return stringVisitable;
5048
}
5149
}

src/main/java/dev/cxntered/rankspoof/text/LegacyFormatting.java

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,17 @@
11
package dev.cxntered.rankspoof.text;
22

3-
import net.minecraft.text.*;
3+
import net.minecraft.text.MutableText;
4+
import net.minecraft.text.OrderedText;
5+
import net.minecraft.text.Style;
6+
import net.minecraft.text.Text;
47
import net.minecraft.util.Formatting;
58

6-
import java.util.Optional;
79
import java.util.concurrent.atomic.AtomicReference;
810

911
/**
1012
* This class is used to convert to and from legacy formatting codes (i.e. codes starting with '§').
1113
*/
1214
public class LegacyFormatting {
13-
/**
14-
* Converts a StringVisitable object to a legacy formatted string.
15-
*
16-
* @param stringVisitable The StringVisitable object to convert.
17-
* @return A string with legacy formatting.
18-
*/
19-
public static String toLegacy(StringVisitable stringVisitable) {
20-
StringBuilder builder = new StringBuilder();
21-
AtomicReference<Style> lastFormatting = new AtomicReference<>(Style.EMPTY);
22-
23-
stringVisitable.visit((style, string) -> {
24-
String formatting = getFormattingCodes(style, lastFormatting.get());
25-
builder.append(formatting).append(string);
26-
lastFormatting.set(style);
27-
return Optional.empty();
28-
}, Style.EMPTY);
29-
30-
return builder.toString();
31-
}
32-
3315
/**
3416
* Converts an OrderedText object to a legacy formatted string.
3517
*
@@ -51,12 +33,12 @@ public static String toLegacy(OrderedText orderedText) {
5133
}
5234

5335
/**
54-
* Converts a legacy formatted string to a StringVisitable object.
36+
* Converts a legacy formatted string to a Text object.
5537
*
5638
* @param string The legacy formatted string to convert.
57-
* @return A StringVisitable object with the equivalent formatting.
39+
* @return A Text object with the equivalent formatting.
5840
*/
59-
public static StringVisitable fromLegacy(String string) {
41+
public static Text fromLegacy(String string) {
6042
if (string == null || string.isEmpty()) {
6143
return Text.empty();
6244
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package dev.cxntered.rankspoof.text;
2+
3+
import dev.cxntered.rankspoof.config.ModConfig;
4+
import net.minecraft.client.MinecraftClient;
5+
import net.minecraft.text.MutableText;
6+
import net.minecraft.text.Text;
7+
import net.minecraft.text.TextColor;
8+
import net.minecraft.util.Formatting;
9+
10+
import java.util.List;
11+
import java.util.regex.Pattern;
12+
13+
public class RankTextModifier {
14+
private static final Pattern RANK_START_PATTERN = Pattern.compile("\\[[A-Za-z]+"); // matches rank start, e.g. "[MVP" (for "[MVP", "++", "]") or "[VIP]"
15+
16+
/**
17+
* Replaces the rank in a Text with a spoofed rank.
18+
* <p>
19+
* As this mod is intended for Hypixel, it makes a few assumptions which may not hold true for other servers:
20+
* <ul>
21+
* <li>The parent text's content is empty (i.e. all text will be in siblings)</li>
22+
* <li>Siblings of the parent text will not have siblings of their own</li>
23+
* <li>Ranks will always have either one or three parts (e.g. "[VIP]" or "[MVP", "++", "]")</li>
24+
* <li>Legacy formatting will not be used</li>
25+
* </ul>
26+
*/
27+
public static Text replaceRank(Text text) {
28+
// TODO: clean up, extract into helper methods
29+
String username = MinecraftClient.getInstance().getSession().getUsername();
30+
String rank = ModConfig.CONFIG.instance().spoofedRank.replace('&', '§');
31+
if (!text.getString().contains(username)) return text;
32+
33+
List<Text> siblings = text.getSiblings();
34+
MutableText mutableText = MutableText.of(text.getContent()).setStyle(text.getStyle());
35+
36+
for (int i = 0; i < siblings.size(); i++) {
37+
Text sibling = siblings.get(i);
38+
String string = sibling.getString();
39+
40+
if (RANK_START_PATTERN.matcher(string).find()) {
41+
// rank is contained within a single sibling, e.g. "[VIP]" or "[MVP]"
42+
if (sibling.getString().endsWith("] ") && i + 1 < siblings.size() && siblings.get(i + 1).getString().equals(username)) {
43+
mutableText.append(LegacyFormatting.fromLegacy(rank + " " + username));
44+
i++; // skip username sibling
45+
continue;
46+
} else if (sibling.getString().endsWith("] " + username)) {
47+
// rank and username are in the same sibling
48+
mutableText.append(LegacyFormatting.fromLegacy(rank + " " + username));
49+
continue;
50+
}
51+
52+
// rank spans multiple siblings, e.g. "[MVP", "++", "]" or "[", "YOUTUBE", "]"
53+
if (i + 3 < siblings.size() && siblings.get(i + 3).getString().equals(username)) {
54+
mutableText.append(LegacyFormatting.fromLegacy(rank + " " + username));
55+
i += 3; // skip rank parts and username
56+
continue;
57+
} else if (i + 2 < siblings.size() && siblings.get(i + 2).getString().endsWith("] " + username)) {
58+
// username is in the same sibling as last rank part
59+
mutableText.append(LegacyFormatting.fromLegacy(rank + " " + username));
60+
i += 2; // skip rank parts and username
61+
continue;
62+
}
63+
} else if (string.contains(username) && (sibling.getStyle().getColor() == TextColor.fromFormatting(Formatting.GRAY) || (sibling.getStyle().isEmpty() && text.getStyle().getColor() == TextColor.fromFormatting(Formatting.GRAY)))) {
64+
// player has no rank
65+
String[] parts = string.split(username, 2);
66+
mutableText.append(Text.literal(parts[0]).setStyle(sibling.getStyle()));
67+
mutableText.append(LegacyFormatting.fromLegacy(rank + " " + username));
68+
mutableText.append(Text.literal(parts[1]).setStyle(sibling.getStyle()));
69+
continue;
70+
} else if (string.contains(username)) {
71+
int checkIndex = (i >= 2) ? i - 2 : (i == 1 ? 0 : -1);
72+
int requiredLength = (i >= 2) ? 1 : 2;
73+
if (checkIndex != -1) {
74+
Text prevText = siblings.get(checkIndex);
75+
String prevString = prevText.getString();
76+
if (prevText.getStyle().isBold() && prevString.length() == requiredLength) {
77+
char c = prevString.charAt(0);
78+
if (c >= 'A' && c <= 'Z') return text;
79+
// player is in a team, skip spoofing
80+
}
81+
}
82+
83+
// rank prefix is omitted, append username with spoofed rank style only
84+
Text rankText = LegacyFormatting.fromLegacy(rank);
85+
String[] parts = string.split(username, 2);
86+
mutableText.append(Text.literal(parts[0]).setStyle(sibling.getStyle()));
87+
mutableText.append(Text.literal(username).setStyle(rankText.getSiblings().getLast().getStyle()));
88+
mutableText.append(Text.literal(parts[1]).setStyle(sibling.getStyle()));
89+
continue;
90+
}
91+
92+
mutableText.append(sibling);
93+
}
94+
95+
return mutableText;
96+
}
97+
98+
}

0 commit comments

Comments
 (0)