--- a/net/minecraft/world/entity/vehicle/AbstractMinecart.java
+++ b/net/minecraft/world/entity/vehicle/AbstractMinecart.java
@@ -93,6 +_,17 @@
             }
         )
     );
+    // CraftBukkit start
+    public boolean slowWhenEmpty = true;
+    private double derailedX = 0.5;
+    private double derailedY = 0.5;
+    private double derailedZ = 0.5;
+    private double flyingX = 0.95;
+    private double flyingY = 0.95;
+    private double flyingZ = 0.95;
+    public @Nullable Double maxSpeed;
+    public net.kyori.adventure.util.TriState frictionState = net.kyori.adventure.util.TriState.NOT_SET; // Paper - Friction API
+    // CraftBukkit end
 
     protected AbstractMinecart(EntityType<?> entityType, Level level) {
         super(entityType, level);
@@ -152,11 +_,19 @@
 
     @Override
     public boolean canCollideWith(Entity entity) {
-        return AbstractBoat.canVehicleCollide(this, entity);
+        // Paper start - fix VehicleEntityCollisionEvent not called when colliding with player
+        boolean collides = AbstractBoat.canVehicleCollide(this, entity);
+        if (!collides) {
+            return false;
+        }
+
+        org.bukkit.event.vehicle.VehicleEntityCollisionEvent collisionEvent = new org.bukkit.event.vehicle.VehicleEntityCollisionEvent((org.bukkit.entity.Vehicle) this.getBukkitEntity(), entity.getBukkitEntity());
+        return collisionEvent.callEvent();
+        // Paper end - fix VehicleEntityCollisionEvent not called when colliding with player
     }
 
     @Override
-    public boolean isPushable() {
+    public boolean isCollidable(boolean ignoreClimbing) { // Paper - Climbing should not bypass cramming gamerule
         return true;
     }
 
@@ -257,6 +_,14 @@
 
     @Override
     public void tick() {
+        // CraftBukkit start
+        double prevX = this.getX();
+        double prevY = this.getY();
+        double prevZ = this.getZ();
+        float prevYaw = this.getYRot();
+        float prevPitch = this.getXRot();
+        // CraftBukkit end
+
         if (this.getHurtTime() > 0) {
             this.setHurtTime(this.getHurtTime() - 1);
         }
@@ -266,8 +_,20 @@
         }
 
         this.checkBelowWorld();
-        this.handlePortal();
+        // this.handlePortal(); // CraftBukkit - handled in postTick
         this.behavior.tick();
+        // CraftBukkit start
+        org.bukkit.World bworld = this.level().getWorld();
+        org.bukkit.Location from = new org.bukkit.Location(bworld, prevX, prevY, prevZ, prevYaw, prevPitch);
+        org.bukkit.Location to = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(this.position(), this.level(), this.getYRot(), this.getXRot());
+        org.bukkit.entity.Vehicle vehicle = (org.bukkit.entity.Vehicle) this.getBukkitEntity();
+
+        new org.bukkit.event.vehicle.VehicleUpdateEvent(vehicle).callEvent();
+
+        if (!from.equals(to)) {
+            new org.bukkit.event.vehicle.VehicleMoveEvent(vehicle, from, to).callEvent();
+        }
+        // CraftBukkit end
         this.updateInWaterStateAndDoFluidPushing();
         if (this.isInLava()) {
             this.lavaIgnite();
@@ -355,12 +_,16 @@
         Vec3 deltaMovement = this.getDeltaMovement();
         this.setDeltaMovement(Mth.clamp(deltaMovement.x, -maxSpeed, maxSpeed), deltaMovement.y, Mth.clamp(deltaMovement.z, -maxSpeed, maxSpeed));
         if (this.onGround()) {
-            this.setDeltaMovement(this.getDeltaMovement().scale(0.5));
+            // CraftBukkit start - replace magic numbers with our variables
+            this.setDeltaMovement(new Vec3(this.getDeltaMovement().x * this.derailedX, this.getDeltaMovement().y * this.derailedY, this.getDeltaMovement().z * this.derailedZ));
+            // CraftBukkit end
         }
 
         this.move(MoverType.SELF, this.getDeltaMovement());
         if (!this.onGround()) {
-            this.setDeltaMovement(this.getDeltaMovement().scale(0.95));
+            // CraftBukkit start - replace magic numbers with our variables
+            this.setDeltaMovement(new Vec3(this.getDeltaMovement().x * this.flyingX, this.getDeltaMovement().y * this.flyingY, this.getDeltaMovement().z * this.flyingZ));
+            // CraftBukkit end
         }
     }
 
@@ -462,6 +_,15 @@
         this.setDisplayOffset(input.getIntOr("DisplayOffset", this.getDefaultDisplayOffset()));
         this.flipped = input.getBooleanOr("FlippedRotation", false);
         this.firstTick = input.getBooleanOr("HasTicked", false);
+        // Paper start - Friction API
+        input.getString("Paper.FrictionState").ifPresent(frictionState -> {
+            try {
+                this.frictionState = net.kyori.adventure.util.TriState.valueOf(frictionState);
+            } catch (Exception ignored) {
+                com.mojang.logging.LogUtils.getLogger().error("Unknown friction state {} for {}", frictionState, this);
+            }
+        });
+        // Paper end - Friction API
     }
 
     @Override
@@ -474,13 +_,26 @@
 
         output.putBoolean("FlippedRotation", this.flipped);
         output.putBoolean("HasTicked", this.firstTick);
+        // Paper start - Friction API
+        if (this.frictionState != net.kyori.adventure.util.TriState.NOT_SET) {
+            output.putString("Paper.FrictionState", this.frictionState.toString());
+        }
+        // Paper end - Friction API
     }
 
     @Override
     public void push(Entity entity) {
         if (!this.level().isClientSide()) {
             if (!entity.noPhysics && !this.noPhysics) {
+                if (!this.level().paperConfig().collisions.allowVehicleCollisions && this.level().paperConfig().collisions.onlyPlayersCollide && !(entity instanceof Player)) return; // Paper - Collision option for requiring a player participant
                 if (!this.hasPassenger(entity)) {
+                    // CraftBukkit start
+                    org.bukkit.event.vehicle.VehicleEntityCollisionEvent collisionEvent = new org.bukkit.event.vehicle.VehicleEntityCollisionEvent(
+                        (org.bukkit.entity.Vehicle) this.getBukkitEntity(),
+                        entity.getBukkitEntity()
+                    );
+                    if (!collisionEvent.callEvent()) return;
+                    // CraftBukkit end
                     double d = entity.getX() - this.getX();
                     double d1 = entity.getZ() - this.getZ();
                     double d2 = d * d + d1 * d1;
@@ -589,4 +_,26 @@
     public boolean isFurnace() {
         return false;
     }
+
+    // CraftBukkit start - Methods for getting and setting flying and derailed velocity modifiers
+    public org.bukkit.util.Vector getFlyingVelocityMod() {
+        return new org.bukkit.util.Vector(this.flyingX, this.flyingY, this.flyingZ);
+    }
+
+    public void setFlyingVelocityMod(org.bukkit.util.Vector flying) {
+        this.flyingX = flying.getX();
+        this.flyingY = flying.getY();
+        this.flyingZ = flying.getZ();
+    }
+
+    public org.bukkit.util.Vector getDerailedVelocityMod() {
+        return new org.bukkit.util.Vector(this.derailedX, this.derailedY, this.derailedZ);
+    }
+
+    public void setDerailedVelocityMod(org.bukkit.util.Vector derailed) {
+        this.derailedX = derailed.getX();
+        this.derailedY = derailed.getY();
+        this.derailedZ = derailed.getZ();
+    }
+    // CraftBukkit end
 }
