From 5d5bf4edd336cf0dffb6589a5e40600dab2c2fd0 Mon Sep 17 00:00:00 2001 From: 502647092 Date: Wed, 13 Jan 2016 13:31:39 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A6=96=E6=AC=A1=E6=8F=90=E4=BA=A4...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 502647092 --- .classpath | 16 + .gitignore | 41 + .project | 23 + pom.xml | 64 + .../cn/citycraft/TellRaw/FancyMessage.java | 1058 +++++++++++++++++ .../citycraft/TellRaw/PluginHelperConfig.java | 64 + .../TellRaw/common/ArrayWrapper.java | 117 ++ .../TellRaw/common/JsonRepresentedObject.java | 22 + .../citycraft/TellRaw/common/JsonString.java | 50 + .../citycraft/TellRaw/common/MessagePart.java | 159 +++ .../citycraft/TellRaw/common/Reflection.java | 300 +++++ .../TellRaw/common/TextualComponent.java | 352 ++++++ .../internal/FancyMessageInternal.java | 223 ++++ .../TellRaw/manual/FancyMessageManual.java | 85 ++ .../citycraft/TellRaw/manual/GItemStack.java | 85 ++ src/main/resources/config.yml | 13 + src/main/resources/plugin.yml | 21 + 17 files changed, 2693 insertions(+) create mode 100644 .classpath create mode 100644 .gitignore create mode 100644 .project create mode 100644 pom.xml create mode 100644 src/main/java/cn/citycraft/TellRaw/FancyMessage.java create mode 100644 src/main/java/cn/citycraft/TellRaw/PluginHelperConfig.java create mode 100644 src/main/java/cn/citycraft/TellRaw/common/ArrayWrapper.java create mode 100644 src/main/java/cn/citycraft/TellRaw/common/JsonRepresentedObject.java create mode 100644 src/main/java/cn/citycraft/TellRaw/common/JsonString.java create mode 100644 src/main/java/cn/citycraft/TellRaw/common/MessagePart.java create mode 100644 src/main/java/cn/citycraft/TellRaw/common/Reflection.java create mode 100644 src/main/java/cn/citycraft/TellRaw/common/TextualComponent.java create mode 100644 src/main/java/cn/citycraft/TellRaw/internal/FancyMessageInternal.java create mode 100644 src/main/java/cn/citycraft/TellRaw/manual/FancyMessageManual.java create mode 100644 src/main/java/cn/citycraft/TellRaw/manual/GItemStack.java create mode 100644 src/main/resources/config.yml create mode 100644 src/main/resources/plugin.yml diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..2c705bb --- /dev/null +++ b/.classpath @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ffb49b3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# Eclipse stuff +/.settings + +# netbeans +/nbproject + +# we use maven! +/build.xml + +# maven +/target +/repo + +# vim +.*.sw[a-p] + +# various other potential build files +/build +/bin +/dist +/manifest.mf + +/world + +# Mac filesystem dust +*.DS_Store + +# intellij +*.iml +*.ipr +*.iws +.idea/ + +# Project Stuff +/src/main/resources/Soulbound + +# Other Libraries +*.jar + +# Atlassian Stuff +/atlassian-ide-plugin.xml \ No newline at end of file diff --git a/.project b/.project new file mode 100644 index 0000000..f696d24 --- /dev/null +++ b/.project @@ -0,0 +1,23 @@ + + + TellRaw + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.jdt.core.javanature + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..0cc3588 --- /dev/null +++ b/pom.xml @@ -0,0 +1,64 @@ + + 4.0.0 + cn.citycraft + TellRaw + 1.0 + TellRaw + + ${project.name} + + + src/main/resources + true + + + + + maven-compiler-plugin + 3.1 + + 1.7 + 1.7 + + + + + + UTF-8 + Debug + Debug + http://ci.citycraft.cn:8080 + + + + spigot-repo + https://hub.spigotmc.org/nexus/content/groups/public/ + + + citycraft-repo + ${jenkins.url}/plugin/repository/everything/ + + + + + org.spigotmc + spigot-api + jar + 1.8.8-R0.1-SNAPSHOT + + + com.comphenix.protocol + ProtocolLib + 3.6.5-SNAPSHOT + system + ${project.basedir}/lib/ProtocolLib-3.6.5-SNAPSHOT.jar + + + cn.citycraft + GsonAgent + jar + 1.0 + + + \ No newline at end of file diff --git a/src/main/java/cn/citycraft/TellRaw/FancyMessage.java b/src/main/java/cn/citycraft/TellRaw/FancyMessage.java new file mode 100644 index 0000000..547c22c --- /dev/null +++ b/src/main/java/cn/citycraft/TellRaw/FancyMessage.java @@ -0,0 +1,1058 @@ +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; + } +} diff --git a/src/main/java/cn/citycraft/TellRaw/PluginHelperConfig.java b/src/main/java/cn/citycraft/TellRaw/PluginHelperConfig.java new file mode 100644 index 0000000..db92e8c --- /dev/null +++ b/src/main/java/cn/citycraft/TellRaw/PluginHelperConfig.java @@ -0,0 +1,64 @@ +package cn.citycraft.TellRaw; + +import java.io.File; +import java.io.IOException; +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.configuration.file.YamlConfiguration; + +/** + * PluginHelper 通用配置文件类 + * + * @author 喵♂呜 + */ +public class PluginHelperConfig { + public static YamlConfiguration a; + public static File configfile = new File(Bukkit.getUpdateFolderFile().getParentFile(), "PluginHelper" + File.separatorChar + "config.yml"); + + static { + try { + if (!configfile.exists()) { + configfile.createNewFile(); + } + a = YamlConfiguration.loadConfiguration(configfile); + initFile(a); + } catch (final IOException e) { + a = new YamlConfiguration(); + try { + initFile(a); + } catch (final IOException e1) { + } + } + } + + public static YamlConfiguration getConfig() { + try { + return YamlConfiguration.loadConfiguration(configfile); + } catch (final Exception e) { + return a; + } + } + + public static String getGUID() { + return a.getString("guid"); + } + + private static void initFile(final YamlConfiguration config) throws IOException { + if (config.getString("guid") == null) { + config.options().header("数据中心 http://yum.citycraft.cn 收集的数据仅用于统计插件使用情况").copyDefaults(true); + config.set("guid", UUID.randomUUID().toString()); + config.set("debug", false); + config.save(configfile); + } + if (!config.contains("YumAccount")) { + config.set("YumAccount.username", "Username Not Set"); + config.set("YumAccount.password", "Password NotSet"); + config.save(configfile); + } + if (!config.contains("TellrawManualHandle")) { + config.set("TellrawManualHandle", false); + config.save(configfile); + } + } +} diff --git a/src/main/java/cn/citycraft/TellRaw/common/ArrayWrapper.java b/src/main/java/cn/citycraft/TellRaw/common/ArrayWrapper.java new file mode 100644 index 0000000..caf4e6c --- /dev/null +++ b/src/main/java/cn/citycraft/TellRaw/common/ArrayWrapper.java @@ -0,0 +1,117 @@ +package cn.citycraft.TellRaw.common; + +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.Collection; + +import org.apache.commons.lang.Validate; + +/** + * Represents a wrapper around an array class of an arbitrary reference type, + * which properly implements "value" hash code and equality functions. + *

