package com.comphenix.protocol.wrappers; import com.comphenix.protocol.reflect.EquivalentConverter; import com.comphenix.protocol.reflect.FuzzyReflection; import com.comphenix.protocol.reflect.StructureModifier; import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract; import com.comphenix.protocol.utility.MinecraftReflection; import com.comphenix.protocol.utility.MinecraftVersion; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import javax.annotation.Nonnull; import java.lang.reflect.Constructor; import java.util.UUID; import java.util.function.Supplier; /** * Represents a wrapper around a AttributeModifier. *

* This is used to compute the final attribute value. * * @author Kristian */ public class WrappedAttributeModifier extends AbstractWrapper { private static final boolean OPERATION_ENUM = MinecraftVersion.VILLAGE_UPDATE.atOrAbove(); private static final Class OPERATION_CLASS; private static final EquivalentConverter OPERATION_CONVERTER; private static class IndexedEnumConverter> implements EquivalentConverter { private final Class specificClass; private final Class genericClass; private IndexedEnumConverter(Class specificClass, Class genericClass) { this.specificClass = specificClass; this.genericClass = genericClass; } @Override public Object getGeneric(T specific) { int ordinal = specific.ordinal(); for (Object elem : genericClass.getEnumConstants()) { if (((Enum) elem).ordinal() == ordinal) { return elem; } } return null; } @Override public T getSpecific(Object generic) { int ordinal = ((Enum) generic).ordinal(); for (T elem : specificClass.getEnumConstants()) { if (elem.ordinal() == ordinal) { return elem; } } return null; } @Override public Class getSpecificType() { return specificClass; } } static { OPERATION_CLASS = OPERATION_ENUM ? MinecraftReflection.getMinecraftClass( "world.entity.ai.attributes.AttributeModifier$Operation", "AttributeModifier$Operation" ) : null; OPERATION_CONVERTER = OPERATION_ENUM ? new IndexedEnumConverter<>(Operation.class, OPERATION_CLASS) : null; } /** * Represents the different modifier operations. *

* The final value is computed as follows: *

    *
  1. Set X = base value.
  2. *
  3. Execute all modifiers with {@link Operation#ADD_NUMBER}. *
  4. Set Y = X.
  5. *
  6. Execute all modifiers with {@link Operation#MULTIPLY_PERCENTAGE}.
  7. *
  8. Execute all modifiers with {@link Operation#ADD_PERCENTAGE}.
  9. *
  10. Y is the final value.
  11. *
* @author Kristian */ public enum Operation { /** * Increment X by amount. */ ADD_NUMBER(0), /** * Increment Y by X * amount. */ MULTIPLY_PERCENTAGE(1), /** * Multiply Y by (1 + amount) */ ADD_PERCENTAGE(2); private final int id; Operation(int id) { this.id = id; } /** * Retrieve the unique operation ID. * @return Operation ID. */ public int getId() { return id; } /** * Retrieve the associated operation from an ID. * @param id - the ID. * @return The operation. */ 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."); } } // Shared structure modifier private static StructureModifier BASE_MODIFIER; // The constructor we are interested in private static Constructor ATTRIBUTE_MODIFIER_CONSTRUCTOR; // A modifier for the wrapped handler protected StructureModifier modifier; // Cached values private final UUID uuid; private final Supplier name; private final Operation operation; private final double amount; /** * Construct a new wrapped attribute modifier with no associated handle. *

* Note that the handle object is not initialized after this constructor. * @param uuid - the UUID. * @param name - the human readable name. * @param amount - the amount. * @param operation - the operation. */ protected WrappedAttributeModifier(UUID uuid, String name, double amount, Operation operation) { super(MinecraftReflection.getAttributeModifierClass()); // Use the supplied values instead of reading from the NMS instance this.uuid = uuid; this.name = () -> name; this.amount = amount; this.operation = operation; } /** * Construct an attribute modifier wrapper around a given NMS instance. * @param handle - the NMS instance. */ @SuppressWarnings("unchecked") protected WrappedAttributeModifier(@Nonnull Object handle) { // Update handle and modifier super(MinecraftReflection.getAttributeModifierClass()); setHandle(handle); initializeModifier(handle); // Load final values, caching them this.uuid = (UUID) modifier.withType(UUID.class).read(0); StructureModifier stringMod = modifier.withType(String.class); if (stringMod.size() == 0) { Supplier supplier = (Supplier) modifier.withType(Supplier.class).read(0); this.name = supplier; } else { this.name = () -> stringMod.read(0); } this.amount = (Double) modifier.withType(double.class).read(0); if (OPERATION_ENUM) { this.operation = modifier.withType(OPERATION_CLASS, OPERATION_CONVERTER).readSafely(0); } else { this.operation = Operation.fromId((Integer) modifier.withType(int.class).readSafely(0)); } } /** * Construct an attribute modifier wrapper around a NMS instance. * @param handle - the NMS instance. * @param uuid - the UUID. * @param name - the human readable name. * @param amount - the amount. * @param operation - the operation. */ protected WrappedAttributeModifier(@Nonnull Object handle, UUID uuid, String name, double amount, Operation operation) { this(uuid, name, amount, operation); // Initialize handle and modifier setHandle(handle); initializeModifier(handle); } /** * Construct a new attribute modifier builder. *

* It will automatically be supplied with a random UUID. * @return The new builder. */ public static Builder newBuilder() { return new Builder(null).uuid(UUID.randomUUID()); } /** * Construct a new attribute modifier builder with the given UUID. * @param id - the new UUID. * @return Thew new builder. */ public static Builder newBuilder(UUID id) { return new Builder(null).uuid(id); } /** * Construct a new wrapped attribute modifier builder initialized to the values from a template. * @param template - the attribute modifier template. * @return The new builder. */ public static Builder newBuilder(@Nonnull WrappedAttributeModifier template) { return new Builder(Preconditions.checkNotNull(template, "template cannot be NULL.")); } /** * Construct an attribute modifier wrapper around a given NMS instance. * @param handle - the NMS instance. * @return The created attribute modifier. * @throws IllegalArgumentException If the handle is not an AttributeModifier. */ public static WrappedAttributeModifier fromHandle(@Nonnull Object handle) { return new WrappedAttributeModifier(handle); } /** * Initialize modifier from a given handle. * @param handle - the handle. */ private void initializeModifier(@Nonnull Object handle) { // Initialize modifier if (BASE_MODIFIER == null) { BASE_MODIFIER = new StructureModifier<>(MinecraftReflection.getAttributeModifierClass()); } this.modifier = BASE_MODIFIER.withTarget(handle); } /** * Retrieve the unique UUID that identifies the origin of this modifier. * @return The unique UUID. */ public UUID getUUID() { return uuid; } /** * Retrieve a human readable name of this modifier. *

* Note that this will be "Unknown synced attribute modifier" on the client side. * @return The attribute key. */ public String getName() { return name.get(); } /** * Retrieve the operation that is used to compute the final attribute value. * @return The operation. */ public Operation getOperation() { return operation; } /** * Retrieve the amount to modify in the operation. * @return The amount. */ public double getAmount() { return amount; } /** * Retrieve the underlying attribute modifier. * @return The underlying modifier. */ public Object getHandle() { return handle; } /** * Set whether the modifier is pending synchronization with the client. *

* This value will be disregarded for {@link #equals(Object)}. * @param pending - TRUE if it is pending, FALSE otherwise. */ public void setPendingSynchronization(boolean pending) { modifier.withType(boolean.class).write(0, pending); } /** * Whether the modifier is pending synchronization with the client. * @return TRUE if it is, FALSE otherwise. */ public boolean isPendingSynchronization() { return (Boolean) modifier.withType(boolean.class).optionRead(0).orElse(false); } /** * Determine if a given modifier is equal to the current modifier. *

* Two modifiers are considered equal if they use the same UUID. * @param obj - the object to check against. * @return TRUE if the given object is the same, FALSE otherwise. */ public boolean equals(Object obj) { if (obj == this) return true; if (obj instanceof WrappedAttributeModifier) { WrappedAttributeModifier other = (WrappedAttributeModifier) obj; // Ensure they are equal return Objects.equal(uuid, other.getUUID()); } return false; } @Override public int hashCode() { return uuid != null ? uuid.hashCode() : 0; } @Override public String toString() { return "[amount=" + amount + ", operation=" + operation + ", name='" + name + "', id=" + uuid + ", serialize=" + isPendingSynchronization() + "]"; } /** * Represents a builder of attribute modifiers. *

* Use {@link WrappedAttributeModifier#newBuilder()} to construct an instance of the builder. * @author Kristian */ public static class Builder { private Operation operation = Operation.ADD_NUMBER; private String name = "Unknown"; private double amount; private UUID uuid; private Builder(WrappedAttributeModifier template) { if (template != null) { operation = template.getOperation(); name = template.getName(); amount = template.getAmount(); uuid = template.getUUID(); } } /** * Set the unique UUID that identifies the origin of this modifier. *

* This parameter is automatically supplied with a random UUID, or the * UUID from an attribute modifier to clone. * * @param uuid - the uuid to supply to the new object. * @return This builder, for chaining. */ public Builder uuid(@Nonnull UUID uuid) { this.uuid = Preconditions.checkNotNull(uuid, "uuid cannot be NULL."); return this; } /** * Set the operation that is used to compute the final attribute value. * * @param operation - the operation to supply to the new object. * @return This builder, for chaining. */ public Builder operation(@Nonnull Operation operation) { this.operation = Preconditions.checkNotNull(operation, "operation cannot be NULL."); return this; } /** * Set a human readable name of this modifier. * @param name - the name of the modifier. * @return This builder, for chaining. */ public Builder name(@Nonnull String name) { this.name = Preconditions.checkNotNull(name, "name cannot be NULL."); return this; } /** * Set the amount to modify in the operation. * * @param amount - the amount to supply to the new object. * @return This builder, for chaining. */ public Builder amount(double amount) { this.amount = WrappedAttribute.checkDouble(amount); return this; } /** * Construct a new attribute modifier and its wrapper using the supplied values in this builder. * @return The new attribute modifier. * @throws NullPointerException If UUID has not been set. * @throws RuntimeException If we are unable to construct the underlying attribute modifier. */ public WrappedAttributeModifier build() { Preconditions.checkNotNull(uuid, "uuid cannot be NULL."); // Retrieve the correct constructor if (ATTRIBUTE_MODIFIER_CONSTRUCTOR == null) { ATTRIBUTE_MODIFIER_CONSTRUCTOR = getConstructor(); } // Construct it try { // No need to read these values with a modifier return new WrappedAttributeModifier( ATTRIBUTE_MODIFIER_CONSTRUCTOR.newInstance( uuid, name, amount, getOperationParam(operation)), uuid, name, amount, operation ); } catch (Exception e) { throw new RuntimeException("Cannot construct AttributeModifier.", e); } } } private static Object getOperationParam(Operation operation) { return OPERATION_ENUM ? OPERATION_CONVERTER.getGeneric(operation) : operation.getId(); } private static Constructor getConstructor() { FuzzyMethodContract.Builder builder = FuzzyMethodContract .newBuilder() .parameterCount(4) .parameterDerivedOf(UUID.class, 0) .parameterExactType(String.class, 1) .parameterExactType(double.class, 2); if (OPERATION_ENUM) { builder = builder.parameterExactType(OPERATION_CLASS, 3); } else { builder = builder.parameterExactType(int.class, 3); } Constructor ret = FuzzyReflection.fromClass(MinecraftReflection.getAttributeModifierClass(), true) .getConstructor(builder.build()); ret.setAccessible(true); return ret; } }