--- a/net/minecraft/world/entity/monster/Slime.java
+++ b/net/minecraft/world/entity/monster/Slime.java
@@ -58,6 +_,7 @@
     public float squish;
     public float oSquish;
     private boolean wasOnGround = false;
+    private boolean canWander = true; // Paper - Slime pathfinder events
 
     public Slime(EntityType<? extends Slime> entityType, Level level) {
         super(entityType, level);
@@ -112,6 +_,7 @@
         super.addAdditionalSaveData(output);
         output.putInt("Size", this.getSize() - 1);
         output.putBoolean("wasOnGround", this.wasOnGround);
+        output.putBoolean("Paper.canWander", this.canWander); // Paper
     }
 
     @Override
@@ -119,6 +_,7 @@
         this.setSize(input.getIntOr("Size", 0) + 1, false);
         super.readAdditionalSaveData(input);
         this.wasOnGround = input.getBooleanOr("wasOnGround", false);
+        this.canWander = input.getBooleanOr("Paper.canWander", true); // Paper
     }
 
     public boolean isTiny() {
@@ -193,7 +_,7 @@
     }
 
     @Override
-    public void remove(Entity.RemovalReason reason) {
+    public void remove(Entity.RemovalReason reason, @Nullable org.bukkit.event.entity.EntityRemoveEvent.Cause eventCause) { // CraftBukkit - add Bukkit remove cause
         int size = this.getSize();
         if (!this.level().isClientSide() && size > 1 && this.isDeadOrDying()) {
             float width = this.getDimensions(this.getPose()).width();
@@ -201,18 +_,43 @@
             int i = size / 2;
             int i1 = 2 + this.random.nextInt(3);
             PlayerTeam team = this.getTeam();
+            // CraftBukkit start
+            org.bukkit.event.entity.SlimeSplitEvent event = new org.bukkit.event.entity.SlimeSplitEvent((org.bukkit.entity.Slime) this.getBukkitEntity(), i1);
+            if (event.callEvent() && event.getCount() > 0) {
+                i1 = event.getCount();
+            } else {
+                super.remove(reason, eventCause); // CraftBukkit - add Bukkit remove cause
+                return;
+            }
+
+            java.util.List<LivingEntity> slimes = new java.util.ArrayList<>(i1);
+            // CraftBukkit end
 
             for (int i2 = 0; i2 < i1; i2++) {
                 float f1 = (i2 % 2 - 0.5F) * f;
                 float f2 = (i2 / 2 - 0.5F) * f;
-                this.convertTo(this.getType(), new ConversionParams(ConversionType.SPLIT_ON_DEATH, false, false, team), EntitySpawnReason.TRIGGERED, mob -> {
+                Slime converted = this.convertTo(this.getType(), new ConversionParams(ConversionType.SPLIT_ON_DEATH, false, false, team), EntitySpawnReason.TRIGGERED, (mob) -> { // CraftBukkit
                     mob.setSize(i, true);
                     mob.snapTo(this.getX() + f1, this.getY() + 0.5, this.getZ() + f2, this.random.nextFloat() * 360.0F, 0.0F);
-                });
-            }
+                // CraftBukkit start
+                }, null, null);
+                if (converted != null) {
+                    slimes.add(converted);
+                }
+                // CraftBukkit end
+            }
+            // CraftBukkit start
+            if (!slimes.isEmpty() && org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTransformEvent(this, slimes, org.bukkit.event.entity.EntityTransformEvent.TransformReason.SPLIT).isCancelled()) { // check for empty converted entities or cancel event
+                super.remove(reason, eventCause); // add Bukkit remove cause
+                return;
+            }
+            for (LivingEntity living : slimes) {
+                this.level().addFreshEntity(living, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.SLIME_SPLIT);
+            }
+            // CraftBukkit end
         }
 
-        super.remove(reason);
+        super.remove(reason, eventCause); // CraftBukkit - add Bukkit remove cause
     }
 
     @Override
@@ -257,12 +_,12 @@
     }
 
     @Override
