1059 lines
42 KiB
Java
1059 lines
42 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();
|
|
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<MessagePart> messageParts;
|
|
|
|
public FancyMessage(final TextualComponent firstPartText) {
|
|
messageParts = new ArrayList<MessagePart>();
|
|
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<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);
|
|
}
|
|
|
|
/**
|
|
* 在客户端显示一个成就.
|
|
* </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;
|
|
}
|
|
}
|