diff --git a/api/src/main/java/net/md_5/bungee/api/connection/ProxiedPlayer.java b/api/src/main/java/net/md_5/bungee/api/connection/ProxiedPlayer.java index 16cfff8bb..2bb92a93e 100644 --- a/api/src/main/java/net/md_5/bungee/api/connection/ProxiedPlayer.java +++ b/api/src/main/java/net/md_5/bungee/api/connection/ProxiedPlayer.java @@ -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 getModList(); + + /** + * Get the {@link Scoreboard} that belongs to this player. + * + * @return this player's {@link Scoreboard} + */ + Scoreboard getScoreboard(); } diff --git a/api/src/main/java/net/md_5/bungee/api/score/Scoreboard.java b/api/src/main/java/net/md_5/bungee/api/score/Scoreboard.java index 09069ae76..c10c7b11e 100644 --- a/api/src/main/java/net/md_5/bungee/api/score/Scoreboard.java +++ b/api/src/main/java/net/md_5/bungee/api/score/Scoreboard.java @@ -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" ); diff --git a/chat/src/main/java/net/md_5/bungee/api/chat/KeybindComponent.java b/chat/src/main/java/net/md_5/bungee/api/chat/KeybindComponent.java index 5b074c635..1fc12600a 100644 --- a/chat/src/main/java/net/md_5/bungee/api/chat/KeybindComponent.java +++ b/chat/src/main/java/net/md_5/bungee/api/chat/KeybindComponent.java @@ -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) { diff --git a/chat/src/main/java/net/md_5/bungee/api/chat/ScoreComponent.java b/chat/src/main/java/net/md_5/bungee/api/chat/ScoreComponent.java new file mode 100644 index 000000000..591a8fd49 --- /dev/null +++ b/chat/src/main/java/net/md_5/bungee/api/chat/ScoreComponent.java @@ -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. + *
+ * The name is the name of the player stored on the scoreboard, which may + * be a "fake" player. It can also be a target selector that must resolve + * to 1 target, and may target non-player entities. + *
+ * 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. + *
+ * Signs cannot use the '*' wildcard + *
+ * These values are filled in by the server-side implementation. + *
+ * 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.
+ * 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.
+ * 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 ); + } +} diff --git a/chat/src/main/java/net/md_5/bungee/api/chat/SelectorComponent.java b/chat/src/main/java/net/md_5/bungee/api/chat/SelectorComponent.java new file mode 100644 index 000000000..1977bd810 --- /dev/null +++ b/chat/src/main/java/net/md_5/bungee/api/chat/SelectorComponent.java @@ -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. + *
+ * 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. + *
+ * These values are filled in by the server-side implementation. + *
+ * 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 ); + } +} 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 7d366e246..4d94c79ec 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 @@ -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 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> serializedComponents = new ThreadLocal>(); @@ -65,6 +69,14 @@ public class ComponentSerializer implements JsonDeserializer { 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 ); } } diff --git a/chat/src/main/java/net/md_5/bungee/chat/ScoreComponentSerializer.java b/chat/src/main/java/net/md_5/bungee/chat/ScoreComponentSerializer.java new file mode 100644 index 000000000..ecff9beff --- /dev/null +++ b/chat/src/main/java/net/md_5/bungee/chat/ScoreComponentSerializer.java @@ -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, JsonDeserializer +{ + + @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; + } +} diff --git a/chat/src/main/java/net/md_5/bungee/chat/SelectorComponentSerializer.java b/chat/src/main/java/net/md_5/bungee/chat/SelectorComponentSerializer.java new file mode 100644 index 000000000..34fd2f693 --- /dev/null +++ b/chat/src/main/java/net/md_5/bungee/chat/SelectorComponentSerializer.java @@ -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, JsonDeserializer +{ + + @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; + } +} 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 2b6c69956..5405a537f 100644 --- a/proxy/src/main/java/net/md_5/bungee/BungeeCord.java +++ b/proxy/src/main/java/net/md_5/bungee/BungeeCord.java @@ -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 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 4c8288157..67adfef92 100644 --- a/proxy/src/main/java/net/md_5/bungee/UserConnection.java +++ b/proxy/src/main/java/net/md_5/bungee/UserConnection.java @@ -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; + } } 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 new file mode 100644 index 000000000..2a711698d --- /dev/null +++ b/proxy/src/main/java/net/md_5/bungee/util/ChatComponentTransformer.java @@ -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. + *
+ * 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 cannot 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 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(); + } +}