From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: William Blake Galbreath <Blake.Galbreath@GMail.com>
Date: Sun, 5 Jul 2020 22:19:49 -0500
Subject: [PATCH] Ridables


diff --git a/src/main/java/net/minecraft/core/BlockPos.java b/src/main/java/net/minecraft/core/BlockPos.java
index b01d7da333bac7820e42b6f645634a15ef88ae4f..30a4b80eacf76ad7f0a48b79bcf01b752c0af48a 100644
--- a/src/main/java/net/minecraft/core/BlockPos.java
+++ b/src/main/java/net/minecraft/core/BlockPos.java
@@ -41,6 +41,12 @@ public class BlockPos extends Vec3i {
     private static final int X_OFFSET = 38;
     // Paper end
 
+    // Purpur start
+    public BlockPos(net.minecraft.world.entity.Entity entity) {
+        super(entity.getX(), entity.getY(), entity.getZ());
+    }
+    // Purpur end
+
     public BlockPos(int x, int y, int z) {
         super(x, y, z);
     }
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
index 961af809af47ea3fa0575f17a8e783de7253fb0b..55b85ff37287d1f0e114073c86bbff445f6d7848 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -1592,6 +1592,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
             ServerLevel worldserver = (ServerLevel) iterator.next();
             worldserver.hasPhysicsEvent =  org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper
             worldserver.hasEntityMoveEvent =  io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper
+            worldserver.hasRidableMoveEvent = net.pl3x.purpur.event.entity.RidableMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Purpur
             net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = worldserver.paperConfig.disableHopperMoveEvents || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper
 
             this.profiler.push(() -> {
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
index 29ce41330bd291a050441fc5aecd7eef65ff471c..e38444328c0e3e6e5f4d51926bc58d9f0e0975ac 100644
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
@@ -201,6 +201,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
     public final UUID uuid;
     public boolean hasPhysicsEvent = true; // Paper
     public boolean hasEntityMoveEvent = false; // Paper
+    public boolean hasRidableMoveEvent = false; // Purpur
     public static Throwable getAddToWorldStackTrace(Entity entity) {
         return new Throwable(entity + " Added to world at " + new java.util.Date());
     }
diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
index 6d25b3397a45d77b96d04a5b099d58a092d43177..651423979d4d5acbcf0bc25d6225a72a16e21826 100644
--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
@@ -665,6 +665,15 @@ public class ServerPlayer extends Player {
         }
 
         this.advancements.flushDirty(this);
+
+        // Purpur start
+        if (this.level.purpurConfig.useNightVisionWhenRiding && this.getVehicle() != null && this.getVehicle().getRider() == this && this.level.getGameTime() % 100 == 0) { // 5 seconds
+            MobEffectInstance nightVision = this.getEffect(MobEffects.NIGHT_VISION);
+            if (nightVision == null || nightVision.getDuration() <= 300) { // 15 seconds
+                this.addEffect(new MobEffectInstance(MobEffects.NIGHT_VISION, 400, 0)); // 20 seconds
+            }
+        }
+        // Purpur end
     }
 
     public void doTick() {
@@ -2439,4 +2448,6 @@ public class ServerPlayer extends Player {
         return (CraftPlayer) super.getBukkitEntity();
     }
     // CraftBukkit end
+
+
 }
diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
index f99d06e53856c14f5b94c28d379b8188ac1411db..c02b3cdd93df7806d52138b02612dd48a451962b 100644
--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
@@ -2412,6 +2412,8 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser
 
                         ServerGamePacketListenerImpl.this.cserver.getPluginManager().callEvent(event);
 
+                        player.processClick(enumhand); // Purpur
+
                         // Fish bucket - SPIGOT-4048
                         if ((entity instanceof AbstractFish && origItem != null && origItem.asItem() == Items.WATER_BUCKET) && (event.isCancelled() || ServerGamePacketListenerImpl.this.player.getInventory().getSelected() == null || ServerGamePacketListenerImpl.this.player.getInventory().getSelected().getItem() != origItem)) {
                             ServerGamePacketListenerImpl.this.send(new ClientboundAddMobPacket((AbstractFish) entity));
diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
index b2ccdd8f687e3bf0dc64110259a301e879bd3657..20b29275c5b0f1e1c9d97af274abdc2e1ca3d847 100644
--- a/src/main/java/net/minecraft/world/entity/Entity.java
+++ b/src/main/java/net/minecraft/world/entity/Entity.java
@@ -230,7 +230,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n
     public float yRotO;
     public float xRotO;
     private AABB bb;
-    protected boolean onGround;
+    public boolean onGround; // Purpur - protected -> public
     public boolean horizontalCollision;
     public boolean verticalCollision;
     public boolean hurtMarked;
@@ -291,7 +291,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n
     private final Set<String> tags;
     private final double[] pistonDeltas;
     private long pistonDeltasGameTime;
-    private EntityDimensions dimensions;
+    protected EntityDimensions dimensions; // Purpur - private -> protected
     private float eyeHeight;
     public boolean isInPowderSnow;
     public boolean wasInPowderSnow;
@@ -2625,6 +2625,12 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n
                 this.passengers = ImmutableList.copyOf(list);
             }
 
+            // Purpur start
+            if (isRidable() && this.passengers.get(0) == entity && entity instanceof Player player) {
+                onMount(player);
+                this.rider = player;
+            }
+            // Purpur end
         }
         return true; // CraftBukkit
     }
@@ -2665,6 +2671,14 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n
                 return false;
             }
             // Spigot end
+
+            // Purpur start
+            if (this.rider != null && this.passengers.get(0) == this.rider) {
+                onDismount(this.rider);
+                this.rider = null;
+            }
+            // Purpur end
+
             if (this.passengers.size() == 1 && this.passengers.get(0) == entity) {
                 this.passengers = ImmutableList.of();
             } else {
@@ -4252,4 +4266,41 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n
         return ((ServerChunkCache) level.getChunkSource()).isPositionTicking(this);
     }
     // Paper end
+
+    // Purpur start
+    @Nullable
+    private Player rider = null;
+
+    @Nullable
+    public Player getRider() {
+        return rider;
+    }
+
+    public boolean isRidable() {
+        return false;
+    }
+
+    public void onMount(Player rider) {
+        if (this instanceof Mob) {
+            ((Mob) this).setGoalTarget(null, null, false);
+            ((Mob) this).getNavigation().stop();
+        }
+        rider.setJumping(false); // fixes jump on mount
+    }
+
+    public void onDismount(Player player) {
+    }
+
+    public boolean onSpacebar() {
+        return false;
+    }
+
+    public boolean onClick(InteractionHand hand) {
+        return false;
+    }
+
+    public boolean processClick(InteractionHand hand) {
+        return false;
+    }
+    // Purpur end
 }