+ * This class is intended for use as a key to a map. + *

+ * + * @author Glen Husman + * @param + * The type of elements in the array. + * @see Arrays + */ +public final class ArrayWrapper { + + private E[] _array; + + /** + * Creates an array wrapper with some elements. + * + * @param elements + * The elements of the array. + */ + @SafeVarargs + public ArrayWrapper(final E... elements) { + setArray(elements); + } + + /** + * Converts an iterable element collection to an array of elements. + * The iteration order of the specified object will be used as the array + * element order. + * + * @param list + * The iterable of objects which will be converted to an array. + * @param c + * The type of the elements of the array. + * @return An array of elements in the specified iterable. + */ + @SuppressWarnings("unchecked") + public static T[] toArray(final Iterable list, final Class c) { + int size = -1; + if (list instanceof Collection) { + @SuppressWarnings("rawtypes") + final Collection coll = (Collection) list; + size = coll.size(); + } + + if (size < 0) { + size = 0; + // Ugly hack: Count it ourselves + for (@SuppressWarnings("unused") + final T element : list) { + size++; + } + } + + final T[] result = (T[]) Array.newInstance(c, size); + int i = 0; + for (final T element : list) { // Assumes iteration order is consistent + result[i++] = element; // Assign array element at index THEN increment counter + } + return result; + } + + /** + * Determines if this object has a value equivalent to another object. + * + * @see Arrays#equals(Object[], Object[]) + */ + @SuppressWarnings("rawtypes") + @Override + public boolean equals(final Object other) { + if (!(other instanceof ArrayWrapper)) { + return false; + } + return Arrays.equals(_array, ((ArrayWrapper) other)._array); + } + + /** + * Retrieves a reference to the wrapped array instance. + * + * @return The array wrapped by this instance. + */ + public E[] getArray() { + return _array; + } + + /** + * Gets the hash code represented by this objects value. + * + * @see Arrays#hashCode(Object[]) + * @return This object's hash code. + */ + @Override + public int hashCode() { + return Arrays.hashCode(_array); + } + + /** + * Set this wrapper to wrap a new array instance. + * + * @param array + * The new wrapped array. + */ + public void setArray(final E[] array) { + Validate.notNull(array, "The array must not be null."); + _array = array; + } +} diff --git a/src/main/java/cn/citycraft/TellRaw/common/JsonRepresentedObject.java b/src/main/java/cn/citycraft/TellRaw/common/JsonRepresentedObject.java new file mode 100644 index 0000000..e7b1fd1 --- /dev/null +++ b/src/main/java/cn/citycraft/TellRaw/common/JsonRepresentedObject.java @@ -0,0 +1,22 @@ +package cn.citycraft.TellRaw.common; + +import java.io.IOException; + +import cn.citycraft.GsonAgent.api.stream.JsonWriter; + +/** + * Represents an object that can be serialized to a JSON writer instance. + */ +public interface JsonRepresentedObject { + + /** + * Writes the JSON representation of this object to the specified writer. + * + * @param writer + * The JSON writer which will receive the object. + * @throws IOException + * If an error occurs writing to the stream. + */ + public void writeJson(JsonWriter writer) throws IOException; + +} diff --git a/src/main/java/cn/citycraft/TellRaw/common/JsonString.java b/src/main/java/cn/citycraft/TellRaw/common/JsonString.java new file mode 100644 index 0000000..41eadf9 --- /dev/null +++ b/src/main/java/cn/citycraft/TellRaw/common/JsonString.java @@ -0,0 +1,50 @@ +package cn.citycraft.TellRaw.common; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.bukkit.configuration.serialization.ConfigurationSerializable; + +import cn.citycraft.GsonAgent.api.stream.JsonWriter; + +/** + * Represents a JSON string value. + * Writes by this object will not write name values nor begin/end objects in the + * JSON stream. + * All writes merely write the represented string value. + */ +public final class JsonString implements JsonRepresentedObject, ConfigurationSerializable { + + private final String _value; + + public JsonString(final CharSequence value) { + _value = value == null ? null : value.toString(); + } + + public static JsonString deserialize(final Map map) { + return new JsonString(map.get("stringValue").toString()); + } + + public String getValue() { + return _value; + } + + @Override + public Map serialize() { + final HashMap theSingleValue = new HashMap(); + theSingleValue.put("stringValue", _value); + return theSingleValue; + } + + @Override + public String toString() { + return _value; + } + + @Override + public void writeJson(final JsonWriter writer) throws IOException { + writer.value(getValue()); + } + +} diff --git a/src/main/java/cn/citycraft/TellRaw/common/MessagePart.java b/src/main/java/cn/citycraft/TellRaw/common/MessagePart.java new file mode 100644 index 0000000..d8f25f9 --- /dev/null +++ b/src/main/java/cn/citycraft/TellRaw/common/MessagePart.java @@ -0,0 +1,159 @@ +package cn.citycraft.TellRaw.common; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.configuration.serialization.ConfigurationSerializable; +import org.bukkit.configuration.serialization.ConfigurationSerialization; + +import com.google.common.collect.BiMap; +import com.google.common.collect.ImmutableBiMap; + +import cn.citycraft.GsonAgent.api.stream.JsonWriter; +import cn.citycraft.TellRaw.internal.FancyMessageInternal; + +/** + * Internal class: Represents a component of a JSON-serializable + * {@link FancyMessageInternal}. + */ +public final class MessagePart implements JsonRepresentedObject, ConfigurationSerializable, Cloneable { + + public static final BiMap stylesToNames; + + static { + final ImmutableBiMap.Builder builder = ImmutableBiMap.builder(); + for (final ChatColor style : ChatColor.values()) { + if (!style.isFormat()) { + continue; + } + + String styleName; + switch (style) { + case MAGIC: + styleName = "obfuscated"; + break; + case UNDERLINE: + styleName = "underlined"; + break; + default: + styleName = style.name().toLowerCase(); + break; + } + + builder.put(style, styleName); + } + stylesToNames = builder.build(); + } + + static { + ConfigurationSerialization.registerClass(MessagePart.class); + } + + public String clickActionData = null; + public String clickActionName = null; + public ChatColor color = ChatColor.WHITE; + public JsonRepresentedObject hoverActionData = null; + public String hoverActionName = null; + public String insertionData = null; + + public ArrayList styles = new ArrayList(); + + public TextualComponent text = null; + + public ArrayList translationReplacements = new ArrayList(); + + public MessagePart() { + this.text = null; + } + + public MessagePart(final TextualComponent text) { + this.text = text; + } + + @SuppressWarnings("unchecked") + public static MessagePart deserialize(final Map serialized) { + final MessagePart part = new MessagePart((TextualComponent) serialized.get("text")); + part.styles = (ArrayList) serialized.get("styles"); + part.color = ChatColor.getByChar(serialized.get("color").toString()); + part.hoverActionName = (String) serialized.get("hoverActionName"); + part.hoverActionData = (JsonRepresentedObject) serialized.get("hoverActionData"); + part.clickActionName = (String) serialized.get("clickActionName"); + part.clickActionData = (String) serialized.get("clickActionData"); + part.insertionData = (String) serialized.get("insertion"); + part.translationReplacements = (ArrayList) serialized.get("translationReplacements"); + return part; + } + + @Override + @SuppressWarnings("unchecked") + public MessagePart clone() throws CloneNotSupportedException { + final MessagePart obj = (MessagePart) super.clone(); + obj.styles = (ArrayList) styles.clone(); + if (hoverActionData instanceof JsonString) { + obj.hoverActionData = new JsonString(((JsonString) hoverActionData).getValue()); + } else if (hoverActionData instanceof FancyMessageInternal) { + obj.hoverActionData = ((FancyMessageInternal) hoverActionData).clone(); + } + obj.translationReplacements = (ArrayList) translationReplacements.clone(); + return obj; + + } + + public boolean hasText() { + return text != null; + } + + @Override + public Map serialize() { + final HashMap map = new HashMap(); + map.put("text", text); + map.put("styles", styles); + map.put("color", color.getChar()); + map.put("hoverActionName", hoverActionName); + map.put("hoverActionData", hoverActionData); + map.put("clickActionName", clickActionName); + map.put("clickActionData", clickActionData); + map.put("insertion", insertionData); + map.put("translationReplacements", translationReplacements); + return map; + } + + @Override + public void writeJson(final JsonWriter json) { + try { + json.beginObject(); + text.writeJson(json); + json.name("color").value(color.name().toLowerCase()); + for (final ChatColor style : styles) { + json.name(stylesToNames.get(style)).value(true); + } + if (clickActionName != null && clickActionData != null) { + json.name("clickEvent").beginObject().name("action").value(clickActionName).name("value").value(clickActionData).endObject(); + } + if (hoverActionName != null && hoverActionData != null) { + json.name("hoverEvent").beginObject().name("action").value(hoverActionName).name("value"); + hoverActionData.writeJson(json); + json.endObject(); + } + if (insertionData != null) { + json.name("insertion").value(insertionData); + } + if (translationReplacements.size() > 0 && text != null && TextualComponent.isTranslatableText(text)) { + json.name("with").beginArray(); + for (final JsonRepresentedObject obj : translationReplacements) { + obj.writeJson(json); + } + json.endArray(); + } + json.endObject(); + } catch (final IOException e) { + Bukkit.getLogger().log(Level.WARNING, "A problem occured during writing of JSON string", e); + } + } + +} diff --git a/src/main/java/cn/citycraft/TellRaw/common/Reflection.java b/src/main/java/cn/citycraft/TellRaw/common/Reflection.java new file mode 100644 index 0000000..0a118b0 --- /dev/null +++ b/src/main/java/cn/citycraft/TellRaw/common/Reflection.java @@ -0,0 +1,300 @@ +package cn.citycraft.TellRaw.common; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.bukkit.Bukkit; + +/** + * A class containing static utility methods and caches which are intended as + * reflective conveniences. + * Unless otherwise noted, upon failure methods will return {@code null}. + */ +public final class Reflection { + private static final Map, Map> _loadedFields = new HashMap, Map>(); + /** + * Contains loaded methods in a cache. + * The map maps [types to maps of [method names to maps of [parameter types + * to method instances]]]. + */ + private static final Map, Map>, Method>>> _loadedMethods = new HashMap, Map>, Method>>>(); + + /** + * Stores loaded classes from the {@code net.minecraft.server} package. + */ + private static final Map> _loadedNMSClasses = new HashMap>(); + + /** + * Stores loaded classes from the {@code org.bukkit.craftbukkit} package + * (and subpackages). + */ + private static final Map> _loadedOBCClasses = new HashMap>(); + private static String _versionString; + + private Reflection() { + + } + + @SuppressWarnings("rawtypes") + public static List getDeclaredFieldByType(final Class source, final Class type) { + final List list = new ArrayList<>(); + for (final Field field : source.getDeclaredFields()) { + if (field.getType() == type) { + field.setAccessible(true); + list.add(field); + } + } + return list; + } + + /** + * Retrieves a {@link Field} instance declared by the specified class with + * the specified name. + * Java access modifiers are ignored during this retrieval. No guarantee is + * made as to whether the field + * returned will be an instance or static field. + *

+ * A global caching mechanism within this class is used to store fields. + * Combined with synchronization, this guarantees that no field will be + * reflectively looked up twice. + *

+ *

+ * If a field is deemed suitable for return, + * {@link Field#setAccessible(boolean) setAccessible} will be invoked with + * an argument of {@code true} before it is returned. This ensures that + * callers do not have to check or worry about Java access modifiers when + * dealing with the returned instance. + *

+ * + * @param clazz + * The class which contains the field to retrieve. + * @param name + * The declared name of the field in the class. + * @return A field object with the specified name declared by the specified + * class. + * @throws Exception + * @see Class#getDeclaredField(String) + */ + public synchronized static Field getField(final Class clazz, final String name) throws Exception { + Map loaded; + if (!_loadedFields.containsKey(clazz)) { + loaded = new HashMap(); + _loadedFields.put(clazz, loaded); + } else { + loaded = _loadedFields.get(clazz); + } + if (loaded.containsKey(name)) { + // If the field is loaded (or cached as not existing), return the + // relevant value, which might be null + return loaded.get(name); + } + try { + final Field field = clazz.getDeclaredField(name); + field.setAccessible(true); + loaded.put(name, field); + return field; + } catch (final Exception e) { + // Cache field as not existing + loaded.put(name, null); + throw e; + } + } + + /** + * Attempts to get the NMS handle of a CraftBukkit object. + *

+ * The only match currently attempted by this method is a retrieval by using + * a parameterless {@code getHandle()} method implemented by the runtime + * type of the specified object. + *

+ * + * @param obj + * The object for which to retrieve an NMS handle. + * @return The NMS handle of the specified object, or {@code null} if it + * could not be retrieved using {@code getHandle()}. + */ + public synchronized static Object getHandle(final Object obj) { + try { + return getMethod(obj.getClass(), "getHandle").invoke(obj); + } catch (final Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * Retrieves a {@link Method} instance declared by the specified class with + * the specified name and argument types. + * Java access modifiers are ignored during this retrieval. No guarantee is + * made as to whether the field + * returned will be an instance or static field. + *

+ * A global caching mechanism within this class is used to store method. + * Combined with synchronization, this guarantees that no method will be + * reflectively looked up twice. + *

+ *

+ * If a method is deemed suitable for return, + * {@link Method#setAccessible(boolean) setAccessible} will be invoked with + * an argument of {@code true} before it is returned. This ensures that + * callers do not have to check or worry about Java access modifiers when + * dealing with the returned instance. + *

+ *

+ * This method does not search superclasses of the specified type + * for methods with the specified signature. Callers wishing this behavior + * should use {@link Class#getDeclaredMethod(String, Class...)}. + * + * @param clazz + * The class which contains the method to retrieve. + * @param name + * The declared name of the method in the class. + * @param args + * The formal argument types of the method. + * @return A method object with the specified name declared by the specified + * class. + */ + public synchronized static Method getMethod(final Class clazz, final String name, final Class... args) { + if (!_loadedMethods.containsKey(clazz)) { + _loadedMethods.put(clazz, new HashMap>, Method>>()); + } + + final Map>, Method>> loadedMethodNames = _loadedMethods.get(clazz); + if (!loadedMethodNames.containsKey(name)) { + loadedMethodNames.put(name, new HashMap>, Method>()); + } + + final Map>, Method> loadedSignatures = loadedMethodNames.get(name); + final ArrayWrapper> wrappedArg = new ArrayWrapper>(args); + if (loadedSignatures.containsKey(wrappedArg)) { + return loadedSignatures.get(wrappedArg); + } + + for (final Method m : clazz.getMethods()) { + if (m.getName().equals(name) && Arrays.equals(args, m.getParameterTypes())) { + m.setAccessible(true); + loadedSignatures.put(wrappedArg, m); + return m; + } + } + loadedSignatures.put(wrappedArg, null); + return null; + } + + @SuppressWarnings("rawtypes") + public static List getMethodByParamsAndType(final Class source, final Class returnType, final Class... args) { + final List list = new ArrayList<>(); + for (final Method method : findMethodByParams(source.getMethods(), args)) { + if (method.getReturnType().equals(returnType)) { + list.add(method); + } + } + return list; + } + + /** + * Gets a {@link Class} object representing a type contained within the + * {@code net.minecraft.server} versioned package. + * The class instances returned by this method are cached, such that no + * lookup will be done twice (unless multiple threads are accessing this + * method simultaneously). + * + * @param className + * The name of the class, excluding the package, within NMS. + * @return The class instance representing the specified NMS class, or + * {@code null} if it could not be loaded. + */ + public synchronized static Class getNMSClass(final String className) { + if (_loadedNMSClasses.containsKey(className)) { + return _loadedNMSClasses.get(className); + } + final String fullName = "net.minecraft.server." + getVersion() + className; + Class clazz = null; + try { + clazz = Class.forName(fullName); + } catch (final Exception | NoClassDefFoundError | NoSuchMethodError e) { + _loadedNMSClasses.put(className, null); + return null; + } + _loadedNMSClasses.put(className, clazz); + return clazz; + } + + /** + * Gets a {@link Class} object representing a type contained within the + * {@code org.bukkit.craftbukkit} versioned package. + * The class instances returned by this method are cached, such that no + * lookup will be done twice (unless multiple threads are accessing this + * method simultaneously). + * + * @param className + * The name of the class, excluding the package, within OBC. This + * name may contain a subpackage name, such as + * {@code inventory.CraftItemStack}. + * @return The class instance representing the specified OBC class, or + * {@code null} if it could not be loaded. + */ + public synchronized static Class getOBCClass(final String className) { + if (_loadedOBCClasses.containsKey(className)) { + return _loadedOBCClasses.get(className); + } + + final String fullName = "org.bukkit.craftbukkit." + getVersion() + className; + Class clazz = null; + try { + clazz = Class.forName(fullName); + } catch (final Exception e) { + e.printStackTrace(); + _loadedOBCClasses.put(className, null); + return null; + } + _loadedOBCClasses.put(className, clazz); + return clazz; + } + + /** + * Gets the version string from the package name of the CraftBukkit server + * implementation. + * This is needed to bypass the JAR package name changing on each update. + * + * @return The version string of the OBC and NMS packages, + * including the trailing dot. + */ + public synchronized static String getVersion() { + if (_versionString == null) { + if (Bukkit.getServer() == null) { + // The server hasn't started, static initializer call? + return null; + } + final String name = Bukkit.getServer().getClass().getPackage().getName(); + _versionString = name.substring(name.lastIndexOf('.') + 1) + "."; + } + + return _versionString; + } + + @SuppressWarnings("rawtypes") + private static List findMethodByParams(final Method[] methods, final Class... args) { + final List list = new ArrayList<>(); + start: + for (final Method method : methods) { + if (method.getParameterTypes().length == args.length) { + final Class[] array = method.getParameterTypes(); + for (int i = 0; i < args.length; i++) { + if (array[i] != args[i]) { + continue start; + } + } + method.setAccessible(true); + list.add(method); + } + } + return list; + } + +} diff --git a/src/main/java/cn/citycraft/TellRaw/common/TextualComponent.java b/src/main/java/cn/citycraft/TellRaw/common/TextualComponent.java new file mode 100644 index 0000000..314ae67 --- /dev/null +++ b/src/main/java/cn/citycraft/TellRaw/common/TextualComponent.java @@ -0,0 +1,352 @@ +package cn.citycraft.TellRaw.common; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.bukkit.configuration.serialization.ConfigurationSerializable; +import org.bukkit.configuration.serialization.ConfigurationSerialization; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; + +import cn.citycraft.GsonAgent.api.stream.JsonWriter; + +/** + * Represents a textual component of a message part. + * This can be used to not only represent string literals in a JSON message, + * but also to represent localized strings and other text values. + *

+ * Different instances of this class can be created with static constructor + * methods. + *

+ */ +public abstract class TextualComponent implements Cloneable { + static { + ConfigurationSerialization.registerClass(TextualComponent.ArbitraryTextTypeComponent.class); + ConfigurationSerialization.registerClass(TextualComponent.ComplexTextTypeComponent.class); + } + + public static TextualComponent deserialize(final Map map) { + if (map.containsKey("key") && map.size() == 2 && map.containsKey("value")) { + // Arbitrary text component + return ArbitraryTextTypeComponent.deserialize(map); + } else if (map.size() >= 2 && map.containsKey("key") && !map.containsKey("value")) { + // Complex JSON object + return ComplexTextTypeComponent.deserialize(map); + } + + return null; + } + + public static boolean isTextKey(final String key) { + return key.equals("translate") || key.equals("text") || key.equals("score") || key.equals("selector"); + } + + /** + * Create a textual component representing a localized string. + * The client will see this text component as their localized version of the + * specified string key, which can be overridden by a resource + * pack. + *

+ * If the specified translation key is not present on the client resource + * pack, the translation key will be displayed as a string literal to the + * client. + *

+ * + * @param translateKey + * The string key which maps to localized text. + * @return The text component representing the specified localized text. + */ + public static TextualComponent localizedText(final String translateKey) { + return new ArbitraryTextTypeComponent("translate", translateKey); + } + + /** + * Create a textual component representing a scoreboard value. + * The client will see their own score for the specified objective as the + * text represented by this component. + *

+ * This method is currently guaranteed to throw an + * {@code UnsupportedOperationException} as it is only supported on snapshot + * clients. + *

+ * + * @param scoreboardObjective + * The name of the objective for which to display the score. + * @return The text component representing the specified scoreboard score + * (for the viewing player), or {@code null} if an error occurs + * during JSON serialization. + */ + public static TextualComponent objectiveScore(final String scoreboardObjective) { + return objectiveScore("*", scoreboardObjective); + } + + /** + * Create a textual component representing a scoreboard value. + * The client will see the score of the specified player for the specified + * objective as the text represented by this component. + *

+ * This method is currently guaranteed to throw an + * {@code UnsupportedOperationException} as it is only supported on snapshot + * clients. + *

+ * + * @param playerName + * The name of the player whos score will be shown. If this + * string represents the single-character sequence "*", the + * viewing player's score will be displayed. + * Standard minecraft selectors (@a, @p, etc) are not + * supported. + * @param scoreboardObjective + * The name of the objective for which to display the score. + * @return The text component representing the specified scoreboard score + * for the specified player, or {@code null} if an error occurs + * during JSON serialization. + */ + public static TextualComponent objectiveScore(final String playerName, final String scoreboardObjective) { + throwUnsupportedSnapshot(); // Remove this line when the feature is + // released to non-snapshot versions, in + // addition to updating ALL THE OVERLOADS + // documentation accordingly + + return new ComplexTextTypeComponent("score", ImmutableMap. builder().put("name", playerName).put("objective", scoreboardObjective).build()); + } + + /** + * Create a textual component representing a string literal. + * This is the default type of textual component when a single string + * literal is given to a method. + * + * @param textValue + * The text which will be represented. + * @return The text component representing the specified literal text. + */ + public static TextualComponent rawText(final String textValue) { + return new ArbitraryTextTypeComponent("text", textValue); + } + + /** + * Create a textual component representing a player name, retrievable by + * using a standard minecraft selector. + * The client will see the players or entities captured by the specified + * selector as the text represented by this component. + *

+ * This method is currently guaranteed to throw an + * {@code UnsupportedOperationException} as it is only supported on snapshot + * clients. + *

+ * + * @param selector + * The minecraft player or entity selector which will capture the + * entities whose string representations will be displayed in the + * place of this text component. + * @return The text component representing the name of the entities captured + * by the selector. + */ + public static TextualComponent selector(final String selector) { + throwUnsupportedSnapshot(); // Remove this line when the feature is + // released to non-snapshot versions, in + // addition to updating ALL THE OVERLOADS + // documentation accordingly + + return new ArbitraryTextTypeComponent("selector", selector); + } + + private static void throwUnsupportedSnapshot() { + throw new UnsupportedOperationException("This feature is only supported in snapshot releases."); + } + + static boolean isTranslatableText(final TextualComponent component) { + return component instanceof ComplexTextTypeComponent && ((ComplexTextTypeComponent) component).getKey().equals("translate"); + } + + /** + * Clones a textual component instance. + * The returned object should not reference this textual component instance, + * but should maintain the same key and value. + */ + @Override + public abstract TextualComponent clone() throws CloneNotSupportedException; + + /** + * @return The JSON key used to represent text components of this type. + */ + public abstract String getKey(); + + /** + * @return A readable String + */ + public abstract String getReadableString(); + + @Override + public String toString() { + return getReadableString(); + } + + /** + * Writes the text data represented by this textual component to the + * specified JSON writer object. + * A new object within the writer is not started. + * + * @param writer + * The object to which to write the JSON data. + * @throws IOException + * If an error occurs while writing to the stream. + */ + public abstract void writeJson(JsonWriter writer) throws IOException; + + /** + * Internal class used to represent all types of text components. + * Exception validating done is on keys and values. + */ + private static final class ArbitraryTextTypeComponent extends TextualComponent implements ConfigurationSerializable { + + private String _key; + + private String _value; + + public ArbitraryTextTypeComponent(final String key, final String value) { + setKey(key); + setValue(value); + } + + public static ArbitraryTextTypeComponent deserialize(final Map map) { + return new ArbitraryTextTypeComponent(map.get("key").toString(), map.get("value").toString()); + } + + @Override + public TextualComponent clone() throws CloneNotSupportedException { + // Since this is a private and final class, we can just + // reinstantiate this class instead of casting super.clone + return new ArbitraryTextTypeComponent(getKey(), getValue()); + } + + @Override + public String getKey() { + return _key; + } + + @Override + public String getReadableString() { + return getValue(); + } + + public String getValue() { + return _value; + } + + @Override + @SuppressWarnings("serial") + public Map serialize() { + return new HashMap() { + { + put("key", getKey()); + put("value", getValue()); + } + }; + } + + public void setKey(final String key) { + Preconditions.checkArgument(key != null && !key.isEmpty(), "The key must be specified."); + _key = key; + } + + public void setValue(final String value) { + Preconditions.checkArgument(value != null, "The value must be specified."); + _value = value; + } + + @Override + public void writeJson(final JsonWriter writer) throws IOException { + writer.name(getKey()).value(getValue()); + } + } + + /** + * Internal class used to represent a text component with a nested JSON + * value. + * Exception validating done is on keys and values. + */ + private static final class ComplexTextTypeComponent extends TextualComponent implements ConfigurationSerializable { + + private String _key; + + private Map _value; + + public ComplexTextTypeComponent(final String key, final Map values) { + setKey(key); + setValue(values); + } + + public static ComplexTextTypeComponent deserialize(final Map map) { + String key = null; + final Map value = new HashMap(); + for (final Map.Entry valEntry : map.entrySet()) { + if (valEntry.getKey().equals("key")) { + key = (String) valEntry.getValue(); + } else if (valEntry.getKey().startsWith("value.")) { + value.put(valEntry.getKey().substring(6) /* + * Strips out the + * value prefix + */, valEntry.getValue().toString()); + } + } + return new ComplexTextTypeComponent(key, value); + } + + @Override + public TextualComponent clone() throws CloneNotSupportedException { + // Since this is a private and final class, we can just + // reinstantiate this class instead of casting super.clone + return new ComplexTextTypeComponent(getKey(), getValue()); + } + + @Override + public String getKey() { + return _key; + } + + @Override + public String getReadableString() { + return getKey(); + } + + public Map getValue() { + return _value; + } + + @Override + @SuppressWarnings("serial") + public Map serialize() { + return new java.util.HashMap() { + { + put("key", getKey()); + for (final Map.Entry valEntry : getValue().entrySet()) { + put("value." + valEntry.getKey(), valEntry.getValue()); + } + } + }; + } + + public void setKey(final String key) { + Preconditions.checkArgument(key != null && !key.isEmpty(), "The key must be specified."); + _key = key; + } + + public void setValue(final Map value) { + Preconditions.checkArgument(value != null, "The value must be specified."); + _value = value; + } + + @Override + public void writeJson(final JsonWriter writer) throws IOException { + writer.name(getKey()); + writer.beginObject(); + for (final Map.Entry jsonPair : _value.entrySet()) { + writer.name(jsonPair.getKey()).value(jsonPair.getValue()); + } + writer.endObject(); + } + } +} diff --git a/src/main/java/cn/citycraft/TellRaw/internal/FancyMessageInternal.java b/src/main/java/cn/citycraft/TellRaw/internal/FancyMessageInternal.java new file mode 100644 index 0000000..3dbff44 --- /dev/null +++ b/src/main/java/cn/citycraft/TellRaw/internal/FancyMessageInternal.java @@ -0,0 +1,223 @@ +package cn.citycraft.TellRaw.internal; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.bukkit.Achievement; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.Statistic; +import org.bukkit.Statistic.Type; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import cn.citycraft.TellRaw.FancyMessage; +import cn.citycraft.TellRaw.common.Reflection; +import cn.citycraft.TellRaw.common.TextualComponent; + +/** + * 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 class FancyMessageInternal extends FancyMessage { + + /** + * 新建自动序列化类 + */ + public FancyMessageInternal() { + this(""); + } + + /** + * 新建自动序列化类 + * + * @param firstPartText + * 第一个文本串 + */ + public FancyMessageInternal(final String firstPartText) { + super(TextualComponent.rawText(firstPartText)); + } + + @Override + public FancyMessage achievementTooltip(final Achievement which) { + try { + final Object achievement = getNMSAchievementMethod.invoke(null, which); + return achievementTooltip((String) nmsAchievement_NameField.get(achievement)); + } catch (final IllegalAccessException e) { + Bukkit.getLogger().log(Level.WARNING, "Could not access method.", e); + return this; + } catch (final IllegalArgumentException e) { + Bukkit.getLogger().log(Level.WARNING, "Argument could not be passed.", e); + return this; + } catch (final InvocationTargetException e) { + Bukkit.getLogger().log(Level.WARNING, "A error has occured durring invoking of method.", e); + return this; + } + } + + @Override + public FancyMessage itemTooltip(final ItemStack itemStack) { + try { + final Object nmsItem = getOBCasNMSCopyMethod.invoke(null, itemStack); + return itemTooltip(getNMSsaveNBTMethod.invoke(nmsItem, nmsNBTTagCompound.newInstance()).toString()); + } catch (final Exception e) { + e.printStackTrace(); + return this; + } + } + + @Override + public void send(final CommandSender sender, final String jsonString) { + if (!(sender instanceof Player)) { + sender.sendMessage(toOldMessageFormat()); + return; + } + final Player player = (Player) sender; + try { + final Object handle = Reflection.getHandle(player); + if (nmsPlayerConnectionField == null) { + try { + nmsPlayerConnectionField = Reflection.getField(handle.getClass(), "playerConnection"); + } catch (final Exception e) { + for (final Field field : handle.getClass().getDeclaredFields()) { + if (field.getType().getSimpleName().contains("NetHandlerPlayServer")) { + nmsPlayerConnectionField = field; + break; + } + } + } + } + final Object connection = nmsPlayerConnectionField.get(handle); + if (nmsSendPacketMethod == null) { + try { + nmsSendPacketMethod = Reflection.getMethod(connection.getClass(), "sendPacket", nmsPacketClass); + } catch (final Exception e) { + nmsSendPacketMethod = Reflection.getMethodByParamsAndType(connection.getClass(), Void.TYPE, nmsPacketClass).get(0); + } + } + if (nmsSendPacketMethod == null) { + throw new RuntimeException("could find field: sendPacket in EntityPlayer class"); + } + nmsSendPacketMethod.invoke(connection, createChatPacket(jsonString)); + } catch (final IllegalArgumentException e) { + Bukkit.getLogger().log(Level.WARNING, "Argument could not be passed.", e); + } catch (final IllegalAccessException e) { + Bukkit.getLogger().log(Level.WARNING, "Could not access method.", e); + } catch (final InstantiationException e) { + Bukkit.getLogger().log(Level.WARNING, "Underlying class is abstract.", e); + } catch (final InvocationTargetException e) { + Bukkit.getLogger().log(Level.WARNING, "A error has occured durring invoking of method.", e); + } catch (final NoSuchMethodException e) { + Bukkit.getLogger().log(Level.WARNING, "Could not find method.", e); + } catch (final ClassNotFoundException e) { + Bukkit.getLogger().log(Level.WARNING, "Could not find class.", e); + } catch (final SecurityException ex) { + Logger.getLogger(FancyMessageInternal.class.getName()).log(Level.SEVERE, null, ex); + } + } + + @Override + public FancyMessage statisticTooltip(final Statistic which) { + final Type type = which.getType(); + if (type != Type.UNTYPED) { + throw new IllegalArgumentException("That statistic requires an additional " + type + " parameter!"); + } + try { + final Object statistic = getNMSStatisticMethod.invoke(null, which); + return achievementTooltip((String) nmsStatistic_NameField.get(statistic)); + } catch (final IllegalAccessException e) { + Bukkit.getLogger().log(Level.WARNING, "Could not access method.", e); + return this; + } catch (final IllegalArgumentException e) { + Bukkit.getLogger().log(Level.WARNING, "Argument could not be passed.", e); + return this; + } catch (final InvocationTargetException e) { + Bukkit.getLogger().log(Level.WARNING, "A error has occured durring invoking of method.", e); + return this; + } + } + + @Override + public FancyMessage statisticTooltip(final Statistic which, final EntityType entity) { + final Type type = which.getType(); + if (type == Type.UNTYPED) { + throw new IllegalArgumentException("That statistic needs no additional parameter!"); + } + if (type != Type.ENTITY) { + throw new IllegalArgumentException("Wrong parameter type for that statistic - needs " + type + "!"); + } + try { + final Object statistic = getNMSEntityStatisticMethod.invoke(null, which, entity); + return achievementTooltip((String) nmsStatistic_NameField.get(statistic)); + } catch (final IllegalAccessException e) { + Bukkit.getLogger().log(Level.WARNING, "Could not access method.", e); + return this; + } catch (final IllegalArgumentException e) { + Bukkit.getLogger().log(Level.WARNING, "Argument could not be passed.", e); + return this; + } catch (final InvocationTargetException e) { + Bukkit.getLogger().log(Level.WARNING, "A error has occured durring invoking of method.", e); + return this; + } + } + + @Override + public FancyMessage statisticTooltip(final Statistic which, final Material item) { + final Type type = which.getType(); + if (type == Type.UNTYPED) { + throw new IllegalArgumentException("That statistic needs no additional parameter!"); + } + if ((type == Type.BLOCK && item.isBlock()) || type == Type.ENTITY) { + throw new IllegalArgumentException("Wrong parameter type for that statistic - needs " + type + "!"); + } + try { + final Object statistic = getNMSMaterialStatisticMethod.invoke(null, which, item); + return achievementTooltip((String) nmsStatistic_NameField.get(statistic)); + } catch (final IllegalAccessException e) { + Bukkit.getLogger().log(Level.WARNING, "Could not access method.", e); + return this; + } catch (final IllegalArgumentException e) { + Bukkit.getLogger().log(Level.WARNING, "Argument could not be passed.", e); + return this; + } catch (final InvocationTargetException e) { + Bukkit.getLogger().log(Level.WARNING, "A error has occured durring invoking of method.", e); + return this; + } + } + + /** + * 创建Chat数据包 + * + * @param json + * 需要创建的数据包 + * @return 创建发包对象 + * @throws IllegalArgumentException + * @throws IllegalAccessException + * @throws InstantiationException + * @throws InvocationTargetException + * @throws NoSuchMethodException + * @throws ClassNotFoundException + */ + private Object createChatPacket(final String json) throws IllegalArgumentException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException, ClassNotFoundException { + final Object serializedChatComponent = fromJsonMethod.invoke(nmsChatSerializerGsonInstance, json, nmsIChatBaseComponentClass); + return nmsPacketPlayOutChatConstructor.newInstance(serializedChatComponent); + } +} diff --git a/src/main/java/cn/citycraft/TellRaw/manual/FancyMessageManual.java b/src/main/java/cn/citycraft/TellRaw/manual/FancyMessageManual.java new file mode 100644 index 0000000..720f480 --- /dev/null +++ b/src/main/java/cn/citycraft/TellRaw/manual/FancyMessageManual.java @@ -0,0 +1,85 @@ +package cn.citycraft.TellRaw.manual; + +import org.bukkit.Achievement; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.Statistic; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import cn.citycraft.TellRaw.FancyMessage; +import cn.citycraft.TellRaw.common.TextualComponent; + +/** + * 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 class FancyMessageManual extends FancyMessage { + /** + * 新建手动序列化类 + */ + public FancyMessageManual() { + this(""); + } + + /** + * 新建手动序列化类 + * + * @param firstPartText + * 第一个文本串 + */ + public FancyMessageManual(final String firstPartText) { + super(TextualComponent.rawText(firstPartText)); + } + + @Override + public FancyMessage achievementTooltip(final Achievement which) { + throw new UnsupportedOperationException("暂时不支持当前操作."); // To change body of generated methods, choose Tools | Templates. + } + + @Override + public FancyMessage itemTooltip(final ItemStack itemStack) { + this.itemTooltip(new GItemStack(itemStack).toString()); + return this; + } + + @Override + public void send(final CommandSender sender, final String jsonString) { + if (sender instanceof Player) { + Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "tellraw " + sender.getName() + " " + jsonString); + } else { + sender.sendMessage(toOldMessageFormat()); + } + } + + @Override + public FancyMessage statisticTooltip(final Statistic which) { + throw new UnsupportedOperationException("暂时不支持当前操作."); + } + + @Override + public FancyMessage statisticTooltip(final Statistic which, final EntityType entity) { + throw new UnsupportedOperationException("暂时不支持当前操作."); + } + + @Override + public FancyMessage statisticTooltip(final Statistic which, final Material item) { + throw new UnsupportedOperationException("暂时不支持当前操作."); + } +} diff --git a/src/main/java/cn/citycraft/TellRaw/manual/GItemStack.java b/src/main/java/cn/citycraft/TellRaw/manual/GItemStack.java new file mode 100644 index 0000000..8ab1ff5 --- /dev/null +++ b/src/main/java/cn/citycraft/TellRaw/manual/GItemStack.java @@ -0,0 +1,85 @@ +package cn.citycraft.TellRaw.manual; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.Map; +import java.util.logging.Level; + +import org.bukkit.Bukkit; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import cn.citycraft.GsonAgent.GsonAgent; +import cn.citycraft.GsonAgent.api.stream.JsonWriter; +import cn.citycraft.TellRaw.common.JsonRepresentedObject; + +/** + * 序列化Item的类 + * + * @since 2015年12月14日 下午1:30:51 + * @author 许凯 + */ +public class GItemStack implements JsonRepresentedObject { + private final ItemStack item; + private final String jsonString; + + protected GItemStack(final ItemStack item) { + this.item = item.clone(); + final StringWriter string = new StringWriter(); + final JsonWriter json = GsonAgent.newJsonWriter(string); + try { + this.writeJson(json); + json.close(); + } catch (final IOException e) { + throw new RuntimeException("invalid message:", e); + } + this.jsonString = string.toString(); + } + + @Override + public String toString() { + return this.jsonString; + } + + @Override + public void writeJson(final JsonWriter json) throws IOException { + try { + json.beginObject(); + json.nameWithoutQuotes("id").value(this.item.getTypeId()); + json.nameWithoutQuotes("Damage").value(this.item.getDurability()); + if (this.item.getAmount() > 1) { + json.nameWithoutQuotes("Count").value(this.item.getAmount()); + } + if (this.item.hasItemMeta()) { + json.nameWithoutQuotes("tag").beginObject(); + final ItemMeta im = item.getItemMeta(); + if (im.hasEnchants()) { + json.nameWithoutQuotes("ench").beginArray(); + for (final Map.Entry ench : im.getEnchants().entrySet()) { + json.beginObject().nameWithoutQuotes("id").value(ench.getKey().getId()).nameWithoutQuotes("lvl").value(ench.getValue()).endObject(); + } + json.endArray(); + } + if (im.hasDisplayName() || im.hasLore()) { + json.nameWithoutQuotes("display").beginObject(); + if (im.hasDisplayName()) { + json.nameWithoutQuotes("Name").value(im.getDisplayName()); + } + if (im.hasLore()) { + json.nameWithoutQuotes("Lore").beginArray(); + for (final String line : im.getLore()) { + json.value(line); + } + json.endArray(); + } + json.endObject(); + } + json.endObject(); + } + json.endObject(); + } catch (final IOException e) { + Bukkit.getLogger().log(Level.WARNING, "A problem occured during writing of JSON string", e); + } + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..b6ab82e --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,13 @@ +Data: + #数据保存方式 [sqlite|MySQL] + FileSystem: sqlite + #MySQL数据库配置 只有当FileSystem配置为MySQL时有效 + MySQL: + #数据库需要自行建立 + database: minecraft + #数据表需要自行建立 + tablename: prefixs + username: root + password: + ip: localhost + port: 3306 \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..3f87c06 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,21 @@ +name: ${project.artifactId} +description: ${project.description} +main: ${project.groupId}.${project.artifactId}.${project.artifactId} +version: ${project.version}-Build#${env.GIT_ID} +author: 喵♂呜 +website: ${jenkins.url}/job/${project.artifactId}/ +commands: + ${project.artifactId}: + description: ${project.artifactId} - ${project.description} + aliases: + - xxxx + usage: §b使用/${project.artifactId} help 查看帮助! + permission: ${project.artifactId}.reload + permission-message: §c你没有 的权限来执行此命令! +permissions: + ${project.artifactId}.use: + description: ${project.artifactId} 使用! + default: true + ${project.artifactId}.reload: + description: 重新载入插件! + default: op \ No newline at end of file