Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements to MessageTranslator #3803

Merged
merged 10 commits into from
Aug 18, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public String checkForRename(GeyserSession session, String rename) {

String originalName = ItemUtils.getCustomName(getInput().getNbt());

String plainOriginalName = MessageTranslator.convertToPlainText(originalName, session.locale());
String plainOriginalName = MessageTranslator.convertToPlainTextLenient(originalName, session.locale());
String plainNewName = MessageTranslator.convertToPlainText(rename);
if (!plainOriginalName.equals(plainNewName)) {
// Strip out formatting since Java Edition does not allow it
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ private void updateInventoryState(GeyserSession session, AnvilContainer anvilCon

// Changing the item in the input slot resets the name field on Bedrock, but
// does not result in a FilterTextPacket
String originalName = MessageTranslator.convertToPlainText(ItemUtils.getCustomName(input.getNbt()), session.locale());
String originalName = MessageTranslator.convertToPlainTextLenient(ItemUtils.getCustomName(input.getNbt()), session.locale());
ServerboundRenameItemPacket renameItemPacket = new ServerboundRenameItemPacket(originalName);
session.sendDownstreamPacket(renameItemPacket);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.serializer.gson.LegacyHoverEventSerializer;
import net.kyori.adventure.text.serializer.json.LegacyHoverEventSerializer;
import net.kyori.adventure.util.Codec;
import org.jetbrains.annotations.NotNull;

Expand All @@ -40,9 +40,9 @@ public final class DummyLegacyHoverEventSerializer implements LegacyHoverEventSe
private final HoverEvent.ShowItem dummyShowItem;

public DummyLegacyHoverEventSerializer() {
dummyShowEntity = HoverEvent.ShowEntity.of(Key.key("geysermc", "dummyshowitem"),
dummyShowEntity = HoverEvent.ShowEntity.showEntity(Key.key("geysermc", "dummyshowitem"),
UUID.nameUUIDFromBytes("entitiesareprettyneat".getBytes(StandardCharsets.UTF_8)));
dummyShowItem = HoverEvent.ShowItem.of(Key.key("geysermc", "dummyshowentity"), 0);
dummyShowItem = HoverEvent.ShowItem.showItem(Key.key("geysermc", "dummyshowentity"), 0);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public void translateTag(NbtMapBuilder builder, CompoundTag tag, int blockState)
// Java and Bedrock values
builder.put("conditionMet", ((ByteTag) tag.get("conditionMet")).getValue());
builder.put("auto", ((ByteTag) tag.get("auto")).getValue());
builder.put("CustomName", MessageTranslator.convertMessage(((StringTag) tag.get("CustomName")).getValue()));
builder.put("CustomName", MessageTranslator.convertJsonMessage(((StringTag) tag.get("CustomName")).getValue()));
builder.put("powered", ((ByteTag) tag.get("powered")).getValue());
builder.put("Command", ((StringTag) tag.get("Command")).getValue());
builder.put("SuccessCount", ((IntTag) tag.get("SuccessCount")).getValue());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
import com.github.steveice10.mc.protocol.data.DefaultComponentSerializer;
import com.github.steveice10.mc.protocol.data.game.scoreboard.TeamColor;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ScoreComponent;
import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.flattener.ComponentFlattener;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.renderer.TranslatableComponentRenderer;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
Expand All @@ -50,8 +52,8 @@ public class MessageTranslator {
// Possible TODO: replace the legacy hover event serializer with an empty one since we have no use for hover events
private static final GsonComponentSerializer GSON_SERIALIZER;

private static final LegacyComponentSerializer LEGACY_SERIALIZER;
private static final String ALL_COLORS;
private static final LegacyComponentSerializer BEDROCK_SERIALIZER;
private static final String BEDROCK_COLORS;

// Store team colors for player names
private static final Map<TeamColor, String> TEAM_COLORS = new EnumMap<>(TeamColor.class);
Expand Down Expand Up @@ -99,47 +101,43 @@ public class MessageTranslator {
// Tell MCProtocolLib to use this serializer, too.
DefaultComponentSerializer.set(GSON_SERIALIZER);

LegacyComponentSerializer legacySerializer;
String allColors;
try {
Class.forName("net.kyori.adventure.text.serializer.legacy.CharacterAndFormat");

List<CharacterAndFormat> formats = new ArrayList<>(CharacterAndFormat.defaults());
// The following two do not yet exist on Bedrock - https://bugs.mojang.com/browse/MCPE-41729
formats.remove(CharacterAndFormat.STRIKETHROUGH);
formats.remove(CharacterAndFormat.UNDERLINED);

formats.add(CharacterAndFormat.characterAndFormat('g', TextColor.color(221, 214, 5))); // Minecoin Gold
// Add the new characters implemented in 1.19.80
formats.add(CharacterAndFormat.characterAndFormat('h', TextColor.color(227, 212, 209))); // Quartz
formats.add(CharacterAndFormat.characterAndFormat('i', TextColor.color(206, 202, 202))); // Iron
formats.add(CharacterAndFormat.characterAndFormat('j', TextColor.color(68, 58, 59))); // Netherite
formats.add(CharacterAndFormat.characterAndFormat('m', TextColor.color(151, 22, 7))); // Redstone
formats.add(CharacterAndFormat.characterAndFormat('n', TextColor.color(180, 104, 77))); // Copper
formats.add(CharacterAndFormat.characterAndFormat('p', TextColor.color(222, 177, 45))); // Gold
formats.add(CharacterAndFormat.characterAndFormat('q', TextColor.color(17, 160, 54))); // Emerald
formats.add(CharacterAndFormat.characterAndFormat('s', TextColor.color(44, 186, 168))); // Diamond
formats.add(CharacterAndFormat.characterAndFormat('t', TextColor.color(33, 73, 123))); // Lapis
formats.add(CharacterAndFormat.characterAndFormat('u', TextColor.color(154, 92, 198))); // Amethyst

legacySerializer = LegacyComponentSerializer.legacySection().toBuilder()
.formats(formats)
.build();

StringBuilder colorBuilder = new StringBuilder();
for (CharacterAndFormat format : formats) {
if (format.format() instanceof TextColor) {
colorBuilder.append(format.character());
}
// Customize the formatting characters of our legacy serializer for bedrock edition
List<CharacterAndFormat> formats = new ArrayList<>(CharacterAndFormat.defaults());
// The following two do not yet exist on Bedrock - https://bugs.mojang.com/browse/MCPE-41729
formats.remove(CharacterAndFormat.STRIKETHROUGH);
formats.remove(CharacterAndFormat.UNDERLINED);

formats.add(CharacterAndFormat.characterAndFormat('g', TextColor.color(221, 214, 5))); // Minecoin Gold
// Add the new characters implemented in 1.19.80
formats.add(CharacterAndFormat.characterAndFormat('h', TextColor.color(227, 212, 209))); // Quartz
formats.add(CharacterAndFormat.characterAndFormat('i', TextColor.color(206, 202, 202))); // Iron
formats.add(CharacterAndFormat.characterAndFormat('j', TextColor.color(68, 58, 59))); // Netherite
formats.add(CharacterAndFormat.characterAndFormat('m', TextColor.color(151, 22, 7))); // Redstone
formats.add(CharacterAndFormat.characterAndFormat('n', TextColor.color(180, 104, 77))); // Copper
formats.add(CharacterAndFormat.characterAndFormat('p', TextColor.color(222, 177, 45))); // Gold
formats.add(CharacterAndFormat.characterAndFormat('q', TextColor.color(17, 160, 54))); // Emerald
formats.add(CharacterAndFormat.characterAndFormat('s', TextColor.color(44, 186, 168))); // Diamond
formats.add(CharacterAndFormat.characterAndFormat('t', TextColor.color(33, 73, 123))); // Lapis
formats.add(CharacterAndFormat.characterAndFormat('u', TextColor.color(154, 92, 198))); // Amethyst

// Can be removed once Adventure 1.15.0 is released (see https://github.com/KyoriPowered/adventure/pull/954)
ComponentFlattener flattener = ComponentFlattener.basic().toBuilder()
.mapper(ScoreComponent.class, component -> "")
.build();

BEDROCK_SERIALIZER = LegacyComponentSerializer.legacySection().toBuilder()
.formats(formats)
.flattener(flattener)
.build();

// cache all the legacy character codes
StringBuilder colorBuilder = new StringBuilder();
for (CharacterAndFormat format : formats) {
if (format.format() instanceof TextColor) {
colorBuilder.append(format.character());
}
allColors = colorBuilder.toString();
} catch (ClassNotFoundException ignored) {
// Velocity doesn't have this yet.
legacySerializer = LegacyComponentSerializer.legacySection();
allColors = "0123456789abcdef";
}
LEGACY_SERIALIZER = legacySerializer;
ALL_COLORS = allColors;
BEDROCK_COLORS = colorBuilder.toString();
}

/**
Expand All @@ -154,7 +152,7 @@ public static String convertMessage(Component message, String locale) {
// Translate any components that require it
message = RENDERER.render(message, locale);

String legacy = LEGACY_SERIALIZER.serialize(message);
String legacy = BEDROCK_SERIALIZER.serialize(message);

StringBuilder finalLegacy = new StringBuilder();
char[] legacyChars = legacy.toCharArray();
Expand All @@ -170,7 +168,7 @@ public static String convertMessage(Component message, String locale) {
}

char next = legacyChars[++i];
if (ALL_COLORS.indexOf(next) != -1) {
if (BEDROCK_COLORS.indexOf(next) != -1) {
// Append this color code, as well as a necessary reset code
if (!lastFormatReset) {
finalLegacy.append(RESET);
Expand All @@ -189,20 +187,20 @@ public static String convertMessage(Component message, String locale) {
}
}

public static String convertMessage(String message, String locale) {
public static String convertJsonMessage(String message, String locale) {
return convertMessage(GSON_SERIALIZER.deserialize(message), locale);
}

public static String convertMessage(String message) {
return convertMessage(message, GeyserLocale.getDefaultLocale());
public static String convertJsonMessage(String message) {
return convertJsonMessage(message, GeyserLocale.getDefaultLocale());
}

public static String convertMessage(Component message) {
return convertMessage(message, GeyserLocale.getDefaultLocale());
}

/**
* Verifies the message is valid JSON in case it's plaintext. Works around GsonComponentSeraializer not using lenient mode.
* Verifies the message is valid JSON in case it's plaintext. Works around GsonComponentSerializer not using lenient mode.
* See https://wiki.vg/Chat for messages sent in lenient mode, and for a description on leniency.
*
* @param message Potentially lenient JSON message
Expand All @@ -218,9 +216,10 @@ public static String convertMessageLenient(String message, String locale) {
}

try {
return convertMessage(message, locale);
return convertJsonMessage(message, locale);
} catch (Exception ignored) {
String convertedMessage = convertMessage(convertToJavaMessage(message), locale);
// Use the default legacy serializer since message is java-legacy
String convertedMessage = convertMessage(LegacyComponentSerializer.legacySection().deserialize(message), locale);

// We have to do this since Adventure strips the starting reset character
if (message.startsWith(RESET) && !convertedMessage.startsWith(RESET)) {
Expand All @@ -242,11 +241,10 @@ public static String convertMessageLenient(String message) {
* @return The formatted JSON string
*/
public static String convertToJavaMessage(String message) {
Component component = LegacyComponentSerializer.legacySection().deserialize(message);
Component component = BEDROCK_SERIALIZER.deserialize(message);
return GSON_SERIALIZER.serialize(component);
}


/**
* Convert legacy format message to plain text
*
Expand Down Expand Up @@ -275,7 +273,7 @@ public static String convertToPlainText(String message) {
* @param locale Locale to use for translation strings
* @return The plain text of the message
*/
public static String convertToPlainText(String message, String locale) {
public static String convertToPlainTextLenient(String message, String locale) {
if (message == null) {
return "";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class MessageTranslatorTest {

private Map<String, String> messages = new HashMap<>();
private final Map<String, String> messages = new HashMap<>();

@BeforeAll
public void setUp() throws Exception {
Expand Down Expand Up @@ -70,7 +70,7 @@ public void setUp() throws Exception {
@Test
public void convertMessage() {
for (Map.Entry<String, String> entry : messages.entrySet()) {
String bedrockMessage = MessageTranslator.convertMessage(entry.getKey(), "en_US");
String bedrockMessage = MessageTranslator.convertJsonMessage(entry.getKey(), "en_US");
Assertions.assertEquals(entry.getValue(), bedrockMessage, "Translation of messages is incorrect");
}
}
Expand All @@ -85,13 +85,13 @@ public void convertMessageLenient() {

@Test
public void convertToPlainText() {
Assertions.assertEquals("Many colors here", MessageTranslator.convertToPlainText("{\"extra\":[{\"color\":\"red\",\"text\":\"M\"},{\"color\":\"gold\",\"text\":\"a\"},{\"color\":\"yellow\",\"text\":\"n\"},{\"color\":\"green\",\"text\":\"y \"},{\"color\":\"aqua\",\"text\":\"c\"},{\"color\":\"dark_purple\",\"text\":\"o\"},{\"color\":\"red\",\"text\":\"l\"},{\"color\":\"gold\",\"text\":\"o\"},{\"color\":\"yellow\",\"text\":\"r\"},{\"color\":\"green\",\"text\":\"s \"},{\"color\":\"aqua\",\"text\":\"h\"},{\"color\":\"dark_purple\",\"text\":\"e\"},{\"color\":\"red\",\"text\":\"r\"},{\"color\":\"gold\",\"text\":\"e\"}],\"text\":\"\"}", "en_US"), "JSON message is not handled properly");
Assertions.assertEquals("Many colors here", MessageTranslator.convertToPlainTextLenient("{\"extra\":[{\"color\":\"red\",\"text\":\"M\"},{\"color\":\"gold\",\"text\":\"a\"},{\"color\":\"yellow\",\"text\":\"n\"},{\"color\":\"green\",\"text\":\"y \"},{\"color\":\"aqua\",\"text\":\"c\"},{\"color\":\"dark_purple\",\"text\":\"o\"},{\"color\":\"red\",\"text\":\"l\"},{\"color\":\"gold\",\"text\":\"o\"},{\"color\":\"yellow\",\"text\":\"r\"},{\"color\":\"green\",\"text\":\"s \"},{\"color\":\"aqua\",\"text\":\"h\"},{\"color\":\"dark_purple\",\"text\":\"e\"},{\"color\":\"red\",\"text\":\"r\"},{\"color\":\"gold\",\"text\":\"e\"}],\"text\":\"\"}", "en_US"), "JSON message is not handled properly");
Assertions.assertEquals("Many colors here", MessageTranslator.convertToPlainText("§cM§6a§en§ay §bc§5o§cl§6o§er§as §bh§5e§cr§6e"), "Legacy formatted message is not handled properly (Colors)");
Assertions.assertEquals("Many colors here", MessageTranslator.convertToPlainText("§cM§6a§en§ay §bc§5o§cl§6o§er§as §bh§5e§cr§6e", "en_US"), "Legacy formatted message is not handled properly (Colors)");
Assertions.assertEquals("Obf Bold Strikethrough Underline Italic Reset", MessageTranslator.convertToPlainText("§kObf §lBold §mStrikethrough §nUnderline §oItalic §rReset", "en_US"), "Legacy formatted message is not handled properly (Style)");
Assertions.assertEquals("Strange", MessageTranslator.convertToPlainText("§rStrange", "en_US"), "Valid lenient JSON is not handled properly");
Assertions.assertEquals("", MessageTranslator.convertToPlainText("", "en_US"), "Empty message is not handled properly");
Assertions.assertEquals(" ", MessageTranslator.convertToPlainText(" ", "en_US"), "Whitespace is not preserved");
Assertions.assertEquals("Many colors here", MessageTranslator.convertToPlainTextLenient("§cM§6a§en§ay §bc§5o§cl§6o§er§as §bh§5e§cr§6e", "en_US"), "Legacy formatted message is not handled properly (Colors)");
Assertions.assertEquals("Obf Bold Strikethrough Underline Italic Reset", MessageTranslator.convertToPlainTextLenient("§kObf §lBold §mStrikethrough §nUnderline §oItalic §rReset", "en_US"), "Legacy formatted message is not handled properly (Style)");
Assertions.assertEquals("Strange", MessageTranslator.convertToPlainTextLenient("§rStrange", "en_US"), "Valid lenient JSON is not handled properly");
Assertions.assertEquals("", MessageTranslator.convertToPlainTextLenient("", "en_US"), "Empty message is not handled properly");
Assertions.assertEquals(" ", MessageTranslator.convertToPlainTextLenient(" ", "en_US"), "Whitespace is not preserved");
}

@Test
Expand Down
6 changes: 3 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ protocol-connection = "3.0.0.Beta1-20230718.000033-101"
raknet = "1.0.0.CR1-20230703.195238-9"
mcauthlib = "d9d773e"
mcprotocollib = "1.20-1-20230607.135651-6" # Temporary hack - needs to be updated to release once publishing is fixed
adventure = "4.14.0-20230424.215040-7"
adventure-platform = "4.1.2"
adventure = "4.14.0"
adventure-platform = "4.3.0"
junit = "5.9.2"
checkerframework = "3.19.0"
log4j = "2.20.0"
Expand All @@ -26,7 +26,7 @@ viaversion = "4.0.0"
adapters = "1.9-SNAPSHOT"
commodore = "2.2"
bungeecord = "a7c6ede"
velocity = "3.0.0"
velocity = "3.1.1"
sponge = "8.0.0"
fabric-minecraft = "1.20"
fabric-loader = "0.14.21"
Expand Down