--- a/net/minecraft/world/level/BaseSpawner.java
+++ b/net/minecraft/world/level/BaseSpawner.java
@@ -10,6 +10,7 @@
 import net.minecraft.nbt.CompoundTag;
 import net.minecraft.nbt.ListTag;
 import net.minecraft.nbt.NbtOps;
+import net.minecraft.nbt.Tag;
 import net.minecraft.server.level.ServerLevel;
 import net.minecraft.util.RandomSource;
 import net.minecraft.util.random.SimpleWeightedRandomList;
@@ -26,6 +27,7 @@
 import org.slf4j.Logger;
 
 public abstract class BaseSpawner {
+
     public static final String SPAWN_DATA_TAG = "SpawnData";
     private static final Logger LOGGER = LogUtils.getLogger();
     private static final int EVENT_SPAWN = 1;
@@ -43,119 +45,173 @@
     public int maxNearbyEntities = 6;
     public int requiredPlayerRange = 16;
     public int spawnRange = 4;
+    private int tickDelay = 0; // Paper
+
+    public BaseSpawner() {}
 
     public void setEntityId(EntityType<?> type, @Nullable Level world, RandomSource random, BlockPos pos) {
         this.getOrCreateNextSpawnData(world, random, pos).getEntityToSpawn().putString("id", BuiltInRegistries.ENTITY_TYPE.getKey(type).toString());
+        this.spawnPotentials = SimpleWeightedRandomList.empty(); // CraftBukkit - SPIGOT-3496, MC-92282
     }
 
     public boolean isNearPlayer(Level world, BlockPos pos) {
-        return world.hasNearbyAlivePlayer((double)pos.getX() + 0.5D, (double)pos.getY() + 0.5D, (double)pos.getZ() + 0.5D, (double)this.requiredPlayerRange);
+        return world.hasNearbyAlivePlayerThatAffectsSpawning((double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D, (double) this.requiredPlayerRange); // Paper - Affects Spawning API
     }
 
     public void clientTick(Level world, BlockPos pos) {
         if (!this.isNearPlayer(world, pos)) {
             this.oSpin = this.spin;
         } else if (this.displayEntity != null) {
-            RandomSource randomSource = world.getRandom();
-            double d = (double)pos.getX() + randomSource.nextDouble();
-            double e = (double)pos.getY() + randomSource.nextDouble();
-            double f = (double)pos.getZ() + randomSource.nextDouble();
-            world.addParticle(ParticleTypes.SMOKE, d, e, f, 0.0D, 0.0D, 0.0D);
-            world.addParticle(ParticleTypes.FLAME, d, e, f, 0.0D, 0.0D, 0.0D);
+            RandomSource randomsource = world.getRandom();
+            double d0 = (double) pos.getX() + randomsource.nextDouble();
+            double d1 = (double) pos.getY() + randomsource.nextDouble();
+            double d2 = (double) pos.getZ() + randomsource.nextDouble();
+
+            world.addParticle(ParticleTypes.SMOKE, d0, d1, d2, 0.0D, 0.0D, 0.0D);
+            world.addParticle(ParticleTypes.FLAME, d0, d1, d2, 0.0D, 0.0D, 0.0D);
             if (this.spawnDelay > 0) {
                 --this.spawnDelay;
             }
 
             this.oSpin = this.spin;
-            this.spin = (this.spin + (double)(1000.0F / ((float)this.spawnDelay + 200.0F))) % 360.0D;
+            this.spin = (this.spin + (double) (1000.0F / ((float) this.spawnDelay + 200.0F))) % 360.0D;
         }
 
     }
 
     public void serverTick(ServerLevel world, BlockPos pos) {
+        if (spawnCount <= 0 || maxNearbyEntities <= 0) return; // Paper - Ignore impossible spawn tick
+        // Paper start - Configurable mob spawner tick rate
+        if (spawnDelay > 0 && --tickDelay > 0) return;
+        tickDelay = world.paperConfig().tickRates.mobSpawner;
+        if (tickDelay == -1) { return; } // If disabled
+        // Paper end
         if (this.isNearPlayer(world, pos)) {
-            if (this.spawnDelay == -1) {
+            if (this.spawnDelay < -tickDelay) {
                 this.delay(world, pos);
             }
 
             if (this.spawnDelay > 0) {
-                --this.spawnDelay;
+                this.spawnDelay -= tickDelay; // Paper
             } else {
-                boolean bl = false;
-                RandomSource randomSource = world.getRandom();
-                SpawnData spawnData = this.getOrCreateNextSpawnData(world, randomSource, pos);
+                boolean flag = false;
+                RandomSource randomsource = world.getRandom();
+                SpawnData mobspawnerdata = this.getOrCreateNextSpawnData(world, randomsource, pos);
+
+                for (int i = 0; i < this.spawnCount; ++i) {
+                    CompoundTag nbttagcompound = mobspawnerdata.getEntityToSpawn();
+                    Optional<EntityType<?>> optional = EntityType.by(nbttagcompound);
 
-                for(int i = 0; i < this.spawnCount; ++i) {
-                    CompoundTag compoundTag = spawnData.getEntityToSpawn();
-                    Optional<EntityType<?>> optional = EntityType.by(compoundTag);
                     if (optional.isEmpty()) {
                         this.delay(world, pos);
                         return;
                     }
 
-                    ListTag listTag = compoundTag.getList("Pos", 6);
-                    int j = listTag.size();
-                    double d = j >= 1 ? listTag.getDouble(0) : (double)pos.getX() + (randomSource.nextDouble() - randomSource.nextDouble()) * (double)this.spawnRange + 0.5D;
-                    double e = j >= 2 ? listTag.getDouble(1) : (double)(pos.getY() + randomSource.nextInt(3) - 1);
-                    double f = j >= 3 ? listTag.getDouble(2) : (double)pos.getZ() + (randomSource.nextDouble() - randomSource.nextDouble()) * (double)this.spawnRange + 0.5D;
-                    if (world.noCollision(optional.get().getAABB(d, e, f))) {
-                        BlockPos blockPos = BlockPos.containing(d, e, f);
-                        if (spawnData.getCustomSpawnRules().isPresent()) {
-                            if (!optional.get().getCategory().isFriendly() && world.getDifficulty() == Difficulty.PEACEFUL) {
+                    ListTag nbttaglist = nbttagcompound.getList("Pos", 6);
+                    int j = nbttaglist.size();
+                    double d0 = j >= 1 ? nbttaglist.getDouble(0) : (double) pos.getX() + (randomsource.nextDouble() - randomsource.nextDouble()) * (double) this.spawnRange + 0.5D;
+                    double d1 = j >= 2 ? nbttaglist.getDouble(1) : (double) (pos.getY() + randomsource.nextInt(3) - 1);
+                    double d2 = j >= 3 ? nbttaglist.getDouble(2) : (double) pos.getZ() + (randomsource.nextDouble() - randomsource.nextDouble()) * (double) this.spawnRange + 0.5D;
+
+                    if (world.noCollision(((EntityType) optional.get()).getAABB(d0, d1, d2))) {
+                        BlockPos blockposition1 = BlockPos.containing(d0, d1, d2);
+
+                        if (mobspawnerdata.getCustomSpawnRules().isPresent()) {
+                            if (!((EntityType) optional.get()).getCategory().isFriendly() && world.getDifficulty() == Difficulty.PEACEFUL) {
                                 continue;
                             }
 
-                            SpawnData.CustomSpawnRules customSpawnRules = spawnData.getCustomSpawnRules().get();
-                            if (!customSpawnRules.blockLightLimit().isValueInRange(world.getBrightness(LightLayer.BLOCK, blockPos)) || !customSpawnRules.skyLightLimit().isValueInRange(world.getBrightness(LightLayer.SKY, blockPos))) {
+                            SpawnData.CustomSpawnRules mobspawnerdata_a = (SpawnData.CustomSpawnRules) mobspawnerdata.getCustomSpawnRules().get();
+
+                            if (!mobspawnerdata_a.blockLightLimit().isValueInRange(world.getBrightness(LightLayer.BLOCK, blockposition1)) || !mobspawnerdata_a.skyLightLimit().isValueInRange(world.getBrightness(LightLayer.SKY, blockposition1))) {
                                 continue;
                             }
-                        } else if (!SpawnPlacements.checkSpawnRules(optional.get(), world, MobSpawnType.SPAWNER, blockPos, world.getRandom())) {
+                        } else if (!SpawnPlacements.checkSpawnRules((EntityType) optional.get(), world, MobSpawnType.SPAWNER, blockposition1, world.getRandom())) {
                             continue;
                         }
+                        // Paper start
+                        EntityType<?> entityType = optional.get();
+                        String key = EntityType.getKey(entityType).getPath();
 
-                        Entity entity = EntityType.loadEntityRecursive(compoundTag, world, (entityx) -> {
-                            entityx.moveTo(d, e, f, entityx.getYRot(), entityx.getXRot());
-                            return entityx;
+                        org.bukkit.entity.EntityType type = org.bukkit.entity.EntityType.fromName(key);
+                        if (type != null) {
+                            com.destroystokyo.paper.event.entity.PreSpawnerSpawnEvent event;
+                            event = new com.destroystokyo.paper.event.entity.PreSpawnerSpawnEvent(
+                                io.papermc.paper.util.MCUtil.toLocation(world, d0, d1, d2),
+                                type,
+                                io.papermc.paper.util.MCUtil.toLocation(world, pos)
+                            );
+                            if (!event.callEvent()) {
+                                flag = true;
+                                if (event.shouldAbortSpawn()) {
+                                    break;
+                                }
+                                continue;
+                            }
+                        }
+                        // Paper end
+
+                        Entity entity = EntityType.loadEntityRecursive(nbttagcompound, world, (entity1) -> {
+                            entity1.moveTo(d0, d1, d2, entity1.getYRot(), entity1.getXRot());
+                            return entity1;
                         });
+
                         if (entity == null) {
                             this.delay(world, pos);
                             return;
                         }
 
-                        int k = world.getEntitiesOfClass(entity.getClass(), (new AABB((double)pos.getX(), (double)pos.getY(), (double)pos.getZ(), (double)(pos.getX() + 1), (double)(pos.getY() + 1), (double)(pos.getZ() + 1))).inflate((double)this.spawnRange)).size();
+                        int k = world.getEntitiesOfClass(entity.getClass(), (new AABB((double) pos.getX(), (double) pos.getY(), (double) pos.getZ(), (double) (pos.getX() + 1), (double) (pos.getY() + 1), (double) (pos.getZ() + 1))).inflate((double) this.spawnRange)).size();
+
                         if (k >= this.maxNearbyEntities) {
                             this.delay(world, pos);
                             return;
                         }
 
-                        entity.moveTo(entity.getX(), entity.getY(), entity.getZ(), randomSource.nextFloat() * 360.0F, 0.0F);
+                        entity.preserveMotion = true; // Paper - preserve entity motion from tag
+                        entity.moveTo(entity.getX(), entity.getY(), entity.getZ(), randomsource.nextFloat() * 360.0F, 0.0F);
                         if (entity instanceof Mob) {
-                            Mob mob = (Mob)entity;
-                            if (spawnData.getCustomSpawnRules().isEmpty() && !mob.checkSpawnRules(world, MobSpawnType.SPAWNER) || !mob.checkSpawnObstruction(world)) {
+                            Mob entityinsentient = (Mob) entity;
+
+                            if (mobspawnerdata.getCustomSpawnRules().isEmpty() && !entityinsentient.checkSpawnRules(world, MobSpawnType.SPAWNER) || !entityinsentient.checkSpawnObstruction(world)) {
                                 continue;
                             }
 
-                            if (spawnData.getEntityToSpawn().size() == 1 && spawnData.getEntityToSpawn().contains("id", 8)) {
-                                ((Mob)entity).finalizeSpawn(world, world.getCurrentDifficultyAt(entity.blockPosition()), MobSpawnType.SPAWNER, (SpawnGroupData)null, (CompoundTag)null);
+                            if (mobspawnerdata.getEntityToSpawn().size() == 1 && mobspawnerdata.getEntityToSpawn().contains("id", 8)) {
+                                ((Mob) entity).finalizeSpawn(world, world.getCurrentDifficultyAt(entity.blockPosition()), MobSpawnType.SPAWNER, (SpawnGroupData) null, (CompoundTag) null);
+                            }
+                            // Spigot Start
+                            if ( entityinsentient.level().spigotConfig.nerfSpawnerMobs )
+                            {
+                                entityinsentient.aware = false;
                             }
+                            // Spigot End
                         }
 
-                        if (!world.tryAddFreshEntityWithPassengers(entity)) {
+                        entity.spawnedViaMobSpawner = true; // Paper
+                        entity.spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.SPAWNER; // Paper
+                        flag = true; // Paper
+                        // CraftBukkit start
+                        if (org.bukkit.craftbukkit.v1_20_R2.event.CraftEventFactory.callSpawnerSpawnEvent(entity, pos).isCancelled()) {
+                            continue;
+                        }
+                        if (!world.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.SPAWNER)) {
+                            // CraftBukkit end
                             this.delay(world, pos);
                             return;
                         }
 
                         world.levelEvent(2004, pos, 0);
-                        world.gameEvent(entity, GameEvent.ENTITY_PLACE, blockPos);
+                        world.gameEvent(entity, GameEvent.ENTITY_PLACE, blockposition1);
                         if (entity instanceof Mob) {
-                            ((Mob)entity).spawnAnim();
+                            ((Mob) entity).spawnAnim();
                         }
 
-                        bl = true;
+                        //flag = true; // Paper - moved up above cancellable event
                     }
                 }
 
-                if (bl) {
+                if (flag) {
                     this.delay(world, pos);
                 }
 
@@ -164,42 +220,59 @@
     }
 
     public void delay(Level world, BlockPos pos) {
-        RandomSource randomSource = world.random;
+        RandomSource randomsource = world.random;
+
         if (this.maxSpawnDelay <= this.minSpawnDelay) {
             this.spawnDelay = this.minSpawnDelay;
         } else {
-            this.spawnDelay = this.minSpawnDelay + randomSource.nextInt(this.maxSpawnDelay - this.minSpawnDelay);
+            this.spawnDelay = this.minSpawnDelay + randomsource.nextInt(this.maxSpawnDelay - this.minSpawnDelay);
         }
 
-        this.spawnPotentials.getRandom(randomSource).ifPresent((spawnPotential) -> {
-            this.setNextSpawnData(world, pos, spawnPotential.getData());
+        this.spawnPotentials.getRandom(randomsource).ifPresent((weightedentry_b) -> {
+            this.setNextSpawnData(world, pos, (SpawnData) weightedentry_b.getData());
         });
         this.broadcastEvent(world, pos, 1);
     }
 
     public void load(@Nullable Level world, BlockPos pos, CompoundTag nbt) {
+        // Paper start - use larger int if set
+        if (nbt.contains("Paper.Delay")) {
+            this.spawnDelay = nbt.getInt("Paper.Delay");
+        } else {
         this.spawnDelay = nbt.getShort("Delay");
-        boolean bl = nbt.contains("SpawnData", 10);
-        if (bl) {
-            SpawnData spawnData = SpawnData.CODEC.parse(NbtOps.INSTANCE, nbt.getCompound("SpawnData")).resultOrPartial((string) -> {
-                LOGGER.warn("Invalid SpawnData: {}", (Object)string);
+        }
+        // Paper end
+        boolean flag = nbt.contains("SpawnData", 10);
+
+        if (flag) {
+            SpawnData mobspawnerdata = (SpawnData) SpawnData.CODEC.parse(NbtOps.INSTANCE, nbt.getCompound("SpawnData")).resultOrPartial((s) -> {
+                BaseSpawner.LOGGER.warn("Invalid SpawnData: {}", s);
             }).orElseGet(SpawnData::new);
-            this.setNextSpawnData(world, pos, spawnData);
+
+            this.setNextSpawnData(world, pos, mobspawnerdata);
         }
 
-        boolean bl2 = nbt.contains("SpawnPotentials", 9);
-        if (bl2) {
-            ListTag listTag = nbt.getList("SpawnPotentials", 10);
-            this.spawnPotentials = SpawnData.LIST_CODEC.parse(NbtOps.INSTANCE, listTag).resultOrPartial((error) -> {
-                LOGGER.warn("Invalid SpawnPotentials list: {}", (Object)error);
+        boolean flag1 = nbt.contains("SpawnPotentials", 9);
+
+        if (flag1) {
+            ListTag nbttaglist = nbt.getList("SpawnPotentials", 10);
+
+            this.spawnPotentials = (SimpleWeightedRandomList) SpawnData.LIST_CODEC.parse(NbtOps.INSTANCE, nbttaglist).resultOrPartial((s) -> {
+                BaseSpawner.LOGGER.warn("Invalid SpawnPotentials list: {}", s);
             }).orElseGet(SimpleWeightedRandomList::empty);
         } else {
             this.spawnPotentials = SimpleWeightedRandomList.single(this.nextSpawnData != null ? this.nextSpawnData : new SpawnData());
         }
 
+        // Paper start - use ints if set
+        if (nbt.contains("Paper.MinSpawnDelay", 99)) {
+            this.minSpawnDelay = nbt.getInt("Paper.MinSpawnDelay");
+            this.maxSpawnDelay = nbt.getInt("Paper.MaxSpawnDelay");
+            this.spawnCount = nbt.getShort("SpawnCount");
+        } else // Paper end
         if (nbt.contains("MinSpawnDelay", 99)) {
-            this.minSpawnDelay = nbt.getShort("MinSpawnDelay");
-            this.maxSpawnDelay = nbt.getShort("MaxSpawnDelay");
+            this.minSpawnDelay = nbt.getInt("MinSpawnDelay"); // Paper - short -> int
+            this.maxSpawnDelay = nbt.getInt("MaxSpawnDelay"); // Paper - short -> int
             this.spawnCount = nbt.getShort("SpawnCount");
         }
 
@@ -216,33 +289,46 @@
     }
 
     public CompoundTag save(CompoundTag nbt) {
-        nbt.putShort("Delay", (short)this.spawnDelay);
-        nbt.putShort("MinSpawnDelay", (short)this.minSpawnDelay);
-        nbt.putShort("MaxSpawnDelay", (short)this.maxSpawnDelay);
-        nbt.putShort("SpawnCount", (short)this.spawnCount);
-        nbt.putShort("MaxNearbyEntities", (short)this.maxNearbyEntities);
-        nbt.putShort("RequiredPlayerRange", (short)this.requiredPlayerRange);
-        nbt.putShort("SpawnRange", (short)this.spawnRange);
+        // Paper start
+        if (spawnDelay > Short.MAX_VALUE) {
+            nbt.putInt("Paper.Delay", this.spawnDelay);
+        }
+        nbt.putShort("Delay", (short) Math.min(Short.MAX_VALUE, this.spawnDelay));
+
+        if (minSpawnDelay > Short.MAX_VALUE || maxSpawnDelay > Short.MAX_VALUE) {
+            nbt.putInt("Paper.MinSpawnDelay", this.minSpawnDelay);
+            nbt.putInt("Paper.MaxSpawnDelay", this.maxSpawnDelay);
+        }
+
+        nbt.putShort("MinSpawnDelay", (short) Math.min(Short.MAX_VALUE, this.minSpawnDelay));
+        nbt.putShort("MaxSpawnDelay", (short) Math.min(Short.MAX_VALUE, this.maxSpawnDelay));
+        // Paper nbt
+        nbt.putShort("SpawnCount", (short) this.spawnCount);
+        nbt.putShort("MaxNearbyEntities", (short) this.maxNearbyEntities);
+        nbt.putShort("RequiredPlayerRange", (short) this.requiredPlayerRange);
+        nbt.putShort("SpawnRange", (short) this.spawnRange);
         if (this.nextSpawnData != null) {
-            nbt.put("SpawnData", SpawnData.CODEC.encodeStart(NbtOps.INSTANCE, this.nextSpawnData).result().orElseThrow(() -> {
+            nbt.put("SpawnData", (Tag) SpawnData.CODEC.encodeStart(NbtOps.INSTANCE, this.nextSpawnData).result().orElseThrow(() -> {
                 return new IllegalStateException("Invalid SpawnData");
             }));
         }
 
-        nbt.put("SpawnPotentials", SpawnData.LIST_CODEC.encodeStart(NbtOps.INSTANCE, this.spawnPotentials).result().orElseThrow());
+        nbt.put("SpawnPotentials", (Tag) SpawnData.LIST_CODEC.encodeStart(NbtOps.INSTANCE, this.spawnPotentials).result().orElseThrow());
         return nbt;
     }
 
     @Nullable
     public Entity getOrCreateDisplayEntity(Level world, RandomSource random, BlockPos pos) {
         if (this.displayEntity == null) {
-            CompoundTag compoundTag = this.getOrCreateNextSpawnData(world, random, pos).getEntityToSpawn();
-            if (!compoundTag.contains("id", 8)) {
+            CompoundTag nbttagcompound = this.getOrCreateNextSpawnData(world, random, pos).getEntityToSpawn();
+
+            if (!nbttagcompound.contains("id", 8)) {
                 return null;
             }
 
-            this.displayEntity = EntityType.loadEntityRecursive(compoundTag, world, Function.identity());
-            if (compoundTag.size() == 1 && this.displayEntity instanceof Mob) {
+            this.displayEntity = EntityType.loadEntityRecursive(nbttagcompound, world, Function.identity());
+            if (nbttagcompound.size() == 1 && this.displayEntity instanceof Mob) {
+                ;
             }
         }
 
@@ -269,7 +355,7 @@
         if (this.nextSpawnData != null) {
             return this.nextSpawnData;
         } else {
-            this.setNextSpawnData(world, pos, this.spawnPotentials.getRandom(random).map(WeightedEntry.Wrapper::getData).orElseGet(SpawnData::new));
+            this.setNextSpawnData(world, pos, (SpawnData) this.spawnPotentials.getRandom(random).map(WeightedEntry.Wrapper::getData).orElseGet(SpawnData::new));
             return this.nextSpawnData;
         }
     }
