mirror of
https://github.com/PlayPro/CoreProtect.git
synced 2026-10-28 09:44:56 +00:00
Reduced entity kill data by skipping default attributes (#1028)
* Reduced entity kill data by skipping default attributes EntityDeathListener stored every registered attribute of a killed mob, about 23 entries on 1.21+, although almost all hold default values. The attribute list made up about 88% of each kill blob. Store an attribute only when its base value differs from the default or it has modifiers. Kill data now always carries index 7 (UUID or null), index 8 (kill location slot, null for mobs) and a format marker at index 9. When restore sees the marker, it first resets every attribute of the respawned entity to its default base value and removes spawn-time modifiers, then applies the stored attributes, so rollback reproduces the logged entity exactly. Rows without the marker restore as before. Capture and restore both use AttributeInstance#getDefaultValue. It is deprecated since 26.1 for returning the server default rather than the entity type default, but it exists on every supported version (1.16.5+) and the same value is used on both sides. Zombie kill blob: 2036 to 1027 bytes in the legacy format, and 250 to 141 bytes with EntityDataCodec (DuckDB and ClickHouse today, SQLite and MySQL after the v26 migration). On SQLite that is 4096 to 1365 bytes per kill on disk now, or 158 bytes after v26. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PdEuKbjcVoVinVQJq4te1f * Restored left-out attributes from a frozen baseline Restoring sparse kill data reset every attribute of the respawned entity to the restoring server's defaults. An attribute the logging server did not have yet was reset as well: a horse logged before 1.20.5 and restored on 26.1 got the global step height, safe fall distance and fall damage multiplier instead of the horse values. AttributeUtils now holds a frozen baseline of 40 attribute values, taken from the 26.2 registry defaults. Kill data leaves out an attribute only when it is in the baseline, has the baseline value and has no modifiers, and sets that attribute's bit in a long stored after the format marker at index 10. Restore sets exactly the attributes with a bit to their baseline values and leaves every other attribute as spawned. Entries are only ever appended, so stored bits keep their meaning, and the restoring server's defaults are never read. Rows without the marker restore as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: TonyJamesStark <tonyjamesstark1@gmail.com>
This commit is contained in:
3 files changed
+168
-4
No files matched your search
@@ -83,7 +83,9 @@ import net.coreprotect.paper.PaperAdapter;
|
||||
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;
|
||||
|
||||
@@ -337,11 +339,19 @@ public final class EntityDeathListener extends Queue implements Listener {
|
||||
}
|
||||
}
|
||||
|
||||
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<Object> attributeData = new ArrayList<>();
|
||||
List<Object> attributeModifiers = new ArrayList<>();
|
||||
attributeData.add(BukkitAdapter.ADAPTER.getRegistryKey(attributeInstance.getAttribute()));
|
||||
@@ -613,9 +623,10 @@ public final class EntityDeathListener extends Queue implements Listener {
|
||||
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);
|
||||
|
||||
@@ -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<BaselineAttribute> 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 @@ public final class AttributeUtils {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -216,6 +229,10 @@ public class EntityUtil {
|
||||
Attributable attributable = (Attributable) entity;
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Object> attributes = (List<Object>) list.get(5);
|
||||
Long baselineBits = baselineBits(list);
|
||||
if (baselineBits != null) {
|
||||
AttributeUtils.restoreBaseline(attributable, baselineBits);
|
||||
}
|
||||
restoreAttributes(attributable, attributes);
|
||||
}
|
||||
|
||||
@@ -718,6 +735,18 @@ public class EntityUtil {
|
||||
return completion;
|
||||
}
|
||||
|
||||
static Long baselineBits(List<Object> 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<Object> attributes) {
|
||||
for (Object value : attributes) {
|
||||
@SuppressWarnings("unchecked")
|
||||
|
||||
Reference in new issue
Block a user