com.comphenix.protocol.wrappers.WrappedDataWatcher.java Source code

Java tutorial

Introduction

Here is the source code for com.comphenix.protocol.wrappers.WrappedDataWatcher.java

Source

/**
 *  ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
 *  Copyright (C) 2016 dmulloy2
 *
 *  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 2 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., 59 Temple Place, Suite 330, Boston, MA
 *  02111-1307 USA
 */
package com.comphenix.protocol.wrappers;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import org.apache.commons.lang.Validate;
import org.bukkit.entity.Entity;
import org.bukkit.inventory.ItemStack;

import com.comphenix.protocol.injector.BukkitUnwrapper;
import com.comphenix.protocol.reflect.FieldAccessException;
import com.comphenix.protocol.reflect.FuzzyReflection;
import com.comphenix.protocol.reflect.StructureModifier;
import com.comphenix.protocol.reflect.accessors.Accessors;
import com.comphenix.protocol.reflect.accessors.ConstructorAccessor;
import com.comphenix.protocol.reflect.accessors.FieldAccessor;
import com.comphenix.protocol.reflect.accessors.MethodAccessor;
import com.comphenix.protocol.reflect.fuzzy.FuzzyFieldContract;
import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract;
import com.comphenix.protocol.utility.MinecraftReflection;
import com.comphenix.protocol.wrappers.collection.ConvertedMap;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableBiMap;

/**
 * Represents a DataWatcher in 1.8 thru 1.10
 * 
 * @author dmulloy2
 */
public class WrappedDataWatcher extends AbstractWrapper implements Iterable<WrappedWatchableObject> {
    private static final Class<?> HANDLE_TYPE = MinecraftReflection.getDataWatcherClass();

    private static MethodAccessor GETTER = null;
    private static MethodAccessor SETTER = null;
    private static MethodAccessor REGISTER = null;

    private static FieldAccessor ENTITY_DATA_FIELD = null;
    private static FieldAccessor ENTITY_FIELD = null;
    private static FieldAccessor MAP_FIELD = null;

    private static ConstructorAccessor constructor = null;
    private static ConstructorAccessor lightningConstructor = null;

    private static Object fakeEntity = null;

    // ---- Construction

    /**
     * Constructs a new DataWatcher wrapper around a NMS handle. The resulting
     * DataWatcher will likely have existing values that can be removed with
     * {@link #clear()}.
     * 
     * @param handle DataWatcher handle
     */
    public WrappedDataWatcher(Object handle) {
        super(HANDLE_TYPE);
        setHandle(handle);
    }

    /**
     * Constructs a new DataWatcher using a fake lightning entity. The
     * resulting DataWatcher will not have any keys or values and new ones will
     * have to be added using watcher objects.
     */
    public WrappedDataWatcher() {
        this(newHandle(fakeEntity()));
    }

    /**
     * Constructs a new DataWatcher using a real entity. The resulting
     * DataWatcher will not have any keys or values and new ones will have to
     * be added using watcher objects.
     * 
     * @param entity The entity
     */
    public WrappedDataWatcher(Entity entity) {
        this(newHandle(BukkitUnwrapper.getInstance().unwrapItem(entity)));
    }

    /**
     * Constructs a new DataWatcher using a fake lightning entity and a given
     * list of watchable objects.
     * 
     * @param objects The list of objects
     */
    public WrappedDataWatcher(List<WrappedWatchableObject> objects) {
        this();

        if (MinecraftReflection.watcherObjectExists()) {
            for (WrappedWatchableObject object : objects) {
                setObject(object.getWatcherObject(), object);
            }
        } else {
            for (WrappedWatchableObject object : objects) {
                setObject(object.getIndex(), object);
            }
        }
    }

    private static Object newHandle(Object entity) {
        if (constructor == null) {
            constructor = Accessors.getConstructorAccessor(HANDLE_TYPE, MinecraftReflection.getEntityClass());
        }

        return constructor.invoke(entity);
    }

    private static Object fakeEntity() {
        if (fakeEntity != null) {
            return fakeEntity;
        }

        // We can create a fake lightning strike without it affecting anything
        if (lightningConstructor == null) {
            lightningConstructor = Accessors.getConstructorAccessor(
                    MinecraftReflection.getMinecraftClass("EntityLightning"),
                    MinecraftReflection.getNmsWorldClass(), double.class, double.class, double.class,
                    boolean.class);
        }

        return fakeEntity = lightningConstructor.invoke(null, 0, 0, 0, true);
    }

