TellRaw/src/main/java/cn/citycraft/TellRaw/FancyMessage.java

1039 lines
41 KiB
Java

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 <a
* href="http://minecraft.gamepedia.com/Tellraw#Raw_JSON_Text">JSON message
* formatter</a>.
* This class allows plugins to emulate the functionality of the vanilla
* Minecraft <a href="http://minecraft.gamepedia.com/Commands#tellraw">tellraw
* command</a>.
* <p>
* 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.
* </p>
*/
public abstract class FancyMessage implements JsonRepresentedObject, Cloneable, Iterable<MessagePart>, ConfigurationSerializable {
private static JsonParser _stringParser;
private static Class<? extends FancyMessage> 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();
try {
nmsPacketPlayOutChatConstructor = com.comphenix.protocol.PacketType.Play.Server.CHAT.getPacketClass().getDeclaredConstructor(MinecraftReflection.getIChatBaseComponentClass());
} catch (final Exception | Error e) {
nmsPacketPlayOutChatConstructor = Reflection.getNMSClass("PacketPlayOutChat").getDeclaredConstructor(Reflection.getNMSClass("IChatBaseComponent"));
}
nmsPacketPlayOutChatConstructor.setAccessible(true);
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();
nmsIChatBaseComponentClass = MinecraftReflection.getIChatBaseComponentClass();
} catch (final Exception | Error ex) {
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;
}
}
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 (getNMSSaveNBTMethod == null || nmsNBTTagCompound == null) {
throw new ClassNotFoundException("Can't find the NBTMethod or NBTTagCompound class");
}
internalClass = FancyMessageInternal.class;
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<MessagePart> messageParts;
/**
* 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<String, Object> serialized) {
final FancyMessage msg = newFM();
msg.messageParts = (List<MessagePart>) 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<String, JsonElement> 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<String, Object> serializedMapForm = new HashMap<String, Object>(); // 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<String, JsonElement> 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);
}
public FancyMessage(final TextualComponent firstPartText) {
messageParts = new ArrayList<MessagePart>();
messageParts.add(new MessagePart(firstPartText));
}
/**
* 在客户端显示一个成就.
* </p>
* 当前方法将不会继承本类的颜色样式等参数.
* </p>
*
* @param which
* 需要显示的成就
*
* @return {@link FancyMessage}
*/
public abstract FancyMessage achievementTooltip(final Achievement which);
/**
* 在客户端显示一个成就.
* </p>
* 当前方法将不会继承本类的颜色样式等参数.
* </p>
*
* @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<MessagePart>(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;
}
/**
* 服务器将特殊的字符串发送给客户端.
* </p>
* 当玩家点击时 客户端 <b>将会</b> 立即将命令发送给服务器.
* </p>
* 命令将绑定在当前编辑的文本上
* </p>
*
* @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.
* <p>
* Tooltips do not inherit display characteristics, such as color and
* styles, from the message component on which they are applied.
* </p>
*
* @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.
* <p>
* Tooltips do not inherit display characteristics, such as color and
* styles, from the message component on which they are applied.
* </p>
*
* @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.
* <p>
* Tooltips do not inherit display characteristics, such as color and
* styles, from the message component on which they are applied.
* </p>
*
* @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<FancyMessage> 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.
* <p>
* Tooltips do not inherit display characteristics, such as color and
* styles, from the message component on which they are applied.
* </p>
*
* @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.
* <p>
* Tooltips do not inherit display characteristics, such as color and
* styles, from the message component on which they are applied.
* </p>
*
* @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;
}
/**
* <b>Internally called method. Not for API consumption.</b>
*/
@Override
public Iterator<MessagePart> 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<? extends CommandSender> 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<String, Object> serialize() {
final HashMap<String, Object> map = new HashMap<String, Object>();
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.
* <p>
* Tooltips do not inherit display characteristics, such as color and
* styles, from the message component on which they are applied.
* </p>
*
* @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.
* <p>
* Tooltips do not inherit display characteristics, such as color and
* styles, from the message component on which they are applied.
* </p>
*
* @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.
* <p>
* Tooltips do not inherit display characteristics, such as color and
* styles, from the message component on which they are applied.
* </p>
*
* @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;
}
/**
* 停止编辑当前的消息串 并且开始一个新的消息串
* </p>
* 之后的操作将会应用于新的消息串上
*
* @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;
}
/**
* 停止编辑当前的消息串 并且开始一个新的消息串
* </p>
* 之后的操作将会应用于新的消息串上
*
* @param text
* 一个新的操作串需要的文本.
* @return {@link FancyMessage}
*/
public FancyMessage then(final String text) {
return then(rawText(text));
}
/**
* 停止编辑当前的消息串 并且开始一个新的消息串
* </p>
* 之后的操作将会应用于新的消息串上
*
* @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格式支持客户端。
* <p>
* 序列化每个消息部分(每个部分都需要分别序列化):
* <ol>
* <li>消息串的颜色.</li>
* <li>消息串的样式.</li>
* <li>消息串的文本.</li>
* </ol>
* 这个方法会丢失点击操作和悬浮操作 所以仅用于最后的手段
* </p>
* <p>
* 颜色和格式可以从返回的字符串中删除 通过{@link ChatColor#stripColor(String)}.
* </p>
*
* @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();
}
/**
* 设置当前的操作串 当鼠标悬浮时将会显示文本
* </p>
* 当前方法将不会继承本类的颜色样式等参数.
* </p>
*
* @param lines
* 当鼠标悬浮时他将会显示 支持换行符
* @return {@link FancyMessage}
*/
public FancyMessage tooltip(final Iterable<String> lines) {
tooltip(ArrayWrapper.toArray(lines, String.class));
return this;
}
/**
* 设置当前的操作串 当鼠标悬浮时将会显示文本
* </p>
* 当前方法将不会继承本类的颜色样式等参数.
* </p>
*
* @param lines
* 当鼠标悬浮时他将会显示 支持换行符
* @return {@link FancyMessage}
*/
public FancyMessage tooltip(final String text) {
onHover("show_text", new JsonString(text));
return this;
}
/**
* 设置当前的操作串 当鼠标悬浮时将会显示文本
* </p>
* 当前方法将不会继承本类的颜色样式等参数.
* </p>
*
* @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<FancyMessage> 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;
}
}