#2342: Add score and selector components to chat API

This commit is contained in:
Senmori 2018-02-19 11:47:21 +11:00 committed by md_5
parent e23195f5f2
commit a3b44aa612
11 changed files with 420 additions and 7 deletions

View File

@ -10,6 +10,7 @@ import net.md_5.bungee.api.SkinConfiguration;
import net.md_5.bungee.api.Title; import net.md_5.bungee.api.Title;
import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.config.ServerInfo; import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.api.score.Scoreboard;
/** /**
* Represents a player who's connection is being connected to somewhere else, * Represents a player who's connection is being connected to somewhere else,
@ -275,4 +276,11 @@ public interface ProxiedPlayer extends Connection, CommandSender
* not occurred for this {@link ProxiedPlayer} yet. * not occurred for this {@link ProxiedPlayer} yet.
*/ */
Map<String, String> getModList(); Map<String, String> getModList();
/**
* Get the {@link Scoreboard} that belongs to this player.
*
* @return this player's {@link Scoreboard}
*/
Scoreboard getScoreboard();
} }

View File

@ -62,6 +62,11 @@ public class Scoreboard
scores.put( score.getItemName(), score ); scores.put( score.getItemName(), score );
} }
public Score getScore(String name)
{
return scores.get( name );
}
public void addTeam(Team team) public void addTeam(Team team)
{ {
Preconditions.checkNotNull( team, "team" ); Preconditions.checkNotNull( team, "team" );

View File

@ -35,7 +35,7 @@ public final class KeybindComponent extends BaseComponent
* Creates a keybind component with the passed internal keybind value. * Creates a keybind component with the passed internal keybind value.
* *
* @param keybind the keybind value * @param keybind the keybind value
* @see Keybind * @see Keybinds
*/ */
public KeybindComponent(String keybind) public KeybindComponent(String keybind)
{ {

View File

@ -0,0 +1,97 @@
package net.md_5.bungee.api.chat;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
* This component displays the score based on a player score on the scoreboard.
* <br>
* The <b>name</b> is the name of the player stored on the scoreboard, which may
* be a "fake" player. It can also be a target selector that <b>must</b> resolve
* to 1 target, and may target non-player entities.
* <br>
* With a book, /tellraw, or /title, using the wildcard '*' in the place of a
* name or target selector will cause all players to see their own score in the
* specified objective.
* <br>
* <b>Signs cannot use the '*' wildcard</b>
* <br>
* These values are filled in by the server-side implementation.
* <br>
* As of 1.12.2, a bug ( MC-56373 ) prevents full usage within hover events.
*/
@Getter
@Setter
@ToString
@AllArgsConstructor
public final class ScoreComponent extends BaseComponent
{
/**
* The name of the entity whose score should be displayed.
*/
private String name;
/**
* The internal name of the objective the score is attached to.
*/
private String objective;
/**
* The optional value to use instead of the one present in the Scoreboard.
*/
private String value = "";
/**
* Creates a new score component with the specified name and objective.<br>
* If not specifically set, value will default to an empty string;
* signifying that the scoreboard value should take precedence. If not null,
* nor empty, {@code value} will override any value found in the
* scoreboard.<br>
* The value defaults to an empty string.
*
* @param name the name of the entity, or an entity selector, whose score
* should be displayed
* @param objective the internal name of the objective the entity's score is
* attached to
*/
public ScoreComponent(String name, String objective)
{
setName( name );
setObjective( objective );
}
/**
* Creates a score component from the original to clone it.
*
* @param original the original for the new score component
*/
public ScoreComponent(ScoreComponent original)
{
super( original );
setName( original.getName() );
setObjective( original.getObjective() );
setValue( original.getValue() );
}
@Override
public ScoreComponent duplicate()
{
return new ScoreComponent( this );
}
@Override
public ScoreComponent duplicateWithoutFormatting()
{
return new ScoreComponent( this.name, this.objective, this.value );
}
@Override
protected void toLegacyText(StringBuilder builder)
{
builder.append( this.value );
super.toLegacyText( builder );
}
}

View File

@ -0,0 +1,63 @@
package net.md_5.bungee.api.chat;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
* This component processes a target selector into a pre-formatted set of
* discovered names.
* <br>
* Multiple targets may be obtained, and with commas separating each one and a
* final "and" for the last target. The resulting format cannot be overwritten.
* This includes all styling from team prefixes, insertions, click events, and
* hover events.
* <br>
* These values are filled in by the server-side implementation.
* <br>
* As of 1.12.2, a bug ( MC-56373 ) prevents full usage within hover events.
*/
@Getter
@Setter
@ToString
@AllArgsConstructor
public final class SelectorComponent extends BaseComponent
{
/**
* An entity target selector (@p, @a, @r, @e, or @s) and, optionally,
* selector arguments (e.g. @e[r=10,type=Creeper]).
*/
private String selector;
/**
* Creates a selector component from the original to clone it.
*
* @param original the original for the new selector component
*/
public SelectorComponent(SelectorComponent original)
{
super( original );
setSelector( original.getSelector() );
}
@Override
public SelectorComponent duplicate()
{
return new SelectorComponent( this );
}
@Override
public SelectorComponent duplicateWithoutFormatting()
{
return new SelectorComponent( this.selector );
}
@Override
protected void toLegacyText(StringBuilder builder)
{
builder.append( this.selector );
super.toLegacyText( builder );
}
}

View File

@ -9,6 +9,8 @@ import com.google.gson.JsonObject;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.KeybindComponent; import net.md_5.bungee.api.chat.KeybindComponent;
import net.md_5.bungee.api.chat.ScoreComponent;
import net.md_5.bungee.api.chat.SelectorComponent;
import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.chat.TranslatableComponent; import net.md_5.bungee.api.chat.TranslatableComponent;
@ -23,6 +25,8 @@ public class ComponentSerializer implements JsonDeserializer<BaseComponent>
registerTypeAdapter( TextComponent.class, new TextComponentSerializer() ). registerTypeAdapter( TextComponent.class, new TextComponentSerializer() ).
registerTypeAdapter( TranslatableComponent.class, new TranslatableComponentSerializer() ). registerTypeAdapter( TranslatableComponent.class, new TranslatableComponentSerializer() ).
registerTypeAdapter( KeybindComponent.class, new KeybindComponentSerializer() ). registerTypeAdapter( KeybindComponent.class, new KeybindComponentSerializer() ).
registerTypeAdapter( ScoreComponent.class, new ScoreComponentSerializer() ).
registerTypeAdapter( SelectorComponent.class, new SelectorComponentSerializer() ).
create(); create();
public final static ThreadLocal<HashSet<BaseComponent>> serializedComponents = new ThreadLocal<HashSet<BaseComponent>>(); public final static ThreadLocal<HashSet<BaseComponent>> serializedComponents = new ThreadLocal<HashSet<BaseComponent>>();
@ -65,6 +69,14 @@ public class ComponentSerializer implements JsonDeserializer<BaseComponent>
{ {
return context.deserialize( json, KeybindComponent.class ); return context.deserialize( json, KeybindComponent.class );
} }
if ( object.has( "score" ) )
{
return context.deserialize( json, ScoreComponent.class );
}
if ( object.has( "selector" ) )
{
return context.deserialize( json, SelectorComponent.class );
}
return context.deserialize( json, TextComponent.class ); return context.deserialize( json, TextComponent.class );
} }
} }

