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 <toy.fighter1@gmail.com>
This commit is contained in:
rtm516 2020-11-16 23:57:57 +00:00 committed by GitHub
parent 47f25f1205
commit 512f8cd6c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 583 additions and 601 deletions

View File

@ -143,17 +143,23 @@
<scope>compile</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<groupId>com.github.kyoripowered.adventure</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>4.1.1</version>
<version>4d8a67d798</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<groupId>com.github.kyoripowered.adventure</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>4.1.1</version>
<version>0599048</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
@ -283,6 +289,15 @@
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.0</version>
<configuration>
<!-- Force the right file encoding during unit testing -->
<argLine>-Dfile.encoding=${project.build.sourceEncoding}</argLine>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -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);
}

View File

@ -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

View File

@ -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);

View File

@ -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.

View File

@ -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();

View File

@ -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

View File

@ -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);
}

View File

@ -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<CommandRequestPacket> {
@ -48,7 +48,7 @@ public class BedrockCommandRequestTranslator extends PacketTranslator<CommandReq
} else {
String message = packet.getCommand().trim();
if (MessageUtils.isTooLong(message, session)) {
if (MessageTranslator.isTooLong(message, session)) {
return;
}

View File

@ -31,7 +31,7 @@ import org.geysermc.connector.network.translators.Translator;
import com.github.steveice10.mc.protocol.packet.ingame.client.ClientChatPacket;
import com.nukkitx.protocol.bedrock.packet.TextPacket;
import org.geysermc.connector.utils.MessageUtils;
import org.geysermc.connector.network.translators.chat.MessageTranslator;
@Translator(packet = TextPacket.class)
public class BedrockTextTranslator extends PacketTranslator<TextPacket> {
@ -40,7 +40,7 @@ public class BedrockTextTranslator extends PacketTranslator<TextPacket> {
public void translate(TextPacket packet, GeyserSession session) {
String message = packet.getMessage().replaceAll("^\\.", "/").trim();
if (MessageUtils.isTooLong(message, session)) {
if (MessageTranslator.isTooLong(message, session)) {
return;
}

View File

@ -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<Locale> RENDERER = TranslatableComponentRenderer.usingTranslationSource(REGISTRY);
// Store team colors for player names
private static final Map<TeamColor, String> 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;
}
}

View File

@ -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) {
}
}

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -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");

View File

@ -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<ServerChatPacket> {
@ -59,21 +56,8 @@ public class JavaChatTranslator extends PacketTranslator<ServerChatPacket> {
break;
}
String locale = session.getLocale();
if (packet.getMessage() instanceof TranslationMessage) {
textPacket.setType(TextPacket.Type.TRANSLATION);
textPacket.setNeedsTranslation(true);
List<String> 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);
}

View File

@ -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<ServerDisconnectPacket> {
@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()));
}
}

View File

@ -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<LoginDisconnectPacket> {
@ -37,6 +37,6 @@ public class JavaLoginDisconnectTranslator extends PacketTranslator<LoginDisconn
@Override
public void translate(LoginDisconnectPacket packet, GeyserSession session) {
// The client doesn't manually get disconnected so we have to do it ourselves
session.disconnect(MessageUtils.getTranslatedBedrockMessage(packet.getReason(), session.getLocale()));
session.disconnect(MessageTranslator.convertMessage(packet.getReason().toString(), session.getLocale()));
}
}

View File

@ -28,7 +28,7 @@ package org.geysermc.connector.network.translators.java;
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;
import com.github.steveice10.mc.protocol.packet.ingame.server.ServerTitlePacket;
import com.nukkitx.protocol.bedrock.packet.SetTitlePacket;
@ -41,14 +41,21 @@ public class JavaTitleTranslator extends PacketTranslator<ServerTitlePacket> {
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<ServerTitlePacket> {
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());

View File

@ -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<ServerSc
switch (packet.getAction()) {
case ADD:
case UPDATE:
objective.setDisplayName(MessageUtils.getBedrockMessage(packet.getDisplayName()))
objective.setDisplayName(MessageTranslator.convertMessage(packet.getDisplayName().toString()))
.setType(packet.getType().ordinal());
break;
case REMOVE:

View File

@ -37,7 +37,7 @@ import org.geysermc.connector.scoreboard.ScoreboardUpdater;
import org.geysermc.connector.scoreboard.Team;
import org.geysermc.connector.scoreboard.UpdateType;
import org.geysermc.connector.utils.LanguageUtils;
import org.geysermc.connector.utils.MessageUtils;
import org.geysermc.connector.network.translators.chat.MessageTranslator;
import java.util.Arrays;
import java.util.Set;
@ -59,11 +59,11 @@ public class JavaTeamTranslator extends PacketTranslator<ServerTeamPacket> {
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<ServerTeamPacket> {
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:

View File

@ -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<ServerOpenWindowPacket> {
@ -57,8 +56,7 @@ public class JavaOpenWindowTranslator extends PacketTranslator<ServerOpenWindowP
return;
}
String name = MessageUtils.getTranslatedBedrockMessage(MessageSerializer.fromString(packet.getName()),
session.getLocale());
String name = MessageTranslator.convertMessageLenient(packet.getName(), session.getLocale());
name = LocaleUtils.getLocaleString(name, session.getLocale());

View File

@ -28,7 +28,7 @@ package org.geysermc.connector.network.translators.world.block.entity;
import com.github.steveice10.opennbt.tag.builtin.*;
import com.nukkitx.nbt.NbtMapBuilder;
import org.geysermc.connector.network.translators.world.block.BlockStateValues;
import org.geysermc.connector.utils.MessageUtils;
import org.geysermc.connector.network.translators.chat.MessageTranslator;
@BlockEntity(name = "CommandBlock", regex = "command_block")
public class CommandBlockBlockEntityTranslator extends BlockEntityTranslator implements RequiresBlockState {
@ -42,7 +42,7 @@ public class CommandBlockBlockEntityTranslator extends BlockEntityTranslator imp
// Java and Bedrock values
builder.put("conditionMet", ((ByteTag) tag.get("conditionMet")).getValue());
builder.put("auto", ((ByteTag) tag.get("auto")).getValue());
builder.put("CustomName", MessageUtils.getBedrockMessage(((StringTag) tag.get("CustomName")).getValue()));
builder.put("CustomName", MessageTranslator.convertMessage(((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());

View File

@ -25,10 +25,9 @@
package org.geysermc.connector.network.translators.world.block.entity;
import com.github.steveice10.mc.protocol.data.message.MessageSerializer;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.nukkitx.nbt.NbtMapBuilder;
import org.geysermc.connector.utils.MessageUtils;
import org.geysermc.connector.network.translators.chat.MessageTranslator;
import org.geysermc.connector.utils.SignUtils;
@BlockEntity(name = "Sign", regex = "sign")
@ -36,12 +35,12 @@ public class SignBlockEntityTranslator extends BlockEntityTranslator {
/**
* Maps a color stored in a sign's Color tag to a Bedrock Edition formatting code.
* <br>
* 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());
}
}

View File

@ -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
*

View File

@ -47,7 +47,7 @@ public class LocaleUtils {
private static final Map<String, Asset> 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
}

View File

@ -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<String, Integer> COLORS = new HashMap<>();
private static final Map<TeamColor, String> 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<String> getTranslationParams(List<Message> messages, String locale, Message parent) {
List<String> 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<String> 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<String> 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<String> 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<String, Integer> 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<ChatFormat> 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;
}
}

View File

@ -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<String, String> 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<String, String> 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"));
}
}