Java tutorial
/* * This file is part of UltimateCore, licensed under the MIT License (MIT). * * Copyright (c) Bammerbom * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package bammerbom.ultimatecore.bukkit.resources.utils; import com.google.common.base.Function; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.base.Splitter; import com.google.common.collect.*; 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; import java.io.*; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.*; import java.util.concurrent.ConcurrentMap; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.Server; import org.bukkit.inventory.ItemStack; public class AttributeUtil { // This may be modified public ItemStack stack; private NbtFactory.NbtList attributes; public AttributeUtil(ItemStack stack) { // Create a CraftItemStack (under the hood) this.stack = NbtFactory.getCraftItemStack(stack); // Load NBT NbtFactory.NbtCompound nbt = NbtFactory.fromItemTag(this.stack); this.attributes = nbt.getList("AttributeModifiers", true); } /** * Retrieve the modified item stack. * * @return The modified item stack. */ public ItemStack getStack() { return stack; } /** * Retrieve the number of attributes. * * @return Number of attributes. */ public int size() { return attributes.size(); } /** * Add a new attribute to the list. * * @param attribute - the new attribute. */ public void add(Attribute attribute) { Preconditions.checkNotNull(attribute.getName(), "must specify an attribute name."); attributes.add(attribute.data); } /** * Remove the first instance of the given attribute. * <p> * The attribute will be removed using its UUID. * * @param attribute - the attribute to remove. * @return TRUE if the attribute was removed, FALSE otherwise. */ public boolean remove(Attribute attribute) { UUID uuid = attribute.getUUID(); for (Iterator<Attribute> it = values().iterator(); it.hasNext();) { if (Objects.equal(it.next().getUUID(), uuid)) { it.remove(); return true; } } return false; } public void clear() { attributes.clear(); } /** * Retrieve the attribute at a given index. * * @param index - the index to look up. * @return The attribute at that index. */ public Attribute get(int index) { return new Attribute((NbtFactory.NbtCompound) attributes.get(index)); } // We can't make Attributes itself iterable without splitting it up into separate classes public Iterable<Attribute> values() { return new Iterable<Attribute>() { @Override public Iterator<Attribute> iterator() { return Iterators.transform(attributes.iterator(), new Function<Object, Attribute>() { @Override public Attribute apply(@Nullable Object element) { return new Attribute((NbtFactory.NbtCompound) element); } }); } }; } public static class AttributeType { private static ConcurrentMap<String, AttributeType> LOOKUP = Maps.newConcurrentMap(); public static final AttributeType GENERIC_MAX_HEALTH = new AttributeType("generic.maxHealth").register(); public static final AttributeType GENERIC_FOLLOW_RANGE = new AttributeType("generic.followRange") .register(); public static final AttributeType GENERIC_ATTACK_DAMAGE = new AttributeType("generic.attackDamage") .register(); public static final AttributeType GENERIC_MOVEMENT_SPEED = new AttributeType("generic.movementSpeed") .register(); public static final AttributeType GENERIC_KNOCKBACK_RESISTANCE = new AttributeType( "generic.knockbackResistance").register(); private final String minecraftId; /** * Construct a new attribute type. * <p> * Remember to {@link #register()} the type. * * @param minecraftId - the ID of the type. */ public AttributeType(String minecraftId) { this.minecraftId = minecraftId; } /** * Retrieve the associated minecraft ID. * * @return The associated ID. */ public String getMinecraftId() { return minecraftId; } /** * Register the type in the central registry. * * @return The registered type. */ // Constructors should have no side-effects! public AttributeType register() { AttributeType old = LOOKUP.putIfAbsent(minecraftId, this); return old != null ? old : this; } /** * Retrieve the attribute type associated with a given ID. * * @param minecraftId The ID to search for. * @return The attribute type, or NULL if not found. */ public static AttributeType fromId(String minecraftId) { return LOOKUP.get(minecraftId); } /** * Retrieve every registered attribute type. * * @return Every type. */ public static Iterable<AttributeType> values() { return LOOKUP.values(); } } public static class Attribute { private NbtFactory.NbtCompound data; private Attribute(Builder builder) { data = NbtFactory.createCompound(); setAmount(builder.amount); setOperation(builder.operation); setAttributeType(builder.type); setName(builder.name); setUUID(builder.uuid); } private Attribute(NbtFactory.NbtCompound data) { this.data = data; } public double getAmount() { return data.getDouble("Amount", 0.0); } public void setAmount(double amount) { data.put("Amount", amount); } public Operation getOperation() { return Operation.fromId(data.getInteger("Operation", 0)); } public void setOperation(@Nonnull Operation operation) { Preconditions.checkNotNull(operation, "operation cannot be NULL."); data.put("Operation", operation.getId()); } public AttributeType getAttributeType() { return AttributeType.fromId(data.getString("AttributeName", null)); } public void setAttributeType(@Nonnull AttributeType type) { Preconditions.checkNotNull(type, "type cannot be NULL."); data.put("AttributeName", type.getMinecraftId()); } public String getName() { return data.getString("Name", null); } public void setName(@Nonnull String name) { Preconditions.checkNotNull(name, "name cannot be NULL."); data.put("Name", name); } public UUID getUUID() { return new UUID(data.getLong("UUIDMost", null), data.getLong("UUIDLeast", null)); } public void setUUID(@Nonnull UUID id) { Preconditions.checkNotNull("id", "id cannot be NULL."); data.put("UUIDLeast", id.getLeastSignificantBits()); data.put("UUIDMost", id.getMostSignificantBits()); } /** * Construct a new attribute builder with a random UUID and default * operation of adding numbers. * * @return The attribute builder. */ public static Builder newBuilder() { return new Builder().uuid(UUID.randomUUID()).operation(Operation.ADD_NUMBER); } // Makes it easier to construct an attribute public static class Builder { private double amount; private Operation operation = Operation.ADD_NUMBER; private AttributeType type; private String name; private UUID uuid; private Builder() { // Don't make this accessible } public Builder amount(double amount) { this.amount = amount; return this; } public Builder operation(Operation operation) { this.operation = operation; return this; } public Builder type(AttributeType type) { this.type = type; return this; } public Builder name(String name) { this.name = name; return this; } public Builder uuid(UUID uuid) { this.uuid = uuid; return this; } public Attribute build() { return new Attribute(this); } } } public enum Operation { ADD_NUMBER(0), MULTIPLY_PERCENTAGE(1), ADD_PERCENTAGE(2); private int id; private Operation(int id) { this.id = id; } public int getId() { return id; } public static Operation fromId(int id) { // Linear scan is very fast for small N for (Operation op : values()) { if (op.getId() == id) { return op; } } throw new IllegalArgumentException("Corrupt operation ID " + id + " detected."); } } } class NbtFactory { // Convert between NBT id and the equivalent class in java private static final BiMap<Integer, Class<?>> NBT_CLASS = HashBiMap.create(); private static final BiMap<Integer, NbtType> 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. * <p> * All changes to this map will be reflected in the underlying NBT compound. * Values may only be one of the following: * <ul> * <li>Primitive types</li> * <li>{@link java.lang.String String}</li> * <li>{@link NbtList}</li> * <li>{@link NbtCompound}</li> * </ul> * <p> * See also: * <ul> * <li>{@link NbtFactory#createCompound()}</li> * <li>{@link NbtFactory#fromCompound(Object)}</li> * </ul> * * @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. * <p> * 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<String> entries = getPathElements(path); Map<String, Object> 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. * <p> * 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> T getPath(String path) { List<String> 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. * <p> * 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<? extends OutputStream> 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<String> 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<String> getPathElements(String path) { return Lists.newArrayList(Splitter.on(".").omitEmptyStrings().split(path)); } } /** * Represents a root NBT list. See also: * <ul> * <li>{@link NbtFactory#createNbtList()}</li> * <li>{@link NbtFactory#fromList(Object)}</li> * </ul> * * @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<String, Object> getDataMap(Object handle) { return (Map<String, Object>) getFieldValue(getDataField(NbtType.TAG_COMPOUND, handle), handle); } @SuppressWarnings("unchecked") private List<Object> getDataList(Object handle) { return (List<Object>) 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<? extends Object> 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. * <p> * 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. * <p> * 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<? extends InputStream> 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. * <p> * 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<? extends OutputStream> 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. * <p> * 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. * <p> * 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. * <p> * 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 = (int) 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<Object, Object> 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<String, Object> implements Wrapper { private final Object handle; private final Map<String, Object> original; private final CachedNativeWrapper cache = new CachedNativeWrapper(); public ConvertedMap(Object handle, Map<String, Object> 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(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<Entry<String, Object>> entrySet() { return new AbstractSet<Entry<String, Object>>() { @Override public boolean add(Entry<String, Object> 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<Entry<String, Object>> iterator() { return ConvertedMap.this.iterator(); } }; } private Iterator<Entry<String, Object>> iterator() { final Iterator<Entry<String, Object>> proxy = original.entrySet().iterator(); return new Iterator<Entry<String, Object>>() { @Override public boolean hasNext() { return proxy.hasNext(); } @Override public Entry<String, Object> next() { Entry<String, Object> 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<Object> implements Wrapper { private final Object handle; private final List<Object> original; private final CachedNativeWrapper cache = new CachedNativeWrapper(); public ConvertedList(Object handle, List<Object> 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); } } }