View File

@ -0,0 +1,49 @@
package net.md_5.bungee.chat;
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 net.md_5.bungee.api.chat.ScoreComponent;
public class ScoreComponentSerializer extends BaseComponentSerializer implements JsonSerializer<ScoreComponent>, JsonDeserializer<ScoreComponent>
{
@Override
public ScoreComponent deserialize(JsonElement element, Type type, JsonDeserializationContext context) throws JsonParseException
{
JsonObject json = element.getAsJsonObject();
if ( !json.has( "name" ) || !json.has( "objective" ) )
{
throw new JsonParseException( "A score component needs at least a name and an objective" );
}
String name = json.get( "name" ).getAsString();
String objective = json.get( "objective" ).getAsString();
ScoreComponent component = new ScoreComponent( name, objective );
if ( json.has( "value" ) && !json.get( "value" ).getAsString().isEmpty() )
{
component.setValue( json.get( "value" ).getAsString() );
}
deserialize( json, component, context );
return component;
}
@Override
public JsonElement serialize(ScoreComponent component, Type type, JsonSerializationContext context)
{
JsonObject root = new JsonObject();
serialize( root, component, context );
JsonObject json = new JsonObject();
json.addProperty( "name", component.getName() );
json.addProperty( "objective", component.getObjective() );
json.addProperty( "value", component.getValue() );
root.add( "score", json );
return root;
}
}

View File

@ -0,0 +1,33 @@
package net.md_5.bungee.chat;
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 net.md_5.bungee.api.chat.SelectorComponent;
public class SelectorComponentSerializer extends BaseComponentSerializer implements JsonSerializer<SelectorComponent>, JsonDeserializer<SelectorComponent>
{
@Override
public SelectorComponent deserialize(JsonElement element, Type type, JsonDeserializationContext context) throws JsonParseException
{
JsonObject object = element.getAsJsonObject();
SelectorComponent component = new SelectorComponent( object.get( "selector" ).getAsString() );
deserialize( object, component, context );
return component;
}
@Override
public JsonElement serialize(SelectorComponent component, Type type, JsonSerializationContext context)
{
JsonObject object = new JsonObject();
serialize( object, component, context );
object.addProperty( "selector", component.getSelector() );
return object;
}
}

