package net.glowstone.command; import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; import com.google.gson.reflect.TypeToken; import net.glowstone.entity.GlowPlayer; import net.glowstone.net.message.play.game.TitleMessage; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; import org.bukkit.command.defaults.BukkitCommand; import org.bukkit.entity.Player; import org.json.simple.JSONObject; import org.json.simple.JSONValue; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; public class TitleCommand extends BukkitCommand { public TitleCommand() { super("title", "Sends a title to the specified player(s)", "/title <player> <title|subtitle|times|clear|reset> ...", Collections.emptyList()); setPermission("glowstone.command.title"); } /** * Converts a valid JSON chat component to a basic colored string. This does not parse * components like hover or click events. This returns null on parse failure. * * @param json the json chat component * @return the colored string, or null */ public String convertJson(Map<String, Object> json) { if (json == null || !json.containsKey("text") && !(json.get("text") instanceof String)) return null; // We can't even parse this ChatColor color = ChatColor.WHITE; List<ChatColor> style = new ArrayList<>(); for (Object key : json.keySet()) { if (!(key instanceof String)) continue; String keyString = (String) key; if (keyString.equalsIgnoreCase("color")) { if (!(json.get("color") instanceof String)) return null; color = toColor((String) json.get(keyString)); } else if (!keyString.equalsIgnoreCase("text")) { if (toColor(keyString) == null) return null; style.add(toColor(keyString)); } } style.add(color); String text = (String) json.get("text"); for (ChatColor c : style) { text = c + text; } return text; } private static ChatColor toColor(String name) { if (name.equals("obfuscated")) return ChatColor.MAGIC; if (name.equals("underlined")) return ChatColor.UNDERLINE; // Loop to avoid exceptions, we'll just return null if it can't be parsed for (ChatColor color : ChatColor.values()) { if (color == ChatColor.MAGIC) continue; // This isn't a valid value for color anyways if (color.name().equalsIgnoreCase(name.toUpperCase())) return color; } return null; } @Override public boolean execute(CommandSender sender, String commandLabel, String[] args) { if (!testPermission(sender)) return true; if (args.length < 2) { sender.sendMessage(ChatColor.RED + "Usage: " + usageMessage); return false; } Player player = Bukkit.getPlayerExact(args[0]); if (player == null || sender instanceof Player && !((Player) sender).canSee(player)) { sender.sendMessage("There's no player by that name online."); return false; } String action = args[1]; if (action.equalsIgnoreCase("clear")) { ((GlowPlayer) player).clearTitle(); sender.sendMessage("Cleared " + player.getName() + "'s title"); } else if (action.equalsIgnoreCase("reset")) { player.resetTitle(); sender.sendMessage("Reset " + player.getName() + "'s title"); } else if (action.equalsIgnoreCase("title")) { if (args.length < 3) { sender.sendMessage(ChatColor.RED + "Usage: /title <player> " + action + " <raw json>"); return false; } StringBuilder message = new StringBuilder(); for (int i = 2; i < args.length; i++) { message.append(args[i]); } String raw = message.toString().trim(); if (!validJson(raw)) { sender.sendMessage(ChatColor.RED + "Invalid JSON: Could not parse, invalid format?"); return false; } String component = raw; Map<String, Object> parsed = getJson(raw); if (parsed != null) { component = convertJson(parsed); } ((GlowPlayer) player).updateTitle(TitleMessage.Action.TITLE, component); ((GlowPlayer) player).sendTitle(); sender.sendMessage("Updated " + player.getName() + "'s title"); } else if (action.equalsIgnoreCase("subtitle")) { if (args.length < 3) { sender.sendMessage(ChatColor.RED + "Usage: /title <player> " + action + " <raw json>"); return false; } StringBuilder message = new StringBuilder(); for (int i = 2; i < args.length; i++) { message.append(args[i]); } String raw = message.toString().trim(); if (!validJson(raw)) { sender.sendMessage(ChatColor.RED + "Invalid JSON: Could not parse, invalid format?"); return false; } String component = raw; Object parsed = JSONValue.parse(raw); if (parsed instanceof JSONObject) { component = convertJson((JSONObject) parsed); } ((GlowPlayer) player).updateTitle(TitleMessage.Action.SUBTITLE, component); sender.sendMessage("Updated " + player.getName() + "'s subtitle"); } else if (action.equalsIgnoreCase("times")) { if (args.length != 5) { sender.sendMessage(ChatColor.RED + "Usage: /title <player> " + action + " <fade in> <stay time> <fade out>"); return false; } if (!tryParseInt(args[2])) { sender.sendMessage(ChatColor.RED + "'" + args[2] + "' is not a number"); return false; } if (!tryParseInt(args[3])) { sender.sendMessage(ChatColor.RED + "'" + args[3] + "' is not a number"); return false; } if (!tryParseInt(args[4])) { sender.sendMessage(ChatColor.RED + "'" + args[4] + "' is not a number"); return false; } ((GlowPlayer) player).updateTitle(TitleMessage.Action.TIMES, toInt(args[2]), toInt(args[3]), toInt(args[4])); sender.sendMessage("Updated " + player.getName() + "'s times"); } else { sender.sendMessage(ChatColor.RED + "Usage: " + usageMessage); return false; } return true; } private boolean tryParseInt(String number) { try { Integer.parseInt(number); } catch (NumberFormatException | NullPointerException e) { return false; } // only got here if we didn't return false return true; } private int toInt(String number) { return Integer.parseInt(number.trim()); } private boolean validJson(String raw) { Map<String, Object> object = getJson(raw); if (object == null) { // Could not parse JSON: Check to see if it's at least a single word // Rule set: // 1. Cannot contain a space (or else the client fails) // 2. Must not look like JSON (first character check is sufficient) return !raw.contains(" ") && raw.charAt(0) != '{' && raw.charAt(0) != '['; } Map<String, Object> json = object; // Run through all of the keys to see if they are valid keys, // and have valid values for (Object key : json.keySet()) { Object value = json.get(key); if (!(key instanceof String)) return false; // The key is not a string, meaning that it is not valid String keyString = (String) key; switch (keyString) { case "text": if (!(value instanceof String)) return false; // The value is not a valid type break; case "color": if (!(value instanceof String)) return false; // The value is not a valid type break; case "bold": if (!(value instanceof Boolean)) return false; // The value is not a valid type break; case "italic": if (!(value instanceof Boolean)) return false; // The value is not a valid type break; case "underlined": if (!(value instanceof Boolean)) return false; // The value is not a valid type break; case "strikethrough": if (!(value instanceof Boolean)) return false; // The value is not a valid type break; case "obfuscated": if (!(value instanceof Boolean)) return false; // The value is not a valid type break; default: // The key is not in the list of valid keys, // meaning that it is not a valid key return false; } } // If we made it this far then it has a pretty good chance at being valid return true; } private Map<String, Object> getJson(String raw) { Gson gson = new Gson(); try { Map<String, Object> map = gson.fromJson(raw, new TypeToken<Map<String, Object>>() { }.getType()); return map; } catch (JsonSyntaxException e) { // Bukkit.getLogger().log(Level.SEVERE, e.getMessage(), e); return null; } } }