Add proper text component parsing from NBT (#5029)

* Attempt creating a simple NBT text component parser

* Fix style merging

* Rename TextDecoration to ChatDecoration, use better style deserialization in ChatDecoration

* Remove unused code

* containsKey optimisations, update documentation, improve getStyleFromNbtMap performance slightly, more slight tweaks

* Remove unnecessary deserializeStyle method
This commit is contained in:
Eclipse 2024-09-10 20:10:31 +00:00 committed by GitHub
parent 14cf104cff
commit 73f7259b6d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 105 additions and 40 deletions

View file

@ -69,7 +69,7 @@ public record Enchantment(String identifier,
// TODO - description is a component. So if a hardcoded literal string is given, this will display normally on Java, // TODO - description is a component. So if a hardcoded literal string is given, this will display normally on Java,
// but Geyser will attempt to lookup the literal string as translation - and will fail, displaying an empty string as enchantment name. // but Geyser will attempt to lookup the literal string as translation - and will fail, displaying an empty string as enchantment name.
String description = bedrockEnchantment == null ? MessageTranslator.deserializeDescription(data) : null; String description = bedrockEnchantment == null ? MessageTranslator.deserializeDescription(context.session(), data) : null;
return new Enchantment(context.id().asString(), effects, supportedItems, maxLevel, return new Enchantment(context.id().asString(), effects, supportedItems, maxLevel,
description, anvilCost, exclusiveSet, bedrockEnchantment); description, anvilCost, exclusiveSet, bedrockEnchantment);

View file

@ -44,7 +44,7 @@ public record JukeboxSong(String soundEvent, String description) {
soundEvent = ""; soundEvent = "";
GeyserImpl.getInstance().getLogger().debug("Sound event for " + context.id() + " was of an unexpected type! Expected string or NBT map, got " + soundEventObject); GeyserImpl.getInstance().getLogger().debug("Sound event for " + context.id() + " was of an unexpected type! Expected string or NBT map, got " + soundEventObject);
} }
String description = MessageTranslator.deserializeDescription(data); String description = MessageTranslator.deserializeDescription(context.session(), data);
return new JukeboxSong(soundEvent, description); return new JukeboxSong(soundEvent, description);
} }
} }

View file

