473 lines
16 KiB
Java
473 lines
16 KiB
Java
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.
|
|
* <p>
|
|
* 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> OPERATION_CONVERTER;
|
|
|
|
private static class IndexedEnumConverter<T extends Enum<T>> implements EquivalentConverter<T> {
|
|
private final Class<T> specificClass;
|
|
private final Class<?> genericClass;
|
|
|
|
private IndexedEnumConverter(Class<T> 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<T> 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.
|
|
* <p>
|
|
* The final value is computed as follows:
|
|
* <ol>
|
|
* <li>Set X = base value.</li>
|
|
* <li>Execute all modifiers with {@link Operation#ADD_NUMBER}.
|
|
* <li>Set Y = X.</li>
|
|
* <li>Execute all modifiers with {@link Operation#MULTIPLY_PERCENTAGE}.</li>
|
|
* <li>Execute all modifiers with {@link Operation#ADD_PERCENTAGE}.</li>
|
|
* <li>Y is the final value.</li>
|
|
* </ol>
|
|
* @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<Object> BASE_MODIFIER;
|
|
|
|
// The constructor we are interested in
|
|
private static Constructor<?> ATTRIBUTE_MODIFIER_CONSTRUCTOR;
|
|
|
|
// A modifier for the wrapped handler
|
|
protected StructureModifier<Object> modifier;
|
|
|
|
// Cached values
|
|
private final UUID uuid;
|
|
private final Supplier<String> name;
|
|
private final Operation operation;
|
|
private final double amount;
|
|
|
|
/**
|
|
* Construct a new wrapped attribute modifier with no associated handle.
|
|
* <p>
|
|
* 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<String> stringMod = modifier.withType(String.class);
|
|
if (stringMod.size() == 0) {
|
|
Supplier<String> supplier = (Supplier<String>) 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.
|
|
* <p>
|
|
* 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.
|
|
* <p>
|
|
* 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.
|
|
* <p>
|
|
* 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.
|
|
* <p>
|
|
* 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.
|
|
* <p>
|
|
* 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.
|
|
* <p>
|
|
* 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;
|
|
}
|
|
}
|