From e40df834dcb3c6861f183781d8bc4ceb44a6a5f3 Mon Sep 17 00:00:00 2001 From: SoSeDiK Date: Tue, 24 Dec 2024 09:43:31 +0200 Subject: [PATCH] Expand on entity serialization API --- .../paper/entity/EntitySerializationFlag.java | 38 +++++++++ .../main/java/org/bukkit/UnsafeValues.java | 77 ++++++++++++++++++- .../main/java/org/bukkit/entity/Entity.java | 19 +++-- .../minecraft/world/entity/Entity.java.patch | 63 +++++++-------- .../craftbukkit/entity/CraftEntity.java | 17 ++-- .../entity/CraftEntitySnapshot.java | 2 +- .../craftbukkit/util/CraftMagicNumbers.java | 61 +++++++++++++-- 7 files changed, 221 insertions(+), 56 deletions(-) create mode 100644 paper-api/src/main/java/io/papermc/paper/entity/EntitySerializationFlag.java diff --git a/paper-api/src/main/java/io/papermc/paper/entity/EntitySerializationFlag.java b/paper-api/src/main/java/io/papermc/paper/entity/EntitySerializationFlag.java new file mode 100644 index 0000000000..4a76c3491b --- /dev/null +++ b/paper-api/src/main/java/io/papermc/paper/entity/EntitySerializationFlag.java @@ -0,0 +1,38 @@ +package io.papermc.paper.entity; + +import org.bukkit.UnsafeValues; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; + +/** + * Represents flags for entity serialization. + * + * @see UnsafeValues#serializeEntity(Entity, EntitySerializationFlag... serializationFlags) + * @since 1.21.4 + */ +public enum EntitySerializationFlag { + + /** + * Serialize entities that wouldn't be serialized normally + * (e.g. dead, despawned, non-persistent, etc.). + * + * @see Entity#isValid() + * @see Entity#isPersistent() + */ + FORCE, + /** + * Serialize misc non-saveable entities like lighting bolts, fishing bobbers, etc. + *
Note: players require a separate flag: {@link #PLAYER}. + */ + MISC, + /** + * Include passengers in the serialized data. + */ + PASSENGERS, + /** + * Allow serializing {@link Player}s. + *

Note: deserializing player data will always fail. + */ + PLAYER + +} diff --git a/paper-api/src/main/java/org/bukkit/UnsafeValues.java b/paper-api/src/main/java/org/bukkit/UnsafeValues.java index d0de7ce3c3..56fa266e4b 100644 --- a/paper-api/src/main/java/org/bukkit/UnsafeValues.java +++ b/paper-api/src/main/java/org/bukkit/UnsafeValues.java @@ -1,6 +1,7 @@ package org.bukkit; import com.google.common.collect.Multimap; +import io.papermc.paper.entity.EntitySerializationFlag; import org.bukkit.advancement.Advancement; import org.bukkit.attribute.Attribute; import org.bukkit.attribute.AttributeModifier; @@ -9,7 +10,9 @@ import org.bukkit.block.data.BlockData; import org.bukkit.damage.DamageEffect; import org.bukkit.damage.DamageSource; import org.bukkit.damage.DamageType; +import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; +import org.bukkit.event.entity.CreatureSpawnEvent; import org.bukkit.inventory.CreativeCategory; import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.ItemStack; @@ -198,13 +201,81 @@ public interface UnsafeValues { */ @NotNull ItemStack deserializeItemFromJson(@NotNull com.google.gson.JsonObject data) throws IllegalArgumentException; - byte[] serializeEntity(org.bukkit.entity.Entity entity); + /** + * Serializes the provided entity. + * + * @param entity entity + * @return serialized entity data + * @see #serializeEntity(Entity, EntitySerializationFlag...) + * @see #deserializeEntity(byte[], World, boolean, boolean) + * @throws IllegalArgumentException if couldn't serialize the entity + * @since 1.17.1 + */ + default byte @NotNull [] serializeEntity(@NotNull Entity entity) { + return serializeEntity(entity, new EntitySerializationFlag[0]); + } - default org.bukkit.entity.Entity deserializeEntity(byte[] data, World world) { + /** + * Serializes the provided entity. + * + * @param entity entity + * @param serializationFlags serialization flags + * @return serialized entity data + * @throws IllegalArgumentException if couldn't serialize the entity + * @see #deserializeEntity(byte[], World, boolean, boolean) + * @since 1.21.4 + */ + byte @NotNull [] serializeEntity(@NotNull Entity entity, @NotNull EntitySerializationFlag... serializationFlags); + + /** + * Deserializes the entity from data. + *
The entity's {@link java.util.UUID} as well as passengers will not be preserved. + * + * @param data serialized entity data + * @param world world + * @return deserialized entity + * @throws IllegalArgumentException if invalid serialized entity data provided + * @see #deserializeEntity(byte[], World, boolean, boolean) + * @see #serializeEntity(Entity, EntitySerializationFlag...) + * @see Entity#spawnAt(Location, CreatureSpawnEvent.SpawnReason) + * @since 1.17.1 + */ + default @NotNull Entity deserializeEntity(byte @NotNull [] data, @NotNull World world) { return deserializeEntity(data, world, false); } - org.bukkit.entity.Entity deserializeEntity(byte[] data, World world, boolean preserveUUID); + /** + * Deserializes the entity from data. + *
The entity's passengers will not be preserved. + * + * @param data serialized entity data + * @param world world + * @param preserveUUID whether to preserve the entity's uuid + * @return deserialized entity + * @throws IllegalArgumentException if invalid serialized entity data provided + * @see #deserializeEntity(byte[], World, boolean, boolean) + * @see #serializeEntity(Entity, EntitySerializationFlag...) + * @see Entity#spawnAt(Location, CreatureSpawnEvent.SpawnReason) + * @since 1.17.1 + */ + default @NotNull Entity deserializeEntity(byte @NotNull [] data, @NotNull World world, boolean preserveUUID) { + return deserializeEntity(data, world, preserveUUID, false); + } + + /** + * Deserializes the entity from data. + * + * @param data serialized entity data + * @param world world + * @param preserveUUID whether to preserve uuids of the entity and its passengers + * @param preservePassengers whether to preserve passengers + * @return deserialized entity + * @throws IllegalArgumentException if invalid serialized entity data provided + * @see #serializeEntity(Entity, EntitySerializationFlag...) + * @see Entity#spawnAt(Location, CreatureSpawnEvent.SpawnReason) + * @since 1.21.4 + */ + @NotNull Entity deserializeEntity(byte @NotNull [] data, @NotNull World world, boolean preserveUUID, boolean preservePassengers); /** * Creates and returns the next EntityId available. diff --git a/paper-api/src/main/java/org/bukkit/entity/Entity.java b/paper-api/src/main/java/org/bukkit/entity/Entity.java index 19272cff8d..996527dbdf 100644 --- a/paper-api/src/main/java/org/bukkit/entity/Entity.java +++ b/paper-api/src/main/java/org/bukkit/entity/Entity.java @@ -13,6 +13,7 @@ import org.bukkit.World; import org.bukkit.block.BlockFace; import org.bukkit.block.PistonMoveReaction; import org.bukkit.command.CommandSender; +import org.bukkit.event.entity.CreatureSpawnEvent; import org.bukkit.event.entity.EntityDamageEvent; import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; import org.bukkit.material.Directional; @@ -1051,11 +1052,12 @@ public interface Entity extends Metadatable, CommandSender, Nameable, Persistent *

* Also, this method will fire the same events as a normal entity spawn. * - * @param location The location to spawn the entity at. - * @return Whether the entity was successfully spawned. + * @param location the location to spawn the entity at + * @return whether the entity was successfully spawned + * @since 1.17.1 */ - public default boolean spawnAt(@NotNull Location location) { - return spawnAt(location, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT); + default boolean spawnAt(@NotNull Location location) { + return spawnAt(location, CreatureSpawnEvent.SpawnReason.DEFAULT); } /** @@ -1065,11 +1067,12 @@ public interface Entity extends Metadatable, CommandSender, Nameable, Persistent *

* Also, this method will fire the same events as a normal entity spawn. * - * @param location The location to spawn the entity at. - * @param reason The reason for the entity being spawned. - * @return Whether the entity was successfully spawned. + * @param location the location to spawn the entity at + * @param reason the reason for the entity being spawned + * @return whether the entity was successfully spawned + * @since 1.17.1 */ - public boolean spawnAt(@NotNull Location location, @NotNull org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason); + boolean spawnAt(@NotNull Location location, @NotNull CreatureSpawnEvent.SpawnReason reason); /** * Check if entity is inside powdered snow. diff --git a/paper-server/patches/sources/net/minecraft/world/entity/Entity.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/Entity.java.patch index f1de578de1..780a51d408 100644 --- a/paper-server/patches/sources/net/minecraft/world/entity/Entity.java.patch +++ b/paper-server/patches/sources/net/minecraft/world/entity/Entity.java.patch @@ -706,52 +706,44 @@ public void awardKillScore(Entity entity, DamageSource damageSource) { if (entity instanceof ServerPlayer) { -@@ -1752,34 +_,70 @@ +@@ -1752,15 +_,22 @@ } public boolean saveAsPassenger(CompoundTag compound) { +- if (this.removalReason != null && !this.removalReason.shouldSave()) { + // CraftBukkit start - allow excluding certain data when saving -+ return this.saveAsPassenger(compound, true); ++ // Paper start - Raw entity serialization API ++ return this.saveAsPassenger(compound, true, false, false); + } -+ -+ public boolean saveAsPassenger(CompoundTag compound, boolean includeAll) { ++ public boolean saveAsPassenger(CompoundTag compound, boolean includeAll, boolean includeNonSaveable, boolean forceSerialization) { ++ // Paper end - Raw entity serialization API + // CraftBukkit end - if (this.removalReason != null && !this.removalReason.shouldSave()) { ++ if (this.removalReason != null && !this.removalReason.shouldSave() && !forceSerialization) { // Paper - Raw entity serialization API return false; } else { - String encodeId = this.getEncodeId(); +- String encodeId = this.getEncodeId(); - if (encodeId == null) { -+ if (!this.persist || encodeId == null) { // CraftBukkit - persist flag ++ String encodeId = this.getEncodeId(includeNonSaveable); // Paper - Raw entity serialization API ++ if ((!this.persist && !forceSerialization) || encodeId == null) { // CraftBukkit - persist flag // Paper - Raw entity serialization API return false; } else { compound.putString("id", encodeId); - this.saveWithoutId(compound); -+ this.saveWithoutId(compound, includeAll); // CraftBukkit - pass on includeAll ++ this.saveWithoutId(compound, includeAll, includeNonSaveable, forceSerialization); // CraftBukkit - pass on includeAll // Paper - Raw entity serialization API return true; } } - } -+ -+ // Paper start - Entity serialization api -+ public boolean serializeEntity(CompoundTag compound) { -+ List pass = new java.util.ArrayList<>(this.getPassengers()); -+ this.passengers = ImmutableList.of(); -+ boolean result = save(compound); -+ this.passengers = ImmutableList.copyOf(pass); -+ return result; -+ } -+ // Paper end - Entity serialization api - - public boolean save(CompoundTag compound) { - return !this.isPassenger() && this.saveAsPassenger(compound); +@@ -1771,15 +_,37 @@ } public CompoundTag saveWithoutId(CompoundTag compound) { + // CraftBukkit start - allow excluding certain data when saving -+ return this.saveWithoutId(compound, true); ++ // Paper start - Raw entity serialization API ++ return this.saveWithoutId(compound, true, false, false); + } + -+ public CompoundTag saveWithoutId(CompoundTag compound, boolean includeAll) { ++ public CompoundTag saveWithoutId(CompoundTag compound, boolean includeAll, boolean includeNonSaveable, boolean forceSerialization) { ++ // Paper end - Raw entity serialization API + // CraftBukkit end try { - if (this.vehicle != null) { @@ -827,7 +819,7 @@ for (Entity entity : this.getPassengers()) { CompoundTag compoundTag = new CompoundTag(); - if (entity.saveAsPassenger(compoundTag)) { -+ if (entity.saveAsPassenger(compoundTag, includeAll)) { // CraftBukkit - pass on includeAll ++ if (entity.saveAsPassenger(compoundTag, includeAll, includeNonSaveable, forceSerialization)) { // CraftBukkit - pass on includeAll // Paper - Raw entity serialization API listTag.add(compoundTag); } } @@ -935,19 +927,30 @@ } catch (Throwable var17) { CrashReport crashReport = CrashReport.forThrowable(var17, "Loading entity NBT"); CrashReportCategory crashReportCategory = crashReport.addCategory("Entity being loaded"); -@@ -1949,6 +_,12 @@ - return type.canSerialize() && key != null ? key.toString() : null; - } +@@ -1944,10 +_,21 @@ + @Nullable + public final String getEncodeId() { ++ // Paper start - Raw entity serialization API ++ return getEncodeId(false); ++ } ++ public final @Nullable String getEncodeId(boolean includeNonSaveable) { ++ // Paper end - Raw entity serialization API + EntityType type = this.getType(); + ResourceLocation key = EntityType.getKey(type); +- return type.canSerialize() && key != null ? key.toString() : null; +- } ++ return (type.canSerialize() || includeNonSaveable) && key != null ? key.toString() : null; // Paper - Raw entity serialization API ++ } ++ + // CraftBukkit start - allow excluding certain data when saving + protected void addAdditionalSaveData(CompoundTag tag, boolean includeAll) { + this.addAdditionalSaveData(tag); + } + // CraftBukkit end -+ + protected abstract void readAdditionalSaveData(CompoundTag tag); - protected abstract void addAdditionalSaveData(CompoundTag tag); @@ -1990,11 +_,61 @@ @Nullable diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java index 91b78711ec..8661604b86 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java @@ -45,6 +45,7 @@ import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; import org.bukkit.entity.Pose; import org.bukkit.entity.SpawnCategory; +import org.bukkit.event.entity.CreatureSpawnEvent; import org.bukkit.event.entity.EntityDamageEvent; import org.bukkit.event.entity.EntityRemoveEvent; import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; @@ -935,7 +936,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { @Override public String getAsString() { CompoundTag tag = new CompoundTag(); - if (!this.getHandle().saveAsPassenger(tag, false)) { + if (!this.getHandle().saveAsPassenger(tag, false, true, true)) { return null; } @@ -968,7 +969,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { private Entity copy(net.minecraft.world.level.Level level) { CompoundTag compoundTag = new CompoundTag(); - this.getHandle().saveAsPassenger(compoundTag, false); + this.getHandle().saveAsPassenger(compoundTag, false, true, true); return net.minecraft.world.entity.EntityType.loadEntityRecursive(compoundTag, level, EntitySpawnReason.LOAD, java.util.function.Function.identity()); } @@ -1204,17 +1205,21 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { } // Paper end - tracked players API - // Paper start - raw entity serialization API @Override - public boolean spawnAt(Location location, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) { + public boolean spawnAt(Location location, CreatureSpawnEvent.SpawnReason reason) { Preconditions.checkNotNull(location, "location cannot be null"); Preconditions.checkNotNull(reason, "reason cannot be null"); this.entity.setLevel(((CraftWorld) location.getWorld()).getHandle()); this.entity.setPos(location.getX(), location.getY(), location.getZ()); this.entity.setRot(location.getYaw(), location.getPitch()); - return !this.entity.valid && this.entity.level().addFreshEntity(this.entity, reason); + boolean spawned = !this.entity.valid && this.entity.level().addFreshEntity(this.entity, reason); + if (spawned) { + for (org.bukkit.entity.Entity pass : getPassengers()) { + pass.spawnAt(getLocation()); + } + } + return spawned; } - // Paper end - raw entity serialization API // Paper start - entity powdered snow API @Override diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftEntitySnapshot.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftEntitySnapshot.java index 6642bdc117..35a3f2412e 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftEntitySnapshot.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftEntitySnapshot.java @@ -66,7 +66,7 @@ public class CraftEntitySnapshot implements EntitySnapshot { public static CraftEntitySnapshot create(CraftEntity entity) { CompoundTag tag = new CompoundTag(); - if (!entity.getHandle().saveAsPassenger(tag, false)) { + if (!entity.getHandle().saveAsPassenger(tag, false, true, true)) { return null; } diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/paper-server/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java index b6665e1875..756cafac87 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java @@ -558,29 +558,74 @@ public final class CraftMagicNumbers implements UnsafeValues { } @Override - public byte[] serializeEntity(org.bukkit.entity.Entity entity) { + public byte[] serializeEntity(org.bukkit.entity.Entity entity, io.papermc.paper.entity.EntitySerializationFlag... serializationFlags) { Preconditions.checkNotNull(entity, "null cannot be serialized"); - Preconditions.checkArgument(entity instanceof org.bukkit.craftbukkit.entity.CraftEntity, "only CraftEntities can be serialized"); + Preconditions.checkArgument(entity instanceof org.bukkit.craftbukkit.entity.CraftEntity, "Only CraftEntities can be serialized"); + + java.util.Set flags = java.util.Set.of(serializationFlags); + boolean forceSerialization = flags.contains(io.papermc.paper.entity.EntitySerializationFlag.FORCE); + Preconditions.checkArgument((entity.isValid() && entity.isPersistent()) || forceSerialization, "Cannot serialize invalid or non-persistent entity without the FORCE flag"); + + boolean includeNonSaveable; + net.minecraft.world.entity.Entity nmsEntity = ((org.bukkit.craftbukkit.entity.CraftEntity) entity).getHandle(); + if (entity instanceof org.bukkit.entity.Player) { + includeNonSaveable = flags.contains(io.papermc.paper.entity.EntitySerializationFlag.PLAYER); + Preconditions.checkArgument(includeNonSaveable, "Cannot serialize Players without the PLAYER flag"); + } else { + includeNonSaveable = flags.contains(io.papermc.paper.entity.EntitySerializationFlag.MISC); + Preconditions.checkArgument(nmsEntity.getType().canSerialize() || includeNonSaveable, String.format("Cannot serialize misc non-saveable entity (%s) without the MISC flag", entity.getType().name())); + } net.minecraft.nbt.CompoundTag compound = new net.minecraft.nbt.CompoundTag(); - ((org.bukkit.craftbukkit.entity.CraftEntity) entity).getHandle().serializeEntity(compound); + if (flags.contains(io.papermc.paper.entity.EntitySerializationFlag.PASSENGERS)) { + if (!nmsEntity.saveAsPassenger(compound, true, includeNonSaveable, forceSerialization)) { + throw new IllegalArgumentException("Couldn't serialize entity"); + } + } else { + java.util.List pass = new java.util.ArrayList<>(nmsEntity.getPassengers()); + nmsEntity.passengers = com.google.common.collect.ImmutableList.of(); + boolean serialized = nmsEntity.saveAsPassenger(compound, true, includeNonSaveable, forceSerialization); + nmsEntity.passengers = com.google.common.collect.ImmutableList.copyOf(pass); + if (!serialized) { + throw new IllegalArgumentException("Couldn't serialize entity"); + } + } return serializeNbtToBytes(compound); } @Override - public org.bukkit.entity.Entity deserializeEntity(byte[] data, org.bukkit.World world, boolean preserveUUID) { + public org.bukkit.entity.Entity deserializeEntity(byte[] data, org.bukkit.World world, boolean preserveUUID, boolean preservePassengers) { Preconditions.checkNotNull(data, "null cannot be deserialized"); - Preconditions.checkArgument(data.length > 0, "cannot deserialize nothing"); + Preconditions.checkArgument(data.length > 0, "Cannot deserialize empty data"); net.minecraft.nbt.CompoundTag compound = deserializeNbtFromBytes(data); int dataVersion = compound.getInt("DataVersion"); compound = ca.spottedleaf.moonrise.common.PlatformHooks.get().convertNBT(References.ENTITY, MinecraftServer.getServer().fixerUpper, compound, dataVersion, this.getDataVersion()); // Paper - possibly use dataconverter + if (!preservePassengers) { + compound.remove("Passengers"); + } + net.minecraft.world.entity.Entity nmsEntity = deserializeEntity(compound, ((org.bukkit.craftbukkit.CraftWorld) world).getHandle(), preserveUUID); + return nmsEntity.getBukkitEntity(); + } + + private net.minecraft.world.entity.Entity deserializeEntity(net.minecraft.nbt.CompoundTag compound, net.minecraft.server.level.ServerLevel world, boolean preserveUUID) { if (!preserveUUID) { - // Generate a new UUID so we don't have to worry about deserializing the same entity twice + // Generate a new UUID, so we don't have to worry about deserializing the same entity twice compound.remove("UUID"); } - return net.minecraft.world.entity.EntityType.create(compound, ((org.bukkit.craftbukkit.CraftWorld) world).getHandle(), net.minecraft.world.entity.EntitySpawnReason.LOAD) - .orElseThrow(() -> new IllegalArgumentException("An ID was not found for the data. Did you downgrade?")).getBukkitEntity(); + net.minecraft.world.entity.Entity nmsEntity = net.minecraft.world.entity.EntityType.create(compound, world, net.minecraft.world.entity.EntitySpawnReason.LOAD) + .orElseThrow(() -> new IllegalArgumentException("An ID was not found for the data. Did you downgrade?")); + if (compound.contains("Passengers", Tag.TAG_LIST)) { + net.minecraft.nbt.ListTag passengersCompound = compound.getList("Passengers", Tag.TAG_COMPOUND); + for (Tag tag : passengersCompound) { + if (!(tag instanceof net.minecraft.nbt.CompoundTag serializedPassenger)) { + continue; + } + net.minecraft.world.entity.Entity passengerEntity = deserializeEntity(serializedPassenger, world, preserveUUID); + passengerEntity.startRiding(nmsEntity, true); + } + } + return nmsEntity; } private byte[] serializeNbtToBytes(net.minecraft.nbt.CompoundTag compound) {