diff --git a/src/main/java/net/coreprotect/listener/entity/EntityDeathListener.java b/src/main/java/net/coreprotect/listener/entity/EntityDeathListener.java index 04364dc87..d31fd4c31 100644 --- a/src/main/java/net/coreprotect/listener/entity/EntityDeathListener.java +++ b/src/main/java/net/coreprotect/listener/entity/EntityDeathListener.java @@ -80,6 +80,7 @@ import net.coreprotect.thread.CacheHandler; import net.coreprotect.thread.Scheduler; import net.coreprotect.utility.EntitySpawnTracking; +import net.coreprotect.utility.entity.EntityUtil; import net.coreprotect.utility.entity.LivingEntityDetails; import net.coreprotect.utility.serialize.ItemMetaHandler; @@ -310,7 +311,7 @@ else if (entity instanceof Player) { Attributable attributable = entity; for (Attribute attribute : Lists.newArrayList(Registry.ATTRIBUTE)) { AttributeInstance attributeInstance = attributable.getAttribute(attribute); - if (attributeInstance != null) { + if (attributeInstance != null && EntityUtil.isAttributeModified(attributeInstance)) { List attributeData = new ArrayList<>(); List attributeModifiers = new ArrayList<>(); attributeData.add(BukkitAdapter.ADAPTER.getRegistryKey(attributeInstance.getAttribute())); @@ -582,9 +583,9 @@ 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.SPARSE_ATTRIBUTES); if (!(entity instanceof Player)) { Queue.queueEntityKill(e, entity.getLocation(), data, type); diff --git a/src/main/java/net/coreprotect/utility/entity/EntityUtil.java b/src/main/java/net/coreprotect/utility/entity/EntityUtil.java index 78e2a3c67..baf3bf84f 100644 --- a/src/main/java/net/coreprotect/utility/entity/EntityUtil.java +++ b/src/main/java/net/coreprotect/utility/entity/EntityUtil.java @@ -84,6 +84,18 @@ public class EntityUtil { private static final long ENTITY_RESTORE_TIMEOUT_SECONDS = 30L; + /** + * Position in the entity kill data list of the attribute format marker. 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 omits every attribute that had the default base value and no + * modifiers. Rows without the marker list every attribute the entity had. + */ + public static final int SPARSE_ATTRIBUTES = 1; + private EntityUtil() { throw new IllegalStateException("Utility class"); } @@ -215,6 +227,9 @@ else if (count == 1) { Attributable attributable = (Attributable) entity; @SuppressWarnings("unchecked") List attributes = (List) list.get(5); + if (hasSparseAttributes(list)) { + resetAttributes(attributeInstances(attributable)); + } restoreAttributes(attributable, attributes); } @@ -717,6 +732,52 @@ else if (entity instanceof Zoglin) { return completion; } + /** + * Returns whether an attribute has to be stored to restore the entity: its base value differs from the default, + * or it has modifiers. {@link #resetAttributes(Iterable)} rebuilds every attribute this skips. + * + * @param attributeInstance + * the attribute of the entity that is being logged + * @return true when the attribute must be stored + */ + @SuppressWarnings("deprecation") // The server default exists on every supported version; restore uses the same value. + public static boolean isAttributeModified(AttributeInstance attributeInstance) { + return !attributeInstance.getModifiers().isEmpty() || Double.compare(attributeInstance.getBaseValue(), attributeInstance.getDefaultValue()) != 0; + } + + static boolean hasSparseAttributes(List list) { + if (list.size() <= ATTRIBUTE_FORMAT_INDEX) { + return false; + } + Object format = list.get(ATTRIBUTE_FORMAT_INDEX); + return format instanceof Number && ((Number) format).intValue() == SPARSE_ATTRIBUTES; + } + + private static List attributeInstances(Attributable attributable) { + List attributeInstances = new ArrayList<>(); + for (Attribute attribute : Registry.ATTRIBUTE) { + AttributeInstance attributeInstance = attributable.getAttribute(attribute); + if (attributeInstance != null) { + attributeInstances.add(attributeInstance); + } + } + return attributeInstances; + } + + /** + * Sets attributes of a restored entity to their default base value without modifiers, so that attributes a + * sparse attribute list omits match the logged entity. Spawning can randomize base values and add modifiers. + */ + @SuppressWarnings("deprecation") // Same default value that isAttributeModified compares against. + static void resetAttributes(Iterable attributeInstances) { + for (AttributeInstance attributeInstance : attributeInstances) { + attributeInstance.setBaseValue(attributeInstance.getDefaultValue()); + for (AttributeModifier modifier : new ArrayList<>(attributeInstance.getModifiers())) { + attributeInstance.removeModifier(modifier); + } + } + } + static void restoreAttributes(Attributable attributable, List attributes) { for (Object value : attributes) { @SuppressWarnings("unchecked")