-    protected SoundEvent getHurtSound(DamageSource damageSource) {
+    public SoundEvent getHurtSound(DamageSource damageSource) {
         return this.isTiny() ? SoundEvents.SLIME_HURT_SMALL : SoundEvents.SLIME_HURT;
     }
 
     @Override
-    protected SoundEvent getDeathSound() {
+    public SoundEvent getDeathSound() {
         return this.isTiny() ? SoundEvents.SLIME_DEATH_SMALL : SoundEvents.SLIME_DEATH;
     }
 
@@ -278,9 +_,13 @@
                 return checkMobSpawnRules(entityType, level, spawnReason, pos, random);
             }
 
+            // Paper start - Replace rules for Height in Swamp Biomes
+            final double maxHeightSwamp = level.getMinecraftWorld().paperConfig().entities.spawning.slimeSpawnHeight.surfaceBiome.maximum;
+            final double minHeightSwamp = level.getMinecraftWorld().paperConfig().entities.spawning.slimeSpawnHeight.surfaceBiome.minimum;
+            // Paper end - Replace rules for Height in Swamp Biomes
             if (level.getBiome(pos).is(BiomeTags.ALLOWS_SURFACE_SLIME_SPAWNS)
-                && pos.getY() > 50
-                && pos.getY() < 70
+                && pos.getY() > minHeightSwamp // Paper - Replace rules for Height in Swamp Biomes
+                && pos.getY() < maxHeightSwamp // Paper - Replace rules for Height in Swamp Biomes
                 && random.nextFloat() < 0.5F
                 && random.nextFloat() < level.getMoonBrightness()
                 && level.getMaxLocalRawBrightness(pos) <= random.nextInt(8)) {
@@ -292,8 +_,11 @@
             }
 
             ChunkPos chunkPos = new ChunkPos(pos);
-            boolean flag = WorldgenRandom.seedSlimeChunk(chunkPos.x, chunkPos.z, ((WorldGenLevel)level).getSeed(), 987234911L).nextInt(10) == 0;
-            if (random.nextInt(10) == 0 && flag && pos.getY() < 40) {
+            boolean flag = level.getMinecraftWorld().paperConfig().entities.spawning.allChunksAreSlimeChunks || WorldgenRandom.seedSlimeChunk(chunkPos.x, chunkPos.z, ((WorldGenLevel) level).getSeed(), level.getMinecraftWorld().spigotConfig.slimeSeed).nextInt(10) == 0; // Paper
+                // Paper start - Replace rules for Height in Slime Chunks
+                final double maxHeightSlimeChunk = level.getMinecraftWorld().paperConfig().entities.spawning.slimeSpawnHeight.slimeChunk.maximum;
+                if (random.nextInt(10) == 0 && flag && pos.getY() < maxHeightSlimeChunk) {
+                // Paper end - Replace rules for Height in Slime Chunks
                 return checkMobSpawnRules(entityType, level, spawnReason, pos, random);
             }
         }
@@ -302,7 +_,7 @@
     }
 
     @Override
-    protected float getSoundVolume() {
+    public float getSoundVolume() {
         return 0.4F * this.getSize();
     }
 
@@ -352,6 +_,16 @@
         return super.getDefaultDimensions(pose).scale(this.getSize());
     }
 
