--- a/net/minecraft/world/level/BaseSpawner.java
+++ b/net/minecraft/world/level/BaseSpawner.java
@@ -30,27 +_,29 @@
     public static final String SPAWN_DATA_TAG = "SpawnData";
     private static final Logger LOGGER = LogUtils.getLogger();
     private static final int EVENT_SPAWN = 1;
-    private int spawnDelay = 20;
-    private SimpleWeightedRandomList<SpawnData> spawnPotentials = SimpleWeightedRandomList.empty();
+    public int spawnDelay = 20;
+    public SimpleWeightedRandomList<SpawnData> spawnPotentials = SimpleWeightedRandomList.empty();
     @Nullable
-    private SpawnData nextSpawnData;
+    public SpawnData nextSpawnData;
     private double spin;
     private double oSpin;
-    private int minSpawnDelay = 200;
-    private int maxSpawnDelay = 800;
-    private int spawnCount = 4;
+    public int minSpawnDelay = 200;
+    public int maxSpawnDelay = 800;
+    public int spawnCount = 4;
     @Nullable
     private Entity displayEntity;
-    private int maxNearbyEntities = 6;
-    private int requiredPlayerRange = 16;
-    private int spawnRange = 4;
+    public int maxNearbyEntities = 6;
+    public int requiredPlayerRange = 16;
+    public int spawnRange = 4;
+    private int tickDelay = 0; // Paper - Configurable mob spawner tick rate
 
     public void setEntityId(EntityType<?> type, @Nullable Level level, RandomSource random, BlockPos pos) {
         this.getOrCreateNextSpawnData(level, random, pos).getEntityToSpawn().putString("id", BuiltInRegistries.ENTITY_TYPE.getKey(type).toString());
+        this.spawnPotentials = SimpleWeightedRandomList.empty(); // CraftBukkit - SPIGOT-3496, MC-92282
     }
 
-    private boolean isNearPlayer(Level level, BlockPos pos) {
-        return level.hasNearbyAlivePlayer(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, this.requiredPlayerRange);
+    public boolean isNearPlayer(Level level, BlockPos pos) {
+        return level.hasNearbyAlivePlayerThatAffectsSpawning(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, this.requiredPlayerRange); // Paper - Affects Spawning API
     }
 
     public void clientTick(Level level, BlockPos pos) {
@@ -73,13 +_,19 @@
     }
 
     public void serverTick(ServerLevel serverLevel, 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 = serverLevel.paperConfig().tickRates.mobSpawner;
+        if (tickDelay == -1) { return; } // If disabled
+        // Paper end - Configurable mob spawner tick rate
         if (this.isNearPlayer(serverLevel, pos)) {
-            if (this.spawnDelay == -1) {
+            if (this.spawnDelay < -tickDelay) { // Paper - Configurable mob spawner tick rate
                 this.delay(serverLevel, pos);
             }
 
             if (this.spawnDelay > 0) {
-                this.spawnDelay--;
+                this.spawnDelay -= tickDelay; // Paper - Configurable mob spawner tick rate
             } else {
                 boolean flag = false;
                 RandomSource random = serverLevel.getRandom();
@@ -113,6 +_,21 @@
                             continue;
                         }
 
+                        // Paper start - PreCreatureSpawnEvent
+                        com.destroystokyo.paper.event.entity.PreSpawnerSpawnEvent event = new com.destroystokyo.paper.event.entity.PreSpawnerSpawnEvent(
+                            io.papermc.paper.util.MCUtil.toLocation(serverLevel, d, d1, d2),
+                            org.bukkit.craftbukkit.entity.CraftEntityType.minecraftToBukkit(optional.get()),
+                            io.papermc.paper.util.MCUtil.toLocation(serverLevel, pos)
+                        );
+                        if (!event.callEvent()) {
+                            flag = true;
+                            if (event.shouldAbortSpawn()) {
+                                break;
+                            }
+                            continue;
+                        }
+                        // Paper end - PreCreatureSpawnEvent
+
                         Entity entity = EntityType.loadEntityRecursive(entityToSpawn, serverLevel, EntitySpawnReason.SPAWNER, entity1 -> {
                             entity1.moveTo(d, d1, d2, entity1.getYRot(), entity1.getXRot());
                             return entity1;
@@ -133,6 +_,7 @@
                             return;
                         }
 
+                        entity.preserveMotion = true; // Paper - Fix Entity Teleportation and cancel velocity if teleported; preserve entity motion from tag
                         entity.moveTo(entity.getX(), entity.getY(), entity.getZ(), random.nextFloat() * 360.0F, 0.0F);
                         if (entity instanceof Mob mob) {
                             if (nextSpawnData.getCustomSpawnRules().isEmpty() && !mob.checkSpawnRules(serverLevel, EntitySpawnReason.SPAWNER)
@@ -147,9 +_,22 @@
                             }
 
                             nextSpawnData.getEquipment().ifPresent(mob::equip);
+                            // Spigot start
+                            if (mob.level().spigotConfig.nerfSpawnerMobs) {
+                                mob.aware = false;
+                            }
+                            // Spigot end
                         }
 
-                        if (!serverLevel.tryAddFreshEntityWithPassengers(entity)) {
+                        entity.spawnedViaMobSpawner = true; // Paper
+                        entity.spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.SPAWNER; // Paper - Entity#getEntitySpawnReason
+                        flag = true; // Paper
+                        // CraftBukkit start
+                        if (org.bukkit.craftbukkit.event.CraftEventFactory.callSpawnerSpawnEvent(entity, pos).isCancelled()) {
+                            continue;
+                        }
+                        if (!serverLevel.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.SPAWNER)) {
+                            // CraftBukkit end
                             this.delay(serverLevel, pos);
                             return;
                         }
@@ -160,7 +_,7 @@
                             ((Mob)entity).spawnAnim();
                         }
 
-                        flag = true;
+                        //flag = true; // Paper - moved up above cancellable event
                     }
                 }
 
@@ -171,7 +_,7 @@
         }
     }
 