    // ---- Collection Methods

    @SuppressWarnings("unchecked")
    private Map<Integer, Object> getMap() {
        if (MAP_FIELD == null) {
            FuzzyReflection fuzzy = FuzzyReflection.fromClass(handleType, true);
            MAP_FIELD = Accessors.getFieldAccessor(fuzzy.getField(
                    FuzzyFieldContract.newBuilder().banModifier(Modifier.STATIC).typeDerivedOf(Map.class).build()));
        }

        if (MAP_FIELD == null) {
            throw new FieldAccessException("Could not find index <-> Item map.");
        }

        return (Map<Integer, Object>) MAP_FIELD.get(handle);
    }

    /**
     * Gets the contents of this DataWatcher as a map.
     * @return The contents
     */
    public Map<Integer, WrappedWatchableObject> asMap() {
        return new ConvertedMap<Integer, Object, WrappedWatchableObject>(getMap()) {
            @Override
            protected WrappedWatchableObject toOuter(Object inner) {
                return inner != null ? new WrappedWatchableObject(inner) : null;
            }

            @Override
            protected Object toInner(WrappedWatchableObject outer) {
                return outer != null ? outer.getHandle() : null;
            }
        };
    }

    /**
     * Gets a set containing the registered indexes.
     * @return The set
     */
    public Set<Integer> getIndexes() {
        return getMap().keySet();
    }

    /**
     * Gets a list of the contents of this DataWatcher.
     * @return The contents
     */
    public List<WrappedWatchableObject> getWatchableObjects() {
        return new ArrayList<>(asMap().values());
    }

    @Override
    public Iterator<WrappedWatchableObject> iterator() {
        return getWatchableObjects().iterator();
    }

    /**
     * Gets the size of this DataWatcher's contents.
     * @return The size
     */
    public int size() {
        return getMap().size();
    }

    /**
     * Gets the item at a given index.
     * 
     * @param index Index to get
     * @return The watchable object, or null if none exists
     */
    public WrappedWatchableObject getWatchableObject(int index) {
        Object handle = getMap().get(index);
        if (handle != null) {
            return new WrappedWatchableObject(handle);
        } else {
            return null;
        }
    }

    /**
     * @deprecated Renamed to {@link #remove(int)}
     */
    @Deprecated
    public WrappedWatchableObject removeObject(int index) {
        return remove(index);
    }

    /**
     * Removes the item at a given index.
     * 
     * @param index Index to remove
     * @return The previous value, or null if none existed
     */
    public WrappedWatchableObject remove(int index) {
        Object removed = getMap().remove(index);
        return removed != null ? new WrappedWatchableObject(removed) : null;
    }

    /**
     * Whether or not this DataWatcher has an object at a given index.
     * 
     * @param index Index to check for
     * @return True if it does, false if not
     */
    public boolean hasIndex(int index) {
        return getObject(index) != null;
    }

    /**
     * Returns a set containing all the registered indexes
     * @return The set
     */
    public Set<Integer> indexSet() {
        return getMap().keySet();
    }

    /**
     * Clears the contents of this DataWatcher. The watcher will be empty after
     * this operation is called.
     */
    public void clear() {
        getMap().clear();
    }

    // ---- Object Getters

    /**
     * Get a watched byte.
     * 
     * @param index - index of the watched byte.
     * @return The watched byte, or NULL if this value doesn't exist.
     */
    public Byte getByte(int index) {
        return (Byte) getObject(index);
    }

    /**
     * Get a watched short.
     * 
     * @param index - index of the watched short.
     * @return The watched short, or NULL if this value doesn't exist.
     */
    public Short getShort(int index) {
        return (Short) getObject(index);
    }

    /**
     * Get a watched integer.
     * 
     * @param index - index of the watched integer.
     * @return The watched integer, or NULL if this value doesn't exist.
     */
    public Integer getInteger(int index) {
        return (Integer) getObject(index);
    }

    /**
     * Get a watched float.
     * 
     * @param index - index of the watched float.
     * @return The watched float, or NULL if this value doesn't exist.
     */
    public Float getFloat(int index) {
        return (Float) getObject(index);
    }

    /**
     * Get a watched string.
     * 
     * @param index - index of the watched string.
     * @return The watched string, or NULL if this value doesn't exist.
     */
    public String getString(int index) {
        return (String) getObject(index);
    }

