From f31b33bbde9882018c91a5ecfa5d8cffe8a851a8 Mon Sep 17 00:00:00 2001 From: boy0001 Date: Wed, 24 Jun 2015 07:44:44 +1000 Subject: [PATCH] Optimize flag system, work on cleaning plot list --- .../plot/PlotSquared.java | 2 + .../plot/commands/DebugFixFlags.java | 23 +- .../plot/commands/FlagCmd.java | 4 +- .../plot/commands/list.java | 19 +- .../plot/config/Settings.java | 4 + .../plot/database/AbstractDB.java | 5 +- .../plot/database/DBFunc.java | 5 +- .../plot/database/SQLManager.java | 38 +- .../plot/events/ClusterFlagRemoveEvent.java | 90 ++ .../plot/flag/AbstractFlag.java | 5 + .../plot/flag/FlagManager.java | 192 ++-- .../plot/listeners/PlayerEvents.java | 19 +- .../plot/object/Plot.java | 15 +- .../plot/object/PlotSettings.java | 3 +- .../plot/object/PlotWorld.java | 4 +- .../plot/util/EventUtil.java | 3 + .../plot/util/MainUtil.java | 2 +- .../plot/util/bukkit/BukkitEventUtil.java | 7 + .../plot/util/bukkit/SendChunk.java | 3 + .../plot/util/bukkit/chat/ArrayWrapper.java | 106 +++ .../plot/util/bukkit/chat/FancyMessage.java | 845 ++++++++++++++++++ .../bukkit/chat/JsonRepresentedObject.java | 19 + .../plot/util/bukkit/chat/JsonString.java | 47 + .../plot/util/bukkit/chat/MessagePart.java | 154 ++++ .../plot/util/bukkit/chat/Reflection.java | 213 +++++ .../util/bukkit/chat/TextualComponent.java | 292 ++++++ 26 files changed, 1960 insertions(+), 159 deletions(-) create mode 100644 PlotSquared/src/main/java/com/intellectualcrafters/plot/events/ClusterFlagRemoveEvent.java create mode 100644 PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/chat/ArrayWrapper.java create mode 100644 PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/chat/FancyMessage.java create mode 100644 PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/chat/JsonRepresentedObject.java create mode 100644 PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/chat/JsonString.java create mode 100644 PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/chat/MessagePart.java create mode 100644 PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/chat/Reflection.java create mode 100644 PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/chat/TextualComponent.java diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/PlotSquared.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/PlotSquared.java index 9aa96c146..a97d92ea0 100644 --- a/PlotSquared/src/main/java/com/intellectualcrafters/plot/PlotSquared.java +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/PlotSquared.java @@ -803,6 +803,7 @@ public class PlotSquared { options.put("confirmation.unlink", Settings.CONFIRM_UNLINK); // Protection + options.put("protection.redstone.disable-offline", Settings.REDSTONE_DISABLER); options.put("protection.tnt-listener.enabled", Settings.TNT_LISTENER); options.put("protection.piston.falling-blocks", Settings.PISTON_FALLING_BLOCK_CHECK); options.put("protection.physics-listener.enabled", Settings.PHYSICS_LISTENER); @@ -885,6 +886,7 @@ public class PlotSquared { Settings.CONFIRM_UNLINK = config.getBoolean("confirmation.unlink"); // Protection + Settings.REDSTONE_DISABLER = config.getBoolean("protection.tnt-listener.enabled"); Settings.TNT_LISTENER = config.getBoolean("protection.tnt-listener.enabled"); Settings.PISTON_FALLING_BLOCK_CHECK = config.getBoolean("protection.piston.falling-blocks"); Settings.PHYSICS_LISTENER = config.getBoolean("protection.physics-listener.enabled"); diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/commands/DebugFixFlags.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/commands/DebugFixFlags.java index bb6bbe244..fd2959b45 100644 --- a/PlotSquared/src/main/java/com/intellectualcrafters/plot/commands/DebugFixFlags.java +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/commands/DebugFixFlags.java @@ -21,6 +21,9 @@ package com.intellectualcrafters.plot.commands; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map.Entry; import java.util.Set; import com.intellectualcrafters.plot.PlotSquared; @@ -56,19 +59,17 @@ public class DebugFixFlags extends SubCommand { } MainUtil.sendMessage(plr, "&8--- &6Starting task &8 ---"); for (final Plot plot : PlotSquared.getPlots(world).values()) { - final Set flags = plot.settings.flags; - final ArrayList toRemove = new ArrayList(); - for (final Flag flag : flags) { - final AbstractFlag af = FlagManager.getFlag(flag.getKey()); - if (af == null) { - toRemove.add(flag); + final HashMap flags = plot.settings.flags; + Iterator> i = flags.entrySet().iterator(); + boolean changed = false; + while (i.hasNext()) { + if (FlagManager.getFlag(i.next().getKey()) == null) { + changed = true; + i.remove(); } } - for (final Flag flag : toRemove) { - plot.settings.flags.remove(flag); - } - if (toRemove.size() > 0) { - DBFunc.setFlags(plot.world, plot, plot.settings.flags); + if (changed) { + DBFunc.setFlags(plot.world, plot, plot.settings.flags.values()); } } MainUtil.sendMessage(plr, "&aDone!"); diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/commands/FlagCmd.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/commands/FlagCmd.java index 082458fb7..4414a483a 100644 --- a/PlotSquared/src/main/java/com/intellectualcrafters/plot/commands/FlagCmd.java +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/commands/FlagCmd.java @@ -154,7 +154,7 @@ public class FlagCmd extends SubCommand { if ((args.length == 3) && flag.getAbstractFlag().isList()) { final String value = StringUtils.join(Arrays.copyOfRange(args, 2, args.length), " "); ((FlagValue.ListValue) flag.getAbstractFlag().value).remove(flag.getValue(), value); - DBFunc.setFlags(plot.world, plot, plot.settings.flags); + DBFunc.setFlags(plot.world, plot, plot.settings.flags.values()); } else { final boolean result = FlagManager.removePlotFlag(plot, flag.getKey()); if (!result) { @@ -201,7 +201,7 @@ public class FlagCmd extends SubCommand { MainUtil.sendMessage(player, C.FLAG_NOT_ADDED); return false; } - DBFunc.setFlags(plot.world, plot, plot.settings.flags); + DBFunc.setFlags(plot.world, plot, plot.settings.flags.values()); MainUtil.sendMessage(player, C.FLAG_ADDED); APlotListener.manager.plotEntry(player, plot); return true; diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/commands/list.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/commands/list.java index d0a203b9a..d27e78f3d 100644 --- a/PlotSquared/src/main/java/com/intellectualcrafters/plot/commands/list.java +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/commands/list.java @@ -23,6 +23,7 @@ package com.intellectualcrafters.plot.commands; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.UUID; @@ -238,12 +239,12 @@ public class list extends SubCommand { return false; } - displayPlots(plr, plots, page, world); + displayPlots(plr, plots, 12, page, world); return true; } - public void displayPlots(PlotPlayer player, Collection oldPlots, int page, String world) { - ArrayList plots; + public void displayPlots(PlotPlayer player, Collection oldPlots, int pageSize, int page, String world) { + List plots; if (world != null) { plots = PlotSquared.sortPlots(oldPlots, world); } @@ -253,18 +254,20 @@ public class list extends SubCommand { if (page < 0) { page = 0; } - // Get the total pages - // int totalPages = ((int) Math.ceil(12 * - // (PlotSquared.getPlotsSorted().size()) / 100)); - final int totalPages = (int) Math.ceil(plots.size() / 12); + final int totalPages = (int) Math.ceil(plots.size() / pageSize); if (page > totalPages) { page = totalPages; } // Only display 12! - int max = (page * 12) + 12; + int max = (page * pageSize) + pageSize; if (max > plots.size()) { max = plots.size(); } + + plots = plots.subList(page * pageSize, max); + + + final StringBuilder string = new StringBuilder(); string.append(C.PLOT_LIST_HEADER_PAGED.s().replaceAll("%cur", page + 1 + "").replaceAll("%max", totalPages + 1 + "").replaceAll("%word%", "all")).append("\n"); Plot p; diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/config/Settings.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/config/Settings.java index 26235dd1a..78d3a0d8b 100644 --- a/PlotSquared/src/main/java/com/intellectualcrafters/plot/config/Settings.java +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/config/Settings.java @@ -57,6 +57,10 @@ public class Settings { * TNT listener */ public static boolean TNT_LISTENER = false; + /** + * Redstone disabler + */ + public static boolean REDSTONE_DISABLER = false; /** * Check for falling blocks when pistons extend? */ diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/database/AbstractDB.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/database/AbstractDB.java index 5e3151d9d..5a6e5dcd8 100644 --- a/PlotSquared/src/main/java/com/intellectualcrafters/plot/database/AbstractDB.java +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/database/AbstractDB.java @@ -22,6 +22,7 @@ package com.intellectualcrafters.plot.database; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -148,7 +149,7 @@ public interface AbstractDB { * @param plot Plot Object * @param flags flags to set (flag[]) */ - public void setFlags(final String world, final Plot plot, final Set flags); + public void setFlags(final String world, final Plot plot, final Collection flags); /** * Set cluster flags @@ -156,7 +157,7 @@ public interface AbstractDB { * @param cluster PlotCluster Object * @param flags flags to set (flag[]) */ - public void setFlags(final PlotCluster cluster, final Set flags); + public void setFlags(final PlotCluster cluster, final Collection flags); /** * Rename a cluster diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/database/DBFunc.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/database/DBFunc.java index 8cb353f42..3c3662b04 100644 --- a/PlotSquared/src/main/java/com/intellectualcrafters/plot/database/DBFunc.java +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/database/DBFunc.java @@ -24,6 +24,7 @@ import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -181,11 +182,11 @@ public class DBFunc { dbManager.setMerged(world, plot, merged); } - public static void setFlags(final String world, final Plot plot, final Set flags) { + public static void setFlags(final String world, final Plot plot, final Collection flags) { dbManager.setFlags(world, plot, flags); } - public static void setFlags(final PlotCluster cluster, final Set flags) { + public static void setFlags(final PlotCluster cluster, final Collection flags) { dbManager.setFlags(cluster, flags); } diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/database/SQLManager.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/database/SQLManager.java index 961566bd5..03b95ae1c 100644 --- a/PlotSquared/src/main/java/com/intellectualcrafters/plot/database/SQLManager.java +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/database/SQLManager.java @@ -28,6 +28,7 @@ import java.sql.SQLException; import java.sql.Statement; import java.sql.Timestamp; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -477,7 +478,7 @@ public class SQLManager implements AbstractDB { else { final StringBuilder flag_string = new StringBuilder(); int k = 0; - for (final Flag flag : pair.settings.flags) { + for (final Flag flag : pair.settings.flags.values()) { if (k != 0) { flag_string.append(","); } @@ -523,7 +524,7 @@ public class SQLManager implements AbstractDB { else { final StringBuilder flag_string = new StringBuilder(); int k = 0; - for (final Flag flag : pair.settings.flags) { + for (final Flag flag : pair.settings.flags.values()) { if (k != 0) { flag_string.append(","); } @@ -1022,7 +1023,7 @@ public class SQLManager implements AbstractDB { flags_string = new String[] {}; } } - final Set flags = new HashSet(); + final HashMap flags = new HashMap<>(); boolean exception = false; for (String element : flags_string) { if (element.contains(":")) { @@ -1030,7 +1031,7 @@ public class SQLManager implements AbstractDB { try { final String flag_str = split[1].replaceAll("\u00AF", ":").replaceAll("\u00B4", ","); final Flag flag = new Flag(FlagManager.getFlag(split[0], true), flag_str); - flags.add(flag); + flags.put(flag.getKey(), flag); } catch (final Exception e) { e.printStackTrace(); exception = true; @@ -1038,7 +1039,8 @@ public class SQLManager implements AbstractDB { } else { element = element.replaceAll("\u00AF", ":").replaceAll("\u00B4", ","); if (StringUtils.isAlpha(element.replaceAll("_", "").replaceAll("-", ""))) { - flags.add(new Flag(FlagManager.getFlag(element, true), "")); + Flag flag = new Flag(FlagManager.getFlag(element, true), ""); + flags.put(flag.getKey(), flag); } else { PlotSquared.log("INVALID FLAG: " + element); @@ -1047,7 +1049,8 @@ public class SQLManager implements AbstractDB { } if (exception) { PlotSquared.log("&cPlot " + id + " had an invalid flag. A fix has been attempted."); - setFlags(id, flags.toArray(new Flag[0])); + PlotSquared.log("&c" + myflags); + setFlags(id, flags.values()); } plot.settings.flags = flags; } else { @@ -1154,7 +1157,7 @@ public class SQLManager implements AbstractDB { } @Override - public void setFlags(final String world, final Plot plot, final Set flags) { + public void setFlags(final String world, final Plot plot, final Collection flags) { final StringBuilder flag_string = new StringBuilder(); int i = 0; for (final Flag flag : flags) { @@ -1181,13 +1184,7 @@ public class SQLManager implements AbstractDB { }); } - public void setFlags(final int id, final Flag[] flags) { - final ArrayList newflags = new ArrayList(); - for (final Flag flag : flags) { - if ((flag != null) && (flag.getKey() != null) && !flag.getKey().equals("")) { - newflags.add(flag); - } - } + public void setFlags(final int id, final Collection newflags) { final String flag_string = StringUtils.join(newflags, ","); TaskManager.runTaskAsync(new Runnable() { @Override @@ -1858,7 +1855,7 @@ public class SQLManager implements AbstractDB { flags_string = new String[] {}; } } - final Set flags = new HashSet(); + final HashMap flags = new HashMap<>(); boolean exception = false; for (final String element : flags_string) { if (element.contains(":")) { @@ -1866,18 +1863,19 @@ public class SQLManager implements AbstractDB { try { final String flag_str = split[1].replaceAll("\u00AF", ":").replaceAll("�", ","); final Flag flag = new Flag(FlagManager.getFlag(split[0], true), flag_str); - flags.add(flag); + flags.put(flag.getKey(), flag); } catch (final Exception e) { e.printStackTrace(); exception = true; } } else { - flags.add(new Flag(FlagManager.getFlag(element, true), "")); + Flag flag = new Flag(FlagManager.getFlag(element, true), ""); + flags.put(flag.getKey(), flag); } } if (exception) { - PlotSquared.log("&cPlot " + id + " had an invalid flag. A fix has been attempted."); - setFlags(id, flags.toArray(new Flag[0])); + PlotSquared.log("&cCluster " + id + " had an invalid flag. A fix has been attempted."); + PlotSquared.log("&c" + myflags); } cluster.settings.flags = flags; } else { @@ -1909,7 +1907,7 @@ public class SQLManager implements AbstractDB { } @Override - public void setFlags(final PlotCluster cluster, final Set flags) { + public void setFlags(final PlotCluster cluster, final Collection flags) { final StringBuilder flag_string = new StringBuilder(); int i = 0; for (final Flag flag : flags) { diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/events/ClusterFlagRemoveEvent.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/events/ClusterFlagRemoveEvent.java new file mode 100644 index 000000000..a8ac60977 --- /dev/null +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/events/ClusterFlagRemoveEvent.java @@ -0,0 +1,90 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// PlotSquared - A plot manager and world generator for the Bukkit API / +// Copyright (c) 2014 IntellectualSites/IntellectualCrafters / +// / +// This program is free software; you can redistribute it and/or modify / +// it under the terms of the GNU General Public License as published by / +// the Free Software Foundation; either version 3 of the License, or / +// (at your option) any later version. / +// / +// This program is distributed in the hope that it will be useful, / +// but WITHOUT ANY WARRANTY; without even the implied warranty of / +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the / +// GNU General Public License for more details. / +// / +// You should have received a copy of the GNU General Public License / +// along with this program; if not, write to the Free Software Foundation, / +// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA / +// / +// You can contact us via: support@intellectualsites.com / +//////////////////////////////////////////////////////////////////////////////////////////////////// +package com.intellectualcrafters.plot.events; + +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +import com.intellectualcrafters.plot.flag.Flag; +import com.intellectualcrafters.plot.object.Plot; +import com.intellectualcrafters.plot.object.PlotCluster; + +/** + * Called when a flag is removed from a plot + * + * @author Citymonstret + * @author Empire92 + */ +public class ClusterFlagRemoveEvent extends Event implements Cancellable { + private static HandlerList handlers = new HandlerList(); + private final PlotCluster cluster; + private final Flag flag; + private boolean cancelled; + + /** + * PlotFlagRemoveEvent: Called when a flag is removed from a plot + * + * @param flag Flag that was removed + * @param plot Plot from which the flag was removed + */ + public ClusterFlagRemoveEvent(final Flag flag, final PlotCluster cluster) { + this.cluster = cluster; + this.flag = flag; + } + + public static HandlerList getHandlerList() { + return handlers; + } + + /** + * Get the cluster involved + * + * @return PlotCluster + */ + public PlotCluster getCluster() { + return this.cluster; + } + + /** + * Get the flag involved + * + * @return Flag + */ + public Flag getFlag() { + return this.flag; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + @Override + public boolean isCancelled() { + return this.cancelled; + } + + @Override + public void setCancelled(final boolean b) { + this.cancelled = b; + } +} diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/flag/AbstractFlag.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/flag/AbstractFlag.java index 0eec59d6c..1fdbb1d0b 100644 --- a/PlotSquared/src/main/java/com/intellectualcrafters/plot/flag/AbstractFlag.java +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/flag/AbstractFlag.java @@ -89,6 +89,11 @@ public class AbstractFlag { public String toString() { return this.key; } + + @Override + public int hashCode() { + return this.key.hashCode(); + } @Override public boolean equals(final Object other) { diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/flag/FlagManager.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/flag/FlagManager.java index f65885567..837c8ad61 100644 --- a/PlotSquared/src/main/java/com/intellectualcrafters/plot/flag/FlagManager.java +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/flag/FlagManager.java @@ -22,6 +22,8 @@ package com.intellectualcrafters.plot.flag; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -48,7 +50,7 @@ public class FlagManager { // - Plot clear interval // - Mob cap // - customized plot composition - private final static ArrayList flags = new ArrayList<>(); + private final static HashSet flags = new HashSet<>(); /** * Register an AbstractFlag with PlotSquared @@ -57,42 +59,35 @@ public class FlagManager { * * @return boolean success */ - public static boolean addFlag(final AbstractFlag af) { + public static boolean addFlag(AbstractFlag af) { PlotSquared.log(C.PREFIX.s() + "&8 - Adding flag: &7" + af); for (PlotWorld plotworld : PlotSquared.getPlotWorldObjects()) { - for (final Flag flag : plotworld.DEFAULT_FLAGS) { - if (flag.getAbstractFlag().getKey().equals(af.getKey())) { - flag.setKey(af); - } + Flag flag = plotworld.DEFAULT_FLAGS.get(af.getKey()); + if (flag != null) { + flag.setKey(af); } } if (PlotSquared.getAllPlotsRaw() != null) { for (final Plot plot : PlotSquared.getPlotsRaw()) { - for (final Flag flag : plot.settings.flags) { - if (flag.getAbstractFlag().getKey().equals(af.getKey())) { - flag.setKey(af); - } + Flag flag = plot.settings.flags.get(af.getKey()); + if (flag != null) { + flag.setKey(af); } } } return (getFlag(af.getKey()) == null) && flags.add(af); } - public static Flag getSettingFlag(final String world, final PlotSettings settings, final String flag) { - final ArrayList flags = new ArrayList<>(); - if ((settings.flags != null) && (settings.flags.size() > 0)) { - flags.addAll(settings.flags); - } - final PlotWorld plotworld = PlotSquared.getPlotWorld(world); - if ((plotworld != null) && (plotworld.DEFAULT_FLAGS != null) && (plotworld.DEFAULT_FLAGS.length > 0)) { - flags.addAll(Arrays.asList(plotworld.DEFAULT_FLAGS)); - } - for (final Flag myflag : flags) { - if (myflag.getKey().equals(flag)) { - return myflag; + public static Flag getSettingFlag(final String world, final PlotSettings settings, final String id) { + Flag flag = settings.flags.get(id); + if (flag == null) { + PlotWorld plotworld = PlotSquared.getPlotWorld(world); + if (plotworld == null) { + return null; } + return plotworld.DEFAULT_FLAGS.get(id); } - return null; + return flag; } public static boolean isBooleanFlag(final Plot plot, final String key, final boolean defaultValue) { @@ -153,12 +148,7 @@ public class FlagManager { if ((settings.flags == null) || (settings.flags.size() == 0)) { return null; } - for (final Flag myflag : settings.flags) { - if (myflag.getKey().equals(flag)) { - return myflag; - } - } - return null; + return settings.flags.get(flag); } /** @@ -175,8 +165,8 @@ public class FlagManager { if (hasFlag != null) { plot.settings.flags.remove(hasFlag); } - plot.settings.flags.add(flag); - DBFunc.setFlags(plot.world, plot, plot.settings.flags); + plot.settings.flags.put(flag.getKey(), flag); + DBFunc.setFlags(plot.world, plot, plot.settings.flags.values()); return true; } @@ -185,22 +175,15 @@ public class FlagManager { if (!result) { return false; } - final Flag hasFlag = getPlotFlag(plot, flag.getKey()); - if (hasFlag != null) { - plot.settings.flags.remove(hasFlag); - } - plot.settings.flags.add(flag); + plot.settings.flags.put(flag.getKey(), flag); return true; } public static boolean addClusterFlag(final PlotCluster cluster, final Flag flag) { //TODO plot cluster flag event final Flag hasFlag = getSettingFlag(cluster.world, cluster.settings, flag.getKey()); - if (hasFlag != null) { - cluster.settings.flags.remove(hasFlag); - } - cluster.settings.flags.add(flag); - DBFunc.setFlags(cluster, cluster.settings.flags); + cluster.settings.flags.put(flag.getKey(), flag); + DBFunc.setFlags(cluster, cluster.settings.flags.values()); return true; } @@ -209,76 +192,81 @@ public class FlagManager { * @param plot * @return set of flags */ - public static Set getPlotFlags(final Plot plot) { + public static Collection getPlotFlags(final Plot plot) { return getSettingFlags(plot.world, plot.settings); } - public static Set getSettingFlags(final String world, final PlotSettings settings) { - final Set plotflags = settings.flags; - final PlotWorld plotworld = PlotSquared.getPlotWorld(world); - if ((plotworld != null) && (plotworld.DEFAULT_FLAGS != null) && (plotworld.DEFAULT_FLAGS.length > 0)) { - final HashSet flagStrings = new HashSet<>(); - for (final Flag flag : plotflags) { - flagStrings.add(flag.getKey()); - } - for (final Flag newflag : plotworld.DEFAULT_FLAGS) { - if (!flagStrings.contains(newflag.getKey())) { - plotflags.add(newflag); - } - } + public static Collection getSettingFlags(final String world, final PlotSettings settings) { + PlotWorld plotworld = PlotSquared.getPlotWorld(world); + HashMap map; + if (plotworld == null) { + map = new HashMap<>(); } - return plotflags; + else { + map = plotworld.DEFAULT_FLAGS; + } + map.putAll(settings.flags); + return map.values(); } - public static boolean removePlotFlag(final Plot plot, final String flag) { - final Flag hasFlag = getPlotFlag(plot, flag); - if (hasFlag != null) { - final Flag flagObj = FlagManager.getPlotFlagAbs(plot, flag); - if (flagObj != null) { - final boolean result = EventUtil.manager.callFlagRemove(flagObj, plot); - if (!result) { - return false; - } - plot.settings.flags.remove(hasFlag); - DBFunc.setFlags(plot.world, plot, plot.settings.flags); - return true; - } + public static boolean removePlotFlag(final Plot plot, final String id) { + Flag flag = plot.settings.flags.remove(id); + if (flag == null) { + return false; } - return false; + final boolean result = EventUtil.manager.callFlagRemove(flag, plot); + if (!result) { + plot.settings.flags.put(id, flag); + return false; + } + DBFunc.setFlags(plot.world, plot, plot.settings.flags.values()); + return true; } - public static boolean removeClusterFlag(final PlotCluster cluster, final String flag) { - final Flag hasFlag = getSettingFlag(cluster.world, cluster.settings, flag); - if (hasFlag != null) { - final Flag flagObj = FlagManager.getSettingFlagAbs(cluster.settings, flag); - if (flagObj != null) { - //TODO cluster flag add event - cluster.settings.flags.remove(hasFlag); - DBFunc.setFlags(cluster, cluster.settings.flags); - return true; - } + public static boolean removeClusterFlag(final PlotCluster cluster, final String id) { + Flag flag = cluster.settings.flags.remove(id); + if (flag == null) { + return false; } - return false; + final boolean result = EventUtil.manager.callFlagRemove(flag, cluster); + if (!result) { + cluster.settings.flags.put(id, flag); + return false; + } + DBFunc.setFlags(cluster, cluster.settings.flags.values()); + return true; } public static void setPlotFlags(final Plot plot, final Set flags) { - if (flags == null) { - plot.settings.flags = new HashSet<>(); - DBFunc.setFlags(plot.world, plot, plot.settings.flags); + if (flags != null && flags.size() != 0) { + plot.settings.flags.clear(); + for (Flag flag : flags) { + plot.settings.flags.put(flag.getKey(), flag); + } + } + else if (plot.settings.flags.size() == 0) { return; } - plot.settings.flags = flags; - DBFunc.setFlags(plot.world, plot, plot.settings.flags); + else { + plot.settings.flags.clear(); + } + DBFunc.setFlags(plot.world, plot, plot.settings.flags.values()); } public static void setClusterFlags(final PlotCluster cluster, final Set flags) { - if (flags == null) { - cluster.settings.flags = new HashSet<>(); - DBFunc.setFlags(cluster, cluster.settings.flags); + if (flags != null && flags.size() != 0) { + cluster.settings.flags.clear(); + for (Flag flag : flags) { + cluster.settings.flags.put(flag.getKey(), flag); + } + } + else if (cluster.settings.flags.size() == 0) { return; } - cluster.settings.flags = flags; - DBFunc.setFlags(cluster, cluster.settings.flags); + else { + cluster.settings.flags.clear(); + } + DBFunc.setFlags(cluster, cluster.settings.flags.values()); } public static Flag[] removeFlag(final Flag[] flags, final String r) { @@ -307,7 +295,7 @@ public class FlagManager { * * @return List (AbstractFlag) */ - public static List getFlags() { + public static HashSet getFlags() { return flags; } @@ -371,22 +359,24 @@ public class FlagManager { return flags.remove(flag); } - public static Flag[] parseFlags(final List flagstrings) { - final Flag[] flags = new Flag[flagstrings.size()]; - for (int i = 0; i < flagstrings.size(); i++) { + public static HashMap parseFlags(final List flagstrings) { + HashMap map = new HashMap(); + for (String key : flagstrings) { final String[] split; - if (flagstrings.get(i).contains(";")) { - split = flagstrings.get(i).split(";"); + if (key.contains(";")) { + split = key.split(";"); } else { - split = flagstrings.get(i).split(":"); + split = key.split(":"); } + Flag flag; if (split.length == 1) { - flags[i] = new Flag(getFlag(split[0], true), ""); + flag = new Flag(getFlag(split[0], true), ""); } else { - flags[i] = new Flag(getFlag(split[0], true), split[1]); + flag = new Flag(getFlag(split[0], true), split[1]); } + map.put(flag.getKey(), flag); } - return flags; + return map; } } diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/listeners/PlayerEvents.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/listeners/PlayerEvents.java index ac3423d67..c7b5aa2e6 100644 --- a/PlotSquared/src/main/java/com/intellectualcrafters/plot/listeners/PlayerEvents.java +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/listeners/PlayerEvents.java @@ -123,13 +123,26 @@ public class PlayerEvents extends com.intellectualcrafters.plot.listeners.PlotLi if (plot == null) { return; } + + if (Settings.REDSTONE_DISABLER) { + if (UUIDHandler.getPlayer(plot.owner) == null) { + boolean disable = true; + for (UUID trusted : plot.trusted) { + if (UUIDHandler.getPlayer(trusted) != null) { + disable = false; + break; + } + } + if (disable) { + event.setNewCurrent(0); + return; + } + } + } Flag redstone = FlagManager.getPlotFlag(plot, "redstone"); if (redstone == null || (Boolean) redstone.getValue()) { return; } - if (!MainUtil.isPlotArea(plot)) { - return; - } switch (block.getType()) { case REDSTONE_LAMP_OFF: case REDSTONE_LAMP_ON: diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/object/Plot.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/object/Plot.java index a09a06c87..f419d3b91 100644 --- a/PlotSquared/src/main/java/com/intellectualcrafters/plot/object/Plot.java +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/object/Plot.java @@ -21,6 +21,8 @@ package com.intellectualcrafters.plot.object; import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.Set; import java.util.UUID; @@ -96,7 +98,7 @@ public class Plot implements Cloneable { this.members = new ArrayList<>(); this.settings.setAlias(""); this.delete = false; - this.settings.flags = new HashSet(); + this.settings.flags = new HashMap<>(); this.world = world; } @@ -109,7 +111,7 @@ public class Plot implements Cloneable { * @param denied * @param merged */ - public Plot(final PlotId id, final UUID owner, final ArrayList trusted, final ArrayList members, final ArrayList denied, final String alias, final BlockLoc position, final Set flags, final String world, final boolean[] merged) { + public Plot(final PlotId id, final UUID owner, final ArrayList trusted, final ArrayList members, final ArrayList denied, final String alias, final BlockLoc position, final Collection flags, final String world, final boolean[] merged) { this.id = id; this.settings = new PlotSettings(this); this.owner = owner; @@ -121,10 +123,11 @@ public class Plot implements Cloneable { this.settings.setPosition(position); this.settings.setMerged(merged); this.delete = false; + this.settings.flags = new HashMap<>(); if (flags != null) { - this.settings.flags = flags; - } else { - this.settings.flags = new HashSet(); + for (Flag flag : flags) { + this.settings.flags.put(flag.getKey(), flag); + } } this.world = world; } @@ -188,7 +191,7 @@ public class Plot implements Cloneable { public Object clone() throws CloneNotSupportedException { final Plot p = (Plot) super.clone(); if (!p.equals(this) || (p != this)) { - return new Plot(this.id, this.owner, this.trusted, this.members, this.denied, this.settings.getAlias(), this.settings.getPosition(), this.settings.flags, this.world, this.settings.getMerged()); + return new Plot(this.id, this.owner, this.trusted, this.members, this.denied, this.settings.getAlias(), this.settings.getPosition(), this.settings.flags.values(), this.world, this.settings.getMerged()); } return p; } diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/object/PlotSettings.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/object/PlotSettings.java index b908e7812..5b3df6d1a 100644 --- a/PlotSquared/src/main/java/com/intellectualcrafters/plot/object/PlotSettings.java +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/object/PlotSettings.java @@ -21,6 +21,7 @@ package com.intellectualcrafters.plot.object; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Set; @@ -57,7 +58,7 @@ public class PlotSettings { /** * Flags */ - public Set flags; + public HashMap flags; /** * Home Position */ diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/object/PlotWorld.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/object/PlotWorld.java index 5aeb9d487..b9541c5e2 100644 --- a/PlotSquared/src/main/java/com/intellectualcrafters/plot/object/PlotWorld.java +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/object/PlotWorld.java @@ -79,7 +79,7 @@ public abstract class PlotWorld { public boolean SCHEMATIC_ON_CLAIM; public String SCHEMATIC_FILE; public List SCHEMATICS; - public Flag[] DEFAULT_FLAGS; + public HashMap DEFAULT_FLAGS; public boolean USE_ECONOMY; public double PLOT_PRICE; public double MERGE_PRICE; @@ -187,7 +187,7 @@ public abstract class PlotWorld { } catch (final Exception e) { e.printStackTrace(); PlotSquared.log("&cInvalid default flags for " + this.worldname + ": " + StringUtils.join(flags, ",")); - this.DEFAULT_FLAGS = new Flag[] {}; + this.DEFAULT_FLAGS = new HashMap<>(); } this.PVP = config.getBoolean("event.pvp"); this.PVE = config.getBoolean("event.pve"); diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/EventUtil.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/EventUtil.java index 7e3a502c8..c9abf104a 100644 --- a/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/EventUtil.java +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/EventUtil.java @@ -7,6 +7,7 @@ import com.intellectualcrafters.plot.PlotSquared; import com.intellectualcrafters.plot.flag.Flag; import com.intellectualcrafters.plot.object.Location; import com.intellectualcrafters.plot.object.Plot; +import com.intellectualcrafters.plot.object.PlotCluster; import com.intellectualcrafters.plot.object.PlotId; import com.intellectualcrafters.plot.object.PlotPlayer; @@ -35,6 +36,8 @@ public abstract class EventUtil { public abstract boolean callFlagRemove(final Flag flag, final Plot plot); + public abstract boolean callFlagRemove(final Flag flag, final PlotCluster cluster); + public abstract boolean callMerge(final String world, final Plot plot, final ArrayList plots); public abstract boolean callUnlink(final String world, final ArrayList plots); diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/MainUtil.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/MainUtil.java index c94b5d2e6..7c1e92e3b 100644 --- a/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/MainUtil.java +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/MainUtil.java @@ -1193,7 +1193,7 @@ public class MainUtil { Plot plot = createPlotAbs(currentPlot.owner, getPlot(world, new PlotId(x, y))); if (currentPlot.settings.flags != null && currentPlot.settings.flags.size() > 0) { plot.settings.flags = currentPlot.settings.flags; - DBFunc.setFlags(world, plot, currentPlot.settings.flags); + DBFunc.setFlags(world, plot, currentPlot.settings.flags.values()); } if (currentPlot.settings.isMerged()) { plot.settings.setMerged(currentPlot.settings.getMerged()); diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/BukkitEventUtil.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/BukkitEventUtil.java index 66f7036ab..e3faed302 100644 --- a/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/BukkitEventUtil.java +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/BukkitEventUtil.java @@ -8,6 +8,7 @@ import org.bukkit.entity.Player; import org.bukkit.event.Cancellable; import org.bukkit.event.Event; +import com.intellectualcrafters.plot.events.ClusterFlagRemoveEvent; import com.intellectualcrafters.plot.events.PlayerClaimPlotEvent; import com.intellectualcrafters.plot.events.PlayerEnterPlotEvent; import com.intellectualcrafters.plot.events.PlayerLeavePlotEvent; @@ -25,6 +26,7 @@ import com.intellectualcrafters.plot.flag.Flag; import com.intellectualcrafters.plot.object.BukkitPlayer; import com.intellectualcrafters.plot.object.Location; import com.intellectualcrafters.plot.object.Plot; +import com.intellectualcrafters.plot.object.PlotCluster; import com.intellectualcrafters.plot.object.PlotId; import com.intellectualcrafters.plot.object.PlotPlayer; import com.intellectualcrafters.plot.util.EventUtil; @@ -107,5 +109,10 @@ public class BukkitEventUtil extends EventUtil { public void callMember(PlotPlayer initiator, Plot plot, UUID player, boolean added) { callEvent(new PlayerPlotTrustedEvent(getPlayer(initiator), plot, player, added)); } + + @Override + public boolean callFlagRemove(Flag flag, PlotCluster cluster) { + return callEvent(new ClusterFlagRemoveEvent(flag, cluster)); + } } diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/SendChunk.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/SendChunk.java index 6c4d8a1af..7f88e052b 100644 --- a/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/SendChunk.java +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/SendChunk.java @@ -61,6 +61,9 @@ public class SendChunk { int diffx, diffz; final int view = Bukkit.getServer().getViewDistance() << 4; for (final Chunk chunk : chunks) { + if (!chunk.isLoaded()) { + continue; + } boolean unload = true; final Object c = methodGetHandle.of(chunk).call(); final Object w = world.of(c).get(); diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/chat/ArrayWrapper.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/chat/ArrayWrapper.java new file mode 100644 index 000000000..bac0473f7 --- /dev/null +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/chat/ArrayWrapper.java @@ -0,0 +1,106 @@ +package com.intellectualcrafters.plot.util.bukkit.chat; + +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 { + + /** + * Creates an array wrapper with some elements. + * @param elements The elements of the array. + */ + public ArrayWrapper(E... elements){ + setArray(elements); + } + + private E[] _array; + + /** + * Retrieves a reference to the wrapped array instance. + * @return The array wrapped by this instance. + */ + public E[] getArray(){ + return _array; + } + + /** + * Set this wrapper to wrap a new array instance. + * @param array The new wrapped array. + */ + public void setArray(E[] array){ + Validate.notNull(array, "The array must not be null."); + _array = array; + } + + /** + * Determines if this object has a value equivalent to another object. + * @see Arrays#equals(Object[], Object[]) + */ + @SuppressWarnings("rawtypes") + @Override + public boolean equals(Object other) + { + if (!(other instanceof ArrayWrapper)) + { + return false; + } + return Arrays.equals(_array, ((ArrayWrapper)other)._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); + } + + /** + * 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(Iterable list, Class c) { + int size = -1; + if(list instanceof Collection){ + @SuppressWarnings("rawtypes") + Collection coll = (Collection)list; + size = coll.size(); + } + + + if(size < 0){ + size = 0; + // Ugly hack: Count it ourselves + for(@SuppressWarnings("unused") T element : list){ + size++; + } + } + + T[] result = (T[]) Array.newInstance(c, size); + int i = 0; + for(T element : list){ // Assumes iteration order is consistent + result[i++] = element; // Assign array element at index THEN increment counter + } + return result; + } +} diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/chat/FancyMessage.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/chat/FancyMessage.java new file mode 100644 index 000000000..64fecb47b --- /dev/null +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/chat/FancyMessage.java @@ -0,0 +1,845 @@ +package com.intellectualcrafters.plot.util.bukkit.chat; + +import static com.intellectualcrafters.plot.util.bukkit.chat.TextualComponent.rawText; + +import java.io.IOException; +import java.io.StringWriter; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +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.Statistic.Type; +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 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; + +/** + * 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 FancyMessage implements JsonRepresentedObject, Cloneable, Iterable, ConfigurationSerializable { + + static{ + ConfigurationSerialization.registerClass(FancyMessage.class); + } + + private List messageParts; + private String jsonString; + private boolean dirty; + + private static Constructor nmsPacketPlayOutChatConstructor; + + @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; + } + + /** + * Creates a JSON message with text. + * @param firstPartText The existing text in the message. + */ + 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; + + if(nmsPacketPlayOutChatConstructor == null){ + try { + nmsPacketPlayOutChatConstructor = Reflection.getNMSClass("PacketPlayOutChat").getDeclaredConstructor(Reflection.getNMSClass("IChatBaseComponent")); + nmsPacketPlayOutChatConstructor.setAccessible(true); + } catch (NoSuchMethodException e) { + Bukkit.getLogger().log(Level.SEVERE, "Could not find Minecraft method or constructor.", e); + } catch (SecurityException e) { + Bukkit.getLogger().log(Level.WARNING, "Could not access constructor.", e); + } + } + } + + /** + * Creates a JSON message without text. + */ + public FancyMessage() { + this((TextualComponent)null); + } + + /** + * Sets the text of the current editing component to a value. + * @param text The new text of the current editing component. + * @return This builder instance. + */ + public FancyMessage text(String text) { + MessagePart latest = latest(); + latest.text = rawText(text); + dirty = true; + return this; + } + + /** + * Sets the text of the current editing component to a value. + * @param text The new text of the current editing component. + * @return This builder instance. + */ + public FancyMessage text(TextualComponent text) { + MessagePart latest = latest(); + latest.text = text; + dirty = true; + return this; + } + + /** + * Sets the color of the current editing component to a value. + * @param color The new color of the current editing component. + * @return This builder instance. + * @exception IllegalArgumentException If the specified {@code ChatColor} enumeration value is not a color (but a format value). + */ + 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; + } + + /** + * Sets the stylization of the current editing component. + * @param styles The array of styles to apply to the editing component. + * @return This builder instance. + * @exception IllegalArgumentException If any of the enumeration values in the array do not represent formatters. + */ + 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; + } + + /** + * 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 This builder instance. + */ + public FancyMessage file(final String path) { + onClick("open_file", path); + return this; + } + + /** + * 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 This builder instance. + */ + public FancyMessage link(final String url) { + onClick("open_url", url); + 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 This builder instance. + */ + public FancyMessage suggest(final String command) { + onClick("suggest_command", command); + return this; + } + + /** + * 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 This builder instance. + */ + public FancyMessage insert(final String command) { + latest().insertionData = command; + dirty = true; + return this; + } + + /** + * Set the behavior of the current editing component to instruct the client to send the specified string to the server as a chat message when the currently edited part of the {@code FancyMessage} is clicked. + * The client will immediately send the command to the server to be executed when the editing component is clicked. + * @param command The text to display in the chat bar of the client. + * @return This builder instance. + */ + public FancyMessage command(final String command) { + onClick("run_command", command); + return this; + } + + /** + * Set the behavior of the current editing component to display information about an achievement 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 name The name of the achievement to display, excluding the "achievement." prefix. + * @return This builder instance. + */ + public FancyMessage achievementTooltip(final String name) { + onHover("show_achievement", new JsonString("achievement." + name)); + return this; + } + + /** + * Set the behavior of the current editing component to display information about an achievement 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 achievement to display. + * @return This builder instance. + */ + public FancyMessage achievementTooltip(final Achievement which) { + try { + Object achievement = Reflection.getMethod(Reflection.getOBCClass("CraftStatistic"), "getNMSAchievement", Achievement.class).invoke(null, which); + return achievementTooltip((String) Reflection.getField(Reflection.getNMSClass("Achievement"), "name").get(achievement)); + } catch (IllegalAccessException e) { + Bukkit.getLogger().log(Level.WARNING, "Could not access method.", e); + return this; + } catch (IllegalArgumentException e) { + Bukkit.getLogger().log(Level.WARNING, "Argument could not be passed.", e); + return this; + } catch (InvocationTargetException e) { + Bukkit.getLogger().log(Level.WARNING, "A error has occured durring invoking of method.", e); + return this; + } + } + + /** + * 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 This builder instance. + * @exception IllegalArgumentException If the statistic requires a parameter which was not supplied. + */ + public FancyMessage statisticTooltip(final Statistic which) { + Type type = which.getType(); + if (type != Type.UNTYPED) { + throw new IllegalArgumentException("That statistic requires an additional " + type + " parameter!"); + } + try { + Object statistic = Reflection.getMethod(Reflection.getOBCClass("CraftStatistic"), "getNMSStatistic", Statistic.class).invoke(null, which); + return achievementTooltip((String) Reflection.getField(Reflection.getNMSClass("Statistic"), "name").get(statistic)); + } catch (IllegalAccessException e) { + Bukkit.getLogger().log(Level.WARNING, "Could not access method.", e); + return this; + } catch (IllegalArgumentException e) { + Bukkit.getLogger().log(Level.WARNING, "Argument could not be passed.", e); + return this; + } catch (InvocationTargetException e) { + Bukkit.getLogger().log(Level.WARNING, "A error has occured durring invoking of method.", e); + return this; + } + } + + /** + * 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 This builder instance. + * @exception IllegalArgumentException If the statistic requires a parameter which was not supplied, or was supplied a parameter that was not required. + */ + public FancyMessage statisticTooltip(final Statistic which, Material item) { + 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 { + Object statistic = Reflection.getMethod(Reflection.getOBCClass("CraftStatistic"), "getMaterialStatistic", Statistic.class, Material.class).invoke(null, which, item); + return achievementTooltip((String) Reflection.getField(Reflection.getNMSClass("Statistic"), "name").get(statistic)); + } catch (IllegalAccessException e) { + Bukkit.getLogger().log(Level.WARNING, "Could not access method.", e); + return this; + } catch (IllegalArgumentException e) { + Bukkit.getLogger().log(Level.WARNING, "Argument could not be passed.", e); + return this; + } catch (InvocationTargetException e) { + Bukkit.getLogger().log(Level.WARNING, "A error has occured durring invoking of method.", e); + return this; + } + } + + /** + * 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 This builder instance. + * @exception IllegalArgumentException If the statistic requires a parameter which was not supplied, or was supplied a parameter that was not required. + */ + public FancyMessage statisticTooltip(final Statistic which, EntityType entity) { + 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 { + Object statistic = Reflection.getMethod(Reflection.getOBCClass("CraftStatistic"), "getEntityStatistic", Statistic.class, EntityType.class).invoke(null, which, entity); + return achievementTooltip((String) Reflection.getField(Reflection.getNMSClass("Statistic"), "name").get(statistic)); + } catch (IllegalAccessException e) { + Bukkit.getLogger().log(Level.WARNING, "Could not access method.", e); + return this; + } catch (IllegalArgumentException e) { + Bukkit.getLogger().log(Level.WARNING, "Argument could not be passed.", e); + return this; + } catch (InvocationTargetException e) { + Bukkit.getLogger().log(Level.WARNING, "A error has occured durring invoking of method.", e); + 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 itemJSON A string representing the JSON-serialized NBT data tag of an {@link ItemStack}. + * @return This builder instance. + */ + public FancyMessage itemTooltip(final String itemJSON) { + onHover("show_item", new JsonString(itemJSON)); // Seems a bit hacky, considering we have a JSON object as a parameter + 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 This builder instance. + */ + public FancyMessage itemTooltip(final ItemStack itemStack) { + try { + Object nmsItem = Reflection.getMethod(Reflection.getOBCClass("inventory.CraftItemStack"), "asNMSCopy", ItemStack.class).invoke(null, itemStack); + return itemTooltip(Reflection.getMethod(Reflection.getNMSClass("ItemStack"), "save", Reflection.getNMSClass("NBTTagCompound")).invoke(nmsItem, Reflection.getNMSClass("NBTTagCompound").newInstance()).toString()); + } catch (Exception e) { + e.printStackTrace(); + return this; + } + } + + + /** + * Set the behavior of the current editing component to display raw 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 text, which supports newlines, which will be displayed to the client upon hovering. + * @return This builder instance. + */ + public FancyMessage tooltip(final String text) { + onHover("show_text", new JsonString(text)); + return this; + } + + /** + * Set the behavior of the current editing component to display raw 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 This builder instance. + */ + public FancyMessage tooltip(final Iterable lines) { + tooltip(ArrayWrapper.toArray(lines, String.class)); + return this; + } + + /** + * Set the behavior of the current editing component to display raw 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. + * @return This builder instance. + */ + 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; + } + + /** + * 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 This builder instance. + */ + 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; + } + + /** + * 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 This builder instance. + */ + public FancyMessage formattedTooltip(FancyMessage... lines){ + if(lines.length < 1){ + onHover(null, null); // Clear tooltip + return this; + } + + FancyMessage result = new FancyMessage(); + 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(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 + } + + /** + * 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 This builder instance. + */ + public FancyMessage formattedTooltip(final Iterable lines){ + return formattedTooltip(ArrayWrapper.toArray(lines, 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 This builder instance. + */ + public FancyMessage translationReplacements(final String... replacements){ + for(String str : replacements){ + latest().translationReplacements.add(new JsonString(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 This builder instance. + */ /* ------------ + public FancyMessage translationReplacements(final Iterable replacements){ + for(CharSequence str : replacements){ + latest().translationReplacements.add(new JsonString(str)); + } + + 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 This builder instance. + */ + public FancyMessage translationReplacements(final FancyMessage... replacements){ + for(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 This builder instance. + */ + public FancyMessage translationReplacements(final Iterable replacements){ + return translationReplacements(ArrayWrapper.toArray(replacements, FancyMessage.class)); + } + + /** + * Terminate construction of the current editing component, and begin construction of a new message component. + * After a successful call to this method, all setter methods will refer to a new message component, created as a result of the call to this method. + * @param text The text which will populate the new message component. + * @return This builder instance. + */ + public FancyMessage then(final String text) { + return then(rawText(text)); + } + + /** + * Terminate construction of the current editing component, and begin construction of a new message component. + * After a successful call to this method, all setter methods will refer to a new message component, created as a result of the call to this method. + * @param text The text which will populate the new message component. + * @return This builder instance. + */ + 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; + } + + /** + * Terminate construction of the current editing component, and begin construction of a new message component. + * After a successful call to this method, all setter methods will refer to a new message component, created as a result of the call to this method. + * @return This builder instance. + */ + 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(); + } + } + + /** + * Serialize this fancy message, converting it into syntactically-valid JSON using a {@link JsonWriter}. + * This JSON should be compatible with vanilla formatter commands such as {@code /tellraw}. + * @return The JSON string representing this object. + */ + 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; + } + + /** + * 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(Player player){ + send(player, toJSONString()); + } + + private void send(CommandSender sender, String jsonString){ + if (!(sender instanceof Player)){ + sender.sendMessage(toOldMessageFormat()); + return; + } + Player player = (Player) sender; + try { + Object handle = Reflection.getHandle(player); + Object connection = Reflection.getField(handle.getClass(), "playerConnection").get(handle); + Reflection.getMethod(connection.getClass(), "sendPacket", Reflection.getNMSClass("Packet")).invoke(connection, createChatPacket(jsonString)); + } catch (IllegalArgumentException e) { + Bukkit.getLogger().log(Level.WARNING, "Argument could not be passed.", e); + } catch (IllegalAccessException e) { + Bukkit.getLogger().log(Level.WARNING, "Could not access method.", e); + } catch (InstantiationException e) { + Bukkit.getLogger().log(Level.WARNING, "Underlying class is abstract.", e); + } catch (InvocationTargetException e) { + Bukkit.getLogger().log(Level.WARNING, "A error has occured durring invoking of method.", e); + } catch (NoSuchMethodException e) { + Bukkit.getLogger().log(Level.WARNING, "Could not find method.", e); + } catch (ClassNotFoundException e) { + Bukkit.getLogger().log(Level.WARNING, "Could not find class.", e); + } + } + + // The ChatSerializer's instance of Gson + private static Object nmsChatSerializerGsonInstance; + private static Method fromJsonMethod; + + private Object createChatPacket(String json) throws IllegalArgumentException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException, ClassNotFoundException { + if(nmsChatSerializerGsonInstance == null){ + // Find the field and its value, completely bypassing obfuscation + Class chatSerializerClazz; + + String version = Reflection.getVersion(); + double majorVersion = Double.parseDouble(version.replace('_', '.').substring(1, 4)); + int lesserVersion = Integer.parseInt(version.substring(6, 7)); + + if (majorVersion < 1.8 || (majorVersion == 1.8 && lesserVersion == 1)) { + chatSerializerClazz = Reflection.getNMSClass("ChatSerializer"); + } else { + chatSerializerClazz = Reflection.getNMSClass("IChatBaseComponent$ChatSerializer"); + } + + if (chatSerializerClazz == null) { + throw new ClassNotFoundException("Can't find the ChatSerializer class"); + } + + for (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; + } + } + } + + // 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 + Object serializedChatComponent = fromJsonMethod.invoke(nmsChatSerializerGsonInstance, json, Reflection.getNMSClass("IChatBaseComponent")); + + return nmsPacketPlayOutChatConstructor.newInstance(serializedChatComponent); + } + + /** + * 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(CommandSender sender) { + send(sender, toJSONString()); + } + + /** + * 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) { + String string = toJSONString(); + for (final CommandSender sender : senders) { + send(sender, string); + } + } + + /** + * Convert this message to a human-readable string with limited formatting. + * This method is used to send this message to clients without JSON formatting support. + *

+ * Serialization of this message by using this message will include (in this order for each message part): + *

    + *
  1. The color of each message part.
  2. + *
  3. The applicable stylizations for each message part.
  4. + *
  5. The core text of the message part.
  6. + *
+ * The primary omissions are tooltips and clickable actions. Consequently, this method should be used only as a last resort. + *

+ *

+ * Color and formatting can be removed from the returned string by using {@link ChatColor#stripColor(String)}.

+ * @return A human-readable string representing limited formatting in addition to the core text of this message. + */ + 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; + } + + // Doc copied from interface + public Map serialize() { + HashMap map = new HashMap(); + map.put("messageParts", messageParts); +// map.put("JSON", toJSONString()); + return map; + } + + /** + * 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(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; + } + + /** + * Internally called method. Not for API consumption. + */ + public Iterator iterator() { + return messageParts.iterator(); + } + + private static JsonParser _stringParser = new JsonParser(); + + /** + * 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(String json){ + JsonObject serialized = _stringParser.parse(json).getAsJsonObject(); + JsonArray extra = serialized.getAsJsonArray("extra"); // Get the extra component + 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()){ + // Deserialize text + if(TextualComponent.isTextKey(entry.getKey())){ + // The map mimics the YAML serialization, which has a "key" field and one or more "value" fields + 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(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()){ + // Assume string + component.hoverActionData = new JsonString(object.get("value").getAsString()); + }else{ + // Assume composite type + // The only composite type we currently store is another FancyMessage + // Therefore, recursion time! + 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{ + // Only composite type stored in this array is - again - FancyMessages + // Recurse within this function to parse this as a translation replacement + component.translationReplacements.add(deserialize(object.toString())); + } + } + } + } + returnVal.messageParts.add(component); + } + return returnVal; + } +} diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/chat/JsonRepresentedObject.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/chat/JsonRepresentedObject.java new file mode 100644 index 000000000..b26d2294f --- /dev/null +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/chat/JsonRepresentedObject.java @@ -0,0 +1,19 @@ +package com.intellectualcrafters.plot.util.bukkit.chat; + +import java.io.IOException; + +import com.google.gson.stream.JsonWriter; + +/** + * Represents an object that can be serialized to a JSON writer instance. + */ +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/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/chat/JsonString.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/chat/JsonString.java new file mode 100644 index 000000000..4453aeb72 --- /dev/null +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/chat/JsonString.java @@ -0,0 +1,47 @@ +package com.intellectualcrafters.plot.util.bukkit.chat; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.bukkit.configuration.serialization.ConfigurationSerializable; + +import com.google.gson.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. + */ +final class JsonString implements JsonRepresentedObject, ConfigurationSerializable { + + private String _value; + + public JsonString(CharSequence value){ + _value = value == null ? null : value.toString(); + } + + @Override + public void writeJson(JsonWriter writer) throws IOException { + writer.value(getValue()); + } + + public String getValue(){ + return _value; + } + + public Map serialize() { + HashMap theSingleValue = new HashMap(); + theSingleValue.put("stringValue", _value); + return theSingleValue; + } + + public static JsonString deserialize(Map map){ + return new JsonString(map.get("stringValue").toString()); + } + + @Override + public String toString(){ + return _value; + } +} diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/chat/MessagePart.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/chat/MessagePart.java new file mode 100644 index 000000000..7da75d714 --- /dev/null +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/chat/MessagePart.java @@ -0,0 +1,154 @@ +package com.intellectualcrafters.plot.util.bukkit.chat; + +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 com.google.gson.stream.JsonWriter; + +/** + * Internal class: Represents a component of a JSON-serializable {@link FancyMessage}. + */ +final class MessagePart implements JsonRepresentedObject, ConfigurationSerializable, Cloneable { + + ChatColor color = ChatColor.WHITE; + ArrayList styles = new ArrayList(); + String clickActionName = null, clickActionData = null, + hoverActionName = null; + JsonRepresentedObject hoverActionData = null; + TextualComponent text = null; + String insertionData = null; + ArrayList translationReplacements = new ArrayList(); + + MessagePart(final TextualComponent text){ + this.text = text; + } + + MessagePart() { + this.text = null; + } + + boolean hasText() { + return text != null; + } + + @Override + @SuppressWarnings("unchecked") + public MessagePart clone() throws CloneNotSupportedException{ + 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 FancyMessage){ + obj.hoverActionData = ((FancyMessage)hoverActionData).clone(); + } + obj.translationReplacements = (ArrayList)translationReplacements.clone(); + return obj; + + } + + static final BiMap stylesToNames; + + static{ + 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(); + } + + public void writeJson(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(JsonRepresentedObject obj : translationReplacements){ + obj.writeJson(json); + } + json.endArray(); + } + json.endObject(); + } catch(IOException e){ + Bukkit.getLogger().log(Level.WARNING, "A problem occured during writing of JSON string", e); + } + } + + public Map serialize() { + 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; + } + + @SuppressWarnings("unchecked") + public static MessagePart deserialize(Map serialized){ + 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; + } + + static{ + ConfigurationSerialization.registerClass(MessagePart.class); + } + +} diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/chat/Reflection.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/chat/Reflection.java new file mode 100644 index 000000000..652c3e93c --- /dev/null +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/chat/Reflection.java @@ -0,0 +1,213 @@ +package com.intellectualcrafters.plot.util.bukkit.chat; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashMap; +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 String _versionString; + + private Reflection(){ + + } + + /** + * 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; + } + String name = Bukkit.getServer().getClass().getPackage().getName(); + _versionString = name.substring(name.lastIndexOf('.') + 1) + "."; + } + + return _versionString; + } + + /** + * 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>(); + + /** + * 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(String className) { + if(_loadedNMSClasses.containsKey(className)){ + return _loadedNMSClasses.get(className); + } + + String fullName = "net.minecraft.server." + getVersion() + className; + Class clazz = null; + try { + clazz = Class.forName(fullName); + } catch (Exception e) { + e.printStackTrace(); + _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(String className) { + if(_loadedOBCClasses.containsKey(className)){ + return _loadedOBCClasses.get(className); + } + + String fullName = "org.bukkit.craftbukkit." + getVersion() + className; + Class clazz = null; + try { + clazz = Class.forName(fullName); + } catch (Exception e) { + e.printStackTrace(); + _loadedOBCClasses.put(className, null); + return null; + } + _loadedOBCClasses.put(className, clazz); + return clazz; + } + + /** + * 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(Object obj) { + try { + return getMethod(obj.getClass(), "getHandle").invoke(obj); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + private static final Map, Map> _loadedFields = new HashMap, Map>(); + + /** + * 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. + * @see Class#getDeclaredField(String) + */ + public synchronized static Field getField(Class clazz, String name) { + 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 { + Field field = clazz.getDeclaredField(name); + field.setAccessible(true); + loaded.put(name, field); + return field; + } catch (Exception e) { + // Error loading + e.printStackTrace(); + // Cache field as not existing + loaded.put(name, null); + return null; + } + } + + /** + * 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>>>(); + + /** + * 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(Class clazz, String name, + Class... args) { + if(!_loadedMethods.containsKey(clazz)){ + _loadedMethods.put(clazz, new HashMap>, Method>>()); + } + + Map>, Method>> loadedMethodNames = _loadedMethods.get(clazz); + if(!loadedMethodNames.containsKey(name)){ + loadedMethodNames.put(name, new HashMap>, Method>()); + } + + Map>, Method> loadedSignatures = loadedMethodNames.get(name); + ArrayWrapper> wrappedArg = new ArrayWrapper>(args); + if(loadedSignatures.containsKey(wrappedArg)){ + return loadedSignatures.get(wrappedArg); + } + + for (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; + } + +} diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/chat/TextualComponent.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/chat/TextualComponent.java new file mode 100644 index 000000000..2be0c8d1d --- /dev/null +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/chat/TextualComponent.java @@ -0,0 +1,292 @@ +package com.intellectualcrafters.plot.util.bukkit.chat; + +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 com.google.gson.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); + } + + @Override + public String toString() { + return getReadableString(); + } + + /** + * @return The JSON key used to represent text components of this type. + */ + public abstract String getKey(); + + /** + * @return A readable String + */ + public abstract String getReadableString(); + + /** + * 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; + + /** + * 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; + + static TextualComponent deserialize(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") /* It contains keys that START WITH value */){ + // Complex JSON object + return ComplexTextTypeComponent.deserialize(map); + } + + return null; + } + + static boolean isTextKey(String key){ + return key.equals("translate") || key.equals("text") || key.equals("score") || key.equals("selector"); + } + + static boolean isTranslatableText(TextualComponent component){ + return component instanceof ComplexTextTypeComponent && ((ComplexTextTypeComponent)component).getKey().equals("translate"); + } + + /** + * 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 { + + public ArbitraryTextTypeComponent(String key, String value){ + setKey(key); + setValue(value); + } + + @Override + public String getKey() { + return _key; + } + + public void setKey(String key) { + Preconditions.checkArgument(key != null && !key.isEmpty(), "The key must be specified."); + _key = key; + } + + public String getValue() { + return _value; + } + + public void setValue(String value) { + Preconditions.checkArgument(value != null, "The value must be specified."); + _value = value; + } + + private String _key; + private String _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 ArbitraryTextTypeComponent(getKey(), getValue()); + } + + @Override + public void writeJson(JsonWriter writer) throws IOException { + writer.name(getKey()).value(getValue()); + } + + @SuppressWarnings("serial") + public Map serialize() { + return new HashMap(){{ + put("key", getKey()); + put("value", getValue()); + }}; + } + + public static ArbitraryTextTypeComponent deserialize(Map map){ + return new ArbitraryTextTypeComponent(map.get("key").toString(), map.get("value").toString()); + } + + @Override + public String getReadableString() { + return 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{ + + public ComplexTextTypeComponent(String key, Map values){ + setKey(key); + setValue(values); + } + + @Override + public String getKey() { + return _key; + } + + public void setKey(String key) { + Preconditions.checkArgument(key != null && !key.isEmpty(), "The key must be specified."); + _key = key; + } + + public Map getValue() { + return _value; + } + + public void setValue(Map value) { + Preconditions.checkArgument(value != null, "The value must be specified."); + _value = value; + } + + private String _key; + private Map _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 void writeJson(JsonWriter writer) throws IOException { + writer.name(getKey()); + writer.beginObject(); + for(Map.Entry jsonPair : _value.entrySet()){ + writer.name(jsonPair.getKey()).value(jsonPair.getValue()); + } + writer.endObject(); + } + + @SuppressWarnings("serial") + public Map serialize() { + return new java.util.HashMap(){{ + put("key", getKey()); + for(Map.Entry valEntry : getValue().entrySet()){ + put("value." + valEntry.getKey(), valEntry.getValue()); + } + }}; + } + + public static ComplexTextTypeComponent deserialize(Map map){ + String key = null; + Map value = new HashMap(); + for(Map.Entry valEntry : map.entrySet()){ + if(valEntry.getKey().equals("key")){ + key = (String) valEntry.getValue(); + }else if(valEntry.getKey().startsWith("value.")){ + value.put(((String) valEntry.getKey()).substring(6) /* Strips out the value prefix */, valEntry.getValue().toString()); + } + } + return new ComplexTextTypeComponent(key, value); + } + + @Override + public String getReadableString() { + return getKey(); + } + } + + /** + * 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(String textValue){ + return new ArbitraryTextTypeComponent("text", textValue); + } + + + /** + * 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(String translateKey){ + return new ArbitraryTextTypeComponent("translate", translateKey); + } + + private static void throwUnsupportedSnapshot(){ + throw new UnsupportedOperationException("This feature is only supported in snapshot releases."); + } + + /** + * 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(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(String playerName, 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 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(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); + } +}