diff --git a/src/main/java/net/minestom/server/attribute/Attribute.java b/src/main/java/net/minestom/server/attribute/Attribute.java index 94a634a3a..b5d2168b5 100644 --- a/src/main/java/net/minestom/server/attribute/Attribute.java +++ b/src/main/java/net/minestom/server/attribute/Attribute.java @@ -1,53 +1,69 @@ package net.minestom.server.attribute; +import java.util.HashMap; +import java.util.Map; + import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public enum Attribute { +public class Attribute { - MAX_HEALTH("generic.max_health", 20, 1024), - FOLLOW_RANGE("generic.follow_range", 32, 2048), - KNOCKBACK_RESISTANCE("generic.knockback_resistance", 0, 1), - MOVEMENT_SPEED("generic.movement_speed", 0.25f, 1024), - ATTACK_DAMAGE("generic.attack_damage", 2, 2048), - ATTACK_SPEED("generic.attack_speed", 4, 1024), - FLYING_SPEED("generic.flying_speed", 0.4f, 1024), - ARMOR("generic.armor", 0, 30), - ARMOR_TOUGHNESS("generic.armor_toughness", 0, 20), - ATTACK_KNOCKBACK("generic.attack_knockback", 0, 5), - LUCK("generic.luck", 0, 1024), - HORSE_JUMP_STRENGTH("horse.jump_strength", 0.7f, 2), - ZOMBIE_SPAWN_REINFORCEMENTS("zombie.spawn_reinforcements", 0, 1); + private static final Map ATTRIBUTES = new HashMap<>(); - private final String key; - private final float defaultValue; - private final float maxVanillaValue; + private final String key; + private final float defaultValue; + private final float maxValue; + private final boolean shareWithClient; - Attribute(@NotNull String key, float defaultValue, float maxVanillaValue) { - this.key = key; - this.defaultValue = defaultValue; - this.maxVanillaValue = maxVanillaValue; - } + public Attribute(@NotNull String key, float defaultValue, float maxValue) { + this(key, false, defaultValue, maxValue); + } - @NotNull - public String getKey() { - return key; - } + public Attribute(@NotNull String key, boolean shareWithClient, float defaultValue, float maxValue) { + if (defaultValue > maxValue) { + throw new IllegalArgumentException("Default value cannot be greater than the maximum allowed"); + } + this.key = key; + this.shareWithClient = shareWithClient; + this.defaultValue = defaultValue; + this.maxValue = maxValue; + } - public float getDefaultValue() { - return defaultValue; - } + @NotNull + public String getKey() { + return key; + } - public float getMaxVanillaValue() { - return maxVanillaValue; - } + public float getDefaultValue() { + return defaultValue; + } - @Nullable - public static Attribute fromKey(@NotNull String key) { - for (Attribute attribute : values()) { - if (attribute.getKey().equals(key)) - return attribute; - } - return null; - } + public float getMaxValue() { + return maxValue; + } + + public boolean isShared() { + return shareWithClient; + } + + @NotNull + public Attribute register() { + ATTRIBUTES.put(key, this); + return this; + } + + @Nullable + public static Attribute fromKey(@NotNull String key) { + return ATTRIBUTES.get(key); + } + + @NotNull + public static Attribute[] values() { + return ATTRIBUTES.values().toArray(new Attribute[0]); + } + + @NotNull + public static Attribute[] sharedAttributes() { + return ATTRIBUTES.values().stream().filter(Attribute::isShared).toArray(Attribute[]::new); + } } diff --git a/src/main/java/net/minestom/server/attribute/AttributeInstance.java b/src/main/java/net/minestom/server/attribute/AttributeInstance.java new file mode 100644 index 000000000..a34aac776 --- /dev/null +++ b/src/main/java/net/minestom/server/attribute/AttributeInstance.java @@ -0,0 +1,94 @@ +package net.minestom.server.attribute; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.function.Consumer; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class AttributeInstance { + + private final Attribute attribute; + private final Map modifiers = new HashMap<>(); + private final Consumer propertyChangeListener; + private float baseValue; + private boolean dirty = true; + private float cachedValue = 0.0f; + + public AttributeInstance(@NotNull Attribute attribute, @Nullable Consumer listener) { + this.attribute = attribute; + this.propertyChangeListener = listener; + this.baseValue = attribute.getDefaultValue(); + } + + @NotNull + public Attribute getAttribute() { + return attribute; + } + + public float getBaseValue() { + return baseValue; + } + + private void setDirty() { + if (!dirty) { + dirty = true; + if (propertyChangeListener != null) { + propertyChangeListener.accept(this); + } + } + } + + public void setBaseValue(float baseValue) { + if (this.baseValue != baseValue) { + this.baseValue = baseValue; + setDirty(); + } + } + + public void addModifier(@NotNull AttributeModifier modifier) { + if (modifiers.putIfAbsent(modifier.getId(), modifier) == null) { + setDirty(); + } + } + + public void removeModifier(@NotNull AttributeModifier modifier) { + if (modifiers.remove(modifier.getId()) != null) { + setDirty(); + } + } + + public Collection getModifiers() { + return modifiers.values(); + } + + public float getValue() { + if (dirty) { + cachedValue = processModifiers(); + dirty = false; + } + return cachedValue; + } + + protected float processModifiers() { + float base = getBaseValue(); + + for (var modifier : modifiers.values().stream().filter(mod -> mod.getOperation() == AttributeOperation.ADDITION).toArray(AttributeModifier[]::new)) { + base += modifier.getAmount(); + } + + float result = base; + + for (var modifier : modifiers.values().stream().filter(mod -> mod.getOperation() == AttributeOperation.MULTIPLY_BASE).toArray(AttributeModifier[]::new)) { + result += (base * modifier.getAmount()); + } + for (var modifier : modifiers.values().stream().filter(mod -> mod.getOperation() == AttributeOperation.MULTIPLY_TOTAL).toArray(AttributeModifier[]::new)) { + result *= (1.0f + modifier.getAmount()); + } + + return Math.min(result, getAttribute().getMaxValue()); + } +} diff --git a/src/main/java/net/minestom/server/attribute/AttributeModifier.java b/src/main/java/net/minestom/server/attribute/AttributeModifier.java new file mode 100644 index 000000000..537603db5 --- /dev/null +++ b/src/main/java/net/minestom/server/attribute/AttributeModifier.java @@ -0,0 +1,45 @@ +package net.minestom.server.attribute; + +import java.util.UUID; + +import io.netty.util.internal.ThreadLocalRandom; +import net.minestom.server.utils.UniqueIdUtils; +import org.jetbrains.annotations.NotNull; + +public class AttributeModifier { + + private final float amount; + private final String name; + private final AttributeOperation operation; + private final UUID id; + + public AttributeModifier(@NotNull String name, float amount, @NotNull AttributeOperation operation) { + this(UniqueIdUtils.createRandomUUID(ThreadLocalRandom.current()), name, amount, operation); + } + + public AttributeModifier(@NotNull UUID id, @NotNull String name, float amount, @NotNull AttributeOperation operation) { + this.id = id; + this.name = name; + this.amount = amount; + this.operation = operation; + } + + @NotNull + public UUID getId() { + return id; + } + + @NotNull + public String getName() { + return name; + } + + public float getAmount() { + return amount; + } + + @NotNull + public AttributeOperation getOperation() { + return operation; + } +} diff --git a/src/main/java/net/minestom/server/attribute/Attributes.java b/src/main/java/net/minestom/server/attribute/Attributes.java new file mode 100644 index 000000000..07eb2ccc4 --- /dev/null +++ b/src/main/java/net/minestom/server/attribute/Attributes.java @@ -0,0 +1,18 @@ +package net.minestom.server.attribute; + +public final class Attributes +{ + public static final Attribute MAX_HEALTH = (new Attribute("generic.max_health", true, 20, 1024)).register(); + public static final Attribute FOLLOW_RANGE = (new Attribute("generic.follow_range", true, 32, 2048)).register(); + public static final Attribute KNOCKBACK_RESISTANCE = (new Attribute("generic.knockback_resistance", true, 0, 1)).register(); + public static final Attribute MOVEMENT_SPEED = (new Attribute("generic.movement_speed", true, 0.25f, 1024)).register(); + public static final Attribute ATTACK_DAMAGE = (new Attribute("generic.attack_damage", true, 2, 2048)).register(); + public static final Attribute ATTACK_SPEED = (new Attribute("generic.attack_speed", true, 4, 1024)).register(); + public static final Attribute FLYING_SPEED = (new Attribute("generic.flying_speed", true, 0.4f, 1024)).register(); + public static final Attribute ARMOR = (new Attribute("generic.armor", true, 0, 30)).register(); + public static final Attribute ARMOR_TOUGHNESS = (new Attribute("generic.armor_toughness", true, 0, 20)).register(); + public static final Attribute ATTACK_KNOCKBACK = (new Attribute("generic.attack_knockback", true, 0, 5)).register(); + public static final Attribute LUCK = (new Attribute("generic.luck", true, 0, 1024)).register(); + public static final Attribute HORSE_JUMP_STRENGTH = (new Attribute("horse.jump_strength", true, 0.7f, 2)).register(); + public static final Attribute ZOMBIE_SPAWN_REINFORCEMENTS = (new Attribute("zombie.spawn_reinforcements", true, 0, 1)).register(); +} diff --git a/src/main/java/net/minestom/server/entity/EntityCreature.java b/src/main/java/net/minestom/server/entity/EntityCreature.java index 8e1229281..601e7d087 100644 --- a/src/main/java/net/minestom/server/entity/EntityCreature.java +++ b/src/main/java/net/minestom/server/entity/EntityCreature.java @@ -3,7 +3,7 @@ package net.minestom.server.entity; import com.extollit.gaming.ai.path.HydrazinePathFinder; import com.extollit.gaming.ai.path.model.IPath; import net.minestom.server.MinecraftServer; -import net.minestom.server.attribute.Attribute; +import net.minestom.server.attribute.Attributes; import net.minestom.server.entity.ai.GoalSelector; import net.minestom.server.entity.ai.TargetSelector; import net.minestom.server.entity.pathfinding.PFPathingEntity; @@ -134,7 +134,7 @@ public abstract class EntityCreature extends LivingEntity { this.path = pathFinder.updatePathFor(pathingEntity); if (path != null) { - final float speed = getAttributeValue(Attribute.MOVEMENT_SPEED); + final float speed = getAttributeValue(Attributes.MOVEMENT_SPEED); final Position targetPosition = pathingEntity.getTargetPosition(); moveTowards(targetPosition, speed); } else { diff --git a/src/main/java/net/minestom/server/entity/LivingEntity.java b/src/main/java/net/minestom/server/entity/LivingEntity.java index db9f2643a..b50c7931b 100644 --- a/src/main/java/net/minestom/server/entity/LivingEntity.java +++ b/src/main/java/net/minestom/server/entity/LivingEntity.java @@ -1,6 +1,8 @@ package net.minestom.server.entity; import net.minestom.server.attribute.Attribute; +import net.minestom.server.attribute.AttributeInstance; +import net.minestom.server.attribute.Attributes; import net.minestom.server.collision.BoundingBox; import net.minestom.server.entity.damage.DamageType; import net.minestom.server.event.entity.EntityDamageEvent; @@ -26,9 +28,12 @@ import net.minestom.server.utils.validate.Check; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.HashMap; +import java.util.Map; import java.util.Set; import java.util.function.Consumer; +//TODO: Default attributes registration (and limitation ?) public abstract class LivingEntity extends Entity implements EquipmentHandler { // Item pickup @@ -44,7 +49,7 @@ public abstract class LivingEntity extends Entity implements EquipmentHandler { // Bounding box used for items' pickup (see LivingEntity#setBoundingBox) protected BoundingBox expandedBoundingBox; - private final float[] attributeValues = new float[Attribute.values().length]; + private final Map attributeModifiers = new HashMap<>(Attribute.values().length); private boolean isHandActive; private boolean offHand; @@ -369,41 +374,48 @@ public abstract class LivingEntity extends Entity implements EquipmentHandler { } /** - * Gets the entity max health from {@link #getAttributeValue(Attribute)} {@link Attribute#MAX_HEALTH}. + * Gets the entity max health from {@link #getAttributeValue(Attribute)} {@link Attributes#MAX_HEALTH}. * * @return the entity max health */ public float getMaxHealth() { - return getAttributeValue(Attribute.MAX_HEALTH); + return getAttributeValue(Attributes.MAX_HEALTH); } /** * Sets the heal of the entity as its max health. *

- * Retrieved from {@link #getAttributeValue(Attribute)} with the attribute {@link Attribute#MAX_HEALTH}. + * Retrieved from {@link #getAttributeValue(Attribute)} with the attribute {@link Attributes#MAX_HEALTH}. */ public void heal() { - setHealth(getAttributeValue(Attribute.MAX_HEALTH)); + setHealth(getAttributeValue(Attributes.MAX_HEALTH)); } /** - * Changes the specified attribute value to {@code value}. + * Retrieves the attribute instance and its modifiers. * - * @param attribute The attribute to change - * @param value the new value of the attribute + * @param attribute the attribute instance to get + * @return the attribute instance */ - public void setAttribute(@NotNull Attribute attribute, float value) { - this.attributeValues[attribute.ordinal()] = value; + @NotNull + public AttributeInstance getAttribute(@NotNull Attribute attribute) { + if (!attributeModifiers.containsKey(attribute.getKey())) { + attributeModifiers.put(attribute.getKey(), new AttributeInstance(attribute, this::onAttributeChanged)); + } + return attributeModifiers.get(attribute.getKey()); } + protected void onAttributeChanged(@NotNull AttributeInstance instance) { } + /** - * Retrieves the attribute value set by {@link #setAttribute(Attribute, float)}. + * Retrieves the attribute value. * * @param attribute the attribute value to get * @return the attribute value */ public float getAttributeValue(@NotNull Attribute attribute) { - return this.attributeValues[attribute.ordinal()]; + AttributeInstance instance = attributeModifiers.get(attribute.getKey()); + return (instance != null) ? instance.getValue() : attribute.getDefaultValue(); } /** @@ -488,15 +500,16 @@ public abstract class LivingEntity extends Entity implements EquipmentHandler { EntityPropertiesPacket propertiesPacket = new EntityPropertiesPacket(); propertiesPacket.entityId = getEntityId(); - final int length = Attribute.values().length; - EntityPropertiesPacket.Property[] properties = new EntityPropertiesPacket.Property[length]; - for (int i = 0; i < length; i++) { + AttributeInstance[] instances = attributeModifiers.values().stream() + .filter(i -> i.getAttribute().isShared()) + .toArray(AttributeInstance[]::new); + EntityPropertiesPacket.Property[] properties = new EntityPropertiesPacket.Property[instances.length]; + for (int i = 0; i < properties.length; ++i) { EntityPropertiesPacket.Property property = new EntityPropertiesPacket.Property(); - final Attribute attribute = Attribute.values()[i]; - final float value = getAttributeValue(attribute); + final float value = instances[i].getBaseValue(); - property.attribute = attribute; + property.attribute = instances[i].getAttribute(); property.value = value; properties[i] = property; @@ -511,7 +524,7 @@ public abstract class LivingEntity extends Entity implements EquipmentHandler { */ private void setupAttributes() { for (Attribute attribute : Attribute.values()) { - setAttribute(attribute, attribute.getDefaultValue()); + attributeModifiers.put(attribute.getKey(), new AttributeInstance(attribute, this::onAttributeChanged)); } } diff --git a/src/main/java/net/minestom/server/entity/Player.java b/src/main/java/net/minestom/server/entity/Player.java index 84ea817c8..31764aa27 100644 --- a/src/main/java/net/minestom/server/entity/Player.java +++ b/src/main/java/net/minestom/server/entity/Player.java @@ -3,6 +3,8 @@ package net.minestom.server.entity; import net.minestom.server.MinecraftServer; import net.minestom.server.advancements.AdvancementTab; import net.minestom.server.attribute.Attribute; +import net.minestom.server.attribute.AttributeInstance; +import net.minestom.server.attribute.Attributes; import net.minestom.server.bossbar.BossBar; import net.minestom.server.chat.ChatParser; import net.minestom.server.chat.ColoredText; @@ -288,7 +290,7 @@ public class Player extends LivingEntity implements CommandSender { @Override public float getAttributeValue(@NotNull Attribute attribute) { - if (attribute == Attribute.MOVEMENT_SPEED) { + if (attribute == Attributes.MOVEMENT_SPEED) { return walkingSpeed; } return super.getAttributeValue(attribute); @@ -1020,9 +1022,8 @@ public class Player extends LivingEntity implements CommandSender { } @Override - public void setAttribute(@NotNull Attribute attribute, float value) { - super.setAttribute(attribute, value); - if (playerConnection != null) + protected void onAttributeChanged(@NotNull final AttributeInstance instance) { + if (instance.getAttribute().isShared() && playerConnection != null) playerConnection.sendPacket(getPropertiesPacket()); } diff --git a/src/main/java/net/minestom/server/entity/pathfinding/PFPathingEntity.java b/src/main/java/net/minestom/server/entity/pathfinding/PFPathingEntity.java index 0e8713c0e..f7cc5696a 100644 --- a/src/main/java/net/minestom/server/entity/pathfinding/PFPathingEntity.java +++ b/src/main/java/net/minestom/server/entity/pathfinding/PFPathingEntity.java @@ -4,7 +4,7 @@ import com.extollit.gaming.ai.path.model.Gravitation; import com.extollit.gaming.ai.path.model.IPathingEntity; import com.extollit.gaming.ai.path.model.Passibility; import com.extollit.linalg.immutable.Vec3d; -import net.minestom.server.attribute.Attribute; +import net.minestom.server.attribute.Attributes; import net.minestom.server.entity.EntityCreature; import net.minestom.server.utils.Position; @@ -26,7 +26,7 @@ public class PFPathingEntity implements IPathingEntity { public PFPathingEntity(EntityCreature entity) { this.entity = entity; - this.searchRange = entity.getAttributeValue(Attribute.FOLLOW_RANGE); + this.searchRange = entity.getAttributeValue(Attributes.FOLLOW_RANGE); } public Position getTargetPosition() { @@ -113,7 +113,7 @@ public class PFPathingEntity implements IPathingEntity { return new Capabilities() { @Override public float speed() { - return entity.getAttributeValue(Attribute.MOVEMENT_SPEED); + return entity.getAttributeValue(Attributes.MOVEMENT_SPEED); } @Override diff --git a/src/main/java/net/minestom/server/network/packet/server/play/EntityPropertiesPacket.java b/src/main/java/net/minestom/server/network/packet/server/play/EntityPropertiesPacket.java index 3f3a3347a..8587236f9 100644 --- a/src/main/java/net/minestom/server/network/packet/server/play/EntityPropertiesPacket.java +++ b/src/main/java/net/minestom/server/network/packet/server/play/EntityPropertiesPacket.java @@ -1,6 +1,10 @@ package net.minestom.server.network.packet.server.play; +import java.util.Collection; + import net.minestom.server.attribute.Attribute; +import net.minestom.server.attribute.AttributeInstance; +import net.minestom.server.attribute.AttributeModifier; import net.minestom.server.network.packet.server.ServerPacket; import net.minestom.server.network.packet.server.ServerPacketIdentifier; import net.minestom.server.utils.binary.BinaryWriter; @@ -30,9 +34,14 @@ public class EntityPropertiesPacket implements ServerPacket { public Attribute attribute; public double value; + public AttributeInstance instance; private void write(BinaryWriter writer) { - float maxValue = attribute.getMaxVanillaValue(); + if (instance != null) { + attribute = instance.getAttribute(); + value = instance.getBaseValue(); + } + float maxValue = attribute.getMaxValue(); // Bypass vanilla limit client-side if needed (by sending the max value allowed) final double v = value > maxValue ? maxValue : value; @@ -40,8 +49,16 @@ public class EntityPropertiesPacket implements ServerPacket { writer.writeSizedString(attribute.getKey()); writer.writeDouble(v); - // TODO support for AttributeOperation - writer.writeVarInt(0); + { + Collection modifiers = instance.getModifiers(); + writer.writeVarInt(modifiers.size()); + + for (var modifier : modifiers) { + writer.writeUuid(modifier.getId()); + writer.writeDouble(modifier.getAmount()); + writer.writeByte((byte) modifier.getOperation().getId()); + } + } } } diff --git a/src/main/java/net/minestom/server/utils/UniqueIdUtils.java b/src/main/java/net/minestom/server/utils/UniqueIdUtils.java index 788406c9a..09f0c70df 100644 --- a/src/main/java/net/minestom/server/utils/UniqueIdUtils.java +++ b/src/main/java/net/minestom/server/utils/UniqueIdUtils.java @@ -1,5 +1,6 @@ package net.minestom.server.utils; +import java.util.Random; import java.util.UUID; import java.util.regex.Pattern; @@ -20,4 +21,11 @@ public final class UniqueIdUtils { return input.matches(UNIQUE_ID_PATTERN.pattern()); } + public static UUID createRandomUUID(Random random) { + long most = random.nextLong() & -61441L | 16384L; + long least = random.nextLong() & 4611686018427387903L | Long.MAX_VALUE; + + return new UUID(most, least); + } + } diff --git a/src/test/java/demo/entity/ChickenCreature.java b/src/test/java/demo/entity/ChickenCreature.java index cf5ddb728..662ad540a 100644 --- a/src/test/java/demo/entity/ChickenCreature.java +++ b/src/test/java/demo/entity/ChickenCreature.java @@ -1,6 +1,6 @@ package demo.entity; -import net.minestom.server.attribute.Attribute; +import net.minestom.server.attribute.Attributes; import net.minestom.server.entity.LivingEntity; import net.minestom.server.entity.ai.goal.RandomStrollGoal; import net.minestom.server.entity.damage.DamageType; @@ -37,7 +37,7 @@ public class ChickenCreature extends EntityChicken { //targetSelectors.add(new ClosestEntityTarget(this, 15, LivingEntity.class)); - setAttribute(Attribute.MOVEMENT_SPEED, 0.1f); + getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(0.1f); addEventCallback(EntityAttackEvent.class, event -> { //System.out.println("CALL ATTACK");