    /**
     * Get a watched string.
     * 
     * @param index - index of the watched string.
     * @return The watched string, or NULL if this value doesn't exist.
     */
    public ItemStack getItemStack(int index) {
        return (ItemStack) getObject(index);
    }

    /**
     * Get a watched string.
     * 
     * @param index - index of the watched string.
     * @return The watched string, or NULL if this value doesn't exist.
     */
    public WrappedChunkCoordinate getChunkCoordinate(int index) {
        return (WrappedChunkCoordinate) getObject(index);
    }

    /**
     * Retrieve a watchable object by index.
     * 
     * @param index Index of the object to retrieve.
     * @return The watched object or null if it doesn't exist.
     */
    public Object getObject(int index) {
        return getObject(WrappedDataWatcherObject.fromIndex(index));
    }

    /**
     * Retrieve a watchable object by watcher object.
     * 
     * @param object The watcher object
     * @return The watched object or null if it doesn't exist.
     */
    public Object getObject(WrappedDataWatcherObject object) {
        Validate.notNull(object, "Watcher object cannot be null!");

        if (GETTER == null) {
            FuzzyReflection fuzzy = FuzzyReflection.fromClass(handleType, true);

            if (MinecraftReflection.watcherObjectExists()) {
                GETTER = Accessors.getMethodAccessor(fuzzy.getMethod(FuzzyMethodContract.newBuilder()
                        .parameterExactType(object.getHandleType()).returnTypeExact(Object.class).build(), "get"));
            } else {
                GETTER = Accessors.getMethodAccessor(
                        fuzzy.getMethod(FuzzyMethodContract.newBuilder().parameterExactType(int.class)
                                .returnTypeExact(MinecraftReflection.getDataWatcherItemClass()).build()));
            }
        }

        try {
            Object value = GETTER.invoke(handle, object.getHandle());
            return WrappedWatchableObject.getWrapped(value);
        } catch (RuntimeException ex) {
            // Nothing exists at this index
            return null;
        }
    }

    // ---- Object Setters

    /**
     * Sets the DataWatcher Item at a given index to a new value. In 1.9 and up,
     * you cannot register objects without a watcher object.
     * 
     * @param index Index of the object to set
     * @param value New value
     * @param update Whether or not to inform the client
     * 
     * @see {@link #setObject(WrappedDataWatcherObject, Object, boolean)}
     * @throws IllegalArgumentException in 1.9 and up if there isn't already an
     *       object at this index
     */
    public void setObject(int index, Object value, boolean update) {
        if (MinecraftReflection.watcherObjectExists() && !hasIndex(index)) {
            throw new IllegalArgumentException("You cannot register objects without a watcher object!");
        }

        setObject(WrappedDataWatcherObject.fromIndex(index), value, update);
    }

    /**
     * Shortcut for {@link #setObject(int, Object, boolean)}
     */
    public void setObject(int index, Object value) {
        setObject(index, value, false);
    }

    /**
     * Sets the DataWatcher Item at a given index to a new value.
     * 
     * @param index Index of the object to set
     * @param Serializer Serializer from {@link Serializer#get(Class)}
     * @param value New value
     * @param update Whether or not to inform the client
     * 
     * @see {@link #setObject(WrappedDataWatcherObject, Object)}
     */
    public void setObject(int index, Serializer serializer, Object value, boolean update) {
        setObject(new WrappedDataWatcherObject(index, serializer), value, update);
    }

    /**
     * Alias for {@link #setObject(int, Serializer, Object, boolean)}
     */
    public void setObject(int index, Serializer serializer, Object value) {
        setObject(new WrappedDataWatcherObject(index, serializer), value, false);
    }

    /**
     * Sets the DataWatcher Item at a given index to a new value.
     * 
     * @param index Index of the object to set
     * @param value New value
     * @param update Whether or not to inform the client
     * 
     * @see {@link #setObject(int, Object, boolean)}
     */
    public void setObject(int index, WrappedWatchableObject value, boolean update) {
        setObject(index, value.getRawValue(), update);
    }

    /**
     * Alias for {@link #setObject(int, WrappedWatchableObject, boolean)}
     */
    public void setObject(int index, WrappedWatchableObject value) {
        setObject(index, value.getRawValue(), false);
    }

