Implement attribute instances

This commit is contained in:
JësFot 2020-11-17 14:47:49 +01:00
parent 089f9a30aa
commit d738f9fddd
11 changed files with 285 additions and 73 deletions

View File

@ -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<String, Attribute> 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);
}
}

View File

@ -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<UUID, AttributeModifier> modifiers = new HashMap<>();
private final Consumer<AttributeInstance> propertyChangeListener;
private float baseValue;
private boolean dirty = true;
private float cachedValue = 0.0f;
public AttributeInstance(@NotNull Attribute attribute, @Nullable Consumer<AttributeInstance> 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<AttributeModifier> 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());
}
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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 {

View File

@ -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<String, AttributeInstance> 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.
* <p>
* 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));
}
}

View File

@ -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());
}

View File

@ -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

View File

@ -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<AttributeModifier> 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());
}
}
}
}

View File

@ -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);
}
}

View File

@ -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");