@ -49,7 +49,7 @@ import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.registry.JavaRegistry; import org.geysermc.geyser.session.cache.registry.JavaRegistry;
import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; import org.geysermc.geyser.session.cache.registry.RegistryEntryContext;
import org.geysermc.geyser.session.cache.registry.SimpleJavaRegistry; import org.geysermc.geyser.session.cache.registry.SimpleJavaRegistry;
import org.geysermc.geyser.text.TextDecoration; import org.geysermc.geyser.text.ChatDecoration;
import org.geysermc.geyser.translator.level.BiomeTranslator; import org.geysermc.geyser.translator.level.BiomeTranslator;
import org.geysermc.geyser.util.MinecraftKey; import org.geysermc.geyser.util.MinecraftKey;
import org.geysermc.mcprotocollib.protocol.MinecraftProtocol; import org.geysermc.mcprotocollib.protocol.MinecraftProtocol;
@ -78,7 +78,7 @@ public final class RegistryCache {
private static final Map<Key, BiConsumer<RegistryCache, List<RegistryEntry>>> REGISTRIES = new HashMap<>(); private static final Map<Key, BiConsumer<RegistryCache, List<RegistryEntry>>> REGISTRIES = new HashMap<>();
static { static {
register("chat_type", cache -> cache.chatTypes, TextDecoration::readChatType); register("chat_type", cache -> cache.chatTypes, ChatDecoration::readChatType);
register("dimension_type", cache -> cache.dimensions, JavaDimension::read); register("dimension_type", cache -> cache.dimensions, JavaDimension::read);
register("enchantment", cache -> cache.enchantments, Enchantment::read); register("enchantment", cache -> cache.enchantments, Enchantment::read);
register("jukebox_song", cache -> cache.jukeboxSongs, JukeboxSong::read); register("jukebox_song", cache -> cache.jukeboxSongs, JukeboxSong::read);

View file

@ -25,17 +25,19 @@
package org.geysermc.geyser.text; package org.geysermc.geyser.text;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.Style; import net.kyori.adventure.text.format.Style;
import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtType; import org.cloudburstmc.nbt.NbtType;
import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; import org.geysermc.geyser.session.cache.registry.RegistryEntryContext;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.chat.ChatType; import org.geysermc.mcprotocollib.protocol.data.game.chat.ChatType;
import org.geysermc.mcprotocollib.protocol.data.game.chat.ChatTypeDecoration; import org.geysermc.mcprotocollib.protocol.data.game.chat.ChatTypeDecoration;
import java.util.*; import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public record TextDecoration(String translationKey, List<Parameter> parameters, Style deserializedStyle) implements ChatTypeDecoration { public record ChatDecoration(String translationKey, List<Parameter> parameters, Style deserializedStyle) implements ChatTypeDecoration {
@Override @Override
public NbtMap style() { public NbtMap style() {
@ -53,38 +55,22 @@ public record TextDecoration(String translationKey, List<Parameter> parameters,
String translationKey = chat.getString("translation_key"); String translationKey = chat.getString("translation_key");
NbtMap styleTag = chat.getCompound("style"); NbtMap styleTag = chat.getCompound("style");
Style style = deserializeStyle(styleTag); Style style = MessageTranslator.getStyleFromNbtMap(styleTag);
List<ChatTypeDecoration.Parameter> parameters = new ArrayList<>(); List<ChatTypeDecoration.Parameter> parameters = new ArrayList<>();
List<String> parametersNbt = chat.getList("parameters", NbtType.STRING); List<String> parametersNbt = chat.getList("parameters", NbtType.STRING);
for (String parameter : parametersNbt) { for (String parameter : parametersNbt) {
parameters.add(ChatTypeDecoration.Parameter.valueOf(parameter.toUpperCase(Locale.ROOT))); parameters.add(ChatTypeDecoration.Parameter.valueOf(parameter.toUpperCase(Locale.ROOT)));
} }
return new ChatType(new TextDecoration(translationKey, parameters, style), null); return new ChatType(new ChatDecoration(translationKey, parameters, style), null);
} }
return new ChatType(null, null); return new ChatType(null, null);
} }
public static Style getStyle(ChatTypeDecoration decoration) { public static Style getStyle(ChatTypeDecoration decoration) {
if (decoration instanceof TextDecoration textDecoration) { if (decoration instanceof ChatDecoration chatDecoration) {
return textDecoration.deserializedStyle(); return chatDecoration.deserializedStyle();
} }
return deserializeStyle(decoration.style()); return MessageTranslator.getStyleFromNbtMap(decoration.style());
}
private static Style deserializeStyle(NbtMap styleTag) {
Style.Builder builder = Style.style();
if (!styleTag.isEmpty()) {
String color = styleTag.getString("color", null);
if (color != null) {
builder.color(NamedTextColor.NAMES.value(color));
}
//TODO implement the rest
boolean italic = styleTag.getBoolean("italic");
if (italic) {
builder.decorate(net.kyori.adventure.text.format.TextDecoration.ITALIC);
}
}
return builder.build();
} }
} }

View file

@ -26,16 +26,21 @@
package org.geysermc.geyser.translator.text; package org.geysermc.geyser.translator.text;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.JoinConfiguration;
import net.kyori.adventure.text.ScoreComponent; import net.kyori.adventure.text.ScoreComponent;
import net.kyori.adventure.text.TranslatableComponent; import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.flattener.ComponentFlattener; import net.kyori.adventure.text.flattener.ComponentFlattener;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.renderer.TranslatableComponentRenderer; import net.kyori.adventure.text.renderer.TranslatableComponentRenderer;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.kyori.adventure.text.serializer.legacy.CharacterAndFormat; import net.kyori.adventure.text.serializer.legacy.CharacterAndFormat;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtType;
import org.cloudburstmc.protocol.bedrock.packet.TextPacket; import org.cloudburstmc.protocol.bedrock.packet.TextPacket;
import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
@ -341,16 +346,16 @@ public class MessageTranslator {
// Though, Bedrock cannot care about the signed stuff. // Though, Bedrock cannot care about the signed stuff.
TranslatableComponent.Builder withDecoration = Component.translatable() TranslatableComponent.Builder withDecoration = Component.translatable()
.key(chat.translationKey()) .key(chat.translationKey())
.style(TextDecoration.getStyle(chat)); .style(ChatDecoration.getStyle(chat));
List<ChatTypeDecoration.Parameter> parameters = chat.parameters(); List<ChatTypeDecoration.Parameter> parameters = chat.parameters();
List<Component> args = new ArrayList<>(3); List<Component> args = new ArrayList<>(3);
if (parameters.contains(TextDecoration.Parameter.TARGET)) { if (parameters.contains(ChatDecoration.Parameter.TARGET)) {
args.add(targetName); args.add(targetName);
} }
if (parameters.contains(TextDecoration.Parameter.SENDER)) { if (parameters.contains(ChatDecoration.Parameter.SENDER)) {
args.add(sender); args.add(sender);
} }
if (parameters.contains(TextDecoration.Parameter.CONTENT)) { if (parameters.contains(ChatDecoration.Parameter.CONTENT)) {
args.add(message); args.add(message);
} }
withDecoration.arguments(args); withDecoration.arguments(args);
@ -426,17 +431,91 @@ public class MessageTranslator {
} }
/** /**
* Deserialize an NbtMap provided from a registry into a string. * Deserialize an NbtMap with a description text component (usually provided from a registry) into a Bedrock-formatted string.
*/ */
// This may be a Component in the future. public static String deserializeDescription(GeyserSession session, NbtMap tag) {
public static String deserializeDescription(NbtMap tag) {
NbtMap description = tag.getCompound("description"); NbtMap description = tag.getCompound("description");
String translate = description.getString("translate", null); Component parsed = componentFromNbtTag(description);
if (translate == null) { return convertMessage(session, parsed);
GeyserImpl.getInstance().getLogger().debug("Don't know how to read description! " + tag);
return "";
} }
return translate;
public static Component componentFromNbtTag(Object nbtTag) {
return componentFromNbtTag(nbtTag, Style.empty());
}
private static Component componentFromNbtTag(Object nbtTag, Style style) {
if (nbtTag instanceof String literal) {
return Component.text(literal).style(style);
} else if (nbtTag instanceof List<?> list) {
return Component.join(JoinConfiguration.noSeparators(), componentsFromNbtList(list, style));
} else if (nbtTag instanceof NbtMap map) {
Component component = null;
String text = map.getString("text", null);
if (text != null) {
component = Component.text(text);
} else {
String translateKey = map.getString("translate", null);
if (translateKey != null) {
String fallback = map.getString("fallback", "");
List<Component> args = new ArrayList<>();
Object with = map.get("with");
if (with instanceof List<?> list) {
args = componentsFromNbtList(list, style);
} else if (with != null) {
args.add(componentFromNbtTag(with, style));
}
component = Component.translatable(translateKey, fallback, args);
}
}
if (component != null) {
Style newStyle = getStyleFromNbtMap(map, style);
component = component.style(newStyle);
Object extra = map.get("extra");
if (extra != null) {
component = component.append(componentFromNbtTag(extra, newStyle));
}
return component;
}
}
throw new IllegalArgumentException("Expected tag to be a literal string, a list of components, or a component object with a text/translate key");
}
private static List<Component> componentsFromNbtList(List<?> list, Style style) {
List<Component> components = new ArrayList<>();
for (Object entry : list) {
components.add(componentFromNbtTag(entry, style));
}
return components;
}
public static Style getStyleFromNbtMap(NbtMap map) {
Style.Builder style = Style.style();
String colorString = map.getString("color", null);
if (colorString != null) {
if (colorString.startsWith(TextColor.HEX_PREFIX)) {
style.color(TextColor.fromHexString(colorString));
} else {
style.color(NamedTextColor.NAMES.value(colorString));
}
}
map.listenForBoolean("bold", value -> style.decoration(TextDecoration.BOLD, value));
map.listenForBoolean("italic", value -> style.decoration(TextDecoration.ITALIC, value));
map.listenForBoolean("underlined", value -> style.decoration(TextDecoration.UNDERLINED, value));
map.listenForBoolean("strikethrough", value -> style.decoration(TextDecoration.STRIKETHROUGH, value));
map.listenForBoolean("obfuscated", value -> style.decoration(TextDecoration.OBFUSCATED, value));
return style.build();
}
public static Style getStyleFromNbtMap(NbtMap map, Style base) {
return base.merge(getStyleFromNbtMap(map));
} }
public static void init() { public static void init() {