Skip to content

Commit 66ccbf3

Browse files
committed
feat: RGB chat support for Minecraft 1.16+ clients
1 parent e309ce4 commit 66ccbf3

File tree

10 files changed

+189
-20
lines changed

10 files changed

+189
-20
lines changed

build.gradle.kts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,25 @@ repositories {
2323
mavenCentral()
2424
maven("https://oss.sonatype.org/content/groups/public/")
2525
maven("https://repo.azisaba.net/repository/maven-public/")
26-
maven("https://papermc.io/repo/repository/maven-snapshots/")
27-
maven("https://papermc.io/repo/repository/maven-public/")
26+
maven("https://repo.papermc.io/repository/maven-public/")
2827
maven("https://repo.aikar.co/content/groups/aikar/")
2928
maven("https://repo.maven.apache.org/maven2/")
29+
maven("https://repo.viaversion.com")
3030
}
3131

3232
dependencies {
3333
implementation(libs.discord4j)
3434
implementation(libs.jedis)
3535
implementation(libs.aikar.taskchain)
3636
implementation(libs.semver4j)
37+
implementation(libs.adventure)
38+
implementation(libs.adventure.serializer.legacy)
39+
implementation(libs.adventure.serializer.plain)
40+
implementation(libs.adventure.serializer.gson)
3741
compileOnly(libs.paper.api)
3842
compileOnly(libs.luckperms.api)
3943
compileOnly(libs.lunachatplus)
44+
compileOnly(libs.viaversion)
4045
compileOnly(libs.jetbrains.annotation)
4146

4247
// Test dependencies
@@ -57,7 +62,7 @@ tasks.withType<Javadoc> {
5762
tasks.processResources {
5863
val props =
5964
mapOf(
60-
"name" to name,
65+
"name" to project.name,
6166
"version" to version,
6267
"description" to description,
6368
"orgName" to orgName,
@@ -76,6 +81,8 @@ tasks.build {
7681

7782
tasks.shadowJar {
7883
isEnableRelocation = true
84+
relocate("io.netty", "io.netty")
85+
exclude("io/netty/**")
7986
relocationPrefix = "net.azisaba.ryuzupluginchat.dependency"
8087
}
8188

gradle/libs.versions.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ paper-api = "1.15.2-R0.1-SNAPSHOT"
88
lombok = "1.18.38"
99
semver4j = "3.1.0"
1010
jedis = "4.3.1"
11+
adventure = "4.25.0"
12+
viaversion = "5.6.0"
1113
aikar-taskchain = "3.7.2"
1214
discord4j = "3.2.3"
1315
lunachatplus = "3.3.0"
@@ -28,6 +30,11 @@ semver4j = { module = "com.vdurmont:semver4j", version.ref = "semver4j" }
2830
lunachatplus = { module = "net.azisaba:lunachatplus", version.ref = "lunachatplus" }
2931
luckperms-api = { module = "net.luckperms:api", version.ref = "luckperms-api" }
3032
jedis = { module = "redis.clients:jedis", version.ref = "jedis" }
33+
adventure = { module = "net.kyori:adventure-api", version.ref = "adventure" }
34+
adventure-serializer-legacy = { module = "net.kyori:adventure-text-serializer-legacy", version.ref = "adventure" }
35+
adventure-serializer-plain = { module = "net.kyori:adventure-text-serializer-plain", version.ref = "adventure" }
36+
adventure-serializer-gson = { module = "net.kyori:adventure-text-serializer-gson", version.ref = "adventure" }
37+
viaversion = { module = "com.viaversion:viaversion-api", version.ref = "viaversion" }
3138
jetbrains-annotation = { module = "org.jetbrains:annotations", version.ref = "jetbrains-annotation" }
3239
junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" }
3340
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }

src/main/java/net/azisaba/ryuzupluginchat/RyuZUPluginChat.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,9 @@ public void onEnable() {
146146
.runTaskTimerAsynchronously(
147147
this, () -> vanishController.refreshAllAsync(), 20 * 30, 20 * 30);
148148

149+
if (Bukkit.getPluginManager().getPlugin("ViaVersion") == null || Bukkit.getPluginManager().getPlugin("ProtocolLib") == null) {
150+
getLogger().warning("ViaVersion/ProtocolLib is not installed. RGB chat might not work depending on the server and client version.");
151+
}
149152
getLogger().info(getName() + " enabled.");
150153
}
151154

src/main/java/net/azisaba/ryuzupluginchat/message/MessageProcessor.java

Lines changed: 103 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import java.util.*;
99
import java.util.stream.Collectors;
10+
1011
import lombok.RequiredArgsConstructor;
1112
import net.azisaba.ryuzupluginchat.RyuZUPluginChat;
1213
import net.azisaba.ryuzupluginchat.event.AsyncChannelMessageEvent;
@@ -17,17 +18,74 @@
1718
import net.azisaba.ryuzupluginchat.message.data.GlobalMessageData;
1819
import net.azisaba.ryuzupluginchat.message.data.PrivateMessageData;
1920
import net.azisaba.ryuzupluginchat.message.data.SystemMessageData;
20-
import net.azisaba.ryuzupluginchat.util.Chat;
21-
import net.azisaba.ryuzupluginchat.util.TaskSchedulingUtils;
21+
import net.azisaba.ryuzupluginchat.util.*;
22+
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
23+
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
24+
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
25+
import net.md_5.bungee.chat.ComponentSerializer;
2226
import org.bukkit.Bukkit;
23-
import org.bukkit.ChatColor;
2427
import org.bukkit.entity.Player;
2528

2629
@RequiredArgsConstructor
2730
public class MessageProcessor {
28-
31+
// net.kyori.adventure.text.Component
32+
private static final char[] COMPONENT_CLASS_NAME =
33+
new char[] {'n', 'e', 't', '.', 'k', 'y', 'o', 'r', 'i', '.', 'a', 'd', 'v', 'e', 'n', 't', 'u', 'r', 'e',
34+
'.', 't', 'e', 'x', 't', '.', 'C', 'o', 'm', 'p', 'o', 'n', 'e', 'n', 't'};
35+
// net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer
36+
private static final char[] LEGACY_COMPONENT_SERIALIZER_CLASS_NAME =
37+
new char[] {'n', 'e', 't', '.', 'k', 'y', 'o', 'r', 'i', '.', 'a', 'd', 'v', 'e', 'n', 't', 'u', 'r', 'e',
38+
'.', 't', 'e', 'x', 't', '.', 's', 'e', 'r', 'i', 'a', 'l', 'i', 'z', 'e', 'r', '.',
39+
'l', 'e', 'g', 'a', 'c', 'y', '.', 'L', 'e', 'g', 'a', 'c', 'y', 'C', 'o', 'm', 'p', 'o', 'n', 'e',
40+
'n', 't', 'S', 'e', 'r', 'i', 'a', 'l', 'i', 'z', 'e', 'r'};
41+
private static final LegacyComponentSerializer LEGACY_DESERIALIZER =
42+
LegacyComponentSerializer.builder()
43+
.character('&')
44+
.hexColors()
45+
.extractUrls()
46+
.build();
47+
private static final LegacyComponentSerializer LEGACY_SERIALIZER_HEX =
48+
LegacyComponentSerializer.builder()
49+
.character('§')
50+
.hexColors()
51+
.build();
52+
private static final LegacyComponentSerializer LEGACY_SERIALIZER_UNUSUAL_X_REPEATED_HEX =
53+
LegacyComponentSerializer.builder()
54+
.character('§')
55+
.hexColors()
56+
.useUnusualXRepeatedCharacterHexFormat()
57+
.build();
2958
private final RyuZUPluginChat plugin;
3059

60+
private void sendRGBMessage(Player player, String message) {
61+
RGBStatus rgbStatus = isRGBSupported(player);
62+
if (rgbStatus == RGBStatus.SERVER_AND_CLIENT || rgbStatus == RGBStatus.SERVER_ONLY) {
63+
try {
64+
// Try to invoke Player#sendMessage(Component) in Paper API
65+
Class<?> legacyComponentSerializerClass = Class.forName(new String(LEGACY_COMPONENT_SERIALIZER_CLASS_NAME));
66+
Object legacyAmpersand = legacyComponentSerializerClass.getMethod("legacyAmpersand").invoke(null);
67+
Object component = legacyComponentSerializerClass.getMethod("deserialize", String.class).invoke(legacyAmpersand, message);
68+
Player.class.getMethod("sendMessage", Class.forName(new String(COMPONENT_CLASS_NAME))).invoke(player, component);
69+
} catch (ReflectiveOperationException ignored) {
70+
try {
71+
player.spigot().sendMessage(ComponentSerializer.parse(GsonComponentSerializer.gson().serialize(LEGACY_DESERIALIZER.deserialize(message))));
72+
} catch (Exception ignored2) {
73+
player.sendMessage(LEGACY_SERIALIZER_HEX.serialize(LEGACY_DESERIALIZER.deserialize(message)));
74+
}
75+
}
76+
} else if (rgbStatus == RGBStatus.CLIENT_ONLY) {
77+
if (Bukkit.getPluginManager().getPlugin("ViaVersion") != null) {
78+
String json = GsonComponentSerializer.gson().serialize(LEGACY_DESERIALIZER.deserialize(message));
79+
ViaUtil.sendJsonMessage(player, json);
80+
} else {
81+
// no way to send RGB chat :(
82+
player.sendMessage(LegacyComponentSerializer.legacySection().serialize(LEGACY_DESERIALIZER.deserialize(message)));
83+
}
84+
} else {
85+
player.sendMessage(LegacyComponentSerializer.legacySection().serialize(LEGACY_DESERIALIZER.deserialize(message)));
86+
}
87+
}
88+
3189
public void processGlobalMessage(GlobalMessageData data) {
3290
String message = data.format();
3391

@@ -56,10 +114,10 @@ public void processGlobalMessage(GlobalMessageData data) {
56114
}
57115

58116
for (Player player : event.getRecipients()) {
59-
player.sendMessage(message);
117+
sendRGBMessage(player, message);
60118
}
61119

62-
plugin.getLogger().info("[Global-Chat] " + ChatColor.stripColor(message));
120+
plugin.getLogger().info("[Global-Chat] " + PlainTextComponentSerializer.plainText().serialize(LegacyComponentSerializer.legacyAmpersand().deserialize(message)));
63121
}
64122

65123
public void processChannelChatMessage(ChannelChatMessageData data) {
@@ -78,7 +136,7 @@ public void processChannelChatMessage(ChannelChatMessageData data) {
78136

79137
plugin
80138
.getLogger()
81-
.info(Chat.f("[Channel-Chat] ({0}) {1}", channel.getName(), ChatColor.stripColor(message)));
139+
.info(Chat.f("[Channel-Chat] ({0}) {1}", channel.getName(), PlainTextComponentSerializer.plainText().serialize(LegacyComponentSerializer.legacyAmpersand().deserialize(message))));
82140

83141
Set<Player> recipients;
84142
if (data.isFromDiscord()) {
@@ -137,7 +195,7 @@ public void processChannelChatMessage(ChannelChatMessageData data) {
137195
}
138196

139197
for (Player player : event.getRecipients()) {
140-
player.sendMessage(message);
198+
sendRGBMessage(player, message);
141199
}
142200
}
143201

@@ -169,7 +227,7 @@ public void processUndeliveredPrivateMessage(PrivateMessageData data) {
169227
recipients.add(targetPlayer);
170228
}
171229

172-
plugin.getLogger().info("[Private-Chat] " + ChatColor.stripColor(message));
230+
plugin.getLogger().info("[Private-Chat] " + PlainTextComponentSerializer.plainText().serialize(LegacyComponentSerializer.legacyAmpersand().deserialize(message)));
173231

174232
AsyncPrivateMessageEvent event = new AsyncPrivateMessageEvent(data, recipients);
175233
Bukkit.getPluginManager().callEvent(event);
@@ -178,7 +236,7 @@ public void processUndeliveredPrivateMessage(PrivateMessageData data) {
178236
}
179237

180238
for (Player player : event.getRecipients()) {
181-
player.sendMessage(message);
239+
sendRGBMessage(player, message);
182240
}
183241

184242
RyuZUPluginChat.newChain()
@@ -221,7 +279,7 @@ public void notifyDeliveredPrivateMessage(PrivateMessageData data) {
221279
String message = data.format();
222280

223281
for (Player player : event.getRecipients()) {
224-
player.sendMessage(message);
282+
sendRGBMessage(player, message);
225283
}
226284
}
227285

@@ -270,4 +328,38 @@ public void processSystemMessage(SystemMessageData data) {
270328
private Map<String, String> convertObjectIntoMap(Object mapObject) {
271329
return (Map<String, String>) mapObject;
272330
}
331+
332+
enum RGBStatus {
333+
UNSUPPORTED,
334+
SERVER_AND_CLIENT,
335+
SERVER_ONLY,
336+
CLIENT_ONLY,
337+
}
338+
339+
private static RGBStatus isRGBSupported(Player player) {
340+
String version = Bukkit.getVersion();
341+
boolean modern = !version.contains("MC: 1.15") && !version.contains("MC: 1.14") && !version.contains("MC: 1.13") &&
342+
!version.contains("MC: 1.12") && !version.contains("MC: 1.11") && !version.contains("MC: 1.10") &&
343+
!version.contains("MC: 1.9") && !version.contains("MC: 1.8");
344+
if (Bukkit.getPluginManager().getPlugin("ViaVersion") == null) {
345+
if (modern) {
346+
return RGBStatus.SERVER_AND_CLIENT;
347+
} else {
348+
return RGBStatus.UNSUPPORTED;
349+
}
350+
}
351+
if (ViaUtil.isRGBSupportedVersion(player)) {
352+
if (modern) {
353+
return RGBStatus.SERVER_AND_CLIENT;
354+
} else {
355+
return RGBStatus.CLIENT_ONLY;
356+
}
357+
} else {
358+
if (modern) {
359+
return RGBStatus.SERVER_ONLY;
360+
} else {
361+
return RGBStatus.UNSUPPORTED;
362+
}
363+
}
364+
}
273365
}

src/main/java/net/azisaba/ryuzupluginchat/message/data/ChannelChatMessageData.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import lombok.Data;
77
import lombok.NoArgsConstructor;
88
import net.azisaba.ryuzupluginchat.util.Chat;
9-
import org.bukkit.ChatColor;
109

1110
@Data
1211
@AllArgsConstructor
@@ -49,7 +48,7 @@ public String format() {
4948
.replace("%ch", convertEmptyIfNull(lunaChatChannelName))
5049
.replace("%color", convertEmptyIfNull(channelColorCode))
5150
.replace("%servername", convertEmptyIfNull(sendServerName));
52-
msg = ChatColor.translateAlternateColorCodes('&', msg);
51+
msg = GlobalMessageData.LEGACY_SERIALIZER.serialize(GlobalMessageData.LEGACY_SERIALIZER.deserialize(msg));
5352
if (japanized) {
5453
msg = msg.replace("%premsg", preReplaceMessage);
5554
} else {

src/main/java/net/azisaba/ryuzupluginchat/message/data/GlobalMessageData.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,19 @@
66
import lombok.Data;
77
import lombok.NoArgsConstructor;
88
import net.azisaba.ryuzupluginchat.util.Chat;
9-
import org.bukkit.ChatColor;
9+
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
1010

1111
@Data
1212
@AllArgsConstructor
1313
@NoArgsConstructor
1414
@JsonIgnoreProperties(ignoreUnknown = true)
1515
public class GlobalMessageData implements MessageData {
16+
static final LegacyComponentSerializer LEGACY_SERIALIZER =
17+
LegacyComponentSerializer
18+
.builder()
19+
.character('&')
20+
.hexColors()
21+
.build();
1622

1723
private String format;
1824
private String lunaChatPrefix; // TODO: remove (存在しない)
@@ -56,7 +62,7 @@ public String format() {
5662
.replace("[LunaChatSuffix]", convertEmptyIfNull(lunaChatSuffix))
5763
.replace("[LuckPermsSuffix]", convertEmptyIfNull(luckPermsSuffix));
5864

59-
formatted = ChatColor.translateAlternateColorCodes('&', formatted);
65+
formatted = LEGACY_SERIALIZER.serialize(LEGACY_SERIALIZER.deserialize(formatted));
6066
if (japanized) {
6167
formatted =
6268
formatted.replace(

src/main/java/net/azisaba/ryuzupluginchat/message/data/PrivateMessageData.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import lombok.Data;
77
import lombok.NoArgsConstructor;
88
import org.bukkit.Bukkit;
9-
import org.bukkit.ChatColor;
109
import org.bukkit.entity.Player;
1110

1211
@Data
@@ -62,7 +61,7 @@ public String format() {
6261
.replace("[PlayerDisplayName]", convertEmptyIfNull(sentDisplayName))
6362
.replace("[ReceivePlayerDisplayName]", convertEmptyIfNull(receivedDisplayName));
6463

65-
formatted = ChatColor.translateAlternateColorCodes('&', formatted);
64+
formatted = GlobalMessageData.LEGACY_SERIALIZER.serialize(GlobalMessageData.LEGACY_SERIALIZER.deserialize(formatted));
6665
if (japanized) {
6766
formatted =
6867
formatted.replace(
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package net.azisaba.ryuzupluginchat.util;
2+
3+
import io.netty.buffer.ByteBuf;
4+
import io.netty.handler.codec.EncoderException;
5+
import org.jetbrains.annotations.NotNull;
6+
7+
import java.nio.charset.StandardCharsets;
8+
9+
public class PacketUtil {
10+
public static void writeVarInt(@NotNull ByteBuf buf, int i) {
11+
while ((i & 0xFFFFFF80) != 0) {
12+
buf.writeByte(i & 0x7F | 0x80);
13+
i >>>= 7;
14+
}
15+
buf.writeByte(i);
16+
}
17+
18+
public static void writeString(@NotNull ByteBuf buf, @NotNull String s, int max) {
19+
byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
20+
if (bytes.length > max) {
21+
throw new EncoderException("String too big (was " + bytes.length + " bytes encoded, max " + max + ")");
22+
}
23+
writeVarInt(buf, bytes.length);
24+
buf.writeBytes(bytes);
25+
}
26+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package net.azisaba.ryuzupluginchat.util;
2+
3+
import com.viaversion.viaversion.api.Via;
4+
import com.viaversion.viaversion.api.ViaAPI;
5+
import com.viaversion.viaversion.api.protocol.version.ProtocolVersion;
6+
import io.netty.buffer.ByteBuf;
7+
import io.netty.buffer.Unpooled;
8+
import io.netty.handler.codec.CodecException;
9+
import org.bukkit.entity.Player;
10+
import org.jetbrains.annotations.NotNull;
11+
12+
import java.util.Objects;
13+
14+
@SuppressWarnings("unchecked")
15+
public class ViaUtil {
16+
public static boolean isRGBSupportedVersion(@NotNull Player player) {
17+
return ((ViaAPI<Player>) Via.getAPI()).getPlayerProtocolVersion(player).newerThanOrEqualTo(ProtocolVersion.v1_16);
18+
}
19+
20+
public static void sendJsonMessage(@NotNull Player player, @NotNull String json) {
21+
ByteBuf buf = Unpooled.buffer();
22+
PacketUtil.writeVarInt(buf, 0x0F);
23+
PacketUtil.writeString(buf, json, 262144);
24+
buf.writeByte(0);
25+
Objects.requireNonNull(Via.getAPI().getConnection(player.getUniqueId())).transformClientbound(buf, CodecException::new);
26+
((ViaAPI<Player>) Via.getAPI()).sendRawPacket(player, buf);
27+
}
28+
}

src/main/resources/plugin.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ website: '${url}'
88
depend:
99
- LuckPerms
1010
- LunaChat
11+
softdepend:
12+
- ViaVersion
1113

1214
commands:
1315
rpc:

0 commit comments

Comments
 (0)