diff --git a/api/src/main/java/net/md_5/bungee/api/ProxyServer.java b/api/src/main/java/net/md_5/bungee/api/ProxyServer.java index b04d728c3..c236d4def 100644 --- a/api/src/main/java/net/md_5/bungee/api/ProxyServer.java +++ b/api/src/main/java/net/md_5/bungee/api/ProxyServer.java @@ -283,4 +283,14 @@ public abstract class ProxyServer */ public abstract Collection matchPlayer(String name); + /** + * Creates a new empty title configuration. + * In most cases you will want to {@link #reset()} the current title first so + * your title won't be affected by a previous one. + * + * @return A new empty title configuration. + * @see Title + */ + public abstract Title createTitle(); + } diff --git a/api/src/main/java/net/md_5/bungee/api/Title.java b/api/src/main/java/net/md_5/bungee/api/Title.java new file mode 100644 index 000000000..ae426f025 --- /dev/null +++ b/api/src/main/java/net/md_5/bungee/api/Title.java @@ -0,0 +1,109 @@ +package net.md_5.bungee.api; + +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.connection.ProxiedPlayer; + +/** + * Represents a configuration of a title. + * A title in Minecraft consists of a main title and a sub title. + * It will {@link #fadeIn(int)}, {@link #stay(int)}, and {@link #fadeOut(int)} + * for a specified amount of time. + * In most cases you will want to {@link #reset()} the current title first so + * your title won't be affected by a previous one. + *

+ * You can create a new configuration by calling {@link ProxyServer#createTitle()}. + */ +public interface Title +{ + /** + * Set the title to send to the player. + * + * @param text The text to use as the title. + * @return This title configuration. + */ + public Title title(BaseComponent text); + + /** + * Set the title to send to the player. + * + * @param text The text to use as the title. + * @return This title configuration. + */ + public Title title(BaseComponent... text); + + + /** + * Set the subtitle to send to the player. + * + * @param text The text to use as the subtitle. + * @return This title configuration. + */ + public Title subTitle(BaseComponent text); + + /** + * Set the subtitle to send to the player. + * + * @param text The text to use as the subtitle. + * @return This title configuration. + */ + public Title subTitle(BaseComponent... text); + + + /** + * Set the duration in ticks of the fade in effect of the title. + * Once this period of time is over the title will stay for the amount + * of time specified in {@link #stay(int)}. + * The default value for the official Minecraft version is 20 (1 second). + * + * @param ticks The amount of ticks (1/20 second) for the fade in effect. + * @return This title configuration. + */ + public Title fadeIn(int ticks); + + /** + * Set the duration in ticks how long the title should stay on the screen. + * Once this period of time is over the title will fade out using the duration + * specified in {@link #fadeOut(int)}. + * The default value for the official Minecraft version is 60 (3 seconds). + * + * @param ticks The amount of ticks (1/20 second) for the fade in effect. + * @return This title configuration. + */ + public Title stay(int ticks); + + /** + * Set the duration in ticks of the fade out effect of the title. + * The default value for the official Minecraft version is 20 (1 second). + * + * @param ticks The amount of ticks (1/20 second) for the fade out effect. + * @return This title configuration. + */ + public Title fadeOut(int ticks); + + + /** + * Remove the currently displayed title from the player's screen. + * This will keep the currently used display times and will only remove the title. + * + * @return This title configuration. + */ + public Title clear(); + + /** + * Remove the currently displayed title from the player's screen + * and set the configuration back to the default values. + * + * @return This title configuration. + */ + public Title reset(); + + + /** + * Send this title configuration to the specified player. + * This is the same as calling {@link ProxiedPlayer#sendTitle(Title)}. + * + * @param player The player to send the title to. + * @return This title configuration. + */ + public Title send(ProxiedPlayer player); +} 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 5657f407a..f3ef5f641 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 @@ -3,6 +3,7 @@ package net.md_5.bungee.api.connection; import java.util.Locale; import net.md_5.bungee.api.Callback; import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.Title; import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.config.ServerInfo; import java.util.UUID; @@ -141,4 +142,13 @@ public interface ProxiedPlayer extends Connection, CommandSender * Clears the header and footer displayed in the tab player list. */ void resetTabHeader(); + + /** + * Sends a {@link Title} to this player. + * This is the same as calling {@link Title#send(ProxiedPlayer)}. + * + * @param title The title to send to the player. + * @see Title + */ + void sendTitle(Title title); } diff --git a/protocol/src/main/java/net/md_5/bungee/protocol/AbstractPacketHandler.java b/protocol/src/main/java/net/md_5/bungee/protocol/AbstractPacketHandler.java index 29f1b9b1b..d292ecd8a 100644 --- a/protocol/src/main/java/net/md_5/bungee/protocol/AbstractPacketHandler.java +++ b/protocol/src/main/java/net/md_5/bungee/protocol/AbstractPacketHandler.java @@ -27,6 +27,7 @@ import net.md_5.bungee.protocol.packet.PingPacket; import net.md_5.bungee.protocol.packet.StatusRequest; import net.md_5.bungee.protocol.packet.StatusResponse; import net.md_5.bungee.protocol.packet.TabCompleteResponse; +import net.md_5.bungee.protocol.packet.Title; public abstract class AbstractPacketHandler { @@ -115,6 +116,10 @@ public abstract class AbstractPacketHandler { } + public void handle(Title title) throws Exception + { + } + public void handle(PluginMessage pluginMessage) throws Exception { } diff --git a/protocol/src/main/java/net/md_5/bungee/protocol/Protocol.java b/protocol/src/main/java/net/md_5/bungee/protocol/Protocol.java index 2a156bc5e..9671b3a3a 100644 --- a/protocol/src/main/java/net/md_5/bungee/protocol/Protocol.java +++ b/protocol/src/main/java/net/md_5/bungee/protocol/Protocol.java @@ -32,6 +32,7 @@ import net.md_5.bungee.protocol.packet.StatusResponse; import net.md_5.bungee.protocol.packet.TabCompleteRequest; import net.md_5.bungee.protocol.packet.TabCompleteResponse; import net.md_5.bungee.protocol.packet.Team; +import net.md_5.bungee.protocol.packet.Title; public enum Protocol { @@ -61,6 +62,7 @@ public enum Protocol TO_CLIENT.registerPacket( 0x3E, Team.class ); TO_CLIENT.registerPacket( 0x3F, PluginMessage.class ); TO_CLIENT.registerPacket( 0x40, Kick.class ); + TO_CLIENT.registerPacket( 0x45, Title.class ); TO_CLIENT.registerPacket( 0x46, SetCompression.class ); TO_CLIENT.registerPacket( 0x47, PlayerListHeaderFooter.class ); diff --git a/protocol/src/main/java/net/md_5/bungee/protocol/packet/Title.java b/protocol/src/main/java/net/md_5/bungee/protocol/packet/Title.java new file mode 100644 index 000000000..bf525b59d --- /dev/null +++ b/protocol/src/main/java/net/md_5/bungee/protocol/packet/Title.java @@ -0,0 +1,77 @@ +package net.md_5.bungee.protocol.packet; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import io.netty.buffer.ByteBuf; +import net.md_5.bungee.protocol.AbstractPacketHandler; +import net.md_5.bungee.protocol.DefinedPacket; +import net.md_5.bungee.protocol.ProtocolConstants; + +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = false) +public class Title extends DefinedPacket +{ + private Action action; + + // TITLE & SUBTITLE + private String text; + + // TIMES + private int fadeIn; + private int stay; + private int fadeOut; + + @Override + public void read(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) + { + action = Action.values()[readVarInt( buf )]; + switch ( action ) + { + case TITLE: + case SUBTITLE: + text = readString( buf ); + break; + case TIMES: + fadeIn = buf.readInt(); + stay = buf.readInt(); + fadeOut = buf.readInt(); + break; + } + } + + @Override + public void write(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) + { + writeVarInt( action.ordinal(), buf ); + switch ( action ) + { + case TITLE: + case SUBTITLE: + writeString( text, buf ); + break; + case TIMES: + buf.writeInt( fadeIn ); + buf.writeInt( stay ); + buf.writeInt( fadeOut ); + break; + } + } + + @Override + public void handle(AbstractPacketHandler handler) throws Exception + { + handler.handle( this ); + } + + public static enum Action + { + TITLE, + SUBTITLE, + TIMES, + CLEAR, + RESET + } +} 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 eebeca08c..e27b245aa 100644 --- a/proxy/src/main/java/net/md_5/bungee/BungeeCord.java +++ b/proxy/src/main/java/net/md_5/bungee/BungeeCord.java @@ -9,6 +9,7 @@ import com.google.common.collect.Sets; import com.google.gson.GsonBuilder; import net.md_5.bungee.api.Favicon; import net.md_5.bungee.api.ServerPing; +import net.md_5.bungee.api.Title; import net.md_5.bungee.module.ModuleManager; import com.google.common.io.ByteStreams; import net.md_5.bungee.api.chat.BaseComponent; @@ -636,4 +637,10 @@ public class BungeeCord extends ProxyServer } } ) ); } + + @Override + public Title createTitle() + { + return new BungeeTitle(); + } } diff --git a/proxy/src/main/java/net/md_5/bungee/BungeeTitle.java b/proxy/src/main/java/net/md_5/bungee/BungeeTitle.java new file mode 100644 index 000000000..5fa6c3fbd --- /dev/null +++ b/proxy/src/main/java/net/md_5/bungee/BungeeTitle.java @@ -0,0 +1,166 @@ +package net.md_5.bungee; + +import net.md_5.bungee.api.Title; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.chat.ComponentSerializer; +import net.md_5.bungee.protocol.DefinedPacket; +import net.md_5.bungee.protocol.ProtocolConstants; +import net.md_5.bungee.protocol.packet.Title.Action; + +public class BungeeTitle implements Title +{ + private net.md_5.bungee.protocol.packet.Title title, subtitle, times, clear, reset; + + private static net.md_5.bungee.protocol.packet.Title createPacket(Action action) + { + net.md_5.bungee.protocol.packet.Title title = new net.md_5.bungee.protocol.packet.Title(); + title.setAction( action ); + + if ( action == Action.TIMES ) + { + // Set packet to default values first + title.setFadeIn( 20 ); + title.setStay( 60 ); + title.setFadeOut( 20 ); + } + return title; + } + + @Override + public Title title(BaseComponent text) + { + if ( title == null ) + { + title = createPacket( Action.TITLE ); + } + + title.setText( ComponentSerializer.toString( text ) ); + return this; + } + + @Override + public Title title(BaseComponent... text) + { + if ( title == null ) + { + title = createPacket( Action.TITLE ); + } + + title.setText( ComponentSerializer.toString( text ) ); + return this; + } + + @Override + public Title subTitle(BaseComponent text) + { + if ( subtitle == null ) + { + subtitle = createPacket( Action.SUBTITLE ); + } + + subtitle.setText( ComponentSerializer.toString( text ) ); + return this; + } + + @Override + public Title subTitle(BaseComponent... text) + { + if ( subtitle == null ) + { + subtitle = createPacket( Action.SUBTITLE ); + } + + subtitle.setText( ComponentSerializer.toString( text ) ); + return this; + } + + @Override + public Title fadeIn(int ticks) + { + if ( times == null ) + { + times = createPacket( Action.TIMES ); + } + + times.setFadeIn( ticks ); + return this; + } + + @Override + public Title stay(int ticks) + { + if ( times == null ) + { + times = createPacket( Action.TIMES ); + } + + times.setStay( ticks ); + return this; + } + + @Override + public Title fadeOut(int ticks) + { + if ( times == null ) + { + times = createPacket( Action.TIMES ); + } + + times.setFadeOut( ticks ); + return this; + } + + @Override + public Title clear() + { + if ( clear == null ) + { + clear = createPacket( Action.CLEAR ); + } + + title = null; // No need to send title if we clear it after that again + + return this; + } + + @Override + public Title reset() + { + if ( reset == null ) + { + reset = createPacket( Action.RESET ); + } + + // No need to send these packets if we reset them later + title = null; + subtitle = null; + times = null; + + return this; + } + + private static void sendPacket(ProxiedPlayer player, DefinedPacket packet) + { + if ( packet != null ) + { + player.unsafe().sendPacket( packet ); + } + } + + @Override + public Title send(ProxiedPlayer player) + { + if ( player.getPendingConnection().getVersion() >= ProtocolConstants.MINECRAFT_SNAPSHOT ) + { + // Send the packets in the correct order + sendPacket( player, clear ); + sendPacket( player, reset ); + sendPacket( player, times ); + sendPacket( player, subtitle ); + sendPacket( player, title ); + } + + return this; + } +} 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 63a2c6607..a719381be 100644 --- a/proxy/src/main/java/net/md_5/bungee/UserConnection.java +++ b/proxy/src/main/java/net/md_5/bungee/UserConnection.java @@ -23,6 +23,7 @@ import lombok.RequiredArgsConstructor; import lombok.Setter; import net.md_5.bungee.api.Callback; import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.Title; import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.config.ServerInfo; @@ -504,6 +505,12 @@ public final class UserConnection implements ProxiedPlayer setTabHeader( (BaseComponent) null, null ); } + @Override + public void sendTitle(Title title) + { + title.send( this ); + } + public void setCompressionThreshold(int compressionThreshold) { if ( this.compressionThreshold == -1 )