package com.comphenix.protocol.wrappers; import com.comphenix.protocol.PacketType; import com.comphenix.protocol.events.PacketContainer; 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.comphenix.protocol.wrappers.collection.CachedSet; import com.comphenix.protocol.wrappers.collection.ConvertedSet; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.collect.Sets; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.lang.reflect.Constructor; import java.util.*; /** * Represents a single attribute sent in packet 44. * * @author Kristian */ public class WrappedAttribute extends AbstractWrapper { public static boolean KEY_WRAPPED = MinecraftVersion.NETHER_UPDATE.atOrAbove(); // Shared structure modifier private static StructureModifier ATTRIBUTE_MODIFIER; // The one constructor private static Constructor ATTRIBUTE_CONSTRUCTOR; /** * Remaps attributes if needed. * * @return the remapped attribute or the attribute itself if no remapping was necessary. */ private static String remap(String string) { switch (string) { case "generic.maxHealth": return "generic.max_health"; case "generic.followRange": return "generic.follow_range"; case "generic.knockbackResistance": return "generic.knockback_resistance"; case "generic.movementSpeed": return "generic.movement_speed"; case "generic.attackDamage": return "generic.attack_damage"; case "generic.attackSpeed": return "generic.attack_speed"; case "generic.armorToughness": return "generic.armor_toughness"; case "generic.attackKnockback": return "generic.attack_knockback"; case "horse.jumpStrength": return "horse.jump_strength"; case "zombie.spawnReinforcements": return "zombie.spawn_reinforcements"; default: return string; } } /** * Reference to the underlying attribute snapshot. */ protected Object handle; protected StructureModifier modifier; // Cached computed value private double computedValue = Double.NaN; // Cached modifiers list private Set attributeModifiers; /** * Construct a wrapper around a specific NMS instance. * * @param handle - the NMS instance. */ private WrappedAttribute(@Nonnull Object handle) { super(MinecraftReflection.getAttributeSnapshotClass()); setHandle(handle); // Initialize modifier if (ATTRIBUTE_MODIFIER == null) { ATTRIBUTE_MODIFIER = new StructureModifier<>(MinecraftReflection.getAttributeSnapshotClass()); } this.modifier = ATTRIBUTE_MODIFIER.withTarget(handle); } /** * Construct a new wrapped attribute around a specific NMS instance. * * @param handle - handle to a NMS AttributeSnapshot. * @return The attribute wrapper. * @throws IllegalArgumentException If the handle is not a AttributeSnapshot. */ public static WrappedAttribute fromHandle(@Nonnull Object handle) { return new WrappedAttribute(handle); } /** * Construct a new wrapped attribute builder. * * @return The new builder. */ public static Builder newBuilder() { return new Builder(null); } /** * Construct a new wrapped attribute builder initialized to the values from a template. * * @param template - the attribute template. * @return The new builder. */ public static Builder newBuilder(@Nonnull WrappedAttribute template) { return new Builder(Preconditions.checkNotNull(template, "template cannot be NULL.")); } public static class WrappedAttributeBase { public double defaultValue; public boolean unknown; public String key; } private static final Class ATTRIBUTE_BASE_CLASS = MinecraftReflection.getNullableNMS( "world.entity.ai.attributes.AttributeBase", "world.entity.ai.attributes.Attribute", "AttributeBase" ); private static final AutoWrapper ATTRIBUTE_BASE = AutoWrapper.wrap( WrappedAttributeBase.class, ATTRIBUTE_BASE_CLASS ); /** * Retrieve the unique attribute key that identifies its function. *

* Example: "generic.maxHealth" * * @return The attribute key. */ public String getAttributeKey() { if (KEY_WRAPPED) { WrappedAttributeBase base = modifier.withType(ATTRIBUTE_BASE_CLASS, ATTRIBUTE_BASE).read(0); return base.key.replace("attribute.name.", ""); // TODO not entirely sure why this happens } else { return (String) modifier.withType(String.class).read(0); } } /** * Retrieve the attribute base instance. New in 1.16. * * @return The attribute base */ public WrappedAttributeBase getBase() { return modifier.withType(WrappedAttributeBase.class, ATTRIBUTE_BASE).readSafely(0); } /** * Retrieve the base value of this attribute, before any of the modifiers have been taken into account. * * @return The base value. */ public double getBaseValue() { return (Double) modifier.withType(double.class).read(0); } /** * Retrieve the final computed value. * * @return The final value. */ public double getFinalValue() { if (Double.isNaN(computedValue)) { computedValue = computeValue(); } return computedValue; } /** * Retrieve the parent update attributes packet. * * @return The parent packet. * @deprecated Removed in 1.17 */ @Nullable @Deprecated public PacketContainer getParentPacket() { if (MinecraftVersion.CAVES_CLIFFS_1.atOrAbove()) { return null; } return new PacketContainer( PacketType.Play.Server.UPDATE_ATTRIBUTES, modifier.withType(MinecraftReflection.getPacketClass()).read(0) ); } /** * Determine if the attribute has a given attribute modifier, identified by UUID. * * @param id - the id to check for. * @return TRUE if it does, FALSE otherwise. */ public boolean hasModifier(UUID id) { return getModifiers().contains(WrappedAttributeModifier.newBuilder(id).build()); } /** * Retrieve an attribute modifier by UUID. * * @param id - the id to look for. * @return The single attribute modifier with the given ID. */ public WrappedAttributeModifier getModifierByUUID(UUID id) { if (hasModifier(id)) { for (WrappedAttributeModifier modifier : getModifiers()) { if (Objects.equal(modifier.getUUID(), id)) { return modifier; } } } return null; } /** * Retrieve an immutable set of all the attribute modifiers that will compute the final value of this attribute. * * @return Every attribute modifier. */ public Set getModifiers() { if (attributeModifiers == null) { @SuppressWarnings("unchecked") Collection collection = (Collection) modifier.withType(Collection.class).read(0); // Convert to an equivalent wrapper ConvertedSet converted = new ConvertedSet(getSetSafely(collection)) { @Override protected Object toInner(WrappedAttributeModifier outer) { return outer.getHandle(); } @Override protected WrappedAttributeModifier toOuter(Object inner) { return WrappedAttributeModifier.fromHandle(inner); } }; attributeModifiers = new CachedSet<>(converted); } return Collections.unmodifiableSet(attributeModifiers); } /** * Construct an attribute with the same key and name, but a different list of modifiers. * * @param modifiers - attribute modifiers. * @return The new attribute. */ public WrappedAttribute withModifiers(Collection modifiers) { return newBuilder(this).modifiers(modifiers).build(); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj instanceof WrappedAttribute) { WrappedAttribute other = (WrappedAttribute) obj; if (getBaseValue() == other.getBaseValue() && Objects.equal(getAttributeKey(), other.getAttributeKey())) { return other.getModifiers().containsAll(getModifiers()); } } return false; } @Override public int hashCode() { if (attributeModifiers == null) getModifiers(); return Objects.hashCode(getAttributeKey(), getBaseValue(), attributeModifiers); } /** * Compute the final value from the current attribute modifers. * * @return The final value. */ private double computeValue() { Collection modifiers = getModifiers(); double x = getBaseValue(); double y = 0; // Compute each phase for (int phase = 0; phase < 3; phase++) { for (WrappedAttributeModifier modifier : modifiers) { if (modifier.getOperation().getId() == phase) { switch (phase) { case 0: // Adding phase x += modifier.getAmount(); break; case 1: // Multiply percentage y += x * modifier.getAmount(); break; case 2: y *= 1 + modifier.getAmount(); break; default: throw new IllegalStateException("Unknown phase: " + phase); } } } // The additive phase is finished if (phase == 0) { y = x; } } return y; } @Override public String toString() { return "WrappedAttribute[key=" + getAttributeKey() + ", base=" + getBaseValue() + ", final=" + getFinalValue() + ", modifiers=" + getModifiers() + "]"; } /** * If the collection is a set, retrieve it - otherwise, create a new set with the same elements. * * @param collection - the collection. * @return A set with the same elements. */ private static Set getSetSafely(Collection collection) { return collection instanceof Set ? (Set) collection : Sets.newHashSet(collection); } /** * Ensure that the given double is not infinite nor NaN. * * @param value - the value to check. */ static double checkDouble(double value) { if (Double.isInfinite(value)) throw new IllegalArgumentException("value cannot be infinite."); if (Double.isNaN(value)) throw new IllegalArgumentException("value cannot be NaN."); return value; } /** * Represents a builder for wrapped attributes. *

* Use {@link WrappedAttribute#newBuilder()} to construct it. * * @author Kristian */ public static class Builder { private double baseValue = Double.NaN; private String attributeKey; private PacketContainer packet; private Collection modifiers = Collections.emptyList(); private Builder(WrappedAttribute template) { if (template != null) { baseValue = template.getBaseValue(); attributeKey = template.getAttributeKey(); packet = template.getParentPacket(); modifiers = template.getModifiers(); } } /** * Change the base value of the attribute. *

* The modifiers will automatically supply a value if this is unset. * * @param baseValue - the base value value. * @return This builder, for chaining. */ public Builder baseValue(double baseValue) { this.baseValue = checkDouble(baseValue); return this; } /** * Set the unique attribute key that identifies its function. *

* This is required. * * @param attributeKey - the unique attribute key. * @return This builder, for chaining. */ public Builder attributeKey(String attributeKey) { this.attributeKey = Preconditions.checkNotNull(attributeKey, "attributeKey cannot be NULL."); return this; } /** * Set the modifers that will be supplied to the client, and used to compute the final value. * * @param modifiers - the attribute modifiers. * @return This builder, for chaining. */ public Builder modifiers(Collection modifiers) { this.modifiers = Preconditions.checkNotNull(modifiers, "modifiers cannot be NULL - use an empty list instead."); return this; } /** * Set the parent update attributes packet (44). * * @param packet - the parent packet. * @return This builder, for chaining. */ public Builder packet(PacketContainer packet) { if (Preconditions.checkNotNull(packet, "packet cannot be NULL").getType() != PacketType.Play.Server.UPDATE_ATTRIBUTES) { throw new IllegalArgumentException("Packet must be UPDATE_ATTRIBUTES (44)"); } this.packet = packet; return this; } /** * Retrieve the unwrapped modifiers. * * @return Unwrapped modifiers. */ private Set getUnwrappedModifiers() { final Set output = new HashSet<>(); for (WrappedAttributeModifier modifier : modifiers) { output.add(modifier.getHandle()); } return output; } /** * Build a new wrapped attribute with the values of this builder. * * @return The wrapped attribute. * @throws RuntimeException If anything went wrong with the reflection. */ public WrappedAttribute build() { Preconditions.checkNotNull(attributeKey, "attributeKey cannot be NULL."); // Remember to set the base value if (Double.isNaN(baseValue)) { throw new IllegalStateException("Base value has not been set."); } boolean isStatic = MinecraftVersion.CAVES_CLIFFS_1.atOrAbove(); if (ATTRIBUTE_CONSTRUCTOR == null) { FuzzyReflection ref = FuzzyReflection.fromClass(MinecraftReflection.getAttributeSnapshotClass(), true); FuzzyMethodContract.Builder contract = FuzzyMethodContract.newBuilder().parameterCount(isStatic ? 3 : 4); if (!isStatic) { contract.parameterDerivedOf(MinecraftReflection.getPacketClass(), 0); } contract.parameterExactType(double.class).parameterDerivedOf(Collection.class); ATTRIBUTE_CONSTRUCTOR = ref.getConstructor(contract.build()); // Just in case ATTRIBUTE_CONSTRUCTOR.setAccessible(true); } Object attributeKey; if (KEY_WRAPPED) { WrappedRegistry registry = WrappedRegistry.getAttributeRegistry(); String strKey = remap(this.attributeKey); attributeKey = registry.get(strKey); if (attributeKey == null) { throw new IllegalArgumentException("Invalid attribute name: " + this.attributeKey); } } else { attributeKey = this.attributeKey; } try { Object handle; if (isStatic) { handle = ATTRIBUTE_CONSTRUCTOR.newInstance(attributeKey, baseValue, getUnwrappedModifiers()); } else { handle = ATTRIBUTE_CONSTRUCTOR.newInstance(packet.getHandle(), attributeKey, baseValue, getUnwrappedModifiers()); } return new WrappedAttribute(handle); } catch (Exception e) { throw new RuntimeException("Cannot construct AttributeSnapshot.", e); } } } }