#2342: Add score and selector components to chat API
This commit is contained in:
parent
e23195f5f2
commit
a3b44aa612
@ -10,6 +10,7 @@ import net.md_5.bungee.api.SkinConfiguration;
|
||||
import net.md_5.bungee.api.Title;
|
||||
import net.md_5.bungee.api.chat.BaseComponent;
|
||||
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,
|
||||
@ -275,4 +276,11 @@ public interface ProxiedPlayer extends Connection, CommandSender
|
||||
* not occurred for this {@link ProxiedPlayer} yet.
|
||||
*/
|
||||
Map<String, String> getModList();
|
||||
|
||||
/**
|
||||
* Get the {@link Scoreboard} that belongs to this player.
|
||||
*
|
||||
* @return this player's {@link Scoreboard}
|
||||
*/
|
||||
Scoreboard getScoreboard();
|
||||
}
|
||||
|
@ -62,6 +62,11 @@ public class Scoreboard
|
||||
scores.put( score.getItemName(), score );
|
||||
}
|
||||
|
||||
public Score getScore(String name)
|
||||
{
|
||||
return scores.get( name );
|
||||
}
|
||||
|
||||
public void addTeam(Team team)
|
||||
{
|
||||
Preconditions.checkNotNull( team, "team" );
|
||||
|
@ -35,7 +35,7 @@ public final class KeybindComponent extends BaseComponent
|
||||
* Creates a keybind component with the passed internal keybind value.
|
||||
*
|
||||
* @param keybind the keybind value
|
||||
* @see Keybind
|
||||
* @see Keybinds
|
||||
*/
|
||||
public KeybindComponent(String keybind)
|
||||
{
|
||||
|
@ -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 );
|
||||
}
|
||||
}
|
@ -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 );
|
||||
}
|
||||
}
|
@ -9,6 +9,8 @@ import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
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.TranslatableComponent;
|
||||
|
||||
@ -23,6 +25,8 @@ public class ComponentSerializer implements JsonDeserializer<BaseComponent>
|
||||
registerTypeAdapter( TextComponent.class, new TextComponentSerializer() ).
|
||||
registerTypeAdapter( TranslatableComponent.class, new TranslatableComponentSerializer() ).
|
||||
registerTypeAdapter( KeybindComponent.class, new KeybindComponentSerializer() ).
|
||||
registerTypeAdapter( ScoreComponent.class, new ScoreComponentSerializer() ).
|
||||
registerTypeAdapter( SelectorComponent.class, new SelectorComponentSerializer() ).
|
||||
create();
|
||||
|
||||
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 );
|
||||
}
|
||||
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 );
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -55,6 +55,9 @@ import net.md_5.bungee.api.ReconnectHandler;
|
||||
import net.md_5.bungee.api.ServerPing;
|
||||
import net.md_5.bungee.api.Title;
|
||||
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.TranslatableComponent;
|
||||
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.PluginManager;
|
||||
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.TranslatableComponentSerializer;
|
||||
import net.md_5.bungee.command.CommandBungee;
|
||||
@ -152,6 +158,9 @@ public class BungeeCord extends ProxyServer
|
||||
.registerTypeAdapter( BaseComponent.class, new ComponentSerializer() )
|
||||
.registerTypeAdapter( TextComponent.class, new TextComponentSerializer() )
|
||||
.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( Favicon.class, Favicon.getFaviconTypeAdapter() ).create();
|
||||
@Getter
|
||||
|
@ -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.TabList;
|
||||
import net.md_5.bungee.util.CaseInsensitiveSet;
|
||||
import net.md_5.bungee.util.ChatComponentTransformer;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class UserConnection implements ProxiedPlayer
|
||||
@ -404,6 +405,9 @@ public final class UserConnection implements ProxiedPlayer
|
||||
@Override
|
||||
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
|
||||
if ( position == ChatMessageType.ACTION_BAR )
|
||||
{
|
||||
@ -417,6 +421,8 @@ public final class UserConnection implements ProxiedPlayer
|
||||
@Override
|
||||
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
|
||||
if ( position == ChatMessageType.ACTION_BAR )
|
||||
{
|
||||
@ -594,23 +600,27 @@ public final class UserConnection implements ProxiedPlayer
|
||||
return ImmutableMap.copyOf( forgeClientHandler.getClientModList() );
|
||||
}
|
||||
|
||||
private static final String EMPTY_TEXT = ComponentSerializer.toString( new TextComponent( "" ) );
|
||||
|
||||
@Override
|
||||
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(
|
||||
( header != null ) ? ComponentSerializer.toString( header ) : EMPTY_TEXT,
|
||||
( footer != null ) ? ComponentSerializer.toString( footer ) : EMPTY_TEXT
|
||||
ComponentSerializer.toString( header ),
|
||||
ComponentSerializer.toString( footer )
|
||||
) );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTabHeader(BaseComponent[] header, BaseComponent[] footer)
|
||||
{
|
||||
header = ChatComponentTransformer.getInstance().transform( this, header );
|
||||
footer = ChatComponentTransformer.getInstance().transform( this, footer );
|
||||
|
||||
unsafe().sendPacket( new PlayerListHeaderFooter(
|
||||
( header != null ) ? ComponentSerializer.toString( header ) : EMPTY_TEXT,
|
||||
( footer != null ) ? ComponentSerializer.toString( footer ) : EMPTY_TEXT
|
||||
ComponentSerializer.toString( header ),
|
||||
ComponentSerializer.toString( footer )
|
||||
) );
|
||||
}
|
||||
|
||||
@ -647,4 +657,10 @@ public final class UserConnection implements ProxiedPlayer
|
||||
{
|
||||
return !ch.isClosed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Scoreboard getScoreboard()
|
||||
{
|
||||
return serverSentScoreboard;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user