package cx.sfy.TheBridge.fanciful; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.stream.JsonWriter; import cx.sfy.TheBridge.fanciful.ArrayWrapper; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; import org.bukkit.configuration.serialization.ConfigurationSerializable; import org.bukkit.configuration.serialization.ConfigurationSerialization; import org.bukkit.entity.Player; import static cx.sfy.TheBridge.fanciful.TextualComponent.rawText; import java.io.IOException; import java.io.StringWriter; 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; public class FancyMessage implements JsonRepresentedObject, Cloneable, Iterable, ConfigurationSerializable { static { ConfigurationSerialization.registerClass(FancyMessage.class); } private List messageParts; private String jsonString; private boolean dirty; @Override public FancyMessage clone() throws CloneNotSupportedException { 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; } public FancyMessage(final String firstPartText) { this(rawText(firstPartText)); } public FancyMessage(final TextualComponent firstPartText) { messageParts = new ArrayList(); messageParts.add(new MessagePart(firstPartText)); jsonString = null; dirty = false; } public FancyMessage() { this((TextualComponent) null); } public FancyMessage text(String text) { MessagePart latest = latest(); latest.text = rawText(text); dirty = true; return this; } public FancyMessage text(TextualComponent text) { MessagePart latest = latest(); latest.text = text; dirty = true; return this; } public FancyMessage color(final ChatColor color) { if (!color.isColor()) { throw new IllegalArgumentException(color.name() + " is not a color"); } latest().color = color; dirty = true; return this; } public FancyMessage style(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; } public FancyMessage file(final String path) { onClick("open_file", path); return this; } public FancyMessage link(final String url) { onClick("open_url", url); return this; } public FancyMessage suggest(final String command) { onClick("suggest_command", command); return this; } public FancyMessage insert(final String command) { latest().insertionData = command; dirty = true; return this; } public FancyMessage command(final String command) { onClick("run_command", command); return this; } public FancyMessage achievementTooltip(final String name) { onHover("show_achievement", new JsonString("achievement." + name)); return this; } public FancyMessage tooltip(final String text) { onHover("show_text", new JsonString(text)); return this; } public FancyMessage tooltip(final Iterable lines) { tooltip(ArrayWrapper.toArray(lines, String.class)); return this; } public FancyMessage tooltip(final String... lines) { 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; } public FancyMessage formattedTooltip(FancyMessage text) { for (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; } public FancyMessage formattedTooltip(FancyMessage... lines) { if (lines.length < 1) { onHover(null, null); return this; } FancyMessage result = new FancyMessage(); result.messageParts.clear(); for (int i = 0; i < lines.length; i++) { try { for (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 (CloneNotSupportedException e) { Bukkit.getLogger().log(Level.WARNING, "Failed to clone object", e); return this; } } return formattedTooltip(result.messageParts.isEmpty() ? null : result); // Throws NPE if size is 0, intended } public FancyMessage formattedTooltip(final Iterable lines) { return formattedTooltip(ArrayWrapper.toArray(lines, FancyMessage.class)); } public FancyMessage translationReplacements(final String... replacements) { for (String str : replacements) { latest().translationReplacements.add(new JsonString(str)); } dirty = true; return this; } public FancyMessage translationReplacements(final FancyMessage... replacements) { for (FancyMessage str : replacements) { latest().translationReplacements.add(str); } dirty = true; return this; } public FancyMessage translationReplacements(final Iterable replacements) { return translationReplacements(ArrayWrapper.toArray(replacements, FancyMessage.class)); } public FancyMessage then(final String text) { return then(rawText(text)); } 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; } public FancyMessage then() { if (!latest().hasText()) { throw new IllegalStateException("previous message part has no text"); } messageParts.add(new MessagePart()); dirty = true; return this; } @Override public void writeJson(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(); } } public String toJSONString() { if (!dirty && jsonString != null) { return jsonString; } StringWriter string = new StringWriter(); JsonWriter json = new JsonWriter(string); try { writeJson(json); json.close(); } catch (IOException e) { throw new RuntimeException("invalid message"); } jsonString = string.toString(); dirty = false; return jsonString; } public void send(Player player) { send(player, toJSONString()); } private void send(CommandSender sender, String jsonString) { if (!(sender instanceof Player)) { sender.sendMessage(toOldMessageFormat()); return; } Player player = (Player) sender; Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "tellraw " + player.getName() + " " + jsonString); } public void send(CommandSender sender) { send(sender, toJSONString()); } public void send(final Iterable senders) { String string = toJSONString(); for (final CommandSender sender : senders) { send(sender, string); } } public String toOldMessageFormat() { StringBuilder result = new StringBuilder(); for (MessagePart part : this) { result.append(part.color == null ? "" : part.color); for (ChatColor formatSpecifier : part.styles) { result.append(formatSpecifier); } result.append(part.text); } return result.toString(); } private MessagePart latest() { return messageParts.get(messageParts.size() - 1); } private void onClick(final String name, final String data) { final MessagePart latest = latest(); latest.clickActionName = name; latest.clickActionData = data; dirty = true; } private void onHover(final String name, final JsonRepresentedObject data) { final MessagePart latest = latest(); latest.hoverActionName = name; latest.hoverActionData = data; dirty = true; } public Map serialize() { HashMap map = new HashMap(); map.put("messageParts", messageParts); return map; } @SuppressWarnings("unchecked") public static FancyMessage deserialize(Map serialized) { FancyMessage msg = new FancyMessage(); msg.messageParts = (List) serialized.get("messageParts"); msg.jsonString = serialized.containsKey("JSON") ? serialized.get("JSON").toString() : null; msg.dirty = !serialized.containsKey("JSON"); return msg; } public Iterator iterator() { return messageParts.iterator(); } private static JsonParser _stringParser = new JsonParser(); public static FancyMessage deserialize(String json) { JsonObject serialized = _stringParser.parse(json).getAsJsonObject(); JsonArray extra = serialized.getAsJsonArray("extra"); FancyMessage returnVal = new FancyMessage(); returnVal.messageParts.clear(); for (JsonElement mPrt : extra) { MessagePart component = new MessagePart(); JsonObject messagePart = mPrt.getAsJsonObject(); for (Map.Entry entry : messagePart.entrySet()) { if (TextualComponent.isTextKey(entry.getKey())) { Map serializedMapForm = new HashMap(); serializedMapForm.put("key", entry.getKey()); if (entry.getValue().isJsonPrimitive()) { serializedMapForm.put("value", entry.getValue().getAsString()); } else { for (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")) { JsonObject object = entry.getValue().getAsJsonObject(); component.clickActionName = object.get("action").getAsString(); component.clickActionData = object.get("value").getAsString(); } else if (entry.getKey().equals("hoverEvent")) { JsonObject object = entry.getValue().getAsJsonObject(); component.hoverActionName = object.get("action").getAsString(); if (object.get("value").isJsonPrimitive()) { component.hoverActionData = new JsonString(object.get("value").getAsString()); } else { component.hoverActionData = deserialize(object.get("value").toString() /* This should properly serialize the JSON object as a JSON string */); } } else if (entry.getKey().equals("insertion")) { component.insertionData = entry.getValue().getAsString(); } else if (entry.getKey().equals("with")) { for (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; } }