diff --git a/src/main/java/net/coreprotect/listener/entity/EntityDeathListener.java b/src/main/java/net/coreprotect/listener/entity/EntityDeathListener.java index 04364dc87..a7b2ecaf9 100644 --- a/src/main/java/net/coreprotect/listener/entity/EntityDeathListener.java +++ b/src/main/java/net/coreprotect/listener/entity/EntityDeathListener.java @@ -79,7 +79,9 @@ import net.coreprotect.spigot.SpigotAdapter; import net.coreprotect.thread.CacheHandler; import net.coreprotect.thread.Scheduler; +import net.coreprotect.utility.AttributeUtils; import net.coreprotect.utility.EntitySpawnTracking; +import net.coreprotect.utility.entity.EntityUtil; import net.coreprotect.utility.entity.LivingEntityDetails; import net.coreprotect.utility.serialize.ItemMetaHandler; @@ -306,11 +308,19 @@ else if (entity instanceof Player) { } } + long baselineBits = 0; if (entity instanceof Attributable) { Attributable attributable = entity; for (Attribute attribute : Lists.newArrayList(Registry.ATTRIBUTE)) { AttributeInstance attributeInstance = attributable.getAttribute(attribute); - if (attributeInstance != null) { + if (attributeInstance == null) { + continue; + } + int baselineBit = AttributeUtils.baselineBit(attributeInstance); + if (baselineBit >= 0) { + baselineBits |= 1L << baselineBit; + } + else { List attributeData = new ArrayList<>(); List attributeModifiers = new ArrayList<>(); attributeData.add(BukkitAdapter.ADAPTER.getRegistryKey(attributeInstance.getAttribute())); @@ -582,9 +592,10 @@ else if (entity instanceof Zoglin) { data.add(entity.getCustomName()); data.add(attributes); data.add(details); - if (EntitySpawnTracking.isTracked(entity)) { - data.add(entity.getUniqueId().toString()); - } + data.add(EntitySpawnTracking.isTracked(entity) ? entity.getUniqueId().toString() : null); + data.add(null); // kill location, only stored for placed entities + data.add(EntityUtil.BASELINE_ATTRIBUTES); + data.add(baselineBits); if (!(entity instanceof Player)) { Queue.queueEntityKill(e, entity.getLocation(), data, type); diff --git a/src/main/java/net/coreprotect/utility/AttributeUtils.java b/src/main/java/net/coreprotect/utility/AttributeUtils.java index 4e45ba730..7b188e3c4 100644 --- a/src/main/java/net/coreprotect/utility/AttributeUtils.java +++ b/src/main/java/net/coreprotect/utility/AttributeUtils.java @@ -1,14 +1,68 @@ package net.coreprotect.utility; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; import org.bukkit.Keyed; import org.bukkit.NamespacedKey; import org.bukkit.Registry; +import org.bukkit.attribute.Attributable; import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeInstance; +import org.bukkit.attribute.AttributeModifier; public final class AttributeUtils { + /** + * Attribute base values that entity kill data can leave out, frozen from the 26.2 attribute registry defaults. + * Kill data marks each left-out attribute with the bit of its position here, and a restore sets exactly this + * value, whatever the defaults of the restoring server are. Entries may only be appended: changing or reordering + * one would change what the bits of stored kill data mean. The bits are a long, so at most 64 entries fit. + */ + private static final List BASELINE = List.of( + new BaselineAttribute("air_drag_modifier", 1.0), + new BaselineAttribute("armor", 0.0), + new BaselineAttribute("armor_toughness", 0.0), + new BaselineAttribute("attack_damage", 2.0), + new BaselineAttribute("attack_knockback", 0.0), + new BaselineAttribute("attack_speed", 4.0), + new BaselineAttribute("below_name_distance", 10.0), + new BaselineAttribute("block_break_speed", 1.0), + new BaselineAttribute("block_interaction_range", 4.5), + new BaselineAttribute("bounciness", 0.0), + new BaselineAttribute("burning_time", 1.0), + new BaselineAttribute("camera_distance", 4.0), + new BaselineAttribute("entity_interaction_range", 3.0), + new BaselineAttribute("explosion_knockback_resistance", 0.0), + new BaselineAttribute("fall_damage_multiplier", 1.0), + new BaselineAttribute("flying_speed", 0.4), + new BaselineAttribute("follow_range", 32.0), + new BaselineAttribute("friction_modifier", 1.0), + new BaselineAttribute("gravity", 0.08), + new BaselineAttribute("jump_strength", 0.41999998688697815), + new BaselineAttribute("knockback_resistance", 0.0), + new BaselineAttribute("luck", 0.0), + new BaselineAttribute("max_absorption", 0.0), + new BaselineAttribute("max_health", 20.0), + new BaselineAttribute("mining_efficiency", 0.0), + new BaselineAttribute("movement_efficiency", 0.0), + new BaselineAttribute("movement_speed", 0.7), + new BaselineAttribute("name_tag_distance", 64.0), + new BaselineAttribute("oxygen_bonus", 0.0), + new BaselineAttribute("safe_fall_distance", 3.0), + new BaselineAttribute("scale", 1.0), + new BaselineAttribute("sneaking_speed", 0.3), + new BaselineAttribute("spawn_reinforcements", 0.0), + new BaselineAttribute("step_height", 0.6), + new BaselineAttribute("submerged_mining_speed", 0.2), + new BaselineAttribute("sweeping_damage_ratio", 0.0), + new BaselineAttribute("tempt_range", 10.0), + new BaselineAttribute("water_movement_efficiency", 0.0), + new BaselineAttribute("waypoint_receive_range", 0.0), + new BaselineAttribute("waypoint_transmit_range", 0.0) + ); + private AttributeUtils() { throw new IllegalStateException("Utility class"); } @@ -81,4 +135,74 @@ private static boolean isLegacyCategory(String category) { return category.equals("generic") || category.equals("player") || category.equals("horse") || category.equals("zombie"); } + + /** + * Returns the baseline bit of an attribute that kill data can leave out: it has no modifiers and its base value is + * the baseline value. + * + * @param attributeInstance + * the attribute of the entity that is being logged + * @return the bit, or -1 when the attribute has to be stored + */ + public static int baselineBit(AttributeInstance attributeInstance) { + if (!attributeInstance.getModifiers().isEmpty()) { + return -1; + } + NamespacedKey key = ((Keyed) attributeInstance.getAttribute()).getKey(); + if (!key.getNamespace().equals(NamespacedKey.MINECRAFT)) { + return -1; + } + String name = normalize(key.getKey()); + for (int bit = 0; bit < BASELINE.size(); bit++) { + BaselineAttribute baseline = BASELINE.get(bit); + if (baseline.key.equals(name)) { + return Double.compare(attributeInstance.getBaseValue(), baseline.value) == 0 ? bit : -1; + } + } + return -1; + } + + /** + * Sets every attribute whose bit is set to its baseline value without modifiers. Attributes without a bit keep the + * value they spawned with, including attributes that did not exist when the kill was logged. + * + * @param attributable + * the restored entity + * @param baselineBits + * the baseline bits stored with the kill data + */ + public static void restoreBaseline(Attributable attributable, long baselineBits) { + for (int bit = 0; bit < BASELINE.size(); bit++) { + if ((baselineBits & (1L << bit)) == 0) { + continue; + } + BaselineAttribute baseline = BASELINE.get(bit); + Attribute attribute; + try { + attribute = resolve(NamespacedKey.MINECRAFT + ":" + baseline.key); + } + catch (IllegalArgumentException exception) { + continue; + } + + AttributeInstance attributeInstance = attributable.getAttribute(attribute); + if (attributeInstance == null) { + continue; + } + attributeInstance.setBaseValue(baseline.value); + for (AttributeModifier modifier : new ArrayList<>(attributeInstance.getModifiers())) { + attributeInstance.removeModifier(modifier); + } + } + } + + private static final class BaselineAttribute { + private final String key; + private final double value; + + private BaselineAttribute(String key, double value) { + this.key = key; + this.value = value; + } + } } diff --git a/src/main/java/net/coreprotect/utility/entity/EntityUtil.java b/src/main/java/net/coreprotect/utility/entity/EntityUtil.java index 78e2a3c67..cc2ca7032 100644 --- a/src/main/java/net/coreprotect/utility/entity/EntityUtil.java +++ b/src/main/java/net/coreprotect/utility/entity/EntityUtil.java @@ -84,6 +84,19 @@ public class EntityUtil { private static final long ENTITY_RESTORE_TIMEOUT_SECONDS = 30L; + /** + * Position in the entity kill data list of the attribute format marker, followed by the baseline bits. Index 7 + * holds the entity UUID or null, and index 8 the kill location that EntitySpawnTracking stores for placed entities. + */ + public static final int ATTRIBUTE_FORMAT_INDEX = 9; + + /** + * Attribute format marker: the attribute list leaves out the attributes whose bits are set in the long that + * follows the marker (see {@link AttributeUtils#baselineBit}). Rows without the marker list every attribute the + * entity had. + */ + public static final int BASELINE_ATTRIBUTES = 1; + private EntityUtil() { throw new IllegalStateException("Utility class"); } @@ -215,6 +228,10 @@ else if (count == 1) { Attributable attributable = (Attributable) entity; @SuppressWarnings("unchecked") List attributes = (List) list.get(5); + Long baselineBits = baselineBits(list); + if (baselineBits != null) { + AttributeUtils.restoreBaseline(attributable, baselineBits); + } restoreAttributes(attributable, attributes); } @@ -717,6 +734,18 @@ else if (entity instanceof Zoglin) { return completion; } + static Long baselineBits(List list) { + if (list.size() <= ATTRIBUTE_FORMAT_INDEX + 1) { + return null; + } + Object format = list.get(ATTRIBUTE_FORMAT_INDEX); + Object bits = list.get(ATTRIBUTE_FORMAT_INDEX + 1); + if (format instanceof Number && ((Number) format).intValue() == BASELINE_ATTRIBUTES && bits instanceof Long) { + return (Long) bits; + } + return null; + } + static void restoreAttributes(Attributable attributable, List attributes) { for (Object value : attributes) { @SuppressWarnings("unchecked")