View File

@ -55,6 +55,9 @@ import net.md_5.bungee.api.ReconnectHandler;
import net.md_5.bungee.api.ServerPing; import net.md_5.bungee.api.ServerPing;
import net.md_5.bungee.api.Title; import net.md_5.bungee.api.Title;
import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.KeybindComponent;
import net.md_5.bungee.api.chat.ScoreComponent;
import net.md_5.bungee.api.chat.SelectorComponent;
import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.chat.TranslatableComponent; import net.md_5.bungee.api.chat.TranslatableComponent;
import net.md_5.bungee.api.config.ConfigurationAdapter; import net.md_5.bungee.api.config.ConfigurationAdapter;
@ -64,6 +67,9 @@ import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Plugin; import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.api.plugin.PluginManager; import net.md_5.bungee.api.plugin.PluginManager;
import net.md_5.bungee.chat.ComponentSerializer; import net.md_5.bungee.chat.ComponentSerializer;
import net.md_5.bungee.chat.KeybindComponentSerializer;
import net.md_5.bungee.chat.ScoreComponentSerializer;
import net.md_5.bungee.chat.SelectorComponentSerializer;
import net.md_5.bungee.chat.TextComponentSerializer; import net.md_5.bungee.chat.TextComponentSerializer;
import net.md_5.bungee.chat.TranslatableComponentSerializer; import net.md_5.bungee.chat.TranslatableComponentSerializer;
import net.md_5.bungee.command.CommandBungee; import net.md_5.bungee.command.CommandBungee;
@ -152,6 +158,9 @@ public class BungeeCord extends ProxyServer
.registerTypeAdapter( BaseComponent.class, new ComponentSerializer() ) .registerTypeAdapter( BaseComponent.class, new ComponentSerializer() )
.registerTypeAdapter( TextComponent.class, new TextComponentSerializer() ) .registerTypeAdapter( TextComponent.class, new TextComponentSerializer() )
.registerTypeAdapter( TranslatableComponent.class, new TranslatableComponentSerializer() ) .registerTypeAdapter( TranslatableComponent.class, new TranslatableComponentSerializer() )
.registerTypeAdapter( KeybindComponent.class, new KeybindComponentSerializer() )
.registerTypeAdapter( ScoreComponent.class, new ScoreComponentSerializer() )
.registerTypeAdapter( SelectorComponent.class, new SelectorComponentSerializer() )
.registerTypeAdapter( ServerPing.PlayerInfo.class, new PlayerInfoSerializer() ) .registerTypeAdapter( ServerPing.PlayerInfo.class, new PlayerInfoSerializer() )
.registerTypeAdapter( Favicon.class, Favicon.getFaviconTypeAdapter() ).create(); .registerTypeAdapter( Favicon.class, Favicon.getFaviconTypeAdapter() ).create();
@Getter @Getter

View File