diff --git a/src/main/java/net/minecraft/world/entity/GlowSquid.java b/src/main/java/net/minecraft/world/entity/GlowSquid.java
index d28cecd9bea7c82fa675d333810e2e63a91c615e..8f8bc29d847801938e251904b8334b4b31bd21c5 100644
--- a/src/main/java/net/minecraft/world/entity/GlowSquid.java
+++ b/src/main/java/net/minecraft/world/entity/GlowSquid.java
@@ -19,6 +19,18 @@ public class GlowSquid extends Squid {
         super(type, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.glowSquidRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return true;
+    }
+    // Purpur end
+
     @Override
     protected ParticleOptions getInkParticle() {
         return ParticleTypes.GLOW_SQUID_INK;
diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java
index f025d03149089a8acdfa55f54b5b754cba4bd4d1..f508bf68172cd5e86c3f0c01f573514b8e302c02 100644
--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java
+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java
@@ -219,9 +219,9 @@ public abstract class LivingEntity extends Entity {
     protected int deathScore;
     public float lastHurt;
     public boolean jumping;
-    public float xxa;
-    public float yya;
-    public float zza;
+    public float xxa; public float getStrafeMot() { return xxa; } public void setStrafeMot(float strafe) { xxa = strafe; } // Purpur - OBFHELPER
+    public float yya; public float getVerticalMot() { return yya; } public void setVerticalMot(float vertical) { yya = vertical; } // Purpur - OBFHELPER
+    public float zza; public float getForwardMot() { return zza; } public void setForwardMot(float forward) { zza = forward; } // Purpur - OBFHELPER
     protected int lerpSteps;
     protected double lerpX;
     protected double lerpY;
@@ -2584,7 +2584,7 @@ public abstract class LivingEntity extends Entity {
         return this.hasEffect(MobEffects.JUMP) ? (double) (0.1F * (float) (this.getEffect(MobEffects.JUMP).getAmplifier() + 1)) : 0.0D;
     }
 
-    protected void jumpFromGround() {
+    public void jumpFromGround() { // Purpur - protected -> public
         double d0 = (double) this.getJumpPower() + this.getJumpBoostPower();
         Vec3 vec3d = this.getDeltaMovement();
 
@@ -3238,8 +3238,10 @@ public abstract class LivingEntity extends Entity {
         this.pushEntities();
         this.level.getProfiler().pop();
         // Paper start
-        if (((ServerLevel) this.level).hasEntityMoveEvent) {
-            if (this.xo != getX() || this.yo != this.getY() || this.zo != this.getZ() || this.yRotO != this.getYRot() || this.xRotO != this.getXRot()) {
+        // Purpur start
+        if (this.xo != this.getX() || this.yo != this.getY() || this.zo != this.getZ() || this.yRotO != this.getYRot() || this.xRotO != this.getXRot()) {
+            if (((ServerLevel) level).hasEntityMoveEvent) {
+                // Purpur end
                 Location from = new Location(this.level.getWorld(), this.xo, this.yo, this.zo, this.yRotO, this.xRotO);
                 Location to = new Location (this.level.getWorld(), this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot());
                 io.papermc.paper.event.entity.EntityMoveEvent event = new io.papermc.paper.event.entity.EntityMoveEvent(this.getBukkitLivingEntity(), from, to.clone());
@@ -3249,6 +3251,21 @@ public abstract class LivingEntity extends Entity {
                     absMoveTo(event.getTo().getX(), event.getTo().getY(), event.getTo().getZ(), event.getTo().getYaw(), event.getTo().getPitch());
                 }
             }
+            // Purpur start
+            if (getRider() != null) {
+                getRider().resetLastActionTime();
+                if (((ServerLevel) level).hasRidableMoveEvent && this instanceof Mob) {
+                    Location from = new Location(level.getWorld(), xo, yo, zo, this.yRotO, this.xRotO);
+                    Location to = new Location(level.getWorld(), getX(), getY(), getZ(), this.getYRot(), this.getXRot());
+                    net.pl3x.purpur.event.entity.RidableMoveEvent event = new net.pl3x.purpur.event.entity.RidableMoveEvent((org.bukkit.entity.Mob) getBukkitLivingEntity(), (Player) getRider().getBukkitEntity(), from, to.clone());
+                    if (!event.callEvent()) {
+                        absMoveTo(from.getX(), from.getY(), from.getZ(), from.getYaw(), from.getPitch());
+                    } else if (!to.equals(event.getTo())) {
+                        absMoveTo(to.getX(), to.getY(), to.getZ(), to.getYaw(), to.getPitch());
+                    }
+                }
+            }
+            // Purpur end
         }
         // Paper end
         if (!this.level.isClientSide && this.isSensitiveToWater() && this.isInWaterRainOrBubble()) {
diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java
index c7a400e72ca2aae31ae8882b8df946b14b18e006..4af25f8d4d48a8d886c9ec823b65fdd0dd199066 100644
--- a/src/main/java/net/minecraft/world/entity/Mob.java
+++ b/src/main/java/net/minecraft/world/entity/Mob.java
@@ -140,6 +140,8 @@ public abstract class Mob extends LivingEntity {
         this.targetSelector = new GoalSelector(world.getProfilerSupplier());
         this.lookControl = new LookControl(this);
         this.moveControl = new MoveControl(this);
+        this.lookControl = new net.pl3x.purpur.controller.LookControllerWASD(this); // Purpur
+        this.moveControl = new net.pl3x.purpur.controller.MoveControllerWASD(this); // Purpur
         this.jumpControl = new JumpControl(this);
         this.bodyRotationControl = this.createBodyControl();
         this.navigation = this.createNavigation(world);
@@ -1285,7 +1287,7 @@ public abstract class Mob extends LivingEntity {
     protected void onOffspringSpawnedFromEgg(Player player, Mob child) {}
 
     protected InteractionResult mobInteract(Player player, InteractionHand hand) {
-        return InteractionResult.PASS;
+        return tryRide(player, hand); // Purpur
     }
 
     public boolean isWithinRestriction() {
@@ -1646,4 +1648,52 @@ public abstract class Mob extends LivingEntity {
 
         return itemmonsteregg == null ? null : new ItemStack(itemmonsteregg);
     }
+
+    // Purpur start
+    public double getMaxY() {
+        return level.getHeight();
+    }
+
+    public InteractionResult tryRide(Player player, InteractionHand hand) {
+        if (!isRidable()) {
+            return InteractionResult.PASS;
+        }
+        if (hand != InteractionHand.MAIN_HAND) {
+            return InteractionResult.PASS;
+        }
+        if (player.isShiftKeyDown()) {
+            return InteractionResult.PASS;
+        }
+        if (!player.getItemInHand(hand).isEmpty()) {
+            return InteractionResult.PASS;
+        }
+        if (!passengers.isEmpty() || player.isPassenger()) {
+            return InteractionResult.PASS;
+        }
+        if (this instanceof TamableAnimal tamable) {
+            if (tamable.isTame() && !tamable.isOwnedBy(player)) {
+                return InteractionResult.PASS;
+            }
+            if (!tamable.isTame() && !level.purpurConfig.untamedTamablesAreRidable) {
+                return InteractionResult.PASS;
+            }
+        }
+        if (this instanceof AgeableMob ageable) {
+            if (ageable.isBaby() && !level.purpurConfig.babiesAreRidable) {
+                return InteractionResult.PASS;
+            }
+        }
+        if (!player.getBukkitEntity().hasPermission("allow.ride." + getType().id)) {
+            player.sendMessage(net.pl3x.purpur.PurpurConfig.cannotRideMob);
+            return InteractionResult.PASS;
+        }
+        player.setYRot(this.getYRot());
+        player.setXRot(this.getXRot());
+        if (player.startRiding(this)) {
+            return InteractionResult.SUCCESS;
+        } else {
+            return InteractionResult.PASS;
+        }
+    }
+    // Purpur end
 }
diff --git a/src/main/java/net/minecraft/world/entity/ai/attributes/DefaultAttributes.java b/src/main/java/net/minecraft/world/entity/ai/attributes/DefaultAttributes.java
index b1e00093ad2b42a1e0a399d261539e06f2a505b9..53d43442ddc711d39ede5f33c3754e0a2097c2bc 100644
--- a/src/main/java/net/minecraft/world/entity/ai/attributes/DefaultAttributes.java
+++ b/src/main/java/net/minecraft/world/entity/ai/attributes/DefaultAttributes.java
@@ -74,7 +74,86 @@ import org.apache.logging.log4j.Logger;
 
 public class DefaultAttributes {
     private static final Logger LOGGER = LogManager.getLogger();
-    private static final Map<EntityType<? extends LivingEntity>, AttributeSupplier> SUPPLIERS = ImmutableMap.<EntityType<? extends LivingEntity>, AttributeSupplier>builder().put(EntityType.ARMOR_STAND, LivingEntity.createLivingAttributes().build()).put(EntityType.AXOLOTL, Axolotl.createAttributes().build()).put(EntityType.BAT, Bat.createAttributes().build()).put(EntityType.BEE, Bee.createAttributes().build()).put(EntityType.BLAZE, Blaze.createAttributes().build()).put(EntityType.CAT, Cat.createAttributes().build()).put(EntityType.CAVE_SPIDER, CaveSpider.createCaveSpider().build()).put(EntityType.CHICKEN, Chicken.createAttributes().build()).put(EntityType.COD, AbstractFish.createAttributes().build()).put(EntityType.COW, Cow.createAttributes().build()).put(EntityType.CREEPER, Creeper.createAttributes().build()).put(EntityType.DOLPHIN, Dolphin.createAttributes().build()).put(EntityType.DONKEY, AbstractChestedHorse.createBaseChestedHorseAttributes().build()).put(EntityType.DROWNED, Zombie.createAttributes().build()).put(EntityType.ELDER_GUARDIAN, ElderGuardian.createAttributes().build()).put(EntityType.ENDERMAN, EnderMan.createAttributes().build()).put(EntityType.ENDERMITE, Endermite.createAttributes().build()).put(EntityType.ENDER_DRAGON, EnderDragon.createAttributes().build()).put(EntityType.EVOKER, Evoker.createAttributes().build()).put(EntityType.FOX, Fox.createAttributes().build()).put(EntityType.GHAST, Ghast.createAttributes().build()).put(EntityType.GIANT, Giant.createAttributes().build()).put(EntityType.GLOW_SQUID, GlowSquid.createAttributes().build()).put(EntityType.GOAT, Goat.createAttributes().build()).put(EntityType.GUARDIAN, Guardian.createAttributes().build()).put(EntityType.HOGLIN, Hoglin.createAttributes().build()).put(EntityType.HORSE, AbstractHorse.createBaseHorseAttributes().build()).put(EntityType.HUSK, Zombie.createAttributes().build()).put(EntityType.ILLUSIONER, Illusioner.createAttributes().build()).put(EntityType.IRON_GOLEM, IronGolem.createAttributes().build()).put(EntityType.LLAMA, Llama.createAttributes().build()).put(EntityType.MAGMA_CUBE, MagmaCube.createAttributes().build()).put(EntityType.MOOSHROOM, Cow.createAttributes().build()).put(EntityType.MULE, AbstractChestedHorse.createBaseChestedHorseAttributes().build()).put(EntityType.OCELOT, Ocelot.createAttributes().build()).put(EntityType.PANDA, Panda.createAttributes().build()).put(EntityType.PARROT, Parrot.createAttributes().build()).put(EntityType.PHANTOM, Monster.createMonsterAttributes().build()).put(EntityType.PIG, Pig.createAttributes().build()).put(EntityType.PIGLIN, Piglin.createAttributes().build()).put(EntityType.PIGLIN_BRUTE, PiglinBrute.createAttributes().build()).put(EntityType.PILLAGER, Pillager.createAttributes().build()).put(EntityType.PLAYER, Player.createAttributes().build()).put(EntityType.POLAR_BEAR, PolarBear.createAttributes().build()).put(EntityType.PUFFERFISH, AbstractFish.createAttributes().build()).put(EntityType.RABBIT, Rabbit.createAttributes().build()).put(EntityType.RAVAGER, Ravager.createAttributes().build()).put(EntityType.SALMON, AbstractFish.createAttributes().build()).put(EntityType.SHEEP, Sheep.createAttributes().build()).put(EntityType.SHULKER, Shulker.createAttributes().build()).put(EntityType.SILVERFISH, Silverfish.createAttributes().build()).put(EntityType.SKELETON, AbstractSkeleton.createAttributes().build()).put(EntityType.SKELETON_HORSE, SkeletonHorse.createAttributes().build()).put(EntityType.SLIME, Monster.createMonsterAttributes().build()).put(EntityType.SNOW_GOLEM, SnowGolem.createAttributes().build()).put(EntityType.SPIDER, Spider.createAttributes().build()).put(EntityType.SQUID, Squid.createAttributes().build()).put(EntityType.STRAY, AbstractSkeleton.createAttributes().build()).put(EntityType.STRIDER, Strider.createAttributes().build()).put(EntityType.TRADER_LLAMA, Llama.createAttributes().build()).put(EntityType.TROPICAL_FISH, AbstractFish.createAttributes().build()).put(EntityType.TURTLE, Turtle.createAttributes().build()).put(EntityType.VEX, Vex.createAttributes().build()).put(EntityType.VILLAGER, Villager.createAttributes().build()).put(EntityType.VINDICATOR, Vindicator.createAttributes().build()).put(EntityType.WANDERING_TRADER, Mob.createMobAttributes().build()).put(EntityType.WITCH, Witch.createAttributes().build()).put(EntityType.WITHER, WitherBoss.createAttributes().build()).put(EntityType.WITHER_SKELETON, AbstractSkeleton.createAttributes().build()).put(EntityType.WOLF, Wolf.createAttributes().build()).put(EntityType.ZOGLIN, Zoglin.createAttributes().build()).put(EntityType.ZOMBIE, Zombie.createAttributes().build()).put(EntityType.ZOMBIE_HORSE, ZombieHorse.createAttributes().build()).put(EntityType.ZOMBIE_VILLAGER, Zombie.createAttributes().build()).put(EntityType.ZOMBIFIED_PIGLIN, ZombifiedPiglin.createAttributes().build()).build();
+
+    // Purpur start
+    private static final Map<EntityType<? extends LivingEntity>, AttributeSupplier> SUPPLIERS = ImmutableMap.<EntityType<? extends LivingEntity>, AttributeSupplier>builder()
+            .put(EntityType.ARMOR_STAND, LivingEntity.createLivingAttributes().build())
+            .put(EntityType.AXOLOTL, Axolotl.createAttributes().build())
+            .put(EntityType.BAT, Bat.createAttributes().build())
+            .put(EntityType.BEE, Bee.createAttributes().build())
+            .put(EntityType.BLAZE, Blaze.createAttributes().build())
+            .put(EntityType.CAT, Cat.createAttributes().build())
+            .put(EntityType.CAVE_SPIDER, CaveSpider.createCaveSpider().build())
+            .put(EntityType.CHICKEN, Chicken.createAttributes().build())
+            .put(EntityType.COD, AbstractFish.createAttributes().build())
+            .put(EntityType.COW, Cow.createAttributes().build())
+            .put(EntityType.CREEPER, Creeper.createAttributes().build())
+            .put(EntityType.DOLPHIN, Dolphin.createAttributes().build())
+            .put(EntityType.DONKEY, AbstractChestedHorse.createBaseChestedHorseAttributes().build())
+            .put(EntityType.DROWNED, Zombie.createAttributes().build())
+            .put(EntityType.ELDER_GUARDIAN, ElderGuardian.createAttributes().build())
+            .put(EntityType.ENDERMAN, EnderMan.createAttributes().build())
+            .put(EntityType.ENDERMITE, Endermite.createAttributes().build())
+            .put(EntityType.ENDER_DRAGON, EnderDragon.createAttributes().build())
+            .put(EntityType.EVOKER, Evoker.createAttributes().build())
+            .put(EntityType.FOX, Fox.createAttributes().build())
+            .put(EntityType.GHAST, Ghast.createAttributes().build())
+            .put(EntityType.GIANT, Giant.createAttributes().build())
+            .put(EntityType.GLOW_SQUID, GlowSquid.createAttributes().build())
+            .put(EntityType.GOAT, Goat.createAttributes().build())
+            .put(EntityType.GUARDIAN, Guardian.createAttributes().build())
+            .put(EntityType.HOGLIN, Hoglin.createAttributes().build())
+            .put(EntityType.HORSE, AbstractHorse.createBaseHorseAttributes().build())
+            .put(EntityType.HUSK, Zombie.createAttributes().build())
+            .put(EntityType.ILLUSIONER, Illusioner.createAttributes().build())
+            .put(EntityType.IRON_GOLEM, IronGolem.createAttributes().build())
+            .put(EntityType.LLAMA, Llama.createAttributes().build())
+            .put(EntityType.MAGMA_CUBE, MagmaCube.createAttributes().build())
+            .put(EntityType.MOOSHROOM, Cow.createAttributes().build())
+            .put(EntityType.MULE, AbstractChestedHorse.createBaseChestedHorseAttributes().build())
+            .put(EntityType.OCELOT, Ocelot.createAttributes().build())
+            .put(EntityType.PANDA, Panda.createAttributes().build())
+            .put(EntityType.PARROT, Parrot.createAttributes().build())
+            .put(EntityType.PHANTOM, net.minecraft.world.entity.monster.Phantom.createAttributes().build()) // Purpur
+            .put(EntityType.PIG, Pig.createAttributes().build())
+            .put(EntityType.PIGLIN, Piglin.createAttributes().build())
+            .put(EntityType.PIGLIN_BRUTE, PiglinBrute.createAttributes().build())
+            .put(EntityType.PILLAGER, Pillager.createAttributes().build())
+            .put(EntityType.PLAYER, Player.createAttributes().build())
+            .put(EntityType.POLAR_BEAR, PolarBear.createAttributes().build())
+            .put(EntityType.PUFFERFISH, AbstractFish.createAttributes().build())
+            .put(EntityType.RABBIT, Rabbit.createAttributes().build())
+            .put(EntityType.RAVAGER, Ravager.createAttributes().build())
+            .put(EntityType.SALMON, AbstractFish.createAttributes().build())
+            .put(EntityType.SHEEP, Sheep.createAttributes().build())
+            .put(EntityType.SHULKER, Shulker.createAttributes().build())
+            .put(EntityType.SILVERFISH, Silverfish.createAttributes().build())
+            .put(EntityType.SKELETON, AbstractSkeleton.createAttributes().build())
+            .put(EntityType.SKELETON_HORSE, SkeletonHorse.createAttributes().build())
+            .put(EntityType.SLIME, Monster.createMonsterAttributes().build())
+            .put(EntityType.SNOW_GOLEM, SnowGolem.createAttributes().build())
+            .put(EntityType.SPIDER, Spider.createAttributes().build())
+            .put(EntityType.SQUID, Squid.createAttributes().build())
+            .put(EntityType.STRAY, AbstractSkeleton.createAttributes().build())
+            .put(EntityType.STRIDER, Strider.createAttributes().build())
+            .put(EntityType.TRADER_LLAMA, Llama.createAttributes().build())
+            .put(EntityType.TROPICAL_FISH, AbstractFish.createAttributes().build())
+            .put(EntityType.TURTLE, Turtle.createAttributes().build())
+            .put(EntityType.VEX, Vex.createAttributes().build())
+            .put(EntityType.VILLAGER, Villager.createAttributes().build())
+            .put(EntityType.VINDICATOR, Vindicator.createAttributes().build())
+            .put(EntityType.WANDERING_TRADER, Mob.createMobAttributes().build())
+            .put(EntityType.WITCH, Witch.createAttributes().build())
+            .put(EntityType.WITHER, WitherBoss.createAttributes().build())
+            .put(EntityType.WITHER_SKELETON, AbstractSkeleton.createAttributes().build())
+            .put(EntityType.WOLF, Wolf.createAttributes().build())
+            .put(EntityType.ZOGLIN, Zoglin.createAttributes().build())
+            .put(EntityType.ZOMBIE, Zombie.createAttributes().build())
+            .put(EntityType.ZOMBIE_HORSE, ZombieHorse.createAttributes().build())
+            .put(EntityType.ZOMBIE_VILLAGER, Zombie.createAttributes().build())
+            .put(EntityType.ZOMBIFIED_PIGLIN, ZombifiedPiglin.createAttributes().build())
+            .build();
+    // Purpur end
 
     public static AttributeSupplier getSupplier(EntityType<? extends LivingEntity> type) {
         return SUPPLIERS.get(type);
diff --git a/src/main/java/net/minecraft/world/entity/ai/control/MoveControl.java b/src/main/java/net/minecraft/world/entity/ai/control/MoveControl.java
index fcdebde11f622d0e35d922286b34c8666f9f6861..43e5efd136937fe215db0e88907f3176e06197d1 100644
--- a/src/main/java/net/minecraft/world/entity/ai/control/MoveControl.java
+++ b/src/main/java/net/minecraft/world/entity/ai/control/MoveControl.java
@@ -29,6 +29,20 @@ public class MoveControl implements Control {
         this.mob = entity;
     }
 
+    // Purpur start
+    public void setSpeedModifier(double speed) {
+        this.speedModifier = speed;
+    }
+
+    public void setForward(float forward) {
+        this.strafeForwards = forward;
+    }
+
+    public void setStrafe(float strafe) {
+        this.strafeRight = strafe;
+    }
+    // Purpur end
+
     public boolean hasWanted() {
         return this.operation == MoveControl.Operation.MOVE_TO;
     }
diff --git a/src/main/java/net/minecraft/world/entity/ai/control/SmoothSwimmingLookControl.java b/src/main/java/net/minecraft/world/entity/ai/control/SmoothSwimmingLookControl.java
index ffc87d14cdc7edc22a7e2ac642d0e37037c52487..f2957d7f4c377a67ee486a9d0f7ae173add20aed 100644
--- a/src/main/java/net/minecraft/world/entity/ai/control/SmoothSwimmingLookControl.java
+++ b/src/main/java/net/minecraft/world/entity/ai/control/SmoothSwimmingLookControl.java
@@ -3,7 +3,7 @@ package net.minecraft.world.entity.ai.control;
 import net.minecraft.util.Mth;
 import net.minecraft.world.entity.Mob;
 
-public class SmoothSwimmingLookControl extends LookControl {
+public class SmoothSwimmingLookControl extends net.pl3x.purpur.controller.LookControllerWASD { // Purpur
     private final int maxYRotFromCenter;
     private static final int HEAD_TILT_X = 10;
     private static final int HEAD_TILT_Y = 20;
@@ -14,7 +14,7 @@ public class SmoothSwimmingLookControl extends LookControl {
     }
 
     @Override
-    public void tick() {
+    public void vanillaTick() { // Purpur
         if (this.hasWanted) {
             this.hasWanted = false;
             this.getYRotD().ifPresent((float_) -> {
@@ -32,9 +32,9 @@ public class SmoothSwimmingLookControl extends LookControl {
         }
 
         float f = Mth.wrapDegrees(this.mob.yHeadRot - this.mob.yBodyRot);
-        if (f < (float)(-this.maxYRotFromCenter)) {
+        if (f < (float) (-this.maxYRotFromCenter)) {
             this.mob.yBodyRot -= 4.0F;
-        } else if (f > (float)this.maxYRotFromCenter) {
+        } else if (f > (float) this.maxYRotFromCenter) {
             this.mob.yBodyRot += 4.0F;
         }
 
diff --git a/src/main/java/net/minecraft/world/entity/ambient/Bat.java b/src/main/java/net/minecraft/world/entity/ambient/Bat.java
index 1d415a91b9c90603e8f738dbafe7a5ea57ef14cc..e0a55fdff847180d08db0bc6bc7e19281c594a0c 100644
--- a/src/main/java/net/minecraft/world/entity/ambient/Bat.java
+++ b/src/main/java/net/minecraft/world/entity/ambient/Bat.java
@@ -20,6 +20,7 @@ import net.minecraft.world.entity.EntityType;
 import net.minecraft.world.entity.LivingEntity;
 import net.minecraft.world.entity.Mob;
 import net.minecraft.world.entity.MobSpawnType;
+import net.minecraft.world.entity.MoverType;
 import net.minecraft.world.entity.Pose;
 import net.minecraft.world.entity.ai.attributes.AttributeSupplier;
 import net.minecraft.world.entity.ai.attributes.Attributes;
@@ -42,9 +43,48 @@ public class Bat extends AmbientCreature {
 
     public Bat(EntityType<? extends Bat> type, Level world) {
         super(type, world);
+        this.moveControl = new net.pl3x.purpur.controller.FlyingWithSpacebarMoveControllerWASD(this, 0.075F); // Purpur
         this.setResting(true);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.batRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.batRidableInWater;
+    }
+
+    @Override
+    public double getMaxY() {
+        return level.purpurConfig.batMaxY;
+    }
+
+    @Override
+    public void onMount(Player rider) {
+        super.onMount(rider);
+        if (isResting()) {
+            setResting(false);
+            level.levelEvent(null, 1025, new BlockPos(this).above(), 0);
+        }
+    }
+
+    @Override
+    public void travel(Vec3 vec3) {
+        super.travel(vec3);
+        if (getRider() != null && !onGround) {
+            float speed = (float) getAttributeValue(Attributes.FLYING_SPEED) * 2;
+            setSpeed(speed);
+            Vec3 mot = getDeltaMovement();
+            move(MoverType.SELF, mot.multiply(speed, 0.25, speed));
+            setDeltaMovement(mot.scale(0.9D));
+        }
+    }
+    // Purpur end
+
     @Override
     public boolean isFlapping() {
         return !this.isResting() && this.tickCount % Bat.TICKS_PER_FLAP == 0;
@@ -94,7 +134,7 @@ public class Bat extends AmbientCreature {
     protected void pushEntities() {}
 
     public static AttributeSupplier.Builder createAttributes() {
-        return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 6.0D);
+        return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 6.0D).add(Attributes.FLYING_SPEED, 0.6D); // Purpur
     }
 
     public boolean isResting() {
@@ -126,6 +166,14 @@ public class Bat extends AmbientCreature {
 
     @Override
     protected void customServerAiStep() {
+        // Purpur start
+        if (getRider() != null) {
+            Vec3 mot = getDeltaMovement();
+            setDeltaMovement(mot.x(), mot.y() + (getVerticalMot() > 0 ? 0.07D : 0.0D), mot.z());
+            return;
+        }
+        // Purpur end
+
         super.customServerAiStep();
         BlockPos blockposition = this.blockPosition();
         BlockPos blockposition1 = blockposition.above();
diff --git a/src/main/java/net/minecraft/world/entity/animal/AbstractFish.java b/src/main/java/net/minecraft/world/entity/animal/AbstractFish.java
index fc0cd86397b12e42756273a0317164d79ac51937..4243c4d3479210e392a1246337a3daabee5629d7 100644
--- a/src/main/java/net/minecraft/world/entity/animal/AbstractFish.java
+++ b/src/main/java/net/minecraft/world/entity/animal/AbstractFish.java
@@ -108,12 +108,9 @@ public abstract class AbstractFish extends WaterAnimal implements Bucketable {
     @Override
     protected void registerGoals() {
         super.registerGoals();
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(0, new PanicGoal(this, 1.25D));
-        GoalSelector pathfindergoalselector = this.goalSelector;
-        Predicate predicate = EntitySelector.NO_SPECTATORS;
-
-        Objects.requireNonNull(predicate);
-        pathfindergoalselector.addGoal(2, new AvoidEntityGoal<>(this, Player.class, 8.0F, 1.6D, 1.4D, predicate::test));
+        this.goalSelector.addGoal(2, new AvoidEntityGoal<>(this, Player.class, 8.0F, 1.6D, 1.4D, EntitySelector.NO_SPECTATORS::test)); // Purpur - decompile fix
         this.goalSelector.addGoal(4, new AbstractFish.FishSwimGoal(this));
     }
 
@@ -125,7 +122,7 @@ public abstract class AbstractFish extends WaterAnimal implements Bucketable {
     @Override
     public void travel(Vec3 movementInput) {
         if (this.isEffectiveAi() && this.isInWater()) {
-            this.moveRelative(0.01F, movementInput);
+            this.moveRelative(getRider() != null ? getSpeed() : 0.01F, movementInput); // Purpur
             this.move(MoverType.SELF, this.getDeltaMovement());
             this.setDeltaMovement(this.getDeltaMovement().scale(0.9D));
             if (this.getTarget() == null) {
@@ -183,7 +180,7 @@ public abstract class AbstractFish extends WaterAnimal implements Bucketable {
     @Override
     protected void playStepSound(BlockPos pos, BlockState state) {}
 
-    private static class FishMoveControl extends MoveControl {
+    private static class FishMoveControl extends net.pl3x.purpur.controller.WaterMoveControllerWASD { // Purpur
 
         private final AbstractFish fish;
 
@@ -192,14 +189,22 @@ public abstract class AbstractFish extends WaterAnimal implements Bucketable {
             this.fish = owner;
         }
 
+        // Purpur start
+        @Override
+        public void purpurTick(Player rider) {
+            super.purpurTick(rider);
+            fish.setDeltaMovement(fish.getDeltaMovement().add(0.0D, 0.005D, 0.0D));
+        }
+        // Purpur end
+
         @Override
-        public void tick() {
+        public void vanillaTick() { // Purpur
             if (this.fish.isEyeInFluid((Tag) FluidTags.WATER)) {
                 this.fish.setDeltaMovement(this.fish.getDeltaMovement().add(0.0D, 0.005D, 0.0D));
             }
 
             if (this.operation == MoveControl.Operation.MOVE_TO && !this.fish.getNavigation().isDone()) {
-                float f = (float) (this.speedModifier * this.fish.getAttributeValue(Attributes.MOVEMENT_SPEED));
+                float f = (float) (this.getSpeedModifier() * this.fish.getAttributeValue(Attributes.MOVEMENT_SPEED)); // Purpur
 
                 this.fish.setSpeed(Mth.lerp(0.125F, this.fish.getSpeed(), f));
                 double d0 = this.wantedX - this.fish.getX();
diff --git a/src/main/java/net/minecraft/world/entity/animal/Bee.java b/src/main/java/net/minecraft/world/entity/animal/Bee.java
index 2639f64f1a50faddc0284fb26b73b563b3e9eba9..8632a4047776723088b9b9fa27c6e8093fb17801 100644
--- a/src/main/java/net/minecraft/world/entity/animal/Bee.java
+++ b/src/main/java/net/minecraft/world/entity/animal/Bee.java
@@ -44,6 +44,7 @@ import net.minecraft.world.entity.EntityType;
 import net.minecraft.world.entity.LivingEntity;
 import net.minecraft.world.entity.Mob;
 import net.minecraft.world.entity.MobType;
+import net.minecraft.world.entity.MoverType;
 import net.minecraft.world.entity.NeutralMob;
 import net.minecraft.world.entity.PathfinderMob;
 import net.minecraft.world.entity.Pose;
@@ -143,6 +144,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal {
     public Bee(EntityType<? extends Bee> type, Level world) {
         super(type, world);
         this.remainingCooldownBeforeLocatingNewFlower = Mth.nextInt(this.random, 20, 60);
+        final net.pl3x.purpur.controller.FlyingMoveControllerWASD flyingController = new net.pl3x.purpur.controller.FlyingMoveControllerWASD(this, 0.25F, false); // Purpur
         // Paper start - apply gravity to bees when they get stuck in the void, fixes MC-167279
         class BeeFlyingMoveControl extends FlyingMoveControl {
             public BeeFlyingMoveControl(final Mob entity, final int maxPitchChange, final boolean noGravity) {
@@ -151,11 +153,24 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal {
 
             @Override
             public void tick() {
+                // Purpur start
+                if (mob.getRider() != null) {
+                    flyingController.purpurTick(mob.getRider());
+                    return;
+                }
+                // Purpur end
                 if (this.mob.getY() <= Bee.this.level.getMinBuildHeight()) {
                     this.mob.setNoGravity(false);
                 }
                 super.tick();
             }
+
+            // Purpur start
+            @Override
+            public boolean hasWanted() {
+                return mob.getRider() != null || super.hasWanted();
+            }
+            // Purpur end
         }
         this.moveControl = new BeeFlyingMoveControl(this, 20, true);
         // Paper end
@@ -167,6 +182,35 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal {
         this.setPathfindingMalus(BlockPathTypes.FENCE, -1.0F);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.beeRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.beeRidableInWater;
+    }
+
+    @Override
+    public double getMaxY() {
+        return level.purpurConfig.beeMaxY;
+    }
+
+    @Override
+    public void travel(Vec3 vec3) {
+        super.travel(vec3);
+        if (getRider() != null && !onGround) {
+            float speed = (float) getAttributeValue(Attributes.FLYING_SPEED) * 2;
+            setSpeed(speed);
+            Vec3 mot = getDeltaMovement();
+            move(MoverType.SELF, mot.multiply(speed, speed, speed));
+            setDeltaMovement(mot.scale(0.9D));
+        }
+    }
+    // Purpur end
+
     @Override
     protected void defineSynchedData() {
         super.defineSynchedData();
@@ -181,6 +225,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal {
 
     @Override
     protected void registerGoals() {
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(0, new Bee.BeeAttackGoal(this, 1.399999976158142D, true));
         this.goalSelector.addGoal(1, new Bee.BeeEnterHiveGoal());
         this.goalSelector.addGoal(2, new BreedGoal(this, 1.0D));
@@ -196,6 +241,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal {
         this.goalSelector.addGoal(7, new Bee.BeeGrowCropGoal());
         this.goalSelector.addGoal(8, new Bee.BeeWanderGoal());
         this.goalSelector.addGoal(9, new FloatGoal(this));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(1, (new Bee.BeeHurtByOtherGoal(this)).setAlertOthers(new Class[0]));
         this.targetSelector.addGoal(2, new Bee.BeeBecomeAngryTargetGoal(this));
         this.targetSelector.addGoal(3, new ResetUniversalAngerTargetGoal<>(this, true));
@@ -871,16 +917,16 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal {
         }
     }
 
-    private class BeeLookControl extends LookControl {
+    private class BeeLookControl extends net.pl3x.purpur.controller.LookControllerWASD { // Purpur
 
         BeeLookControl(Mob entity) {
             super(entity);
         }
 
         @Override
-        public void tick() {
+        public void vanillaTick() { // Purpur
             if (!Bee.this.isAngry()) {
-                super.tick();
+                super.vanillaTick(); // Purpur
             }
         }
 
diff --git a/src/main/java/net/minecraft/world/entity/animal/Cat.java b/src/main/java/net/minecraft/world/entity/animal/Cat.java
index 15773d61deb357917e2478f0731f0b470669b8f0..14822b20971b63290a7022f8efe65693c41664d2 100644
--- a/src/main/java/net/minecraft/world/entity/animal/Cat.java
+++ b/src/main/java/net/minecraft/world/entity/animal/Cat.java
@@ -121,6 +121,26 @@ public class Cat extends TamableAnimal {
         super(type, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.catRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.catRidableInWater;
+    }
+
+    @Override
+    public void onMount(Player rider) {
+        super.onMount(rider);
+        setInSittingPose(false);
+        setLying(false);
+        setRelaxStateOne(false);
+    }
+    // Purpur end
+
     public ResourceLocation getResourceLocation() {
         return (ResourceLocation) Cat.TEXTURE_BY_TYPE.getOrDefault(this.getCatType(), (ResourceLocation) Cat.TEXTURE_BY_TYPE.get(0));
     }
@@ -129,6 +149,7 @@ public class Cat extends TamableAnimal {
     protected void registerGoals() {
         this.temptGoal = new Cat.CatTemptGoal(this, 0.6D, Cat.TEMPT_INGREDIENT, true);
         this.goalSelector.addGoal(1, new FloatGoal(this));
+        this.goalSelector.addGoal(1, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(1, new SitWhenOrderedToGoal(this));
         this.goalSelector.addGoal(2, new Cat.CatRelaxOnOwnerGoal(this));
         this.goalSelector.addGoal(3, this.temptGoal);
@@ -140,6 +161,7 @@ public class Cat extends TamableAnimal {
         this.goalSelector.addGoal(10, new BreedGoal(this, 0.8D));
         this.goalSelector.addGoal(11, new WaterAvoidingRandomStrollGoal(this, 0.8D, 1.0000001E-5F));
         this.goalSelector.addGoal(12, new LookAtPlayerGoal(this, Player.class, 10.0F));
+        this.targetSelector.addGoal(1, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(1, new NonTameRandomTargetGoal<>(this, Rabbit.class, false, (Predicate) null));
         this.targetSelector.addGoal(1, new NonTameRandomTargetGoal<>(this, Turtle.class, false, Turtle.BABY_ON_LAND_SELECTOR));
     }
@@ -394,6 +416,7 @@ public class Cat extends TamableAnimal {
 
     @Override
     public InteractionResult mobInteract(Player player, InteractionHand hand) {
+        if (getRider() != null) return InteractionResult.PASS; // Purpur
         ItemStack itemstack = player.getItemInHand(hand);
         Item item = itemstack.getItem();
 
diff --git a/src/main/java/net/minecraft/world/entity/animal/Chicken.java b/src/main/java/net/minecraft/world/entity/animal/Chicken.java
index cece07e000a4355e88aef7848fd264191521484e..7a5119e007d69adc9a77fbd167a8d19a9c71c6ff 100644
--- a/src/main/java/net/minecraft/world/entity/animal/Chicken.java
+++ b/src/main/java/net/minecraft/world/entity/animal/Chicken.java
@@ -52,9 +52,22 @@ public class Chicken extends Animal {
         this.setPathfindingMalus(BlockPathTypes.WATER, 0.0F);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.chickenRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.chickenRidableInWater;
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
         this.goalSelector.addGoal(0, new FloatGoal(this));
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(1, new PanicGoal(this, 1.4D));
         this.goalSelector.addGoal(2, new BreedGoal(this, 1.0D));
         this.goalSelector.addGoal(3, new TemptGoal(this, 1.0D, Chicken.FOOD_ITEMS, false));
diff --git a/src/main/java/net/minecraft/world/entity/animal/Cod.java b/src/main/java/net/minecraft/world/entity/animal/Cod.java
index ec9c235f00cc1b992340dfe0a0b79803361caac5..5275e4ed3d552d1e164ef580caf6c247ec5fd8c2 100644
--- a/src/main/java/net/minecraft/world/entity/animal/Cod.java
+++ b/src/main/java/net/minecraft/world/entity/animal/Cod.java
@@ -13,6 +13,18 @@ public class Cod extends AbstractSchoolingFish {
         super(type, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.codRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return true;
+    }
+    // Purpur end
+
     @Override
     public ItemStack getBucketItemStack() {
         return new ItemStack(Items.COD_BUCKET);
diff --git a/src/main/java/net/minecraft/world/entity/animal/Cow.java b/src/main/java/net/minecraft/world/entity/animal/Cow.java
index c9dcbc2dcb2736d0f448496c67121db29b7d4deb..1cb5342d63011605b567a4fe16d07de831f04216 100644
--- a/src/main/java/net/minecraft/world/entity/animal/Cow.java
+++ b/src/main/java/net/minecraft/world/entity/animal/Cow.java
@@ -40,9 +40,22 @@ public class Cow extends Animal {
         super(type, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.cowRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.cowRidableInWater;
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
         this.goalSelector.addGoal(0, new FloatGoal(this));
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(1, new PanicGoal(this, 2.0D));
         this.goalSelector.addGoal(2, new BreedGoal(this, 1.0D));
         this.goalSelector.addGoal(3, new TemptGoal(this, 1.25D, Ingredient.of(Items.WHEAT), false));
@@ -83,6 +96,7 @@ public class Cow extends Animal {
 
     @Override
     public InteractionResult mobInteract(Player player, InteractionHand hand) {
+        if (getRider() != null) return InteractionResult.PASS; // Purpur
         ItemStack itemstack = player.getItemInHand(hand);
 
         if (itemstack.is(Items.BUCKET) && !this.isBaby()) {
@@ -90,7 +104,7 @@ public class Cow extends Animal {
             org.bukkit.event.player.PlayerBucketFillEvent event = CraftEventFactory.callPlayerBucketFillEvent((ServerLevel) player.level, player, this.blockPosition(), this.blockPosition(), null, itemstack, Items.MILK_BUCKET, hand); // Paper - add enumHand
 
             if (event.isCancelled()) {
-                return InteractionResult.PASS;
+                return tryRide(player, hand); // Purpur
             }
             // CraftBukkit end
 
diff --git a/src/main/java/net/minecraft/world/entity/animal/Dolphin.java b/src/main/java/net/minecraft/world/entity/animal/Dolphin.java
index f3093815066e6881a2bb638ae4643f69374450b3..2300abc4e80449e6b92992f6fb8cfe8e99dea351 100644
--- a/src/main/java/net/minecraft/world/entity/animal/Dolphin.java
+++ b/src/main/java/net/minecraft/world/entity/animal/Dolphin.java
@@ -84,14 +84,82 @@ public class Dolphin extends WaterAnimal {
     public static final Predicate<ItemEntity> ALLOWED_ITEMS = (entityitem) -> {
         return !entityitem.hasPickUpDelay() && entityitem.isAlive() && entityitem.isInWater();
     };
+    private int spitCooldown; // Purpur
 
     public Dolphin(EntityType<? extends Dolphin> type, Level world) {
         super(type, world);
-        this.moveControl = new SmoothSwimmingMoveControl(this, 85, 10, 0.02F, 0.1F, true);
+        // Purpur start
+        class DolphinMoveControl extends SmoothSwimmingMoveControl {
+            private final net.pl3x.purpur.controller.WaterMoveControllerWASD waterMoveControllerWASD;
+            private final Dolphin dolphin;
+
+            public DolphinMoveControl(Dolphin dolphin, int pitchChange, int yawChange, float speedInWater, float speedInAir, boolean buoyant) {
+                super(dolphin, pitchChange, yawChange, speedInWater, speedInAir, buoyant);
+                this.dolphin = dolphin;
+                this.waterMoveControllerWASD = new net.pl3x.purpur.controller.WaterMoveControllerWASD(dolphin);
+            }
+
+            @Override
+            public void tick() {
+                if (dolphin.getRider() != null) {
+                    purpurTick(dolphin.getRider());
+                } else {
+                    super.tick();
+                }
+            }
+
+            public void purpurTick(Player rider) {
+                if (dolphin.getAirSupply() < 150) {
+                    // if drowning override player WASD controls to find air
+                    tick();
+                } else {
+                    waterMoveControllerWASD.purpurTick(rider);
+                    dolphin.setDeltaMovement(dolphin.getDeltaMovement().add(0.0D, 0.005D, 0.0D));
+                }
+            }
+        };
+        this.moveControl = new DolphinMoveControl(this, 85, 10, 0.02F, 0.1F, true);
+        // Purpur end
         this.lookControl = new SmoothSwimmingLookControl(this, 10);
         this.setCanPickUpLoot(true);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.dolphinRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return true;
+    }
+
+    @Override
+    public boolean onSpacebar() {
+        if (spitCooldown == 0 && getRider() != null) {
+            spitCooldown = level.purpurConfig.dolphinSpitCooldown;
+
+            org.bukkit.craftbukkit.entity.CraftPlayer player = (org.bukkit.craftbukkit.entity.CraftPlayer) getRider().getBukkitEntity();
+            if (!player.hasPermission("allow.special.dolphin")) {
+                return false;
+            }
+
+            org.bukkit.Location loc = player.getEyeLocation();
+            loc.setPitch(loc.getPitch() - 10);
+            org.bukkit.util.Vector target = loc.getDirection().normalize().multiply(10).add(loc.toVector());
+
+            net.pl3x.purpur.entity.DolphinSpit spit = new net.pl3x.purpur.entity.DolphinSpit(level, this);
+            spit.shoot(target.getX() - getX(), target.getY() - getY(), target.getZ() - getZ(), level.purpurConfig.dolphinSpitSpeed, 5.0F);
+
+            level.addFreshEntity(spit);
+            playSound(SoundEvents.DOLPHIN_ATTACK, 1.0F, 1.0F + (random.nextFloat() - random.nextFloat()) * 0.2F);
+            return true;
+        }
+        return false;
+    }
+    // Purpur end
+
     @Nullable
     @Override
     public SpawnGroupData finalizeSpawn(ServerLevelAccessor world, DifficultyInstance difficulty, MobSpawnType spawnReason, @Nullable SpawnGroupData entityData, @Nullable CompoundTag entityNbt) {
@@ -166,6 +234,7 @@ public class Dolphin extends WaterAnimal {
     protected void registerGoals() {
         this.goalSelector.addGoal(0, new BreathAirGoal(this));
         this.goalSelector.addGoal(0, new TryFindWaterGoal(this));
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(1, new Dolphin.DolphinSwimToTreasureGoal(this));
         this.goalSelector.addGoal(2, new Dolphin.DolphinSwimWithPlayerGoal(this, 4.0D));
         this.goalSelector.addGoal(4, new RandomSwimmingGoal(this, 1.0D, 10));
@@ -176,6 +245,7 @@ public class Dolphin extends WaterAnimal {
         this.goalSelector.addGoal(8, new Dolphin.PlayWithItemsGoal());
         this.goalSelector.addGoal(8, new FollowBoatGoal(this));
         this.goalSelector.addGoal(9, new AvoidEntityGoal<>(this, Guardian.class, 8.0F, 1.0D, 1.0D));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[]{Guardian.class})).setAlertOthers(new Class[0])); // CraftBukkit - decompile error
     }
 
@@ -227,7 +297,7 @@ public class Dolphin extends WaterAnimal {
 
     @Override
     protected boolean canRide(Entity entity) {
-        return true;
+        return boardingCooldown <= 0; // Purpur - make dolphin honor ride cooldown like all other non-boss mobs;
     }
 
     @Override
@@ -262,6 +332,11 @@ public class Dolphin extends WaterAnimal {
     @Override
     public void tick() {
         super.tick();
+        // Purpur start
+        if (spitCooldown > 0) {
+            spitCooldown--;
+        }
+        // Purpur end
         if (this.isNoAi()) {
             this.setAirSupply(this.getMaxAirSupply());
         } else {
diff --git a/src/main/java/net/minecraft/world/entity/animal/Fox.java b/src/main/java/net/minecraft/world/entity/animal/Fox.java
index 21274d2c8c78d8454dbfc19076a52e54876bd78d..ee9dfc609cc9e4a94dca7803e51bb456c1e63389 100644
--- a/src/main/java/net/minecraft/world/entity/animal/Fox.java
+++ b/src/main/java/net/minecraft/world/entity/animal/Fox.java
@@ -138,6 +138,39 @@ public class Fox extends Animal {
         this.setCanPickUpLoot(true);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.foxRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.foxRidableInWater;
+    }
+
+    @Override
+    public float getJumpPower() {
+        return getRider() != null ? 0.5F : super.getJumpPower();
+    }
+
+    @Override
+    public void onMount(Player rider) {
+        super.onMount(rider);
+        setCanPickUpLoot(false);
+        clearStates();
+        setIsPouncing(false);
+        spitOutItem(getItemBySlot(EquipmentSlot.MAINHAND));
+        setItemSlot(EquipmentSlot.MAINHAND, ItemStack.EMPTY);
+    }
+
+    @Override
+    public void onDismount(Player rider) {
+        super.onDismount(rider);
+        setCanPickUpLoot(true);
+    }
+    // Purpur end
+
     @Override
     protected void defineSynchedData() {
         super.defineSynchedData();
@@ -157,6 +190,7 @@ public class Fox extends Animal {
             return entityliving instanceof AbstractSchoolingFish;
         });
         this.goalSelector.addGoal(0, new Fox.FoxFloatGoal());
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(1, new Fox.FaceplantGoal());
         this.goalSelector.addGoal(2, new Fox.FoxPanicGoal(2.2D));
         this.goalSelector.addGoal(3, new Fox.FoxBreedGoal(1.0D));
@@ -182,6 +216,7 @@ public class Fox extends Animal {
         this.goalSelector.addGoal(11, new Fox.FoxSearchForItemsGoal());
         this.goalSelector.addGoal(12, new Fox.FoxLookAtPlayerGoal(this, Player.class, 24.0F));
         this.goalSelector.addGoal(13, new Fox.PerchAndSearchGoal());
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(3, new Fox.DefendTrustedTargetGoal(LivingEntity.class, false, false, (entityliving) -> {
             return Fox.TRUSTED_TARGET_SELECTOR.test(entityliving) && !this.trusts(entityliving.getUUID());
         }));
@@ -737,16 +772,16 @@ public class Fox extends Animal {
         return new Vec3(0.0D, (double) (0.55F * this.getEyeHeight()), (double) (this.getBbWidth() * 0.4F));
     }
 
-    public class FoxLookControl extends LookControl {
+    public class FoxLookControl extends net.pl3x.purpur.controller.LookControllerWASD { // Purpur
 
         public FoxLookControl() {
             super(Fox.this);
         }
 
         @Override
-        public void tick() {
+        public void vanillaTick() { // Purpur
             if (!Fox.this.isSleeping()) {
-                super.tick();
+                super.vanillaTick(); // Purpur
             }
 
         }
@@ -757,16 +792,16 @@ public class Fox extends Animal {
         }
     }
 
-    private class FoxMoveControl extends MoveControl {
+    private class FoxMoveControl extends net.pl3x.purpur.controller.MoveControllerWASD { // Purpur
 
         public FoxMoveControl() {
             super(Fox.this);
         }
 
         @Override
-        public void tick() {
+        public void vanillaTick() { // Purpur
             if (Fox.this.canMove()) {
-                super.tick();
+                super.vanillaTick(); // Purpur
             }
 
         }
diff --git a/src/main/java/net/minecraft/world/entity/animal/IronGolem.java b/src/main/java/net/minecraft/world/entity/animal/IronGolem.java
index ec00c2dd8f969eb99ec6a014e3bcd09c7484b237..63b739c86d2ea22aa9a610796ad2f208b0db289e 100644
--- a/src/main/java/net/minecraft/world/entity/animal/IronGolem.java
+++ b/src/main/java/net/minecraft/world/entity/animal/IronGolem.java
@@ -70,8 +70,22 @@ public class IronGolem extends AbstractGolem implements NeutralMob {
         this.maxUpStep = 1.0F;
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.ironGolemRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.ironGolemRidableInWater;
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
+        if (level.purpurConfig.ironGolemCanSwim) this.goalSelector.addGoal(0, new net.minecraft.world.entity.ai.goal.FloatGoal(this)); // Purpur
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(1, new MeleeAttackGoal(this, 1.0D, true));
         this.goalSelector.addGoal(2, new MoveTowardsTargetGoal(this, 0.9D, 32.0F));
         this.goalSelector.addGoal(2, new MoveBackToVillageGoal(this, 0.6D, false));
@@ -79,6 +93,7 @@ public class IronGolem extends AbstractGolem implements NeutralMob {
         this.goalSelector.addGoal(5, new OfferFlowerGoal(this));
         this.goalSelector.addGoal(7, new LookAtPlayerGoal(this, Player.class, 6.0F));
         this.goalSelector.addGoal(8, new RandomLookAroundGoal(this));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(1, new DefendVillageTargetGoal(this));
         this.targetSelector.addGoal(2, new HurtByTargetGoal(this, new Class[0]));
         this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, this::isAngryAt));
@@ -266,13 +281,13 @@ public class IronGolem extends AbstractGolem implements NeutralMob {
         ItemStack itemstack = player.getItemInHand(hand);
 
         if (!itemstack.is(Items.IRON_INGOT)) {
-            return InteractionResult.PASS;
+            return tryRide(player, hand); // Purpur
         } else {
             float f = this.getHealth();
 
             this.heal(25.0F);
             if (this.getHealth() == f) {
-                return InteractionResult.PASS;
+                return tryRide(player, hand); // Purpur
             } else {
                 float f1 = 1.0F + (this.random.nextFloat() - this.random.nextFloat()) * 0.2F;
 
diff --git a/src/main/java/net/minecraft/world/entity/animal/MushroomCow.java b/src/main/java/net/minecraft/world/entity/animal/MushroomCow.java
index b2765fba362e5f6d641abcc9ee7ccc177db08eb5..81a393f29643a0cf3478ca8ff640c21153d1514b 100644
--- a/src/main/java/net/minecraft/world/entity/animal/MushroomCow.java
+++ b/src/main/java/net/minecraft/world/entity/animal/MushroomCow.java
@@ -61,6 +61,18 @@ public class MushroomCow extends Cow implements Shearable {
         super(type, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.mooshroomRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.mooshroomRidableInWater;
+    }
+    // Purpur end
+
     @Override
     public float getWalkTargetValue(BlockPos pos, LevelReader world) {
         return world.getBlockState(pos.below()).is(Blocks.MYCELIUM) ? 10.0F : world.getBrightness(pos) - 0.5F;
@@ -122,7 +134,7 @@ public class MushroomCow extends Cow implements Shearable {
         } else if (itemstack.is(Items.SHEARS) && this.readyForShearing()) {
             // CraftBukkit start
             if (!CraftEventFactory.handlePlayerShearEntityEvent(player, this, itemstack, hand)) {
-                return InteractionResult.PASS;
+                return tryRide(player, hand); // Purpur
             }
             // CraftBukkit end
             this.shear(SoundSource.PLAYERS);
@@ -143,7 +155,7 @@ public class MushroomCow extends Cow implements Shearable {
                 Optional<Pair<MobEffect, Integer>> optional = this.getEffectFromItemStack(itemstack);
 
                 if (!optional.isPresent()) {
-                    return InteractionResult.PASS;
+                    return tryRide(player, hand); // Purpur
                 }
 
                 Pair<MobEffect, Integer> pair = (Pair) optional.get();
diff --git a/src/main/java/net/minecraft/world/entity/animal/Ocelot.java b/src/main/java/net/minecraft/world/entity/animal/Ocelot.java
index 06bf44ceb6f959a99f268fe1e1dca494985fbf4e..2a8f6d920370b4087895e9f0098a0f41d9b1e152 100644
--- a/src/main/java/net/minecraft/world/entity/animal/Ocelot.java
+++ b/src/main/java/net/minecraft/world/entity/animal/Ocelot.java
@@ -68,6 +68,18 @@ public class Ocelot extends Animal {
         this.reassessTrustingGoals();
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.ocelotRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.ocelotRidableInWater;
+    }
+    // Purpur end
+
     public boolean isTrusting() {
         return (Boolean) this.entityData.get(Ocelot.DATA_TRUSTING);
     }
@@ -99,12 +111,14 @@ public class Ocelot extends Animal {
     protected void registerGoals() {
         this.temptGoal = new Ocelot.OcelotTemptGoal(this, 0.6D, Ocelot.TEMPT_INGREDIENT, true);
         this.goalSelector.addGoal(1, new FloatGoal(this));
+        this.goalSelector.addGoal(1, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(3, this.temptGoal);
         this.goalSelector.addGoal(7, new LeapAtTargetGoal(this, 0.3F));
         this.goalSelector.addGoal(8, new OcelotAttackGoal(this));
         this.goalSelector.addGoal(9, new BreedGoal(this, 0.8D));
         this.goalSelector.addGoal(10, new WaterAvoidingRandomStrollGoal(this, 0.8D, 1.0000001E-5F));
         this.goalSelector.addGoal(11, new LookAtPlayerGoal(this, Player.class, 10.0F));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, Chicken.class, false));
         this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, Turtle.class, 10, false, false, Turtle.BABY_ON_LAND_SELECTOR));
     }
diff --git a/src/main/java/net/minecraft/world/entity/animal/Panda.java b/src/main/java/net/minecraft/world/entity/animal/Panda.java
index 12ed864bedf2201fad68e2aeba249c3c18a12444..fa9dbb22dd13c650ef08af6030d60c5fc8ee8044 100644
--- a/src/main/java/net/minecraft/world/entity/animal/Panda.java
+++ b/src/main/java/net/minecraft/world/entity/animal/Panda.java
@@ -108,6 +108,27 @@ public class Panda extends Animal {
 
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.pandaRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.pandaRidableInWater;
+    }
+
+    @Override
+    public void onMount(Player rider) {
+        super.onMount(rider);
+        setForwardMot(0.0F);
+        sit(false);
+        eat(false);
+        setOnBack(false);
+    }
+    // Purpur end
+
     @Override
     public boolean canTakeItem(ItemStack stack) {
         EquipmentSlot enumitemslot = Mob.getEquipmentSlotForItem(stack);
@@ -263,6 +284,7 @@ public class Panda extends Animal {
     @Override
     protected void registerGoals() {
         this.goalSelector.addGoal(0, new FloatGoal(this));
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(2, new Panda.PandaPanicGoal(this, 2.0D));
         this.goalSelector.addGoal(2, new Panda.PandaBreedGoal(this, 1.0D));
         this.goalSelector.addGoal(3, new Panda.PandaAttackGoal(this, 1.2000000476837158D, true));
@@ -278,6 +300,7 @@ public class Panda extends Animal {
         this.goalSelector.addGoal(12, new Panda.PandaRollGoal(this));
         this.goalSelector.addGoal(13, new FollowParentGoal(this, 1.25D));
         this.goalSelector.addGoal(14, new WaterAvoidingRandomStrollGoal(this, 1.0D));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(1, (new Panda.PandaHurtByTargetGoal(this, new Class[0])).setAlertOthers(new Class[0]));
     }
 
@@ -617,7 +640,7 @@ public class Panda extends Animal {
         ItemStack itemstack = player.getItemInHand(hand);
 
         if (this.isScared()) {
-            return InteractionResult.PASS;
+            return tryRide(player, hand); // Purpur
         } else if (this.isOnBack()) {
             this.setOnBack(false);
             return InteractionResult.sidedSuccess(this.level.isClientSide);
@@ -636,7 +659,7 @@ public class Panda extends Animal {
                 this.gameEvent(GameEvent.MOB_INTERACT, this.eyeBlockPosition());
             } else {
                 if (this.level.isClientSide || this.isSitting() || this.isInWater()) {
-                    return InteractionResult.PASS;
+                    return tryRide(player, hand); // Purpur
                 }
 
                 this.tryToSit();
@@ -653,7 +676,7 @@ public class Panda extends Animal {
 
             return InteractionResult.SUCCESS;
         } else {
-            return InteractionResult.PASS;
+            return tryRide(player, hand); // Purpur
         }
     }
 
@@ -693,7 +716,7 @@ public class Panda extends Animal {
         return !this.isOnBack() && !this.isScared() && !this.isEating() && !this.isRolling() && !this.isSitting();
     }
 
-    private static class PandaMoveControl extends MoveControl {
+    private static class PandaMoveControl extends net.pl3x.purpur.controller.MoveControllerWASD { // Purpur
 
         private final Panda panda;
 
@@ -703,9 +726,9 @@ public class Panda extends Animal {
         }
 
         @Override
-        public void tick() {
+        public void vanillaTick() { // Purpur
             if (this.panda.canPerformAction()) {
-                super.tick();
+                super.vanillaTick(); // Purpur
             }
         }
     }
diff --git a/src/main/java/net/minecraft/world/entity/animal/Parrot.java b/src/main/java/net/minecraft/world/entity/animal/Parrot.java
index 345fe87d5d6c3883c28d2c1b34d1020e18864d97..b881f6e948b21ba851913d9f244d74d886f2cc3a 100644
--- a/src/main/java/net/minecraft/world/entity/animal/Parrot.java
+++ b/src/main/java/net/minecraft/world/entity/animal/Parrot.java
@@ -121,12 +121,63 @@ public class Parrot extends ShoulderRidingEntity implements FlyingAnimal {
 
     public Parrot(EntityType<? extends Parrot> type, Level world) {
         super(type, world);
-        this.moveControl = new FlyingMoveControl(this, 10, false);
+        // Purpur start
+        final net.pl3x.purpur.controller.FlyingWithSpacebarMoveControllerWASD flyingController = new net.pl3x.purpur.controller.FlyingWithSpacebarMoveControllerWASD(this, 0.3F);
+        class ParrotMoveControl extends FlyingMoveControl {
+            public ParrotMoveControl(Mob entity, int maxPitchChange, boolean noGravity) {
+                super(entity, maxPitchChange, noGravity);
+            }
+
+            @Override
+            public void tick() {
+                if (mob.getRider() != null) {
+                    flyingController.purpurTick(mob.getRider());
+                } else {
+                    super.tick();
+                }
+            }
+
+            @Override
+            public boolean hasWanted() {
+                return mob.getRider() != null ? getForwardMot() != 0 || getStrafeMot() != 0 : super.hasWanted();
+            }
+        }
+        this.moveControl = new ParrotMoveControl(this, 10, false);
+        // Purpur end
         this.setPathfindingMalus(BlockPathTypes.DANGER_FIRE, -1.0F);
         this.setPathfindingMalus(BlockPathTypes.DAMAGE_FIRE, -1.0F);
         this.setPathfindingMalus(BlockPathTypes.COCOA, -1.0F);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.parrotRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.parrotRidableInWater;
+    }
+
+    @Override
+    public double getMaxY() {
+        return level.purpurConfig.parrotMaxY;
+    }
+
+    @Override
+    public void travel(Vec3 vec3) {
+        super.travel(vec3);
+        if (getRider() != null && !onGround) {
+            float speed = (float) getAttributeValue(Attributes.FLYING_SPEED) * 2;
+            setSpeed(speed);
+            Vec3 mot = getDeltaMovement();
+            move(net.minecraft.world.entity.MoverType.SELF, mot.multiply(speed, 0.25, speed));
+            setDeltaMovement(mot.scale(0.9D));
+        }
+    }
+    // Purpur end
+
     @Nullable
     @Override
     public SpawnGroupData finalizeSpawn(ServerLevelAccessor world, DifficultyInstance difficulty, MobSpawnType spawnReason, @Nullable SpawnGroupData entityData, @Nullable CompoundTag entityNbt) {
@@ -145,8 +196,10 @@ public class Parrot extends ShoulderRidingEntity implements FlyingAnimal {
 
     @Override
     protected void registerGoals() {
-        this.goalSelector.addGoal(0, new PanicGoal(this, 1.25D));
+        //this.goalSelector.addGoal(0, new PanicGoal(this, 1.25D)); // Purpur - move down
         this.goalSelector.addGoal(0, new FloatGoal(this));
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
+        this.goalSelector.addGoal(1, new PanicGoal(this, 1.25D)); // Purpur
         this.goalSelector.addGoal(1, new LookAtPlayerGoal(this, Player.class, 8.0F));
         this.goalSelector.addGoal(2, new SitWhenOrderedToGoal(this));
         this.goalSelector.addGoal(2, new FollowOwnerGoal(this, 1.0D, 5.0F, 1.0F, true));
diff --git a/src/main/java/net/minecraft/world/entity/animal/Pig.java b/src/main/java/net/minecraft/world/entity/animal/Pig.java
index 2c95b6eddfe46e5d2ad495bfc86ccc24ae75e704..80c8ceda47bf8b3ff370f89a61aa01869d606ee6 100644
--- a/src/main/java/net/minecraft/world/entity/animal/Pig.java
+++ b/src/main/java/net/minecraft/world/entity/animal/Pig.java
@@ -65,9 +65,22 @@ public class Pig extends Animal implements ItemSteerable, Saddleable {
         this.steering = new ItemBasedSteering(this.entityData, Pig.DATA_BOOST_TIME, Pig.DATA_SADDLE_ID);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.pigRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.pigRidableInWater;
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
         this.goalSelector.addGoal(0, new FloatGoal(this));
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(1, new PanicGoal(this, 1.25D));
         this.goalSelector.addGoal(3, new BreedGoal(this, 1.0D));
         this.goalSelector.addGoal(4, new TemptGoal(this, 1.2D, Ingredient.of(Items.CARROT_ON_A_STICK), false));
diff --git a/src/main/java/net/minecraft/world/entity/animal/PolarBear.java b/src/main/java/net/minecraft/world/entity/animal/PolarBear.java
index 0694cd0b994ee595adca43c988485e6dc13c7244..dc6bc8f10d147cb1d0e5c69b6f6df63b0e8d4531 100644
--- a/src/main/java/net/minecraft/world/entity/animal/PolarBear.java
+++ b/src/main/java/net/minecraft/world/entity/animal/PolarBear.java
@@ -61,11 +61,35 @@ public class PolarBear extends Animal implements NeutralMob {
     private static final UniformInt PERSISTENT_ANGER_TIME = TimeUtil.rangeOfSeconds(20, 39);
     private int remainingPersistentAngerTime;
     private UUID persistentAngerTarget;
+    private int standTimer = 0; // Purpur
 
     public PolarBear(EntityType<? extends PolarBear> type, Level world) {
         super(type, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.polarBearRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.polarBearRidableInWater;
+    }
+
+    @Override
+    public boolean onSpacebar() {
+        if (!isStanding()) {
+            if (getRider() != null && getRider().getForwardMot() == 0 && getRider().getStrafeMot() == 0) {
+                setStanding(true);
+                playSound(SoundEvents.POLAR_BEAR_WARNING, 1.0F, 1.0F);
+            }
+        }
+        return false;
+    }
+    // Purpur end
+
     @Override
     public AgeableMob getBreedOffspring(ServerLevel world, AgeableMob entity) {
         return EntityType.POLAR_BEAR.create(world);
@@ -80,12 +104,14 @@ public class PolarBear extends Animal implements NeutralMob {
     protected void registerGoals() {
         super.registerGoals();
         this.goalSelector.addGoal(0, new FloatGoal(this));
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(1, new PolarBear.PolarBearMeleeAttackGoal());
         this.goalSelector.addGoal(1, new PolarBear.PolarBearPanicGoal());
         this.goalSelector.addGoal(4, new FollowParentGoal(this, 1.25D));
         this.goalSelector.addGoal(5, new RandomStrollGoal(this, 1.0D));
         this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 6.0F));
         this.goalSelector.addGoal(7, new RandomLookAroundGoal(this));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(1, new PolarBear.PolarBearHurtByTargetGoal());
         this.targetSelector.addGoal(2, new PolarBear.PolarBearAttackPlayersGoal());
         this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, this::isAngryAt));
@@ -201,6 +227,11 @@ public class PolarBear extends Animal implements NeutralMob {
             this.updatePersistentAnger((ServerLevel)this.level, true);
         }
 
+        // Purpur start
+        if (isStanding() && --standTimer <= 0) {
+            setStanding(false);
+        }
+        // Purpur end
     }
 
     @Override
@@ -230,6 +261,7 @@ public class PolarBear extends Animal implements NeutralMob {
 
     public void setStanding(boolean warning) {
         this.entityData.set(DATA_STANDING_ID, warning);
+        standTimer = warning ? 20 : -1; // Purpur
     }
 
     public float getStandingAnimationScale(float tickDelta) {
diff --git a/src/main/java/net/minecraft/world/entity/animal/Pufferfish.java b/src/main/java/net/minecraft/world/entity/animal/Pufferfish.java
index 1b76e267cd36010a57d31852086dec0585d4bce5..42446fd8ba1d78cd5992bc6c2e5259da361eb716 100644
--- a/src/main/java/net/minecraft/world/entity/animal/Pufferfish.java
+++ b/src/main/java/net/minecraft/world/entity/animal/Pufferfish.java
@@ -45,6 +45,18 @@ public class Pufferfish extends AbstractFish {
         this.refreshDimensions();
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.pufferfishRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return true;
+    }
+    // Purpur end
+
     @Override
     protected void defineSynchedData() {
         super.defineSynchedData();
diff --git a/src/main/java/net/minecraft/world/entity/animal/Rabbit.java b/src/main/java/net/minecraft/world/entity/animal/Rabbit.java
index 3a9391b512974e812cac0d89119f68ba6728bfc9..29551a3b87f06c5876de5fda80615acf6c1f4764 100644
--- a/src/main/java/net/minecraft/world/entity/animal/Rabbit.java
+++ b/src/main/java/net/minecraft/world/entity/animal/Rabbit.java
@@ -81,6 +81,7 @@ public class Rabbit extends Animal {
     private boolean wasOnGround;
     private int jumpDelayTicks;
     int moreCarrotTicks;
+    private boolean actualJump; // Purpur
 
     public Rabbit(EntityType<? extends Rabbit> type, Level world) {
         super(type, world);
@@ -89,6 +90,46 @@ public class Rabbit extends Animal {
         this.initializePathFinderGoals(); // CraftBukkit - moved code
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.rabbitRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.rabbitRidableInWater;
+    }
+
+    @Override
+    public boolean onSpacebar() {
+        if (onGround) {
+            actualJump = true;
+            jumpFromGround();
+            actualJump = false;
+        }
+        return true;
+    }
+
+    private void handleJumping() {
+        if (onGround) {
+            RabbitJumpControl jumpController = (RabbitJumpControl) jumpControl;
+            if (!wasOnGround) {
+                setJumping(false);
+                jumpController.setCanJump(false);
+            }
+            if (!jumpController.wantJump()) {
+                if (moveControl.hasWanted()) {
+                    startJumping();
+                }
+            } else if (!jumpController.canJump()) {
+                jumpController.setCanJump(true);
+            }
+        }
+        wasOnGround = onGround;
+    }
+    // Purpur end
+
     // CraftBukkit start - code from constructor
     public void initializePathFinderGoals(){
         this.setSpeedModifier(0.0D);
@@ -98,6 +139,7 @@ public class Rabbit extends Animal {
     @Override
     public void registerGoals() {
         this.goalSelector.addGoal(1, new FloatGoal(this));
+        this.goalSelector.addGoal(1, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(1, new Rabbit.RabbitPanicGoal(this, 2.2D));
         this.goalSelector.addGoal(2, new BreedGoal(this, 0.8D));
         this.goalSelector.addGoal(3, new TemptGoal(this, 1.0D, Ingredient.of(Items.CARROT, Items.GOLDEN_CARROT, Blocks.DANDELION), false));
@@ -111,6 +153,13 @@ public class Rabbit extends Animal {
 
     @Override
     protected float getJumpPower() {
+        if (getRider() != null) {
+            if (getForwardMot() < 0) {
+                setSpeed(getForwardMot() * 2F);
+            }
+            return actualJump ? 0.5F : 0.3F;
+        }
+        // Purpur end
         if (!this.horizontalCollision && (!this.moveControl.hasWanted() || this.moveControl.getWantedY() <= this.getY() + 0.5D)) {
             Path pathentity = this.navigation.getPath();
 
@@ -129,7 +178,7 @@ public class Rabbit extends Animal {
     }
 
     @Override
-    protected void jumpFromGround() {
+    public void jumpFromGround() { // Purpur - protected -> public
         super.jumpFromGround();
         double d0 = this.moveControl.getSpeedModifier();
 
@@ -179,6 +228,13 @@ public class Rabbit extends Animal {
 
     @Override
     public void customServerAiStep() {
+        // Purpur start
+        if (getRider() != null) {
+            handleJumping();
+            return;
+        }
+        // Purpur end
+
         if (this.jumpDelayTicks > 0) {
             --this.jumpDelayTicks;
         }
@@ -451,7 +507,7 @@ public class Rabbit extends Animal {
         }
     }
 
-    private static class RabbitMoveControl extends MoveControl {
+    private static class RabbitMoveControl extends net.pl3x.purpur.controller.MoveControllerWASD { // Purpur
 
         private final Rabbit rabbit;
         private double nextJumpSpeed;
@@ -462,14 +518,14 @@ public class Rabbit extends Animal {
         }
 
         @Override
-        public void tick() {
+        public void vanillaTick() { // Purpur
             if (this.rabbit.onGround && !this.rabbit.jumping && !((Rabbit.RabbitJumpControl) this.rabbit.jumpControl).wantJump()) {
                 this.rabbit.setSpeedModifier(0.0D);
             } else if (this.hasWanted()) {
                 this.rabbit.setSpeedModifier(this.nextJumpSpeed);
             }
 
-            super.tick();
+            super.vanillaTick(); // Purpur
         }
 
         @Override
diff --git a/src/main/java/net/minecraft/world/entity/animal/Salmon.java b/src/main/java/net/minecraft/world/entity/animal/Salmon.java
index 574f91414ecacd18fadef5a56a4142d9cfdb6713..02a9ce0b98bab47d57b51cdfb2194f0e72a9f705 100644
--- a/src/main/java/net/minecraft/world/entity/animal/Salmon.java
+++ b/src/main/java/net/minecraft/world/entity/animal/Salmon.java
@@ -13,6 +13,18 @@ public class Salmon extends AbstractSchoolingFish {
         super(type, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.salmonRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return true;
+    }
+    // Purpur end
+
     @Override
     public int getMaxSchoolSize() {
         return 5;
diff --git a/src/main/java/net/minecraft/world/entity/animal/Sheep.java b/src/main/java/net/minecraft/world/entity/animal/Sheep.java
index 9315c34e61aa0432175385736de51f1bfdafbf85..5890bf8aa714dc219059bca4950a1b5b1882dd3a 100644
--- a/src/main/java/net/minecraft/world/entity/animal/Sheep.java
+++ b/src/main/java/net/minecraft/world/entity/animal/Sheep.java
@@ -116,10 +116,23 @@ public class Sheep extends Animal implements Shearable {
         super(type, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.sheepRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.sheepRidableInWater;
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
         this.eatBlockGoal = new EatBlockGoal(this);
         this.goalSelector.addGoal(0, new FloatGoal(this));
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(1, new PanicGoal(this, 1.25D));
         this.goalSelector.addGoal(2, new BreedGoal(this, 1.0D));
         this.goalSelector.addGoal(3, new TemptGoal(this, 1.1D, Ingredient.of(Items.WHEAT), false));
diff --git a/src/main/java/net/minecraft/world/entity/animal/SnowGolem.java b/src/main/java/net/minecraft/world/entity/animal/SnowGolem.java
index 8c2f447d0d307df35d84b4907000023eb82ac6c2..229b3cc63d2ec0bc5e5797d45efd9ca45f3ea82a 100644
--- a/src/main/java/net/minecraft/world/entity/animal/SnowGolem.java
+++ b/src/main/java/net/minecraft/world/entity/animal/SnowGolem.java
@@ -53,12 +53,26 @@ public class SnowGolem extends AbstractGolem implements Shearable, RangedAttackM
         super(type, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.snowGolemRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.snowGolemRidableInWater;
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(1, new RangedAttackGoal(this, 1.25D, 20, 10.0F));
         this.goalSelector.addGoal(2, new WaterAvoidingRandomStrollGoal(this, 1.0D, 1.0000001E-5F));
         this.goalSelector.addGoal(3, new LookAtPlayerGoal(this, Player.class, 6.0F));
         this.goalSelector.addGoal(4, new RandomLookAroundGoal(this));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, Mob.class, 10, true, false, (entityliving) -> {
             return entityliving instanceof Enemy;
         }));
@@ -110,6 +124,7 @@ public class SnowGolem extends AbstractGolem implements Shearable, RangedAttackM
                 return;
             }
 
+            if (getRider() != null && !level.purpurConfig.snowGolemLeaveTrailWhenRidden) return; // Purpur - don't leave snow trail when being ridden
             BlockState iblockdata = Blocks.SNOW.defaultBlockState();
 
             for (int l = 0; l < 4; ++l) {
@@ -152,7 +167,7 @@ public class SnowGolem extends AbstractGolem implements Shearable, RangedAttackM
         if (itemstack.is(Items.SHEARS) && this.readyForShearing()) {
             // CraftBukkit start
             if (!CraftEventFactory.handlePlayerShearEntityEvent(player, this, itemstack, hand)) {
-                return InteractionResult.PASS;
+                return tryRide(player, hand); // Purpur
             }
             // CraftBukkit end
             this.shear(SoundSource.PLAYERS);
@@ -165,7 +180,7 @@ public class SnowGolem extends AbstractGolem implements Shearable, RangedAttackM
 
             return InteractionResult.sidedSuccess(this.level.isClientSide);
         } else {
-            return InteractionResult.PASS;
+            return tryRide(player, hand); // Purpur
         }
     }
 
diff --git a/src/main/java/net/minecraft/world/entity/animal/Squid.java b/src/main/java/net/minecraft/world/entity/animal/Squid.java
index 56838c9f214c0f75041e75c45ad1a0c72fcacc66..151bb142c553d733367a92b39b430b536e569480 100644
--- a/src/main/java/net/minecraft/world/entity/animal/Squid.java
+++ b/src/main/java/net/minecraft/world/entity/animal/Squid.java
@@ -54,9 +54,32 @@ public class Squid extends WaterAnimal {
         this.tentacleSpeed = 1.0F / (this.random.nextFloat() + 1.0F) * 0.2F;
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.squidRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return true;
+    }
+
+    protected void rotateVectorAroundY(org.bukkit.util.Vector vector, double degrees) {
+        double rad = Math.toRadians(degrees);
+        double cos = Math.cos(rad);
+        double sine = Math.sin(rad);
+        double x = vector.getX();
+        double z = vector.getZ();
+        vector.setX(cos * x - sine * z);
+        vector.setZ(sine * x + cos * z);
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
         this.goalSelector.addGoal(0, new Squid.SquidRandomMovementGoal(this));
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(1, new Squid.SquidFleeGoal());
     }
 
@@ -250,6 +273,38 @@ public class Squid extends WaterAnimal {
 
         @Override
         public void tick() {
+            // Purpur start
+            Player rider = squid.getRider();
+            if (rider != null) {
+                if (rider.jumping) {
+                    squid.onSpacebar();
+                }
+                float forward = rider.getForwardMot();
+                float strafe = rider.getStrafeMot();
+                float speed = (float) squid.getAttributeValue(Attributes.MOVEMENT_SPEED) * 10F;
+                if (forward < 0.0F) {
+                    speed *= -0.5;
+                }
+                org.bukkit.util.Vector dir = rider.getBukkitEntity().getEyeLocation().getDirection().normalize().multiply(speed / 20.0F);
+                if (strafe != 0.0F) {
+                    if (forward == 0.0F) {
+                        dir.setY(0);
+                        rotateVectorAroundY(dir, strafe > 0.0F ? -90 : 90);
+                    } else if (forward < 0.0F) {
+                        rotateVectorAroundY(dir, strafe > 0.0F ? 45 : -45);
+                    } else {
+                        rotateVectorAroundY(dir, strafe > 0.0F ? -45 : 45);
+                    }
+                }
+                if (forward != 0.0F || strafe != 0.0F) {
+                    squid.setMovementVector((float) dir.getX(), (float) dir.getY(), (float) dir.getZ());
+                } else {
+                    squid.setMovementVector(0.0F, 0.0F, 0.0F);
+                }
+                return;
+            }
+            // Purpur end
+
             int i = this.squid.getNoActionTime();
 
             if (i > 100) {
diff --git a/src/main/java/net/minecraft/world/entity/animal/TropicalFish.java b/src/main/java/net/minecraft/world/entity/animal/TropicalFish.java
index 84015b5bf4b9a11670ad4d984844a431876efb63..5f61fcffebf4d853711a38d1f315f3def25e31a7 100644
--- a/src/main/java/net/minecraft/world/entity/animal/TropicalFish.java
+++ b/src/main/java/net/minecraft/world/entity/animal/TropicalFish.java
@@ -43,6 +43,18 @@ public class TropicalFish extends AbstractSchoolingFish {
         super(type, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.tropicalFishRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return true;
+    }
+    // Purpur end
+
     public static String getPredefinedName(int variant) {
         return "entity.minecraft.tropical_fish.predefined." + variant;
     }
diff --git a/src/main/java/net/minecraft/world/entity/animal/Turtle.java b/src/main/java/net/minecraft/world/entity/animal/Turtle.java
index 01d8af06f19427354cac95d691e65d31253fef94..631539a752a038926355c23aeb160af64f363a61 100644
--- a/src/main/java/net/minecraft/world/entity/animal/Turtle.java
+++ b/src/main/java/net/minecraft/world/entity/animal/Turtle.java
@@ -90,6 +90,18 @@ public class Turtle extends Animal {
         this.maxUpStep = 1.0F;
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.turtleRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.turtleRidableInWater;
+    }
+    // Purpur end
+
     public void setHomePos(BlockPos pos) {
         this.entityData.set(Turtle.HOME_POS, pos.immutable()); // Paper - called with mutablepos...
     }
@@ -192,6 +204,7 @@ public class Turtle extends Animal {
 
     @Override
     protected void registerGoals() {
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(0, new Turtle.TurtlePanicGoal(this, 1.2D));
         this.goalSelector.addGoal(1, new Turtle.TurtleBreedGoal(this, 1.0D));
         this.goalSelector.addGoal(1, new Turtle.TurtleLayEggGoal(this, 1.0D));
@@ -349,13 +362,15 @@ public class Turtle extends Animal {
         org.bukkit.craftbukkit.event.CraftEventFactory.entityDamage = null; // CraftBukkit
     }
 
-    private static class TurtleMoveControl extends MoveControl {
+    private static class TurtleMoveControl extends net.pl3x.purpur.controller.MoveControllerWASD { // Purpur
 
         private final Turtle turtle;
+        private final net.pl3x.purpur.controller.WaterMoveControllerWASD waterController; // Purpur
 
         TurtleMoveControl(Turtle turtle) {
             super(turtle);
             this.turtle = turtle;
+            waterController = new net.pl3x.purpur.controller.WaterMoveControllerWASD(turtle, 0.25D); // Purpur
         }
 
         private void updateSpeed() {
@@ -374,8 +389,18 @@ public class Turtle extends Animal {
 
         }
 
+        // Purpur start
+        public void purpurTick(Player rider) {
+            if (turtle.isInWater()) {
+                waterController.purpurTick(rider);
+            } else {
+                super.purpurTick(rider);
+            }
+        }
+        // Purpur end
+
         @Override
-        public void tick() {
+        public void vanillaTick() { // Purpur
             this.updateSpeed();
             if (this.operation == MoveControl.Operation.MOVE_TO && !this.turtle.getNavigation().isDone()) {
                 double d0 = this.wantedX - this.turtle.getX();
@@ -388,7 +413,7 @@ public class Turtle extends Animal {
 
                 this.turtle.setYRot(this.rotlerp(this.turtle.getYRot(), f, 90.0F));
                 this.turtle.yBodyRot = this.turtle.getYRot();
-                float f1 = (float) (this.speedModifier * this.turtle.getAttributeValue(Attributes.MOVEMENT_SPEED));
+                float f1 = (float) (this.getSpeedModifier() * this.turtle.getAttributeValue(Attributes.MOVEMENT_SPEED));
 
                 this.turtle.setSpeed(Mth.lerp(0.125F, this.turtle.getSpeed(), f1));
                 this.turtle.setDeltaMovement(this.turtle.getDeltaMovement().add(0.0D, (double) this.turtle.getSpeed() * d1 * 0.1D, 0.0D));
diff --git a/src/main/java/net/minecraft/world/entity/animal/Wolf.java b/src/main/java/net/minecraft/world/entity/animal/Wolf.java
index 80caabee4d2100208f117a1c3e35247b65e318ad..bca3300e06d6eb0c6acdfb11d715a1e8447c9198 100644
--- a/src/main/java/net/minecraft/world/entity/animal/Wolf.java
+++ b/src/main/java/net/minecraft/world/entity/animal/Wolf.java
@@ -93,9 +93,27 @@ public class Wolf extends TamableAnimal implements NeutralMob {
         this.setTame(false);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.wolfRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.wolfRidableInWater;
+    }
+
+    public void onMount(Player rider) {
+        super.onMount(rider);
+        setInSittingPose(false);
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
         this.goalSelector.addGoal(1, new FloatGoal(this));
+        this.goalSelector.addGoal(1, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(2, new SitWhenOrderedToGoal(this));
         this.goalSelector.addGoal(3, new Wolf.WolfAvoidEntityGoal<>(this, Llama.class, 24.0F, 1.5D, 1.5D));
         this.goalSelector.addGoal(4, new LeapAtTargetGoal(this, 0.4F));
@@ -106,6 +124,7 @@ public class Wolf extends TamableAnimal implements NeutralMob {
         this.goalSelector.addGoal(9, new BegGoal(this, 8.0F));
         this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Player.class, 8.0F));
         this.goalSelector.addGoal(10, new RandomLookAroundGoal(this));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(1, new OwnerHurtByTargetGoal(this));
         this.targetSelector.addGoal(2, new OwnerHurtTargetGoal(this));
         this.targetSelector.addGoal(3, (new HurtByTargetGoal(this, new Class[0])).setAlertOthers(new Class[0])); // CraftBukkit - decompile error
diff --git a/src/main/java/net/minecraft/world/entity/animal/axolotl/Axolotl.java b/src/main/java/net/minecraft/world/entity/animal/axolotl/Axolotl.java
index 9c4bf9b66e97d06e17e008784feda1a192a81b00..f0f1a286011bded016897cc8a0783a8a49a43b33 100644
--- a/src/main/java/net/minecraft/world/entity/animal/axolotl/Axolotl.java
+++ b/src/main/java/net/minecraft/world/entity/animal/axolotl/Axolotl.java
@@ -90,6 +90,23 @@ public class Axolotl extends Animal implements LerpingModel, Bucketable {
         this.maxUpStep = 1.0F;
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.axolotlRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return true;
+    }
+
+    @Override
+    protected void registerGoals() {
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
+    }
+    // Purpur end
+
     @Override
     public Map<String, Vector3f> getModelRotationValues() {
         return this.modelRotationValues;
@@ -511,14 +528,22 @@ public class Axolotl extends Animal implements LerpingModel, Bucketable {
     private static class AxolotlMoveControl extends SmoothSwimmingMoveControl {
 
         private final Axolotl axolotl;
+        private final net.pl3x.purpur.controller.WaterMoveControllerWASD waterController; // Purpur
 
         public AxolotlMoveControl(Axolotl axolotl) {
             super(axolotl, 85, 10, 0.1F, 0.5F, false);
             this.axolotl = axolotl;
+            waterController = new net.pl3x.purpur.controller.WaterMoveControllerWASD(axolotl, 0.5D); // Purpur
         }
 
         @Override
         public void tick() {
+            // Purpur start
+            if (axolotl.getRider() != null) {
+                waterController.purpurTick(axolotl.getRider());
+                return;
+            }
+            // Purpur end
             if (!this.axolotl.isPlayingDead()) {
                 super.tick();
             }
@@ -533,9 +558,9 @@ public class Axolotl extends Animal implements LerpingModel, Bucketable {
         }
 
         @Override
-        public void tick() {
+        public void vanillaTick() { // Purpur
             if (!Axolotl.this.isPlayingDead()) {
-                super.tick();
+                super.vanillaTick(); // Purpur
             }
 
         }
diff --git a/src/main/java/net/minecraft/world/entity/animal/goat/Goat.java b/src/main/java/net/minecraft/world/entity/animal/goat/Goat.java
index 0d220ea2d2651c46ba60c68bdd8dad3182c69740..cb3b6c1866a4c5c05bc6c7f0d4ac03cd747271fe 100644
--- a/src/main/java/net/minecraft/world/entity/animal/goat/Goat.java
+++ b/src/main/java/net/minecraft/world/entity/animal/goat/Goat.java
@@ -72,6 +72,18 @@ public class Goat extends Animal {
         this.getNavigation().setCanFloat(true);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.goatRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.goatRidableInWater;
+    }
+    // Purpur end
+
     @Override
     protected Brain.Provider<Goat> brainProvider() {
         return Brain.provider((Collection) Goat.MEMORY_TYPES, (Collection) Goat.SENSOR_TYPES);
@@ -148,6 +160,7 @@ public class Goat extends Animal {
     @Override
     protected void customServerAiStep() {
         this.level.getProfiler().push("goatBrain");
+        if (getRider() == null) // Purpur - only use brain if no rider
         if (this.behaviorTick++ % this.activatedPriority == 0) // Airplane
         this.getBrain().tick((ServerLevel) this.level, this); // CraftBukkit - decompile error
         this.level.getProfiler().pop();
diff --git a/src/main/java/net/minecraft/world/entity/animal/horse/AbstractHorse.java b/src/main/java/net/minecraft/world/entity/animal/horse/AbstractHorse.java
index 5c104bf219e5e4acb8acfb160bd92f0a0621d864..fbd96e516ac34f874b0cca2da9076120e29254fb 100644
--- a/src/main/java/net/minecraft/world/entity/animal/horse/AbstractHorse.java
+++ b/src/main/java/net/minecraft/world/entity/animal/horse/AbstractHorse.java
@@ -113,12 +113,22 @@ public abstract class AbstractHorse extends Animal implements ContainerListener,
 
     protected AbstractHorse(EntityType<? extends AbstractHorse> type, Level world) {
         super(type, world);
+        this.moveControl = new net.minecraft.world.entity.ai.control.MoveControl(this); // Purpur - use vanilla controller
+        this.lookControl = new net.minecraft.world.entity.ai.control.LookControl(this); // Purpur - use vanilla controller
         this.maxUpStep = 1.0F;
         this.createInventory();
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return false; // vanilla handles
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HorseHasRider(this)); // Purpur
         this.goalSelector.addGoal(1, new PanicGoal(this, 1.2D));
         this.goalSelector.addGoal(1, new RunAroundLikeCrazyGoal(this, 1.2D));
         this.goalSelector.addGoal(2, new BreedGoal(this, 1.0D, AbstractHorse.class));
@@ -126,6 +136,7 @@ public abstract class AbstractHorse extends Animal implements ContainerListener,
         this.goalSelector.addGoal(6, new WaterAvoidingRandomStrollGoal(this, 0.7D));
         this.goalSelector.addGoal(7, new LookAtPlayerGoal(this, Player.class, 6.0F));
         this.goalSelector.addGoal(8, new RandomLookAroundGoal(this));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HorseHasRider(this)); // Purpur
         this.addBehaviourGoals();
     }
 
diff --git a/src/main/java/net/minecraft/world/entity/animal/horse/Donkey.java b/src/main/java/net/minecraft/world/entity/animal/horse/Donkey.java
index ab588b990c15ff809a1937321d11a03aab826f61..e5b13558f45966bf70593931922ca73f4a66c66a 100644
--- a/src/main/java/net/minecraft/world/entity/animal/horse/Donkey.java
+++ b/src/main/java/net/minecraft/world/entity/animal/horse/Donkey.java
@@ -15,6 +15,13 @@ public class Donkey extends AbstractChestedHorse {
         super(type, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.donkeyRidableInWater;
+    }
+    // Purpur end
+
     @Override
     protected SoundEvent getAmbientSound() {
         super.getAmbientSound();
diff --git a/src/main/java/net/minecraft/world/entity/animal/horse/Horse.java b/src/main/java/net/minecraft/world/entity/animal/horse/Horse.java
index 6326471fe48133bef94e98fd028e60a951bccf2b..c7b0d6a987644e0b589c143b0b6b68053be04f37 100644
--- a/src/main/java/net/minecraft/world/entity/animal/horse/Horse.java
+++ b/src/main/java/net/minecraft/world/entity/animal/horse/Horse.java
@@ -39,6 +39,13 @@ public class Horse extends AbstractHorse {
         super(type, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.horseRidableInWater;
+    }
+    // Purpur end
+
     @Override
     protected void randomizeAttributes() {
         this.getAttribute(Attributes.MAX_HEALTH).setBaseValue((double)this.generateRandomMaxHealth());
diff --git a/src/main/java/net/minecraft/world/entity/animal/horse/Llama.java b/src/main/java/net/minecraft/world/entity/animal/horse/Llama.java
index 5c31519193126b715105e1e83bb54f6a1681d19e..d336e2c63b39b22f9f3a7a84ee059c2d9d71239c 100644
--- a/src/main/java/net/minecraft/world/entity/animal/horse/Llama.java
+++ b/src/main/java/net/minecraft/world/entity/animal/horse/Llama.java
@@ -68,7 +68,46 @@ public class Llama extends AbstractChestedHorse implements RangedAttackMob {
 
     public Llama(EntityType<? extends Llama> type, Level world) {
         super(type, world);
+        // Purpur start
+        this.moveControl = new net.pl3x.purpur.controller.MoveControllerWASD(this) {
+            @Override
+            public void tick() {
+                if (entity.getRider() != null && isSaddled()) {
+                    purpurTick(entity.getRider());
+                } else {
+                    vanillaTick();
+                }
+            }
+        };
+        this.lookControl = new net.pl3x.purpur.controller.LookControllerWASD(this) {
+            @Override
+            public void tick() {
+                if (entity.getRider() != null && isSaddled()) {
+                    purpurTick(entity.getRider());
+                } else {
+                    vanillaTick();
+                }
+            }
+        };
+        // Purpur end
+    }
+
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.llamaRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.llamaRidableInWater;
+    }
+
+    @Override
+    public boolean isSaddled() {
+        return super.isSaddled() || (isTamed() && getSwag() != null);
     }
+    // Purpur end
 
     public boolean isTraderLlama() {
         return false;
@@ -113,6 +152,7 @@ public class Llama extends AbstractChestedHorse implements RangedAttackMob {
     @Override
     protected void registerGoals() {
         this.goalSelector.addGoal(0, new FloatGoal(this));
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HorseHasRider(this)); // Purpur
         this.goalSelector.addGoal(1, new RunAroundLikeCrazyGoal(this, 1.2D));
         this.goalSelector.addGoal(2, new LlamaFollowCaravanGoal(this, (double)2.1F));
         this.goalSelector.addGoal(3, new RangedAttackGoal(this, 1.25D, 40, 20.0F));
@@ -122,6 +162,7 @@ public class Llama extends AbstractChestedHorse implements RangedAttackMob {
         this.goalSelector.addGoal(6, new WaterAvoidingRandomStrollGoal(this, 0.7D));
         this.goalSelector.addGoal(7, new LookAtPlayerGoal(this, Player.class, 6.0F));
         this.goalSelector.addGoal(8, new RandomLookAroundGoal(this));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HorseHasRider(this)); // Purpur
         this.targetSelector.addGoal(1, new Llama.LlamaHurtByTargetGoal(this));
         this.targetSelector.addGoal(2, new Llama.LlamaAttackWolfGoal(this));
     }
diff --git a/src/main/java/net/minecraft/world/entity/animal/horse/Mule.java b/src/main/java/net/minecraft/world/entity/animal/horse/Mule.java
index 6b10aeab9d671327f6e85a31b0c85ef310a1d0b2..40cc7e20913938d6e30e6bca9f4ad70fb5e9435b 100644
--- a/src/main/java/net/minecraft/world/entity/animal/horse/Mule.java
+++ b/src/main/java/net/minecraft/world/entity/animal/horse/Mule.java
@@ -14,6 +14,13 @@ public class Mule extends AbstractChestedHorse {
         super(type, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.muleRidableInWater;
+    }
+    // Purpur end
+
     @Override
     protected SoundEvent getAmbientSound() {
         super.getAmbientSound();
diff --git a/src/main/java/net/minecraft/world/entity/animal/horse/SkeletonHorse.java b/src/main/java/net/minecraft/world/entity/animal/horse/SkeletonHorse.java
index 1d0a2296bab0140d3209c0749710d5fb952c79b4..d4057c300d39eca4ff2e11791ce5ba7993d9b66e 100644
--- a/src/main/java/net/minecraft/world/entity/animal/horse/SkeletonHorse.java
+++ b/src/main/java/net/minecraft/world/entity/animal/horse/SkeletonHorse.java
@@ -29,6 +29,13 @@ public class SkeletonHorse extends AbstractHorse {
         super(type, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean isTamed() {
+        return true;
+    }
+    // Purpur end
+
     public static AttributeSupplier.Builder createAttributes() {
         return createBaseHorseAttributes().add(Attributes.MAX_HEALTH, 15.0D).add(Attributes.MOVEMENT_SPEED, (double)0.2F);
     }
@@ -40,6 +47,7 @@ public class SkeletonHorse extends AbstractHorse {
 
     @Override
     protected void addBehaviourGoals() {
+        if (level.purpurConfig.skeletonHorseCanSwim) goalSelector.addGoal(0, new net.minecraft.world.entity.ai.goal.FloatGoal(this)); // Purpur
     }
 
     @Override
@@ -135,7 +143,7 @@ public class SkeletonHorse extends AbstractHorse {
 
     @Override
     public boolean rideableUnderWater() {
-        return true;
+        return level.purpurConfig.skeletonHorseRidableInWater; // Purpur
     }
 
     @Override
diff --git a/src/main/java/net/minecraft/world/entity/animal/horse/TraderLlama.java b/src/main/java/net/minecraft/world/entity/animal/horse/TraderLlama.java
index 885937d7001259d9a8ee8d5bc16629a196a13fe8..ff252d9ca75b90ab7606f63aa5f89b6230e33a36 100644
--- a/src/main/java/net/minecraft/world/entity/animal/horse/TraderLlama.java
+++ b/src/main/java/net/minecraft/world/entity/animal/horse/TraderLlama.java
@@ -27,6 +27,23 @@ public class TraderLlama extends Llama {
         super(type, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.traderLlamaRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.traderLlamaRidableInWater;
+    }
+
+    @Override
+    public boolean isSaddled() {
+        return super.isSaddled() || isTamed();
+    }
+    // Purpur end
+
     @Override
     public boolean isTraderLlama() {
         return true;
diff --git a/src/main/java/net/minecraft/world/entity/animal/horse/ZombieHorse.java b/src/main/java/net/minecraft/world/entity/animal/horse/ZombieHorse.java
index c6c7096ec476d91ef91b164e32c5a9cd112f3dd7..d328e36015b6b7d6a9e093fbe232eb5ecda46d96 100644
--- a/src/main/java/net/minecraft/world/entity/animal/horse/ZombieHorse.java
+++ b/src/main/java/net/minecraft/world/entity/animal/horse/ZombieHorse.java
@@ -22,6 +22,18 @@ public class ZombieHorse extends AbstractHorse {
         super(type, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.zombieHorseRidableInWater;
+    }
+
+    @Override
+    public boolean isTamed() {
+        return true;
+    }
+    // Purpur end
+
     public static AttributeSupplier.Builder createAttributes() {
         return createBaseHorseAttributes().add(Attributes.MAX_HEALTH, 15.0D).add(Attributes.MOVEMENT_SPEED, (double)0.2F);
     }
@@ -92,5 +104,6 @@ public class ZombieHorse extends AbstractHorse {
 
     @Override
     protected void addBehaviourGoals() {
+        if (level.purpurConfig.zombieHorseCanSwim) goalSelector.addGoal(0, new net.minecraft.world.entity.ai.goal.FloatGoal(this)); // Purpur
     }
 }
diff --git a/src/main/java/net/minecraft/world/entity/boss/EnderDragonPart.java b/src/main/java/net/minecraft/world/entity/boss/EnderDragonPart.java
index 305a891e4b51d1031d9e9238ff00e2ea7de8d954..84625d09df800fcfd477fc493fb5f8246567b7e8 100644
--- a/src/main/java/net/minecraft/world/entity/boss/EnderDragonPart.java
+++ b/src/main/java/net/minecraft/world/entity/boss/EnderDragonPart.java
@@ -21,6 +21,13 @@ public class EnderDragonPart extends Entity {
         this.name = name;
     }
 
+    // Purpur start
+    @Override
+    public net.minecraft.world.InteractionResult interact(net.minecraft.world.entity.player.Player player, net.minecraft.world.InteractionHand hand) {
+        return parentMob.isAlive() ? parentMob.tryRide(player, hand) : net.minecraft.world.InteractionResult.PASS;
+    }
+    // Purpur end
+
     @Override
     protected void defineSynchedData() {
     }
diff --git a/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java b/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java
index 29aa428e019681af8d6b0020c12b18660ff6af6c..5df112f87fba042f13f615a22a5c6f850b779bf7 100644
--- a/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java
+++ b/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java
@@ -102,6 +102,7 @@ public class EnderDragon extends Mob implements Enemy {
     private final int[] nodeAdjacency = new int[24];
     private final BinaryHeap openSet = new BinaryHeap();
     private Explosion explosionSource = new Explosion(null, this, null, null, Double.NaN, Double.NaN, Double.NaN, Float.NaN, true, Explosion.BlockInteraction.DESTROY); // CraftBukkit - reusable source for CraftTNTPrimed.getSource()
+    private boolean hadRider; // Purpur
 
     public EnderDragon(EntityType<? extends EnderDragon> entitytypes, Level world) {
         super(EntityType.ENDER_DRAGON, world);
@@ -116,8 +117,45 @@ public class EnderDragon extends Mob implements Enemy {
         }
 
         this.phaseManager = new EnderDragonPhaseManager(this);
+
+        // Purpur start
+        this.moveControl = new net.pl3x.purpur.controller.FlyingMoveControllerWASD(this) {
+            @Override
+            public void vanillaTick() {
+                // dragon doesn't use the controller. do nothing
+            }
+        };
+        this.lookControl = new net.pl3x.purpur.controller.LookControllerWASD(this) {
+            @Override
+            public void vanillaTick() {
+                // dragon doesn't use the controller. do nothing
+            }
+
+            @Override
+            public void purpurTick(Player rider) {
+                setYawPitch(rider.yRot - 180F, rider.xRotO * 0.5F);
+            }
+        };
+        // Purpur end
+    }
+
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.enderDragonRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.enderDragonRidableInWater;
     }
 
+    @Override
+    public double getMaxY() {
+        return level.purpurConfig.enderDragonMaxY;
+    }
+    // Purpur end
+
     public static AttributeSupplier.Builder createAttributes() {
         return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 200.0D);
     }
@@ -166,6 +204,37 @@ public class EnderDragon extends Mob implements Enemy {
 
     @Override
     public void aiStep() {
+        // Purpur start
+        boolean hasRider = getRider() != null;
+        if (hasRider) {
+            if (!hadRider) {
+                hadRider = true;
+                noPhysics = false;
+                this.dimensions = net.minecraft.world.entity.EntityDimensions.scalable(4.0F, 2.0F);
+            }
+
+            // dragon doesn't use controllers, so must tick manually
+            moveControl.tick();
+            lookControl.tick();
+
+            moveRelative((float) getAttributeValue(Attributes.MOVEMENT_SPEED) * 0.1F, new Vec3(-getStrafeMot(), getVerticalMot(), -getForwardMot()));
+            Vec3 mot = getDeltaMovement();
+            setDeltaMovement(mot);
+            move(MoverType.PLAYER, mot);
+
+            mot = mot.multiply(0.9F, 0.9F, 0.9F);
+            setDeltaMovement(mot);
+
+            // control wing flap speed on client
+            phaseManager.setPhase(mot.x() * mot.x() + mot.z() * mot.z() < 0.005F ? EnderDragonPhase.HOVERING : EnderDragonPhase.HOLDING_PATTERN);
+        } else if (hadRider) {
+            hadRider = false;
+            noPhysics = true;
+            this.dimensions = net.minecraft.world.entity.EntityDimensions.scalable(16.0F, 8.0F);
+            phaseManager.setPhase(EnderDragonPhase.HOLDING_PATTERN); // HoldingPattern
+        }
+        // Purpur end
+
         this.processFlappingMovement();
         if (this.level.isClientSide) {
             this.setHealth(this.getHealth());
@@ -179,6 +248,8 @@ public class EnderDragon extends Mob implements Enemy {
         float f;
 
         if (this.isDeadOrDying()) {
+            if (hasRider) ejectPassengers(); // Purpur
+
             float f1 = (this.random.nextFloat() - 0.5F) * 8.0F;
 
             f = (this.random.nextFloat() - 0.5F) * 4.0F;
@@ -191,9 +262,9 @@ public class EnderDragon extends Mob implements Enemy {
 
             f = 0.2F / ((float) vec3d.horizontalDistance() * 10.0F + 1.0F);
             f *= (float) Math.pow(2.0D, vec3d.y);
-            if (this.phaseManager.getCurrentPhase().isSitting()) {
+            if (!hasRider && this.phaseManager.getCurrentPhase().isSitting()) { // Purpur
                 this.flapTime += 0.1F;
-            } else if (this.inWall) {
+            } else if (!hasRider && this.inWall) { // Purpur
                 this.flapTime += f * 0.5F;
             } else {
                 this.flapTime += f;
@@ -238,7 +309,7 @@ public class EnderDragon extends Mob implements Enemy {
                     }
 
                     this.phaseManager.getCurrentPhase().doClientTick();
-                } else {
+                } else if (!hasRider) { // Purpur
                     DragonPhaseInstance idragoncontroller = this.phaseManager.getCurrentPhase();
 
                     idragoncontroller.doServerTick();
@@ -308,7 +379,7 @@ public class EnderDragon extends Mob implements Enemy {
                 this.tickPart(this.body, (double) (f11 * 0.5F), 0.0D, (double) (-f12 * 0.5F));
                 this.tickPart(this.wing1, (double) (f12 * 4.5F), 2.0D, (double) (f11 * 4.5F));
                 this.tickPart(this.wing2, (double) (f12 * -4.5F), 2.0D, (double) (f11 * -4.5F));
-                if (!this.level.isClientSide && this.hurtTime == 0) {
+                if (!hasRider && !this.level.isClientSide && this.hurtTime == 0) { // Purpur
                     this.knockBack(this.level.getEntities(this, this.wing1.getBoundingBox().inflate(4.0D, 2.0D, 4.0D).move(0.0D, -2.0D, 0.0D), EntitySelector.NO_CREATIVE_OR_SPECTATOR));
                     this.knockBack(this.level.getEntities(this, this.wing2.getBoundingBox().inflate(4.0D, 2.0D, 4.0D).move(0.0D, -2.0D, 0.0D), EntitySelector.NO_CREATIVE_OR_SPECTATOR));
                     this.hurt(this.level.getEntities(this, this.head.getBoundingBox().inflate(1.0D), EntitySelector.NO_CREATIVE_OR_SPECTATOR));
@@ -352,7 +423,7 @@ public class EnderDragon extends Mob implements Enemy {
                 }
 
                 if (!this.level.isClientSide) {
-                    this.inWall = this.checkWalls(this.head.getBoundingBox()) | this.checkWalls(this.neck.getBoundingBox()) | this.checkWalls(this.body.getBoundingBox());
+                    this.inWall = !hasRider && this.checkWalls(this.head.getBoundingBox()) | this.checkWalls(this.neck.getBoundingBox()) | this.checkWalls(this.body.getBoundingBox()); // Purpur
                     if (this.dragonFight != null) {
                         this.dragonFight.updateDragon(this);
                     }
diff --git a/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java b/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java
index bb329e9c1a2295e6433d3728692a1e716c89dcc0..fdc9068de403e98e18af11d4d6c5b708d2cc3c52 100644
--- a/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java
+++ b/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java
@@ -83,6 +83,7 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob
         return entityliving.getMobType() != MobType.UNDEAD && entityliving.attackable();
     };
     private static final TargetingConditions TARGETING_CONDITIONS = TargetingConditions.forCombat().range(20.0D).selector(WitherBoss.LIVING_ENTITY_SELECTOR);
+    private int shootCooldown = 0; // Purpur
     // Paper start
     private boolean canPortal = false;
 
@@ -95,15 +96,117 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob
         this.setHealth(this.getMaxHealth());
         this.getNavigation().setCanFloat(true);
         this.xpReward = 50;
+        this.moveControl = new net.pl3x.purpur.controller.FlyingWithSpacebarMoveControllerWASD(this, 0.1F); // Purpur
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.witherRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.witherRidableInWater;
+    }
+
+    @Override
+    public double getMaxY() {
+        return level.purpurConfig.witherMaxY;
+    }
+
+    @Override
+    public void travel(Vec3 vec3) {
+        super.travel(vec3);
+        if (getRider() != null && !onGround) {
+            float speed = (float) getAttributeValue(Attributes.FLYING_SPEED) * 5F;
+            setSpeed(speed);
+            Vec3 mot = getDeltaMovement();
+            move(net.minecraft.world.entity.MoverType.SELF, mot.multiply(speed, 0.5, speed));
+            setDeltaMovement(mot.scale(0.9D));
+        }
+    }
+
+    @Override
+    public void onMount(Player rider) {
+        super.onMount(rider);
+        this.entityData.set(DATA_TARGETS.get(0), 0);
+        this.entityData.set(DATA_TARGETS.get(1), 0);
+        this.entityData.set(DATA_TARGETS.get(2), 0);
+        getNavigation().stop();
+        shootCooldown = 20;
+    }
+
+    @Override
+    public boolean onClick(net.minecraft.world.InteractionHand hand) {
+        return shoot(getRider(), hand == net.minecraft.world.InteractionHand.MAIN_HAND ? new int[]{1} : new int[]{2});
+    }
+
+    public boolean shoot(@Nullable Player rider, int[] heads) {
+        if (shootCooldown > 0) {
+            return false;
+        }
+
+        shootCooldown = 20;
+        if (rider == null) {
+            return false;
+        }
+
+        org.bukkit.craftbukkit.entity.CraftHumanEntity player = rider.getBukkitEntity();
+        if (!player.hasPermission("allow.special.wither")) {
+            return false;
+        }
+
+        net.minecraft.world.phys.HitResult rayTrace = getRayTrace(120, net.minecraft.world.level.ClipContext.Fluid.NONE);
+        if (rayTrace == null) {
+            return false;
+        }
+
+        Vec3 loc;
+        if (rayTrace.getType() == net.minecraft.world.phys.HitResult.Type.BLOCK) {
+            BlockPos pos = ((net.minecraft.world.phys.BlockHitResult) rayTrace).getBlockPos();
+            loc = new Vec3(pos.getX() + 0.5D, pos.getY() + 0.5D, pos.getZ() + 0.5D);
+        } else if (rayTrace.getType() == net.minecraft.world.phys.HitResult.Type.ENTITY) {
+            Entity target = ((net.minecraft.world.phys.EntityHitResult) rayTrace).getEntity();
+            loc = new Vec3(target.getX(), target.getY() + (target.getEyeHeight() / 2), target.getZ());
+        } else {
+            org.bukkit.block.Block block = player.getTargetBlock(null, 120);
+            loc = new Vec3(block.getX() + 0.5D, block.getY() + 0.5D, block.getZ() + 0.5D);
+        }
+
+        for (int head : heads) {
+            shoot(head, loc.x(), loc.y(), loc.z(), rider);
+        }
+
+        return true; // handled
+    }
+
+    public void shoot(int head, double x, double y, double z, Player rider) {
+        level.levelEvent(null, 1024, blockPosition(), 0);
+        double headX = getHeadX(head);
+        double headY = getHeadY(head);
+        double headZ = getHeadZ(head);
+        WitherSkull skull = new WitherSkull(level, this, x - headX, y - headY, z - headZ) {
+            @Override
+            public boolean canHitEntity(Entity target) {
+                // do not hit rider
+                return target != rider && super.canHitEntity(target);
+            }
+        };
+        skull.setPosRaw(headX, headY, headZ);
+        level.addFreshEntity(skull);
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(0, new WitherBoss.WitherDoNothingGoal());
         this.goalSelector.addGoal(2, new RangedAttackGoal(this, 1.0D, 40, 20.0F));
         this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 1.0D));
         this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 8.0F));
         this.goalSelector.addGoal(7, new RandomLookAroundGoal(this));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(1, new HurtByTargetGoal(this, new Class[0]));
         if(this.level.paperConfig.fixWitherTargetingBug) this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, 0, false, false, null)); // Paper - Fix MC-29274
         this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Mob.class, 0, false, false, WitherBoss.LIVING_ENTITY_SELECTOR));
@@ -246,6 +349,16 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob
 
     @Override
     protected void customServerAiStep() {
+        // Purpur start
+        if (getRider() != null) {
+            Vec3 mot = getDeltaMovement();
+            setDeltaMovement(mot.x(), mot.y() + (getVerticalMot() > 0 ? 0.07D : 0.0D), mot.z());
+        }
+        if (shootCooldown > 0) {
+            shootCooldown--;
+        }
+        // Purpur end
+
         int i;
 
         if (this.getInvulnerableTicks() > 0) {
@@ -553,7 +666,7 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob
     }
 
     public static AttributeSupplier.Builder createAttributes() {
-        return Monster.createMonsterAttributes().add(Attributes.MAX_HEALTH, 300.0D).add(Attributes.MOVEMENT_SPEED, 0.6000000238418579D).add(Attributes.FOLLOW_RANGE, 40.0D).add(Attributes.ARMOR, 4.0D);
+        return Monster.createMonsterAttributes().add(Attributes.MAX_HEALTH, 300.0D).add(Attributes.MOVEMENT_SPEED, 0.6000000238418579D).add(Attributes.FOLLOW_RANGE, 40.0D).add(Attributes.ARMOR, 4.0D).add(Attributes.FLYING_SPEED, 0.6D); // Purpur
     }
 
     public float getHeadYRot(int headIndex) {
@@ -573,11 +686,11 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob
     }
 
     public int getAlternativeTarget(int headIndex) {
-        return (Integer) this.entityData.get((EntityDataAccessor) WitherBoss.DATA_TARGETS.get(headIndex));
+        return getRider() != null ? 0 : this.entityData.get(WitherBoss.DATA_TARGETS.get(headIndex)); // Purpur
     }
 
     public void setAlternativeTarget(int headIndex, int id) {
-        this.entityData.set((EntityDataAccessor) WitherBoss.DATA_TARGETS.get(headIndex), id);
+        if (getRider() == null) this.entityData.set(WitherBoss.DATA_TARGETS.get(headIndex), id); // Purpur
     }
 
     @Override
diff --git a/src/main/java/net/minecraft/world/entity/monster/AbstractSkeleton.java b/src/main/java/net/minecraft/world/entity/monster/AbstractSkeleton.java
index cd75f895b6818fbb7ed4b0ef3df873f264bb2d1b..74f4f8e1c9faef3b50fa817ca3dfbb6b19b1622d 100644
--- a/src/main/java/net/minecraft/world/entity/monster/AbstractSkeleton.java
+++ b/src/main/java/net/minecraft/world/entity/monster/AbstractSkeleton.java
@@ -69,12 +69,14 @@ public abstract class AbstractSkeleton extends Monster implements RangedAttackMo
 
     @Override
     protected void registerGoals() {
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(2, new RestrictSunGoal(this));
         this.goalSelector.addGoal(3, new FleeSunGoal(this, 1.0D));
         this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, Wolf.class, 6.0F, 1.0D, 1.2D));
         this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 1.0D));
         this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 8.0F));
         this.goalSelector.addGoal(6, new RandomLookAroundGoal(this));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(1, new HurtByTargetGoal(this, new Class[0]));
         this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true));
         this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, IronGolem.class, true));
diff --git a/src/main/java/net/minecraft/world/entity/monster/Blaze.java b/src/main/java/net/minecraft/world/entity/monster/Blaze.java
index a1bbe0c34bd5693f041dca9028a5a52b2ca71bc5..a5294012d432d5d1fce570a1eb1b1716a24522de 100644
--- a/src/main/java/net/minecraft/world/entity/monster/Blaze.java
+++ b/src/main/java/net/minecraft/world/entity/monster/Blaze.java
@@ -32,6 +32,7 @@ public class Blaze extends Monster {
 
     public Blaze(EntityType<? extends Blaze> type, Level world) {
         super(type, world);
+        this.moveControl = new net.pl3x.purpur.controller.FlyingWithSpacebarMoveControllerWASD(this, 0.3F); // Purpur
         this.setPathfindingMalus(BlockPathTypes.WATER, -1.0F);
         this.setPathfindingMalus(BlockPathTypes.LAVA, 8.0F);
         this.setPathfindingMalus(BlockPathTypes.DANGER_FIRE, 0.0F);
@@ -39,19 +40,50 @@ public class Blaze extends Monster {
         this.xpReward = 10;
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.blazeRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.blazeRidableInWater;
+    }
+
+    @Override
+    public double getMaxY() {
+        return level.purpurConfig.blazeMaxY;
+    }
+
+    @Override
+    public void travel(Vec3 vec3) {
+        super.travel(vec3);
+        if (getRider() != null && !onGround) {
+            float speed = (float) getAttributeValue(Attributes.FLYING_SPEED);
+            setSpeed(speed);
+            Vec3 mot = getDeltaMovement();
+            move(net.minecraft.world.entity.MoverType.SELF, mot.multiply(speed, 1.0, speed));
+            setDeltaMovement(mot.scale(0.9D));
+        }
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(4, new Blaze.BlazeAttackGoal(this));
         this.goalSelector.addGoal(5, new MoveTowardsRestrictionGoal(this, 1.0D));
         this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0D, 0.0F));
         this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F));
         this.goalSelector.addGoal(8, new RandomLookAroundGoal(this));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(1, (new HurtByTargetGoal(this)).setAlertOthers());
         this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true));
     }
 
     public static AttributeSupplier.Builder createAttributes() {
-        return Monster.createMonsterAttributes().add(Attributes.ATTACK_DAMAGE, 6.0D).add(Attributes.MOVEMENT_SPEED, (double)0.23F).add(Attributes.FOLLOW_RANGE, 48.0D);
+        return Monster.createMonsterAttributes().add(Attributes.ATTACK_DAMAGE, 6.0D).add(Attributes.MOVEMENT_SPEED, (double)0.23F).add(Attributes.FOLLOW_RANGE, 48.0D).add(Attributes.FLYING_SPEED, 0.6D); // Purpur
     }
 
     @Override
@@ -106,6 +138,14 @@ public class Blaze extends Monster {
 
     @Override
     protected void customServerAiStep() {
+        // Purpur start
+        if (getRider() != null) {
+            Vec3 mot = getDeltaMovement();
+            setDeltaMovement(mot.x(), getVerticalMot() > 0 ? 0.07D : -0.07D, mot.z());
+            return;
+        }
+        // Purpur end
+
         --this.nextHeightOffsetChangeTick;
         if (this.nextHeightOffsetChangeTick <= 0) {
             this.nextHeightOffsetChangeTick = 100;
diff --git a/src/main/java/net/minecraft/world/entity/monster/CaveSpider.java b/src/main/java/net/minecraft/world/entity/monster/CaveSpider.java
index d980b906d9206560741576fa4153c57212f307a0..dd05c5f35d78d1385f793f4e34333ff242d136e1 100644
--- a/src/main/java/net/minecraft/world/entity/monster/CaveSpider.java
+++ b/src/main/java/net/minecraft/world/entity/monster/CaveSpider.java
@@ -28,6 +28,18 @@ public class CaveSpider extends Spider {
         return Spider.createAttributes().add(Attributes.MAX_HEALTH, 12.0D);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.caveSpiderRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.caveSpiderRidableInWater;
+    }
+    // Purpur end
+
     @Override
     public boolean doHurtTarget(Entity target) {
         if (super.doHurtTarget(target)) {
diff --git a/src/main/java/net/minecraft/world/entity/monster/Creeper.java b/src/main/java/net/minecraft/world/entity/monster/Creeper.java
index e8c36e8541f041a0d72a86f49ced2a3ce1549be0..1bf65519883585569a1acc780554505dc5d6c30b 100644
--- a/src/main/java/net/minecraft/world/entity/monster/Creeper.java
+++ b/src/main/java/net/minecraft/world/entity/monster/Creeper.java
@@ -60,21 +60,89 @@ public class Creeper extends Monster implements PowerableMob {
     public int maxSwell = 30;
     public int explosionRadius = 3;
     private int droppedSkulls;
+    // Purpur start
+    private int spacebarCharge = 0;
+    private int prevSpacebarCharge = 0;
+    private int powerToggleDelay = 0;
+    // Purpur end
 
     public Creeper(EntityType<? extends Creeper> type, Level world) {
         super(type, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.creeperRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.creeperRidableInWater;
+    }
+
+    @Override
+    protected void customServerAiStep() {
+        if (powerToggleDelay > 0) {
+            powerToggleDelay--;
+        }
+        if (getRider() != null) {
+            if (getRider().getForwardMot() != 0 || getRider().getStrafeMot() != 0) {
+                spacebarCharge = 0;
+                setIgnited(false);
+            }
+            if (spacebarCharge == prevSpacebarCharge) {
+                spacebarCharge = 0;
+            }
+            prevSpacebarCharge = spacebarCharge;
+        }
+        super.customServerAiStep();
+    }
+
+    @Override
+    public void onMount(Player rider) {
+        super.onMount(rider);
+        setIgnited(false);
+    }
+
+    @Override
+    public boolean onSpacebar() {
+        if (powerToggleDelay > 0) {
+            return true; // just toggled power, do not jump or ignite
+        }
+        spacebarCharge++;
+        if (spacebarCharge > maxSwell - 2) {
+            spacebarCharge = 0;
+            if (getRider() != null && getRider().getBukkitEntity().hasPermission("allow.powered.creeper")) {
+                powerToggleDelay = 20;
+                setPowered(!isPowered());
+                setIgnited(false);
+                return true;
+            }
+        }
+        if (!isIgnited()) {
+            if (getRider() != null && getRider().getForwardMot() == 0 && getRider().getStrafeMot() == 0 &&
+                    getRider().getBukkitEntity().hasPermission("allow.special.creeper")) {
+                setIgnited(true);
+                return true;
+            }
+        }
+        return getForwardMot() == 0 && getStrafeMot() == 0; // do not jump if standing still
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
         this.goalSelector.addGoal(1, new FloatGoal(this));
         this.goalSelector.addGoal(2, new SwellGoal(this));
+        this.goalSelector.addGoal(3, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, Ocelot.class, 6.0F, 1.0D, 1.2D));
         this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, Cat.class, 6.0F, 1.0D, 1.2D));
         this.goalSelector.addGoal(4, new MeleeAttackGoal(this, 1.0D, false));
         this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 0.8D));
         this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 8.0F));
         this.goalSelector.addGoal(6, new RandomLookAroundGoal(this));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, Player.class, true));
         this.targetSelector.addGoal(2, new HurtByTargetGoal(this, new Class[0]));
     }
@@ -318,6 +386,7 @@ public class Creeper extends Monster implements PowerableMob {
             com.destroystokyo.paper.event.entity.CreeperIgniteEvent event = new com.destroystokyo.paper.event.entity.CreeperIgniteEvent((org.bukkit.entity.Creeper) getBukkitEntity(), ignited);
             if (event.callEvent()) {
                 this.entityData.set(Creeper.DATA_IS_IGNITED, event.isIgnited());
+                if (!event.isIgnited()) setSwellDir(-1); // Purpur
             }
         }
         // Paper end
diff --git a/src/main/java/net/minecraft/world/entity/monster/Drowned.java b/src/main/java/net/minecraft/world/entity/monster/Drowned.java
index 50228e59d629e75e97d23bd3ec92088f75480827..ace909fab2334105eabe0593aba47736e3f67451 100644
--- a/src/main/java/net/minecraft/world/entity/monster/Drowned.java
+++ b/src/main/java/net/minecraft/world/entity/monster/Drowned.java
@@ -69,6 +69,18 @@ public class Drowned extends Zombie implements RangedAttackMob {
         this.groundNavigation = new GroundPathNavigation(this, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.drownedRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.drownedRidableInWater;
+    }
+    // Purpur end
+
     @Override
     protected void addBehaviourGoals() {
         this.goalSelector.addGoal(1, new Drowned.DrownedGoToWaterGoal(this, 1.0D));
@@ -378,7 +390,7 @@ public class Drowned extends Zombie implements RangedAttackMob {
         }
     }
 
-    static class DrownedMoveControl extends MoveControl {
+    static class DrownedMoveControl extends net.pl3x.purpur.controller.MoveControllerWASD { // Purpur
         private final Drowned drowned;
 
         public DrownedMoveControl(Drowned drowned) {
@@ -387,7 +399,7 @@ public class Drowned extends Zombie implements RangedAttackMob {
         }
 
         @Override
-        public void tick() {
+        public void vanillaTick() { // Purpur
             LivingEntity livingEntity = this.drowned.getTarget();
             if (this.drowned.wantsToSwim() && this.drowned.isInWater()) {
                 if (livingEntity != null && livingEntity.getY() > this.drowned.getY() || this.drowned.searchingForLand) {
@@ -407,7 +419,7 @@ public class Drowned extends Zombie implements RangedAttackMob {
                 float h = (float)(Mth.atan2(f, d) * (double)(180F / (float)Math.PI)) - 90.0F;
                 this.drowned.setYRot(this.rotlerp(this.drowned.getYRot(), h, 90.0F));
                 this.drowned.yBodyRot = this.drowned.getYRot();
-                float i = (float)(this.speedModifier * this.drowned.getAttributeValue(Attributes.MOVEMENT_SPEED));
+                float i = (float)(this.getSpeedModifier() * this.drowned.getAttributeValue(Attributes.MOVEMENT_SPEED));
                 float j = Mth.lerp(0.125F, this.drowned.getSpeed(), i);
                 this.drowned.setSpeed(j);
                 this.drowned.setDeltaMovement(this.drowned.getDeltaMovement().add((double)j * d * 0.005D, (double)j * e * 0.1D, (double)j * f * 0.005D));
@@ -416,7 +428,7 @@ public class Drowned extends Zombie implements RangedAttackMob {
                     this.drowned.setDeltaMovement(this.drowned.getDeltaMovement().add(0.0D, -0.008D, 0.0D));
                 }
 
-                super.tick();
+                super.vanillaTick(); // Purpur
             }
 
         }
diff --git a/src/main/java/net/minecraft/world/entity/monster/ElderGuardian.java b/src/main/java/net/minecraft/world/entity/monster/ElderGuardian.java
index ee2febe92309f277f1607c0ea024d6cd291490bc..5f8233c1be6f92bcf58c5c5db360b2660c9439ad 100644
--- a/src/main/java/net/minecraft/world/entity/monster/ElderGuardian.java
+++ b/src/main/java/net/minecraft/world/entity/monster/ElderGuardian.java
@@ -30,6 +30,18 @@ public class ElderGuardian extends Guardian {
 
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.elderGuardianRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return true;
+    }
+    // Purpur end
+
     public static AttributeSupplier.Builder createAttributes() {
         return Guardian.createAttributes().add(Attributes.MOVEMENT_SPEED, 0.30000001192092896D).add(Attributes.ATTACK_DAMAGE, 8.0D).add(Attributes.MAX_HEALTH, 80.0D);
     }
diff --git a/src/main/java/net/minecraft/world/entity/monster/EnderMan.java b/src/main/java/net/minecraft/world/entity/monster/EnderMan.java
index 5d371a3e94720e24058d007474355af6aeb7cbdd..f505cc4fc7945b2b462bc24ee3ecc76995072e14 100644
--- a/src/main/java/net/minecraft/world/entity/monster/EnderMan.java
+++ b/src/main/java/net/minecraft/world/entity/monster/EnderMan.java
@@ -83,9 +83,22 @@ public class EnderMan extends Monster implements NeutralMob {
         this.setPathfindingMalus(BlockPathTypes.WATER, -1.0F);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.endermanRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.endermanRidableInWater;
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
         this.goalSelector.addGoal(0, new FloatGoal(this));
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(1, new EnderMan.EndermanFreezeWhenLookedAt(this));
         this.goalSelector.addGoal(2, new MeleeAttackGoal(this, 1.0D, false));
         this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0D, 0.0F));
@@ -93,6 +106,7 @@ public class EnderMan extends Monster implements NeutralMob {
         this.goalSelector.addGoal(8, new RandomLookAroundGoal(this));
         this.goalSelector.addGoal(10, new EnderMan.EndermanLeaveBlockGoal(this));
         this.goalSelector.addGoal(11, new EnderMan.EndermanTakeBlockGoal(this));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(1, new EnderMan.EndermanLookForPlayerGoal(this, this::isAngryAt));
         this.targetSelector.addGoal(2, new HurtByTargetGoal(this, new Class[0]));
         this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Endermite.class, true, false));
@@ -273,7 +287,7 @@ public class EnderMan extends Monster implements NeutralMob {
 
     @Override
     protected void customServerAiStep() {
-        if (this.level.isDay() && this.tickCount >= this.targetChangeTime + 600) {
+        if (getRider() == null && this.level.isDay() && this.tickCount >= this.targetChangeTime + 600) { // Purpur - no random teleporting
             float f = this.getBrightness();
 
             if (f > 0.5F && this.level.canSeeSky(this.blockPosition()) && this.random.nextFloat() * 30.0F < (f - 0.4F) * 2.0F && this.tryEscape(com.destroystokyo.paper.event.entity.EndermanEscapeEvent.Reason.RUNAWAY)) { // Paper
@@ -379,6 +393,7 @@ public class EnderMan extends Monster implements NeutralMob {
     public boolean hurt(DamageSource source, float amount) {
         if (this.isInvulnerableTo(source)) {
             return false;
+        } else if (getRider() != null) { return super.hurt(source, amount); // Purpur - no teleporting on damage
         } else if (source instanceof IndirectEntityDamageSource) {
             if (this.tryEscape(com.destroystokyo.paper.event.entity.EndermanEscapeEvent.Reason.INDIRECT)) { // Paper start
             for (int i = 0; i < 64; ++i) {
diff --git a/src/main/java/net/minecraft/world/entity/monster/Endermite.java b/src/main/java/net/minecraft/world/entity/monster/Endermite.java
index 4aecf69a335c2c6b6457f3b15a1420400878b6e6..1f315cc57f6365a782ca4e645b0dcfbe5485138a 100644
--- a/src/main/java/net/minecraft/world/entity/monster/Endermite.java
+++ b/src/main/java/net/minecraft/world/entity/monster/Endermite.java
@@ -36,13 +36,27 @@ public class Endermite extends Monster {
         this.xpReward = 3;
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.endermiteRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.endermiteRidableInWater;
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
         this.goalSelector.addGoal(1, new FloatGoal(this));
+        this.goalSelector.addGoal(1, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(2, new MeleeAttackGoal(this, 1.0D, false));
         this.goalSelector.addGoal(3, new WaterAvoidingRandomStrollGoal(this, 1.0D));
         this.goalSelector.addGoal(7, new LookAtPlayerGoal(this, Player.class, 8.0F));
         this.goalSelector.addGoal(8, new RandomLookAroundGoal(this));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(1, (new HurtByTargetGoal(this)).setAlertOthers());
         this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true));
     }
diff --git a/src/main/java/net/minecraft/world/entity/monster/Evoker.java b/src/main/java/net/minecraft/world/entity/monster/Evoker.java
index 4b812fc118126040d773f87ab50047cbbda79d1c..053374baf3ec7730bcddf072ddf98e3176656435 100644
--- a/src/main/java/net/minecraft/world/entity/monster/Evoker.java
+++ b/src/main/java/net/minecraft/world/entity/monster/Evoker.java
@@ -46,10 +46,23 @@ public class Evoker extends SpellcasterIllager {
         this.xpReward = 10;
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.evokerRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.evokerRidableInWater;
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
         super.registerGoals();
         this.goalSelector.addGoal(0, new FloatGoal(this));
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(1, new Evoker.EvokerCastingSpellGoal());
         this.goalSelector.addGoal(2, new AvoidEntityGoal<>(this, Player.class, 8.0F, 0.6D, 1.0D));
         this.goalSelector.addGoal(4, new Evoker.EvokerSummonSpellGoal());
@@ -58,6 +71,7 @@ public class Evoker extends SpellcasterIllager {
         this.goalSelector.addGoal(8, new RandomStrollGoal(this, 0.6D));
         this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 3.0F, 1.0F));
         this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Mob.class, 8.0F));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, Raider.class)).setAlertOthers());
         this.targetSelector.addGoal(2, (new NearestAttackableTargetGoal<>(this, Player.class, true)).setUnseenMemoryTicks(300));
         this.targetSelector.addGoal(3, (new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false)).setUnseenMemoryTicks(300));
diff --git a/src/main/java/net/minecraft/world/entity/monster/Ghast.java b/src/main/java/net/minecraft/world/entity/monster/Ghast.java
index 886b6eac6c6ae4d97b1b25624504c0f48e20efc4..bb1a6f6847d0e459eb26a029d8a1f5646168a422 100644
--- a/src/main/java/net/minecraft/world/entity/monster/Ghast.java
+++ b/src/main/java/net/minecraft/world/entity/monster/Ghast.java
@@ -44,11 +44,42 @@ public class Ghast extends FlyingMob implements Enemy {
         this.moveControl = new Ghast.GhastMoveControl(this);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.ghastRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.ghastRidableInWater;
+    }
+
+    @Override
+    public double getMaxY() {
+        return level.purpurConfig.ghastMaxY;
+    }
+
+    @Override
+    public void travel(Vec3 vec3) {
+        super.travel(vec3);
+        if (getRider() != null && !onGround) {
+            float speed = (float) getAttributeValue(Attributes.FLYING_SPEED);
+            setSpeed(speed);
+            Vec3 mot = getDeltaMovement();
+            move(net.minecraft.world.entity.MoverType.SELF, mot.multiply(speed, 1.0, speed));
+            setDeltaMovement(mot.scale(0.9D));
+        }
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(5, new Ghast.RandomFloatAroundGoal(this));
         this.goalSelector.addGoal(7, new Ghast.GhastLookGoal(this));
         this.goalSelector.addGoal(7, new Ghast.GhastShootFireballGoal(this));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, (entityliving) -> {
             return Math.abs(entityliving.getY() - this.getY()) <= 4.0D;
         }));
@@ -90,7 +121,7 @@ public class Ghast extends FlyingMob implements Enemy {
     }
 
     public static AttributeSupplier.Builder createAttributes() {
-        return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 10.0D).add(Attributes.FOLLOW_RANGE, 100.0D);
+        return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 10.0D).add(Attributes.FOLLOW_RANGE, 100.0D).add(Attributes.FLYING_SPEED, 0.6D); // Purpur
     }
 
     @Override
@@ -147,7 +178,7 @@ public class Ghast extends FlyingMob implements Enemy {
         return 2.6F;
     }
 
-    private static class GhastMoveControl extends MoveControl {
+    private static class GhastMoveControl extends net.pl3x.purpur.controller.FlyingMoveControllerWASD { // Purpur
 
         private final Ghast ghast;
         private int floatDuration;
@@ -158,7 +189,7 @@ public class Ghast extends FlyingMob implements Enemy {
         }
 
         @Override
-        public void tick() {
+        public void vanillaTick() { // Purpur
             if (this.operation == MoveControl.Operation.MOVE_TO) {
                 if (this.floatDuration-- <= 0) {
                     this.floatDuration += this.ghast.getRandom().nextInt(5) + 2;
diff --git a/src/main/java/net/minecraft/world/entity/monster/Giant.java b/src/main/java/net/minecraft/world/entity/monster/Giant.java
index 0d578ab12c874bd2daccc4322a3fe1abafa4bc18..d95b110a025d48e7ab18f172e455593740e45252 100644
--- a/src/main/java/net/minecraft/world/entity/monster/Giant.java
+++ b/src/main/java/net/minecraft/world/entity/monster/Giant.java
@@ -14,6 +14,24 @@ public class Giant extends Monster {
         super(type, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.giantRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.giantRidableInWater;
+    }
+
+    @Override
+    protected void registerGoals() {
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this));
+    }
+    // Purpur end
+
     @Override
     protected float getStandingEyeHeight(Pose pose, EntityDimensions dimensions) {
         return 10.440001F;
diff --git a/src/main/java/net/minecraft/world/entity/monster/Guardian.java b/src/main/java/net/minecraft/world/entity/monster/Guardian.java
index c5441f2784463b35fefbb567bf4eca579b3769bb..20b7104b7fd38b3c6c05a68a1472c6006814cb81 100644
--- a/src/main/java/net/minecraft/world/entity/monster/Guardian.java
+++ b/src/main/java/net/minecraft/world/entity/monster/Guardian.java
@@ -65,15 +65,36 @@ public class Guardian extends Monster {
         this.xpReward = 10;
         this.setPathfindingMalus(BlockPathTypes.WATER, 0.0F);
         this.moveControl = new Guardian.GuardianMoveControl(this);
+        // Purpur start
+        this.lookControl = new net.pl3x.purpur.controller.LookControllerWASD(this) {
+            @Override
+            public void setYawPitch(float yaw, float pitch) {
+                super.setYawPitch(yaw, pitch * 0.35F);
+            }
+        };
+        // Purpur end
         this.clientSideTailAnimation = this.random.nextFloat();
         this.clientSideTailAnimationO = this.clientSideTailAnimation;
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.guardianRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return true;
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
         MoveTowardsRestrictionGoal pathfindergoalmovetowardsrestriction = new MoveTowardsRestrictionGoal(this, 1.0D);
 
         this.randomStrollGoal = new RandomStrollGoal(this, 1.0D, 80);
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(4, new Guardian.GuardianAttackGoal(this));
         this.goalSelector.addGoal(5, pathfindergoalmovetowardsrestriction);
         this.goalSelector.addGoal(7, this.randomStrollGoal);
@@ -82,6 +103,7 @@ public class Guardian extends Monster {
         this.goalSelector.addGoal(9, new RandomLookAroundGoal(this));
         this.randomStrollGoal.setFlags(EnumSet.of(Goal.Flag.MOVE, Goal.Flag.LOOK));
         pathfindergoalmovetowardsrestriction.setFlags(EnumSet.of(Goal.Flag.MOVE, Goal.Flag.LOOK));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, LivingEntity.class, 10, true, false, new Guardian.GuardianAttackSelector(this)));
     }
 
@@ -337,7 +359,7 @@ public class Guardian extends Monster {
     @Override
     public void travel(Vec3 movementInput) {
         if (this.isEffectiveAi() && this.isInWater()) {
-            this.moveRelative(0.1F, movementInput);
+            this.moveRelative(getRider() != null ? getSpeed() : 0.1F, movementInput); // Purpur
             this.move(MoverType.SELF, this.getDeltaMovement());
             this.setDeltaMovement(this.getDeltaMovement().scale(0.9D));
             if (!this.isMoving() && this.getTarget() == null) {
@@ -349,8 +371,7 @@ public class Guardian extends Monster {
 
     }
 
-    private static class GuardianMoveControl extends MoveControl {
-
+    private static class GuardianMoveControl extends net.pl3x.purpur.controller.WaterMoveControllerWASD { // Purpur
         private final Guardian guardian;
 
         public GuardianMoveControl(Guardian guardian) {
@@ -358,8 +379,17 @@ public class Guardian extends Monster {
             this.guardian = guardian;
         }
 
+        // Purpur start
         @Override
-        public void tick() {
+        public void purpurTick(Player rider) {
+            super.purpurTick(rider);
+            guardian.setDeltaMovement(guardian.getDeltaMovement().add(0.0D, 0.005D, 0.0D));
+            guardian.setMoving(guardian.getForwardMot() > 0.0F); // control tail speed
+        }
+        // Purpur end
+
+        @Override
+        public void vanillaTick() { // Purpur
             if (this.operation == MoveControl.Operation.MOVE_TO && !this.guardian.getNavigation().isDone()) {
                 Vec3 vec3d = new Vec3(this.wantedX - this.guardian.getX(), this.wantedY - this.guardian.getY(), this.wantedZ - this.guardian.getZ());
                 double d0 = vec3d.length();
@@ -370,7 +400,7 @@ public class Guardian extends Monster {
 
                 this.guardian.setYRot(this.rotlerp(this.guardian.getYRot(), f, 90.0F));
                 this.guardian.yBodyRot = this.guardian.getYRot();
-                float f1 = (float) (this.speedModifier * this.guardian.getAttributeValue(Attributes.MOVEMENT_SPEED));
+                float f1 = (float) (this.getSpeedModifier() * this.guardian.getAttributeValue(Attributes.MOVEMENT_SPEED)); // Purpur
                 float f2 = Mth.lerp(0.125F, this.guardian.getSpeed(), f1);
 
                 this.guardian.setSpeed(f2);
diff --git a/src/main/java/net/minecraft/world/entity/monster/Husk.java b/src/main/java/net/minecraft/world/entity/monster/Husk.java
index 28fb67c0a5992cbd77c5d3c6efa0f0493466d81c..75397a8e1ae8d48bf07f5c0409536acd54851c48 100644
--- a/src/main/java/net/minecraft/world/entity/monster/Husk.java
+++ b/src/main/java/net/minecraft/world/entity/monster/Husk.java
@@ -22,6 +22,18 @@ public class Husk extends Zombie {
         super(type, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.huskRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.huskRidableInWater;
+    }
+    // Purpur end
+
     public static boolean checkHuskSpawnRules(EntityType<Husk> type, ServerLevelAccessor world, MobSpawnType spawnReason, BlockPos pos, Random random) {
         return checkMonsterSpawnRules(type, world, spawnReason, pos, random) && (spawnReason == MobSpawnType.SPAWNER || world.canSeeSky(pos));
     }
diff --git a/src/main/java/net/minecraft/world/entity/monster/Illusioner.java b/src/main/java/net/minecraft/world/entity/monster/Illusioner.java
index c9fa01b910de7ecb494d3000afebea9a2bd1276a..5f57c14a7ba03af9432a839e6caed47286638b2c 100644
--- a/src/main/java/net/minecraft/world/entity/monster/Illusioner.java
+++ b/src/main/java/net/minecraft/world/entity/monster/Illusioner.java
@@ -59,10 +59,23 @@ public class Illusioner extends SpellcasterIllager implements RangedAttackMob {
 
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.illusionerRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.illusionerRidableInWater;
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
         super.registerGoals();
         this.goalSelector.addGoal(0, new FloatGoal(this));
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(1, new SpellcasterIllager.SpellcasterCastingSpellGoal());
         this.goalSelector.addGoal(4, new Illusioner.IllusionerMirrorSpellGoal());
         this.goalSelector.addGoal(5, new Illusioner.IllusionerBlindnessSpellGoal());
@@ -70,6 +83,7 @@ public class Illusioner extends SpellcasterIllager implements RangedAttackMob {
         this.goalSelector.addGoal(8, new RandomStrollGoal(this, 0.6D));
         this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 3.0F, 1.0F));
         this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Mob.class, 8.0F));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[]{Raider.class})).setAlertOthers(new Class[0])); // CraftBukkit - decompile error
         this.targetSelector.addGoal(2, (new NearestAttackableTargetGoal<>(this, Player.class, true)).setUnseenMemoryTicks(300));
         this.targetSelector.addGoal(3, (new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false)).setUnseenMemoryTicks(300));
diff --git a/src/main/java/net/minecraft/world/entity/monster/MagmaCube.java b/src/main/java/net/minecraft/world/entity/monster/MagmaCube.java
index 130205f0d101debaa74c1172fb80863e6fa0ebe1..7d39f25734f2c52b11931e141524acd2c244c2c5 100644
--- a/src/main/java/net/minecraft/world/entity/monster/MagmaCube.java
+++ b/src/main/java/net/minecraft/world/entity/monster/MagmaCube.java
@@ -27,6 +27,23 @@ public class MagmaCube extends Slime {
         super(type, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.magmaCubeRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.magmaCubeRidableInWater;
+    }
+
+    @Override
+    public float getJumpPower() {
+        return 0.42F * this.getBlockJumpFactor(); // from EntityLiving
+    }
+    // Purpur end
+
     public static AttributeSupplier.Builder createAttributes() {
         return Monster.createMonsterAttributes().add(Attributes.MOVEMENT_SPEED, (double)0.2F);
     }
@@ -57,7 +74,7 @@ public class MagmaCube extends Slime {
     }
 
     @Override
-    protected ResourceLocation getDefaultLootTable() {
+    public ResourceLocation getDefaultLootTable() { // Purpur - decompile fix
         return this.isTiny() ? BuiltInLootTables.EMPTY : this.getType().getDefaultLootTable();
     }
 
@@ -77,10 +94,11 @@ public class MagmaCube extends Slime {
     }
 
     @Override
-    protected void jumpFromGround() {
+    public void jumpFromGround() { // Purpur - protected -> public
         Vec3 vec3 = this.getDeltaMovement();
         this.setDeltaMovement(vec3.x, (double)(this.getJumpPower() + (float)this.getSize() * 0.1F), vec3.z);
         this.hasImpulse = true;
+        this.actualJump = false; // Purpur
     }
 
     @Override
diff --git a/src/main/java/net/minecraft/world/entity/monster/Phantom.java b/src/main/java/net/minecraft/world/entity/monster/Phantom.java
index 39291a0cea465613733a905141b584f05e597b4c..61aff98a7b56eab6a43ddc9f07618cbbedb6b77d 100644
--- a/src/main/java/net/minecraft/world/entity/monster/Phantom.java
+++ b/src/main/java/net/minecraft/world/entity/monster/Phantom.java
@@ -61,6 +61,59 @@ public class Phantom extends FlyingMob implements Enemy {
         this.lookControl = new Phantom.PhantomLookControl(this);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.phantomRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.phantomRidableInWater;
+    }
+
+    @Override
+    public double getMaxY() {
+        return level.purpurConfig.phantomMaxY;
+    }
+
+    @Override
+    public void travel(Vec3 vec3) {
+        super.travel(vec3);
+        if (getRider() != null && !onGround) {
+            float speed = (float) getAttributeValue(Attributes.FLYING_SPEED);
+            setSpeed(speed);
+            Vec3 mot = getDeltaMovement();
+            move(net.minecraft.world.entity.MoverType.SELF, mot.multiply(speed, speed, speed));
+            setDeltaMovement(mot.scale(0.9D));
+        }
+    }
+
+    public static net.minecraft.world.entity.ai.attributes.AttributeSupplier.Builder createAttributes() {
+        return Monster.createMonsterAttributes().add(Attributes.FLYING_SPEED, 3.0D);
+    }
+
+    @Override
+    public boolean onSpacebar() {
+        if (getRider() != null && getRider().getBukkitEntity().hasPermission("allow.special.phantom")) {
+            shoot();
+        }
+        return false;
+    }
+
+    public boolean shoot() {
+        org.bukkit.Location loc = ((org.bukkit.entity.LivingEntity) getBukkitEntity()).getEyeLocation();
+        loc.setPitch(-loc.getPitch());
+        org.bukkit.util.Vector target = loc.getDirection().normalize().multiply(100).add(loc.toVector());
+
+        net.pl3x.purpur.entity.PhantomFlames flames = new net.pl3x.purpur.entity.PhantomFlames(level, this);
+        flames.canGrief = level.purpurConfig.phantomAllowGriefing;
+        flames.shoot(target.getX() - getX(), target.getY() - getY(), target.getZ() - getZ(), 1.0F, 5.0F);
+        level.addFreshEntity(flames);
+        return true;
+    }
+    // Purpur end
+
     @Override
     public boolean isFlapping() {
         return (this.getUniqueFlapTickOffset() + this.tickCount) % Phantom.TICKS_PER_FLAP == 0;
@@ -73,9 +126,11 @@ public class Phantom extends FlyingMob implements Enemy {
 
     @Override
     protected void registerGoals() {
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(1, new Phantom.PhantomAttackStrategyGoal());
         this.goalSelector.addGoal(2, new Phantom.PhantomSweepAttackGoal());
         this.goalSelector.addGoal(3, new Phantom.PhantomCircleAroundAnchorGoal());
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(1, new Phantom.PhantomAttackPlayerTargetGoal());
     }
 
@@ -145,7 +200,7 @@ public class Phantom extends FlyingMob implements Enemy {
 
     @Override
     public void aiStep() {
-        if (this.isAlive() && shouldBurnInDay && this.isSunBurnTick()) { // Paper - Configurable Burning
+        if (this.isAlive() && getRider() == null && shouldBurnInDay && this.isSunBurnTick()) { // Paper - Configurable Burning // Purpur
             this.setSecondsOnFire(8);
         }
 
@@ -265,7 +320,7 @@ public class Phantom extends FlyingMob implements Enemy {
         private AttackPhase() {}
     }
 
-    private class PhantomMoveControl extends MoveControl {
+    private class PhantomMoveControl extends net.pl3x.purpur.controller.FlyingMoveControllerWASD { // Purpur
 
         private float speed = 0.1F;
 
@@ -273,8 +328,19 @@ public class Phantom extends FlyingMob implements Enemy {
             super(entity);
         }
 
+        // Purpur start
+        public void purpurTick(Player rider) {
+            if (!Phantom.this.onGround) {
+                // phantom is always in motion when flying
+                // TODO - FIX THIS
+                // rider.setForward(1.0F);
+            }
+            super.purpurTick(rider);
+        }
+        // Purpur end
+
         @Override
-        public void tick() {
+        public void vanillaTick() { // Purpur
             if (Phantom.this.horizontalCollision) {
                 Phantom.this.setYRot(Phantom.this.getYRot() + 180.0F);
                 this.speed = 0.1F;
@@ -320,14 +386,20 @@ public class Phantom extends FlyingMob implements Enemy {
         }
     }
 
-    private class PhantomLookControl extends LookControl {
+    private class PhantomLookControl extends net.pl3x.purpur.controller.LookControllerWASD { // Purpur
 
         public PhantomLookControl(Mob entity) {
             super(entity);
         }
 
+        // Purpur start
+        public void purpurTick(Player rider) {
+            setYawPitch(rider.yRot, -rider.xRotO * 0.75F);
+        }
+        // Purpur end
+
         @Override
-        public void tick() {}
+        public void vanillaTick() {} // Purpur
     }
 
     private class PhantomBodyRotationControl extends BodyRotationControl {
diff --git a/src/main/java/net/minecraft/world/entity/monster/Pillager.java b/src/main/java/net/minecraft/world/entity/monster/Pillager.java
index c0de613024de7b9b55f96be37e4648e83dea9b8f..84bd5e7df663f315fd099742195e85508ea25adf 100644
--- a/src/main/java/net/minecraft/world/entity/monster/Pillager.java
+++ b/src/main/java/net/minecraft/world/entity/monster/Pillager.java
@@ -66,15 +66,29 @@ public class Pillager extends AbstractIllager implements CrossbowAttackMob, Inve
         super(type, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.pillagerRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.pillagerRidableInWater;
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
         super.registerGoals();
         this.goalSelector.addGoal(0, new FloatGoal(this));
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(2, new Raider.HoldGroundAttackGoal(this, 10.0F));
         this.goalSelector.addGoal(3, new RangedCrossbowAttackGoal<>(this, 1.0D, 8.0F));
         this.goalSelector.addGoal(8, new RandomStrollGoal(this, 0.6D));
         this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 15.0F, 1.0F));
         this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Mob.class, 15.0F));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[]{Raider.class})).setAlertOthers(new Class[0])); // CraftBukkit - decompile error
         this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true));
         this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false));
diff --git a/src/main/java/net/minecraft/world/entity/monster/Ravager.java b/src/main/java/net/minecraft/world/entity/monster/Ravager.java
index 4f51de49757a912ec84ccf5dab087c9a3e11a60e..32ef769b2b3e3ab42c7a50bf4c8ec7ec548d5e7b 100644
--- a/src/main/java/net/minecraft/world/entity/monster/Ravager.java
+++ b/src/main/java/net/minecraft/world/entity/monster/Ravager.java
@@ -70,14 +70,34 @@ public class Ravager extends Raider {
         this.xpReward = 20;
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.ravagerRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.ravagerRidableInWater;
+    }
+
+    @Override
+    public void onMount(Player rider) {
+        super.onMount(rider);
+        getNavigation().stop();
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
         super.registerGoals();
         this.goalSelector.addGoal(0, new FloatGoal(this));
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(4, new Ravager.RavagerMeleeAttackGoal());
         this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 0.4D));
         this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 6.0F));
         this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Mob.class, 8.0F));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(2, (new HurtByTargetGoal(this, new Class[]{Raider.class})).setAlertOthers(new Class[0])); // CraftBukkit - decompile error
         this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Player.class, true));
         this.targetSelector.addGoal(4, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, true));
@@ -149,7 +169,7 @@ public class Ravager extends Raider {
     @Override
     public void aiStep() {
         super.aiStep();
-        if (this.isAlive()) {
+        if (this.isAlive() && getRider() == null) { // Purpur
             if (this.isImmobile()) {
                 this.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(0.0D);
             } else {
diff --git a/src/main/java/net/minecraft/world/entity/monster/Shulker.java b/src/main/java/net/minecraft/world/entity/monster/Shulker.java
index ca0d1c059a6ad94590bcbff34b37b9c13ef19474..42635be6183d86978df3f174c74a71691f2bfcdd 100644
--- a/src/main/java/net/minecraft/world/entity/monster/Shulker.java
+++ b/src/main/java/net/minecraft/world/entity/monster/Shulker.java
@@ -92,12 +92,26 @@ public class Shulker extends AbstractGolem implements Enemy {
         this.lookControl = new Shulker.ShulkerLookControl(this);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.shulkerRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.shulkerRidableInWater;
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(1, new LookAtPlayerGoal(this, Player.class, 8.0F, 0.02F, true));
         this.goalSelector.addGoal(4, new Shulker.ShulkerAttackGoal());
         this.goalSelector.addGoal(7, new Shulker.ShulkerPeekGoal());
         this.goalSelector.addGoal(8, new RandomLookAroundGoal(this));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[]{this.getClass()})).setAlertOthers(new Class[0])); // CraftBukkit - decompile error
         this.targetSelector.addGoal(2, new Shulker.ShulkerNearestAttackGoal(this));
         this.targetSelector.addGoal(3, new Shulker.ShulkerDefenseAttackGoal(this));
@@ -597,7 +611,7 @@ public class Shulker extends AbstractGolem implements Enemy {
         return b0 != 16 && b0 <= 15 ? DyeColor.byId(b0) : null;
     }
 
-    private class ShulkerLookControl extends LookControl {
+    private class ShulkerLookControl extends net.pl3x.purpur.controller.LookControllerWASD { // Purpur
 
         public ShulkerLookControl(Mob entity) {
             super(entity);
diff --git a/src/main/java/net/minecraft/world/entity/monster/Silverfish.java b/src/main/java/net/minecraft/world/entity/monster/Silverfish.java
index 2459ae800a5f6b234a4f4bb1cd3738e4e9cac67d..e66cc79dc61721b31ffb743f68f4388cc499a92d 100644
--- a/src/main/java/net/minecraft/world/entity/monster/Silverfish.java
+++ b/src/main/java/net/minecraft/world/entity/monster/Silverfish.java
@@ -39,13 +39,27 @@ public class Silverfish extends Monster {
         super(type, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.silverfishRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.silverfishRidableInWater;
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
         this.friendsGoal = new Silverfish.SilverfishWakeUpFriendsGoal(this);
         this.goalSelector.addGoal(1, new FloatGoal(this));
+        this.goalSelector.addGoal(1, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(3, this.friendsGoal);
         this.goalSelector.addGoal(4, new MeleeAttackGoal(this, 1.0D, false));
         this.goalSelector.addGoal(5, new Silverfish.SilverfishMergeWithStoneGoal(this));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[0])).setAlertOthers(new Class[0])); // CraftBukkit - decompile error
         this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true));
     }
diff --git a/src/main/java/net/minecraft/world/entity/monster/Skeleton.java b/src/main/java/net/minecraft/world/entity/monster/Skeleton.java
index ccf706acafc20e7ba5408d1648b873d6937a030c..059a62da29b7ec11e2ff9baf18dde788bec4923c 100644
--- a/src/main/java/net/minecraft/world/entity/monster/Skeleton.java
+++ b/src/main/java/net/minecraft/world/entity/monster/Skeleton.java
@@ -25,6 +25,18 @@ public class Skeleton extends AbstractSkeleton {
         super(type, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.skeletonRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.skeletonRidableInWater;
+    }
+    // Purpur end
+
     @Override
     protected void defineSynchedData() {
         super.defineSynchedData();
diff --git a/src/main/java/net/minecraft/world/entity/monster/Slime.java b/src/main/java/net/minecraft/world/entity/monster/Slime.java
index e1a593b464c35f68b22e84a09f99ee72af73da32..25ce1910a03947ce070b318f57379f0da5ac5db8 100644
--- a/src/main/java/net/minecraft/world/entity/monster/Slime.java
+++ b/src/main/java/net/minecraft/world/entity/monster/Slime.java
@@ -67,18 +67,50 @@ public class Slime extends Mob implements Enemy {
     public float squish;
     public float oSquish;
     private boolean wasOnGround;
+    protected boolean actualJump; // Purpur
 
     public Slime(EntityType<? extends Slime> type, Level world) {
         super(type, world);
         this.moveControl = new Slime.SlimeMoveControl(this);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.slimeRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.slimeRidableInWater;
+    }
+
+    @Override
+    public float getJumpPower() {
+        float height = super.getJumpPower();
+        return getRider() != null && actualJump ? height * 1.5F : height;
+    }
+
+    @Override
+    public boolean onSpacebar() {
+        if (onGround && getRider() != null) {
+            actualJump = true;
+            if (getRider().getForwardMot() == 0 || getRider().getStrafeMot() == 0) {
+                jumpFromGround(); // jump() here if not moving
+            }
+        }
+        return true; // do not jump() in wasd controller, let vanilla controller handle
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(1, new Slime.SlimeFloatGoal(this));
         this.goalSelector.addGoal(2, new Slime.SlimeAttackGoal(this));
         this.goalSelector.addGoal(3, new Slime.SlimeRandomDirectionGoal(this));
         this.goalSelector.addGoal(5, new Slime.SlimeKeepOnJumpingGoal(this));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, Player.class, 10, true, false, (entityliving) -> {
             return Math.abs(entityliving.getY() - this.getY()) <= 4.0D;
         }));
@@ -360,11 +392,12 @@ public class Slime extends Mob implements Enemy {
     }
 
     @Override
-    protected void jumpFromGround() {
+    public void jumpFromGround() { // Purpur - protected -> public
         Vec3 vec3d = this.getDeltaMovement();
 
         this.setDeltaMovement(vec3d.x, (double) this.getJumpPower(), vec3d.z);
         this.hasImpulse = true;
+        this.actualJump = false; // Purpur
     }
 
     @Nullable
@@ -397,7 +430,7 @@ public class Slime extends Mob implements Enemy {
         return super.getDimensions(pose).scale(0.255F * (float) this.getSize());
     }
 
-    private static class SlimeMoveControl extends MoveControl {
+    private static class SlimeMoveControl extends net.pl3x.purpur.controller.MoveControllerWASD { // Purpur
 
         private float yRot;
         private int jumpDelay;
@@ -416,21 +449,33 @@ public class Slime extends Mob implements Enemy {
         }
 
         public void setWantedMovement(double speed) {
-            this.speedModifier = speed;
+            this.setSpeedModifier(speed); // Purpur
             this.operation = MoveControl.Operation.MOVE_TO;
         }
 
         @Override
         public void tick() {
+            // Purpur start
+            if (slime.getRider() != null) {
+                purpurTick(slime.getRider());
+                if (slime.getForwardMot() != 0 || slime.getStrafeMot() != 0) {
+                    if (jumpDelay > 10) {
+                        jumpDelay = 6;
+                    }
+                } else {
+                    jumpDelay = 20;
+                }
+            } else {
+            // Purpur end
             this.mob.setYRot(this.rotlerp(this.mob.getYRot(), this.yRot, 90.0F));
             this.mob.yHeadRot = this.mob.getYRot();
             this.mob.yBodyRot = this.mob.getYRot();
-            if (this.operation != MoveControl.Operation.MOVE_TO) {
+            } if (entity.getRider() == null && this.operation != MoveControl.Operation.MOVE_TO) { // Purpur
                 this.mob.setZza(0.0F);
             } else {
                 this.operation = MoveControl.Operation.WAIT;
                 if (this.mob.isOnGround()) {
-                    this.mob.setSpeed((float) (this.speedModifier * this.mob.getAttributeValue(Attributes.MOVEMENT_SPEED)));
+                    this.mob.setSpeed((float) (this.getSpeedModifier() * this.mob.getAttributeValue(Attributes.MOVEMENT_SPEED) * (entity.getRider() != null && (entity.getRider().getForwardMot() != 0 || entity.getRider().getStrafeMot() != 0) ? 2.0D : 1.0D))); // Purpur
                     if (this.jumpDelay-- <= 0) {
                         this.jumpDelay = this.slime.getJumpDelay();
                         if (this.isAggressive) {
@@ -447,7 +492,7 @@ public class Slime extends Mob implements Enemy {
                         this.mob.setSpeed(0.0F);
                     }
                 } else {
-                    this.mob.setSpeed((float) (this.speedModifier * this.mob.getAttributeValue(Attributes.MOVEMENT_SPEED)));
+                    this.mob.setSpeed((float) (this.getSpeedModifier() * this.mob.getAttributeValue(Attributes.MOVEMENT_SPEED) * (entity.getRider() != null && (entity.getRider().getForwardMot() != 0 || entity.getRider().getStrafeMot() != 0) ? 2.0D : 1.0D))); // Purpur
                 }
 
             }
diff --git a/src/main/java/net/minecraft/world/entity/monster/Spider.java b/src/main/java/net/minecraft/world/entity/monster/Spider.java
index a8162116eb888106e6f48ee836d0cc5d33b72399..5874d86ca34200e9ce7d41cf7a80eb0f1f13410c 100644
--- a/src/main/java/net/minecraft/world/entity/monster/Spider.java
+++ b/src/main/java/net/minecraft/world/entity/monster/Spider.java
@@ -51,14 +51,28 @@ public class Spider extends Monster {
         super(type, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.spiderRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.spiderRidableInWater;
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
         this.goalSelector.addGoal(1, new FloatGoal(this));
+        this.goalSelector.addGoal(1, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(3, new LeapAtTargetGoal(this, 0.4F));
         this.goalSelector.addGoal(4, new Spider.SpiderAttackGoal(this));
         this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 0.8D));
         this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 8.0F));
         this.goalSelector.addGoal(6, new RandomLookAroundGoal(this));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(1, new HurtByTargetGoal(this, new Class[0]));
         this.targetSelector.addGoal(2, new Spider.SpiderTargetGoal<>(this, Player.class));
         this.targetSelector.addGoal(3, new Spider.SpiderTargetGoal<>(this, IronGolem.class));
diff --git a/src/main/java/net/minecraft/world/entity/monster/Stray.java b/src/main/java/net/minecraft/world/entity/monster/Stray.java
index 0cfcd9c69213d75b52dc93392da727208c13150d..6a8a0cd09e0bf17c7ecb6e55342b645f111dac22 100644
--- a/src/main/java/net/minecraft/world/entity/monster/Stray.java
+++ b/src/main/java/net/minecraft/world/entity/monster/Stray.java
@@ -21,6 +21,18 @@ public class Stray extends AbstractSkeleton {
         super(type, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.strayRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.strayRidableInWater;
+    }
+    // Purpur end
+
     public static boolean checkStraySpawnRules(EntityType<Stray> type, ServerLevelAccessor world, MobSpawnType spawnReason, BlockPos pos, Random random) {
         BlockPos blockPos = pos;
 
diff --git a/src/main/java/net/minecraft/world/entity/monster/Strider.java b/src/main/java/net/minecraft/world/entity/monster/Strider.java
index fc772387713d4313ec8f90a8abcaf7b703fd9f21..6c0af80f47c53e4573efb0c50412d289c0bb5540 100644
--- a/src/main/java/net/minecraft/world/entity/monster/Strider.java
+++ b/src/main/java/net/minecraft/world/entity/monster/Strider.java
@@ -97,6 +97,18 @@ public class Strider extends Animal implements ItemSteerable, Saddleable {
         this.setPathfindingMalus(BlockPathTypes.DAMAGE_FIRE, 0.0F);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.striderRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.striderRidableInWater;
+    }
+    // Purpur end
+
     public static boolean checkStriderSpawnRules(EntityType<Strider> type, LevelAccessor world, MobSpawnType spawnReason, BlockPos pos, Random random) {
         BlockPos.MutableBlockPos blockposition_mutableblockposition = pos.mutable();
 
@@ -158,6 +170,7 @@ public class Strider extends Animal implements ItemSteerable, Saddleable {
     @Override
     protected void registerGoals() {
         this.panicGoal = new PanicGoal(this, 1.65D);
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(1, this.panicGoal);
         this.goalSelector.addGoal(2, new BreedGoal(this, 1.0D));
         this.temptGoal = new TemptGoal(this, 1.4D, Strider.TEMPT_ITEMS, false);
@@ -438,7 +451,7 @@ public class Strider extends Animal implements ItemSteerable, Saddleable {
             if (!enuminteractionresult.consumesAction()) {
                 ItemStack itemstack = player.getItemInHand(hand);
 
-                return itemstack.is(Items.SADDLE) ? itemstack.interactLivingEntity(player, (LivingEntity) this, hand) : InteractionResult.PASS;
+                return itemstack.is(Items.SADDLE) ? itemstack.interactLivingEntity(player, (LivingEntity) this, hand) : tryRide(player, hand); // Purpur
             } else {
                 if (flag && !this.isSilent()) {
                     this.level.playSound((Player) null, this.getX(), this.getY(), this.getZ(), SoundEvents.STRIDER_EAT, this.getSoundSource(), 1.0F, 1.0F + (this.random.nextFloat() - this.random.nextFloat()) * 0.2F);
diff --git a/src/main/java/net/minecraft/world/entity/monster/Vex.java b/src/main/java/net/minecraft/world/entity/monster/Vex.java
index d4ea25f30ff6b914958da0c7a4914ba9a65327a3..c914cfc75cc0426c0333a6bb30aab7b9e4c52971 100644
--- a/src/main/java/net/minecraft/world/entity/monster/Vex.java
+++ b/src/main/java/net/minecraft/world/entity/monster/Vex.java
@@ -57,6 +57,45 @@ public class Vex extends Monster {
         this.xpReward = 3;
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.vexRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.vexRidableInWater;
+    }
+
+    @Override
+    public double getMaxY() {
+        return level.purpurConfig.vexMaxY;
+    }
+
+    @Override
+    public void travel(Vec3 vec3) {
+        super.travel(vec3);
+        if (getRider() != null) {
+            float speed;
+            if (onGround) {
+                speed = (float) getAttributeValue(Attributes.MOVEMENT_SPEED) * 0.1F;
+            } else {
+                speed = (float) getAttributeValue(Attributes.FLYING_SPEED);
+            }
+            setSpeed(speed);
+            Vec3 mot = getDeltaMovement();
+            move(MoverType.SELF, mot.multiply(speed, 1.0, speed));
+            setDeltaMovement(mot.scale(0.9D));
+        }
+    }
+
+    @Override
+    public boolean causeFallDamage(float fallDistance, float damageMultiplier, DamageSource damageSource) {
+        return false; //  no fall damage please
+    }
+    // Purpur end
+
     @Override
     public boolean isFlapping() {
         return this.tickCount % Vex.TICKS_PER_FLAP == 0;
@@ -70,7 +109,7 @@ public class Vex extends Monster {
 
     @Override
     public void tick() {
-        this.noPhysics = true;
+        this.noPhysics = getRider() == null; // Purpur
         super.tick();
         this.noPhysics = false;
         this.setNoGravity(true);
@@ -85,17 +124,19 @@ public class Vex extends Monster {
     protected void registerGoals() {
         super.registerGoals();
         this.goalSelector.addGoal(0, new FloatGoal(this));
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(4, new Vex.VexChargeAttackGoal());
         this.goalSelector.addGoal(8, new Vex.VexRandomMoveGoal());
         this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 3.0F, 1.0F));
         this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Mob.class, 8.0F));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, new Class[]{Raider.class})).setAlertOthers(new Class[0])); // CraftBukkit - decompile error
         this.targetSelector.addGoal(2, new Vex.VexCopyOwnerTargetGoal(this));
         this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Player.class, true));
     }
 
     public static AttributeSupplier.Builder createAttributes() {
-        return Monster.createMonsterAttributes().add(Attributes.MAX_HEALTH, 14.0D).add(Attributes.ATTACK_DAMAGE, 4.0D);
+        return Monster.createMonsterAttributes().add(Attributes.MAX_HEALTH, 14.0D).add(Attributes.ATTACK_DAMAGE, 4.0D).add(Attributes.FLYING_SPEED, 0.6D); // Purpur;
     }
 
     @Override
@@ -215,14 +256,14 @@ public class Vex extends Monster {
         this.setDropChance(EquipmentSlot.MAINHAND, 0.0F);
     }
 
-    private class VexMoveControl extends MoveControl {
+    private class VexMoveControl extends net.pl3x.purpur.controller.FlyingMoveControllerWASD { // Purpur
 
         public VexMoveControl(Vex entityvex) {
             super(entityvex);
         }
 
         @Override
-        public void tick() {
+        public void vanillaTick() { // Purpur
             if (this.operation == MoveControl.Operation.MOVE_TO) {
                 Vec3 vec3d = new Vec3(this.wantedX - Vex.this.getX(), this.wantedY - Vex.this.getY(), this.wantedZ - Vex.this.getZ());
                 double d0 = vec3d.length();
@@ -231,7 +272,7 @@ public class Vex extends Monster {
                     this.operation = MoveControl.Operation.WAIT;
                     Vex.this.setDeltaMovement(Vex.this.getDeltaMovement().scale(0.5D));
                 } else {
-                    Vex.this.setDeltaMovement(Vex.this.getDeltaMovement().add(vec3d.scale(this.speedModifier * 0.05D / d0)));
+                    Vex.this.setDeltaMovement(Vex.this.getDeltaMovement().add(vec3d.scale(this.getSpeedModifier() * 0.05D / d0))); // Purpur
                     if (Vex.this.getTarget() == null) {
                         Vec3 vec3d1 = Vex.this.getDeltaMovement();
 
diff --git a/src/main/java/net/minecraft/world/entity/monster/Vindicator.java b/src/main/java/net/minecraft/world/entity/monster/Vindicator.java
index 51082fb81477b96c778796e8daf288b366cecf22..a3b1332a92824255b807adc9a5a1d29569e7073b 100644
--- a/src/main/java/net/minecraft/world/entity/monster/Vindicator.java
+++ b/src/main/java/net/minecraft/world/entity/monster/Vindicator.java
@@ -57,14 +57,28 @@ public class Vindicator extends AbstractIllager {
         super(type, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.vindicatorRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.vindicatorRidableInWater;
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
         super.registerGoals();
         this.goalSelector.addGoal(0, new FloatGoal(this));
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(1, new Vindicator.VindicatorBreakDoorGoal(this));
         this.goalSelector.addGoal(2, new AbstractIllager.RaiderOpenDoorGoal(this));
         this.goalSelector.addGoal(3, new Raider.HoldGroundAttackGoal(this, 10.0F));
         this.goalSelector.addGoal(4, new Vindicator.VindicatorMeleeAttackGoal(this));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(1, (new HurtByTargetGoal(this, Raider.class)).setAlertOthers());
         this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true));
         this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, true));
diff --git a/src/main/java/net/minecraft/world/entity/monster/Witch.java b/src/main/java/net/minecraft/world/entity/monster/Witch.java
index 8c3e8c12d7405ad388342e304430834a5fad12a9..e70fb661c62add74cdbe9fc9ef1e3143e96333b2 100644
--- a/src/main/java/net/minecraft/world/entity/monster/Witch.java
+++ b/src/main/java/net/minecraft/world/entity/monster/Witch.java
@@ -60,6 +60,18 @@ public class Witch extends Raider implements RangedAttackMob {
         super(type, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.witchRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.witchRidableInWater;
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
         super.registerGoals();
@@ -68,10 +80,12 @@ public class Witch extends Raider implements RangedAttackMob {
         });
         this.attackPlayersGoal = new NearestAttackableWitchTargetGoal<>(this, Player.class, 10, true, false, (Predicate) null);
         this.goalSelector.addGoal(1, new FloatGoal(this));
+        this.goalSelector.addGoal(1, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.goalSelector.addGoal(2, new RangedAttackGoal(this, 1.0D, 60, 10.0F));
         this.goalSelector.addGoal(2, new WaterAvoidingRandomStrollGoal(this, 1.0D));
         this.goalSelector.addGoal(3, new LookAtPlayerGoal(this, Player.class, 8.0F));
         this.goalSelector.addGoal(3, new RandomLookAroundGoal(this));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.targetSelector.addGoal(1, new HurtByTargetGoal(this, new Class[]{Raider.class}));
         this.targetSelector.addGoal(2, this.healRaidersGoal);
         this.targetSelector.addGoal(3, this.attackPlayersGoal);
diff --git a/src/main/java/net/minecraft/world/entity/monster/WitherSkeleton.java b/src/main/java/net/minecraft/world/entity/monster/WitherSkeleton.java
index f9bc07be70fae9aced51a69bccb5eda309187a47..4cea4ba127dc036ce5d585323a25bbcf06bd46a6 100644
--- a/src/main/java/net/minecraft/world/entity/monster/WitherSkeleton.java
+++ b/src/main/java/net/minecraft/world/entity/monster/WitherSkeleton.java
@@ -34,6 +34,18 @@ public class WitherSkeleton extends AbstractSkeleton {
         this.setPathfindingMalus(BlockPathTypes.LAVA, 8.0F);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.witherSkeletonRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.witherSkeletonRidableInWater;
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
         this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractPiglin.class, true));
diff --git a/src/main/java/net/minecraft/world/entity/monster/Zoglin.java b/src/main/java/net/minecraft/world/entity/monster/Zoglin.java
index 056c0e66d2f90850906c78a25d759f22c20e4d35..8fd4e26ebe0527fd8a69b15095dd4091fcdde206 100644
--- a/src/main/java/net/minecraft/world/entity/monster/Zoglin.java
+++ b/src/main/java/net/minecraft/world/entity/monster/Zoglin.java
@@ -67,6 +67,18 @@ public class Zoglin extends Monster implements Enemy, HoglinBase {
         this.xpReward = 5;
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.zoglinRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.zoglinRidableInWater;
+    }
+    // Purpur end
+
     @Override
     protected Brain.Provider<Zoglin> brainProvider() {
         return Brain.provider(MEMORY_TYPES, SENSOR_TYPES);
@@ -89,11 +101,11 @@ public class Zoglin extends Monster implements Enemy, HoglinBase {
     }
 
     private static void initIdleActivity(Brain<Zoglin> brain) {
-        brain.addActivity(Activity.IDLE, 10, ImmutableList.of(new StartAttacking<>(Zoglin::findNearestValidAttackTarget), new RunSometimes(new SetEntityLookTarget(8.0F), UniformInt.of(30, 60)), new RunOne(ImmutableList.of(Pair.of(new RandomStroll(0.4F), 2), Pair.of(new SetWalkTargetFromLookTarget(0.4F, 3), 2), Pair.of(new DoNothing(30, 60), 1)))));
+        brain.addActivity(Activity.IDLE, 10, ImmutableList.of(new StartAttacking<>(Zoglin::findNearestValidAttackTarget), new RunSometimes<>(new SetEntityLookTarget(8.0F), UniformInt.of(30, 60)), new RunOne<>(ImmutableList.of(Pair.of(new RandomStroll(0.4F), 2), Pair.of(new SetWalkTargetFromLookTarget(0.4F, 3), 2), Pair.of(new DoNothing(30, 60), 1))))); // Purpur - decompile fix
     }
 
     private static void initFightActivity(Brain<Zoglin> brain) {
-        brain.addActivityAndRemoveMemoryWhenStopped(Activity.FIGHT, 10, ImmutableList.of(new SetWalkTargetFromAttackTargetIfTargetOutOfReach(1.0F), new RunIf<>(Zoglin::isAdult, new MeleeAttack(40)), new RunIf<>(Zoglin::isBaby, new MeleeAttack(15)), new StopAttackingIfTargetInvalid()), MemoryModuleType.ATTACK_TARGET);
+        brain.addActivityAndRemoveMemoryWhenStopped(Activity.FIGHT, 10, ImmutableList.of(new SetWalkTargetFromAttackTargetIfTargetOutOfReach(1.0F), new RunIf<>(Zoglin::isAdult, new MeleeAttack(40)), new RunIf<>(Zoglin::isBaby, new MeleeAttack(15)), new StopAttackingIfTargetInvalid<>()), MemoryModuleType.ATTACK_TARGET); // Purpur - decompile fix
     }
 
     private Optional<? extends LivingEntity> findNearestValidAttackTarget() {
@@ -182,7 +194,7 @@ public class Zoglin extends Monster implements Enemy, HoglinBase {
 
     @Override
     public Brain<Zoglin> getBrain() {
-        return super.getBrain();
+        return (Brain<Zoglin>) super.getBrain(); // Purpur - decompile error
     }
 
     protected void updateActivity() {
@@ -199,6 +211,7 @@ public class Zoglin extends Monster implements Enemy, HoglinBase {
     @Override
     protected void customServerAiStep() {
         this.level.getProfiler().push("zoglinBrain");
+        if (getRider() == null) // Purpur - only use brain if no rider
         this.getBrain().tick((ServerLevel)this.level, this);
         this.level.getProfiler().pop();
         this.updateActivity();
diff --git a/src/main/java/net/minecraft/world/entity/monster/Zombie.java b/src/main/java/net/minecraft/world/entity/monster/Zombie.java
index 6408e158745c20ab449c44a28420bc9b28e1efac..6dee348b4e1faec208e5144527ca3df0dab4f96e 100644
--- a/src/main/java/net/minecraft/world/entity/monster/Zombie.java
+++ b/src/main/java/net/minecraft/world/entity/monster/Zombie.java
@@ -107,11 +107,25 @@ public class Zombie extends Monster {
         this(EntityType.ZOMBIE, world);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.zombieRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.zombieRidableInWater;
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         if (level.paperConfig.zombiesTargetTurtleEggs) this.goalSelector.addGoal(4, new Zombie.ZombieAttackTurtleEggGoal(this, 1.0D, 3)); // Paper
         this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F));
         this.goalSelector.addGoal(8, new RandomLookAroundGoal(this));
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
         this.addBehaviourGoals();
     }
 
diff --git a/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java b/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java
index 7a2e3d2ec86536c6caa5a0af0013474143566df8..d487b4cb2da18eac12ec3dc71997cdc1217a85f6 100644
--- a/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java
+++ b/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java
@@ -74,6 +74,18 @@ public class ZombieVillager extends Zombie implements VillagerDataHolder {
         this.setVillagerData(this.getVillagerData().setProfession((VillagerProfession) Registry.VILLAGER_PROFESSION.getRandom(this.random)));
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.zombieVillagerRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.zombieVillagerRidableInWater;
+    }
+    // Purpur end
+
     @Override
     protected void defineSynchedData() {
         super.defineSynchedData();
diff --git a/src/main/java/net/minecraft/world/entity/monster/ZombifiedPiglin.java b/src/main/java/net/minecraft/world/entity/monster/ZombifiedPiglin.java
index 025d53ab0787d596f4c486b15d286b9547838e16..708ba2c64a0736a30e477017b46dc711948cfc3d 100644
--- a/src/main/java/net/minecraft/world/entity/monster/ZombifiedPiglin.java
+++ b/src/main/java/net/minecraft/world/entity/monster/ZombifiedPiglin.java
@@ -58,6 +58,18 @@ public class ZombifiedPiglin extends Zombie implements NeutralMob {
         this.setPathfindingMalus(BlockPathTypes.LAVA, 8.0F);
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.zombifiedPiglinRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.zombifiedPiglinRidableInWater;
+    }
+    // Purpur end
+
     @Override
     public void setPersistentAngerTarget(@Nullable UUID uuid) {
         this.persistentAngerTarget = uuid;
diff --git a/src/main/java/net/minecraft/world/entity/monster/hoglin/Hoglin.java b/src/main/java/net/minecraft/world/entity/monster/hoglin/Hoglin.java
index 3f6697d21cf32b83d3dcd74d810effc28c9efc95..060676e094c425ab11889ee221808e5814d34f43 100644
--- a/src/main/java/net/minecraft/world/entity/monster/hoglin/Hoglin.java
+++ b/src/main/java/net/minecraft/world/entity/monster/hoglin/Hoglin.java
@@ -67,6 +67,18 @@ public class Hoglin extends Animal implements Enemy, HoglinBase {
         this.xpReward = 5;
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.hoglinRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.hoglinRidableInWater;
+    }
+    // Purpur end
+
     @Override
     public boolean canBeLeashed(Player player) {
         return !this.isLeashed();
@@ -130,6 +142,7 @@ public class Hoglin extends Animal implements Enemy, HoglinBase {
     @Override
     protected void customServerAiStep() {
         this.level.getProfiler().push("hoglinBrain");
+        if (getRider() == null) // Purpur - only use brain if no rider
         if (this.behaviorTick++ % this.activatedPriority == 0) // Airplane
         this.getBrain().tick((ServerLevel)this.level, this);
         this.level.getProfiler().pop();
diff --git a/src/main/java/net/minecraft/world/entity/monster/piglin/Piglin.java b/src/main/java/net/minecraft/world/entity/monster/piglin/Piglin.java
index 06c6ed64488bb3685b9de3e05d8a1e74ebc62e37..ace6891fd0e14af5268249b60e14a99c17a2f435 100644
--- a/src/main/java/net/minecraft/world/entity/monster/piglin/Piglin.java
+++ b/src/main/java/net/minecraft/world/entity/monster/piglin/Piglin.java
@@ -97,6 +97,18 @@ public class Piglin extends AbstractPiglin implements CrossbowAttackMob, Invento
         this.xpReward = 5;
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.piglinRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.piglinRidableInWater;
+    }
+    // Purpur end
+
     @Override
     public void addAdditionalSaveData(CompoundTag nbt) {
         super.addAdditionalSaveData(nbt);
@@ -293,6 +305,7 @@ public class Piglin extends AbstractPiglin implements CrossbowAttackMob, Invento
     @Override
     protected void customServerAiStep() {
         this.level.getProfiler().push("piglinBrain");
+        if (getRider() == null) // Purpur - only use brain if no rider
         if (this.behaviorTick++ % this.activatedPriority == 0) // Airplane
         this.getBrain().tick((ServerLevel) this.level, (Piglin) this); // CraftBukkit - decompile error
         this.level.getProfiler().pop();
diff --git a/src/main/java/net/minecraft/world/entity/monster/piglin/PiglinBrute.java b/src/main/java/net/minecraft/world/entity/monster/piglin/PiglinBrute.java
index af579b2b6f6e18da70e67ce74431a57d9a1236dd..d01a8fe6dd5c84ea5b7370a90c0d57130b27e447 100644
--- a/src/main/java/net/minecraft/world/entity/monster/piglin/PiglinBrute.java
+++ b/src/main/java/net/minecraft/world/entity/monster/piglin/PiglinBrute.java
@@ -40,6 +40,18 @@ public class PiglinBrute extends AbstractPiglin {
         this.xpReward = 20;
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.piglinBruteRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.piglinBruteRidableInWater;
+    }
+    // Purpur end
+
     public static AttributeSupplier.Builder createAttributes() {
         return Monster.createMonsterAttributes().add(Attributes.MAX_HEALTH, 50.0D).add(Attributes.MOVEMENT_SPEED, (double)0.35F).add(Attributes.ATTACK_DAMAGE, 7.0D);
     }
@@ -69,7 +81,7 @@ public class PiglinBrute extends AbstractPiglin {
 
     @Override
     public Brain<PiglinBrute> getBrain() {
-        return super.getBrain();
+        return (Brain<PiglinBrute>) super.getBrain(); // Purpur - decompile error
     }
 
     @Override
@@ -85,6 +97,7 @@ public class PiglinBrute extends AbstractPiglin {
     @Override
     protected void customServerAiStep() {
         this.level.getProfiler().push("piglinBruteBrain");
+        if (getRider() == null) // Purpur - only use brain if no rider
         this.getBrain().tick((ServerLevel)this.level, this);
         this.level.getProfiler().pop();
         PiglinBruteAi.updateActivity(this);
diff --git a/src/main/java/net/minecraft/world/entity/npc/Villager.java b/src/main/java/net/minecraft/world/entity/npc/Villager.java
index 0f7f7aef78a1f34fe113eeb00a88fd2610351344..8a37e6b42a50da4822389004123e3b72b33f50b4 100644
--- a/src/main/java/net/minecraft/world/entity/npc/Villager.java
+++ b/src/main/java/net/minecraft/world/entity/npc/Villager.java
@@ -157,6 +157,23 @@ public class Villager extends AbstractVillager implements ReputationEventHandler
         this.setVillagerData(this.getVillagerData().setType(type).setProfession(VillagerProfession.NONE));
     }
 
+    // Purpur start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.villagerRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.villagerRidableInWater;
+    }
+
+    @Override
+    protected void registerGoals() {
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this));
+    }
+    // Purpur end
+
     @Override
     public Brain<Villager> getBrain() {
         return (Brain<Villager>) super.getBrain(); // CraftBukkit - decompile error
@@ -314,7 +331,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler
         if (!itemstack.is(Items.VILLAGER_SPAWN_EGG) && this.isAlive() && !this.isTrading() && !this.isSleeping()) {
             if (this.isBaby()) {
                 this.setUnhappy();
-                return InteractionResult.sidedSuccess(this.level.isClientSide);
+                return tryRide(player, hand); // Purpur
             } else {
                 boolean flag = this.getOffers().isEmpty();
 
@@ -327,8 +344,9 @@ public class Villager extends AbstractVillager implements ReputationEventHandler
                 }
 
                 if (flag) {
-                    return InteractionResult.sidedSuccess(this.level.isClientSide);
+                    return tryRide(player, hand); // Purpur
                 } else {
+                    if (level.purpurConfig.villagerRidable && itemstack.isEmpty()) return tryRide(player, hand); // Purpur
                     if (!this.level.isClientSide && !this.offers.isEmpty()) {
                         this.startTrading(player);
                     }
diff --git a/src/main/java/net/minecraft/world/entity/npc/WanderingTrader.java b/src/main/java/net/minecraft/world/entity/npc/WanderingTrader.java
index c4f7c94255e4631a3c0355f9260132ba28296f50..d6c31596e21041a124a263054ccb6447829eccdd 100644
--- a/src/main/java/net/minecraft/world/entity/npc/WanderingTrader.java
+++ b/src/main/java/net/minecraft/world/entity/npc/WanderingTrader.java
@@ -67,6 +67,18 @@ public class WanderingTrader extends net.minecraft.world.entity.npc.AbstractVill
         //this.setDespawnDelay(48000); // CraftBukkit - set default from MobSpawnerTrader // Paper - move back to MobSpawnerTrader - Vanilla behavior is that only traders spawned by it have this value set.
     }
 
+    // Purpur - start
+    @Override
+    public boolean isRidable() {
+        return level.purpurConfig.wanderingTraderRidable;
+    }
+
+    @Override
+    public boolean rideableUnderWater() {
+        return level.purpurConfig.wanderingTraderRidableInWater;
+    }
+    // Purpur end
+
     @Override
     protected void registerGoals() {
         this.goalSelector.addGoal(0, new FloatGoal(this));
@@ -114,8 +126,9 @@ public class WanderingTrader extends net.minecraft.world.entity.npc.AbstractVill
             }
 
             if (this.getOffers().isEmpty()) {
-                return InteractionResult.sidedSuccess(this.level.isClientSide);
+                return tryRide(player, hand); // Purpur
             } else {
+                if (level.purpurConfig.wanderingTraderRidable && itemstack.isEmpty()) return tryRide(player, hand); // Purpur
                 if (!this.level.isClientSide) {
                     this.setTradingPlayer(player);
                     this.openTradingScreen(player, this.getDisplayName(), 1);
diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java
index 9e0eec259fdae57e235bfe00ece4df9957edc642..19b65c0d2fd96b4516c0717511a6bb02b2d66065 100644
--- a/src/main/java/net/minecraft/world/entity/player/Player.java
+++ b/src/main/java/net/minecraft/world/entity/player/Player.java
@@ -193,6 +193,19 @@ public abstract class Player extends LivingEntity {
     }
     // CraftBukkit end
 
+    // Purpur start
+    public abstract void resetLastActionTime();
+
+    @Override
+    public boolean processClick(InteractionHand hand) {
+        Entity vehicle = getRootVehicle();
+        if (vehicle != null && vehicle.getRider() == this) {
+            return vehicle.onClick(hand);
+        }
+        return false;
+    }
+    // Purpur end
+
     public Player(Level world, BlockPos pos, float yaw, GameProfile profile) {
         super(EntityType.PLAYER, world);
         this.lastItemInMainHand = ItemStack.EMPTY;
diff --git a/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java b/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java
index 85a509e4fc0e4b9f182585e17b7deab2fea7e6c0..f1a12b147d55e34d4f8374593640a311598cf1a6 100644
--- a/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java
+++ b/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java
@@ -27,6 +27,12 @@ public class LlamaSpit extends Projectile {
         this.setPos(owner.getX() - (double) (owner.getBbWidth() + 1.0F) * 0.5D * (double) Mth.sin(owner.yBodyRot * 0.017453292F), owner.getEyeY() - 0.10000000149011612D, owner.getZ() + (double) (owner.getBbWidth() + 1.0F) * 0.5D * (double) Mth.cos(owner.yBodyRot * 0.017453292F));
     }
 
+    // Purpur start
+    public void super_tick() {
+        super.tick();
+    }
+    // Purpur end
+
     @Override
     public void tick() {
         super.tick();
diff --git a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java
index 008e3b769011df76697ad0abcdde5727cef05f64..0837875231ef96ff58f2fee3e558d7066d613cb8 100644
--- a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java
+++ b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java
@@ -35,7 +35,7 @@ public abstract class Projectile extends Entity {
     private boolean hasBeenShot;
 
     // CraftBukkit start
-    private boolean hitCancelled = false;
+    public boolean hitCancelled = false; // Purpur - private -> public
     // CraftBukkit end
 
     Projectile(EntityType<? extends Projectile> type, Level world) {
diff --git a/src/main/java/net/pl3x/purpur/PurpurConfig.java b/src/main/java/net/pl3x/purpur/PurpurConfig.java
index faa4ad6af45ce1ef62084194340c6789634bff5f..ad7dbfa8dd5f38af87ec8980fd972ea4449da7ed 100644
--- a/src/main/java/net/pl3x/purpur/PurpurConfig.java
+++ b/src/main/java/net/pl3x/purpur/PurpurConfig.java
@@ -156,4 +156,9 @@ public class PurpurConfig {
         }
         return builder.build();
     }
+
+    public static String cannotRideMob = "<red>You cannot mount that mob";
+    private static void messages() {
+        cannotRideMob = getString("settings.messages.cannot-ride-mob", cannotRideMob);
+    }
 }
diff --git a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java
index 683ee20f08eac69238bea33021e35b1838c44a8b..3f91e7f49a1f00ff21fd65ea443af02729b7cc10 100644
--- a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java
+++ b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java
@@ -92,4 +92,532 @@ public class PurpurWorldConfig {
         final Map<String, Object> value = PurpurConfig.getMap("world-settings." + worldName + "." + path, null);
         return value.isEmpty() ? fallback : value;
     }
+
+    public boolean babiesAreRidable = true;
+    public boolean untamedTamablesAreRidable = true;
+    public boolean useNightVisionWhenRiding = false;
+    private void ridableSettings() {
+        babiesAreRidable = getBoolean("ridable-settings.babies-are-ridable", babiesAreRidable);
+        untamedTamablesAreRidable = getBoolean("ridable-settings.untamed-tamables-are-ridable", untamedTamablesAreRidable);
+        useNightVisionWhenRiding = getBoolean("ridable-settings.use-night-vision", useNightVisionWhenRiding);
+    }
+
+    public boolean axolotlRidable = false;
+    private void axolotlSettings() {
+        axolotlRidable = getBoolean("mobs.axolotl.ridable", axolotlRidable);
+    }
+
+    public boolean batRidable = false;
+    public boolean batRidableInWater = false;
+    public double batMaxY = 256D;
+    private void batSettings() {
+        batRidable = getBoolean("mobs.bat.ridable", batRidable);
+        batRidableInWater = getBoolean("mobs.bat.ridable-in-water", batRidableInWater);
+        batMaxY = getDouble("mobs.bat.ridable-max-y", batMaxY);
+    }
+
+    public boolean beeRidable = false;
+    public boolean beeRidableInWater = false;
+    public double beeMaxY = 256D;
+    private void beeSettings() {
+        beeRidable = getBoolean("mobs.bee.ridable", beeRidable);
+        beeRidableInWater = getBoolean("mobs.bee.ridable-in-water", beeRidableInWater);
+        beeMaxY = getDouble("mobs.bee.ridable-max-y", beeMaxY);
+    }
+
+    public boolean blazeRidable = false;
+    public boolean blazeRidableInWater = false;
+    public double blazeMaxY = 256D;
+    private void blazeSettings() {
+        blazeRidable = getBoolean("mobs.blaze.ridable", blazeRidable);
+        blazeRidableInWater = getBoolean("mobs.blaze.ridable-in-water", blazeRidableInWater);
+        blazeMaxY = getDouble("mobs.blaze.ridable-max-y", blazeMaxY);
+    }
+
+    public boolean catRidable = false;
+    public boolean catRidableInWater = false;
+    private void catSettings() {
+        catRidable = getBoolean("mobs.cat.ridable", catRidable);
+        catRidableInWater = getBoolean("mobs.cat.ridable-in-water", catRidableInWater);
+    }
+
+    public boolean caveSpiderRidable = false;
+    public boolean caveSpiderRidableInWater = false;
+    private void caveSpiderSettings() {
+        caveSpiderRidable = getBoolean("mobs.cave_spider.ridable", caveSpiderRidable);
+        caveSpiderRidableInWater = getBoolean("mobs.cave_spider.ridable-in-water", caveSpiderRidableInWater);
+    }
+
+    public boolean chickenRidable = false;
+    public boolean chickenRidableInWater = false;
+    private void chickenSettings() {
+        chickenRidable = getBoolean("mobs.chicken.ridable", chickenRidable);
+        chickenRidableInWater = getBoolean("mobs.chicken.ridable-in-water", chickenRidableInWater);
+    }
+
+    public boolean codRidable = false;
+    private void codSettings() {
+        codRidable = getBoolean("mobs.cod.ridable", codRidable);
+    }
+
+    public boolean cowRidable = false;
+    public boolean cowRidableInWater = false;
+    private void cowSettings() {
+        cowRidable = getBoolean("mobs.cow.ridable", cowRidable);
+        cowRidableInWater = getBoolean("mobs.cow.ridable-in-water", cowRidableInWater);
+    }
+
+    public boolean creeperRidable = false;
+    public boolean creeperRidableInWater = false;
+    private void creeperSettings() {
+        creeperRidable = getBoolean("mobs.creeper.ridable", creeperRidable);
+        creeperRidableInWater = getBoolean("mobs.creeper.ridable-in-water", creeperRidableInWater);
+    }
+
+    public boolean dolphinRidable = false;
+    public int dolphinSpitCooldown = 20;
+    public float dolphinSpitSpeed = 1.0F;
+    public float dolphinSpitDamage = 2.0F;
+    private void dolphinSettings() {
+        dolphinRidable = getBoolean("mobs.dolphin.ridable", dolphinRidable);
+        dolphinSpitCooldown = getInt("mobs.dolphin.spit.cooldown", dolphinSpitCooldown);
+        dolphinSpitSpeed = (float) getDouble("mobs.dolphin.spit.speed", dolphinSpitSpeed);
+        dolphinSpitDamage = (float) getDouble("mobs.dolphin.spit.damage", dolphinSpitDamage);
+    }
+
+    public boolean donkeyRidableInWater = false;
+    private void donkeySettings() {
+        donkeyRidableInWater = getBoolean("mobs.donkey.ridable-in-water", donkeyRidableInWater);
+    }
+
+    public boolean drownedRidable = false;
+    public boolean drownedRidableInWater = false;
+    private void drownedSettings() {
+        drownedRidable = getBoolean("mobs.drowned.ridable", drownedRidable);
+        drownedRidableInWater = getBoolean("mobs.drowned.ridable-in-water", drownedRidableInWater);
+    }
+
+    public boolean elderGuardianRidable = false;
+    private void elderGuardianSettings() {
+        elderGuardianRidable = getBoolean("mobs.elder_guardian.ridable", elderGuardianRidable);
+    }
+
+    public boolean enderDragonRidable = false;
+    public boolean enderDragonRidableInWater = false;
+    public double enderDragonMaxY = 256D;
+    private void enderDragonSettings() {
+        enderDragonRidable = getBoolean("mobs.ender_dragon.ridable", enderDragonRidable);
+        enderDragonRidableInWater = getBoolean("mobs.ender_dragon.ridable-in-water", enderDragonRidableInWater);
+        enderDragonMaxY = getDouble("mobs.ender_dragon.ridable-max-y", enderDragonMaxY);
+    }
+
+    public boolean endermanRidable = false;
+    public boolean endermanRidableInWater = false;
+    private void endermanSettings() {
+        endermanRidable = getBoolean("mobs.enderman.ridable", endermanRidable);
+        endermanRidableInWater = getBoolean("mobs.enderman.ridable-in-water", endermanRidableInWater);
+    }
+
+    public boolean endermiteRidable = false;
+    public boolean endermiteRidableInWater = false;
+    private void endermiteSettings() {
+        endermiteRidable = getBoolean("mobs.endermite.ridable", endermiteRidable);
+        endermiteRidableInWater = getBoolean("mobs.endermite.ridable-in-water", endermiteRidableInWater);
+    }
+
+    public boolean evokerRidable = false;
+    public boolean evokerRidableInWater = false;
+    private void evokerSettings() {
+        evokerRidable = getBoolean("mobs.evoker.ridable", evokerRidable);
+        evokerRidableInWater = getBoolean("mobs.evoker.ridable-in-water", evokerRidableInWater);
+    }
+
+    public boolean foxRidable = false;
+    public boolean foxRidableInWater = false;
+    private void foxSettings() {
+        foxRidable = getBoolean("mobs.fox.ridable", foxRidable);
+        foxRidableInWater = getBoolean("mobs.fox.ridable-in-water", foxRidableInWater);
+    }
+
+    public boolean ghastRidable = false;
+    public boolean ghastRidableInWater = false;
+    public double ghastMaxY = 256D;
+    private void ghastSettings() {
+        ghastRidable = getBoolean("mobs.ghast.ridable", ghastRidable);
+        ghastRidableInWater = getBoolean("mobs.ghast.ridable-in-water", ghastRidableInWater);
+        ghastMaxY = getDouble("mobs.ghast.ridable-max-y", ghastMaxY);
+    }
+
+    public boolean giantRidable = false;
+    public boolean giantRidableInWater = false;
+    private void giantSettings() {
+        giantRidable = getBoolean("mobs.giant.ridable", giantRidable);
+        giantRidableInWater = getBoolean("mobs.giant.ridable-in-water", giantRidableInWater);
+    }
+
+    public boolean glowSquidRidable = false;
+    private void glowSquidSettings() {
+        glowSquidRidable = getBoolean("mobs.glow_squid.ridable", glowSquidRidable);
+    }
+
+    public boolean goatRidable = false;
+    public boolean goatRidableInWater = false;
+    private void goatSettings() {
+        goatRidable = getBoolean("mobs.goat.ridable", goatRidable);
+        goatRidableInWater = getBoolean("mobs.goat.ridable-in-water", goatRidableInWater);
+    }
+
+    public boolean guardianRidable = false;
+    private void guardianSettings() {
+        guardianRidable = getBoolean("mobs.guardian.ridable", guardianRidable);
+    }
+
+    public boolean hoglinRidable = false;
+    public boolean hoglinRidableInWater = false;
+    private void hoglinSettings() {
+        hoglinRidable = getBoolean("mobs.hoglin.ridable", hoglinRidable);
+        hoglinRidableInWater = getBoolean("mobs.hoglin.ridable-in-water", hoglinRidableInWater);
+    }
+
+    public boolean horseRidableInWater = false;
+    private void horseSettings() {
+        horseRidableInWater = getBoolean("mobs.horse.ridable-in-water", horseRidableInWater);
+    }
+
+    public boolean huskRidable = false;
+    public boolean huskRidableInWater = false;
+    private void huskSettings() {
+        huskRidable = getBoolean("mobs.husk.ridable", huskRidable);
+        huskRidableInWater = getBoolean("mobs.husk.ridable-in-water", huskRidableInWater);
+    }
+
+    public boolean illusionerRidable = false;
+    public boolean illusionerRidableInWater = false;
+    private void illusionerSettings() {
+        illusionerRidable = getBoolean("mobs.illusioner.ridable", illusionerRidable);
+        illusionerRidableInWater = getBoolean("mobs.illusioner.ridable-in-water", illusionerRidableInWater);
+    }
+
+    public boolean ironGolemRidable = false;
+    public boolean ironGolemRidableInWater = false;
+    public boolean ironGolemCanSwim = false;
+    private void ironGolemSettings() {
+        ironGolemRidable = getBoolean("mobs.iron_golem.ridable", ironGolemRidable);
+        ironGolemRidableInWater = getBoolean("mobs.iron_golem.ridable-in-water", ironGolemRidableInWater);
+        ironGolemCanSwim = getBoolean("mobs.iron_golem.can-swim", ironGolemCanSwim);
+    }
+
+    public boolean llamaRidable = false;
+    public boolean llamaRidableInWater = false;
+    private void llamaSettings() {
+        llamaRidable = getBoolean("mobs.llama.ridable", llamaRidable);
+        llamaRidableInWater = getBoolean("mobs.llama.ridable-in-water", llamaRidableInWater);
+    }
+
+    public boolean magmaCubeRidable = false;
+    public boolean magmaCubeRidableInWater = false;
+    private void magmaCubeSettings() {
+        magmaCubeRidable = getBoolean("mobs.magma_cube.ridable", magmaCubeRidable);
+        magmaCubeRidableInWater = getBoolean("mobs.magma_cube.ridable-in-water", magmaCubeRidableInWater);
+    }
+
+    public boolean mooshroomRidable = false;
+    public boolean mooshroomRidableInWater = false;
+    private void mooshroomSettings() {
+        mooshroomRidable = getBoolean("mobs.mooshroom.ridable", mooshroomRidable);
+        mooshroomRidableInWater = getBoolean("mobs.mooshroom.ridable-in-water", mooshroomRidableInWater);
+    }
+
+    public boolean muleRidableInWater = false;
+    private void muleSettings() {
+        muleRidableInWater = getBoolean("mobs.mule.ridable-in-water", muleRidableInWater);
+    }
+
+    public boolean ocelotRidable = false;
+    public boolean ocelotRidableInWater = false;
+    private void ocelotSettings() {
+        ocelotRidable = getBoolean("mobs.ocelot.ridable", ocelotRidable);
+        ocelotRidableInWater = getBoolean("mobs.ocelot.ridable-in-water", ocelotRidableInWater);
+    }
+
+    public boolean pandaRidable = false;
+    public boolean pandaRidableInWater = false;
+    private void pandaSettings() {
+        pandaRidable = getBoolean("mobs.panda.ridable", pandaRidable);
+        pandaRidableInWater = getBoolean("mobs.panda.ridable-in-water", pandaRidableInWater);
+    }
+
+    public boolean parrotRidable = false;
+    public boolean parrotRidableInWater = false;
+    public double parrotMaxY = 256D;
+    private void parrotSettings() {
+        parrotRidable = getBoolean("mobs.parrot.ridable", parrotRidable);
+        parrotRidableInWater = getBoolean("mobs.parrot.ridable-in-water", parrotRidableInWater);
+        parrotMaxY = getDouble("mobs.parrot.ridable-max-y", parrotMaxY);
+    }
+
+    public boolean phantomRidable = false;
+    public boolean phantomRidableInWater = false;
+    public double phantomMaxY = 256D;
+    public float phantomFlameDamage = 1.0F;
+    public int phantomFlameFireTime = 8;
+    public boolean phantomAllowGriefing = false;
+    private void phantomSettings() {
+        phantomRidable = getBoolean("mobs.phantom.ridable", phantomRidable);
+        phantomRidableInWater = getBoolean("mobs.phantom.ridable-in-water", phantomRidableInWater);
+        phantomMaxY = getDouble("mobs.phantom.ridable-max-y", phantomMaxY);
+        phantomFlameDamage = (float) getDouble("mobs.phantom.flames.damage", phantomFlameDamage);
+        phantomFlameFireTime = getInt("mobs.phantom.flames.fire-time", phantomFlameFireTime);
+        phantomAllowGriefing = getBoolean("mobs.phantom.allow-griefing", phantomAllowGriefing);
+    }
+
+    public boolean pigRidable = false;
+    public boolean pigRidableInWater = false;
+    private void pigSettings() {
+        pigRidable = getBoolean("mobs.pig.ridable", pigRidable);
+        pigRidableInWater = getBoolean("mobs.pig.ridable-in-water", pigRidableInWater);
+    }
+
+    public boolean piglinRidable = false;
+    public boolean piglinRidableInWater = false;
+    private void piglinSettings() {
+        piglinRidable = getBoolean("mobs.piglin.ridable", piglinRidable);
+        piglinRidableInWater = getBoolean("mobs.piglin.ridable-in-water", piglinRidableInWater);
+    }
+
+    public boolean piglinBruteRidable = false;
+    public boolean piglinBruteRidableInWater = false;
+    private void piglinBruteSettings() {
+        piglinBruteRidable = getBoolean("mobs.piglin_brute.ridable", piglinBruteRidable);
+        piglinBruteRidableInWater = getBoolean("mobs.piglin_brute.ridable-in-water", piglinBruteRidableInWater);
+    }
+
+    public boolean pillagerRidable = false;
+    public boolean pillagerRidableInWater = false;
+    private void pillagerSettings() {
+        pillagerRidable = getBoolean("mobs.pillager.ridable", pillagerRidable);
+        pillagerRidableInWater = getBoolean("mobs.pillager.ridable-in-water", pillagerRidableInWater);
+    }
+
+    public boolean polarBearRidable = false;
+    public boolean polarBearRidableInWater = false;
+    private void polarBearSettings() {
+        polarBearRidable = getBoolean("mobs.polar_bear.ridable", polarBearRidable);
+        polarBearRidableInWater = getBoolean("mobs.polar_bear.ridable-in-water", polarBearRidableInWater);
+    }
+
+    public boolean pufferfishRidable = false;
+    private void pufferfishSettings() {
+        pufferfishRidable = getBoolean("mobs.pufferfish.ridable", pufferfishRidable);
+    }
+
+    public boolean rabbitRidable = false;
+    public boolean rabbitRidableInWater = false;
+    private void rabbitSettings() {
+        rabbitRidable = getBoolean("mobs.rabbit.ridable", rabbitRidable);
+        rabbitRidableInWater = getBoolean("mobs.rabbit.ridable-in-water", rabbitRidableInWater);
+    }
+
+    public boolean ravagerRidable = false;
+    public boolean ravagerRidableInWater = false;
+    private void ravagerSettings() {
+        ravagerRidable = getBoolean("mobs.ravager.ridable", ravagerRidable);
+        ravagerRidableInWater = getBoolean("mobs.ravager.ridable-in-water", ravagerRidableInWater);
+    }
+
+    public boolean salmonRidable = false;
+    private void salmonSettings() {
+        salmonRidable = getBoolean("mobs.salmon.ridable", salmonRidable);
+    }
+
+    public boolean sheepRidable = false;
+    public boolean sheepRidableInWater = false;
+    private void sheepSettings() {
+        sheepRidable = getBoolean("mobs.sheep.ridable", sheepRidable);
+        sheepRidableInWater = getBoolean("mobs.sheep.ridable-in-water", sheepRidableInWater);
+    }
+
+    public boolean shulkerRidable = false;
+    public boolean shulkerRidableInWater = false;
+    private void shulkerSettings() {
+        shulkerRidable = getBoolean("mobs.shulker.ridable", shulkerRidable);
+        shulkerRidableInWater = getBoolean("mobs.shulker.ridable-in-water", shulkerRidableInWater);
+    }
+
+    public boolean silverfishRidable = false;
+    public boolean silverfishRidableInWater = false;
+    private void silverfishSettings() {
+        silverfishRidable = getBoolean("mobs.silverfish.ridable", silverfishRidable);
+        silverfishRidableInWater = getBoolean("mobs.silverfish.ridable-in-water", silverfishRidableInWater);
+    }
+
+    public boolean skeletonRidable = false;
+    public boolean skeletonRidableInWater = false;
+    private void skeletonSettings() {
+        skeletonRidable = getBoolean("mobs.skeleton.ridable", skeletonRidable);
+        skeletonRidableInWater = getBoolean("mobs.skeleton.ridable-in-water", skeletonRidableInWater);
+    }
+
+    public boolean skeletonHorseRidableInWater = false;
+    public boolean skeletonHorseCanSwim = false;
+    private void skeletonHorseSettings() {
+        skeletonHorseRidableInWater = getBoolean("mobs.skeleton_horse.ridable-in-water", skeletonHorseRidableInWater);
+        skeletonHorseCanSwim = getBoolean("mobs.skeleton_horse.can-swim", skeletonHorseCanSwim);
+    }
+
+    public boolean slimeRidable = false;
+    public boolean slimeRidableInWater = false;
+    private void slimeSettings() {
+        slimeRidable = getBoolean("mobs.slime.ridable", slimeRidable);
+        slimeRidableInWater = getBoolean("mobs.slime.ridable-in-water", slimeRidableInWater);
+    }
+
+    public boolean snowGolemRidable = false;
+    public boolean snowGolemRidableInWater = false;
+    public boolean snowGolemLeaveTrailWhenRidden = false;
+    private void snowGolemSettings() {
+        snowGolemRidable = getBoolean("mobs.snow_golem.ridable", snowGolemRidable);
+        snowGolemRidableInWater = getBoolean("mobs.snow_golem.ridable-in-water", snowGolemRidableInWater);
+        snowGolemLeaveTrailWhenRidden = getBoolean("mobs.snow_golem.leave-trail-when-ridden", snowGolemLeaveTrailWhenRidden);
+    }
+
+    public boolean squidRidable = false;
+    private void squidSettings() {
+        squidRidable = getBoolean("mobs.squid.ridable", squidRidable);
+    }
+
+    public boolean spiderRidable = false;
+    public boolean spiderRidableInWater = false;
+    private void spiderSettings() {
+        spiderRidable = getBoolean("mobs.spider.ridable", spiderRidable);
+        spiderRidableInWater = getBoolean("mobs.spider.ridable-in-water", spiderRidableInWater);
+    }
+
+    public boolean strayRidable = false;
+    public boolean strayRidableInWater = false;
+    private void straySettings() {
+        strayRidable = getBoolean("mobs.stray.ridable", strayRidable);
+        strayRidableInWater = getBoolean("mobs.stray.ridable-in-water", strayRidableInWater);
+    }
+
+    public boolean striderRidable = false;
+    public boolean striderRidableInWater = false;
+    private void striderSettings() {
+        striderRidable = getBoolean("mobs.strider.ridable", striderRidable);
+        striderRidableInWater = getBoolean("mobs.strider.ridable-in-water", striderRidableInWater);
+    }
+
+    public boolean traderLlamaRidable = false;
+    public boolean traderLlamaRidableInWater = false;
+    private void traderLlamaSettings() {
+        traderLlamaRidable = getBoolean("mobs.trader_llama.ridable", traderLlamaRidable);
+        traderLlamaRidableInWater = getBoolean("mobs.trader_llama.ridable-in-water", traderLlamaRidableInWater);
+    }
+
+    public boolean tropicalFishRidable = false;
+    private void tropicalFishSettings() {
+        tropicalFishRidable = getBoolean("mobs.tropical_fish.ridable", tropicalFishRidable);
+    }
+
+    public boolean turtleRidable = false;
+    public boolean turtleRidableInWater = false;
+    private void turtleSettings() {
+        turtleRidable = getBoolean("mobs.turtle.ridable", turtleRidable);
+        turtleRidableInWater = getBoolean("mobs.turtle.ridable-in-water", turtleRidableInWater);
+    }
+
+    public boolean vexRidable = false;
+    public boolean vexRidableInWater = false;
+    public double vexMaxY = 256D;
+    private void vexSettings() {
+        vexRidable = getBoolean("mobs.vex.ridable", vexRidable);
+        vexRidableInWater = getBoolean("mobs.vex.ridable-in-water", vexRidableInWater);
+        vexMaxY = getDouble("mobs.vex.ridable-max-y", vexMaxY);
+    }
+
+    public boolean villagerRidable = false;
+    public boolean villagerRidableInWater = false;
+    private void villagerSettings() {
+        villagerRidable = getBoolean("mobs.villager.ridable", villagerRidable);
+        villagerRidableInWater = getBoolean("mobs.villager.ridable-in-water", villagerRidableInWater);
+    }
+
+    public boolean vindicatorRidable = false;
+    public boolean vindicatorRidableInWater = false;
+    private void vindicatorSettings() {
+        vindicatorRidable = getBoolean("mobs.vindicator.ridable", vindicatorRidable);
+        vindicatorRidableInWater = getBoolean("mobs.vindicator.ridable-in-water", vindicatorRidableInWater);
+    }
+
+    public boolean wanderingTraderRidable = false;
+    public boolean wanderingTraderRidableInWater = false;
+    private void wanderingTraderSettings() {
+        wanderingTraderRidable = getBoolean("mobs.wandering_trader.ridable", wanderingTraderRidable);
+        wanderingTraderRidableInWater = getBoolean("mobs.wandering_trader.ridable-in-water", wanderingTraderRidableInWater);
+    }
+
+    public boolean witchRidable = false;
+    public boolean witchRidableInWater = false;
+    private void witchSettings() {
+        witchRidable = getBoolean("mobs.witch.ridable", witchRidable);
+        witchRidableInWater = getBoolean("mobs.witch.ridable-in-water", witchRidableInWater);
+    }
+
+    public boolean witherRidable = false;
+    public boolean witherRidableInWater = false;
+    public double witherMaxY = 256D;
+    private void witherSettings() {
+        witherRidable = getBoolean("mobs.wither.ridable", witherRidable);
+        witherRidableInWater = getBoolean("mobs.wither.ridable-in-water", witherRidableInWater);
+        witherMaxY = getDouble("mobs.wither.ridable-max-y", witherMaxY);
+    }
+
+    public boolean witherSkeletonRidable = false;
+    public boolean witherSkeletonRidableInWater = false;
+    private void witherSkeletonSettings() {
+        witherSkeletonRidable = getBoolean("mobs.wither_skeleton.ridable", witherSkeletonRidable);
+        witherSkeletonRidableInWater = getBoolean("mobs.wither_skeleton.ridable-in-water", witherSkeletonRidableInWater);
+    }
+
+    public boolean wolfRidable = false;
+    public boolean wolfRidableInWater = false;
+    private void wolfSettings() {
+        wolfRidable = getBoolean("mobs.wolf.ridable", wolfRidable);
+        wolfRidableInWater = getBoolean("mobs.wolf.ridable-in-water", wolfRidableInWater);
+    }
+
+    public boolean zoglinRidable = false;
+    public boolean zoglinRidableInWater = false;
+    private void zoglinSettings() {
+        zoglinRidable = getBoolean("mobs.zoglin.ridable", zoglinRidable);
+        zoglinRidableInWater = getBoolean("mobs.zoglin.ridable-in-water", zoglinRidableInWater);
+    }
+
+    public boolean zombieRidable = false;
+    public boolean zombieRidableInWater = false;
+    private void zombieSettings() {
+        zombieRidable = getBoolean("mobs.zombie.ridable", zombieRidable);
+        zombieRidableInWater = getBoolean("mobs.zombie.ridable-in-water", zombieRidableInWater);
+    }
+
+    public boolean zombieHorseRidableInWater = false;
+    public boolean zombieHorseCanSwim = false;
+    private void zombieHorseSettings() {
+        zombieHorseRidableInWater = getBoolean("mobs.zombie_horse.ridable-in-water", zombieHorseRidableInWater);
+        zombieHorseCanSwim = getBoolean("mobs.zombie_horse.can-swim", zombieHorseCanSwim);
+    }
+
+    public boolean zombieVillagerRidable = false;
+    public boolean zombieVillagerRidableInWater = false;
+    private void zombieVillagerSettings() {
+        zombieVillagerRidable = getBoolean("mobs.zombie_villager.ridable", zombieVillagerRidable);
+        zombieVillagerRidableInWater = getBoolean("mobs.zombie_villager.ridable-in-water", zombieVillagerRidableInWater);
+    }
+
+    public boolean zombifiedPiglinRidable = false;
+    public boolean zombifiedPiglinRidableInWater = false;
+    private void zombifiedPiglinSettings() {
+        zombifiedPiglinRidable = getBoolean("mobs.zombified_piglin.ridable", zombifiedPiglinRidable);
+        zombifiedPiglinRidableInWater = getBoolean("mobs.zombified_piglin.ridable-in-water", zombifiedPiglinRidableInWater);
+    }
 }
diff --git a/src/main/java/net/pl3x/purpur/controller/FlyingMoveControllerWASD.java b/src/main/java/net/pl3x/purpur/controller/FlyingMoveControllerWASD.java
new file mode 100644
index 0000000000000000000000000000000000000000..d0f6c7d47d158b49fb6a5fe2a7480ffd53f844c5
--- /dev/null
+++ b/src/main/java/net/pl3x/purpur/controller/FlyingMoveControllerWASD.java
@@ -0,0 +1,61 @@
+package net.pl3x.purpur.controller;
+
+import net.minecraft.world.entity.Mob;
+import net.minecraft.world.entity.ai.attributes.Attributes;
+import net.minecraft.world.entity.player.Player;
+
+public class FlyingMoveControllerWASD extends MoveControllerWASD {
+    protected final float groundSpeedModifier;
+    protected int tooHighCooldown = 0;
+    protected boolean setGravityFlag = true;
+
+    public FlyingMoveControllerWASD(Mob entity) {
+        this(entity, 1.0F);
+    }
+
+    public FlyingMoveControllerWASD(Mob entity, float groundSpeedModifier) {
+        this(entity, groundSpeedModifier, true);
+    }
+
+    public FlyingMoveControllerWASD(Mob entity, float groundSpeedModifier, boolean setGravityFlag) {
+        super(entity);
+        this.groundSpeedModifier = groundSpeedModifier;
+        this.setGravityFlag = setGravityFlag;
+    }
+
+    @Override
+    public void purpurTick(Player rider) {
+        float forward = Math.max(0.0F, rider.getForwardMot());
+        float vertical = forward == 0.0F ? 0.0F : -(rider.xRotO / 45.0F);
+        float strafe = rider.getStrafeMot();
+
+        if (rider.jumping && spacebarEvent(entity)) {
+            entity.onSpacebar();
+        }
+
+        if (entity.getY() >= entity.getMaxY() || --tooHighCooldown > 0) {
+            tooHighCooldown = 60;
+            entity.setDeltaMovement(entity.getDeltaMovement().add(0.0D, -0.05D, 0.0D));
+            vertical = 0.0F;
+        }
+
+        setSpeedModifier(entity.getAttributeValue(Attributes.MOVEMENT_SPEED));
+        float speed = (float) getSpeedModifier();
+
+        if (entity.onGround) {
+            speed *= groundSpeedModifier; // TODO = fix this!
+        }
+
+        if (setGravityFlag) {
+            entity.setNoGravity(forward > 0);
+        }
+
+        entity.setSpeed(speed);
+        entity.setVerticalMot(vertical);
+        entity.setStrafeMot(strafe);
+        entity.setForwardMot(forward);
+
+        setForward(entity.getForwardMot());
+        setStrafe(entity.getStrafeMot());
+    }
+}
diff --git a/src/main/java/net/pl3x/purpur/controller/FlyingWithSpacebarMoveControllerWASD.java b/src/main/java/net/pl3x/purpur/controller/FlyingWithSpacebarMoveControllerWASD.java
new file mode 100644
index 0000000000000000000000000000000000000000..79d23cb66bc825fe78ecc249f12ef8b9851e09f8
--- /dev/null
+++ b/src/main/java/net/pl3x/purpur/controller/FlyingWithSpacebarMoveControllerWASD.java
@@ -0,0 +1,61 @@
+package net.pl3x.purpur.controller;
+
+import net.minecraft.world.entity.Mob;
+import net.minecraft.world.entity.ai.attributes.Attributes;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.phys.Vec3;
+
+public class FlyingWithSpacebarMoveControllerWASD extends FlyingMoveControllerWASD {
+    public FlyingWithSpacebarMoveControllerWASD(Mob entity) {
+        super(entity);
+    }
+
+    public FlyingWithSpacebarMoveControllerWASD(Mob entity, float groundSpeedModifier) {
+        super(entity, groundSpeedModifier);
+    }
+
+    @Override
+    public void purpurTick(Player rider) {
+        float forward = rider.getForwardMot();
+        float strafe = rider.getStrafeMot() * 0.5F;
+        float vertical = 0;
+
+        if (forward < 0.0F) {
+            forward *= 0.5F;
+            strafe *= 0.5F;
+        }
+
+        float speed = (float) entity.getAttributeValue(Attributes.MOVEMENT_SPEED);
+
+        if (entity.onGround) {
+            speed *= groundSpeedModifier;
+        }
+
+        if (rider.jumping && spacebarEvent(entity) && !entity.onSpacebar()) {
+            entity.setNoGravity(true);
+            vertical = 1.0F;
+        } else {
+            entity.setNoGravity(false);
+        }
+
+        if (entity.getY() >= entity.getMaxY() || --tooHighCooldown > 0) {
+            tooHighCooldown = 60;
+            entity.setDeltaMovement(entity.getDeltaMovement().add(0.0D, -0.2D, 0.0D));
+            vertical = 0.0F;
+        }
+
+        setSpeedModifier(speed);
+        entity.setSpeed((float) getSpeedModifier());
+        entity.setVerticalMot(vertical);
+        entity.setStrafeMot(strafe);
+        entity.setForwardMot(forward);
+
+        setForward(entity.getForwardMot());
+        setStrafe(entity.getStrafeMot());
+
+        Vec3 mot = entity.getDeltaMovement();
+        if (mot.y > 0.2D) {
+            entity.setDeltaMovement(mot.x, 0.2D, mot.z);
+        }
+    }
+}
diff --git a/src/main/java/net/pl3x/purpur/controller/LookControllerWASD.java b/src/main/java/net/pl3x/purpur/controller/LookControllerWASD.java
new file mode 100644
index 0000000000000000000000000000000000000000..b93fe26c357930d5c0242da8137443feb216f9dd
--- /dev/null
+++ b/src/main/java/net/pl3x/purpur/controller/LookControllerWASD.java
@@ -0,0 +1,76 @@
+package net.pl3x.purpur.controller;
+
+
+import net.minecraft.network.protocol.game.ClientboundMoveEntityPacket;
+import net.minecraft.util.Mth;
+import net.minecraft.world.entity.Mob;
+import net.minecraft.world.entity.ai.control.LookControl;
+import net.minecraft.world.entity.player.Player;
+
+public class LookControllerWASD extends LookControl {
+    protected final Mob entity;
+    private float yOffset = 0;
+    private float xOffset = 0;
+
+    public LookControllerWASD(Mob entity) {
+        super(entity);
+        this.entity = entity;
+    }
+
+    // tick
+    @Override
+    public void tick() {
+        if (entity.getRider() != null) {
+            purpurTick(entity.getRider());
+        } else {
+            vanillaTick();
+        }
+    }
+
+    protected void purpurTick(Player rider) {
+        setYawPitch(rider.getYRot(), rider.getXRot());
+    }
+
+    public void vanillaTick() {
+        super.tick();
+    }
+
+    public void setYawPitch(float yRot, float xRot) {
+        entity.setXRot(normalizePitch(xRot + xOffset));
+        entity.setYRot(normalizeYaw(yRot + yOffset));
+        entity.setYHeadRot(entity.getYRot());
+        entity.xRotO = entity.getXRot();
+        entity.yRotO = entity.getYRot();
+
+        entity.tracker.broadcast(new ClientboundMoveEntityPacket
+                .PosRot(entity.getId(),
+                (short) 0, (short) 0, (short) 0,
+                (byte) Mth.floor(entity.getYRot() * 256.0F / 360.0F),
+                (byte) Mth.floor(entity.getXRot() * 256.0F / 360.0F),
+                entity.onGround));
+    }
+
+    public void setOffsets(float yaw, float pitch) {
+        yOffset = yaw;
+        xOffset = pitch;
+    }
+
+    public float normalizeYaw(float yaw) {
+        yaw %= 360.0f;
+        if (yaw >= 180.0f) {
+            yaw -= 360.0f;
+        } else if (yaw < -180.0f) {
+            yaw += 360.0f;
+        }
+        return yaw;
+    }
+
+    public float normalizePitch(float pitch) {
+        if (pitch > 90.0f) {
+            pitch = 90.0f;
+        } else if (pitch < -90.0f) {
+            pitch = -90.0f;
+        }
+        return pitch;
+    }
+}
diff --git a/src/main/java/net/pl3x/purpur/controller/MoveControllerWASD.java b/src/main/java/net/pl3x/purpur/controller/MoveControllerWASD.java
new file mode 100644
index 0000000000000000000000000000000000000000..4a3a1eca04a58682d5aeb720832bad5cc8ba2d9c
--- /dev/null
+++ b/src/main/java/net/pl3x/purpur/controller/MoveControllerWASD.java
@@ -0,0 +1,89 @@
+package net.pl3x.purpur.controller;
+
+import net.minecraft.world.entity.Mob;
+import net.minecraft.world.entity.ai.attributes.Attributes;
+import net.minecraft.world.entity.ai.control.MoveControl;
+import net.minecraft.world.entity.player.Player;
+import net.pl3x.purpur.event.entity.RidableSpacebarEvent;
+
+public class MoveControllerWASD extends MoveControl {
+    protected final Mob entity;
+    private final double speedModifier;
+
+    public MoveControllerWASD(Mob entity) {
+        this(entity, 1.0D);
+    }
+
+    public MoveControllerWASD(Mob entity, double speedModifier) {
+        super(entity);
+        this.entity = entity;
+        this.speedModifier = speedModifier;
+    }
+
+    @Override
+    public boolean hasWanted() {
+        return entity.getRider() != null ? strafeForwards != 0 || strafeRight != 0 : super.hasWanted();
+    }
+
+    @Override
+    public void tick() {
+        if (entity.getRider() != null) {
+            purpurTick(entity.getRider());
+        } else {
+            vanillaTick();
+        }
+    }
+
+    public void vanillaTick() {
+        super.tick();
+    }
+
+    public void purpurTick(Player rider) {
+        float forward = rider.getForwardMot() * 0.5F;
+        float strafe = rider.getStrafeMot() * 0.25F;
+
+        if (forward <= 0.0F) {
+            forward *= 0.5F;
+        }
+
+        float yawOffset = 0;
+        if (strafe != 0) {
+            if (forward == 0) {
+                yawOffset += strafe > 0 ? -90 : 90;
+                forward = Math.abs(strafe * 2);
+            } else {
+                yawOffset += strafe > 0 ? -30 : 30;
+                strafe /= 2;
+                if (forward < 0) {
+                    yawOffset += strafe > 0 ? -110 : 110;
+                    forward *= -1;
+                }
+            }
+        } else if (forward < 0) {
+            yawOffset -= 180;
+            forward *= -1;
+        }
+
+        ((LookControllerWASD) entity.getLookControl()).setOffsets(yawOffset, 0);
+
+        if (rider.jumping && spacebarEvent(entity) && !entity.onSpacebar() && entity.onGround) {
+            entity.jumpFromGround();
+        }
+
+        setSpeedModifier(entity.getAttributeValue(Attributes.MOVEMENT_SPEED) * speedModifier);
+
+        entity.setSpeed((float) getSpeedModifier());
+        entity.setForwardMot(forward);
+
+        setForward(entity.getForwardMot());
+        setStrafe(entity.getStrafeMot());
+    }
+
+    public static boolean spacebarEvent(Mob entity) {
+        if (RidableSpacebarEvent.getHandlerList().getRegisteredListeners().length > 0) {
+            return new RidableSpacebarEvent(entity.getBukkitEntity()).callEvent();
+        } else {
+            return true;
+        }
+    }
+}
diff --git a/src/main/java/net/pl3x/purpur/controller/WaterMoveControllerWASD.java b/src/main/java/net/pl3x/purpur/controller/WaterMoveControllerWASD.java
new file mode 100644
index 0000000000000000000000000000000000000000..c9b24eeaa4221c56dc5030ad4574499e1a03727d
--- /dev/null
+++ b/src/main/java/net/pl3x/purpur/controller/WaterMoveControllerWASD.java
@@ -0,0 +1,50 @@
+package net.pl3x.purpur.controller;
+
+import net.minecraft.world.entity.Mob;
+import net.minecraft.world.entity.ai.attributes.Attributes;
+import net.minecraft.world.entity.player.Player;
+
+public class WaterMoveControllerWASD extends MoveControllerWASD {
+    private final double speedModifier;
+
+    public WaterMoveControllerWASD(Mob entity) {
+        this(entity, 1.0D);
+    }
+
+    public WaterMoveControllerWASD(Mob entity, double speedModifier) {
+        super(entity);
+        this.speedModifier = speedModifier;
+    }
+
+    @Override
+    public void purpurTick(Player rider) {
+        float forward = rider.getForwardMot();
+        float strafe = rider.getStrafeMot() * 0.5F; // strafe slower by default
+        float vertical = -(rider.xRotO / 90);
+
+        if (forward == 0.0F) {
+            // strafe slower if not moving forward
+            strafe *= 0.5F;
+            // do not move vertically if not moving forward
+            vertical = 0.0F;
+        } else if (forward < 0.0F) {
+            // water animals can't swim backwards
+            forward = 0.0F;
+            vertical = 0.0F;
+        }
+
+        if (rider.jumping && spacebarEvent(entity)) {
+            entity.onSpacebar();
+        }
+
+        setSpeedModifier(entity.getAttributeValue(Attributes.MOVEMENT_SPEED) * speedModifier);
+        entity.setSpeed((float) getSpeedModifier() * 0.1F);
+
+        entity.setForwardMot(forward * (float) speedModifier);
+        entity.setStrafeMot(strafe * (float) speedModifier);
+        entity.setVerticalMot(vertical * (float) speedModifier);
+
+        setForward(entity.getForwardMot());
+        setStrafe(entity.getStrafeMot());
+    }
+}
diff --git a/src/main/java/net/pl3x/purpur/entity/DolphinSpit.java b/src/main/java/net/pl3x/purpur/entity/DolphinSpit.java
new file mode 100644
index 0000000000000000000000000000000000000000..fda24d4ecff91cc28d2a7a45fbb55ea734261153
--- /dev/null
+++ b/src/main/java/net/pl3x/purpur/entity/DolphinSpit.java
@@ -0,0 +1,99 @@
+package net.pl3x.purpur.entity;
+
+import net.minecraft.core.particles.ParticleTypes;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.util.Mth;
+import net.minecraft.world.damagesource.DamageSource;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.entity.animal.Dolphin;
+import net.minecraft.world.entity.projectile.LlamaSpit;
+import net.minecraft.world.entity.projectile.ProjectileUtil;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.phys.BlockHitResult;
+import net.minecraft.world.phys.EntityHitResult;
+import net.minecraft.world.phys.HitResult;
+import net.minecraft.world.phys.Vec3;
+
+public class DolphinSpit extends LlamaSpit {
+    public LivingEntity dolphin;
+    public int ticksLived;
+
+    public DolphinSpit(EntityType<? extends LlamaSpit> type, Level world) {
+        super(type, world);
+    }
+
+    public DolphinSpit(Level world, Dolphin dolphin) {
+        this(EntityType.LLAMA_SPIT, world);
+        setOwner(dolphin.getRider() != null ? dolphin.getRider() : dolphin);
+        this.dolphin = dolphin;
+        this.setPos(
+                dolphin.getX() - (double) (dolphin.getBbWidth() + 1.0F) * 0.5D * (double) Mth.sin(dolphin.yBodyRot * 0.017453292F),
+                dolphin.getEyeY() - 0.10000000149011612D,
+                dolphin.getZ() + (double) (dolphin.getBbWidth() + 1.0F) * 0.5D * (double) Mth.cos(dolphin.yBodyRot * 0.017453292F));
+    }
+
+    public void tick() {
+        super_tick();
+
+        Vec3 mot = this.getDeltaMovement();
+        HitResult hitResult = ProjectileUtil.getHitResult(this, this::canHitEntity);
+
+        this.preOnHit(hitResult);
+
+        double x = this.getX() + mot.x;
+        double y = this.getY() + mot.y;
+        double z = this.getZ() + mot.z;
+
+        this.updateRotation();
+
+        Vec3 motDouble = mot.scale(2.0);
+        for (int i = 0; i < 5; i++) {
+            ((ServerLevel) level).sendParticles(null, ParticleTypes.BUBBLE,
+                    getX() + random.nextFloat() / 2 - 0.25F,
+                    getY() + random.nextFloat() / 2 - 0.25F,
+                    getZ() + random.nextFloat() / 2 - 0.25F,
+                    0, motDouble.x(), motDouble.y(), motDouble.z(), 0.1D, true);
+        }
+
+        if (++ticksLived > 20) {
+            this.discard();
+        } else {
+            this.setDeltaMovement(mot.scale(0.99D));
+            if (!this.isNoGravity()) {
+                this.setDeltaMovement(this.getDeltaMovement().add(0.0D, -0.06D, 0.0D));
+            }
+
+            this.setPos(x, y, z);
+        }
+    }
+
+    @Override
+    public void shoot(double x, double y, double z, float speed, float inaccuracy) {
+        setDeltaMovement(new Vec3(x, y, z).normalize().add(
+                random.nextGaussian() * (double) 0.0075F * (double) inaccuracy,
+                random.nextGaussian() * (double) 0.0075F * (double) inaccuracy,
+                random.nextGaussian() * (double) 0.0075F * (double) inaccuracy)
+                .scale(speed));
+    }
+
+    @Override
+    protected void onHitEntity(EntityHitResult entityHitResult) {
+        Entity shooter = this.getOwner();
+        if (shooter instanceof LivingEntity) {
+            entityHitResult.getEntity().hurt(DamageSource.indirectMobAttack(this, (LivingEntity) shooter).setProjectile(), level.purpurConfig.dolphinSpitDamage);
+        }
+    }
+
+    @Override
+    protected void onHitBlock(BlockHitResult blockHitResult) {
+        if (this.hitCancelled) {
+            return;
+        }
+        BlockState state = this.level.getBlockState(blockHitResult.getBlockPos());
+        state.onProjectileHit(this.level, state, blockHitResult, this);
+        this.discard();
+    }
+}
diff --git a/src/main/java/net/pl3x/purpur/entity/PhantomFlames.java b/src/main/java/net/pl3x/purpur/entity/PhantomFlames.java
new file mode 100644
index 0000000000000000000000000000000000000000..b196bee2ea6e67897b367d2be1800f16a89a6167
--- /dev/null
+++ b/src/main/java/net/pl3x/purpur/entity/PhantomFlames.java
@@ -0,0 +1,114 @@
+package net.pl3x.purpur.entity;
+
+import net.minecraft.core.particles.ParticleTypes;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.util.Mth;
+import net.minecraft.world.damagesource.DamageSource;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.entity.decoration.ArmorStand;
+import net.minecraft.world.entity.monster.Phantom;
+import net.minecraft.world.entity.projectile.LlamaSpit;
+import net.minecraft.world.entity.projectile.ProjectileUtil;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.block.state.BlockBehaviour;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.phys.BlockHitResult;
+import net.minecraft.world.phys.EntityHitResult;
+import net.minecraft.world.phys.HitResult;
+import net.minecraft.world.phys.Vec3;
+
+public class PhantomFlames extends LlamaSpit {
+    public Phantom phantom;
+    public int ticksLived;
+    public boolean canGrief = false;
+
+    public PhantomFlames(EntityType<? extends LlamaSpit> type, Level world) {
+        super(type, world);
+    }
+
+    public PhantomFlames(Level world, Phantom phantom) {
+        this(EntityType.LLAMA_SPIT, world);
+        setOwner(phantom.getRider() != null ? phantom.getRider() : phantom);
+        this.phantom = phantom;
+        this.setPos(
+                phantom.getX() - (double) (phantom.getBbWidth() + 1.0F) * 0.5D * (double) Mth.sin(phantom.yBodyRot * 0.017453292F),
+                phantom.getEyeY() - 0.10000000149011612D,
+                phantom.getZ() + (double) (phantom.getBbWidth() + 1.0F) * 0.5D * (double) Mth.cos(phantom.yBodyRot * 0.017453292F));
+    }
+
+    public void tick() {
+        super_tick();
+
+        Vec3 mot = this.getDeltaMovement();
+        HitResult hitResult = ProjectileUtil.getHitResult(this, this::canHitEntity);
+
+        this.preOnHit(hitResult);
+
+        double x = this.getX() + mot.x;
+        double y = this.getY() + mot.y;
+        double z = this.getZ() + mot.z;
+
+        this.updateRotation();
+
+        Vec3 motDouble = mot.scale(2.0);
+        for (int i = 0; i < 5; i++) {
+            ((ServerLevel) level).sendParticles(null, ParticleTypes.FLAME,
+                    getX() + random.nextFloat() / 2 - 0.25F,
+                    getY() + random.nextFloat() / 2 - 0.25F,
+                    getZ() + random.nextFloat() / 2 - 0.25F,
+                    0, motDouble.x(), motDouble.y(), motDouble.z(), 0.1D, true);
+        }
+
+        if (++ticksLived > 20) {
+            this.discard();
+        } else if (this.level.getBlockStates(this.getBoundingBox()).noneMatch(BlockBehaviour.BlockStateBase::isAir)) {
+            this.discard();
+        } else if (this.isInWaterOrBubble()) {
+            this.discard();
+        } else {
+            this.setDeltaMovement(mot.scale(0.99D));
+            if (!this.isNoGravity()) {
+                this.setDeltaMovement(this.getDeltaMovement().add(0.0D, -0.06D, 0.0D));
+            }
+
+            this.setPos(x, y, z);
+        }
+    }
+
+    @Override
+    public void shoot(double x, double y, double z, float speed, float inaccuracy) {
+        setDeltaMovement(new Vec3(x, y, z).normalize().add(
+                random.nextGaussian() * (double) 0.0075F * (double) inaccuracy,
+                random.nextGaussian() * (double) 0.0075F * (double) inaccuracy,
+                random.nextGaussian() * (double) 0.0075F * (double) inaccuracy)
+                .scale(speed));
+    }
+
+    @Override
+    protected void onHitEntity(EntityHitResult entityHitResult) {
+        Entity shooter = this.getOwner();
+        if (shooter instanceof LivingEntity) {
+            Entity target = entityHitResult.getEntity();
+            if (canGrief || (target instanceof LivingEntity && !(target instanceof ArmorStand))) {
+                boolean hurt = target.hurt(DamageSource.indirectMobAttack(this, (LivingEntity) shooter).setProjectile(), level.purpurConfig.phantomFlameDamage);
+                if (hurt && level.purpurConfig.phantomFlameFireTime > 0) {
+                    target.setSecondsOnFire(level.purpurConfig.phantomFlameFireTime);
+                }
+            }
+        }
+    }
+
+    @Override
+    protected void onHitBlock(BlockHitResult blockHitResult) {
+        if (this.hitCancelled) {
+            return;
+        }
+        if (this.canGrief) {
+            BlockState state = this.level.getBlockState(blockHitResult.getBlockPos());
+            state.onProjectileHit(this.level, state, blockHitResult, this);
+        }
+        this.discard();
+    }
+}
diff --git a/src/main/java/net/pl3x/purpur/entity/ai/HasRider.java b/src/main/java/net/pl3x/purpur/entity/ai/HasRider.java
new file mode 100644
index 0000000000000000000000000000000000000000..bd48d16bbe3d9b10541a5e65c037926b3a58af22
--- /dev/null
+++ b/src/main/java/net/pl3x/purpur/entity/ai/HasRider.java
@@ -0,0 +1,20 @@
+package net.pl3x.purpur.entity.ai;
+
+import net.minecraft.world.entity.Mob;
+import net.minecraft.world.entity.ai.goal.Goal;
+
+import java.util.EnumSet;
+
+public class HasRider extends Goal {
+    public final Mob entity;
+
+    public HasRider(Mob entity) {
+        this.entity = entity;
+        setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK, Flag.TARGET, Flag.UNKNOWN_BEHAVIOR));
+    }
+
+    @Override
+    public boolean canUse() {
+        return entity.getRider() != null;
+    }
+}
diff --git a/src/main/java/net/pl3x/purpur/entity/ai/HorseHasRider.java b/src/main/java/net/pl3x/purpur/entity/ai/HorseHasRider.java
new file mode 100644
index 0000000000000000000000000000000000000000..8eefb7b7eb33aecf48ac206d3f0139e0cf342e35
--- /dev/null
+++ b/src/main/java/net/pl3x/purpur/entity/ai/HorseHasRider.java
@@ -0,0 +1,17 @@
+package net.pl3x.purpur.entity.ai;
+
+import net.minecraft.world.entity.animal.horse.AbstractHorse;
+
+public class HorseHasRider extends HasRider {
+    public final AbstractHorse horse;
+
+    public HorseHasRider(AbstractHorse entity) {
+        super(entity);
+        this.horse = entity;
+    }
+
+    @Override
+    public boolean canUse() {
+        return super.canUse() && horse.isSaddled();
+    }
+}
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
index 8ce49478441e77cedf5148ecb81d78b32660329e..fb5cd6fe7903f3f96af1abb55832bafb7ca7f4a8 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
@@ -1234,4 +1234,27 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
         return getHandle().isTicking();
     }
     // Paper end
+
+    // Purpur start
+    @Override
+    public org.bukkit.entity.Player getRider() {
+        Player rider = getHandle().getRider();
+        return rider != null ? (org.bukkit.entity.Player) rider.getBukkitEntity() : null;
+    }
+
+    @Override
+    public boolean hasRider() {
+        return getHandle().getRider() != null;
+    }
+
+    @Override
+    public boolean isRidable() {
+        return getHandle().isRidable();
+    }
+
+    @Override
+    public boolean isRidableInWater() {
+        return getHandle().rideableUnderWater();
+    }
+    // Purpur end
 }
diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
index db6c0917824b9127abd9f9250dc195a9f1d56f4f..89ad1c69456fb4aaf2ab8742c76343b30b74ecd0 100644
--- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
+++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
@@ -537,6 +537,15 @@ public class CraftEventFactory {
         }
         craftServer.getPluginManager().callEvent(event);
 
+        // Purpur start
+        if (who != null) {
+            switch (action) {
+                case LEFT_CLICK_BLOCK, LEFT_CLICK_AIR -> who.processClick(InteractionHand.MAIN_HAND);
+                case RIGHT_CLICK_BLOCK, RIGHT_CLICK_AIR -> who.processClick(InteractionHand.OFF_HAND);
+            }
+        }
+        // Purpur end
+
         return event;
     }
 
@@ -960,6 +969,7 @@ public class CraftEventFactory {
                     damageCause = DamageCause.ENTITY_EXPLOSION;
                 }
                 event = new EntityDamageByEntityEvent(damager.getBukkitEntity(), entity.getBukkitEntity(), damageCause, modifiers, modifierFunctions);
+                damager.processClick(InteractionHand.MAIN_HAND); // Purpur
             }
             event.setCancelled(cancelled);
 
@@ -1045,6 +1055,7 @@ public class CraftEventFactory {
             if (!event.isCancelled()) {
                 event.getEntity().setLastDamageCause(event);
             }
+            damager.getHandle().processClick(InteractionHand.MAIN_HAND); // Purpur
             return event;
         }
 
@@ -1096,6 +1107,7 @@ public class CraftEventFactory {
         EntityDamageEvent event;
         if (damager != null) {
             event = new EntityDamageByEntityEvent(damager.getBukkitEntity(), damagee.getBukkitEntity(), cause, modifiers, modifierFunctions);
+            damager.processClick(InteractionHand.MAIN_HAND); // Purpur
         } else {
             event = new EntityDamageEvent(damagee.getBukkitEntity(), cause, modifiers, modifierFunctions);
         }
diff --git a/src/test/java/com/destroystokyo/paper/entity/ai/VanillaMobGoalTest.java b/src/test/java/com/destroystokyo/paper/entity/ai/VanillaMobGoalTest.java
index b2d510459bcf90a3611f3d91dae4ccc3d29b4079..2ddf338f40092f8ef1db3ea5c175300471566d51 100644
--- a/src/test/java/com/destroystokyo/paper/entity/ai/VanillaMobGoalTest.java
+++ b/src/test/java/com/destroystokyo/paper/entity/ai/VanillaMobGoalTest.java
@@ -37,7 +37,7 @@ public class VanillaMobGoalTest {
         }
 
         List<Class<?>> classes;
-        try (ScanResult scanResult = new ClassGraph().enableAllInfo().whitelistPackages("net.minecraft").scan()) {
+        try (ScanResult scanResult = new ClassGraph().enableAllInfo().whitelistPackages("net.minecraft", "net.pl3x.purpur.entity.ai").scan()) { // Purpur
             classes = scanResult.getSubclasses(net.minecraft.world.entity.ai.goal.Goal.class.getName()).loadClasses();
         }
 