    /**
     * Sets the DataWatcher Item associated with a given watcher object to a new value.
     * 
     * @param object Associated watcher object
     * @param value Wrapped value
     * @param update Whether or not to inform the client
     * 
     * @see {@link #setObject(WrappedDataWatcherObject, Object)}
     */
    public void setObject(WrappedDataWatcherObject object, WrappedWatchableObject value, boolean update) {
        setObject(object, value.getRawValue(), update);
    }

    /**
     * Shortcut for {@link #setObject(WrappedDataWatcherObject, WrappedWatchableObject, boolean)}
     */
    public void setObject(WrappedDataWatcherObject object, WrappedWatchableObject value) {
        setObject(object, value.getRawValue(), false);
    }

    /**
     * Sets the DataWatcher Item associated with a given watcher object to a
     * new value. If there is not already an object at this index, the
     * specified watcher object must have a serializer.
     * 
     * @param object Associated watcher object
     * @param value New value
     * 
     * @throws IllegalArgumentException If the watcher object is null or must
     *          have a serializer and does not have one.
     */
    public void setObject(WrappedDataWatcherObject object, Object value, boolean update) {
        Validate.notNull(object, "Watcher object cannot be null!");

        if (SETTER == null && REGISTER == null) {
            FuzzyReflection fuzzy = FuzzyReflection.fromClass(handleType, true);
            FuzzyMethodContract contract = FuzzyMethodContract.newBuilder().banModifier(Modifier.STATIC)
                    .requireModifier(Modifier.PUBLIC).parameterExactArray(object.getHandleType(), Object.class)
                    .build();
            List<Method> methods = fuzzy.getMethodList(contract);
            for (Method method : methods) {
                if (method.getName().equals("set") || method.getName().equals("watch")) {
                    SETTER = Accessors.getMethodAccessor(method);
                } else {
                    REGISTER = Accessors.getMethodAccessor(method);
                }
            }
        }

        // Unwrap the object
        value = WrappedWatchableObject.getUnwrapped(value);

        if (hasIndex(object.getIndex())) {
            SETTER.invoke(handle, object.getHandle(), value);
        } else {
            object.checkSerializer();
            REGISTER.invoke(handle, object.getHandle(), value);
        }

        if (update) {
            getWatchableObject(object.getIndex()).setDirtyState(update);
        }
    }

    /**
     * Shortcut for {@link #setObject(WrappedDataWatcherObject, Object, boolean)}
     */
    public void setObject(WrappedDataWatcherObject object, Object value) {
        setObject(object, value, false);
    }

    // ---- Utility Methods

    /**
     * Clone the content of the current DataWatcher.
     * 
     * @return A cloned data watcher.
     */
    public WrappedDataWatcher deepClone() {
        WrappedDataWatcher clone = new WrappedDataWatcher(getEntity());

        if (MinecraftReflection.watcherObjectExists()) {
            for (WrappedWatchableObject wrapper : this) {
                clone.setObject(wrapper.getWatcherObject(), wrapper);
            }
        } else {
            for (WrappedWatchableObject wrapper : this) {
                clone.setObject(wrapper.getIndex(), wrapper);
            }
        }

        return clone;
    }

    /**
     * Retrieve the data watcher associated with an entity.
     * 
     * @param entity - the entity to read from.
     * @return Associated data watcher.
     */
    public static WrappedDataWatcher getEntityWatcher(Entity entity) {
        if (ENTITY_DATA_FIELD == null) {
            ENTITY_DATA_FIELD = Accessors.getFieldAccessor(MinecraftReflection.getEntityClass(),
                    MinecraftReflection.getDataWatcherClass(), true);
        }

        BukkitUnwrapper unwrapper = new BukkitUnwrapper();
        Object handle = ENTITY_DATA_FIELD.get(unwrapper.unwrapItem(entity));
        return handle != null ? new WrappedDataWatcher(handle) : null;
    }

    /**
     * Retrieve the entity associated with this data watcher.
     * @return The entity, or NULL.
     */
    public Entity getEntity() {
        if (ENTITY_FIELD == null) {
            ENTITY_FIELD = Accessors.getFieldAccessor(HANDLE_TYPE, MinecraftReflection.getEntityClass(), true);
        }

        return (Entity) MinecraftReflection.getBukkitEntity(ENTITY_FIELD.get(handle));
    }

    /**
     * Set the entity associated with this data watcher.
     * @param entity - the new entity.
     */
    public void setEntity(Entity entity) {
        if (ENTITY_FIELD == null) {
            ENTITY_FIELD = Accessors.getFieldAccessor(HANDLE_TYPE, MinecraftReflection.getEntityClass(), true);
        }

        ENTITY_FIELD.set(handle, BukkitUnwrapper.getInstance().unwrapItem(entity));
    }

