--- a/net/minecraft/world/entity/EntityType.java
+++ b/net/minecraft/world/entity/EntityType.java
@@ -1265,6 +_,22 @@
         boolean shouldOffsetY,
         boolean shouldOffsetYMore
     ) {
+        // CraftBukkit start
+        return this.spawn(level, spawnedFrom, owner, pos, reason, shouldOffsetY, shouldOffsetYMore, reason == EntitySpawnReason.DISPENSER ? org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DISPENSE_EGG : org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.SPAWNER_EGG); // Paper - use correct spawn reason for dispenser spawn eggs
+    }
+
+    @Nullable
+    public T spawn(
+        ServerLevel level,
+        @Nullable ItemStack spawnedFrom,
+        @Nullable LivingEntity owner,
+        BlockPos pos,
+        EntitySpawnReason reason,
+        boolean shouldOffsetY,
+        boolean shouldOffsetYMore,
+        org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason createSpawnReason
+    ) {
+        // CraftBukkit end
         Consumer<T> consumer;
         if (spawnedFrom != null) {
             consumer = createDefaultStackConfig(level, spawnedFrom, owner);
@@ -1272,7 +_,7 @@
             consumer = entity -> {};
         }
 
-        return this.spawn(level, consumer, pos, reason, shouldOffsetY, shouldOffsetYMore);
+        return this.spawn(level, consumer, pos, reason, shouldOffsetY, shouldOffsetYMore, createSpawnReason); // CraftBukkit
     }
 
     public static <T extends Entity> Consumer<T> createDefaultStackConfig(Level level, ItemStack stack, @Nullable LivingEntity owner) {
@@ -1289,19 +_,56 @@
 
     public static <T extends Entity> Consumer<T> appendCustomEntityStackConfig(Consumer<T> consumer, Level level, ItemStack stack, @Nullable LivingEntity owner) {
         TypedEntityData<EntityType<?>> typedEntityData = stack.get(DataComponents.ENTITY_DATA);
-        return typedEntityData != null ? consumer.andThen(entity -> updateCustomEntityTag(level, owner, entity, typedEntityData)) : consumer;
+        // CraftBukkit start - SPIGOT-5665
+        return typedEntityData != null ? consumer.andThen(entity -> {
+            try {
+                updateCustomEntityTag(level, owner, entity, typedEntityData);
+            } catch (Throwable t) {
+                EntityType.LOGGER.warn("Error loading spawn egg NBT", t);
+            }
+        }) : consumer;
+        // CraftBukkit end
     }
 
     @Nullable
     public T spawn(ServerLevel level, BlockPos pos, EntitySpawnReason reason) {
-        return this.spawn(level, null, pos, reason, false, false);
+        // CraftBukkit start
+        return this.spawn(level, pos, reason, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT);
+    }
+
+    @Nullable
+    public T spawn(ServerLevel level, BlockPos pos, EntitySpawnReason reason, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason creatureSpawnReason) {
+        return this.spawn(level, null, pos, reason, false, false, creatureSpawnReason);
+        // CraftBukkit End
     }
 
     @Nullable
     public T spawn(ServerLevel level, @Nullable Consumer<T> consumer, BlockPos pos, EntitySpawnReason reason, boolean shouldOffsetY, boolean shouldOffsetYMore) {
+        // CraftBukkit start
+        return this.spawn(level, consumer, pos, reason, shouldOffsetY, shouldOffsetYMore, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT);
+    }
+
+    @Nullable
+    public T spawn(ServerLevel level, @Nullable Consumer<T> consumer, BlockPos pos, EntitySpawnReason reason, boolean shouldOffsetY, boolean shouldOffsetYMore, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason creatureSpawnReason) {
+        // CraftBukkit end
+        // Paper start - PreCreatureSpawnEvent
+        com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent event = new com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent(
+            org.bukkit.craftbukkit.util.CraftLocation.toBukkit(pos, level),
+            org.bukkit.craftbukkit.entity.CraftEntityType.minecraftToBukkit(this),
+            creatureSpawnReason
+        );
+        if (!event.callEvent()) {
+            return null;
+        }
+        // Paper end - PreCreatureSpawnEvent
         T entity = this.create(level, consumer, pos, reason, shouldOffsetY, shouldOffsetYMore);
         if (entity != null) {
-            level.addFreshEntityWithPassengers(entity);
+            // CraftBukkit start
+            level.addFreshEntityWithPassengers(entity, creatureSpawnReason);
+            if (entity.isRemoved()) {
+                return null; // Don't return an entity when CreatureSpawnEvent is canceled
+            }
+            // CraftBukkit end
             if (entity instanceof Mob mob) {
                 mob.playAmbientSound();
             }
@@ -1356,6 +_,13 @@
                 if (level.isClientSide()
                     || !entity.getType().onlyOpCanSetNbt()
                     || owner instanceof Player player && server.getPlayerList().isOp(player.nameAndId())) {
+                    // Paper start - filter out protected tags
+                    if (owner == null || !owner.getBukkitEntity().hasPermission("minecraft.nbt.place")) {
+                        for (net.minecraft.commands.arguments.NbtPathArgument.NbtPath tag : level.paperConfig().entities.spawning.filteredEntityTagNbtPaths) {
+                            tag.remove(data.getUnsafe());
+                        }
+                    }
+                    // Paper end - filter out protected tags
                     data.loadInto(entity);
                 }
             }
@@ -1427,9 +_,20 @@
     }
 
     public static Optional<Entity> create(ValueInput input, Level level, EntitySpawnReason spawnReason) {
+        // Paper start - Don't fire sync event during generation
+        return create(input, level, spawnReason, false);
+    }
+
+    public static Optional<Entity> create(ValueInput input, Level level, EntitySpawnReason spawnReason, boolean generation) {
+        // Paper end - Don't fire sync event during generation
         return Util.ifElse(
             by(input).map(entityType -> entityType.create(level, spawnReason)),
-            entity -> entity.load(input),
+            // Paper start - Don't fire sync event during generation
+            entity -> {
+                if (generation) entity.generation = true; // Paper - Don't fire sync event during generation
+                entity.load(input);
+            },
+            // Paper end - Don't fire sync event during generation
             () -> LOGGER.warn("Skipping Entity with id {}", input.getStringOr("id", "[invalid]"))
         );
     }
@@ -1519,7 +_,18 @@
     }
 
     public static Stream<Entity> loadEntitiesRecursive(ValueInput.ValueInputList input, Level level, EntitySpawnReason spawnReason) {
+        final java.util.Map<EntityType<?>, Integer> loadedEntityCounts = new java.util.HashMap<>(); // Paper - Entity load/save limit per chunk
         return input.stream().mapMulti((valueInput, consumer) -> loadEntityRecursive(valueInput, level, spawnReason, entity -> {
+            // Paper start - Entity load/save limit per chunk
+            final EntityType<?> entityType = entity.getType();
+            final int saveLimit = level.paperConfig().chunks.entityPerChunkSaveLimit.getOrDefault(entityType, -1);
+            if (saveLimit > -1) {
+                if (loadedEntityCounts.getOrDefault(entityType, 0) >= saveLimit) {
+                    return null;
+                }
+                loadedEntityCounts.merge(entityType, 1, Integer::sum);
+            }
+            // Paper end - Entity load/save limit per chunk
             consumer.accept(entity);
             return entity;
         }));
@@ -1588,8 +_,23 @@
         return this.builtInRegistryHolder;
     }
 
-    public boolean isAllowedInPeaceful() {
+    // Paper start
+    public final boolean isTypeAllowedInPeaceful() {
         return this.allowedInPeaceful;
+    }
+    public final boolean isAllowedInPeaceful(@Nullable CompoundTag tag) {
+        if (tag == null || tag.isEmpty()) {
+            return this.isTypeAllowedInPeaceful();
+        }
+        final var input = TagValueInput.create(net.minecraft.util.ProblemReporter.DISCARDING, MinecraftServer.getServer().registryAccess(), tag);
+        final var override = Mob.readDespawnInPeacefulOverride(input);
+        return !override.toBooleanOrElse(!this.isTypeAllowedInPeaceful());
+    }
+    // Audit all vanilla uses of this method to ensure per-entity override API applies where possible
+    @io.papermc.paper.annotation.DoNotUse
+    public final boolean isAllowedInPeaceful() {
+        return this.isTypeAllowedInPeaceful();
+        // Paper end
     }
 
     private static EntityType.EntityFactory<Boat> boatFactory(Supplier<Item> boatItemGetter) {
