diff --git a/src/main/java/com/comphenix/attribute/AttributeStorage.java b/src/main/java/com/comphenix/attribute/AttributeStorage.java
new file mode 100644
index 000000000..55f4fced5
--- /dev/null
+++ b/src/main/java/com/comphenix/attribute/AttributeStorage.java
@@ -0,0 +1,124 @@
+package com.comphenix.attribute;
+
+import java.util.UUID;
+
+import org.bukkit.inventory.ItemStack;
+
+import com.comphenix.attribute.Attributes.Attribute;
+import com.comphenix.attribute.Attributes.AttributeType;
+import com.comphenix.attribute.Attributes.Operation;
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+
+/**
+ * Store meta-data in an ItemStack as attributes.
+ * @author Kristian
+ */
+public class AttributeStorage {
+ private ItemStack target;
+ private final UUID uniqueKey;
+
+ private AttributeStorage(ItemStack target, UUID uniqueKey) {
+ this.target = Preconditions.checkNotNull(target, "target cannot be NULL");
+ this.uniqueKey = Preconditions.checkNotNull(uniqueKey, "uniqueKey cannot be NULL");
+ }
+
+ /**
+ * Construct a new attribute storage system.
+ *
+ * The key must be the same in order to retrieve the same data.
+ * @param target - the item stack where the data will be stored.
+ * @param uniqueKey - the unique key used to retrieve the correct data.
+ */
+ public static AttributeStorage newTarget(ItemStack target, UUID uniqueKey) {
+ return new AttributeStorage(target, uniqueKey);
+ }
+
+ /**
+ * Retrieve the data stored in the item's attribute.
+ * @param defaultValue - the default value to return if no data can be found.
+ * @return The stored data, or defaultValue if not found.
+ */
+ public String getData(String defaultValue) {
+ Attribute current = getAttribute(new Attributes(target), uniqueKey);
+ return current != null ? current.getName() : defaultValue;
+ }
+
+ /**
+ * Determine if we are storing any data.
+ * @return TRUE if we are, FALSE otherwise.
+ */
+ public boolean hasData() {
+ return getAttribute(new Attributes(target), uniqueKey) != null;
+ }
+
+ /**
+ * Set the data stored in the attributes.
+ * @param data - the data.
+ */
+ public void setData(String data) {
+ Attributes attributes = new Attributes(target);
+ Attribute current = getAttribute(attributes, uniqueKey);
+
+ if (current == null) {
+ attributes.add(
+ Attribute.newBuilder().
+ name(data).
+ amount(getBaseDamage(target)).
+ uuid(uniqueKey).
+ operation(Operation.ADD_NUMBER).
+ type(AttributeType.GENERIC_ATTACK_DAMAGE).
+ build()
+ );
+ } else {
+ current.setName(data);
+ }
+ this.target = attributes.getStack();
+ }
+
+ /**
+ * Retrieve the base damage of the given item.
+ * @param stack - the stack.
+ * @return The base damage.
+ */
+ private int getBaseDamage(ItemStack stack) {
+ // Yes - we have to hard code these values. Cannot use Operation.ADD_PERCENTAGE either.
+ switch (stack.getType()) {
+ case WOOD_SWORD: return 4;
+ case GOLD_SWORD: return 4;
+ case STONE_SWORD: return 5;
+ case IRON_SWORD: return 6;
+ case DIAMOND_SWORD: return 7;
+
+ case WOOD_AXE: return 3;
+ case GOLD_AXE: return 3;
+ case STONE_AXE: return 4;
+ case IRON_AXE: return 5;
+ case DIAMOND_AXE: return 6;
+ default: return 0;
+ }
+ }
+
+ /**
+ * Retrieve the target stack. May have been changed.
+ * @return The target stack.
+ */
+ public ItemStack getTarget() {
+ return target;
+ }
+
+ /**
+ * Retrieve an attribute by UUID.
+ * @param attributes - the attribute.
+ * @param id - the UUID to search for.
+ * @return The first attribute associated with this UUID, or NULL.
+ */
+ private Attribute getAttribute(Attributes attributes, UUID id) {
+ for (Attribute attribute : attributes.values()) {
+ if (Objects.equal(attribute.getUUID(), id)) {
+ return attribute;
+ }
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/comphenix/attribute/Attributes.java b/src/main/java/com/comphenix/attribute/Attributes.java
new file mode 100644
index 000000000..274b13cd3
--- /dev/null
+++ b/src/main/java/com/comphenix/attribute/Attributes.java
@@ -0,0 +1,337 @@
+package com.comphenix.attribute;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentMap;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.bukkit.inventory.ItemStack;
+
+import com.comphenix.attribute.NbtFactory.NbtCompound;
+import com.comphenix.attribute.NbtFactory.NbtList;
+import com.google.common.base.Function;
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Iterators;
+import com.google.common.collect.Maps;
+
+public class Attributes {
+ 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.");
+ }
+ }
+
+ public static class AttributeType {
+ private static ConcurrentMap 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.
+ *
+ * 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 values() {
+ return LOOKUP.values();
+ }
+ }
+
+ public static class Attribute {
+ private NbtCompound data;
+
+ public Attribute(Builder builder) {
+ data = NbtFactory.createCompound();
+ setAmount(builder.amount);
+ setOperation(builder.operation);
+ setAttributeType(builder.type);
+ setName(builder.name);
+ setUUID(builder.uuid);
+ }
+
+ private Attribute(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(double amount, Operation operation, AttributeType type, String name, UUID uuid) {
+ this.amount = amount;
+ this.operation = operation;
+ this.type = type;
+ this.name = name;
+ this.uuid = uuid;
+ }
+
+ 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);
+ }
+ }
+ }
+
+ // This may be modified
+ public ItemStack stack;
+ private NbtList attributes;
+
+ public Attributes(ItemStack stack) {
+ // Create a CraftItemStack (under the hood)
+ this.stack = NbtFactory.getCraftItemStack(stack);
+ loadAttributes(false);
+ }
+
+ /**
+ * Load the NBT list from the TAG compound.
+ * @param createIfMissing - create the list if its missing.
+ */
+ private void loadAttributes(boolean createIfMissing) {
+ if (this.attributes == null) {
+ NbtCompound nbt = NbtFactory.fromItemTag(this.stack);
+ this.attributes = nbt.getList("AttributeModifiers", createIfMissing);
+ }
+ }
+
+ /**
+ * Remove the NBT list from the TAG compound.
+ */
+ private void removeAttributes() {
+ NbtCompound nbt = NbtFactory.fromItemTag(this.stack);
+ nbt.remove("AttributeModifiers");
+ this.attributes = null;
+ }
+
+ /**
+ * 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 != null ? attributes.size() : 0;
+ }
+
+ /**
+ * 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.");
+ loadAttributes(true);
+ attributes.add(attribute.data);
+ }
+
+ /**
+ * Remove the first instance of the given attribute.
+ *
+ * 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) {
+ if (attributes == null)
+ return false;
+ UUID uuid = attribute.getUUID();
+
+ for (Iterator it = values().iterator(); it.hasNext(); ) {
+ if (Objects.equal(it.next().getUUID(), uuid)) {
+ it.remove();
+
+ // Last removed attribute?
+ if (size() == 0) {
+ removeAttributes();
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Remove every attribute.
+ */
+ public void clear() {
+ removeAttributes();
+ }
+
+ /**
+ * 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) {
+ if (size() == 0)
+ throw new IllegalStateException("Attribute list is empty.");
+ return new Attribute((NbtCompound) attributes.get(index));
+ }
+
+ // We can't make Attributes itself iterable without splitting it up into separate classes
+ public Iterable values() {
+ return new Iterable() {
+ @Override
+ public Iterator iterator() {
+ // Handle the empty case
+ if (size() == 0)
+ return Collections.emptyList().iterator();
+
+ return Iterators.transform(attributes.iterator(),
+ new Function