    private static final ImmutableBiMap<Class<?>, Integer> CLASS_TO_ID = new ImmutableBiMap.Builder<Class<?>, Integer>()
            .put(Byte.class, 0).put(Short.class, 1).put(Integer.class, 2).put(Float.class, 3).put(String.class, 4)
            .put(MinecraftReflection.getItemStackClass(), 5).put(MinecraftReflection.getBlockPositionClass(), 6)
            .put(Vector3F.getMinecraftClass(), 7).build();

    /**
     * Retrieves the type ID associated with a given class. No longer supported
     * in 1.9 and up due to the removal of type IDs.
     * 
     * @param clazz Class to find ID for
     * @return The ID, or null if not found
     */
    public static Integer getTypeID(Class<?> clazz) {
        return CLASS_TO_ID.get(clazz);
    }

    /**
     * Retrieves the class associated with a given type ID. No longer
     * supported in 1.9 and up due to the removal of type IDs.
     * 
     * @param typeID ID to find Class for
     * @return The Class, or null if not found
     */
    public static Class<?> getTypeClass(int typeID) {
        return CLASS_TO_ID.inverse().get(typeID);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this)
            return true;
        if (obj == null)
            return false;

        if (obj instanceof WrappedDataWatcher) {
            WrappedDataWatcher other = (WrappedDataWatcher) obj;
            Iterator<WrappedWatchableObject> first = iterator(), second = other.iterator();

            // Make sure they're the same size
            if (size() != other.size())
                return false;

            for (; first.hasNext() && second.hasNext();) {
                // See if the two elements are equal
                if (!first.next().equals(second.next()))
                    return false;
            }

            return true;
        }