+    // Paper start - Slime pathfinder events
+    public boolean canWander() {
+        return this.canWander;
+    }
+
+    public void setWander(boolean canWander) {
+        this.canWander = canWander;
+    }
+    // Paper end - Slime pathfinder events
+
     static class SlimeAttackGoal extends Goal {
         private final Slime slime;
         private int growTiredTimer;
@@ -364,7 +_,16 @@
         @Override
         public boolean canUse() {
             LivingEntity target = this.slime.getTarget();
-            return target != null && this.slime.canAttack(target) && this.slime.getMoveControl() instanceof Slime.SlimeMoveControl;
+
+            // Paper start - Slime pathfinder events
+            if (target == null || !target.isAlive()) {
+                return false;
+            }
+            if (!this.slime.canAttack(target)) {
+                return false;
+            }
+            return this.slime.getMoveControl() instanceof Slime.SlimeMoveControl && this.slime.canWander && new com.destroystokyo.paper.event.entity.SlimeTargetLivingEntityEvent((org.bukkit.entity.Slime) this.slime.getBukkitEntity(), (org.bukkit.entity.LivingEntity) target.getBukkitEntity()).callEvent();
+            // Paper end - Slime pathfinder events
         }
 
         @Override
@@ -376,7 +_,16 @@
         @Override
         public boolean canContinueToUse() {
             LivingEntity target = this.slime.getTarget();
-            return target != null && this.slime.canAttack(target) && --this.growTiredTimer > 0;
+
+            // Paper start - Slime pathfinder events
+            if (target == null || !target.isAlive()) {
+                return false;
+            }
+            if (!this.slime.canAttack(target)) {
+                return false;
+            }
+            return --this.growTiredTimer > 0 && this.slime.canWander && new com.destroystokyo.paper.event.entity.SlimeTargetLivingEntityEvent((org.bukkit.entity.Slime) this.slime.getBukkitEntity(), (org.bukkit.entity.LivingEntity) target.getBukkitEntity()).callEvent();
+            // Paper end - Slime pathfinder events
         }
 
         @Override
@@ -395,6 +_,13 @@
                 slimeMoveControl.setDirection(this.slime.getYRot(), this.slime.isDealsDamage());
             }
         }
+
+        // Paper start - Slime pathfinder events; clear timer and target when goal resets
+        public void stop() {
+            this.growTiredTimer = 0;
+            this.slime.setTarget(null);
+        }
+        // Paper end - Slime pathfinder events
     }
 
     static class SlimeFloatGoal extends Goal {
@@ -408,7 +_,7 @@
 
         @Override
         public boolean canUse() {
-            return (this.slime.isInWater() || this.slime.isInLava()) && this.slime.getMoveControl() instanceof Slime.SlimeMoveControl;
+            return (this.slime.isInWater() || this.slime.isInLava()) && this.slime.getMoveControl() instanceof Slime.SlimeMoveControl && this.slime.canWander && new com.destroystokyo.paper.event.entity.SlimeSwimEvent((org.bukkit.entity.Slime) this.slime.getBukkitEntity()).callEvent(); // Paper - Slime pathfinder events
         }
 
         @Override
@@ -438,7 +_,7 @@
 
         @Override
         public boolean canUse() {
-            return !this.slime.isPassenger();
+            return !this.slime.isPassenger() && this.slime.canWander && new com.destroystokyo.paper.event.entity.SlimeWanderEvent((org.bukkit.entity.Slime) this.slime.getBukkitEntity()).callEvent(); // Paper - Slime pathfinder events
         }
 
         @Override
@@ -516,7 +_,7 @@
 
         @Override
         public boolean canUse() {
-            return this.slime.getTarget() == null
+            return this.slime.getTarget() == null && this.slime.canWander // Paper - Slime pathfinder events
                 && (this.slime.onGround() || this.slime.isInWater() || this.slime.isInLava() || this.slime.hasEffect(MobEffects.LEVITATION))
                 && this.slime.getMoveControl() instanceof Slime.SlimeMoveControl;
         }
@@ -526,6 +_,11 @@
             if (--this.nextRandomizeTime <= 0) {
                 this.nextRandomizeTime = this.adjustedTickDelay(40 + this.slime.getRandom().nextInt(60));
                 this.chosenDegrees = this.slime.getRandom().nextInt(360);
+                // Paper start - Slime pathfinder events
+                com.destroystokyo.paper.event.entity.SlimeChangeDirectionEvent event = new com.destroystokyo.paper.event.entity.SlimeChangeDirectionEvent((org.bukkit.entity.Slime) this.slime.getBukkitEntity(), this.chosenDegrees);
+                if (!this.slime.canWander || !event.callEvent()) return;
+                this.chosenDegrees = event.getNewYaw();
+                // Paper end - Slime pathfinder events
             }
 
             if (this.slime.getMoveControl() instanceof Slime.SlimeMoveControl slimeMoveControl) {
