diff --git a/api/src/main/java/net/md_5/bungee/api/CommandSender.java b/api/src/main/java/net/md_5/bungee/api/CommandSender.java index 0a51b57f..061f889f 100644 --- a/api/src/main/java/net/md_5/bungee/api/CommandSender.java +++ b/api/src/main/java/net/md_5/bungee/api/CommandSender.java @@ -1,5 +1,7 @@ package net.md_5.bungee.api; +import net.md_5.bungee.api.chat.BaseComponent; + import java.util.Collection; public interface CommandSender @@ -27,6 +29,20 @@ public interface CommandSender */ public void sendMessages(String... messages); + /** + * Send a message to this sender. + * + * @param message the message to send + */ + public void sendMessage(BaseComponent[] message); + + /** + * Send a message to this sender. + * + * @param message the message to send + */ + public void sendMessage(BaseComponent message); + /** * Get all groups this user is part of. This returns an unmodifiable * collection. 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 65d20404..bc7bf2c3 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 @@ -1,5 +1,6 @@ package net.md_5.bungee.api; +import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.plugin.PluginManager; import com.google.common.base.Preconditions; import java.io.File; @@ -233,6 +234,20 @@ public abstract class ProxyServer */ public abstract void broadcast(String message); + /** + * Send the specified message to the console and all connected players. + * + * @param message the message to broadcast + */ + public abstract void broadcast(BaseComponent[] message); + + /** + * Send the specified message to the console and all connected players. + * + * @param message the message to broadcast + */ + public abstract void broadcast(BaseComponent message); + /** * Gets a new instance of this proxies custom tab list. * diff --git a/api/src/main/java/net/md_5/bungee/api/chat/BaseComponent.java b/api/src/main/java/net/md_5/bungee/api/chat/BaseComponent.java new file mode 100644 index 00000000..3a52cf13 --- /dev/null +++ b/api/src/main/java/net/md_5/bungee/api/chat/BaseComponent.java @@ -0,0 +1,256 @@ +package net.md_5.bungee.api.chat; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import net.md_5.bungee.api.ChatColor; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +public abstract class BaseComponent { + + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + BaseComponent parent; + + //Formatting + @Getter(AccessLevel.NONE) + private ChatColor color; + @Getter(AccessLevel.NONE) + private Boolean bold; + @Getter(AccessLevel.NONE) + private Boolean italic; + @Getter(AccessLevel.NONE) + private Boolean underlined; + @Getter(AccessLevel.NONE) + private Boolean strikethrough; + @Getter(AccessLevel.NONE) + private Boolean obfuscated; + + //Appended components + private List extra; + + //Events + private ClickEvent clickEvent; + private HoverEvent hoverEvent; + + public BaseComponent(BaseComponent old) { + setColor(old.getColorRaw()); + setBold(old.isBoldRaw()); + setItalic(old.isItalicRaw()); + setUnderlined(old.isUnderlined()); + setStrikethrough(old.isStrikethroughRaw()); + setObfuscated(old.isObfuscatedRaw()); + } + + + /** + * Returns the color of this component. This uses the parent's color + * if this component doesn't have one. {@link net.md_5.bungee.api.ChatColor#WHITE} + * is returned if no color is found. + * @return the color of this component + */ + public ChatColor getColor() { + if (color == null) { + if (parent == null) { + return ChatColor.WHITE; + } + return parent.getColor(); + } + return color; + } + + /** + * Returns the color of this component without checking the parents + * color. May return null + * @return the color of this component + */ + public ChatColor getColorRaw() { + return color; + } + + /** + * Returns whether this component is bold. This uses the parent's + * setting if this component hasn't been set. false is returned + * if none of the parent chain has been set. + * @return whether the component is bold + */ + public boolean isBold() { + if (bold == null) { + return parent != null && parent.isBold(); + } + return bold; + } + + /** + * Returns whether this component is bold without checking + * the parents setting. May return null + * @return whether the component is bold + */ + public Boolean isBoldRaw() { + return bold; + } + + /** + * Returns whether this component is italic. This uses the parent's + * setting if this component hasn't been set. false is returned + * if none of the parent chain has been set. + * @return whether the component is italic + */ + public boolean isItalic() { + if (italic == null) { + return parent != null && parent.isItalic(); + } + return italic; + } + + /** + * Returns whether this component is italic without checking + * the parents setting. May return null + * @return whether the component is italic + */ + public Boolean isItalicRaw() { + return italic; + } + + /** + * Returns whether this component is underlined. This uses the parent's + * setting if this component hasn't been set. false is returned + * if none of the parent chain has been set. + * @return whether the component is underlined + */ + public boolean isUnderlined() { + if (underlined == null) { + return parent != null && parent.isUnderlined(); + } + return underlined; + } + + /** + * Returns whether this component is underlined without checking + * the parents setting. May return null + * @return whether the component is underlined + */ + public Boolean isUnderlinedRaw() { + return underlined; + } + + /** + * Returns whether this component is strikethrough. This uses the parent's + * setting if this component hasn't been set. false is returned + * if none of the parent chain has been set. + * @return whether the component is strikethrough + */ + public boolean isStrikethrough() { + if (strikethrough == null) { + return parent != null && parent.isStrikethrough(); + } + return strikethrough; + } + + /** + * Returns whether this component is strikethrough without checking + * the parents setting. May return null + * @return whether the component is strikethrough + */ + public Boolean isStrikethroughRaw() { + return strikethrough; + } + + /** + * Returns whether this component is obfuscated. This uses the parent's + * setting if this component hasn't been set. false is returned + * if none of the parent chain has been set. + * @return whether the component is obfuscated + */ + public boolean isObfuscated() { + if (obfuscated == null) { + return parent != null && parent.isObfuscated(); + } + return obfuscated; + } + + /** + * Returns whether this component is obfuscated without checking + * the parents setting. May return null + * @return whether the component is obfuscated + */ + public Boolean isObfuscatedRaw() { + return obfuscated; + } + + public void setExtra(List components) { + for (BaseComponent component : components) { + component.parent = this; + } + extra = components; + } + + /** + * Appends a text element to the component. The text will + * inherit this component's formatting + * @param text the text to append + */ + public void addExtra(String text) { + addExtra(new TextComponent(text)); + } + + /** + * Appends a component to the component. The text will + * inherit this component's formatting + * @param component the component to append + */ + public void addExtra(BaseComponent component) { + if (extra == null) { + extra = new ArrayList<>(); + } + component.parent = this; + extra.add(component); + } + + public boolean hasFormatting() { + return color != null || bold != null || + italic != null || underlined != null || + strikethrough != null || obfuscated != null; + } + + public String toPlainText() { + StringBuilder builder = new StringBuilder(); + toPlainText(builder); + return builder.toString(); + } + + protected void toPlainText(StringBuilder builder) { + if (extra != null) { + for (BaseComponent e : extra) { + e.toPlainText(builder); + } + } + } + + public String toLegacyText() { + StringBuilder builder = new StringBuilder(); + toLegacyText(builder); + return builder.toString(); + } + + protected void toLegacyText(StringBuilder builder) { + if (extra != null) { + for (BaseComponent e : extra) { + e.toLegacyText(builder); + } + } + } + + + @Override + public String toString() { + return String.format("BaseComponent{color=%s, bold=%b, italic=%b, underlined=%b, strikethrough=%b, obfuscated=%b}", getColor().getName(), isBold(), isItalic(), isUnderlined(), isStrikethrough(), isObfuscated()); + } +} diff --git a/api/src/main/java/net/md_5/bungee/api/chat/ClickEvent.java b/api/src/main/java/net/md_5/bungee/api/chat/ClickEvent.java new file mode 100644 index 00000000..05ccf63a --- /dev/null +++ b/api/src/main/java/net/md_5/bungee/api/chat/ClickEvent.java @@ -0,0 +1,23 @@ +package net.md_5.bungee.api.chat; + + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class ClickEvent { + private Action action; + private String value; + + public enum Action { + OPEN_URL, + OPEN_FILE, + RUN_COMMAND, + SUGGEST_COMMAND + } +} diff --git a/api/src/main/java/net/md_5/bungee/api/chat/HoverEvent.java b/api/src/main/java/net/md_5/bungee/api/chat/HoverEvent.java new file mode 100644 index 00000000..00283e35 --- /dev/null +++ b/api/src/main/java/net/md_5/bungee/api/chat/HoverEvent.java @@ -0,0 +1,39 @@ +package net.md_5.bungee.api.chat; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +public class HoverEvent { + @Getter + @Setter + private Action action; + + @Getter + private Object value; + + public HoverEvent(Action action, String value) { + setAction(action); + setValue(value); + } + + public HoverEvent(Action action, BaseComponent value) { + setAction(action); + setValue(value); + } + + public void setValue(String value) { + this.value = value; + } + + public void setValue(BaseComponent value) { + this.value = value; + } + + public enum Action { + SHOW_TEXT, + SHOW_ACHIEVEMENT, + SHOW_ITEM + } +} diff --git a/api/src/main/java/net/md_5/bungee/api/chat/TextComponent.java b/api/src/main/java/net/md_5/bungee/api/chat/TextComponent.java new file mode 100644 index 00000000..6ec8da9e --- /dev/null +++ b/api/src/main/java/net/md_5/bungee/api/chat/TextComponent.java @@ -0,0 +1,43 @@ +package net.md_5.bungee.api.chat; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import net.md_5.bungee.api.ChatColor; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class TextComponent extends BaseComponent { + private String text; + + public TextComponent(TextComponent old) { + super(old); + setText(old.getText()); + } + + @Override + protected void toPlainText(StringBuilder builder) { + builder.append(text); + super.toPlainText(builder); + } + + @Override + protected void toLegacyText(StringBuilder builder) { + builder.append(getColor()); + if (isBold()) builder.append(ChatColor.BOLD); + if (isItalic()) builder.append(ChatColor.ITALIC); + if (isUnderlined()) builder.append(ChatColor.UNDERLINE); + if (isStrikethrough()) builder.append(ChatColor.STRIKETHROUGH); + if (isObfuscated()) builder.append(ChatColor.MAGIC); + builder.append(text); + super.toLegacyText(builder); + } + + @Override + public String toString() { + return String.format("TextComponent{text=%s, %s}", text, super.toString()); + } +} diff --git a/api/src/main/java/net/md_5/bungee/api/chat/TranslatableComponent.java b/api/src/main/java/net/md_5/bungee/api/chat/TranslatableComponent.java new file mode 100644 index 00000000..bdad73aa --- /dev/null +++ b/api/src/main/java/net/md_5/bungee/api/chat/TranslatableComponent.java @@ -0,0 +1,53 @@ +package net.md_5.bungee.api.chat; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +public class TranslatableComponent extends BaseComponent { + + private String translate; + private List with; + + public TranslatableComponent(String translate, Object ...with) { + setTranslate(translate); + this.with = new ArrayList<>(); + for (Object w : with) { + if (w instanceof String) { + this.with.add(new TextComponent((String) w)); + } else { + this.with.add((BaseComponent) w); + } + } + } + + public void setWith(List components) { + for (BaseComponent component : components) { + component.parent = this; + } + with = components; + } + + @Override + protected void toPlainText(StringBuilder builder) { + //TODO + super.toPlainText(builder); + } + + @Override + protected void toLegacyText(StringBuilder builder) { + //TODO + super.toLegacyText(builder); + } + + @Override + public String toString() { + return String.format("TranslatableComponent{translate=%s, with=%s, %s}", translate, with, super.toString()); + } +} 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 a1d3b127..67c8f93f 100644 --- a/proxy/src/main/java/net/md_5/bungee/BungeeCord.java +++ b/proxy/src/main/java/net/md_5/bungee/BungeeCord.java @@ -1,6 +1,8 @@ package net.md_5.bungee; import com.google.common.io.ByteStreams; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.chat.ComponentSerializer; import net.md_5.bungee.log.BungeeLogger; import net.md_5.bungee.reconnect.YamlReconnectHandler; import net.md_5.bungee.scheduler.BungeeScheduler; @@ -487,10 +489,23 @@ public class BungeeCord extends ProxyServer public void broadcast(String message) { getConsole().sendMessage( message ); - // TODO: Here too - for (String msg : ChatConverter.toJSONChat( message )) { - broadcast( new Chat( msg ) ); + broadcast(ComponentSerializer.fromLegacyChat(message)); + } + + @Override + public void broadcast(BaseComponent[] message) { + StringBuilder constr = new StringBuilder(); + for (BaseComponent msg : message) { + constr.append( msg.toLegacyText() ); } + getConsole().sendMessage( constr.toString() ); + broadcast( new Chat(ComponentSerializer.toString(message)) ); + } + + @Override + public void broadcast(BaseComponent message) { + getConsole().sendMessage( message.toLegacyText() ); + broadcast( new Chat(ComponentSerializer.toString(message)) ); } public void addConnection(UserConnection con) diff --git a/proxy/src/main/java/net/md_5/bungee/ChatConverter.java b/proxy/src/main/java/net/md_5/bungee/ChatConverter.java deleted file mode 100644 index 83aa21cc..00000000 --- a/proxy/src/main/java/net/md_5/bungee/ChatConverter.java +++ /dev/null @@ -1,197 +0,0 @@ -package net.md_5.bungee; - -import com.google.gson.Gson; -import com.google.gson.annotations.SerializedName; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class ChatConverter { - - private static final Gson gson = new Gson(); - private static final char COLOR_CHAR = '\u00A7'; - private static final Pattern url = Pattern.compile("^(?:(https?)://)?([-\\w_\\.]{2,}\\.[a-z]{2,4})(/\\S*)?$"); - - public static String[] toJSONChat(String txt) { - Message msg = new Message(); - ArrayList parts = new ArrayList(); - StringBuilder outBuffer = new StringBuilder("["); - StringBuilder buf = new StringBuilder(); - Matcher matcher = url.matcher(txt); - for (int i = 0; i < txt.length(); i++) { - char c = txt.charAt(i); - if (c != COLOR_CHAR) { - int pos = txt.indexOf(' ', i); - if (pos == -1) pos = txt.length(); - if (matcher.region(i, pos).find()) { //Web link handling - msg.text = buf.toString(); - buf = new StringBuilder(); - outBuffer = append(parts, outBuffer, msg); - Message old = msg; - msg = new Message(old); - msg.clickEvent = new ClickEvent(); - msg.clickEvent.action = "open_url"; - String urlString = txt.substring(i, pos); - if (urlString.startsWith("http")) { - msg.text = msg.clickEvent.value = urlString; - } else { - msg.text = urlString; - msg.clickEvent.value = "http://" + urlString; - } - outBuffer = append(parts, outBuffer, msg); - i += pos - i - 1; - msg = new Message(old); - continue; - } - buf.append(c); - continue; - } - i++; - c = txt.charAt(i); - if (c >= 'A' && c <= 'Z') { - c += 32; - } - msg.text = buf.toString(); - buf = new StringBuilder(); - outBuffer = append(parts, outBuffer, msg); - msg = new Message(msg); - switch(c) { - case 'k': - msg.obfuscated = Boolean.TRUE; - break; - case 'l': - msg.bold = Boolean.TRUE; - break; - case 'm': - msg.strikethrough = Boolean.TRUE; - break; - case 'n': - msg.underlined = Boolean.TRUE; - break; - case 'o': - msg.italic = Boolean.TRUE; - break; - default: - msg.obfuscated = null; - msg.bold = null; - msg.strikethrough = null; - msg.underlined = null; - msg.italic = null; - if (c != 'r') { - msg.color = Color.fromCode(Character.toString(c)); - } else { - msg.color = Color.WHITE; - } - break; - } - } - msg.text = buf.toString(); - append(parts, outBuffer, msg); - - parts.add(outBuffer.append("]").toString()); - String[] pArray = new String[parts.size()]; - parts.toArray(pArray); - return pArray; - } - - private static StringBuilder append(ArrayList parts, StringBuilder outBuffer, Message part) { - String p = gson.toJson(part); - if (p.length() + outBuffer.length() + 1 >= Short.MAX_VALUE - 20) { - outBuffer.append("]"); - parts.add(outBuffer.toString()); - outBuffer = new StringBuilder("["); - } - if (outBuffer.length() != 1) { - outBuffer.append(","); - } - outBuffer.append(p); - return outBuffer; - } -} - -class Message { - public String text; - - public Boolean bold; - public Boolean italic; - public Boolean underlined; - public Boolean strikethrough; - public Boolean obfuscated; - - public Color color; - - public ClickEvent clickEvent; - - public Message() { - - } - - public Message(Message old) { - this.bold = old.bold; - this.italic = old.italic; - this.underlined = old.underlined; - this.strikethrough = old.strikethrough; - this.color = old.color; - } -} - -class ClickEvent { - public String action; - public String value; -} - -enum Color { - @SerializedName("black") - BLACK("0"), - @SerializedName("dark_blue") - DARK_BLUE("1"), - @SerializedName("dark_green") - DARK_GREEN("2"), - @SerializedName("dark_aqua") - DARK_AQUA("3"), - @SerializedName("dark_red") - DARK_RED("4"), - @SerializedName("dark_purple") - DARK_PURPLE("5"), - @SerializedName("gold") - GOLD("6"), - @SerializedName("gray") - GRAY("7"), - @SerializedName("dark_gray") - DARK_GRAY("8"), - @SerializedName("blue") - BLUE("9"), - @SerializedName("green") - GREEN("a"), - @SerializedName("aqua") - AQUA("b"), - @SerializedName("red") - RED("c"), - @SerializedName("light_purple") - LIGHT_PURPLE("d"), - @SerializedName("yellow") - YELLOW("e"), - @SerializedName("white") - WHITE("f"); - - public String code; - - Color(String code) { - this.code = code; - } - - - private static HashMap codeMap = new HashMap(); - - public static Color fromCode(String code) { - return codeMap.get(code); - } - - static { - for (Color color : values()) { - codeMap.put(color.code, color); - } - } -} 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 1e4919b1..af5ee912 100644 --- a/proxy/src/main/java/net/md_5/bungee/UserConnection.java +++ b/proxy/src/main/java/net/md_5/bungee/UserConnection.java @@ -20,12 +20,14 @@ import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.Setter; import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.config.ServerInfo; import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.event.PermissionCheckEvent; import net.md_5.bungee.api.event.ServerConnectEvent; import net.md_5.bungee.api.score.Scoreboard; import net.md_5.bungee.api.tab.TabListHandler; +import net.md_5.bungee.chat.ComponentSerializer; import net.md_5.bungee.connection.InitialHandler; import net.md_5.bungee.netty.ChannelWrapper; import net.md_5.bungee.netty.HandlerBoss; @@ -256,7 +258,7 @@ public final class UserConnection implements ProxiedPlayer @Override public synchronized void disconnect(String reason) { - disconnect0( ChatConverter.toJSONChat( reason )[0] ); + disconnect0( ComponentSerializer.toString( ComponentSerializer.fromLegacyChat( reason ) ) ); } public synchronized void disconnect0(String reason) @@ -283,9 +285,7 @@ public final class UserConnection implements ProxiedPlayer @Override public void sendMessage(String message) { - for(String msg : ChatConverter.toJSONChat( message )) { - unsafe().sendPacket( new Chat( msg ) ); - } + sendMessage(ComponentSerializer.fromLegacyChat(message)); } @Override @@ -297,6 +297,16 @@ public final class UserConnection implements ProxiedPlayer } } + @Override + public void sendMessage(BaseComponent[] message) { + unsafe().sendPacket( new Chat( ComponentSerializer.toString( message ) ) ); + } + + @Override + public void sendMessage(BaseComponent message) { + unsafe().sendPacket( new Chat( ComponentSerializer.toString( message ) ) ); + } + @Override public void sendData(String channel, byte[] data) { diff --git a/proxy/src/main/java/net/md_5/bungee/chat/BaseComponentSerializer.java b/proxy/src/main/java/net/md_5/bungee/chat/BaseComponentSerializer.java new file mode 100644 index 00000000..60e9049a --- /dev/null +++ b/proxy/src/main/java/net/md_5/bungee/chat/BaseComponentSerializer.java @@ -0,0 +1,95 @@ +package net.md_5.bungee.chat; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.HoverEvent; + +import java.util.Arrays; + +public class BaseComponentSerializer { + + protected void deserialize(JsonObject object, BaseComponent component, JsonDeserializationContext context) { + if (object.has("color")) { + component.setColor(ChatColor.valueOf(object.get("color").getAsString().toUpperCase())); + } + if (object.has("bold")) { + component.setBold(object.get("bold").getAsBoolean()); + } + if (object.has("italic")) { + component.setItalic(object.get("italic").getAsBoolean()); + } + if (object.has("underlined")) { + component.setUnderlined(object.get("underlined").getAsBoolean()); + } + if (object.has("strikethrough")) { + component.setUnderlined(object.get("strikethrough").getAsBoolean()); + } + if (object.has("obfuscated")) { + component.setUnderlined(object.get("obfuscated").getAsBoolean()); + } + if (object.has("extra")) { + component.setExtra(Arrays.asList((BaseComponent[])context.deserialize(object.get("extra"), BaseComponent[].class))); + } + + //Events + if (object.has("clickEvent")) { + JsonObject event = object.getAsJsonObject("clickEvent"); + component.setClickEvent(new ClickEvent( + ClickEvent.Action.valueOf(event.get("action").getAsString().toUpperCase()), + event.get("value").getAsString())); + } + if (object.has("hoverEvent")) { + JsonObject event = object.getAsJsonObject("hoverEvent"); + HoverEvent hoverEvent = new HoverEvent(); + hoverEvent.setAction(HoverEvent.Action.valueOf(event.get("action").getAsString().toUpperCase())); + Object res = context.deserialize(event.get("value"), BaseComponent.class); + if (res instanceof String) { + hoverEvent.setValue((String) res); + } else { + hoverEvent.setValue((BaseComponent) res); + } + component.setHoverEvent(hoverEvent); + } + } + + protected void serialize(JsonObject object, BaseComponent component, JsonSerializationContext context) { + if (component.getColorRaw() != null) { + object.addProperty("color", component.getColorRaw().getName()); + } + if (component.isBoldRaw() != null) { + object.addProperty("bold", component.isBoldRaw()); + } + if (component.isItalicRaw() != null) { + object.addProperty("italic", component.isItalicRaw()); + } + if (component.isUnderlinedRaw() != null) { + object.addProperty("underlined", component.isUnderlinedRaw()); + } + if (component.isStrikethroughRaw() != null) { + object.addProperty("strikethrough", component.isStrikethroughRaw()); + } + if (component.isObfuscatedRaw() != null) { + object.addProperty("obfuscated", component.isObfuscatedRaw()); + } + + if (component.getExtra() != null) { + object.add("extra", context.serialize(component.getExtra())); + } + + //Events + if (component.getClickEvent() != null) { + JsonObject clickEvent = new JsonObject(); + clickEvent.addProperty("action", component.getClickEvent().getAction().toString().toLowerCase()); + clickEvent.addProperty("value", component.getClickEvent().getValue()); + } + if (component.getHoverEvent() != null) { + JsonObject clickEvent = new JsonObject(); + clickEvent.addProperty("action", component.getHoverEvent().getAction().toString().toLowerCase()); + clickEvent.add("value", context.serialize(component.getHoverEvent().getValue())); + } + } +} diff --git a/proxy/src/main/java/net/md_5/bungee/chat/ComponentSerializer.java b/proxy/src/main/java/net/md_5/bungee/chat/ComponentSerializer.java new file mode 100644 index 00000000..7dd90c0a --- /dev/null +++ b/proxy/src/main/java/net/md_5/bungee/chat/ComponentSerializer.java @@ -0,0 +1,147 @@ +package net.md_5.bungee.chat; + +import com.google.gson.*; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.chat.TranslatableComponent; + +import javax.xml.soap.Text; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ComponentSerializer implements JsonSerializer, JsonDeserializer { + + private final static Gson gson = new GsonBuilder(). + registerTypeAdapter(BaseComponent.class, new ComponentSerializer()). + registerTypeAdapter(TextComponent.class, new TextComponentSerializer()). + registerTypeAdapter(TranslatableComponent.class, new TranslatableComponentSerializer()). + create(); + + private static final Pattern url = Pattern.compile("^(?:(https?)://)?([-\\w_\\.]{2,}\\.[a-z]{2,4})(/\\S*)?$"); + + public static BaseComponent[] parse(String json) { + if (json.startsWith("[")) { //Array + return gson.fromJson(json, BaseComponent[].class); + } + return new BaseComponent[]{gson.fromJson(json, BaseComponent.class)}; + } + + public static String toString(BaseComponent component) { + return gson.toJson(component); + } + + public static String toString(BaseComponent[] components) { + return gson.toJson(components); + } + + public static BaseComponent[] fromLegacyChat(String message) { + ArrayList components = new ArrayList<>(); + StringBuilder builder = new StringBuilder(); + TextComponent component = new TextComponent(); + Matcher matcher = url.matcher(message); + + for ( int i = 0; i < message.length(); i++ ) { + char c = message.charAt(i); + if (c == ChatColor.COLOR_CHAR) { + i++; + c = message.charAt(i); + if (c >= 'A' && c <= 'Z') { + c += 32; + } + if (builder.length() > 0) { + TextComponent old = component; + component = new TextComponent(old); + old.setText(builder.toString()); + builder = new StringBuilder(); + components.add(old); + } + ChatColor format = ChatColor.getByChar(c); + switch (format) { + case BOLD: + component.setBold(true); + break; + case ITALIC: + component.setItalic(true); + break; + case UNDERLINE: + component.setUnderlined(true); + break; + case STRIKETHROUGH: + component.setStrikethrough(true); + break; + case MAGIC: + component.setObfuscated(true); + break; + case RESET: + format = ChatColor.WHITE; + default: + component = new TextComponent(); + component.setColor(format); + break; + } + continue; + } + int pos = message.indexOf(' ', i); + if (pos == -1) pos = message.length(); + if (matcher.region(i, pos).find()) { //Web link handling + + if (builder.length() > 0) { + TextComponent old = component; + component = new TextComponent(old); + old.setText(builder.toString()); + builder = new StringBuilder(); + components.add(old); + } + + TextComponent old = component; + component = new TextComponent(old); + ClickEvent clickEvent = new ClickEvent(); + clickEvent.setAction(ClickEvent.Action.OPEN_URL); + String urlString = message.substring(i, pos); + if (urlString.startsWith("http")) { + component.setText(urlString); + clickEvent.setValue(urlString); + } else { + component.setText(urlString); + clickEvent.setValue("http://" + urlString); + } + component.setClickEvent(clickEvent); + components.add(component); + i += pos - i - 1; + component = old; + continue; + } + builder.append(c); + } + if (builder.length() > 0) { + component.setText(builder.toString()); + components.add(component); + } + return components.toArray(new BaseComponent[components.size()]); + } + + @Override + public BaseComponent deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if (json.isJsonPrimitive()) { + return new TextComponent(json.getAsString()); + } + JsonObject object = json.getAsJsonObject(); + if (object.has("translate")) { + return context.deserialize(json, TranslatableComponent.class); + } + return context.deserialize(json, TextComponent.class); + } + + @Override + public JsonElement serialize(BaseComponent src, Type typeOfSrc, JsonSerializationContext context) { + return context.serialize(src, src.getClass()); + } +} diff --git a/proxy/src/main/java/net/md_5/bungee/chat/TextComponentSerializer.java b/proxy/src/main/java/net/md_5/bungee/chat/TextComponentSerializer.java new file mode 100644 index 00000000..a8d758e3 --- /dev/null +++ b/proxy/src/main/java/net/md_5/bungee/chat/TextComponentSerializer.java @@ -0,0 +1,28 @@ +package net.md_5.bungee.chat; + +import com.google.gson.*; +import net.md_5.bungee.api.chat.TextComponent; + +import java.lang.reflect.Type; + +public class TextComponentSerializer extends BaseComponentSerializer implements JsonSerializer , JsonDeserializer{ + @Override + public TextComponent deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + TextComponent component = new TextComponent(); + JsonObject object = json.getAsJsonObject(); + deserialize(object, component, context); + component.setText(object.get("text").getAsString()); + return component; + } + + @Override + public JsonElement serialize(TextComponent src, Type typeOfSrc, JsonSerializationContext context) { + if (!src.hasFormatting()) { + return new JsonPrimitive(src.getText()); + } + JsonObject object = new JsonObject(); + serialize(object, src, context); + object.addProperty("text", src.getText()); + return object; + } +} diff --git a/proxy/src/main/java/net/md_5/bungee/chat/TranslatableComponentSerializer.java b/proxy/src/main/java/net/md_5/bungee/chat/TranslatableComponentSerializer.java new file mode 100644 index 00000000..364c286c --- /dev/null +++ b/proxy/src/main/java/net/md_5/bungee/chat/TranslatableComponentSerializer.java @@ -0,0 +1,35 @@ +package net.md_5.bungee.chat; + +import com.google.gson.*; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.chat.TranslatableComponent; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; + +public class TranslatableComponentSerializer extends BaseComponentSerializer implements JsonSerializer , JsonDeserializer{ + @Override + public TranslatableComponent deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + TranslatableComponent component = new TranslatableComponent(); + JsonObject object = json.getAsJsonObject(); + deserialize(object, component, context); + component.setTranslate(object.get("translate").getAsString()); + if (object.has("with")) { + component.setWith(Arrays.asList((BaseComponent[])context.deserialize(object.get("with"), BaseComponent[].class))); + } + return component; + } + + @Override + public JsonElement serialize(TranslatableComponent src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject object = new JsonObject(); + serialize(object, src, context); + object.addProperty("translate", src.getTranslate()); + if (src.getWith() != null) { + object.add("with", context.serialize(src.getWith())); + } + return object; + } +} diff --git a/proxy/src/main/java/net/md_5/bungee/command/ConsoleCommandSender.java b/proxy/src/main/java/net/md_5/bungee/command/ConsoleCommandSender.java index 65a2505a..3cce5a38 100644 --- a/proxy/src/main/java/net/md_5/bungee/command/ConsoleCommandSender.java +++ b/proxy/src/main/java/net/md_5/bungee/command/ConsoleCommandSender.java @@ -5,6 +5,7 @@ import java.util.Collections; import lombok.Getter; import net.md_5.bungee.api.CommandSender; import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.chat.BaseComponent; /** * Command sender representing the proxy console. @@ -34,6 +35,20 @@ public class ConsoleCommandSender implements CommandSender } } + @Override + public void sendMessage(BaseComponent[] message) { + StringBuilder constr = new StringBuilder(); + for (BaseComponent msg : message) { + constr.append( msg.toLegacyText() ); + } + sendMessage( constr.toString() ); + } + + @Override + public void sendMessage(BaseComponent message) { + sendMessage( message.toLegacyText() ); + } + @Override public String getName() { diff --git a/proxy/src/main/java/net/md_5/bungee/connection/InitialHandler.java b/proxy/src/main/java/net/md_5/bungee/connection/InitialHandler.java index 05ab8e98..a5cb8bb2 100644 --- a/proxy/src/main/java/net/md_5/bungee/connection/InitialHandler.java +++ b/proxy/src/main/java/net/md_5/bungee/connection/InitialHandler.java @@ -23,6 +23,7 @@ import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.event.LoginEvent; import net.md_5.bungee.api.event.PostLoginEvent; import net.md_5.bungee.api.event.ProxyPingEvent; +import net.md_5.bungee.chat.ComponentSerializer; import net.md_5.bungee.http.HttpClient; import net.md_5.bungee.netty.HandlerBoss; import net.md_5.bungee.netty.ChannelWrapper; @@ -397,7 +398,7 @@ public class InitialHandler extends PacketHandler implements PendingConnection { if ( !ch.isClosed() ) { - unsafe().sendPacket( new Kick( ChatConverter.toJSONChat( reason )[0] ) ); + unsafe().sendPacket( new Kick( ComponentSerializer.toString(ComponentSerializer.fromLegacyChat(reason)) ) ); ch.close(); } }