From aac246ae29349b04334529fe12f88aa3cd8e0c7c Mon Sep 17 00:00:00 2001 From: SoSeDiK Date: Fri, 27 Dec 2024 01:08:00 +0200 Subject: [PATCH] Expand on entity serialization API (#11807) --- .../paper/entity/EntitySerializationFlag.java | 38 ++++++ .../main/java/org/bukkit/UnsafeValues.java | 77 +++++++++++- .../main/java/org/bukkit/entity/Entity.java | 19 +-- .../0017-Moonrise-optimisation-patches.patch | 20 +-- .../minecraft/world/entity/Entity.java.patch | 63 +++++----- .../craftbukkit/entity/CraftEntity.java | 15 ++- .../entity/CraftEntitySnapshot.java | 2 +- .../craftbukkit/util/CraftMagicNumbers.java | 118 +++++++++++++++--- 8 files changed, 275 insertions(+), 77 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 3fe738c0d0..ddf7829eee 100644 --- a/paper-api/src/main/java/org/bukkit/entity/Entity.java +++ b/paper-api/src/main/java/org/bukkit/entity/Entity.java @@ -14,6 +14,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; @@ -1072,11 +1073,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); } /** @@ -1086,11 +1088,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/features/0017-Moonrise-optimisation-patches.patch b/paper-server/patches/features/0017-Moonrise-optimisation-patches.patch index 45b4a15fe7..93faf37563 100644 --- a/paper-server/patches/features/0017-Moonrise-optimisation-patches.patch +++ b/paper-server/patches/features/0017-Moonrise-optimisation-patches.patch @@ -26719,7 +26719,7 @@ index 2f49dbc919f7f5eea9abce6106723c72f5ae45fb..87d4291a3944f706a694536da6de0f28 } } diff --git a/net/minecraft/server/level/ServerEntity.java b/net/minecraft/server/level/ServerEntity.java -index 70f6d068b3f3665b282d9750310c883839120ab2..870b9efd445ddadb3725e88351555ad986ce7c72 100644 +index da793ad12565c36fffb26eb771ff68c76632caf7..db06f966077928419bfe469260f04d7dfda69f28 100644 --- a/net/minecraft/server/level/ServerEntity.java +++ b/net/minecraft/server/level/ServerEntity.java @@ -91,6 +91,11 @@ public class ServerEntity { @@ -27496,7 +27496,7 @@ index 192977dd661ee795ada13db895db770293e9b402..95a4e37a3c93f9b3c56c7a7376ed521c } diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java -index ff5889f8fed0707a6654d9d21862e32e2ebc866d..e61fe83479f095e8addbd3e8f1d5179c998ae1eb 100644 +index 097ec55166b9e9269142be58992c29687122fe28..aeabb79512aabd7a9e8af1be72e1745f0e7eefe4 100644 --- a/net/minecraft/server/level/ServerPlayer.java +++ b/net/minecraft/server/level/ServerPlayer.java @@ -178,7 +178,7 @@ import net.minecraft.world.scores.Team; @@ -28372,7 +28372,7 @@ index 8cc5c0716392ba06501542ff5cbe71ee43979e5d..09fd99c9cbd23b5f3c899bfb00c9b896 + // Paper end - block counting } diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java -index 45f69a914d5a0565196c4105d61541047301470f..f42bdae2f80805e5069212bd16e3ab6814aef9ee 100644 +index 189385600b9094291152035b17df869eaccc0428..25a1089a7376f0cbd96bb43b5c203640c88fc282 100644 --- a/net/minecraft/world/entity/Entity.java +++ b/net/minecraft/world/entity/Entity.java @@ -135,7 +135,7 @@ import net.minecraft.world.scores.ScoreHolder; @@ -28725,7 +28725,7 @@ index 45f69a914d5a0565196c4105d61541047301470f..f42bdae2f80805e5069212bd16e3ab68 } private static float[] collectCandidateStepUpHeights(AABB box, List colliders, float deltaY, float maxUpStep) { -@@ -2664,23 +2812,110 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess +@@ -2662,23 +2810,110 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess } public boolean isInWall() { @@ -28849,7 +28849,7 @@ index 45f69a914d5a0565196c4105d61541047301470f..f42bdae2f80805e5069212bd16e3ab68 } public InteractionResult interact(Player player, InteractionHand hand) { -@@ -4104,15 +4339,17 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess +@@ -4102,15 +4337,17 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess } public Iterable getIndirectPassengers() { @@ -28875,7 +28875,7 @@ index 45f69a914d5a0565196c4105d61541047301470f..f42bdae2f80805e5069212bd16e3ab68 } public int countPlayerPassengers() { -@@ -4250,77 +4487,136 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess +@@ -4248,77 +4485,136 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess return Mth.lerp(partialTick, this.yRotO, this.yRot); } @@ -29066,7 +29066,7 @@ index 45f69a914d5a0565196c4105d61541047301470f..f42bdae2f80805e5069212bd16e3ab68 public boolean touchingUnloadedChunk() { AABB aabb = this.getBoundingBox().inflate(1.0); -@@ -4473,6 +4769,15 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess +@@ -4471,6 +4767,15 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess this.setPosRaw(x, y, z, false); } public final void setPosRaw(double x, double y, double z, boolean forceBoundingBoxUpdate) { @@ -29082,7 +29082,7 @@ index 45f69a914d5a0565196c4105d61541047301470f..f42bdae2f80805e5069212bd16e3ab68 if (!checkPosition(this, x, y, z)) { return; } -@@ -4603,6 +4908,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess +@@ -4601,6 +4906,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess @Override public final void setRemoved(Entity.RemovalReason removalReason, org.bukkit.event.entity.EntityRemoveEvent.Cause cause) { @@ -29095,7 +29095,7 @@ index 45f69a914d5a0565196c4105d61541047301470f..f42bdae2f80805e5069212bd16e3ab68 org.bukkit.craftbukkit.event.CraftEventFactory.callEntityRemoveEvent(this, cause); // CraftBukkit end final boolean alreadyRemoved = this.removalReason != null; // Paper - Folia schedulers -@@ -4614,7 +4925,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess +@@ -4612,7 +4923,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess this.stopRiding(); } @@ -29104,7 +29104,7 @@ index 45f69a914d5a0565196c4105d61541047301470f..f42bdae2f80805e5069212bd16e3ab68 this.levelCallback.onRemove(removalReason); this.onRemoval(removalReason); // Paper start - Folia schedulers -@@ -4648,7 +4959,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess +@@ -4646,7 +4957,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess public boolean shouldBeSaved() { return (this.removalReason == null || this.removalReason.shouldSave()) && !this.isPassenger() 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 5587c8828a..86388f04b1 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 @@ -46,6 +46,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; @@ -955,7 +956,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, false, false)) { return null; } @@ -988,7 +989,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()); } @@ -1224,17 +1225,19 @@ 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); + final boolean spawned = !this.entity.valid && this.entity.level().addFreshEntity(this.entity, reason); + if (!spawned) return false; // Do not attempt to spawn rest if root was not spawned in + // Like net.minecraft.world.level.ServerLevelAccessor.addFreshEntityWithPassengers(net.minecraft.world.entity.Entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason) + this.entity.getIndirectPassengers().forEach(e -> e.level().addFreshEntity(e, reason)); + return true; } - // 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..cb8d7fe3a0 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, false, false)) { 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..5f6d67b0ee 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 @@ -1,5 +1,6 @@ package org.bukkit.craftbukkit.util; +import ca.spottedleaf.moonrise.common.PlatformHooks; import com.google.common.base.Charsets; import com.google.common.base.Preconditions; import com.google.common.collect.Multimap; @@ -13,24 +14,34 @@ import com.mojang.serialization.Dynamic; import com.mojang.serialization.JsonOps; import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.EnumSet; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Stream; +import io.papermc.paper.entity.EntitySerializationFlag; import net.minecraft.SharedConstants; import net.minecraft.advancements.AdvancementHolder; import net.minecraft.commands.Commands; import net.minecraft.commands.arguments.item.ItemParser; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; import net.minecraft.nbt.NbtOps; import net.minecraft.nbt.StringTag; import net.minecraft.nbt.Tag; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; import net.minecraft.util.datafix.DataFixers; import net.minecraft.util.datafix.fixes.References; +import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.Item; import net.minecraft.world.item.alchemy.Potion; import net.minecraft.world.level.block.Block; @@ -43,6 +54,7 @@ import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.Registry; import org.bukkit.UnsafeValues; +import org.bukkit.World; import org.bukkit.advancement.Advancement; import org.bukkit.attribute.Attribute; import org.bukkit.attribute.AttributeModifier; @@ -51,10 +63,12 @@ import org.bukkit.block.data.BlockData; // import org.bukkit.craftbukkit.CraftFeatureFlag; // Paper import org.bukkit.craftbukkit.CraftRegistry; import org.bukkit.craftbukkit.CraftServer; +import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.craftbukkit.block.CraftBiome; import org.bukkit.craftbukkit.block.data.CraftBlockData; import org.bukkit.craftbukkit.damage.CraftDamageEffect; import org.bukkit.craftbukkit.damage.CraftDamageSourceBuilder; +import org.bukkit.craftbukkit.entity.CraftEntity; import org.bukkit.craftbukkit.inventory.CraftItemStack; import org.bukkit.craftbukkit.legacy.CraftLegacy; import org.bukkit.craftbukkit.legacy.FieldRename; @@ -513,7 +527,7 @@ public final class CraftMagicNumbers implements UnsafeValues { Preconditions.checkNotNull(item, "null cannot be serialized"); Preconditions.checkArgument(item.getType() != Material.AIR, "air cannot be serialized"); - return serializeNbtToBytes((net.minecraft.nbt.CompoundTag) (item instanceof CraftItemStack ? ((CraftItemStack) item).handle : CraftItemStack.asNMSCopy(item)).save(MinecraftServer.getServer().registryAccess())); + return serializeNbtToBytes((CompoundTag) (item instanceof CraftItemStack ? ((CraftItemStack) item).handle : CraftItemStack.asNMSCopy(item)).save(MinecraftServer.getServer().registryAccess())); } @Override @@ -521,9 +535,9 @@ public final class CraftMagicNumbers implements UnsafeValues { Preconditions.checkNotNull(data, "null cannot be deserialized"); Preconditions.checkArgument(data.length > 0, "cannot deserialize nothing"); - net.minecraft.nbt.CompoundTag compound = deserializeNbtFromBytes(data); + CompoundTag compound = deserializeNbtFromBytes(data); final int dataVersion = compound.getInt("DataVersion"); - compound = ca.spottedleaf.moonrise.common.PlatformHooks.get().convertNBT(References.ITEM_STACK, MinecraftServer.getServer().fixerUpper, compound, dataVersion, this.getDataVersion()); // Paper - possibly use dataconverter + compound = PlatformHooks.get().convertNBT(References.ITEM_STACK, MinecraftServer.getServer().fixerUpper, compound, dataVersion, this.getDataVersion()); // Paper - possibly use dataconverter return CraftItemStack.asCraftMirror(net.minecraft.world.item.ItemStack.parse(MinecraftServer.getServer().registryAccess(), compound).orElseThrow()); } @@ -558,32 +572,98 @@ public final class CraftMagicNumbers implements UnsafeValues { } @Override - public byte[] serializeEntity(org.bukkit.entity.Entity entity) { + public byte[] serializeEntity(org.bukkit.entity.Entity 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 CraftEntity, "Only CraftEntities can be serialized"); - net.minecraft.nbt.CompoundTag compound = new net.minecraft.nbt.CompoundTag(); - ((org.bukkit.craftbukkit.entity.CraftEntity) entity).getHandle().serializeEntity(compound); + Set flags = Set.of(serializationFlags); + final boolean serializePassangers = flags.contains(EntitySerializationFlag.PASSENGERS); + final boolean forceSerialization = flags.contains(EntitySerializationFlag.FORCE); + final boolean allowPlayerSerialization = flags.contains(EntitySerializationFlag.PLAYER); + final boolean allowMiscSerialization = flags.contains(EntitySerializationFlag.MISC); + final boolean includeNonSaveable = allowPlayerSerialization || allowMiscSerialization; + + net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle(); + (serializePassangers ? nmsEntity.getSelfAndPassengers() : Stream.of(nmsEntity)).forEach(e -> { + // Ensure force flag is not needed + Preconditions.checkArgument( + (e.getBukkitEntity().isValid() && e.getBukkitEntity().isPersistent()) || forceSerialization, + "Cannot serialize invalid or non-persistent entity %s(%s) without the FORCE flag", + e.getType().toShortString(), + e.getStringUUID() + ); + + if (e instanceof Player) { + // Ensure player flag is not needed + Preconditions.checkArgument( + allowPlayerSerialization, + "Cannot serialize player(%s) without the PLAYER flag", + e.getStringUUID() + ); + } else { + // Ensure player flag is not needed + Preconditions.checkArgument( + nmsEntity.getType().canSerialize() || allowMiscSerialization, + "Cannot serialize misc non-saveable entity %s(%s) without the MISC flag", + e.getType().toShortString(), + e.getStringUUID() + ); + } + }); + + CompoundTag compound = new CompoundTag(); + if (serializePassangers) { + if (!nmsEntity.saveAsPassenger(compound, true, includeNonSaveable, forceSerialization)) { + throw new IllegalArgumentException("Couldn't serialize entity"); + } + } else { + List pass = new 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, 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); + 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 (!preserveUUID) { - // Generate a new UUID so we don't have to worry about deserializing the same entity twice - compound.remove("UUID"); + compound = PlatformHooks.get().convertNBT(References.ENTITY, MinecraftServer.getServer().fixerUpper, compound, dataVersion, this.getDataVersion()); // Paper - possibly use dataconverter + if (!preservePassengers) { + compound.remove("Passengers"); } - 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 = deserializeEntity(compound, ((CraftWorld) world).getHandle(), preserveUUID); + return nmsEntity.getBukkitEntity(); } - private byte[] serializeNbtToBytes(net.minecraft.nbt.CompoundTag compound) { + private net.minecraft.world.entity.Entity deserializeEntity(CompoundTag compound, ServerLevel world, boolean preserveUUID) { + if (!preserveUUID) { + // Generate a new UUID, so we don't have to worry about deserializing the same entity twice + compound.remove("UUID"); + } + 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)) { + ListTag passengersCompound = compound.getList("Passengers", Tag.TAG_COMPOUND); + for (Tag tag : passengersCompound) { + if (!(tag instanceof CompoundTag serializedPassenger)) { + continue; + } + net.minecraft.world.entity.Entity passengerEntity = deserializeEntity(serializedPassenger, world, preserveUUID); + passengerEntity.startRiding(nmsEntity, true); + } + } + return nmsEntity; + } + + private byte[] serializeNbtToBytes(CompoundTag compound) { compound.putInt("DataVersion", getDataVersion()); java.io.ByteArrayOutputStream outputStream = new java.io.ByteArrayOutputStream(); try { @@ -597,8 +677,8 @@ public final class CraftMagicNumbers implements UnsafeValues { return outputStream.toByteArray(); } - private net.minecraft.nbt.CompoundTag deserializeNbtFromBytes(byte[] data) { - net.minecraft.nbt.CompoundTag compound; + private CompoundTag deserializeNbtFromBytes(byte[] data) { + CompoundTag compound; try { compound = net.minecraft.nbt.NbtIo.readCompressed( new java.io.ByteArrayInputStream(data), net.minecraft.nbt.NbtAccounter.unlimitedHeap()