        return false;
    }

    @Override
    public int hashCode() {
        return getWatchableObjects().hashCode();
    }

    @Override
    public String toString() {
        return "WrappedDataWatcher[handle=" + handle + "]";
    }

    // ---- 1.9 classes

    /**
     * Represents a DataWatcherObject in 1.9. In order to register an object,
     * the serializer must be specified.
     * 
     * @author dmulloy2
     */
    public static class WrappedDataWatcherObject {
        private static final Class<?> HANDLE_TYPE = MinecraftReflection.getDataWatcherObjectClass();
        private static ConstructorAccessor constructor = null;
        private static MethodAccessor getSerializer = null;

        private StructureModifier<Object> modifier;
        private Object handle;

        protected WrappedDataWatcherObject() {
        }

        /**
         * Creates a new watcher object from a NMS handle.
         * 
         * @param handle The handle
         */
        public WrappedDataWatcherObject(Object handle) {
            this.handle = handle;
            this.modifier = new StructureModifier<Object>(HANDLE_TYPE).withTarget(handle);
        }

        /**
         * Creates a new watcher object from an index and serializer.
         * 
         * @param index Index
         * @param serializer Serializer, see {@link Registry}
         */
        public WrappedDataWatcherObject(int index, Serializer serializer) {
            this(newHandle(index, serializer));
        }

        static WrappedDataWatcherObject fromIndex(int index) {
            if (MinecraftReflection.watcherObjectExists()) {
                return new WrappedDataWatcherObject(index, null);
            } else {
                return new DummyWatcherObject(index);
            }
        }

        private static Object newHandle(int index, Serializer serializer) {
            if (constructor == null) {
                constructor = Accessors.getConstructorAccessor(HANDLE_TYPE.getConstructors()[0]);
            }

            Object handle = serializer != null ? serializer.getHandle() : null;
            return constructor.invoke(index, handle);
        }

        /**
         * Gets this watcher object's index.
         * 
         * @return The index
         */
        public int getIndex() {
            return (int) modifier.read(0);
        }

        /**
         * Gets this watcher object's serializer. Will return null if the
         * serializer was never specified.
         * 
         * @return The serializer, or null
         */
        public Serializer getSerializer() {
            if (getSerializer == null) {
                getSerializer = Accessors.getMethodAccessor(
                        FuzzyReflection.fromClass(HANDLE_TYPE, true).getMethodByParameters("getSerializer",
                                MinecraftReflection.getDataWatcherSerializerClass(), new Class[0]));
            }

            Object serializer = getSerializer.invoke(handle);
            if (serializer != null) {
                Serializer wrapper = Registry.fromHandle(serializer);
                if (wrapper != null) {
                    return wrapper;
                } else {
                    return new Serializer(null, serializer, false);
                }
            } else {
                return null;
            }
        }

        public void checkSerializer() {
            Validate.notNull(getSerializer(), "You must specify a serializer to register an object!");
        }

        public Object getHandle() {
            return handle;
        }

        public Class<?> getHandleType() {
            return HANDLE_TYPE;
        }

        @Override
        public String toString() {
            return "DataWatcherObject[index=" + getIndex() + ", serializer=" + getSerializer() + "]";
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == this)
                return true;

            if (obj instanceof WrappedDataWatcherObject) {
                WrappedDataWatcherObject other = (WrappedDataWatcherObject) obj;
                return handle.equals(other.handle);
            }

            return false;
        }

        @Override
        public int hashCode() {
            return handle.hashCode();
        }
    }

    private static class DummyWatcherObject extends WrappedDataWatcherObject {
        private final int index;

        public DummyWatcherObject(int index) {
            this.index = index;
        }

        @Override
        public int getIndex() {
            return index;
        }

        @Override
        public Serializer getSerializer() {
            return null;
        }

        @Override
        public Object getHandle() {
            return getIndex();
        }

        @Override
        public Class<?> getHandleType() {
            return int.class;
        }

        @Override
        public void checkSerializer() {
            // Do nothing
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == this)
                return true;
            if (obj == null)
                return false;

            if (obj instanceof DummyWatcherObject) {
                DummyWatcherObject that = (DummyWatcherObject) obj;
                return this.index == that.index;
            }

            return false;
        }
    }

    /**
     * Represents a DataWatcherSerializer in 1.9. If a Serializer is optional,
     * values must be wrapped in a {@link Optional}.
     * 
     * @author dmulloy2
     */
    public static class Serializer extends AbstractWrapper {
        private static final Class<?> HANDLE_TYPE = MinecraftReflection.getDataWatcherSerializerClass();

        private final Class<?> type;
        private final boolean optional;

        /**
         * Constructs a new Serializer
         * 
         * @param type Type it serializes
         * @param handle NMS handle
         * @param optional Whether or not it's {@link Optional}
         */
        public Serializer(Class<?> type, Object handle, boolean optional) {
            super(HANDLE_TYPE);
            this.type = type;
            this.optional = optional;

            setHandle(handle);
        }

        /**
         * Gets the type this serializer serializes.
         * 
         * @return The type
         */
        public Class<?> getType() {
            return type;
        }

        /**
         * Whether or not this serializer is optional, that is whether or not
         * the return type is wrapped in a {@link Optional}.
         * 
         * @return True if it is, false if not
         */
        public boolean isOptional() {
            return optional;
        }

        @Override
        public String toString() {
            return "Serializer[type=" + type + ", handle=" + handle + ", optional=" + optional + "]";
        }
    }

    /**
     * Represents a DataWatcherRegistry containing the supported
     * {@link Serializer}s in 1.9.
     *
     * <ul>
     *   <li>Byte</li>
     *   <li>Integer</li>
     *   <li>Float</li>
     *   <li>String</li>
     *   <li>IChatBaseComponent</li>
     *   <li>Optional&lt;ItemStack&gt;</li>
     *   <li>Optional&lt;IBlockData&gt;</li>
     *   <li>Boolean</li>
     *   <li>Vector3f</li>
     *   <li>BlockPosition</li>
     *   <li>Optional&lt;BlockPosition&gt;</li>
     *   <li>EnumDirection</li>
     *   <li>Optional&lt;UUID&gt;</li>
     * </ul>
     *
     * @author dmulloy2
     */
    public static class Registry {
        private static boolean INITIALIZED = false;
        private static List<Serializer> REGISTRY = new ArrayList<>();

        /**
         * Gets the first serializer associated with a given class.
         *
         * <p><b>Note</b>: If {@link Serializer#isOptional() the serializer is optional},
         *   values <i>must</i> be wrapped in an {@link Optional}.</p>
         *
         * <p>If there are multiple serializers for a given class (i.e. BlockPosition),
         *   you should use {@link #get(Class, boolean)} for more precision.</p>
         *
         * @param clazz Class to find serializer for
         * @return The serializer, or null if none exists
         */
        public static Serializer get(Class<?> clazz) {
            Validate.notNull("Class cannot be null!");
            initialize();

            for (Serializer serializer : REGISTRY) {
                if (serializer.getType().equals(clazz)) {
                    return serializer;
                }
            }

            return null;
        }

        /**
         * Gets the first serializer associated with a given class and optional state.
         * 
         * <p><b>Note</b>: If the serializer is optional, values <i>must<i> be
         * wrapped in an {@link Optional}
         *
         * @param clazz Class to find serializer for
         * @param optional Optional state
         * @return The serializer, or null if none exists
         */
        public static Serializer get(Class<?> clazz, boolean optional) {
            Validate.notNull(clazz, "Class cannot be null!");
            initialize();

            for (Serializer serializer : REGISTRY) {
                if (serializer.getType().equals(clazz) && serializer.isOptional() == optional) {
                    return serializer;
                }
            }

            return null;
        }

        /**
         * Gets the serializer associated with a given NMS handle.
         * @param handle The handle
         * @return The serializer, or null if none exists
         */
        public static Serializer fromHandle(Object handle) {
            Validate.notNull("handle cannot be null!");
            initialize();

            for (Serializer serializer : REGISTRY) {
                if (serializer.getHandle().equals(handle)) {
                    return serializer;
                }
            }

            return null;
        }

        private static void initialize() {
            if (!INITIALIZED) {
                INITIALIZED = true;
            } else {
                return;
            }

            List<Field> candidates = FuzzyReflection
                    .fromClass(MinecraftReflection.getDataWatcherRegistryClass(), true)
                    .getFieldListByType(MinecraftReflection.getDataWatcherSerializerClass());
            for (Field candidate : candidates) {
                Type generic = candidate.getGenericType();
                if (generic instanceof ParameterizedType) {
                    ParameterizedType type = (ParameterizedType) generic;
                    Type[] args = type.getActualTypeArguments();
                    Type arg = args[0];

                    Class<?> innerClass = null;
                    boolean optional = false;

                    if (arg instanceof Class<?>) {
                        innerClass = (Class<?>) arg;
                    } else if (arg instanceof ParameterizedType) {
                        innerClass = (Class<?>) ((ParameterizedType) arg).getActualTypeArguments()[0];
                        optional = true;
                    } else {
                        throw new IllegalStateException("Failed to find inner class of field " + candidate);
                    }

                    Object serializer;

                    try {
                        serializer = candidate.get(null);
                    } catch (ReflectiveOperationException e) {
                        throw new IllegalStateException("Failed to read field " + candidate);
                    }

                    REGISTRY.add(new Serializer(innerClass, serializer, optional));
                }
            }
        }

        // ---- Helper methods

        /**
         * Gets the serializer for IChatBaseComponents
         * @return The serializer
         */
        public static Serializer getChatComponentSerializer() {
            return get(MinecraftReflection.getIChatBaseComponentClass());
        }

        /**
         * Gets the serializer for ItemStacks
         * @param optional If true, objects <b>must</b> be wrapped in an {@link Optional}
         * @return The serializer
         */
        public static Serializer getItemStackSerializer(boolean optional) {
            return get(MinecraftReflection.getItemStackClass(), optional);
        }

        /**
         * Gets the serializer for BlockData
         * @param optional If true, objects <b>must</b> be wrapped in an {@link Optional}
         * @return The serializer
         */
        public static Serializer getBlockDataSerializer(boolean optional) {
            return get(MinecraftReflection.getIBlockDataClass(), optional);
        }

        /**
         * Gets the serializer for Vector3Fs
         * @return The serializer
         */
        public static Serializer getVectorSerializer() {
            return get(Vector3F.getMinecraftClass());
        }

        /**
         * Gets the serializer for BlockPositions
         * @param optional If true, objects <b>must</b> be wrapped in an {@link Optional}
         * @return The serializer
         */
        public static Serializer getBlockPositionSerializer(boolean optional) {
            return get(MinecraftReflection.getBlockPositionClass(), optional);
        }

        /**
         * Gets the serializer for Directions
         * @return The serializer
         */
        public static Serializer getDirectionSerializer() {
            return get(EnumWrappers.getDirectionClass());
        }

        /**
         * Gets the serializer for UUIDs
         * @param optional If true, objects <b>must</b> be wrapped in an {@link Optional}
         * @return The serializer
         */
        public static Serializer getUUIDSerializer(boolean optional) {
            return get(UUID.class, optional);
        }
    }
}