#2866: Add support for contents in Hover Event

This commit is contained in:
Mystiflow 2020-07-01 10:57:09 +10:00 committed by md_5
parent 26f538d193
commit 1d40b8a88a
No known key found for this signature in database
GPG Key ID: E8E901AC7C617C11
9 changed files with 675 additions and 28 deletions

View File

@ -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<Content> 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<ContentText>, JsonDeserializer<ContentText>
{
@Override
public ContentText deserialize(JsonElement element, Type type, JsonDeserializationContext context) throws JsonParseException
{
if ( element.isJsonArray() )
{
return new ContentText( context.<BaseComponent[]>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<ContentEntity>, JsonDeserializer<ContentEntity>
{
@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<ContentItem>, JsonDeserializer<ContentItem>
{
@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" );
}
}
}

View File

@ -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<Enchantment> enchantments = new ArrayList<>();
@Singular("lore")
private List<BaseComponent[]> 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<ItemTag>, JsonDeserializer<ItemTag>
{
@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;
}
}
}

View File

@ -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.<BaseComponent>asList( context.<BaseComponent[]>deserialize( object.get( "extra" ), BaseComponent[].class ) ) );
component.setExtra( Arrays.asList( context.<BaseComponent[]>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.<BaseComponent>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

View File

@ -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<BaseComponent>
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<Set<BaseComponent>> serializedComponents = new ThreadLocal<Set<BaseComponent>>();

View File

@ -30,11 +30,11 @@ public class TextComponentSerializer extends BaseComponentSerializer implements
{
List<BaseComponent> 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;
}
}

View File

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

View File

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

View File

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

View File

@ -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 <b>cannot</b> 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;
}
/**