From 512f8cd6c2cf002aaf8e3087c361784b8c603fd2 Mon Sep 17 00:00:00 2001 From: rtm516 Date: Mon, 16 Nov 2020 23:57:57 +0000 Subject: [PATCH] Rewrite message handling in MessageUtils to use Adventure (#1498) * Rewrite message handling in MessageUtils to use Adventure * Move to static Adventure commit to fix a bug * Initial test implementation * Add RGB downgrade test * Move MessageUtils and rename * Clean-up and fix tests * Fixed sign and book content handling * Fix blank signs causing NPEs * Fix reset before message being stripped * Add comment about the reset character * Fix legacy style server motds * Fix more messages being handled wrong * Fix title packets being handled wrong * Fix trailing formatting characters on the end of sign lines * Add auto updating of Java locale files * Add en_us locale updating and hash caching * Changes to hash determining Co-authored-by: DoctorMacc --- connector/pom.xml | 23 +- .../entity/CommandBlockMinecartEntity.java | 5 +- .../org/geysermc/connector/entity/Entity.java | 4 +- .../connector/entity/PlayerEntity.java | 6 +- .../network/ConnectorServerEventHandler.java | 5 +- .../connector/network/QueryPacketHandler.java | 5 +- .../network/session/GeyserSession.java | 4 +- .../network/session/cache/BossBar.java | 6 +- .../BedrockCommandRequestTranslator.java | 4 +- .../bedrock/BedrockTextTranslator.java | 4 +- .../translators/chat/MessageTranslator.java | 278 ++++++++++ .../chat/MinecraftTranslationRegistry.java | 81 +++ .../translators/item/ItemTranslator.java | 26 +- .../translators/nbt/BasicItemTranslator.java | 3 +- .../translators/nbt/BookPagesTranslator.java | 6 +- .../translators/java/JavaChatTranslator.java | 22 +- .../java/JavaDisconnectPacket.java | 4 +- .../java/JavaLoginDisconnectTranslator.java | 4 +- .../translators/java/JavaTitleTranslator.java | 16 +- .../JavaScoreboardObjectiveTranslator.java | 4 +- .../java/scoreboard/JavaTeamTranslator.java | 14 +- .../java/window/JavaOpenWindowTranslator.java | 6 +- .../CommandBlockBlockEntityTranslator.java | 4 +- .../entity/SignBlockEntityTranslator.java | 16 +- .../geysermc/connector/utils/FileUtils.java | 21 +- .../geysermc/connector/utils/LocaleUtils.java | 52 +- .../connector/utils/MessageUtils.java | 494 ------------------ .../chat/MessageTranslatorTest.java | 67 +++ 28 files changed, 583 insertions(+), 601 deletions(-) create mode 100644 connector/src/main/java/org/geysermc/connector/network/translators/chat/MessageTranslator.java create mode 100644 connector/src/main/java/org/geysermc/connector/network/translators/chat/MinecraftTranslationRegistry.java delete mode 100644 connector/src/main/java/org/geysermc/connector/utils/MessageUtils.java create mode 100644 connector/src/test/java/org/geysermc/connector/network/translators/chat/MessageTranslatorTest.java diff --git a/connector/pom.xml b/connector/pom.xml index d837f057..db267a71 100644 --- a/connector/pom.xml +++ b/connector/pom.xml @@ -143,17 +143,23 @@ compile - net.kyori + com.github.kyoripowered.adventure adventure-text-serializer-gson - 4.1.1 + 4d8a67d798 compile - net.kyori + com.github.kyoripowered.adventure adventure-text-serializer-legacy - 4.1.1 + 0599048 compile + + junit + junit + 4.13.1 + test + @@ -283,6 +289,15 @@ + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.0 + + + -Dfile.encoding=${project.build.sourceEncoding} + + diff --git a/connector/src/main/java/org/geysermc/connector/entity/CommandBlockMinecartEntity.java b/connector/src/main/java/org/geysermc/connector/entity/CommandBlockMinecartEntity.java index 8cabba64..7d34cc79 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/CommandBlockMinecartEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/CommandBlockMinecartEntity.java @@ -26,13 +26,12 @@ package org.geysermc.connector.entity; import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; -import com.github.steveice10.mc.protocol.data.message.Message; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.protocol.bedrock.data.entity.EntityData; import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.world.block.BlockTranslator; -import org.geysermc.connector.utils.MessageUtils; +import org.geysermc.connector.network.translators.chat.MessageTranslator; public class CommandBlockMinecartEntity extends DefaultBlockMinecartEntity { @@ -51,7 +50,7 @@ public class CommandBlockMinecartEntity extends DefaultBlockMinecartEntity { metadata.put(EntityData.COMMAND_BLOCK_COMMAND, entityMetadata.getValue()); } if (entityMetadata.getId() == 14) { - metadata.put(EntityData.COMMAND_BLOCK_LAST_OUTPUT, MessageUtils.getBedrockMessage((Message) entityMetadata.getValue())); + metadata.put(EntityData.COMMAND_BLOCK_LAST_OUTPUT, MessageTranslator.convertMessage(entityMetadata.getValue().toString())); } super.updateBedrockMetadata(entityMetadata, session); } diff --git a/connector/src/main/java/org/geysermc/connector/entity/Entity.java b/connector/src/main/java/org/geysermc/connector/entity/Entity.java index 20cd2f76..7b1fa1cf 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/Entity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/Entity.java @@ -54,7 +54,7 @@ import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.item.ItemRegistry; import org.geysermc.connector.utils.AttributeUtils; import org.geysermc.connector.utils.ChunkUtils; -import org.geysermc.connector.utils.MessageUtils; +import org.geysermc.connector.network.translators.chat.MessageTranslator; import java.util.ArrayList; import java.util.HashMap; @@ -318,7 +318,7 @@ public class Entity { Message message = (Message) entityMetadata.getValue(); if (message != null) // Always translate even if it's a TextMessage since there could be translatable parameters - metadata.put(EntityData.NAMETAG, MessageUtils.getTranslatedBedrockMessage(message, session.getLocale(), true)); + metadata.put(EntityData.NAMETAG, MessageTranslator.convertMessage(message.toString(), session.getLocale())); } break; case 3: // is custom name visible diff --git a/connector/src/main/java/org/geysermc/connector/entity/PlayerEntity.java b/connector/src/main/java/org/geysermc/connector/entity/PlayerEntity.java index 8eeae473..be65525c 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/PlayerEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/PlayerEntity.java @@ -51,7 +51,7 @@ import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.cache.EntityEffectCache; import org.geysermc.connector.scoreboard.Team; import org.geysermc.connector.utils.AttributeUtils; -import org.geysermc.connector.utils.MessageUtils; +import org.geysermc.connector.network.translators.chat.MessageTranslator; import java.util.ArrayList; import java.util.List; @@ -243,13 +243,13 @@ public class PlayerEntity extends LivingEntity { String username = this.username; TextMessage name = (TextMessage) entityMetadata.getValue(); if (name != null) { - username = MessageUtils.getBedrockMessage(name); + username = MessageTranslator.convertMessage(name.toString()); } Team team = session.getWorldCache().getScoreboard().getTeamFor(username); if (team != null) { String displayName = ""; if (team.isVisibleFor(session.getPlayerEntity().getUsername())) { - displayName = MessageUtils.toChatColor(team.getColor()) + username; + displayName = MessageTranslator.toChatColor(team.getColor()) + username; displayName = team.getCurrentData().getDisplayName(displayName); } metadata.put(EntityData.NAMETAG, displayName); diff --git a/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java b/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java index 9fb4ad9e..150d298c 100644 --- a/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java +++ b/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java @@ -25,7 +25,6 @@ package org.geysermc.connector.network; -import com.github.steveice10.mc.protocol.data.message.MessageSerializer; import com.nukkitx.protocol.bedrock.BedrockPong; import com.nukkitx.protocol.bedrock.BedrockServerEventHandler; import com.nukkitx.protocol.bedrock.BedrockServerSession; @@ -36,7 +35,7 @@ import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.configuration.GeyserConfiguration; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.ping.IGeyserPingPassthrough; -import org.geysermc.connector.utils.MessageUtils; +import org.geysermc.connector.network.translators.chat.MessageTranslator; import org.geysermc.connector.utils.LanguageUtils; import java.net.InetSocketAddress; @@ -76,7 +75,7 @@ public class ConnectorServerEventHandler implements BedrockServerEventHandler { pong.setIpv4Port(config.getBedrock().getPort()); if (config.isPassthroughMotd() && pingInfo != null && pingInfo.getDescription() != null) { - String[] motd = MessageUtils.getBedrockMessage(MessageSerializer.fromString(pingInfo.getDescription())).split("\n"); + String[] motd = MessageTranslator.convertMessageLenient(pingInfo.getDescription()).split("\n"); String mainMotd = motd[0]; // First line of the motd. String subMotd = (motd.length != 1) ? motd[1] : ""; // Second line of the motd if present, otherwise blank. diff --git a/connector/src/main/java/org/geysermc/connector/network/QueryPacketHandler.java b/connector/src/main/java/org/geysermc/connector/network/QueryPacketHandler.java index 7faf36bd..510bba2d 100644 --- a/connector/src/main/java/org/geysermc/connector/network/QueryPacketHandler.java +++ b/connector/src/main/java/org/geysermc/connector/network/QueryPacketHandler.java @@ -25,12 +25,11 @@ package org.geysermc.connector.network; -import com.github.steveice10.mc.protocol.data.message.MessageSerializer; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import org.geysermc.connector.common.ping.GeyserPingInfo; import org.geysermc.connector.GeyserConnector; -import org.geysermc.connector.utils.MessageUtils; +import org.geysermc.connector.network.translators.chat.MessageTranslator; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -148,7 +147,7 @@ public class QueryPacketHandler { } if (connector.getConfig().isPassthroughMotd() && pingInfo != null) { - String[] javaMotd = MessageUtils.getBedrockMessage(MessageSerializer.fromString(pingInfo.getDescription())).split("\n"); + String[] javaMotd = MessageTranslator.convertMessageLenient(pingInfo.getDescription()).split("\n"); motd = javaMotd[0].trim(); // First line of the motd. } else { motd = connector.getConfig().getBedrock().getMotd1(); diff --git a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java index a6085e21..00b48a56 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java @@ -34,7 +34,6 @@ import com.github.steveice10.mc.protocol.data.SubProtocol; import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; import com.github.steveice10.mc.protocol.data.game.statistic.Statistic; import com.github.steveice10.mc.protocol.data.game.window.VillagerTrade; -import com.github.steveice10.mc.protocol.data.message.MessageSerializer; import com.github.steveice10.mc.protocol.packet.handshake.client.HandshakePacket; import com.github.steveice10.mc.protocol.packet.ingame.client.world.ClientTeleportConfirmPacket; import com.github.steveice10.mc.protocol.packet.ingame.server.ServerRespawnPacket; @@ -66,6 +65,7 @@ import org.geysermc.connector.common.AuthType; import org.geysermc.connector.entity.Entity; import org.geysermc.connector.entity.PlayerEntity; import org.geysermc.connector.inventory.PlayerInventory; +import org.geysermc.connector.network.translators.chat.MessageTranslator; import org.geysermc.connector.network.remote.RemoteServer; import org.geysermc.connector.network.session.auth.AuthData; import org.geysermc.connector.network.session.auth.BedrockClientData; @@ -496,7 +496,7 @@ public class GeyserSession implements CommandSender { event.getCause().printStackTrace(); } - upstream.disconnect(MessageUtils.getBedrockMessage(MessageSerializer.fromString(event.getReason()))); + upstream.disconnect(MessageTranslator.convertMessageLenient(event.getReason())); } @Override diff --git a/connector/src/main/java/org/geysermc/connector/network/session/cache/BossBar.java b/connector/src/main/java/org/geysermc/connector/network/session/cache/BossBar.java index fdc609ab..7eadb794 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/cache/BossBar.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/cache/BossBar.java @@ -33,7 +33,7 @@ import com.nukkitx.protocol.bedrock.packet.BossEventPacket; import com.nukkitx.protocol.bedrock.packet.RemoveEntityPacket; import lombok.AllArgsConstructor; import org.geysermc.connector.network.session.GeyserSession; -import org.geysermc.connector.utils.MessageUtils; +import org.geysermc.connector.network.translators.chat.MessageTranslator; @AllArgsConstructor public class BossBar { @@ -58,7 +58,7 @@ public class BossBar { BossEventPacket bossEventPacket = new BossEventPacket(); bossEventPacket.setBossUniqueEntityId(entityId); bossEventPacket.setAction(BossEventPacket.Action.CREATE); - bossEventPacket.setTitle(MessageUtils.getTranslatedBedrockMessage(title, session.getLocale())); + bossEventPacket.setTitle(MessageTranslator.convertMessage(title.toString(), session.getLocale())); bossEventPacket.setHealthPercentage(health); bossEventPacket.setColor(color); //ignored by client bossEventPacket.setOverlay(overlay); @@ -72,7 +72,7 @@ public class BossBar { BossEventPacket bossEventPacket = new BossEventPacket(); bossEventPacket.setBossUniqueEntityId(entityId); bossEventPacket.setAction(BossEventPacket.Action.UPDATE_NAME); - bossEventPacket.setTitle(MessageUtils.getTranslatedBedrockMessage(title, session.getLocale())); + bossEventPacket.setTitle(MessageTranslator.convertMessage(title.toString(), session.getLocale())); session.sendUpstreamPacket(bossEventPacket); } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockCommandRequestTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockCommandRequestTranslator.java index 1f31367c..f572538e 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockCommandRequestTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockCommandRequestTranslator.java @@ -34,7 +34,7 @@ import org.geysermc.connector.network.translators.Translator; import com.github.steveice10.mc.protocol.packet.ingame.client.ClientChatPacket; import com.nukkitx.protocol.bedrock.packet.CommandRequestPacket; -import org.geysermc.connector.utils.MessageUtils; +import org.geysermc.connector.network.translators.chat.MessageTranslator; @Translator(packet = CommandRequestPacket.class) public class BedrockCommandRequestTranslator extends PacketTranslator { @@ -48,7 +48,7 @@ public class BedrockCommandRequestTranslator extends PacketTranslator { @@ -40,7 +40,7 @@ public class BedrockTextTranslator extends PacketTranslator { public void translate(TextPacket packet, GeyserSession session) { String message = packet.getMessage().replaceAll("^\\.", "/").trim(); - if (MessageUtils.isTooLong(message, session)) { + if (MessageTranslator.isTooLong(message, session)) { return; } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/chat/MessageTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/chat/MessageTranslator.java new file mode 100644 index 00000000..be01362f --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/chat/MessageTranslator.java @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2019-2020 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.connector.network.translators.chat; + +import com.github.steveice10.mc.protocol.data.game.scoreboard.TeamColor; +import com.github.steveice10.mc.protocol.data.message.style.ChatColor; +import com.github.steveice10.mc.protocol.data.message.style.ChatFormat; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.renderer.TranslatableComponentRenderer; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import net.kyori.adventure.translation.TranslationRegistry; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.utils.LanguageUtils; + +import java.util.*; + +public class MessageTranslator { + + // These are used for handling the translations of the messages + private static final TranslationRegistry REGISTRY = new MinecraftTranslationRegistry(); + private static final TranslatableComponentRenderer RENDERER = TranslatableComponentRenderer.usingTranslationSource(REGISTRY); + + // Store team colors for player names + private static final Map TEAM_COLORS = new HashMap<>(); + + static { + TEAM_COLORS.put(TeamColor.BLACK, getColor(ChatColor.BLACK)); + TEAM_COLORS.put(TeamColor.DARK_BLUE, getColor(ChatColor.DARK_BLUE)); + TEAM_COLORS.put(TeamColor.DARK_GREEN, getColor(ChatColor.DARK_GREEN)); + TEAM_COLORS.put(TeamColor.DARK_AQUA, getColor(ChatColor.DARK_AQUA)); + TEAM_COLORS.put(TeamColor.DARK_RED, getColor(ChatColor.DARK_RED)); + TEAM_COLORS.put(TeamColor.DARK_PURPLE, getColor(ChatColor.DARK_PURPLE)); + TEAM_COLORS.put(TeamColor.GOLD, getColor(ChatColor.GOLD)); + TEAM_COLORS.put(TeamColor.GRAY, getColor(ChatColor.GRAY)); + TEAM_COLORS.put(TeamColor.DARK_GRAY, getColor(ChatColor.DARK_GRAY)); + TEAM_COLORS.put(TeamColor.BLUE, getColor(ChatColor.BLUE)); + TEAM_COLORS.put(TeamColor.GREEN, getColor(ChatColor.GREEN)); + TEAM_COLORS.put(TeamColor.AQUA, getColor(ChatColor.AQUA)); + TEAM_COLORS.put(TeamColor.RED, getColor(ChatColor.RED)); + TEAM_COLORS.put(TeamColor.LIGHT_PURPLE, getColor(ChatColor.LIGHT_PURPLE)); + TEAM_COLORS.put(TeamColor.YELLOW, getColor(ChatColor.YELLOW)); + TEAM_COLORS.put(TeamColor.WHITE, getColor(ChatColor.WHITE)); + TEAM_COLORS.put(TeamColor.OBFUSCATED, getFormat(ChatFormat.OBFUSCATED)); + TEAM_COLORS.put(TeamColor.BOLD, getFormat(ChatFormat.BOLD)); + TEAM_COLORS.put(TeamColor.STRIKETHROUGH, getFormat(ChatFormat.STRIKETHROUGH)); + TEAM_COLORS.put(TeamColor.ITALIC, getFormat(ChatFormat.ITALIC)); + } + + /** + * Convert a Java message to the legacy format ready for bedrock + * + * @param message Java message + * @param locale Locale to use for translation strings + * @return Parsed and formatted message for bedrock + */ + public static String convertMessage(String message, String locale) { + Component component = GsonComponentSerializer.gson().deserialize(message); + + // Get a Locale from the given locale string + Locale localeCode = Locale.forLanguageTag(locale.replace('_', '-')); + component = RENDERER.render(component, localeCode); + + return LegacyComponentSerializer.legacySection().serialize(component); + } + + public static String convertMessage(String message) { + return convertMessage(message, LanguageUtils.getDefaultLocale()); + } + + /** + * Verifies the message is valid JSON in case it's plaintext. Works around GsonComponentSeraializer 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 + * @param locale Locale to use for translation strings + * @return Bedrock formatted message + */ + public static String convertMessageLenient(String message, String locale) { + if (isMessage(message)) { + return convertMessage(message, locale); + } else { + String convertedMessage = convertMessage(convertToJavaMessage(message), locale); + + // We have to do this since Adventure strips the starting reset character + if (message.startsWith(getColor(ChatColor.RESET))) { + convertedMessage = getColor(ChatColor.RESET) + convertedMessage; + } + + return convertedMessage; + } + } + + public static String convertMessageLenient(String message) { + return convertMessageLenient(message, LanguageUtils.getDefaultLocale()); + } + + /** + * Convert a Bedrock message string back to a format Java can understand + * + * @param message Message to convert + * @return The formatted JSON string + */ + public static String convertToJavaMessage(String message) { + Component component = LegacyComponentSerializer.legacySection().deserialize(message); + return GsonComponentSerializer.gson().serialize(component); + } + + /** + * Checks if the given text string is a JSON message + * + * @param text String to test + * @return True if its a valid message JSON string, false if not + */ + public static boolean isMessage(String text) { + if (text.trim().isEmpty()) { + return false; + } + + try { + GsonComponentSerializer.gson().deserialize(text); + } catch (Exception ex) { + return false; + } + + return true; + } + + /** + * Convert a {@link ChatColor} into a string for inserting into messages + * + * @param color {@link ChatColor} to convert + * @return The converted color string + */ + private static String getColor(String color) { + String base = "\u00a7"; + switch (color) { + case ChatColor.BLACK: + base += "0"; + break; + case ChatColor.DARK_BLUE: + base += "1"; + break; + case ChatColor.DARK_GREEN: + base += "2"; + break; + case ChatColor.DARK_AQUA: + base += "3"; + break; + case ChatColor.DARK_RED: + base += "4"; + break; + case ChatColor.DARK_PURPLE: + base += "5"; + break; + case ChatColor.GOLD: + base += "6"; + break; + case ChatColor.GRAY: + base += "7"; + break; + case ChatColor.DARK_GRAY: + base += "8"; + break; + case ChatColor.BLUE: + base += "9"; + break; + case ChatColor.GREEN: + base += "a"; + break; + case ChatColor.AQUA: + base += "b"; + break; + case ChatColor.RED: + base += "c"; + break; + case ChatColor.LIGHT_PURPLE: + base += "d"; + break; + case ChatColor.YELLOW: + base += "e"; + break; + case ChatColor.WHITE: + base += "f"; + break; + case ChatColor.RESET: + base += "r"; + break; + default: + return ""; + } + + return base; + } + + /** + * Convert a {@link ChatFormat} into a string for inserting into messages + * + * @param format {@link ChatFormat} to convert + * @return The converted chat formatting string + */ + private static String getFormat(ChatFormat format) { + StringBuilder str = new StringBuilder(); + String base = "\u00a7"; + switch (format) { + case OBFUSCATED: + base += "k"; + break; + case BOLD: + base += "l"; + break; + case STRIKETHROUGH: + base += "m"; + break; + case UNDERLINED: + base += "n"; + break; + case ITALIC: + base += "o"; + break; + default: + return ""; + } + + str.append(base); + + return str.toString(); + } + + /** + * Convert a team color to a chat color + * + * @param teamColor + * @return The chat color character + */ + public static String toChatColor(TeamColor teamColor) { + return TEAM_COLORS.getOrDefault(teamColor, ""); + } + + /** + * Checks if the given message is over 256 characters (Java edition server chat limit) and sends a message to the user if it is + * + * @param message Message to check + * @param session {@link GeyserSession} for the user + * @return True if the message is too long, false if not + */ + public static boolean isTooLong(String message, GeyserSession session) { + if (message.length() > 256) { + session.sendMessage(LanguageUtils.getPlayerLocaleString("geyser.chat.too_long", session.getLocale(), message.length())); + return true; + } + + return false; + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/chat/MinecraftTranslationRegistry.java b/connector/src/main/java/org/geysermc/connector/network/translators/chat/MinecraftTranslationRegistry.java new file mode 100644 index 00000000..a23167ac --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/chat/MinecraftTranslationRegistry.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2019-2020 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.connector.network.translators.chat; + +import net.kyori.adventure.key.Key; +import net.kyori.adventure.translation.TranslationRegistry; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.connector.utils.LocaleUtils; + +import java.text.MessageFormat; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * This class is used for mapping a translation key with the already loaded Java locale data + * Used in MessageTranslator.java as part of the KyoriPowered/Adventure library + */ +public class MinecraftTranslationRegistry implements TranslationRegistry { + @Override + public @NonNull Key name() { + return Key.key("", ""); + } + + @Override + public @Nullable MessageFormat translate(@NonNull String key, @NonNull Locale locale) { + // Get the locale string + String localeString = LocaleUtils.getLocaleString(key, locale.toString()); + + // Replace the `%s` with numbered inserts `{0}` + Pattern p = Pattern.compile("%s"); + Matcher m = p.matcher(localeString); + StringBuffer sb = new StringBuffer(); + int i = 0; + while (m.find()) { + m.appendReplacement(sb, "{" + (i++) + "}"); + } + m.appendTail(sb); + + return new MessageFormat(sb.toString(), locale); + } + + @Override + public void defaultLocale(@NonNull Locale locale) { + + } + + @Override + public void register(@NonNull String key, @NonNull Locale locale, @NonNull MessageFormat format) { + + } + + @Override + public void unregister(@NonNull String key) { + + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemTranslator.java index 55db9a25..00c9138a 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemTranslator.java @@ -26,7 +26,6 @@ package org.geysermc.connector.network.translators.item; import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; -import com.github.steveice10.mc.protocol.data.message.MessageSerializer; import com.github.steveice10.opennbt.tag.builtin.*; import com.nukkitx.nbt.NbtList; import com.nukkitx.nbt.NbtMap; @@ -44,7 +43,7 @@ import org.geysermc.connector.network.translators.ItemRemapper; import org.geysermc.connector.network.translators.world.block.BlockTranslator; import org.geysermc.connector.utils.FileUtils; import org.geysermc.connector.utils.LanguageUtils; -import org.geysermc.connector.utils.MessageUtils; +import org.geysermc.connector.network.translators.chat.MessageTranslator; import org.reflections.Reflections; import java.util.*; @@ -385,26 +384,17 @@ public abstract class ItemTranslator { public static void translateDisplayProperties(GeyserSession session, CompoundTag tag) { if (tag != null) { CompoundTag display = tag.get("display"); - if (display != null && !display.isEmpty() && display.contains("Name")) { + if (display != null && display.contains("Name")) { String name = ((StringTag) display.get("Name")).getValue(); - // If its not a message convert it - if (!MessageUtils.isMessage(name)) { - Component component = LegacyComponentSerializer.legacySection().deserialize(name); - name = GsonComponentSerializer.gson().serialize(component); - } + // Get the translated name and prefix it with a reset char + name = MessageTranslator.convertMessageLenient(name, session.getLocale()); - // Check if its a message to translate - if (MessageUtils.isMessage(name)) { - // Get the translated name - name = MessageUtils.getTranslatedBedrockMessage(MessageSerializer.fromString(name), session.getLocale()); + // Add the new name tag + display.put(new StringTag("Name", name)); - // Add the new name tag - display.put(new StringTag("Name", name)); - - // Add to the new root tag - tag.put(display); - } + // Add to the new root tag + tag.put(display); } } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/BasicItemTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/BasicItemTranslator.java index 1d21bbfb..3fd9df8a 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/BasicItemTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/BasicItemTranslator.java @@ -37,7 +37,6 @@ import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.ItemRemapper; import org.geysermc.connector.network.translators.item.ItemEntry; import org.geysermc.connector.network.translators.item.NbtItemStackTranslator; -import org.geysermc.connector.utils.MessageUtils; import java.util.ArrayList; import java.util.List; @@ -108,7 +107,7 @@ public class BasicItemTranslator extends NbtItemStackTranslator { private String toBedrockMessage(StringTag tag) { String message = tag.getValue(); if (message == null) return null; - TextComponent component = (TextComponent) MessageUtils.phraseJavaMessage(message); + TextComponent component = (TextComponent) GsonComponentSerializer.gson().deserialize(message); String legacy = LegacyComponentSerializer.legacySection().serialize(component); if (hasFormatting(LegacyComponentSerializer.legacySection().deserialize(legacy))) { return "§r" + legacy; diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/BookPagesTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/BookPagesTranslator.java index 41ee4fbc..294dd81e 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/BookPagesTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/BookPagesTranslator.java @@ -33,7 +33,7 @@ import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.ItemRemapper; import org.geysermc.connector.network.translators.item.NbtItemStackTranslator; import org.geysermc.connector.network.translators.item.ItemEntry; -import org.geysermc.connector.utils.MessageUtils; +import org.geysermc.connector.network.translators.chat.MessageTranslator; import java.util.ArrayList; import java.util.List; @@ -56,7 +56,7 @@ public class BookPagesTranslator extends NbtItemStackTranslator { CompoundTag pageTag = new CompoundTag(""); pageTag.put(new StringTag("photoname", "")); - pageTag.put(new StringTag("text", MessageUtils.getBedrockMessageLenient(textTag.getValue()))); + pageTag.put(new StringTag("text", MessageTranslator.convertMessageLenient(textTag.getValue()))); pages.add(pageTag); } @@ -78,7 +78,7 @@ public class BookPagesTranslator extends NbtItemStackTranslator { CompoundTag pageTag = (CompoundTag) tag; StringTag textTag = pageTag.get("text"); - pages.add(new StringTag(MessageUtils.getJavaMessage(textTag.getValue()))); + pages.add(new StringTag(MessageTranslator.convertToJavaMessage(textTag.getValue()))); } itemTag.remove("pages"); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaChatTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaChatTranslator.java index 186aaf66..f5128ed6 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaChatTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaChatTranslator.java @@ -25,15 +25,12 @@ package org.geysermc.connector.network.translators.java; -import com.github.steveice10.mc.protocol.data.message.TranslationMessage; import com.github.steveice10.mc.protocol.packet.ingame.server.ServerChatPacket; import com.nukkitx.protocol.bedrock.packet.TextPacket; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.Translator; -import org.geysermc.connector.utils.MessageUtils; - -import java.util.List; +import org.geysermc.connector.network.translators.chat.MessageTranslator; @Translator(packet = ServerChatPacket.class) public class JavaChatTranslator extends PacketTranslator { @@ -59,21 +56,8 @@ public class JavaChatTranslator extends PacketTranslator { break; } - String locale = session.getLocale(); - - if (packet.getMessage() instanceof TranslationMessage) { - textPacket.setType(TextPacket.Type.TRANSLATION); - textPacket.setNeedsTranslation(true); - - List paramsTranslated = MessageUtils.getTranslationParams(((TranslationMessage) packet.getMessage()).getWith(), locale, packet.getMessage()); - textPacket.setParameters(paramsTranslated); - - textPacket.setMessage(MessageUtils.insertParams(MessageUtils.getTranslatedBedrockMessage(packet.getMessage(), locale, true, packet.getMessage()), paramsTranslated)); - } else { - textPacket.setNeedsTranslation(false); - - textPacket.setMessage(MessageUtils.getTranslatedBedrockMessage(packet.getMessage(), locale, false, packet.getMessage())); - } + textPacket.setNeedsTranslation(false); + textPacket.setMessage(MessageTranslator.convertMessage(packet.getMessage().toString(), session.getLocale())); session.sendUpstreamPacket(textPacket); } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDisconnectPacket.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDisconnectPacket.java index f36da367..1945a8e1 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDisconnectPacket.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDisconnectPacket.java @@ -29,13 +29,13 @@ import com.github.steveice10.mc.protocol.packet.ingame.server.ServerDisconnectPa import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.Translator; -import org.geysermc.connector.utils.MessageUtils; +import org.geysermc.connector.network.translators.chat.MessageTranslator; @Translator(packet = ServerDisconnectPacket.class) public class JavaDisconnectPacket extends PacketTranslator { @Override public void translate(ServerDisconnectPacket packet, GeyserSession session) { - session.disconnect(MessageUtils.getTranslatedBedrockMessage(packet.getReason(), session.getLocale(), true)); + session.disconnect(MessageTranslator.convertMessage(packet.getReason().toString(), session.getLocale())); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaLoginDisconnectTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaLoginDisconnectTranslator.java index e7486c99..0a1cc3dd 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaLoginDisconnectTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaLoginDisconnectTranslator.java @@ -29,7 +29,7 @@ import com.github.steveice10.mc.protocol.packet.login.server.LoginDisconnectPack import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.Translator; -import org.geysermc.connector.utils.MessageUtils; +import org.geysermc.connector.network.translators.chat.MessageTranslator; @Translator(packet = LoginDisconnectPacket.class) public class JavaLoginDisconnectTranslator extends PacketTranslator { @@ -37,6 +37,6 @@ public class JavaLoginDisconnectTranslator extends PacketTranslator { SetTitlePacket titlePacket = new SetTitlePacket(); String locale = session.getLocale(); + String text; + if (packet.getTitle() == null) { + text = " "; + } else { + text = MessageTranslator.convertMessage(packet.getTitle().toString(), locale); + } + switch (packet.getAction()) { case TITLE: titlePacket.setType(SetTitlePacket.Type.TITLE); - titlePacket.setText(MessageUtils.getTranslatedBedrockMessage(packet.getTitle(), locale)); + titlePacket.setText(text); break; case SUBTITLE: titlePacket.setType(SetTitlePacket.Type.SUBTITLE); - titlePacket.setText(MessageUtils.getTranslatedBedrockMessage(packet.getTitle(), locale)); + titlePacket.setText(text); break; case CLEAR: case RESET: @@ -57,9 +64,10 @@ public class JavaTitleTranslator extends PacketTranslator { break; case ACTION_BAR: titlePacket.setType(SetTitlePacket.Type.ACTIONBAR); - titlePacket.setText(MessageUtils.getTranslatedBedrockMessage(packet.getTitle(), locale)); + titlePacket.setText(text); break; case TIMES: + titlePacket.setType(SetTitlePacket.Type.TIMES); titlePacket.setFadeInTime(packet.getFadeIn()); titlePacket.setFadeOutTime(packet.getFadeOut()); titlePacket.setStayTime(packet.getStay()); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaScoreboardObjectiveTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaScoreboardObjectiveTranslator.java index 31b9d95b..1996f696 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaScoreboardObjectiveTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaScoreboardObjectiveTranslator.java @@ -32,7 +32,7 @@ import org.geysermc.connector.network.translators.Translator; import org.geysermc.connector.scoreboard.Objective; import org.geysermc.connector.scoreboard.Scoreboard; import org.geysermc.connector.scoreboard.ScoreboardUpdater; -import org.geysermc.connector.utils.MessageUtils; +import org.geysermc.connector.network.translators.chat.MessageTranslator; import com.github.steveice10.mc.protocol.data.game.scoreboard.ObjectiveAction; import com.github.steveice10.mc.protocol.packet.ingame.server.scoreboard.ServerScoreboardObjectivePacket; @@ -54,7 +54,7 @@ public class JavaScoreboardObjectiveTranslator extends PacketTranslator { switch (packet.getAction()) { case CREATE: scoreboard.registerNewTeam(packet.getTeamName(), toPlayerSet(packet.getPlayers())) - .setName(MessageUtils.getBedrockMessage(packet.getDisplayName())) + .setName(MessageTranslator.convertMessage(packet.getDisplayName().toString())) .setColor(packet.getColor()) .setNameTagVisibility(packet.getNameTagVisibility()) - .setPrefix(MessageUtils.getTranslatedBedrockMessage(packet.getPrefix(), session.getLocale())) - .setSuffix(MessageUtils.getTranslatedBedrockMessage(packet.getSuffix(), session.getLocale())); + .setPrefix(MessageTranslator.convertMessage(packet.getPrefix().toString(), session.getLocale())) + .setSuffix(MessageTranslator.convertMessage(packet.getSuffix().toString(), session.getLocale())); break; case UPDATE: if (team == null) { @@ -74,11 +74,11 @@ public class JavaTeamTranslator extends PacketTranslator { return; } - team.setName(MessageUtils.getBedrockMessage(packet.getDisplayName())) + team.setName(MessageTranslator.convertMessage(packet.getDisplayName().toString())) .setColor(packet.getColor()) .setNameTagVisibility(packet.getNameTagVisibility()) - .setPrefix(MessageUtils.getTranslatedBedrockMessage(packet.getPrefix(), session.getLocale())) - .setSuffix(MessageUtils.getTranslatedBedrockMessage(packet.getSuffix(), session.getLocale())) + .setPrefix(MessageTranslator.convertMessage(packet.getPrefix().toString(), session.getLocale())) + .setSuffix(MessageTranslator.convertMessage(packet.getSuffix().toString(), session.getLocale())) .setUpdateType(UpdateType.UPDATE); break; case ADD_PLAYER: diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaOpenWindowTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaOpenWindowTranslator.java index 2c10ded6..1fb08871 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaOpenWindowTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaOpenWindowTranslator.java @@ -25,7 +25,6 @@ package org.geysermc.connector.network.translators.java.window; -import com.github.steveice10.mc.protocol.data.message.MessageSerializer; import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientCloseWindowPacket; import com.github.steveice10.mc.protocol.packet.ingame.server.window.ServerOpenWindowPacket; import org.geysermc.connector.inventory.Inventory; @@ -35,7 +34,7 @@ import org.geysermc.connector.network.translators.Translator; import org.geysermc.connector.network.translators.inventory.InventoryTranslator; import org.geysermc.connector.utils.InventoryUtils; import org.geysermc.connector.utils.LocaleUtils; -import org.geysermc.connector.utils.MessageUtils; +import org.geysermc.connector.network.translators.chat.MessageTranslator; @Translator(packet = ServerOpenWindowPacket.class) public class JavaOpenWindowTranslator extends PacketTranslator { @@ -57,8 +56,7 @@ public class JavaOpenWindowTranslator extends PacketTranslator - * The color names correspond to dye names, because of this we can't use {@link MessageUtils#getColor(String)}. + * The color names correspond to dye names, because of this we can't use {@link MessageTranslator#getColor(String)}. * * @param javaColor The dye color stored in the sign's Color tag. * @return A Bedrock Edition formatting code for valid dye colors, otherwise an empty string. */ - private static String getBedrockSignColor(String javaColor) { + private String getBedrockSignColor(String javaColor) { String base = "\u00a7"; switch (javaColor) { case "white": @@ -100,7 +99,12 @@ public class SignBlockEntityTranslator extends BlockEntityTranslator { for (int i = 0; i < 4; i++) { int currentLine = i + 1; String signLine = getOrDefault(tag.getValue().get("Text" + currentLine), ""); - signLine = MessageUtils.getBedrockMessage(MessageSerializer.fromString(signLine)); + signLine = MessageTranslator.convertMessageLenient(signLine); + + // Trim any trailing formatting codes + if (signLine.length() > 2 && signLine.toCharArray()[signLine.length() - 2] == '\u00a7') { + signLine = signLine.substring(0, signLine.length() - 2); + } // Check the character width on the sign to ensure there is no overflow that is usually hidden // to Java Edition clients but will appear to Bedrock clients @@ -124,6 +128,6 @@ public class SignBlockEntityTranslator extends BlockEntityTranslator { signText.append("\n"); } - builder.put("Text", MessageUtils.getBedrockMessage(MessageSerializer.fromString(signText.toString()))); + builder.put("Text", signText.toString()); } } diff --git a/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java b/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java index 63255cfa..0b2b132a 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java @@ -159,7 +159,8 @@ public class FileUtils { } /** - * Calculate the SHA256 hash of the resource pack file + * Calculate the SHA256 hash of a file + * * @param file File to calculate the hash for * @return A byte[] representation of the hash */ @@ -175,6 +176,24 @@ public class FileUtils { return sha256; } + /** + * Calculate the SHA1 hash of a file + * + * @param file File to calculate the hash for + * @return A byte[] representation of the hash + */ + public static byte[] calculateSHA1(File file) { + byte[] sha1; + + try { + sha1 = MessageDigest.getInstance("SHA-1").digest(Files.readAllBytes(file.toPath())); + } catch (Exception e) { + throw new RuntimeException("Could not calculate pack hash", e); + } + + return sha1; + } + /** * Get the stored reflection data for a given path * diff --git a/connector/src/main/java/org/geysermc/connector/utils/LocaleUtils.java b/connector/src/main/java/org/geysermc/connector/utils/LocaleUtils.java index dfde21b3..4e9e4b00 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/LocaleUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/LocaleUtils.java @@ -47,7 +47,7 @@ public class LocaleUtils { private static final Map ASSET_MAP = new HashMap<>(); - private static String smallestURL = ""; + private static VersionDownload clientJarInfo; static { // Create the locales folder @@ -87,9 +87,8 @@ public class LocaleUtils { // Get the client jar for use when downloading the en_us locale GeyserConnector.getInstance().getLogger().debug(GeyserConnector.JSON_MAPPER.writeValueAsString(versionInfo.getDownloads())); - VersionDownload download = versionInfo.getDownloads().get("client"); - GeyserConnector.getInstance().getLogger().debug(GeyserConnector.JSON_MAPPER.writeValueAsString(download)); - smallestURL = download.getUrl(); + clientJarInfo = versionInfo.getDownloads().get("client"); + GeyserConnector.getInstance().getLogger().debug(GeyserConnector.JSON_MAPPER.writeValueAsString(clientJarInfo)); // Get the assets list JsonNode assets = GeyserConnector.JSON_MAPPER.readTree(WebUtils.getBody(versionInfo.getAssetIndex().getUrl())).get("objects"); @@ -136,8 +135,28 @@ public class LocaleUtils { // Check if we have already downloaded the locale file if (localeFile.exists()) { - GeyserConnector.getInstance().getLogger().debug("Locale already downloaded: " + locale); - return; + String curHash = ""; + String targetHash = ""; + + if (locale.equals("en_us")) { + try { + Path hashFile = localeFile.getParentFile().toPath().resolve("en_us.hash"); + if (hashFile.toFile().exists()) { + curHash = String.join("", Files.readAllLines(hashFile)); + } + } catch (IOException ignored) { } + targetHash = clientJarInfo.getSha1(); + } else { + curHash = byteArrayToHexString(FileUtils.calculateSHA1(localeFile)); + targetHash = ASSET_MAP.get("minecraft/lang/" + locale + ".json").getHash(); + } + + if (!curHash.equals(targetHash)) { + GeyserConnector.getInstance().getLogger().debug("Locale out of date; re-downloading: " + locale); + } else { + GeyserConnector.getInstance().getLogger().debug("Locale already downloaded and up-to date: " + locale); + return; + } } // Create the en_us locale @@ -202,11 +221,11 @@ public class LocaleUtils { try { // Let the user know we are downloading the JAR GeyserConnector.getInstance().getLogger().info(LanguageUtils.getLocaleStringLog("geyser.locale.download.en_us")); - GeyserConnector.getInstance().getLogger().debug("Download URL: " + smallestURL); + GeyserConnector.getInstance().getLogger().debug("Download URL: " + clientJarInfo.getUrl()); // Download the smallest JAR (client or server) Path tmpFilePath = GeyserConnector.getInstance().getBootstrap().getConfigFolder().resolve("tmp_locale.jar"); - WebUtils.downloadFile(smallestURL, tmpFilePath.toString()); + WebUtils.downloadFile(clientJarInfo.getUrl(), tmpFilePath.toString()); // Load in the JAR as a zip and extract the file ZipFile localeJar = new ZipFile(tmpFilePath.toString()); @@ -227,6 +246,9 @@ public class LocaleUtils { fileStream.close(); localeJar.close(); + // Store the latest jar hash + FileUtils.writeFile(localeFile.getParentFile().toPath().resolve("en_us.hash").toString(), clientJarInfo.getSha1().toCharArray()); + // Delete the nolonger needed client/server jar Files.delete(tmpFilePath); } catch (Exception e) { @@ -255,6 +277,20 @@ public class LocaleUtils { return localeStrings.getOrDefault(messageText, messageText); } + /** + * Convert a byte array into a hex string + * + * @param b Byte array to convert + * @return The hex representation of the given byte array + */ + private static String byteArrayToHexString(byte[] b) { + StringBuilder result = new StringBuilder(); + for (byte value : b) { + result.append(Integer.toString((value & 0xff) + 0x100, 16).substring(1)); + } + return result.toString(); + } + public static void init() { // no-op } diff --git a/connector/src/main/java/org/geysermc/connector/utils/MessageUtils.java b/connector/src/main/java/org/geysermc/connector/utils/MessageUtils.java deleted file mode 100644 index b5a2bfdc..00000000 --- a/connector/src/main/java/org/geysermc/connector/utils/MessageUtils.java +++ /dev/null @@ -1,494 +0,0 @@ -/* - * Copyright (c) 2019-2020 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Geyser - */ - -package org.geysermc.connector.utils; - -import com.github.steveice10.mc.protocol.data.game.scoreboard.TeamColor; -import com.github.steveice10.mc.protocol.data.message.Message; -import com.github.steveice10.mc.protocol.data.message.MessageSerializer; -import com.github.steveice10.mc.protocol.data.message.TextMessage; -import com.github.steveice10.mc.protocol.data.message.TranslationMessage; -import com.github.steveice10.mc.protocol.data.message.style.ChatColor; -import com.github.steveice10.mc.protocol.data.message.style.ChatFormat; -import com.github.steveice10.mc.protocol.data.message.style.MessageStyle; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; -import org.geysermc.connector.network.session.GeyserSession; - -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class MessageUtils { - - private static final Map COLORS = new HashMap<>(); - private static final Map TEAM_COLORS = new HashMap<>(); - - static { - COLORS.put(ChatColor.BLACK, 0x000000); - COLORS.put(ChatColor.DARK_BLUE, 0x0000aa); - COLORS.put(ChatColor.DARK_GREEN, 0x00aa00); - COLORS.put(ChatColor.DARK_AQUA, 0x00aaaa); - COLORS.put(ChatColor.DARK_RED, 0xaa0000); - COLORS.put(ChatColor.DARK_PURPLE, 0xaa00aa); - COLORS.put(ChatColor.GOLD, 0xffaa00); - COLORS.put(ChatColor.GRAY, 0xaaaaaa); - COLORS.put(ChatColor.DARK_GRAY, 0x555555); - COLORS.put(ChatColor.BLUE, 0x5555ff); - COLORS.put(ChatColor.GREEN, 0x55ff55); - COLORS.put(ChatColor.AQUA, 0x55ffff); - COLORS.put(ChatColor.RED, 0xff5555); - COLORS.put(ChatColor.LIGHT_PURPLE, 0xff55ff); - COLORS.put(ChatColor.YELLOW, 0xffff55); - COLORS.put(ChatColor.WHITE, 0xffffff); - - TEAM_COLORS.put(TeamColor.BLACK, getColor(ChatColor.BLACK)); - TEAM_COLORS.put(TeamColor.DARK_BLUE, getColor(ChatColor.DARK_BLUE)); - TEAM_COLORS.put(TeamColor.DARK_GREEN, getColor(ChatColor.DARK_GREEN)); - TEAM_COLORS.put(TeamColor.DARK_AQUA, getColor(ChatColor.DARK_AQUA)); - TEAM_COLORS.put(TeamColor.DARK_RED, getColor(ChatColor.DARK_RED)); - TEAM_COLORS.put(TeamColor.DARK_PURPLE, getColor(ChatColor.DARK_PURPLE)); - TEAM_COLORS.put(TeamColor.GOLD, getColor(ChatColor.GOLD)); - TEAM_COLORS.put(TeamColor.GRAY, getColor(ChatColor.GRAY)); - TEAM_COLORS.put(TeamColor.DARK_GRAY, getColor(ChatColor.DARK_GRAY)); - TEAM_COLORS.put(TeamColor.BLUE, getColor(ChatColor.BLUE)); - TEAM_COLORS.put(TeamColor.GREEN, getColor(ChatColor.GREEN)); - TEAM_COLORS.put(TeamColor.AQUA, getColor(ChatColor.AQUA)); - TEAM_COLORS.put(TeamColor.RED, getColor(ChatColor.RED)); - TEAM_COLORS.put(TeamColor.LIGHT_PURPLE, getColor(ChatColor.LIGHT_PURPLE)); - TEAM_COLORS.put(TeamColor.YELLOW, getColor(ChatColor.YELLOW)); - TEAM_COLORS.put(TeamColor.WHITE, getColor(ChatColor.WHITE)); - TEAM_COLORS.put(TeamColor.OBFUSCATED, getFormat(Collections.singletonList(ChatFormat.OBFUSCATED))); - TEAM_COLORS.put(TeamColor.BOLD, getFormat(Collections.singletonList(ChatFormat.BOLD))); - TEAM_COLORS.put(TeamColor.STRIKETHROUGH, getFormat(Collections.singletonList(ChatFormat.STRIKETHROUGH))); - TEAM_COLORS.put(TeamColor.ITALIC, getFormat(Collections.singletonList(ChatFormat.ITALIC))); - } - - /** - * Recursively parse each message from a list for usage in a {@link TranslationMessage} - * - * @param messages A {@link List} of {@link Message} to parse - * @param locale A locale loaded to get the message for - * @param parent A {@link Message} to use as the parent (can be null) - * @return the translation parameters - */ - public static List getTranslationParams(List messages, String locale, Message parent) { - List strings = new ArrayList<>(); - for (Message message : messages) { - message = fixMessageStyle(message, parent); - - if (message instanceof TranslationMessage) { - TranslationMessage translation = (TranslationMessage) message; - - if (locale == null) { - String builder = "%" + translation.getKey(); - strings.add(builder); - } - - // Collect all params and add format corrections to the end of them - List furtherParams = new ArrayList<>(); - for (String param : getTranslationParams(translation.getWith(), locale, message)) { - String newParam = param; - if (parent.getStyle().getFormats().size() != 0) { - newParam += getFormat(parent.getStyle().getFormats()); - } - if (parent.getStyle().getColor() != ChatColor.NONE) { - newParam += getColor(parent.getStyle().getColor()); - } - - furtherParams.add(newParam); - } - - if (locale != null) { - String builder = getFormat(message.getStyle().getFormats()) + - getColor(message.getStyle().getColor()); - builder += insertParams(LocaleUtils.getLocaleString(translation.getKey(), locale), furtherParams); - strings.add(builder); - } else { - String format = getFormat(message.getStyle().getFormats()) + - getColor(message.getStyle().getColor()); - for (String param : furtherParams) { - strings.add(format + param); - } - } - } else { - String builder = getFormat(message.getStyle().getFormats()) + - getColor(message.getStyle().getColor()); - builder += getTranslatedBedrockMessage(message, locale, false, parent); - strings.add(builder); - } - } - - return strings; - } - - public static String getTranslatedBedrockMessage(Message message, String locale) { - return getTranslatedBedrockMessage(message, locale, true); - } - - public static String getTranslatedBedrockMessage(Message message, String locale, boolean shouldTranslate) { - return getTranslatedBedrockMessage(message, locale, shouldTranslate, null); - } - - /** - * Translate a given {@link TranslationMessage} to the given locale - * - * @param message The {@link Message} to send - * @param locale the locale - * @param shouldTranslate if the message should be translated - * @param parent the parent message - * @return the given translation message translated from the given locale - */ - public static String getTranslatedBedrockMessage(Message message, String locale, boolean shouldTranslate, Message parent) { - JsonParser parser = new JsonParser(); - if (isMessage(message.toString())) { - JsonObject object = parser.parse(message.toString()).getAsJsonObject(); - message = MessageSerializer.fromJson(object); - } - - message = fixMessageStyle(message, parent); - - String messageText = (message instanceof TranslationMessage) ? ((TranslationMessage) message).getKey() : ((TextMessage) message).getText(); - if (locale != null && shouldTranslate) { - messageText = LocaleUtils.getLocaleString(messageText, locale); - } - - StringBuilder builder = new StringBuilder(); - builder.append(getFormat(message.getStyle().getFormats())); - builder.append(getColor(message.getStyle().getColor())); - builder.append(messageText); - - for (Message msg : message.getExtra()) { - builder.append(getFormat(msg.getStyle().getFormats())); - builder.append(getColor(msg.getStyle().getColor())); - if (!(msg.toString() == null)) { - boolean isTranslationMessage = (msg instanceof TranslationMessage); - String extraText = ""; - - if (isTranslationMessage) { - List paramsTranslated = getTranslationParams(((TranslationMessage) msg).getWith(), locale, message); - extraText = insertParams(getTranslatedBedrockMessage(msg, locale, isTranslationMessage, message), paramsTranslated); - } else { - extraText = getTranslatedBedrockMessage(msg, locale, isTranslationMessage, message); - } - - builder.append(extraText); - builder.append("\u00a7r"); - } - } - - return builder.toString(); - } - - /** - * If the passed {@link Message} color or format are empty then copy from parent - * - * @param message {@link Message} to update - * @param parent Parent {@link Message} for style - * @return The updated {@link Message} - */ - private static Message fixMessageStyle(Message message, Message parent) { - if (parent == null) { - return message; - } - MessageStyle.Builder styleBuilder = message.getStyle().toBuilder(); - - // Copy color from parent - if (message.getStyle().getColor() == ChatColor.NONE) { - styleBuilder.color(parent.getStyle().getColor()); - } - - // Copy formatting from parent - if (message.getStyle().getFormats().size() == 0) { - styleBuilder.formats(parent.getStyle().getFormats()); - } - - return message.toBuilder().style(styleBuilder.build()).build(); - } - - public static String getBedrockMessage(Message message) { - if (isMessage(((TextMessage) message).getText())) { - return getBedrockMessage(((TextMessage) message).getText()); - } else { - return getBedrockMessage(MessageSerializer.toJsonString(message)); - } - } - - /** - * Verifies the message is valid JSON in case it's plaintext. Works around GsonComponentSeraializer 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 - * @return Bedrock formatted message - */ - public static String getBedrockMessageLenient(String message) { - if (isMessage(message)) { - return getBedrockMessage(message); - } else { - final JsonObject obj = new JsonObject(); - obj.addProperty("text", message); - return getBedrockMessage(obj.toString()); - } - } - - public static String getBedrockMessage(String message) { - Component component = phraseJavaMessage(message); - return LegacyComponentSerializer.legacySection().serialize(component); - } - - public static Component phraseJavaMessage(String message) { - return GsonComponentSerializer.gson().deserialize(message); - } - - public static String getJavaMessage(String message) { - Component component = LegacyComponentSerializer.legacySection().deserialize(message); - return GsonComponentSerializer.gson().serialize(component); - } - - /** - * Inserts the given parameters into the given message both in sequence and as requested - * - * @param message Message containing possible parameter replacement strings - * @param params A list of parameter strings - * @return Parsed message with all params inserted as needed - */ - public static String insertParams(String message, List params) { - String newMessage = message; - - Pattern p = Pattern.compile("%([1-9])\\$s"); - Matcher m = p.matcher(message); - while (m.find()) { - try { - newMessage = newMessage.replaceFirst("%" + m.group(1) + "\\$s", params.get(Integer.parseInt(m.group(1)) - 1)); - } catch (Exception e) { - // Couldn't find the param to replace - } - } - - for (String text : params) { - newMessage = newMessage.replaceFirst("%s", text.replaceAll("%s", "%r")); - } - - newMessage = newMessage.replaceAll("%r", "MISSING!"); - - return newMessage; - } - - /** - * Convert a ChatColor into a string for inserting into messages - * - * @param color ChatColor to convert - * @return The converted color string - */ - private static String getColor(String color) { - String base = "\u00a7"; - switch (color) { - case ChatColor.BLACK: - base += "0"; - break; - case ChatColor.DARK_BLUE: - base += "1"; - break; - case ChatColor.DARK_GREEN: - base += "2"; - break; - case ChatColor.DARK_AQUA: - base += "3"; - break; - case ChatColor.DARK_RED: - base += "4"; - break; - case ChatColor.DARK_PURPLE: - base += "5"; - break; - case ChatColor.GOLD: - base += "6"; - break; - case ChatColor.GRAY: - base += "7"; - break; - case ChatColor.DARK_GRAY: - base += "8"; - break; - case ChatColor.BLUE: - base += "9"; - break; - case ChatColor.GREEN: - base += "a"; - break; - case ChatColor.AQUA: - base += "b"; - break; - case ChatColor.RED: - base += "c"; - break; - case ChatColor.LIGHT_PURPLE: - base += "d"; - break; - case ChatColor.YELLOW: - base += "e"; - break; - case ChatColor.WHITE: - base += "f"; - break; - case ChatColor.RESET: - //case NONE: - base += "r"; - break; - case "": // To stop recursion - return ""; - default: - return getClosestColor(color); - } - - return base; - } - - /** - * Based on https://github.com/ViaVersion/ViaBackwards/blob/master/core/src/main/java/nl/matsv/viabackwards/protocol/protocol1_15_2to1_16/chat/TranslatableRewriter1_16.java - * - * @param color A color string - * @return The closest color to that string - */ - private static String getClosestColor(String color) { - if (!color.startsWith("#")) { - return ""; - } - - int rgb = Integer.parseInt(color.substring(1), 16); - int r = (rgb >> 16) & 0xFF; - int g = (rgb >> 8) & 0xFF; - int b = rgb & 0xFF; - - String closest = null; - int smallestDiff = 0; - - for (Map.Entry testColor : COLORS.entrySet()) { - if (testColor.getValue() == rgb) { - closest = testColor.getKey(); - break; - } - - int testR = (testColor.getValue() >> 16) & 0xFF; - int testG = (testColor.getValue() >> 8) & 0xFF; - int testB = testColor.getValue() & 0xFF; - - // Check by the greatest diff of the 3 values - int rAverage = (testR + r) / 2; - int rDiff = testR - r; - int gDiff = testG - g; - int bDiff = testB - b; - int diff = ((2 + (rAverage >> 8)) * rDiff * rDiff) - + (4 * gDiff * gDiff) - + ((2 + ((255 - rAverage) >> 8)) * bDiff * bDiff); - if (closest == null || diff < smallestDiff) { - closest = testColor.getKey(); - smallestDiff = diff; - } - } - - return getColor(closest); - } - - /** - * Convert a list of ChatFormats into a string for inserting into messages - * - * @param formats ChatFormats to convert - * @return The converted chat formatting string - */ - private static String getFormat(List formats) { - StringBuilder str = new StringBuilder(); - for (ChatFormat cf : formats) { - String base = "\u00a7"; - switch (cf) { - case OBFUSCATED: - base += "k"; - break; - case BOLD: - base += "l"; - break; - case STRIKETHROUGH: - base += "m"; - break; - case UNDERLINED: - base += "n"; - break; - case ITALIC: - base += "o"; - break; - default: - return ""; - } - - str.append(base); - } - - return str.toString(); - } - - /** - * Checks if the given text string is a json message - * - * @param text String to test - * @return True if its a valid message json string, false if not - */ - public static boolean isMessage(String text) { - JsonParser parser = new JsonParser(); - try { - JsonObject object = parser.parse(text).getAsJsonObject(); - try { - MessageSerializer.fromJson(object); - } catch (Exception ex) { - return false; - } - } catch (Exception ex) { - return false; - } - return true; - } - - public static String toChatColor(TeamColor teamColor) { - return TEAM_COLORS.getOrDefault(teamColor, ""); - } - - /** - * Checks if the given message is over 256 characters (Java edition server chat limit) and sends a message to the user if it is - * - * @param message Message to check - * @param session GeyserSession for the user - * @return True if the message is too long, false if not - */ - public static boolean isTooLong(String message, GeyserSession session) { - if (message.length() > 256) { - session.sendMessage(LanguageUtils.getPlayerLocaleString("geyser.chat.too_long", session.getLocale(), message.length())); - return true; - } - - return false; - } -} diff --git a/connector/src/test/java/org/geysermc/connector/network/translators/chat/MessageTranslatorTest.java b/connector/src/test/java/org/geysermc/connector/network/translators/chat/MessageTranslatorTest.java new file mode 100644 index 00000000..5d52c79b --- /dev/null +++ b/connector/src/test/java/org/geysermc/connector/network/translators/chat/MessageTranslatorTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2019-2020 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.connector.network.translators.chat; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +public class MessageTranslatorTest { + + private Map messages = new HashMap<>(); + + @Before + public void setUp() throws Exception { + messages.put("{\"text\":\"\",\"extra\":[{\"text\":\"DoctorMad9952 joined the game\",\"color\":\"yellow\"}]}", + "§eDoctorMad9952 joined the game"); + + messages.put("{\"text\":\"\",\"extra\":[\"Plugins (3): \",{\"text\":\"WorldEdit\",\"color\":\"green\"},{\"text\":\", \",\"color\":\"white\"},{\"text\":\"ViaVersion\",\"color\":\"green\"},{\"text\":\", \",\"color\":\"white\"},{\"text\":\"Geyser-Spigot\",\"color\":\"green\"}]}", + "Plugins (3): §aWorldEdit§f, §aViaVersion§f, §aGeyser-Spigot"); + + // RGB downgrade test + messages.put("{\"extra\":[{\"text\":\" \"},{\"color\":\"gold\",\"text\":\"The \"},{\"color\":\"#E14248\",\"obfuscated\":true,\"text\":\"||\"},{\"color\":\"#3AA9FF\",\"bold\":true,\"text\":\"CubeCraft\"},{\"color\":\"#E14248\",\"obfuscated\":true,\"text\":\"||\"},{\"color\":\"gold\",\"text\":\" Network \"},{\"color\":\"green\",\"text\":\"[1.8/1.9+]\\n \"},{\"color\":\"#f5e342\",\"text\":\"✦ \"},{\"color\":\"#b042f5\",\"bold\":true,\"text\":\"N\"},{\"color\":\"#c142f5\",\"bold\":true,\"text\":\"E\"},{\"color\":\"#d342f5\",\"bold\":true,\"text\":\"W\"},{\"color\":\"#e442f5\",\"bold\":true,\"text\":\":\"},{\"color\":\"#f542f5\",\"bold\":true,\"text\":\" \"},{\"color\":\"#bcf542\",\"bold\":true,\"text\":\"A\"},{\"color\":\"#acee3f\",\"bold\":true,\"text\":\"M\"},{\"color\":\"#9ce73c\",\"bold\":true,\"text\":\"O\"},{\"color\":\"#8ce039\",\"bold\":true,\"text\":\"N\"},{\"color\":\"#7cd936\",\"bold\":true,\"text\":\"G\"},{\"color\":\"#6cd233\",\"bold\":true,\"text\":\" \"},{\"color\":\"#5ccb30\",\"bold\":true,\"text\":\"S\"},{\"color\":\"#4cc42d\",\"bold\":true,\"text\":\"L\"},{\"color\":\"#3cbd2a\",\"bold\":true,\"text\":\"I\"},{\"color\":\"#2cb627\",\"bold\":true,\"text\":\"M\"},{\"color\":\"#1caf24\",\"bold\":true,\"text\":\"E\"},{\"color\":\"#0ca821\",\"bold\":true,\"text\":\"S\"},{\"color\":\"#f5e342\",\"text\":\" \"},{\"color\":\"#6d7c87\",\"text\":\"(kinda sus) \"},{\"color\":\"#f5e342\",\"text\":\"✦\"}],\"text\":\"\"}", + " §6The §c§k||§r§3§lCubeCraft§r§c§k||§r§6 Network §a[1.8/1.9+]\n" + + " §e✦ §d§lN§r§d§lE§r§d§lW§r§d§l:§r§d§l §r§e§lA§r§e§lM§r§a§lO§r§a§lN§r§a§lG§r§a§l §r§a§lS§r§a§lL§r§2§lI§r§2§lM§r§2§lE§r§2§lS§r§e §8(kinda sus) §e✦"); + } + + @Test + public void convertMessage() { + for (Map.Entry entry : messages.entrySet()) { + String bedrockMessage = MessageTranslator.convertMessage(entry.getKey(), "en_US"); + Assert.assertEquals("Translation of messages is incorrect", bedrockMessage, entry.getValue()); + } + } + + @Test + public void convertMessageLenient() { + Assert.assertEquals("All newline message is not handled properly", "\n\n\n\n", MessageTranslator.convertMessageLenient("\n\n\n\n")); + Assert.assertEquals("Empty message is not handled properly", "", MessageTranslator.convertMessageLenient("")); + Assert.assertEquals("Reset before message is not handled properly", "§r§eGame Selector", MessageTranslator.convertMessageLenient("§r§eGame Selector")); + } +}