diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/BukkitMain.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/BukkitMain.java index 14db2da11..35222eeaa 100644 --- a/PlotSquared/src/main/java/com/intellectualcrafters/plot/BukkitMain.java +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/BukkitMain.java @@ -524,7 +524,6 @@ public class BukkitMain extends JavaPlugin implements Listener, IPlotMain { FlagManager.removeFlag(FlagManager.getFlag("titles")); } else { AbstractTitle.TITLE_CLASS = new DefaultTitle(); - if (UUIDHandler.uuidWrapper instanceof DefaultUUIDWrapper) { Settings.TWIN_MODE_UUID = true; } diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/PlotSquared.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/PlotSquared.java index da74d0d26..f86e96ee7 100644 --- a/PlotSquared/src/main/java/com/intellectualcrafters/plot/PlotSquared.java +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/PlotSquared.java @@ -718,9 +718,7 @@ public class PlotSquared { for (final String flag : intFlags) { FlagManager.addFlag(new AbstractFlag(flag, new FlagValue.UnsignedIntegerValue())); } - if (Settings.PHYSICS_LISTENER) { - FlagManager.addFlag(new AbstractFlag("disable-physics", new FlagValue.BooleanValue())); - } + FlagManager.addFlag(new AbstractFlag("disable-physics", new FlagValue.BooleanValue())); FlagManager.addFlag(new AbstractFlag("fly", new FlagValue.BooleanValue())); FlagManager.addFlag(new AbstractFlag("explosion", new FlagValue.BooleanValue())); FlagManager.addFlag(new AbstractFlag("hostile-interact", new FlagValue.BooleanValue())); @@ -806,7 +804,6 @@ public class PlotSquared { 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); // Clusters options.put("clusters.enabled", Settings.ENABLE_CLUSTERS); @@ -890,7 +887,6 @@ public class PlotSquared { 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"); // Clusters Settings.ENABLE_CLUSTERS = config.getBoolean("clusters.enabled"); @@ -902,7 +898,7 @@ public class PlotSquared { // UUID Settings.OFFLINE_MODE = config.getBoolean("UUID.offline"); - Settings.UUID_LOWERCASE = config.getBoolean("UUID.force-lowercase"); + Settings.UUID_LOWERCASE = Settings.OFFLINE_MODE && config.getBoolean("UUID.force-lowercase"); Settings.UUID_FROM_DISK = config.getBoolean("uuid.read-from-disk"); // Mob stuff diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/commands/NamedSubCommand.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/commands/NamedSubCommand.java new file mode 100644 index 000000000..3b859a83e --- /dev/null +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/commands/NamedSubCommand.java @@ -0,0 +1,14 @@ +package com.intellectualcrafters.plot.commands; + +public abstract class NamedSubCommand extends SubCommand { + + public NamedSubCommand(Command command, String description, String usage, CommandCategory category, boolean isPlayer) { + super(command, description, usage, category, isPlayer); + } + public NamedSubCommand(String cmd, String permission, String description, String usage, CommandCategory category, boolean isPlayer, String[] aliases) { + super(cmd, permission, description, usage, category, isPlayer, aliases); + } + public NamedSubCommand(String cmd, String permission, String description, String usage, String alias, CommandCategory category, boolean isPlayer) { + super(cmd, permission, description, usage, alias, category, isPlayer); + } +} diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/commands/Purge.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/commands/Purge.java index 45446fd33..e729212e1 100644 --- a/PlotSquared/src/main/java/com/intellectualcrafters/plot/commands/Purge.java +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/commands/Purge.java @@ -39,7 +39,7 @@ public class Purge extends SubCommand { public Purge() { super("purge", "plots.admin", "Purge all plots for a world", "purge", "", CommandCategory.DEBUG, false); } - + public PlotId getId(final String id) { try { final String[] split = id.split(";"); diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/commands/SubCommand.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/commands/SubCommand.java index 245aadc82..3ba556281 100644 --- a/PlotSquared/src/main/java/com/intellectualcrafters/plot/commands/SubCommand.java +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/commands/SubCommand.java @@ -150,7 +150,7 @@ public abstract class SubCommand { MainUtil.sendMessage(plr, c, args); return true; } - + /** * CommandCategory * 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 448e12579..83417de9f 100644 --- a/PlotSquared/src/main/java/com/intellectualcrafters/plot/commands/list.java +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/commands/list.java @@ -288,9 +288,9 @@ public class list extends SubCommand { } } i++; - if (Settings.FANCY_CHAT) { + if (player != null && Settings.FANCY_CHAT) { ChatColor color; - if (player == null) { + if (plot.owner == null) { color = ChatColor.GOLD; } else if (plot.isOwner(player.getUUID())) { @@ -397,7 +397,7 @@ public class list extends SubCommand { MainUtil.sendMessage(player, message); } } - if (Settings.FANCY_CHAT) { + if (player != null && Settings.FANCY_CHAT) { if (page < totalPages && page > 0) { // back | next new FancyMessage("") 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 c694cc37f..92432f5a3 100644 --- a/PlotSquared/src/main/java/com/intellectualcrafters/plot/config/Settings.java +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/config/Settings.java @@ -65,10 +65,6 @@ public class Settings { * Check for falling blocks when pistons extend? */ public static boolean PISTON_FALLING_BLOCK_CHECK = true; - /** - * Physics listener - */ - public static boolean PHYSICS_LISTENER = false; /** * Max auto claiming size */ 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 c7b5aa2e6..2dad7f28b 100644 --- a/PlotSquared/src/main/java/com/intellectualcrafters/plot/listeners/PlayerEvents.java +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/listeners/PlayerEvents.java @@ -123,7 +123,6 @@ 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; @@ -225,7 +224,7 @@ public class PlayerEvents extends com.intellectualcrafters.plot.listeners.PlotLi return; } } - if (Settings.PHYSICS_LISTENER && block.getType().hasGravity()) { + if (block.getType().hasGravity()) { Plot plot = MainUtil.getPlot(loc); if (plot == null) { return; @@ -698,7 +697,7 @@ public class PlayerEvents extends com.intellectualcrafters.plot.listeners.PlotLi if (MainUtil.isPlotRoad(loc)) { e.setCancelled(true); } - else if (Settings.PHYSICS_LISTENER) { + else { Plot plot = MainUtil.getPlot(loc); if (FlagManager.isPlotFlagTrue(plot, "disable-physics")) { e.setCancelled(true); @@ -912,9 +911,6 @@ public class PlayerEvents extends com.intellectualcrafters.plot.listeners.PlotLi @EventHandler(ignoreCancelled=true, priority=EventPriority.HIGHEST) public void onEntityFall(EntityChangeBlockEvent event) { - if (!Settings.PHYSICS_LISTENER) { - return; - } if (event.getEntityType() != EntityType.FALLING_BLOCK) { return; } @@ -1580,11 +1576,9 @@ public class PlayerEvents extends com.intellectualcrafters.plot.listeners.PlotLi return; } } - if (Settings.PHYSICS_LISTENER) { - if (FlagManager.isPlotFlagTrue(plot, "disable-physics")) { - Block block = event.getBlockPlaced(); - sendBlockChange(block.getLocation(), block.getType(), block.getData()); - } + if (FlagManager.isPlotFlagTrue(plot, "disable-physics")) { + Block block = event.getBlockPlaced(); + sendBlockChange(block.getLocation(), block.getType(), block.getData()); } return; } diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/NbtFactory.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/NbtFactory.java new file mode 100644 index 000000000..aebf425d6 --- /dev/null +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/NbtFactory.java @@ -0,0 +1,1003 @@ +package com.intellectualcrafters.plot.util; + +import java.io.BufferedInputStream; +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.AbstractList; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.Server; +import org.bukkit.inventory.ItemStack; + +import com.google.common.base.Splitter; +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import com.google.common.collect.Lists; +import com.google.common.collect.MapMaker; +import com.google.common.io.Closeables; +import com.google.common.io.Files; +import com.google.common.io.InputSupplier; +import com.google.common.io.OutputSupplier; +import com.google.common.primitives.Primitives; + +public class NbtFactory { + // Convert between NBT id and the equivalent class in java + private static final BiMap> NBT_CLASS = HashBiMap.create(); + private static final BiMap NBT_ENUM = HashBiMap.create(); + + /** + * Whether or not to enable stream compression. + * @author Kristian + */ + public enum StreamOptions { + NO_COMPRESSION, + GZIP_COMPRESSION, + } + + private enum NbtType { + TAG_END(0, Void.class), + TAG_BYTE(1, byte.class), + TAG_SHORT(2, short.class), + TAG_INT(3, int.class), + TAG_LONG(4, long.class), + TAG_FLOAT(5, float.class), + TAG_DOUBLE(6, double.class), + TAG_BYTE_ARRAY(7, byte[].class), + TAG_INT_ARRAY(11, int[].class), + TAG_STRING(8, String.class), + TAG_LIST(9, List.class), + TAG_COMPOUND(10, Map.class); + + // Unique NBT id + public final int id; + + private NbtType(int id, Class type) { + this.id = id; + NBT_CLASS.put(id, type); + NBT_ENUM.put(id, this); + } + + private String getFieldName() { + if (this == TAG_COMPOUND) + return "map"; + else if (this == TAG_LIST) + return "list"; + else + return "data"; + } + } + + // The NBT base class + private Class BASE_CLASS; + private Class COMPOUND_CLASS; + private Class STREAM_TOOLS; + private Class READ_LIMITER_CLASS; + private Method NBT_CREATE_TAG; + private Method NBT_GET_TYPE; + private Field NBT_LIST_TYPE; + private final Field[] DATA_FIELD = new Field[12]; + + // CraftItemStack + private Class CRAFT_STACK; + private Field CRAFT_HANDLE; + private Field STACK_TAG; + + // Loading/saving compounds + private LoadCompoundMethod LOAD_COMPOUND; + private Method SAVE_COMPOUND; + + // Shared instance + private static NbtFactory INSTANCE; + + /** + * Represents a root NBT compound. + *

+ * All changes to this map will be reflected in the underlying NBT compound. Values may only be one of the following: + *

+ *

+ * See also: + *

+ * @author Kristian + */ + public final class NbtCompound extends ConvertedMap { + private NbtCompound(Object handle) { + super(handle, getDataMap(handle)); + } + + // Simplifiying access to each value + public Byte getByte(String key, Byte defaultValue) { + return containsKey(key) ? (Byte)get(key) : defaultValue; + } + public Short getShort(String key, Short defaultValue) { + return containsKey(key) ? (Short)get(key) : defaultValue; + } + public Integer getInteger(String key, Integer defaultValue) { + return containsKey(key) ? (Integer)get(key) : defaultValue; + } + public Long getLong(String key, Long defaultValue) { + return containsKey(key) ? (Long)get(key) : defaultValue; + } + public Float getFloat(String key, Float defaultValue) { + return containsKey(key) ? (Float)get(key) : defaultValue; + } + public Double getDouble(String key, Double defaultValue) { + return containsKey(key) ? (Double)get(key) : defaultValue; + } + public String getString(String key, String defaultValue) { + return containsKey(key) ? (String)get(key) : defaultValue; + } + public byte[] getByteArray(String key, byte[] defaultValue) { + return containsKey(key) ? (byte[])get(key) : defaultValue; + } + public int[] getIntegerArray(String key, int[] defaultValue) { + return containsKey(key) ? (int[])get(key) : defaultValue; + } + + /** + * Retrieve the list by the given name. + * @param key - the name of the list. + * @param createNew - whether or not to create a new list if its missing. + * @return An existing list, a new list or NULL. + */ + public NbtList getList(String key, boolean createNew) { + NbtList list = (NbtList) get(key); + + if (list == null && createNew) + put(key, list = createList()); + return list; + } + + /** + * Retrieve the map by the given name. + * @param key - the name of the map. + * @param createNew - whether or not to create a new map if its missing. + * @return An existing map, a new map or NULL. + */ + public NbtCompound getMap(String key, boolean createNew) { + return getMap(Arrays.asList(key), createNew); + } + // Done + + /** + * Set the value of an entry at a given location. + *

+ * Every element of the path (except the end) are assumed to be compounds, and will + * be created if they are missing. + * @param path - the path to the entry. + * @param value - the new value of this entry. + * @return This compound, for chaining. + */ + public NbtCompound putPath(String path, Object value) { + List entries = getPathElements(path); + Map map = getMap(entries.subList(0, entries.size() - 1), true); + + map.put(entries.get(entries.size() - 1), value); + return this; + } + + /** + * Retrieve the value of a given entry in the tree. + *

+ * Every element of the path (except the end) are assumed to be compounds. The + * retrieval operation will be cancelled if any of them are missing. + * @param path - path to the entry. + * @return The value, or NULL if not found. + */ + @SuppressWarnings("unchecked") + public T getPath(String path) { + List entries = getPathElements(path); + NbtCompound map = getMap(entries.subList(0, entries.size() - 1), false); + + if (map != null) { + return (T) map.get(entries.get(entries.size() - 1)); + } + return null; + } + + /** + * Save the content of a NBT compound to a stream. + *

+ * Use {@link Files#newOutputStreamSupplier(java.io.File)} to provide a stream supplier to a file. + * @param stream - the output stream. + * @param option - whether or not to compress the output. + * @throws IOException If anything went wrong. + */ + public void saveTo(OutputSupplier stream, StreamOptions option) throws IOException { + saveStream(this, stream, option); + } + + /** + * Retrieve a map from a given path. + * @param path - path of compounds to look up. + * @param createNew - whether or not to create new compounds on the way. + * @return The map at this location. + */ + private NbtCompound getMap(Iterable path, boolean createNew) { + NbtCompound current = this; + + for (String entry : path) { + NbtCompound child = (NbtCompound) current.get(entry); + + if (child == null) { + if (!createNew) + return null; + current.put(entry, child = createCompound()); + } + current = child; + } + return current; + } + + /** + * Split the path into separate elements. + * @param path - the path to split. + * @return The elements. + */ + private List getPathElements(String path) { + return Lists.newArrayList(Splitter.on(".").omitEmptyStrings().split(path)); + } + } + + /** + * Represents a root NBT list. + * See also: + *

+ * @author Kristian + */ + public final class NbtList extends ConvertedList { + private NbtList(Object handle) { + super(handle, getDataList(handle)); + } + } + + /** + * Represents an object that provides a view of a native NMS class. + * @author Kristian + */ + public static interface Wrapper { + /** + * Retrieve the underlying native NBT tag. + * @return The underlying NBT. + */ + public Object getHandle(); + } + + /** + * Retrieve or construct a shared NBT factory. + * @return The factory. + */ + private static NbtFactory get() { + if (INSTANCE == null) + INSTANCE = new NbtFactory(); + return INSTANCE; + } + + /** + * Construct an instance of the NBT factory by deducing the class of NBTBase. + */ + private NbtFactory() { + if (BASE_CLASS == null) { + try { + // Keep in mind that I do use hard-coded field names - but it's okay as long as we're dealing + // with CraftBukkit or its derivatives. This does not work in MCPC+ however. + ClassLoader loader = NbtFactory.class.getClassLoader(); + + String packageName = getPackageName(); + Class offlinePlayer = loader.loadClass(packageName + ".CraftOfflinePlayer"); + + // Prepare NBT + COMPOUND_CLASS = getMethod(0, Modifier.STATIC, offlinePlayer, "getData").getReturnType(); + BASE_CLASS = COMPOUND_CLASS.getSuperclass(); + NBT_GET_TYPE = getMethod(0, Modifier.STATIC, BASE_CLASS, "getTypeId"); + NBT_CREATE_TAG = getMethod(Modifier.STATIC, 0, BASE_CLASS, "createTag", byte.class); + + // Prepare CraftItemStack + CRAFT_STACK = loader.loadClass(packageName + ".inventory.CraftItemStack"); + CRAFT_HANDLE = getField(null, CRAFT_STACK, "handle"); + STACK_TAG = getField(null, CRAFT_HANDLE.getType(), "tag"); + + // Loading/saving + String nmsPackage = BASE_CLASS.getPackage().getName(); + initializeNMS(loader, nmsPackage); + + LOAD_COMPOUND = READ_LIMITER_CLASS != null ? + new LoadMethodSkinUpdate(STREAM_TOOLS, READ_LIMITER_CLASS) : + new LoadMethodWorldUpdate(STREAM_TOOLS); + SAVE_COMPOUND = getMethod(Modifier.STATIC, 0, STREAM_TOOLS, null, BASE_CLASS, DataOutput.class); + + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Unable to find offline player.", e); + } + } + } + + private void initializeNMS(ClassLoader loader, String nmsPackage) { + try { + STREAM_TOOLS = loader.loadClass(nmsPackage + ".NBTCompressedStreamTools"); + READ_LIMITER_CLASS = loader.loadClass(nmsPackage + ".NBTReadLimiter"); + } catch (ClassNotFoundException e) { + // Ignore - we will detect this later + } + } + + private String getPackageName() { + Server server = Bukkit.getServer(); + String name = server != null ? server.getClass().getPackage().getName() : null; + + if (name != null && name.contains("craftbukkit")) { + return name; + } else { + // Fallback + return "org.bukkit.craftbukkit.v1_7_R3"; + } + } + + @SuppressWarnings("unchecked") + private Map getDataMap(Object handle) { + return (Map) getFieldValue( + getDataField(NbtType.TAG_COMPOUND, handle), handle); + } + + @SuppressWarnings("unchecked") + private List getDataList(Object handle) { + return (List) getFieldValue( + getDataField(NbtType.TAG_LIST, handle), handle); + } + + /** + * Construct a new NBT list of an unspecified type. + * @return The NBT list. + */ + public static NbtList createList(Object... content) { + return createList(Arrays.asList(content)); + } + + /** + * Construct a new NBT list of an unspecified type. + * @return The NBT list. + */ + public static NbtList createList(Iterable iterable) { + NbtList list = get().new NbtList( + INSTANCE.createNbtTag(NbtType.TAG_LIST, null) + ); + + // Add the content as well + for (Object obj : iterable) + list.add(obj); + return list; + } + + /** + * Construct a new NBT compound. + *

+ * Use {@link NbtCompound#asMap()} to modify it. + * @return The NBT compound. + */ + public static NbtCompound createCompound() { + return get().new NbtCompound( + INSTANCE.createNbtTag(NbtType.TAG_COMPOUND, null) + ); + } + + /** + * Construct a new NBT wrapper from a list. + * @param nmsList - the NBT list. + * @return The wrapper. + */ + public static NbtList fromList(Object nmsList) { + return get().new NbtList(nmsList); + } + + /** + * Load the content of a file from a stream. + *

+ * Use {@link Files#newInputStreamSupplier(java.io.File)} to provide a stream from a file. + * @param stream - the stream supplier. + * @param option - whether or not to decompress the input stream. + * @return The decoded NBT compound. + * @throws IOException If anything went wrong. + */ + public static NbtCompound fromStream(InputSupplier stream, StreamOptions option) throws IOException { + InputStream input = null; + DataInputStream data = null; + boolean suppress = true; + + try { + input = stream.getInput(); + data = new DataInputStream(new BufferedInputStream( + option == StreamOptions.GZIP_COMPRESSION ? new GZIPInputStream(input) : input + )); + + NbtCompound result = fromCompound(get().LOAD_COMPOUND.loadNbt(data)); + suppress = false; + return result; + + } finally { + if (data != null) + Closeables.close(data, suppress); + else if (input != null) + Closeables.close(input, suppress); + } + } + + /** + * Save the content of a NBT compound to a stream. + *

+ * Use {@link Files#newOutputStreamSupplier(java.io.File)} to provide a stream supplier to a file. + * @param source - the NBT compound to save. + * @param stream - the stream. + * @param option - whether or not to compress the output. + * @throws IOException If anything went wrong. + */ + public static void saveStream(NbtCompound source, OutputSupplier stream, StreamOptions option) throws IOException { + OutputStream output = null; + DataOutputStream data = null; + boolean suppress = true; + + try { + output = stream.getOutput(); + data = new DataOutputStream( + option == StreamOptions.GZIP_COMPRESSION ? new GZIPOutputStream(output) : output + ); + + invokeMethod(get().SAVE_COMPOUND, null, source.getHandle(), data); + suppress = false; + + } finally { + if (data != null) + Closeables.close(data, suppress); + else if (output != null) + Closeables.close(output, suppress); + } + } + + /** + * Construct a new NBT wrapper from a compound. + * @param nmsCompound - the NBT compund. + * @return The wrapper. + */ + public static NbtCompound fromCompound(Object nmsCompound) { + return get().new NbtCompound(nmsCompound); + } + + /** + * Set the NBT compound tag of a given item stack. + *

+ * The item stack must be a wrapper for a CraftItemStack. Use + * {@link MinecraftReflection#getBukkitItemStack(ItemStack)} if not. + * @param stack - the item stack, cannot be air. + * @param compound - the new NBT compound, or NULL to remove it. + * @throws IllegalArgumentException If the stack is not a CraftItemStack, or it represents air. + */ + public static void setItemTag(ItemStack stack, NbtCompound compound) { + checkItemStack(stack); + Object nms = getFieldValue(get().CRAFT_HANDLE, stack); + + // Now update the tag compound + setFieldValue(get().STACK_TAG, nms, compound.getHandle()); + } + + /** + * Construct a wrapper for an NBT tag stored (in memory) in an item stack. This is where + * auxillary data such as enchanting, name and lore is stored. It does not include items + * material, damage value or count. + *

+ * The item stack must be a wrapper for a CraftItemStack. + * @param stack - the item stack. + * @return A wrapper for its NBT tag. + */ + public static NbtCompound fromItemTag(ItemStack stack) { + checkItemStack(stack); + Object nms = getFieldValue(get().CRAFT_HANDLE, stack); + Object tag = getFieldValue(get().STACK_TAG, nms); + + // Create the tag if it doesn't exist + if (tag == null) { + NbtCompound compound = createCompound(); + setItemTag(stack, compound); + return compound; + } + return fromCompound(tag); + } + + /** + * Retrieve a CraftItemStack version of the stack. + * @param stack - the stack to convert. + * @return The CraftItemStack version. + */ + public static ItemStack getCraftItemStack(ItemStack stack) { + // Any need to convert? + if (stack == null || get().CRAFT_STACK.isAssignableFrom(stack.getClass())) + return stack; + try { + // Call the private constructor + Constructor caller = INSTANCE.CRAFT_STACK.getDeclaredConstructor(ItemStack.class); + caller.setAccessible(true); + return (ItemStack) caller.newInstance(stack); + } catch (Exception e) { + throw new IllegalStateException("Unable to convert " + stack + " + to a CraftItemStack."); + } + } + + /** + * Ensure that the given stack can store arbitrary NBT information. + * @param stack - the stack to check. + */ + private static void checkItemStack(ItemStack stack) { + if (stack == null) + throw new IllegalArgumentException("Stack cannot be NULL."); + if (!get().CRAFT_STACK.isAssignableFrom(stack.getClass())) + throw new IllegalArgumentException("Stack must be a CraftItemStack."); + if (stack.getType() == Material.AIR) + throw new IllegalArgumentException("ItemStacks representing air cannot store NMS information."); + } + + /** + * Convert wrapped List and Map objects into their respective NBT counterparts. + * @param name - the name of the NBT element to create. + * @param value - the value of the element to create. Can be a List or a Map. + * @return The NBT element. + */ + private Object unwrapValue(Object value) { + if (value == null) + return null; + + if (value instanceof Wrapper) { + return ((Wrapper) value).getHandle(); + + } else if (value instanceof List) { + throw new IllegalArgumentException("Can only insert a WrappedList."); + } else if (value instanceof Map) { + throw new IllegalArgumentException("Can only insert a WrappedCompound."); + + } else { + return createNbtTag(getPrimitiveType(value), value); + } + } + + /** + * Convert a given NBT element to a primitive wrapper or List/Map equivalent. + *

+ * All changes to any mutable objects will be reflected in the underlying NBT element(s). + * @param nms - the NBT element. + * @return The wrapper equivalent. + */ + private Object wrapNative(Object nms) { + if (nms == null) + return null; + + if (BASE_CLASS.isAssignableFrom(nms.getClass())) { + final NbtType type = getNbtType(nms); + + // Handle the different types + switch (type) { + case TAG_COMPOUND: + return new NbtCompound(nms); + case TAG_LIST: + return new NbtList(nms); + default: + return getFieldValue(getDataField(type, nms), nms); + } + } + throw new IllegalArgumentException("Unexpected type: " + nms); + } + + /** + * Construct a new NMS NBT tag initialized with the given value. + * @param type - the NBT type. + * @param value - the value, or NULL to keep the original value. + * @return The created tag. + */ + private Object createNbtTag(NbtType type, Object value) { + Object tag = invokeMethod(NBT_CREATE_TAG, null, (byte)type.id); + + if (value != null) { + setFieldValue(getDataField(type, tag), tag, value); + } + return tag; + } + + /** + * Retrieve the field where the NBT class stores its value. + * @param type - the NBT type. + * @param nms - the NBT class instance. + * @return The corresponding field. + */ + private Field getDataField(NbtType type, Object nms) { + if (DATA_FIELD[type.id] == null) + DATA_FIELD[type.id] = getField(nms, null, type.getFieldName()); + return DATA_FIELD[type.id]; + } + + /** + * Retrieve the NBT type from a given NMS NBT tag. + * @param nms - the native NBT tag. + * @return The corresponding type. + */ + private NbtType getNbtType(Object nms) { + int type = (Byte) invokeMethod(NBT_GET_TYPE, nms); + return NBT_ENUM.get(type); + } + + /** + * Retrieve the nearest NBT type for a given primitive type. + * @param primitive - the primitive type. + * @return The corresponding type. + */ + private NbtType getPrimitiveType(Object primitive) { + NbtType type = NBT_ENUM.get(NBT_CLASS.inverse().get( + Primitives.unwrap(primitive.getClass()) + )); + + // Display the illegal value at least + if (type == null) + throw new IllegalArgumentException(String.format( + "Illegal type: %s (%s)", primitive.getClass(), primitive)); + return type; + } + + /** + * Invoke a method on the given target instance using the provided parameters. + * @param method - the method to invoke. + * @param target - the target. + * @param params - the parameters to supply. + * @return The result of the method. + */ + private static Object invokeMethod(Method method, Object target, Object... params) { + try { + return method.invoke(target, params); + } catch (Exception e) { + throw new RuntimeException("Unable to invoke method " + method + " for " + target, e); + } + } + + private static void setFieldValue(Field field, Object target, Object value) { + try { + field.set(target, value); + } catch (Exception e) { + throw new RuntimeException("Unable to set " + field + " for " + target, e); + } + } + + private static Object getFieldValue(Field field, Object target) { + try { + return field.get(target); + } catch (Exception e) { + throw new RuntimeException("Unable to retrieve " + field + " for " + target, e); + } + } + + /** + * Search for the first publically and privately defined method of the given name and parameter count. + * @param requireMod - modifiers that are required. + * @param bannedMod - modifiers that are banned. + * @param clazz - a class to start with. + * @param methodName - the method name, or NULL to skip. + * @param params - the expected parameters. + * @return The first method by this name. + * @throws IllegalStateException If we cannot find this method. + */ + private static Method getMethod(int requireMod, int bannedMod, Class clazz, String methodName, Class... params) { + for (Method method : clazz.getDeclaredMethods()) { + // Limitation: Doesn't handle overloads + if ((method.getModifiers() & requireMod) == requireMod && + (method.getModifiers() & bannedMod) == 0 && + (methodName == null || method.getName().equals(methodName)) && + Arrays.equals(method.getParameterTypes(), params)) { + + method.setAccessible(true); + return method; + } + } + // Search in every superclass + if (clazz.getSuperclass() != null) + return getMethod(requireMod, bannedMod, clazz.getSuperclass(), methodName, params); + throw new IllegalStateException(String.format( + "Unable to find method %s (%s).", methodName, Arrays.asList(params))); + } + + /** + * Search for the first publically and privately defined field of the given name. + * @param instance - an instance of the class with the field. + * @param clazz - an optional class to start with, or NULL to deduce it from instance. + * @param fieldName - the field name. + * @return The first field by this name. + * @throws IllegalStateException If we cannot find this field. + */ + private static Field getField(Object instance, Class clazz, String fieldName) { + if (clazz == null) + clazz = instance.getClass(); + // Ignore access rules + for (Field field : clazz.getDeclaredFields()) { + if (field.getName().equals(fieldName)) { + field.setAccessible(true); + return field; + } + } + // Recursively fild the correct field + if (clazz.getSuperclass() != null) + return getField(instance, clazz.getSuperclass(), fieldName); + throw new IllegalStateException("Unable to find field " + fieldName + " in " + instance); + } + + /** + * Represents a class for caching wrappers. + * @author Kristian + */ + private final class CachedNativeWrapper { + // Don't recreate wrapper objects + private final ConcurrentMap cache = new MapMaker().weakKeys().makeMap(); + + public Object wrap(Object value) { + Object current = cache.get(value); + + if (current == null) { + current = wrapNative(value); + + // Only cache composite objects + if (current instanceof ConvertedMap || + current instanceof ConvertedList) { + cache.put(value, current); + } + } + return current; + } + } + + /** + * Represents a map that wraps another map and automatically + * converts entries of its type and another exposed type. + * @author Kristian + */ + private class ConvertedMap extends AbstractMap implements Wrapper { + private final Object handle; + private final Map original; + + private final CachedNativeWrapper cache = new CachedNativeWrapper(); + + public ConvertedMap(Object handle, Map original) { + this.handle = handle; + this.original = original; + } + + // For converting back and forth + protected Object wrapOutgoing(Object value) { + return cache.wrap(value); + } + protected Object unwrapIncoming(Object wrapped) { + return unwrapValue(wrapped); + } + + // Modification + @Override + public Object put(String key, Object value) { + return wrapOutgoing(original.put( + (String) key, + unwrapIncoming(value) + )); + } + + // Performance + @Override + public Object get(Object key) { + return wrapOutgoing(original.get(key)); + } + @Override + public Object remove(Object key) { + return wrapOutgoing(original.remove(key)); + } + @Override + public boolean containsKey(Object key) { + return original.containsKey(key); + } + + @Override + public Set> entrySet() { + return new AbstractSet>() { + @Override + public boolean add(Entry e) { + String key = e.getKey(); + Object value = e.getValue(); + + original.put(key, unwrapIncoming(value)); + return true; + } + + @Override + public int size() { + return original.size(); + } + + @Override + public Iterator> iterator() { + return ConvertedMap.this.iterator(); + } + }; + } + + private Iterator> iterator() { + final Iterator> proxy = original.entrySet().iterator(); + + return new Iterator>() { + @Override + public boolean hasNext() { + return proxy.hasNext(); + } + + @Override + public Entry next() { + Entry entry = proxy.next(); + + return new SimpleEntry( + entry.getKey(), wrapOutgoing(entry.getValue()) + ); + } + + @Override + public void remove() { + proxy.remove(); + } + }; + } + + @Override + public Object getHandle() { + return handle; + } + } + + /** + * Represents a list that wraps another list and converts elements + * of its type and another exposed type. + * @author Kristian + */ + private class ConvertedList extends AbstractList implements Wrapper { + private final Object handle; + + private final List original; + private final CachedNativeWrapper cache = new CachedNativeWrapper(); + + public ConvertedList(Object handle, List original) { + if (NBT_LIST_TYPE == null) + NBT_LIST_TYPE = getField(handle, null, "type"); + this.handle = handle; + this.original = original; + } + + protected Object wrapOutgoing(Object value) { + return cache.wrap(value); + } + protected Object unwrapIncoming(Object wrapped) { + return unwrapValue(wrapped); + } + + @Override + public Object get(int index) { + return wrapOutgoing(original.get(index)); + } + @Override + public int size() { + return original.size(); + } + @Override + public Object set(int index, Object element) { + return wrapOutgoing( + original.set(index, unwrapIncoming(element)) + ); + } + @Override + public void add(int index, Object element) { + Object nbt = unwrapIncoming(element); + + // Set the list type if its the first element + if (size() == 0) + setFieldValue(NBT_LIST_TYPE, handle, (byte)getNbtType(nbt).id); + original.add(index, nbt); + } + @Override + public Object remove(int index) { + return wrapOutgoing(original.remove(index)); + } + @Override + public boolean remove(Object o) { + return original.remove(unwrapIncoming(o)); + } + + @Override + public Object getHandle() { + return handle; + } + } + + /** + * Represents a method for loading an NBT compound. + * @author Kristian + */ + private static abstract class LoadCompoundMethod { + protected Method staticMethod; + + protected void setMethod(Method method) { + this.staticMethod = method; + this.staticMethod.setAccessible(true); + } + + /** + * Load an NBT compound from a given stream. + * @param input - the input stream. + * @return The loaded NBT compound. + */ + public abstract Object loadNbt(DataInput input); + } + + /** + * Load an NBT compound from the NBTCompressedStreamTools static method in 1.7.2 - 1.7.5 + */ + private static class LoadMethodWorldUpdate extends LoadCompoundMethod { + public LoadMethodWorldUpdate(Class streamClass) { + setMethod(getMethod(Modifier.STATIC, 0, streamClass, null, DataInput.class)); + } + + @Override + public Object loadNbt(DataInput input) { + return invokeMethod(staticMethod, null, input); + } + } + + /** + * Load an NBT compound from the NBTCompressedStreamTools static method in 1.7.8 + */ + private static class LoadMethodSkinUpdate extends LoadCompoundMethod { + private Object readLimiter; + + public LoadMethodSkinUpdate(Class streamClass, Class readLimiterClass) { + setMethod(getMethod(Modifier.STATIC, 0, streamClass, null, DataInput.class, readLimiterClass)); + + // Find the unlimited read limiter + for (Field field : readLimiterClass.getDeclaredFields()) { + if (readLimiterClass.isAssignableFrom(field.getType())) { + try { + readLimiter = field.get(null); + } catch (Exception e) { + throw new RuntimeException("Cannot retrieve read limiter.", e); + } + } + } + } + + @Override + public Object loadNbt(DataInput input) { + return invokeMethod(staticMethod, null, input, readLimiter); + } + } +} \ No newline at end of file diff --git a/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/UUIDHandler.java b/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/UUIDHandler.java index 963950952..5c8b753a9 100644 --- a/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/UUIDHandler.java +++ b/PlotSquared/src/main/java/com/intellectualcrafters/plot/util/bukkit/UUIDHandler.java @@ -1,9 +1,13 @@ package com.intellectualcrafters.plot.util.bukkit; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.FilenameFilter; +import java.nio.file.StandardOpenOption; import java.util.HashMap; import java.util.HashSet; +import java.util.Map.Entry; import java.util.UUID; import org.bukkit.Bukkit; @@ -11,6 +15,9 @@ import org.bukkit.OfflinePlayer; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; +import com.google.common.io.Files; +import com.google.common.io.InputSupplier; +import com.google.common.io.OutputSupplier; import com.intellectualcrafters.plot.PlotSquared; import com.intellectualcrafters.plot.config.C; import com.intellectualcrafters.plot.config.Settings; @@ -21,6 +28,10 @@ import com.intellectualcrafters.plot.object.Plot; import com.intellectualcrafters.plot.object.PlotPlayer; import com.intellectualcrafters.plot.object.StringWrapper; import com.intellectualcrafters.plot.util.ExpireManager; +import com.intellectualcrafters.plot.util.NbtFactory; +import com.intellectualcrafters.plot.util.TaskManager; +import com.intellectualcrafters.plot.util.NbtFactory.NbtCompound; +import com.intellectualcrafters.plot.util.NbtFactory.StreamOptions; import com.intellectualcrafters.plot.uuid.DefaultUUIDWrapper; import com.intellectualcrafters.plot.uuid.OfflineUUIDWrapper; import com.intellectualcrafters.plot.uuid.UUIDWrapper; @@ -106,111 +117,148 @@ public class UUIDHandler { } return uuids; } - + public static void cacheAll(final String world) { if (CACHED) { return; } - PlotSquared.log(C.PREFIX.s() + "&6Starting player data caching: " + world); + final File container = Bukkit.getWorldContainer(); UUIDHandler.CACHED = true; - add(new StringWrapper("*"), DBFunc.everyone); - if (Settings.TWIN_MODE_UUID) { - HashSet all = getAllUUIDS(); - final File playerdataFolder = new File(Bukkit.getWorldContainer(), world + File.separator + "playerdata"); - String[] dat = playerdataFolder.list(new FilenameFilter() { - @Override - public boolean accept(final File f, final String s) { - return s.endsWith(".dat"); - } - }); - boolean check = all.size() == 0; - if (dat != null) { - for (final String current : dat) { - final String s = current.replaceAll(".dat$", ""); - try { - final UUID uuid = UUID.fromString(s); - if (check || all.contains(uuid)) { - OfflinePlayer op = Bukkit.getOfflinePlayer(uuid); - ExpireManager.dates.put(uuid, op.getLastPlayed()); - add(new StringWrapper(op.getName()), uuid); + TaskManager.runTaskAsync(new Runnable() { + @Override + public void run() { + PlotSquared.log(C.PREFIX.s() + "&6Starting player data caching for: " + world); + final HashMap toAdd = new HashMap<>(); + toAdd.put(new StringWrapper("*"), DBFunc.everyone); + if (Settings.TWIN_MODE_UUID) { + HashSet all = getAllUUIDS(); + PlotSquared.log("&aFast mod UUID caching enabled!"); + final File playerdataFolder = new File(container, world + File.separator + "playerdata"); + String[] dat = playerdataFolder.list(new FilenameFilter() { + @Override + public boolean accept(final File f, final String s) { + return s.endsWith(".dat"); } - } catch (final Exception e) { - PlotSquared.log(C.PREFIX.s() + "Invalid playerdata: " + current); + }); + boolean check = all.size() == 0; + if (dat != null) { + for (final String current : dat) { + final String s = current.replaceAll(".dat$", ""); + try { + UUID uuid = UUID.fromString(s); + if (check || all.contains(uuid)) { + File file = new File(playerdataFolder + File.separator + current); + InputSupplier is = Files.newInputStreamSupplier(file); + NbtCompound compound = NbtFactory.fromStream(is, StreamOptions.GZIP_COMPRESSION); + NbtCompound bukkit = (NbtCompound) compound.get("bukkit"); + String name = (String) bukkit.get("lastKnownName"); + long last = (long) bukkit.get("lastPlayed"); + ExpireManager.dates.put(uuid, last); + toAdd.put(new StringWrapper(name), uuid); + } + } catch (final Exception e) { + e.printStackTrace(); + PlotSquared.log(C.PREFIX.s() + "Invalid playerdata: " + current); + } + } + } + cache(toAdd); + return; + } + final HashSet worlds = new HashSet<>(); + worlds.add(world); + worlds.add("world"); + final HashSet uuids = new HashSet<>(); + final HashSet names = new HashSet<>(); + File playerdataFolder = null; + for (final String worldname : worlds) { + // Getting UUIDs + playerdataFolder = new File(container, worldname + File.separator + "playerdata"); + String[] dat = playerdataFolder.list(new FilenameFilter() { + @Override + public boolean accept(final File f, final String s) { + return s.endsWith(".dat"); + } + }); + if (dat != null && dat.length != 0) { + for (final String current : dat) { + final String s = current.replaceAll(".dat$", ""); + try { + final UUID uuid = UUID.fromString(s); + uuids.add(uuid); + } catch (final Exception e) { + PlotSquared.log(C.PREFIX.s() + "Invalid playerdata: " + current); + } + } + break; + } + // Getting names + final File playersFolder = new File(worldname + File.separator + "players"); + dat = playersFolder.list(new FilenameFilter() { + @Override + public boolean accept(final File f, final String s) { + return s.endsWith(".dat"); + } + }); + if (dat != null && dat.length != 0) { + for (final String current : dat) { + names.add(current.replaceAll(".dat$", "")); + } + break; } } - } - PlotSquared.log(C.PREFIX.s() + "&6Cached a total of: " + UUIDHandler.uuidMap.size() + " UUIDs"); - return; - } - final HashSet worlds = new HashSet<>(); - worlds.add(world); - worlds.add("world"); - final HashSet uuids = new HashSet<>(); - final HashSet names = new HashSet<>(); - for (final String worldname : worlds) { - // Getting UUIDs - final File playerdataFolder = new File(Bukkit.getWorldContainer(), worldname + File.separator + "playerdata"); - String[] dat = playerdataFolder.list(new FilenameFilter() { - @Override - public boolean accept(final File f, final String s) { - return s.endsWith(".dat"); - } - }); - if (dat != null) { - for (final String current : dat) { - final String s = current.replaceAll(".dat$", ""); + for (UUID uuid : uuids) { try { - final UUID uuid = UUID.fromString(s); - uuids.add(uuid); - } catch (final Exception e) { - PlotSquared.log(C.PREFIX.s() + "Invalid playerdata: " + current); + File file = new File(playerdataFolder + File.separator + uuid.toString() + ".dat"); + InputSupplier is = Files.newInputStreamSupplier(file); + NbtCompound compound = NbtFactory.fromStream(is, StreamOptions.GZIP_COMPRESSION); + NbtCompound bukkit = (NbtCompound) compound.get("bukkit"); + String name = (String) bukkit.get("lastKnownName"); + long last = (long) bukkit.get("lastPlayed"); + if (Settings.OFFLINE_MODE) { + if (!Settings.UUID_LOWERCASE || !name.toLowerCase().equals(name)) { + long most = (long) compound.get("UUIDMost"); + long least = (long) compound.get("UUIDLeast"); + uuid = new UUID(most, least); + } + } + ExpireManager.dates.put(uuid, last); + toAdd.put(new StringWrapper(name), uuid); + } catch (final Throwable e) { + PlotSquared.log(C.PREFIX.s() + "&6Invalid playerdata: " + uuid.toString() + ".dat"); } } - } - // Getting names - final File playersFolder = new File(worldname + File.separator + "players"); - dat = playersFolder.list(new FilenameFilter() { - @Override - public boolean accept(final File f, final String s) { - return s.endsWith(".dat"); + for (final String name : names) { + final UUID uuid = uuidWrapper.getUUID(name); + final StringWrapper nameWrap = new StringWrapper(name); + toAdd.put(nameWrap, uuid); } - }); - if (dat != null) { - for (final String current : dat) { - names.add(current.replaceAll(".dat$", "")); + + if (uuidMap.size() == 0) { + for (OfflinePlotPlayer op : uuidWrapper.getOfflinePlayers()) { + if (op.getLastPlayed() != 0) { + String name = op.getName(); + StringWrapper wrap = new StringWrapper(name); + UUID uuid = uuidWrapper.getUUID(op); + toAdd.put(wrap, uuid); + } + } } + cache(toAdd); } - } - final UUIDWrapper wrapper = new DefaultUUIDWrapper(); - for (UUID uuid : uuids) { - try { - final OfflinePlotPlayer player = wrapper.getOfflinePlayer(uuid); - ExpireManager.dates.put(uuid, player.getLastPlayed()); - uuid = UUIDHandler.uuidWrapper.getUUID(player); - final StringWrapper name = new StringWrapper(player.getName()); - add(name, uuid); - } catch (final Throwable e) { - PlotSquared.log(C.PREFIX.s() + "&6Invalid playerdata: " + uuid.toString() + ".dat"); - } - } - for (final String name : names) { - final UUID uuid = uuidWrapper.getUUID(name); - final StringWrapper nameWrap = new StringWrapper(name); - add(nameWrap, uuid); - } - - - if (uuidMap.size() == 0) { - for (OfflinePlotPlayer op : uuidWrapper.getOfflinePlayers()) { - if (op.getLastPlayed() != 0) { - String name = op.getName(); - StringWrapper wrap = new StringWrapper(name); - UUID uuid = uuidWrapper.getUUID(op); - add(wrap, uuid); + }); + } + + public static void cache(final HashMap toAdd) { + TaskManager.runTask(new Runnable() { + @Override + public void run() { + for (Entry entry : toAdd.entrySet()) { + add(entry.getKey(), entry.getValue()); } + PlotSquared.log(C.PREFIX.s() + "&6Cached a total of: " + UUIDHandler.uuidMap.size() + " UUIDs"); } - } - PlotSquared.log(C.PREFIX.s() + "&6Cached a total of: " + UUIDHandler.uuidMap.size() + " UUIDs"); + }); } public static UUID getUUID(final PlotPlayer player) {