diff --git a/chat/src/main/java/net/md_5/bungee/api/chat/HoverEvent.java b/chat/src/main/java/net/md_5/bungee/api/chat/HoverEvent.java index 253ee734..0603083b 100644 --- a/chat/src/main/java/net/md_5/bungee/api/chat/HoverEvent.java +++ b/chat/src/main/java/net/md_5/bungee/api/chat/HoverEvent.java @@ -1,8 +1,26 @@ package net.md_5.bungee.api.chat; +import com.google.common.base.Preconditions; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; import lombok.EqualsAndHashCode; import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; import lombok.RequiredArgsConstructor; +import lombok.Setter; import lombok.ToString; @Getter @@ -12,15 +30,335 @@ import lombok.ToString; public final class HoverEvent { + /** + * The action of this event. + */ private final Action action; - private final BaseComponent[] value; + /** + * List of contents to provide for this event. + */ + private final List contents; + /** + * Returns whether this hover event is prior to 1.16 + */ + @Setter + private boolean legacy = false; + + /** + * Creates event with an action and a list of contents. + * + * @param action action of this event + * @param contents array of contents, provide at least one + */ + public HoverEvent(Action action, Content... contents) + { + Preconditions.checkArgument( contents.length != 0, "Must contain at least one content" ); + this.action = action; + this.contents = new ArrayList<>(); + for ( Content it : contents ) + { + addContent( it ); + } + } + + /** + * Legacy constructor to create hover event. + * + * @param action the action + * @param value the value + * @deprecated {@link #HoverEvent(Action, Content[])} + */ + @Deprecated + public HoverEvent(Action action, BaseComponent[] value) + { + // Old plugins may have somehow hacked BaseComponent[] into + // anything other than SHOW_TEXT action. Ideally continue support. + this.action = action; + this.contents = new ArrayList<>( Collections.singletonList( new ContentText( value ) ) ); + this.legacy = true; + } + + /** + * Adds a content to this hover event. + * + * @param content the content add + * @throws IllegalArgumentException if is a legacy component and already has + * a content + * @throws UnsupportedOperationException if content action does not match + * hover event action + */ + public void addContent(Content content) throws UnsupportedOperationException + { + Preconditions.checkArgument( !legacy || contents.size() == 0, "Legacy HoverEvent may not have more than one content" ); + content.assertAction( action ); + contents.add( content ); + } + + @ToString + @EqualsAndHashCode + public abstract static class Content + { + + /** + * Required action for this content type. + * + * @return action + */ + abstract Action requiredAction(); + + /** + * Tests this content against an action + * + * @param input input to test + * @throws UnsupportedOperationException if action incompatible + */ + void assertAction(Action input) throws UnsupportedOperationException + { + if ( input != requiredAction() ) + { + throw new UnsupportedOperationException( "Action " + input + " not compatible! Expected " + requiredAction() ); + } + } + } + + @Data + @ToString + public static class ContentText extends Content + { + + /** + * The value. + * + * May be a component or raw text depending on constructor used. + */ + private Object value; + + public ContentText(BaseComponent[] value) + { + this.value = value; + } + + public ContentText(String value) + { + this.value = value; + } + + @Override + Action requiredAction() + { + return Action.SHOW_TEXT; + } + + @Override + public boolean equals(Object o) + { + if ( value instanceof BaseComponent[] ) + { + return o instanceof ContentText + && ( (ContentText) o ).value instanceof BaseComponent[] + && Arrays.equals( (BaseComponent[]) value, (BaseComponent[]) ( (ContentText) o ).value ); + } else + { + return value.equals( o ); + } + } + + @Override + public int hashCode() + { + return ( value instanceof BaseComponent[] ) ? Arrays.hashCode( (BaseComponent[]) value ) : value.hashCode(); + } + + public static class Serializer implements JsonSerializer, JsonDeserializer + { + + @Override + public ContentText deserialize(JsonElement element, Type type, JsonDeserializationContext context) throws JsonParseException + { + if ( element.isJsonArray() ) + { + return new ContentText( context.deserialize( element, BaseComponent[].class ) ); + } else if ( element.getAsJsonObject().isJsonPrimitive() ) + { + return new ContentText( element.getAsJsonObject().getAsJsonPrimitive().getAsString() ); + } else + { + return new ContentText( new BaseComponent[] + { + context.deserialize( element, BaseComponent.class ) + } ); + } + } + + @Override + public JsonElement serialize(ContentText content, Type type, JsonSerializationContext context) + { + return context.serialize( content.getValue() ); + } + } + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + @ToString + @EqualsAndHashCode(callSuper = true) + public static class ContentEntity extends Content + { + + /** + * Namespaced entity ID. + * + * Will use 'minecraft:pig' if null. + */ + private String type; + /** + * Entity UUID in hyphenated hexadecimal format. + * + * Should be valid UUID. TODO : validate? + */ + @NonNull + private String id; + /** + * Name to display as the entity. + * + * This is optional and will be hidden if null. + */ + private BaseComponent name; + + @Override + Action requiredAction() + { + return Action.SHOW_ENTITY; + } + + public static class Serializer implements JsonSerializer, JsonDeserializer + { + + @Override + public ContentEntity deserialize(JsonElement element, Type type, JsonDeserializationContext context) throws JsonParseException + { + JsonObject value = element.getAsJsonObject(); + + return new ContentEntity( + ( value.has( "type" ) ) ? value.get( "type" ).getAsString() : null, + value.get( "id" ).getAsString(), + ( value.has( "name" ) ) ? context.deserialize( value.get( "name" ), BaseComponent.class ) : null + ); + } + + @Override + public JsonElement serialize(ContentEntity content, Type type, JsonSerializationContext context) + { + JsonObject object = new JsonObject(); + object.addProperty( "type", ( content.getType() != null ) ? content.getType() : "minecraft:pig" ); + object.addProperty( "id", content.getId() ); + if ( content.getName() != null ) + { + object.add( "name", context.serialize( content.getName() ) ); + } + return object; + } + } + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + @ToString + @EqualsAndHashCode(callSuper = true) + public static class ContentItem extends Content + { + + /** + * Namespaced item ID. Will use 'minecraft:air' if null. + */ + private String id; + /** + * Optional. Size of the item stack. + */ + private int count = -1; + /** + * Optional. Item tag. + */ + private ItemTag tag; + + @Override + Action requiredAction() + { + return Action.SHOW_ITEM; + } + + public static class Serializer implements JsonSerializer, JsonDeserializer + { + + @Override + public ContentItem deserialize(JsonElement element, Type type, JsonDeserializationContext context) throws JsonParseException + { + JsonObject value = element.getAsJsonObject(); + + return new ContentItem( + ( value.has( "id" ) ) ? value.get( "id" ).getAsString() : null, + ( value.has( "Count" ) ) ? value.get( "Count" ).getAsInt() : -1, + ( value.has( "tag" ) ) ? context.deserialize( value.get( "tag" ), ItemTag.class ) : null + ); + } + + @Override + public JsonElement serialize(ContentItem content, Type type, JsonSerializationContext context) + { + JsonObject object = new JsonObject(); + object.addProperty( "id", ( content.getId() == null ) ? "minecraft:air" : content.getId() ); + if ( content.getCount() != -1 ) + { + object.addProperty( "Count", content.getCount() ); + } + if ( content.getTag() != null ) + { + object.add( "tag", context.serialize( content.getTag() ) ); + } + return object; + } + } + } public enum Action { SHOW_TEXT, - SHOW_ACHIEVEMENT, SHOW_ITEM, - SHOW_ENTITY + SHOW_ENTITY, + /** + * Removed since 1.12. Advancements instead simply use show_text. The ID + * of an achievement or statistic to display. Example: new + * ComponentText( "achievement.openInventory" ) + */ + @Deprecated + SHOW_ACHIEVEMENT, + } + + /** + * Gets the appropriate {@link Content} class for an {@link Action} for the + * GSON serialization + * + * @param action the action to get for + * @param array if to return the arrayed class + * @return the class + */ + public static Class getClass(HoverEvent.Action action, boolean array) + { + Preconditions.checkArgument( action != null, "action" ); + + switch ( action ) + { + case SHOW_TEXT: + return ( array ) ? HoverEvent.ContentText[].class : HoverEvent.ContentText.class; + case SHOW_ENTITY: + return ( array ) ? HoverEvent.ContentEntity[].class : HoverEvent.ContentEntity.class; + case SHOW_ITEM: + return ( array ) ? HoverEvent.ContentItem[].class : HoverEvent.ContentItem.class; + default: + throw new UnsupportedOperationException( "Action '" + action.name() + " not supported" ); + } } } diff --git a/chat/src/main/java/net/md_5/bungee/api/chat/ItemTag.java b/chat/src/main/java/net/md_5/bungee/api/chat/ItemTag.java new file mode 100644 index 00000000..f2b1bc91 --- /dev/null +++ b/chat/src/main/java/net/md_5/bungee/api/chat/ItemTag.java @@ -0,0 +1,158 @@ +package net.md_5.bungee.api.chat; + +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.Singular; +import lombok.ToString; + +/** + * Metadata for use in conjunction with {@link HoverEvent.Action#SHOW_ITEM} + */ +@ToString(callSuper = true) +@Setter +@Builder(builderClassName = "Builder", access = AccessLevel.PUBLIC) +@AllArgsConstructor +@EqualsAndHashCode +public final class ItemTag +{ + + private BaseComponent name; + @Singular("ench") + private List enchantments = new ArrayList<>(); + @Singular("lore") + private List lore = new ArrayList<>(); + private Boolean unbreakable; + + private ItemTag() + { + } + + @RequiredArgsConstructor + public static class Enchantment + { + + private final int level; + private final int id; + } + + public static class Serializer implements JsonSerializer, JsonDeserializer + { + + @Override + public ItemTag deserialize(JsonElement element, Type type, JsonDeserializationContext context) throws JsonParseException + { + ItemTag itemTag = new ItemTag(); + JsonObject object = element.getAsJsonObject(); + if ( object.has( "ench" ) ) + { + for ( JsonElement jsonElement : object.get( "ench" ).getAsJsonArray() ) + { + JsonObject next = jsonElement.getAsJsonObject(); + itemTag.enchantments.add( new Enchantment( next.get( "id" ).getAsInt(), next.get( "lvl" ).getAsInt() ) ); + } + } + if ( object.has( "Unbreakable" ) ) + { + int status = object.get( "Unbreakable" ).getAsInt(); + if ( status == 1 ) + { + itemTag.unbreakable = true; + } else if ( status == 0 ) + { + itemTag.unbreakable = false; + } + } + if ( object.has( "display" ) ) + { + JsonObject display = object.get( "display" ).getAsJsonObject(); + if ( display.has( "Name" ) ) + { + itemTag.name = context.deserialize( display.get( "Name" ).getAsJsonObject(), BaseComponent.class ); + } + + if ( display.has( "Lore" ) ) + { + JsonElement lore = display.get( "Lore" ); + if ( lore.isJsonArray() ) + { + for ( JsonElement loreIt : lore.getAsJsonArray() ) + { + if ( loreIt.isJsonArray() ) + { + itemTag.lore.add( context.deserialize( loreIt, BaseComponent[].class ) ); + } else + { + itemTag.lore.add( new BaseComponent[] + { + context.deserialize( loreIt, BaseComponent.class ) + } ); + } + } + } else + { + itemTag.lore.add( context.deserialize( display.get( "Lore" ), BaseComponent[].class ) ); + } + } + } + return itemTag; + } + + @Override + public JsonElement serialize(ItemTag itemTag, Type type, JsonSerializationContext context) + { + JsonObject object = new JsonObject(); + + if ( !itemTag.enchantments.isEmpty() ) + { + JsonArray enchArray = new JsonArray(); + for ( Enchantment ench : itemTag.enchantments ) + { + JsonObject enchObj = new JsonObject(); + enchObj.addProperty( "id", ench.id ); + enchObj.addProperty( "lvl", ench.level ); + enchArray.add( enchObj ); + } + object.add( "ench", enchArray ); + } + + if ( itemTag.unbreakable != null ) + { + object.addProperty( "Unbreakable", ( itemTag.unbreakable ) ? 1 : 0 ); + } + + JsonObject display = new JsonObject(); + + if ( itemTag.name != null ) + { + display.add( "Name", context.serialize( itemTag.name ) ); + } + + if ( !itemTag.lore.isEmpty() ) + { + display.add( "Lore", context.serialize( itemTag.lore ) ); + } + + if ( display.size() != 0 ) + { + object.add( "display", display ); + } + + return object; + } + } +} diff --git a/chat/src/main/java/net/md_5/bungee/chat/BaseComponentSerializer.java b/chat/src/main/java/net/md_5/bungee/chat/BaseComponentSerializer.java index cacd9b64..dde8a6d7 100644 --- a/chat/src/main/java/net/md_5/bungee/chat/BaseComponentSerializer.java +++ b/chat/src/main/java/net/md_5/bungee/chat/BaseComponentSerializer.java @@ -2,8 +2,10 @@ package net.md_5.bungee.chat; import com.google.common.base.Preconditions; import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonSerializationContext; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.IdentityHashMap; @@ -52,7 +54,7 @@ public class BaseComponentSerializer } if ( object.has( "extra" ) ) { - component.setExtra( Arrays.asList( context.deserialize( object.get( "extra" ), BaseComponent[].class ) ) ); + component.setExtra( Arrays.asList( context.deserialize( object.get( "extra" ), BaseComponent[].class ) ) ); } //Events @@ -66,18 +68,34 @@ public class BaseComponentSerializer if ( object.has( "hoverEvent" ) ) { JsonObject event = object.getAsJsonObject( "hoverEvent" ); - BaseComponent[] res; - if ( event.get( "value" ).isJsonArray() ) + HoverEvent hoverEvent = null; + HoverEvent.Action action = HoverEvent.Action.valueOf( event.get( "action" ).getAsString().toUpperCase( Locale.ROOT ) ); + + if ( event.has( "value" ) ) { - res = context.deserialize( event.get( "value" ), BaseComponent[].class ); - } else - { - res = new BaseComponent[] + HoverEvent.Content[] ret = new HoverEvent.Content[] { - context.deserialize( event.get( "value" ), BaseComponent.class ) + context.deserialize( event.get( "value" ), HoverEvent.getClass( action, false ) ) }; + hoverEvent = new HoverEvent( action, ret ); + } else if ( event.has( "contents" ) ) + { + HoverEvent.Content[] list; + JsonElement contents = event.get( "contents" ); + if ( contents.isJsonArray() ) + { + list = context.deserialize( contents, HoverEvent.getClass( action, true ) ); + } else + { + list = new HoverEvent.Content[] + { + context.deserialize( contents, HoverEvent.getClass( action, false ) ) + }; + } + + hoverEvent = new HoverEvent( action, new ArrayList<>( Arrays.asList( list ) ) ); } - component.setHoverEvent( new HoverEvent( HoverEvent.Action.valueOf( event.get( "action" ).getAsString().toUpperCase( Locale.ROOT ) ), res ) ); + component.setHoverEvent( hoverEvent ); } } @@ -143,7 +161,13 @@ public class BaseComponentSerializer { JsonObject hoverEvent = new JsonObject(); hoverEvent.addProperty( "action", component.getHoverEvent().getAction().toString().toLowerCase( Locale.ROOT ) ); - hoverEvent.add( "value", context.serialize( component.getHoverEvent().getValue() ) ); + if ( component.getHoverEvent().isLegacy() ) + { + hoverEvent.add( "value", context.serialize( component.getHoverEvent().getContents().get( 0 ) ) ); + } else + { + hoverEvent.add( "contents", context.serialize( component.getHoverEvent().getContents() ) ); + } object.add( "hoverEvent", hoverEvent ); } } finally diff --git a/chat/src/main/java/net/md_5/bungee/chat/ComponentSerializer.java b/chat/src/main/java/net/md_5/bungee/chat/ComponentSerializer.java index 2a780694..9dd751b0 100644 --- a/chat/src/main/java/net/md_5/bungee/chat/ComponentSerializer.java +++ b/chat/src/main/java/net/md_5/bungee/chat/ComponentSerializer.java @@ -11,6 +11,8 @@ import com.google.gson.JsonParser; import java.lang.reflect.Type; import java.util.Set; import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.HoverEvent; +import net.md_5.bungee.api.chat.ItemTag; import net.md_5.bungee.api.chat.KeybindComponent; import net.md_5.bungee.api.chat.ScoreComponent; import net.md_5.bungee.api.chat.SelectorComponent; @@ -28,6 +30,10 @@ public class ComponentSerializer implements JsonDeserializer registerTypeAdapter( KeybindComponent.class, new KeybindComponentSerializer() ). registerTypeAdapter( ScoreComponent.class, new ScoreComponentSerializer() ). registerTypeAdapter( SelectorComponent.class, new SelectorComponentSerializer() ). + registerTypeAdapter( HoverEvent.ContentEntity.class, new HoverEvent.ContentEntity.Serializer() ). + registerTypeAdapter( HoverEvent.ContentText.class, new HoverEvent.ContentText.Serializer() ). + registerTypeAdapter( HoverEvent.ContentItem.class, new HoverEvent.ContentItem.Serializer() ). + registerTypeAdapter( ItemTag.class, new ItemTag.Serializer() ). create(); public static final ThreadLocal> serializedComponents = new ThreadLocal>(); diff --git a/chat/src/main/java/net/md_5/bungee/chat/TextComponentSerializer.java b/chat/src/main/java/net/md_5/bungee/chat/TextComponentSerializer.java index b76afd05..657fde2a 100644 --- a/chat/src/main/java/net/md_5/bungee/chat/TextComponentSerializer.java +++ b/chat/src/main/java/net/md_5/bungee/chat/TextComponentSerializer.java @@ -30,11 +30,11 @@ public class TextComponentSerializer extends BaseComponentSerializer implements { List extra = src.getExtra(); JsonObject object = new JsonObject(); + object.addProperty( "text", src.getText() ); if ( src.hasFormatting() || ( extra != null && !extra.isEmpty() ) ) { serialize( object, src, context ); } - object.addProperty( "text", src.getText() ); return object; } } diff --git a/chat/src/test/java/net/md_5/bungee/api/chat/ComponentsTest.java b/chat/src/test/java/net/md_5/bungee/api/chat/ComponentsTest.java index 54ec98fd..b41b84fb 100644 --- a/chat/src/test/java/net/md_5/bungee/api/chat/ComponentsTest.java +++ b/chat/src/test/java/net/md_5/bungee/api/chat/ComponentsTest.java @@ -124,6 +124,70 @@ public class ComponentsTest ); } + @Test + public void testItemTag() + { + TextComponent component = new TextComponent( "Hello world" ); + HoverEvent.ContentItem content = new HoverEvent.ContentItem(); + content.setId( "minecraft:diamond_sword" ); + content.setCount( 1 ); + content.setTag( ItemTag.builder() + .ench( new ItemTag.Enchantment( 5, 16 ) ) + .name( new TextComponent( "Sharp Sword" ) ) + .unbreakable( true ) + .lore( new ComponentBuilder( "Line1" ).create() ) + .lore( new ComponentBuilder( "Line2" ).create() ) + .build() ); + HoverEvent event = new HoverEvent( HoverEvent.Action.SHOW_ITEM, content ); + component.setHoverEvent( event ); + String serialised = ComponentSerializer.toString( component ); + BaseComponent[] deserialised = ComponentSerializer.parse( serialised ); + Assert.assertEquals( TextComponent.toLegacyText( deserialised ), TextComponent.toLegacyText( component ) ); + } + + @Test + public void testModernShowAdvancement() + { + String advancement = "achievement.openInventory"; + // First do the text using the newer contents system + HoverEvent hoverEvent = new HoverEvent( + HoverEvent.Action.SHOW_TEXT, + new HoverEvent.ContentText( advancement ) + ); + TextComponent component = new TextComponent( "test" ); + component.setHoverEvent( hoverEvent ); + Assert.assertEquals( component.getHoverEvent().getContents().size(), 1 ); + Assert.assertTrue( component.getHoverEvent().getContents().get( 0 ) instanceof HoverEvent.ContentText ); + Assert.assertEquals( ( (HoverEvent.ContentText) component.getHoverEvent().getContents().get( 0 ) ).getValue(), advancement ); + } + + @Test + public void testHoverEventContents() + { + // First do the text using the newer contents system + HoverEvent hoverEvent = new HoverEvent( + HoverEvent.Action.SHOW_TEXT, + new HoverEvent.ContentText( new ComponentBuilder( "First" ).create() ), + new HoverEvent.ContentText( new ComponentBuilder( "Second" ).create() ) + ); + + TextComponent component = new TextComponent( "Sample text" ); + component.setHoverEvent( hoverEvent ); + Assert.assertEquals( hoverEvent.getContents().size(), 2 ); + Assert.assertFalse( hoverEvent.isLegacy() ); + String serialized = ComponentSerializer.toString( component ); + BaseComponent[] deserialized = ComponentSerializer.parse( serialized ); + Assert.assertEquals( component.getHoverEvent(), deserialized[0].getHoverEvent() ); + + // check the test still works with the value method + hoverEvent = new HoverEvent( HoverEvent.Action.SHOW_TEXT, new ComponentBuilder( "Sample text" ).create() ); + Assert.assertEquals( hoverEvent.getContents().size(), 1 ); + Assert.assertTrue( hoverEvent.isLegacy() ); + serialized = ComponentSerializer.toString( component ); + deserialized = ComponentSerializer.parse( serialized ); + Assert.assertEquals( component.getHoverEvent(), deserialized[0].getHoverEvent() ); + } + @Test public void testFormatRetentionCopyFormatting() { diff --git a/proxy/src/main/java/net/md_5/bungee/BungeeCord.java b/proxy/src/main/java/net/md_5/bungee/BungeeCord.java index be5bd490..c04c3e8e 100644 --- a/proxy/src/main/java/net/md_5/bungee/BungeeCord.java +++ b/proxy/src/main/java/net/md_5/bungee/BungeeCord.java @@ -90,7 +90,6 @@ import net.md_5.bungee.module.ModuleManager; import net.md_5.bungee.netty.PipelineUtils; import net.md_5.bungee.protocol.DefinedPacket; import net.md_5.bungee.protocol.ProtocolConstants; -import net.md_5.bungee.protocol.packet.Chat; import net.md_5.bungee.protocol.packet.PluginMessage; import net.md_5.bungee.query.RemoteQuery; import net.md_5.bungee.scheduler.BungeeScheduler; @@ -707,14 +706,20 @@ public class BungeeCord extends ProxyServer public void broadcast(BaseComponent... message) { getConsole().sendMessage( BaseComponent.toLegacyText( message ) ); - broadcast( new Chat( ComponentSerializer.toString( message ) ) ); + for ( ProxiedPlayer player : getPlayers() ) + { + player.sendMessage( message ); + } } @Override public void broadcast(BaseComponent message) { getConsole().sendMessage( message.toLegacyText() ); - broadcast( new Chat( ComponentSerializer.toString( message ) ) ); + for ( ProxiedPlayer player : getPlayers() ) + { + player.sendMessage( message ); + } } public void addConnection(UserConnection con) diff --git a/proxy/src/main/java/net/md_5/bungee/UserConnection.java b/proxy/src/main/java/net/md_5/bungee/UserConnection.java index 0b037802..4199bdad 100644 --- a/proxy/src/main/java/net/md_5/bungee/UserConnection.java +++ b/proxy/src/main/java/net/md_5/bungee/UserConnection.java @@ -450,7 +450,7 @@ public final class UserConnection implements ProxiedPlayer public void sendMessage(ChatMessageType position, BaseComponent... message) { // transform score components - message = ChatComponentTransformer.getInstance().transform( this, message ); + message = ChatComponentTransformer.getInstance().transform( this, true, message ); if ( position == ChatMessageType.ACTION_BAR ) { @@ -475,7 +475,7 @@ public final class UserConnection implements ProxiedPlayer @Override public void sendMessage(ChatMessageType position, BaseComponent message) { - message = ChatComponentTransformer.getInstance().transform( this, message )[0]; + message = ChatComponentTransformer.getInstance().transform( this, true, message )[0]; // Action bar doesn't display the new JSON formattings, legacy works - send it using this for now if ( position == ChatMessageType.ACTION_BAR ) @@ -663,8 +663,8 @@ public final class UserConnection implements ProxiedPlayer @Override public void setTabHeader(BaseComponent header, BaseComponent footer) { - header = ChatComponentTransformer.getInstance().transform( this, header )[0]; - footer = ChatComponentTransformer.getInstance().transform( this, footer )[0]; + header = ChatComponentTransformer.getInstance().transform( this, true, header )[0]; + footer = ChatComponentTransformer.getInstance().transform( this, true, footer )[0]; unsafe().sendPacket( new PlayerListHeaderFooter( ComponentSerializer.toString( header ), @@ -675,8 +675,8 @@ public final class UserConnection implements ProxiedPlayer @Override public void setTabHeader(BaseComponent[] header, BaseComponent[] footer) { - header = ChatComponentTransformer.getInstance().transform( this, header ); - footer = ChatComponentTransformer.getInstance().transform( this, footer ); + header = ChatComponentTransformer.getInstance().transform( this, true, header ); + footer = ChatComponentTransformer.getInstance().transform( this, true, footer ); unsafe().sendPacket( new PlayerListHeaderFooter( ComponentSerializer.toString( header ), diff --git a/proxy/src/main/java/net/md_5/bungee/util/ChatComponentTransformer.java b/proxy/src/main/java/net/md_5/bungee/util/ChatComponentTransformer.java index d69c35d9..8b483fe2 100644 --- a/proxy/src/main/java/net/md_5/bungee/util/ChatComponentTransformer.java +++ b/proxy/src/main/java/net/md_5/bungee/util/ChatComponentTransformer.java @@ -7,10 +7,12 @@ import java.util.regex.Pattern; import lombok.AccessLevel; import lombok.NoArgsConstructor; import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.HoverEvent; import net.md_5.bungee.api.chat.ScoreComponent; import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.score.Score; +import net.md_5.bungee.protocol.ProtocolConstants; /** * This class transforms chat components by attempting to replace transformable @@ -32,6 +34,32 @@ public final class ChatComponentTransformer */ private static final Pattern SELECTOR_PATTERN = Pattern.compile( "^@([pares])(?:\\[([^ ]*)\\])?$" ); + public BaseComponent[] legacyHoverTransform(ProxiedPlayer player, BaseComponent... components) + { + if ( player.getPendingConnection().getVersion() < ProtocolConstants.MINECRAFT_1_16 ) + { + for ( int i = 0; i < components.length; i++ ) + { + BaseComponent next = components[i]; + if ( next.getHoverEvent().isLegacy() ) + { + continue; + } + next = next.duplicate(); + next.getHoverEvent().setLegacy( true ); + if ( next.getHoverEvent().getContents().size() > 1 ) + { + HoverEvent.Content exception = next.getHoverEvent().getContents().get( 0 ); + next.getHoverEvent().getContents().clear(); + next.getHoverEvent().getContents().add( exception ); + } + components[i] = next; + } + } + + return components; + } + public static ChatComponentTransformer getInstance() { return INSTANCE; @@ -44,14 +72,33 @@ public final class ChatComponentTransformer * {@link BaseComponent#getExtra()}). * * @param player player - * @param component the component to transform + * @param components the component to transform * @return the transformed component, or an array containing a single empty * TextComponent if the components are null or empty * @throws IllegalArgumentException if an entity selector pattern is present */ - public BaseComponent[] transform(ProxiedPlayer player, BaseComponent... component) + public BaseComponent[] transform(ProxiedPlayer player, BaseComponent... components) { - if ( component == null || component.length < 1 || ( component.length == 1 && component[0] == null ) ) + return transform( player, false, components ); + } + + /** + * Transform a set of components, and attempt to transform the transformable + * fields. Entity selectors cannot be evaluated. This will + * recursively search for all extra components (see + * {@link BaseComponent#getExtra()}). + * + * @param player player + * @param transformHover if the hover event should replace contents with + * value + * @param components the component to transform + * @return the transformed component, or an array containing a single empty + * TextComponent if the components are null or empty + * @throws IllegalArgumentException if an entity selector pattern is present + */ + public BaseComponent[] transform(ProxiedPlayer player, boolean transformHover, BaseComponent... components) + { + if ( components == null || components.length < 1 || ( components.length == 1 && components[0] == null ) ) { return new BaseComponent[] { @@ -59,7 +106,12 @@ public final class ChatComponentTransformer }; } - for ( BaseComponent root : component ) + if ( transformHover ) + { + components = legacyHoverTransform( player, components ); + } + + for ( BaseComponent root : components ) { if ( root.getExtra() != null && !root.getExtra().isEmpty() ) { @@ -72,7 +124,7 @@ public final class ChatComponentTransformer transformScoreComponent( player, (ScoreComponent) root ); } } - return component; + return components; } /**