package cn.citycraft.TellRaw; import static cn.citycraft.TellRaw.common.TextualComponent.rawText; import java.io.IOException; import java.io.StringWriter; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.logging.Level; import org.bukkit.Achievement; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Material; import org.bukkit.Statistic; import org.bukkit.command.CommandSender; import org.bukkit.configuration.serialization.ConfigurationSerializable; import org.bukkit.configuration.serialization.ConfigurationSerialization; import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.Plugin; import com.comphenix.protocol.utility.MinecraftReflection; import cn.citycraft.GsonAgent.GsonAgent; import cn.citycraft.GsonAgent.api.JsonArray; import cn.citycraft.GsonAgent.api.JsonElement; import cn.citycraft.GsonAgent.api.JsonObject; import cn.citycraft.GsonAgent.api.JsonParser; import cn.citycraft.GsonAgent.api.stream.JsonWriter; import cn.citycraft.TellRaw.common.ArrayWrapper; import cn.citycraft.TellRaw.common.JsonRepresentedObject; import cn.citycraft.TellRaw.common.JsonString; import cn.citycraft.TellRaw.common.MessagePart; import cn.citycraft.TellRaw.common.Reflection; import cn.citycraft.TellRaw.common.TextualComponent; import cn.citycraft.TellRaw.internal.FancyMessageInternal; import cn.citycraft.TellRaw.manual.FancyMessageManual; /** * Represents a formattable message. Such messages can use elements such as * colors, formatting codes, hover and click data, and other features provided * by the vanilla Minecraft JSON message * formatter. * This class allows plugins to emulate the functionality of the vanilla * Minecraft tellraw * command. *

* This class follows the builder pattern, allowing for method chaining. It is * set up such that invocations of property-setting methods will affect the * current editing component, and a call to {@link #then()} or * {@link #then(Object)} will append a new editing component to the end of the * message, optionally initializing it with text. Further property-setting * method calls will affect that editing component. *

*/ public abstract class FancyMessage implements JsonRepresentedObject, Cloneable, Iterable, ConfigurationSerializable { private static JsonParser _stringParser; private static Class internalClass; protected static Class chatSerializerClazz; protected static Method fromJsonMethod; protected static Method getNMSAchievementMethod = null; protected static Method getNMSEntityStatisticMethod = null; protected static Method getNMSMaterialStatisticMethod = null; protected static Method getNMSsaveNBTMethod = null; protected static Method getNMSStatisticMethod = null; protected static Method getOBCasNMSCopyMethod = null; protected static Field nmsAchievement_NameField = null; protected static Class nmsAchievementClass = null; protected static Object nmsChatSerializerGsonInstance; protected static Class nmsIChatBaseComponentClass; protected static Class nmsItemStack = null; protected static Class nmsNBTTagCompound = null; protected static Class nmsPacketClass; protected static Constructor nmsPacketPlayOutChatConstructor; protected static Field nmsPlayerConnectionField = null; protected static Method nmsSendPacketMethod = null; protected static Field nmsStatistic_NameField = null; protected static Class nmsStatisticClass = null; protected static Class obcCraftItemStack = null; protected static Class obcCraftStatisticClass = null; static { ConfigurationSerialization.registerClass(FancyMessage.class); if (PluginHelperConfig.getConfig().getBoolean("TellrawManualHandle", false)) { internalClass = FancyMessageManual.class; Bukkit.getLogger().info("[TellRawLib] 指定使用手动序列化类处理..."); } else { boolean useProtocolLib = false; final Plugin plugin = Bukkit.getPluginManager().getPlugin("ProtocolLib"); if (plugin != null) { if (!plugin.isEnabled()) { Bukkit.getPluginManager().enablePlugin(plugin); } if (plugin.isEnabled()) { useProtocolLib = true; } } try { _stringParser = GsonAgent.newJsonParser(); if (!useProtocolLib) { try { nmsPacketPlayOutChatConstructor = Reflection.getNMSClass("PacketPlayOutChat").getDeclaredConstructor(Reflection.getNMSClass("IChatBaseComponent")); nmsPacketPlayOutChatConstructor.setAccessible(true); } catch (final NoSuchMethodException e) { Bukkit.getLogger().log(Level.SEVERE, "Could not find Minecraft method or constructor.", e); throw e; } catch (final SecurityException e) { Bukkit.getLogger().log(Level.WARNING, "Could not access constructor.", e); throw e; } } else { try { nmsPacketPlayOutChatConstructor = com.comphenix.protocol.PacketType.Play.Server.CHAT.getPacketClass().getDeclaredConstructor(MinecraftReflection.getIChatBaseComponentClass()); nmsPacketPlayOutChatConstructor.setAccessible(true); } catch (final NoSuchMethodException ex) { Bukkit.getLogger().log(Level.SEVERE, "Could not find Minecraft method or constructor.", ex); throw ex; } catch (final SecurityException e) { Bukkit.getLogger().log(Level.WARNING, "Could not access constructor.", e); throw e; } } internalClass = FancyMessageInternal.class; if (!useProtocolLib) { try { obcCraftStatisticClass = Reflection.getOBCClass("CraftStatistic"); getNMSStatisticMethod = Reflection.getMethod(obcCraftStatisticClass, "getNMSStatistic", Statistic.class); nmsStatisticClass = Reflection.getNMSClass("Statistic"); try { nmsStatistic_NameField = Reflection.getField(nmsStatisticClass, "name"); } catch (final Exception e) { nmsStatistic_NameField = Reflection.getDeclaredFieldByType(nmsStatisticClass, String.class).get(0); } getNMSMaterialStatisticMethod = Reflection.getMethod(obcCraftStatisticClass, "getMaterialStatistic", Statistic.class, Material.class); getNMSEntityStatisticMethod = Reflection.getMethod(obcCraftStatisticClass, "getEntityStatistic", Statistic.class, EntityType.class); nmsAchievementClass = Reflection.getNMSClass("Achievement"); getNMSAchievementMethod = Reflection.getMethod(obcCraftStatisticClass, "getNMSAchievement", Achievement.class); try { nmsAchievement_NameField = Reflection.getField(nmsAchievementClass, "name"); } catch (final Exception e) { nmsAchievement_NameField = Reflection.getDeclaredFieldByType(nmsAchievementClass, String.class).get(0); } obcCraftItemStack = Reflection.getOBCClass("inventory.CraftItemStack"); getOBCasNMSCopyMethod = Reflection.getMethod(obcCraftItemStack, "asNMSCopy", ItemStack.class); nmsItemStack = Reflection.getNMSClass("ItemStack"); nmsNBTTagCompound = Reflection.getNMSClass("NBTTagCompound"); try { getNMSsaveNBTMethod = Reflection.getMethod(nmsItemStack, "save", nmsNBTTagCompound); } catch (final Exception e) { getNMSsaveNBTMethod = Reflection.getMethodByParamsAndType(nmsItemStack, nmsNBTTagCompound, nmsNBTTagCompound).get(0); } nmsPacketClass = Reflection.getNMSClass("Packet"); try { chatSerializerClazz = Reflection.getNMSClass("IChatBaseComponent$ChatSerializer"); } catch (final Exception e) { chatSerializerClazz = Reflection.getNMSClass("ChatSerializer"); } nmsIChatBaseComponentClass = Reflection.getNMSClass("IChatBaseComponent"); } catch (final Exception e) { throw e; } } else { try { obcCraftStatisticClass = MinecraftReflection.getCraftBukkitClass("CraftStatistic"); getNMSStatisticMethod = Reflection.getMethod(obcCraftStatisticClass, "getNMSStatistic", Statistic.class); nmsStatisticClass = MinecraftReflection.getMinecraftClass("Statistic"); try { nmsStatistic_NameField = Reflection.getField(nmsStatisticClass, "name"); } catch (final Exception e) { nmsStatistic_NameField = Reflection.getDeclaredFieldByType(nmsStatisticClass, String.class).get(0); } getNMSMaterialStatisticMethod = Reflection.getMethod(obcCraftStatisticClass, "getMaterialStatistic", Statistic.class, Material.class); getNMSEntityStatisticMethod = Reflection.getMethod(obcCraftStatisticClass, "getEntityStatistic", Statistic.class, EntityType.class); nmsAchievementClass = MinecraftReflection.getMinecraftClass("Achievement"); getNMSAchievementMethod = Reflection.getMethod(obcCraftStatisticClass, "getNMSAchievement", Achievement.class); try { nmsAchievement_NameField = Reflection.getField(nmsAchievementClass, "name"); } catch (final Exception e) { nmsAchievement_NameField = Reflection.getDeclaredFieldByType(nmsAchievementClass, String.class).get(0); } obcCraftItemStack = MinecraftReflection.getCraftItemStackClass(); getOBCasNMSCopyMethod = Reflection.getMethod(obcCraftItemStack, "asNMSCopy", ItemStack.class); nmsItemStack = MinecraftReflection.getItemStackClass(); nmsNBTTagCompound = MinecraftReflection.getNBTCompoundClass(); try { getNMSsaveNBTMethod = Reflection.getMethod(nmsItemStack, "save", nmsNBTTagCompound); } catch (final Exception e) { getNMSsaveNBTMethod = Reflection.getMethodByParamsAndType(nmsItemStack, nmsNBTTagCompound, nmsNBTTagCompound).get(0); } nmsPacketClass = MinecraftReflection.getPacketClass(); chatSerializerClazz = MinecraftReflection.getChatSerializerClass(); // Since the method is so simple, and all the obfuscated methods have the same name, it's easier to reimplement 'IChatBaseComponent a(String)' than to reflectively call it // Of course, the implementation may change, but fuzzy matches might break with signature changes nmsIChatBaseComponentClass = MinecraftReflection.getIChatBaseComponentClass(); } catch (final Exception e) { throw e; } } if (nmsChatSerializerGsonInstance == null) { // Find the field and its value, completely bypassing obfuscation if (chatSerializerClazz == null) { throw new ClassNotFoundException("Can't find the ChatSerializer class"); } for (final Field declaredField : chatSerializerClazz.getDeclaredFields()) { if (Modifier.isFinal(declaredField.getModifiers()) && Modifier.isStatic(declaredField.getModifiers()) && declaredField.getType().getName().endsWith("Gson")) { // We've found our field declaredField.setAccessible(true); nmsChatSerializerGsonInstance = declaredField.get(null); fromJsonMethod = nmsChatSerializerGsonInstance.getClass().getMethod("fromJson", String.class, Class.class); break; } } } if (!useProtocolLib) { Bukkit.getLogger().info("[TellRawLib] 使用ChatSerializer序列化类处理..."); } else { Bukkit.getLogger().info("[TellRawLib] 使用ProtocolLib序列化类处理..."); } } catch (final Exception | Error e) { internalClass = FancyMessageManual.class; Bukkit.getLogger().info("[TellRawLib] 使用GItemStack序列化类处理..."); } } } private boolean dirty; private String jsonString; protected List messageParts; public FancyMessage(final TextualComponent firstPartText) { messageParts = new ArrayList(); messageParts.add(new MessagePart(firstPartText)); } /** * Deserializes a JSON-represented message from a mapping of key-value * pairs. * This is called by the Bukkit serialization API. * It is not intended for direct public API consumption. * * @param serialized * The key-value mapping which represents a fancy message. */ @SuppressWarnings("unchecked") public static FancyMessage deserialize(final Map serialized) { final FancyMessage msg = newFM(); msg.messageParts = (List) serialized.get("messageParts"); msg.jsonString = serialized.containsKey("JSON") ? serialized.get("JSON").toString() : null; msg.dirty = !serialized.containsKey("JSON"); return msg; } /** * Deserializes a fancy message from its JSON representation. This JSON * representation is of the format of * that returned by {@link #toJSONString()}, and is compatible with vanilla * inputs. * * @param json * The JSON string which represents a fancy message. * @return A {@code FancyMessage} representing the parameterized JSON * message. */ public static FancyMessage deserialize(final String json) { final JsonObject serialized = _stringParser.parse(json).getAsJsonObject(); final JsonArray extra = serialized.getAsJsonArray("extra"); // Get the extra // component final FancyMessage returnVal = newFM(); returnVal.messageParts.clear(); for (final JsonElement mPrt : extra) { final MessagePart component = new MessagePart(); final JsonObject messagePart = mPrt.getAsJsonObject(); for (final Map.Entry entry : messagePart.entrySet()) { // Deserialize text if (TextualComponent.isTextKey(entry.getKey())) { // The map mimics the YAML serialization, which has a "key" field and one or more "value" fields final Map serializedMapForm = new HashMap(); // Must be object due to Bukkit serializer API compliance serializedMapForm.put("key", entry.getKey()); if (entry.getValue().isJsonPrimitive()) { // Assume string serializedMapForm.put("value", entry.getValue().getAsString()); } else { // Composite object, but we assume each element is a string for (final Map.Entry compositeNestedElement : entry.getValue().getAsJsonObject().entrySet()) { serializedMapForm.put("value." + compositeNestedElement.getKey(), compositeNestedElement.getValue().getAsString()); } } component.text = TextualComponent.deserialize(serializedMapForm); } else if (MessagePart.stylesToNames.inverse().containsKey(entry.getKey())) { if (entry.getValue().getAsBoolean()) { component.styles.add(MessagePart.stylesToNames.inverse().get(entry.getKey())); } } else if (entry.getKey().equals("color")) { component.color = ChatColor.valueOf(entry.getValue().getAsString().toUpperCase()); } else if (entry.getKey().equals("clickEvent")) { final JsonObject object = entry.getValue().getAsJsonObject(); component.clickActionName = object.get("action").getAsString(); component.clickActionData = object.get("value").getAsString(); } else if (entry.getKey().equals("hoverEvent")) { final JsonObject object = entry.getValue().getAsJsonObject(); component.hoverActionName = object.get("action").getAsString(); if (object.get("value").isJsonPrimitive()) { // Assume string component.hoverActionData = new JsonString(object.get("value").getAsString()); } else { // Assume composite type component.hoverActionData = deserialize(object.get("value").toString()); } } else if (entry.getKey().equals("insertion")) { component.insertionData = entry.getValue().getAsString(); } else if (entry.getKey().equals("with")) { for (final JsonElement object : entry.getValue().getAsJsonArray()) { if (object.isJsonPrimitive()) { component.translationReplacements.add(new JsonString(object.getAsString())); } else { component.translationReplacements.add(deserialize(object.toString())); } } } } returnVal.messageParts.add(component); } return returnVal; } /** * 获得消息工厂类 * * @return 消息工厂 */ public static FancyMessage newFM() { try { return FancyMessage.internalClass.newInstance(); } catch (final Exception e) { return new FancyMessageManual(); } } /** * 获得消息工厂类 * * @param firstPartText * 首字符串 * @return 消息工厂 */ public static FancyMessage newFM(final String firstPartText) { return newFM().text(firstPartText); } /** * 在客户端显示一个成就. *

* 当前方法将不会继承本类的颜色样式等参数. *

* * @param which * 需要显示的成就 * * @return {@link FancyMessage} */ public abstract FancyMessage achievementTooltip(final Achievement which); /** * 在客户端显示一个成就. *

* 当前方法将不会继承本类的颜色样式等参数. *

* * @param name * 需要显示的成就名称 * * @return {@link FancyMessage} */ public FancyMessage achievementTooltip(final String name) { onHover("show_achievement", new JsonString("achievement." + name)); return this; } @Override public FancyMessage clone() throws CloneNotSupportedException { final FancyMessage instance = (FancyMessage) super.clone(); instance.messageParts = new ArrayList(messageParts.size()); for (int i = 0; i < messageParts.size(); i++) { instance.messageParts.add(i, messageParts.get(i).clone()); } instance.dirty = false; instance.jsonString = null; return instance; } /** * 设置当前被编辑串的 * * @param color * 当前串的颜色 * @return {@link FancyMessage} * @exception IllegalArgumentException * 如果参数不是一个 {@link ChatColor} 的颜色值 (是一个样式字符). */ public FancyMessage color(final ChatColor color) { if (!color.isColor()) { throw new IllegalArgumentException(color.name() + "不是一个颜色"); } latest().color = color; dirty = true; return this; } /** * 服务器将特殊的字符串发送给客户端. *

* 当玩家点击时 客户端 将会 立即将命令发送给服务器. *

* 命令将绑定在当前编辑的文本上 *

* * @param command * 点击执行命令 * @return {@link FancyMessage} */ public FancyMessage command(final String command) { onClick("run_command", command); return this; } /** * Set the behavior of the current editing component to instruct the client * to open a file on the client side filesystem when the currently edited * part of the {@code FancyMessage} is clicked. * * @param path * The path of the file on the client filesystem. * @return {@link FancyMessage} */ public FancyMessage file(final String path) { onClick("open_file", path); return this; } /** * Set the behavior of the current editing component to display formatted * text when the client hovers over the text. *

* Tooltips do not inherit display characteristics, such as color and * styles, from the message component on which they are applied. *

* * @param text * The formatted text which will be displayed to the client upon * hovering. * @return {@link FancyMessage} */ public FancyMessage formattedTooltip(final FancyMessage text) { for (final MessagePart component : text.messageParts) { if (component.clickActionData != null && component.clickActionName != null) { throw new IllegalArgumentException("The tooltip text cannot have click data."); } else if (component.hoverActionData != null && component.hoverActionName != null) { throw new IllegalArgumentException("The tooltip text cannot have a tooltip."); } } onHover("show_text", text); return this; } /** * Set the behavior of the current editing component to display the * specified lines of formatted text when the client hovers over the text. *

* Tooltips do not inherit display characteristics, such as color and * styles, from the message component on which they are applied. *

* * @param lines * The lines of formatted text which will be displayed to the * client upon hovering. * @return {@link FancyMessage} */ public FancyMessage formattedTooltip(final FancyMessage... lines) { if (lines.length < 1) { onHover(null, null); // Clear tooltip return this; } final FancyMessage result = newFM(); result.messageParts.clear(); // Remove the one existing text component // that exists by default, which // destabilizes the object for (int i = 0; i < lines.length; i++) { try { for (final MessagePart component : lines[i]) { if (component.clickActionData != null && component.clickActionName != null) { throw new IllegalArgumentException("The tooltip text cannot have click data."); } else if (component.hoverActionData != null && component.hoverActionName != null) { throw new IllegalArgumentException("The tooltip text cannot have a tooltip."); } if (component.hasText()) { result.messageParts.add(component.clone()); } } if (i != lines.length - 1) { result.messageParts.add(new MessagePart(rawText("\n"))); } } catch (final CloneNotSupportedException e) { Bukkit.getLogger().log(Level.WARNING, "Failed to clone object", e); return this; } } return formattedTooltip(result.messageParts.isEmpty() ? null : result); } /** * Set the behavior of the current editing component to display the * specified lines of formatted text when the client hovers over the text. *

* Tooltips do not inherit display characteristics, such as color and * styles, from the message component on which they are applied. *

* * @param lines * The lines of text which will be displayed to the client upon * hovering. The iteration order of this object will be the order * in which the lines of the tooltip are created. * @return {@link FancyMessage} */ public FancyMessage formattedTooltip(final Iterable lines) { return formattedTooltip(ArrayWrapper.toArray(lines, FancyMessage.class)); } /** * Set the behavior of the current editing component to instruct the client * to append the chat input box content with the specified string when the * currently edited part of the {@code FancyMessage} is SHIFT-CLICKED. * The client will not immediately send the command to the server to be * executed unless the client player submits the command/chat message, * usually with the enter key. * * @param command * The text to append to the chat bar of the client. * @return {@link FancyMessage} */ public FancyMessage insert(final String command) { latest().insertionData = command; dirty = true; return this; } /** * Set the behavior of the current editing component to display information * about an item when the client hovers over the text. *

* Tooltips do not inherit display characteristics, such as color and * styles, from the message component on which they are applied. *

* * @param itemStack * The stack for which to display information. * @return {@link FancyMessage} */ public abstract FancyMessage itemTooltip(final ItemStack itemStack); /** * Set the behavior of the current editing component to display information * about an item when the client hovers over the text. *

* Tooltips do not inherit display characteristics, such as color and * styles, from the message component on which they are applied. *

* * @param itemJSON * A string representing the JSON-serialized NBT data tag of an * {@link ItemStack}. * @return {@link FancyMessage} */ public FancyMessage itemTooltip(final String itemJSON) { onHover("show_item", new JsonString(itemJSON)); return this; } /** * Internally called method. Not for API consumption. */ @Override public Iterator iterator() { return messageParts.iterator(); } /** * Set the behavior of the current editing component to instruct the client * to open a webpage in the client's web browser when the currently edited * part of the {@code FancyMessage} is clicked. * * @param url * The URL of the page to open when the link is clicked. * @return {@link FancyMessage} */ public FancyMessage link(final String url) { onClick("open_url", url); return this; } /** * Sends this message to a command sender. * If the sender is a player, they will receive the fully-fledged formatted * display of this message. * Otherwise, they will receive a version of this message with less * formatting. * * @param sender * The command sender who will receive the message. * @see #toOldMessageFormat() */ public void send(final CommandSender sender) { send(sender, toJSONString()); } public abstract void send(final CommandSender sender, final String jsonString); /** * Sends this message to multiple command senders. * * @param senders * The command senders who will receive the message. * @see #send(CommandSender) */ public void send(final Iterable senders) { final String string = toJSONString(); for (final CommandSender sender : senders) { send(sender, string); } } /** * Sends this message to a player. The player will receive the fully-fledged * formatted display of this message. * * @param player * The player who will receive the message. */ public void send(final Player player) { send(player, toJSONString()); } @Override public Map serialize() { final HashMap map = new HashMap(); map.put("messageParts", messageParts); // map.put("JSON", toJSONString()); return map; } /** * Set the behavior of the current editing component to display information * about a parameterless statistic when the client hovers over the text. *

* Tooltips do not inherit display characteristics, such as color and * styles, from the message component on which they are applied. *

* * @param which * The statistic to display. * @return {@link FancyMessage} * @exception IllegalArgumentException * If the statistic requires a parameter which was not * supplied. */ public abstract FancyMessage statisticTooltip(final Statistic which); /** * Set the behavior of the current editing component to display information * about a statistic parameter with an entity type when the client hovers * over the text. *

* Tooltips do not inherit display characteristics, such as color and * styles, from the message component on which they are applied. *

* * @param which * The statistic to display. * @param entity * The sole entity type parameter to the statistic. * @return {@link FancyMessage} * @exception IllegalArgumentException * If the statistic requires a parameter which was not * supplied, or was supplied a parameter that was not * required. */ public abstract FancyMessage statisticTooltip(final Statistic which, final EntityType entity); /** * Set the behavior of the current editing component to display information * about a statistic parameter with a material when the client hovers over * the text. *

* Tooltips do not inherit display characteristics, such as color and * styles, from the message component on which they are applied. *

* * @param which * The statistic to display. * @param item * The sole material parameter to the statistic. * @return {@link FancyMessage} * @exception IllegalArgumentException * If the statistic requires a parameter which was not * supplied, or was supplied a parameter that was not * required. */ public abstract FancyMessage statisticTooltip(final Statistic which, final Material item); /** * Sets the stylization of the current editing component. * * @param styles * The array of styles to apply to the editing component. * @return {@link FancyMessage} * @exception IllegalArgumentException * If any of the enumeration values in the array do not * represent formatters. */ public FancyMessage style(final ChatColor... styles) { for (final ChatColor style : styles) { if (!style.isFormat()) { throw new IllegalArgumentException(style.name() + " is not a style"); } } latest().styles.addAll(Arrays.asList(styles)); dirty = true; return this; } /** * Set the behavior of the current editing component to instruct the client * to replace the chat input box content with the specified string when the * currently edited part of the {@code FancyMessage} is clicked. * The client will not immediately send the command to the server to be * executed unless the client player submits the command/chat message, * usually with the enter key. * * @param command * The text to display in the chat bar of the client. * @return {@link FancyMessage} */ public FancyMessage suggest(final String command) { onClick("suggest_command", command); return this; } /** * 设置当前消息串的文本 * * @param text * 需要设置的文本 * @return {@link FancyMessage} */ public FancyMessage text(final String text) { final MessagePart latest = latest(); latest.text = rawText(text); dirty = true; return this; } /** * 设置当前消息串的文本 * * @param text * 需要设置的文本 * @return {@link FancyMessage} */ public FancyMessage text(final TextualComponent text) { final MessagePart latest = latest(); latest.text = text; dirty = true; return this; } /** * 停止编辑当前的消息串 并且开始一个新的消息串 *

* 之后的操作将会应用于新的消息串上 * * @return {@link FancyMessage} */ public FancyMessage then() { if (!latest().hasText()) { throw new IllegalStateException("previous message part has no text"); } messageParts.add(new MessagePart()); dirty = true; return this; } /** * 停止编辑当前的消息串 并且开始一个新的消息串 *

* 之后的操作将会应用于新的消息串上 * * @param text * 一个新的操作串需要的文本. * @return {@link FancyMessage} */ public FancyMessage then(final String text) { return then(rawText(text)); } /** * 停止编辑当前的消息串 并且开始一个新的消息串 *

* 之后的操作将会应用于新的消息串上 * * @param text * 一个新的操作串. * @return {@link FancyMessage} */ public FancyMessage then(final TextualComponent text) { if (!latest().hasText()) { throw new IllegalStateException("previous message part has no text"); } messageParts.add(new MessagePart(text)); dirty = true; return this; } /** * 序列化当前对象, 使用 {@link JsonWriter} 转换为一个有效的Json串 * 当前Json串可用于类似 {@code /tellraw} 的命令上 * * @return 返回这个对象的Json序列化字符串. */ public String toJSONString() { if (!dirty && jsonString != null) { return jsonString; } final StringWriter string = new StringWriter(); final JsonWriter json = GsonAgent.newJsonWriter(string); try { writeJson(json); json.close(); } catch (final IOException e) { throw new RuntimeException("invalid message"); } jsonString = string.toString(); dirty = false; return jsonString; } /** * 将此消息转换为具有有限格式的人可读字符串。 * 此方法用于发送此消息给没有JSON格式支持客户端。 *

* 序列化每个消息部分(每个部分都需要分别序列化): *

    *
  1. 消息串的颜色.
  2. *
  3. 消息串的样式.
  4. *
  5. 消息串的文本.
  6. *
* 这个方法会丢失点击操作和悬浮操作 所以仅用于最后的手段 *

*

* 颜色和格式可以从返回的字符串中删除 通过{@link ChatColor#stripColor(String)}. *

* * @return 发送给老版本客户端以及控制台。 */ public String toOldMessageFormat() { final StringBuilder result = new StringBuilder(); for (final MessagePart part : this) { result.append(part.color == null ? "" : part.color); for (final ChatColor formatSpecifier : part.styles) { result.append(formatSpecifier); } result.append(part.text); } return result.toString(); } /** * 设置当前的操作串 当鼠标悬浮时将会显示文本 *

* 当前方法将不会继承本类的颜色样式等参数. *

* * @param lines * 当鼠标悬浮时他将会显示 支持换行符 * @return {@link FancyMessage} */ public FancyMessage tooltip(final Iterable lines) { tooltip(ArrayWrapper.toArray(lines, String.class)); return this; } /** * 设置当前的操作串 当鼠标悬浮时将会显示文本 *

* 当前方法将不会继承本类的颜色样式等参数. *

* * @param lines * 当鼠标悬浮时他将会显示 支持换行符 * @return {@link FancyMessage} */ public FancyMessage tooltip(final String text) { onHover("show_text", new JsonString(text)); return this; } /** * 设置当前的操作串 当鼠标悬浮时将会显示文本 *

* 当前方法将不会继承本类的颜色样式等参数. *

* * @param lines * 当鼠标悬浮时他将会显示 支持换行符 * @return {@link FancyMessage} */ public FancyMessage tooltip(final String... lines) { final StringBuilder builder = new StringBuilder(); for (int i = 0; i < lines.length; i++) { builder.append(lines[i]); if (i != lines.length - 1) { builder.append('\n'); } } tooltip(builder.toString()); return this; } /** * If the text is a translatable key, and it has replaceable values, this * function can be used to set the replacements that will be used in the * message. * * @param replacements * The replacements, in order, that will be used in the * language-specific message. * @return {@link FancyMessage} */ public FancyMessage translationReplacements(final FancyMessage... replacements) { for (final FancyMessage str : replacements) { latest().translationReplacements.add(str); } dirty = true; return this; } /** * If the text is a translatable key, and it has replaceable values, this * function can be used to set the replacements that will be used in the * message. * * @param replacements * The replacements, in order, that will be used in the * language-specific message. * @return {@link FancyMessage} */ public FancyMessage translationReplacements(final Iterable replacements) { return translationReplacements(ArrayWrapper.toArray(replacements, FancyMessage.class)); } /** * If the text is a translatable key, and it has replaceable values, this * function can be used to set the replacements that will be used in the * message. * * @param replacements * The replacements, in order, that will be used in the * language-specific message. * @return {@link FancyMessage} */ public FancyMessage translationReplacements(final String... replacements) { for (final String str : replacements) { latest().translationReplacements.add(new JsonString(str)); } dirty = true; return this; } @Override public void writeJson(final JsonWriter writer) throws IOException { if (messageParts.size() == 1) { latest().writeJson(writer); } else { writer.beginObject().name("text").value("").name("extra").beginArray(); for (final MessagePart part : this) { part.writeJson(writer); } writer.endArray().endObject(); } } /** * 获得最后一个操作串 * * @return 最后一个操作的消息串 */ private MessagePart latest() { return messageParts.get(messageParts.size() - 1); } /** * 添加点击操作 * * @param name * 点击名称 * @param data * 点击操作 */ private void onClick(final String name, final String data) { final MessagePart latest = latest(); latest.clickActionName = name; latest.clickActionData = data; dirty = true; } /** * 添加显示操作 * * @param name * 悬浮显示 * @param data * 显示内容 */ private void onHover(final String name, final JsonRepresentedObject data) { final MessagePart latest = latest(); latest.hoverActionName = name; latest.hoverActionData = data; dirty = true; } }