-    private void delay(Level level, BlockPos pos) {
+    public void delay(Level level, BlockPos pos) {
         RandomSource randomSource = level.random;
         if (this.maxSpawnDelay <= this.minSpawnDelay) {
             this.spawnDelay = this.minSpawnDelay;
@@ -184,7 +_,13 @@
     }
 
     public void load(@Nullable Level level, BlockPos pos, CompoundTag tag) {
+        // Paper start - use larger int if set
+        if (tag.contains("Paper.Delay")) {
+            this.spawnDelay = tag.getInt("Paper.Delay");
+        } else {
         this.spawnDelay = tag.getShort("Delay");
+        }
+        // Paper end
         boolean flag = tag.contains("SpawnData", 10);
         if (flag) {
             SpawnData spawnData = SpawnData.CODEC
@@ -205,9 +_,15 @@
             this.spawnPotentials = SimpleWeightedRandomList.single(this.nextSpawnData != null ? this.nextSpawnData : new SpawnData());
         }
 
+        // Paper start - use ints if set
+        if (tag.contains("Paper.MinSpawnDelay", net.minecraft.nbt.Tag.TAG_ANY_NUMERIC)) {
+            this.minSpawnDelay = tag.getInt("Paper.MinSpawnDelay");
+            this.maxSpawnDelay = tag.getInt("Paper.MaxSpawnDelay");
+            this.spawnCount = tag.getShort("SpawnCount");
+        } else // Paper end
         if (tag.contains("MinSpawnDelay", 99)) {
-            this.minSpawnDelay = tag.getShort("MinSpawnDelay");
-            this.maxSpawnDelay = tag.getShort("MaxSpawnDelay");
+            this.minSpawnDelay = tag.getInt("MinSpawnDelay"); // Paper - short -> int
+            this.maxSpawnDelay = tag.getInt("MaxSpawnDelay"); // Paper - short -> int
             this.spawnCount = tag.getShort("SpawnCount");
         }
 
@@ -224,9 +_,20 @@
     }
 
     public CompoundTag save(CompoundTag tag) {
-        tag.putShort("Delay", (short)this.spawnDelay);
-        tag.putShort("MinSpawnDelay", (short)this.minSpawnDelay);
-        tag.putShort("MaxSpawnDelay", (short)this.maxSpawnDelay);
+        // Paper start
+        if (spawnDelay > Short.MAX_VALUE) {
+            tag.putInt("Paper.Delay", this.spawnDelay);
+        }
+        tag.putShort("Delay", (short) Math.min(Short.MAX_VALUE, this.spawnDelay));
+
+        if (minSpawnDelay > Short.MAX_VALUE || maxSpawnDelay > Short.MAX_VALUE) {
+            tag.putInt("Paper.MinSpawnDelay", this.minSpawnDelay);
+            tag.putInt("Paper.MaxSpawnDelay", this.maxSpawnDelay);
+        }
+
+        tag.putShort("MinSpawnDelay", (short) Math.min(Short.MAX_VALUE, this.minSpawnDelay));
+        tag.putShort("MaxSpawnDelay", (short) Math.min(Short.MAX_VALUE, this.maxSpawnDelay));
+        // Paper end
         tag.putShort("SpawnCount", (short)this.spawnCount);
         tag.putShort("MaxNearbyEntities", (short)this.maxNearbyEntities);
         tag.putShort("RequiredPlayerRange", (short)this.requiredPlayerRange);
@@ -272,7 +_,7 @@
         }
     }
 
-    protected void setNextSpawnData(@Nullable Level level, BlockPos pos, SpawnData nextSpawnData) {
+    public void setNextSpawnData(@Nullable Level level, BlockPos pos, SpawnData nextSpawnData) {
         this.nextSpawnData = nextSpawnData;
     }
 