@ -61,6 +61,7 @@ import net.md_5.bungee.protocol.packet.SetCompression;
import net.md_5.bungee.tab.ServerUnique; import net.md_5.bungee.tab.ServerUnique;
import net.md_5.bungee.tab.TabList; import net.md_5.bungee.tab.TabList;
import net.md_5.bungee.util.CaseInsensitiveSet; import net.md_5.bungee.util.CaseInsensitiveSet;
import net.md_5.bungee.util.ChatComponentTransformer;
@RequiredArgsConstructor @RequiredArgsConstructor
public final class UserConnection implements ProxiedPlayer public final class UserConnection implements ProxiedPlayer
@ -404,6 +405,9 @@ public final class UserConnection implements ProxiedPlayer
@Override @Override
public void sendMessage(ChatMessageType position, BaseComponent... message) public void sendMessage(ChatMessageType position, BaseComponent... message)
{ {
// transform score components
message = ChatComponentTransformer.getInstance().transform( this, message );
// Action bar doesn't display the new JSON formattings, legacy works - send it using this for now // Action bar doesn't display the new JSON formattings, legacy works - send it using this for now
if ( position == ChatMessageType.ACTION_BAR ) if ( position == ChatMessageType.ACTION_BAR )
{ {
@ -417,6 +421,8 @@ public final class UserConnection implements ProxiedPlayer
@Override @Override
public void sendMessage(ChatMessageType position, BaseComponent message) public void sendMessage(ChatMessageType position, BaseComponent message)
{ {
message = ChatComponentTransformer.getInstance().transform( this, message )[0];
// Action bar doesn't display the new JSON formattings, legacy works - send it using this for now // Action bar doesn't display the new JSON formattings, legacy works - send it using this for now
if ( position == ChatMessageType.ACTION_BAR ) if ( position == ChatMessageType.ACTION_BAR )
{ {
@ -594,23 +600,27 @@ public final class UserConnection implements ProxiedPlayer
return ImmutableMap.copyOf( forgeClientHandler.getClientModList() ); return ImmutableMap.copyOf( forgeClientHandler.getClientModList() );
} }
private static final String EMPTY_TEXT = ComponentSerializer.toString( new TextComponent( "" ) );
@Override @Override
public void setTabHeader(BaseComponent header, BaseComponent footer) public void setTabHeader(BaseComponent header, BaseComponent footer)
{ {
header = ChatComponentTransformer.getInstance().transform( this, header )[0];
footer = ChatComponentTransformer.getInstance().transform( this, footer )[0];
unsafe().sendPacket( new PlayerListHeaderFooter( unsafe().sendPacket( new PlayerListHeaderFooter(
( header != null ) ? ComponentSerializer.toString( header ) : EMPTY_TEXT, ComponentSerializer.toString( header ),
( footer != null ) ? ComponentSerializer.toString( footer ) : EMPTY_TEXT ComponentSerializer.toString( footer )
) ); ) );
} }
@Override @Override
public void setTabHeader(BaseComponent[] header, BaseComponent[] footer) public void setTabHeader(BaseComponent[] header, BaseComponent[] footer)
{ {
header = ChatComponentTransformer.getInstance().transform( this, header );
footer = ChatComponentTransformer.getInstance().transform( this, footer );
unsafe().sendPacket( new PlayerListHeaderFooter( unsafe().sendPacket( new PlayerListHeaderFooter(
( header != null ) ? ComponentSerializer.toString( header ) : EMPTY_TEXT, ComponentSerializer.toString( header ),
( footer != null ) ? ComponentSerializer.toString( footer ) : EMPTY_TEXT ComponentSerializer.toString( footer )
) ); ) );
} }
@ -647,4 +657,10 @@ public final class UserConnection implements ProxiedPlayer
{ {
return !ch.isClosed(); return !ch.isClosed();
} }
@Override
public Scoreboard getScoreboard()
{
return serverSentScoreboard;
}
} }

View File

@ -0,0 +1,121 @@
package net.md_5.bungee.util;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import java.util.List;
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.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;
/**
* This class transforms chat components by attempting to replace transformable
* fields with the appropriate value.
* <br>
* ScoreComponents are transformed by replacing their
* {@link ScoreComponent#getName()}} into the matching entity's name as well as
* replacing the {@link ScoreComponent#getValue()} with the matching value in
* the {@link net.md_5.bungee.api.score.Scoreboard} if and only if the
* {@link ScoreComponent#getValue()} is not present.
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class ChatComponentTransformer
{
private static final ChatComponentTransformer INSTANCE = new ChatComponentTransformer();
/**
* The Pattern to match entity selectors.
*/
private static final Pattern SELECTOR_PATTERN = Pattern.compile( "^@([pares])(?:\\[([^ ]*)\\])?$" );
public static ChatComponentTransformer getInstance()
{
return INSTANCE;
}
/**
* 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 component 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)
{
if ( component == null || component.length < 1 )
{
return new BaseComponent[]
{
new TextComponent( "" )
};
}
for ( BaseComponent root : component )
{
if ( root.getExtra() != null && !root.getExtra().isEmpty() )
{
List<BaseComponent> list = Lists.newArrayList( transform( player, root.getExtra().toArray( new BaseComponent[ root.getExtra().size() ] ) ) );
root.setExtra( list );
}
if ( root instanceof ScoreComponent )
{
transformScoreComponent( player, (ScoreComponent) root );
}
}
return component;
}
/**
* Transform a ScoreComponent by replacing the name and value with the
* appropriate values.
*
* @param component the component to transform
* @param scoreboard the scoreboard to retrieve scores from
* @param player the player to use for the component's name
*/
private void transformScoreComponent(ProxiedPlayer player, ScoreComponent component)
{
Preconditions.checkArgument( !isSelectorPattern( component.getName() ), "Cannot transform entity selector patterns" );
if ( component.getValue() != null && !component.getValue().isEmpty() )
{
return; // pre-defined values override scoreboard values
}
// check for '*' wildcard
if ( component.getName().equals( "*" ) )
{
component.setName( player.getName() );
}
if ( player.getScoreboard().getObjective( component.getObjective() ) != null )
{
Score score = player.getScoreboard().getScore( component.getName() );
if ( score != null )
{
component.setValue( Integer.toString( score.getValue() ) );
}
}
}
/**
* Checks if the given string is an entity selector.
*
* @param pattern the pattern to check
* @return true if it is an entity selector
*/
public boolean isSelectorPattern(String pattern)
{
return SELECTOR_PATTERN.matcher( pattern ).matches();
}
}