From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Aikar <aikar@aikar.co>
Date: Fri, 13 May 2016 01:38:06 -0400
Subject: [PATCH] Entity Activation Range 2.0

Optimizes performance of Activation Range

Adds many new configurations and a new wake up inactive system

Fixes and adds new Immunities to improve gameplay behavior

Adds water Mobs to activation range config and nerfs fish
Adds flying monsters to control ghast and phantoms
Adds villagers as separate config

diff --git a/io/papermc/paper/entity/activation/ActivationRange.java b/io/papermc/paper/entity/activation/ActivationRange.java
new file mode 100644
index 0000000000000000000000000000000000000000..ae2bb9a73106febfe5f0d090abd4252bbb5fd27e
--- /dev/null
+++ b/io/papermc/paper/entity/activation/ActivationRange.java
@@ -0,0 +1,334 @@
+package io.papermc.paper.entity.activation;
+
+import net.minecraft.core.BlockPos;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.entity.ExperienceOrb;
+import net.minecraft.world.entity.LightningBolt;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.entity.Marker;
+import net.minecraft.world.entity.Mob;
+import net.minecraft.world.entity.ai.Brain;
+import net.minecraft.world.entity.animal.Animal;
+import net.minecraft.world.entity.animal.Bee;
+import net.minecraft.world.entity.animal.sheep.Sheep;
+import net.minecraft.world.entity.animal.horse.Llama;
+import net.minecraft.world.entity.boss.EnderDragonPart;
+import net.minecraft.world.entity.boss.enderdragon.EndCrystal;
+import net.minecraft.world.entity.boss.enderdragon.EnderDragon;
+import net.minecraft.world.entity.boss.wither.WitherBoss;
+import net.minecraft.world.entity.item.FallingBlockEntity;
+import net.minecraft.world.entity.item.ItemEntity;
+import net.minecraft.world.entity.item.PrimedTnt;
+import net.minecraft.world.entity.monster.Creeper;
+import net.minecraft.world.entity.monster.Pillager;
+import net.minecraft.world.entity.npc.Villager;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.entity.projectile.AbstractArrow;
+import net.minecraft.world.entity.projectile.AbstractHurtingProjectile;
+import net.minecraft.world.entity.projectile.EyeOfEnder;
+import net.minecraft.world.entity.projectile.FireworkRocketEntity;
+import net.minecraft.world.entity.projectile.ThrowableProjectile;
+import net.minecraft.world.entity.projectile.ThrownTrident;
+import net.minecraft.world.entity.schedule.Activity;
+import net.minecraft.world.entity.vehicle.AbstractBoat;
+import net.minecraft.world.entity.vehicle.AbstractMinecart;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.phys.AABB;
+import org.spigotmc.SpigotWorldConfig;
+import java.util.List;
+import java.util.Set;
+
+public final class ActivationRange {
+
+    private ActivationRange() {
+    }
+
+    static Activity[] VILLAGER_PANIC_IMMUNITIES = {
+        Activity.HIDE,
+        Activity.PRE_RAID,
+        Activity.RAID,
+        Activity.PANIC
+    };
+
+    private static int checkInactiveWakeup(final Entity entity) {
+        final Level world = entity.level();
+        final SpigotWorldConfig config = world.spigotConfig;
+        final long inactiveFor = MinecraftServer.currentTick - entity.activatedTick;
+        if (entity.activationType == ActivationType.VILLAGER) {
+            if (inactiveFor > config.wakeUpInactiveVillagersEvery && world.wakeupInactiveRemainingVillagers > 0) {
+                world.wakeupInactiveRemainingVillagers--;
+                return config.wakeUpInactiveVillagersFor;
+            }
+        } else if (entity.activationType == ActivationType.ANIMAL) {
+            if (inactiveFor > config.wakeUpInactiveAnimalsEvery && world.wakeupInactiveRemainingAnimals > 0) {
+                world.wakeupInactiveRemainingAnimals--;
+                return config.wakeUpInactiveAnimalsFor;
+            }
+        } else if (entity.activationType == ActivationType.FLYING_MONSTER) {
+            if (inactiveFor > config.wakeUpInactiveFlyingEvery && world.wakeupInactiveRemainingFlying > 0) {
+                world.wakeupInactiveRemainingFlying--;
+                return config.wakeUpInactiveFlyingFor;
+            }
+        } else if (entity.activationType == ActivationType.MONSTER || entity.activationType == ActivationType.RAIDER) {
+            if (inactiveFor > config.wakeUpInactiveMonstersEvery && world.wakeupInactiveRemainingMonsters > 0) {
+                world.wakeupInactiveRemainingMonsters--;
+                return config.wakeUpInactiveMonstersFor;
+            }
+        }
+        return -1;
+    }
+
+    static AABB maxBB = new AABB(0, 0, 0, 0, 0, 0);
+
+    /**
+     * These entities are excluded from Activation range checks.
+     *
+     * @param entity
+     * @param config
+     * @return boolean If it should always tick.
+     */
+    public static boolean initializeEntityActivationState(final Entity entity, final SpigotWorldConfig config) {
+        return (entity.activationType == ActivationType.MISC && config.miscActivationRange <= 0)
+            || (entity.activationType == ActivationType.RAIDER && config.raiderActivationRange <= 0)
+            || (entity.activationType == ActivationType.ANIMAL && config.animalActivationRange <= 0)
+            || (entity.activationType == ActivationType.MONSTER && config.monsterActivationRange <= 0)
+            || (entity.activationType == ActivationType.VILLAGER && config.villagerActivationRange <= 0)
+            || (entity.activationType == ActivationType.WATER && config.waterActivationRange <= 0)
+            || (entity.activationType == ActivationType.FLYING_MONSTER && config.flyingMonsterActivationRange <= 0)
+            || entity instanceof EyeOfEnder
+            || entity instanceof Player
+            || entity instanceof ThrowableProjectile
+            || entity instanceof EnderDragon
+            || entity instanceof EnderDragonPart
+            || entity instanceof WitherBoss
+            || entity instanceof AbstractHurtingProjectile
+            || entity instanceof LightningBolt
+            || entity instanceof PrimedTnt
+            || entity instanceof FallingBlockEntity
+            || entity instanceof AbstractMinecart
+            || entity instanceof AbstractBoat
+            || entity instanceof EndCrystal
+            || entity instanceof FireworkRocketEntity
+            || entity instanceof ThrownTrident;
+    }
+
+    /**
+     * Find what entities are in range of the players in the world and set
+     * active if in range.
+     *
+     * @param world
+     */
+    public static void activateEntities(final Level world) {
+        final int miscActivationRange = world.spigotConfig.miscActivationRange;
+        final int raiderActivationRange = world.spigotConfig.raiderActivationRange;
+        final int animalActivationRange = world.spigotConfig.animalActivationRange;
+        final int monsterActivationRange = world.spigotConfig.monsterActivationRange;
+        final int waterActivationRange = world.spigotConfig.waterActivationRange;
+        final int flyingActivationRange = world.spigotConfig.flyingMonsterActivationRange;
+        final int villagerActivationRange = world.spigotConfig.villagerActivationRange;
+        world.wakeupInactiveRemainingAnimals = Math.min(world.wakeupInactiveRemainingAnimals + 1, world.spigotConfig.wakeUpInactiveAnimals);
+        world.wakeupInactiveRemainingVillagers = Math.min(world.wakeupInactiveRemainingVillagers + 1, world.spigotConfig.wakeUpInactiveVillagers);
+        world.wakeupInactiveRemainingMonsters = Math.min(world.wakeupInactiveRemainingMonsters + 1, world.spigotConfig.wakeUpInactiveMonsters);
+        world.wakeupInactiveRemainingFlying = Math.min(world.wakeupInactiveRemainingFlying + 1, world.spigotConfig.wakeUpInactiveFlying);
+
+        int maxRange = Math.max(monsterActivationRange, animalActivationRange);
+        maxRange = Math.max(maxRange, raiderActivationRange);
+        maxRange = Math.max(maxRange, miscActivationRange);
+        maxRange = Math.max(maxRange, flyingActivationRange);
+        maxRange = Math.max(maxRange, waterActivationRange);
+        maxRange = Math.max(maxRange, villagerActivationRange);
+        maxRange = Math.min((world.spigotConfig.simulationDistance << 4) - 8, maxRange);
+
+        for (final Player player : world.players()) {
+            player.activatedTick = MinecraftServer.currentTick;
+            if (world.spigotConfig.ignoreSpectatorActivation && player.isSpectator()) {
+                continue;
+            }
+
+            final int worldHeight = world.getHeight();
+            ActivationRange.maxBB = player.getBoundingBox().inflate(maxRange, worldHeight, maxRange);
+            ActivationType.MISC.boundingBox = player.getBoundingBox().inflate(miscActivationRange, worldHeight, miscActivationRange);
+            ActivationType.RAIDER.boundingBox = player.getBoundingBox().inflate(raiderActivationRange, worldHeight, raiderActivationRange);
+            ActivationType.ANIMAL.boundingBox = player.getBoundingBox().inflate(animalActivationRange, worldHeight, animalActivationRange);
+            ActivationType.MONSTER.boundingBox = player.getBoundingBox().inflate(monsterActivationRange, worldHeight, monsterActivationRange);
+            ActivationType.WATER.boundingBox = player.getBoundingBox().inflate(waterActivationRange, worldHeight, waterActivationRange);
+            ActivationType.FLYING_MONSTER.boundingBox = player.getBoundingBox().inflate(flyingActivationRange, worldHeight, flyingActivationRange);
+            ActivationType.VILLAGER.boundingBox = player.getBoundingBox().inflate(villagerActivationRange, worldHeight, villagerActivationRange);
+
+            final List<Entity> entities = world.getEntities((Entity) null, ActivationRange.maxBB, e -> true);
+            final boolean tickMarkers = world.paperConfig().entities.markers.tick;
+            for (final Entity entity : entities) {
+                if (!tickMarkers && entity instanceof Marker) {
+                    continue;
+                }
+
+                ActivationRange.activateEntity(entity);
+            }
+        }
+    }
+
+    /**
+     * Tries to activate an entity.
+     *
+     * @param entity
+     */
+    private static void activateEntity(final Entity entity) {
+        if (MinecraftServer.currentTick > entity.activatedTick) {
+            if (entity.defaultActivationState) {
+                entity.activatedTick = MinecraftServer.currentTick;
+                return;
+            }
+            if (entity.activationType.boundingBox.intersects(entity.getBoundingBox())) {
+                entity.activatedTick = MinecraftServer.currentTick;
+            }
+        }
+    }
+
+    /**
+     * If an entity is not in range, do some more checks to see if we should
+     * give it a shot.
+     *
+     * @param entity
+     * @return
+     */
+    public static int checkEntityImmunities(final Entity entity) { // return # of ticks to get immunity
+        final SpigotWorldConfig config = entity.level().spigotConfig;
+        final int inactiveWakeUpImmunity = checkInactiveWakeup(entity);
+        if (inactiveWakeUpImmunity > -1) {
+            return inactiveWakeUpImmunity;
+        }
+        if (entity.getRemainingFireTicks() > 0) {
+            return 2;
+        }
+        if (entity.activatedImmunityTick >= MinecraftServer.currentTick) {
+            return 1;
+        }
+        final long inactiveFor = MinecraftServer.currentTick - entity.activatedTick;
+        if ((entity.activationType != ActivationType.WATER && entity.isInWater() && entity.isPushedByFluid())) {
+            return 100;
+        }
+        if (!entity.onGround() || entity.getDeltaMovement().horizontalDistanceSqr() > 9.999999747378752E-6D) {
+            return 100;
+        }
+        if (!(entity instanceof final AbstractArrow arrow)) {
+            if ((!entity.onGround() && !isEntityThatFlies(entity))) {
+                return 10;
+            }
+        } else if (!arrow.isInGround()) {
+            return 1;
+        }
+        // special cases.
+        if (entity instanceof final LivingEntity living) {
+            if (living.onClimbable() || living.jumping || living.hurtTime > 0 || !living.activeEffects.isEmpty() || living.isFreezing()) {
+                return 1;
+            }
+            if (entity instanceof final Mob mob && mob.getTarget() != null) {
+                return 20;
+            }
+            if (entity instanceof final Bee bee) {
+                final BlockPos movingTarget = bee.getMovingTarget();
+                if (bee.isAngry() ||
+                    (bee.getHivePos() != null && bee.getHivePos().equals(movingTarget)) ||
+                    (bee.getSavedFlowerPos() != null && bee.getSavedFlowerPos().equals(movingTarget))
+                ) {
+                    return 20;
+                }
+            }
+            if (entity instanceof final Villager villager) {
+                final Brain<Villager> behaviorController = villager.getBrain();
+
+                if (config.villagersActiveForPanic) {
+                    for (final Activity activity : VILLAGER_PANIC_IMMUNITIES) {
+                        if (behaviorController.isActive(activity)) {
+                            return 20 * 5;
+                        }
+                    }
+                }
+
+                if (config.villagersWorkImmunityAfter > 0 && inactiveFor >= config.villagersWorkImmunityAfter) {
+                    if (behaviorController.isActive(Activity.WORK)) {
+                        return config.villagersWorkImmunityFor;
+                    }
+                }
+            }
+            if (entity instanceof final Llama llama && llama.inCaravan()) {
+                return 1;
+            }
+            if (entity instanceof final Animal animal) {
+                if (animal.isBaby() || animal.isInLove()) {
+                    return 5;
+                }
+                if (entity instanceof final Sheep sheep && sheep.isSheared()) {
+                    return 1;
+                }
+            }
+            if (entity instanceof final Creeper creeper && creeper.isIgnited()) { // isExplosive
+                return 20;
+            }
+            if (entity instanceof final Mob mob && mob.targetSelector.hasTasks()) {
+                return 0;
+            }
+            if (entity instanceof final Pillager pillager) {
+                // TODO:?
+            }
+        }
+        // SPIGOT-6644: Otherwise the target refresh tick will be missed
+        if (entity instanceof ExperienceOrb) {
+            return 20;
+        }
+        return -1;
+    }
+
+    /**
+     * Checks if the entity is active for this tick.
+     *
+     * @param entity
+     * @return
+     */
+    public static boolean checkIfActive(final Entity entity) {
+        // Never safe to skip fireworks or item gravity
+        if (entity instanceof FireworkRocketEntity || (entity instanceof ItemEntity && (entity.tickCount + entity.getId()) % 4 == 0)) { // Needed for item gravity, see ItemEntity tick
+            return true;
+        }
+        // special case always immunities
+        // immunize brand-new entities, dead entities, and portal scenarios
+        if (entity.defaultActivationState || entity.tickCount < 20 * 10 || !entity.isAlive() || (entity.portalProcess != null && !entity.portalProcess.hasExpired()) || entity.portalCooldown > 0) {
+            return true;
+        }
+        // immunize leashed entities
+        if (entity instanceof final Mob mob && mob.getLeashHolder() instanceof Player) {
+            return true;
+        }
+
+        boolean isActive = entity.activatedTick >= MinecraftServer.currentTick;
+        entity.isTemporarilyActive = false;
+
+        // Should this entity tick?
+        if (!isActive) {
+            if ((MinecraftServer.currentTick - entity.activatedTick - 1) % 20 == 0) {
+                // Check immunities every 20 ticks.
+                final int immunity = checkEntityImmunities(entity);
+                if (immunity >= 0) {
+                    entity.activatedTick = MinecraftServer.currentTick + immunity;
+                } else {
+                    entity.isTemporarilyActive = true;
+                }
+                isActive = true;
+            }
+        }
+        // removed the original's dumb tick skipping for active entities
+        return isActive;
+    }
+
+    private static Set<EntityType<?>> ENTITIES_THAT_FLY = Set.of(
+        EntityType.GHAST,
+        EntityType.HAPPY_GHAST,
+        EntityType.PHANTOM
+    );
+
+    private static boolean isEntityThatFlies(final Entity entity) {
+        return ENTITIES_THAT_FLY.contains(entity.getType());
+    }
+}
diff --git a/net/minecraft/server/level/ChunkMap.java b/net/minecraft/server/level/ChunkMap.java
index d51645b115780dac9ff6010806e8bd62dedc8e9f..3faf1b5556c55f3468182f75b2dbff4aaa3aae3a 100644
--- a/net/minecraft/server/level/ChunkMap.java
+++ b/net/minecraft/server/level/ChunkMap.java
@@ -4,7 +4,6 @@ import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Queues;
-import com.google.common.collect.Sets;
 import com.google.common.collect.ImmutableList.Builder;
 import com.mojang.datafixers.DataFixer;
 import com.mojang.logging.LogUtils;
@@ -19,7 +18,6 @@ import it.unimi.dsi.fastutil.longs.LongIterator;
 import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
 import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
 import it.unimi.dsi.fastutil.longs.LongSet;
-import it.unimi.dsi.fastutil.longs.Long2ObjectMap.Entry;
 import java.io.IOException;
 import java.io.Writer;
 import java.nio.file.Path;
diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java
index 18551bebb7e40c936de94c6d1b009db4752d3bba..e6f958b6128213fb9577406c6495148dccac40ca 100644
--- a/net/minecraft/server/level/ServerLevel.java
+++ b/net/minecraft/server/level/ServerLevel.java
@@ -548,6 +548,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
                 profilerFiller.pop();
             }
 
+            io.papermc.paper.entity.activation.ActivationRange.activateEntities(this); // Paper - EAR
             this.entityTickList
                 .forEach(
                     entity -> {
@@ -990,12 +991,15 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
         entity.totalEntityAge++; // Paper - age-like counter for all entities
         profilerFiller.push(() -> BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString());
         profilerFiller.incrementCounter("tickNonPassenger");
+        final boolean isActive = io.papermc.paper.entity.activation.ActivationRange.checkIfActive(entity); // Paper - EAR 2
+        if (isActive) { // Paper - EAR 2
         entity.tick();
         entity.postTick(); // CraftBukkit
+        } else {entity.inactiveTick();} // Paper - EAR 2
         profilerFiller.pop();
 
         for (Entity entity1 : entity.getPassengers()) {
-            this.tickPassenger(entity, entity1);
+            this.tickPassenger(entity, entity1, isActive); // Paper - EAR 2
         }
         // Paper start - log detailed entity tick information
         } finally {
@@ -1006,7 +1010,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
         // Paper end - log detailed entity tick information
     }
 
-    private void tickPassenger(Entity ridingEntity, Entity passengerEntity) {
+    private void tickPassenger(Entity ridingEntity, Entity passengerEntity, final boolean isActive) { // Paper - EAR 2
         if (passengerEntity.isRemoved() || passengerEntity.getVehicle() != ridingEntity) {
             passengerEntity.stopRiding();
         } else if (passengerEntity instanceof Player || this.entityTickList.contains(passengerEntity)) {
@@ -1016,12 +1020,21 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
             ProfilerFiller profilerFiller = Profiler.get();
             profilerFiller.push(() -> BuiltInRegistries.ENTITY_TYPE.getKey(passengerEntity.getType()).toString());
             profilerFiller.incrementCounter("tickPassenger");
+            // Paper start - EAR 2
+            if (isActive) {
             passengerEntity.rideTick();
             passengerEntity.postTick(); // CraftBukkit
+            } else {
+                passengerEntity.setDeltaMovement(Vec3.ZERO);
+                passengerEntity.inactiveTick();
+                // copied from inside of if (isPassenger()) of passengerTick, but that ifPassenger is unnecessary
+                ridingEntity.positionRider(passengerEntity);
+            }
+            // Paper end - EAR 2
             profilerFiller.pop();
 
             for (Entity entity : passengerEntity.getPassengers()) {
-                this.tickPassenger(passengerEntity, entity);
+                this.tickPassenger(passengerEntity, entity, isActive); // Paper - EAR 2
             }
         }
     }
diff --git a/net/minecraft/world/entity/AgeableMob.java b/net/minecraft/world/entity/AgeableMob.java
index 16c5ad1547eb383e40c9fbae9b1119afc418b3a4..04875840085541ebfc7014868beec49bb7ab9976 100644
--- a/net/minecraft/world/entity/AgeableMob.java
+++ b/net/minecraft/world/entity/AgeableMob.java
@@ -130,6 +130,23 @@ public abstract class AgeableMob extends PathfinderMob {
         super.onSyncedDataUpdated(key);
     }
 
+    // Paper start - EAR 2
+    @Override
+    public void inactiveTick() {
+        super.inactiveTick();
+        if (this.level().isClientSide || this.ageLocked) { // CraftBukkit
+            this.refreshDimensions();
+        } else {
+            int age = this.getAge();
+            if (age < 0) {
+                this.setAge(++age);
+            } else if (age > 0) {
+                this.setAge(--age);
+            }
+        }
+    }
+    // Paper end - EAR 2
+
     @Override
     public void aiStep() {
         super.aiStep();
diff --git a/net/minecraft/world/entity/AreaEffectCloud.java b/net/minecraft/world/entity/AreaEffectCloud.java
index c70a58f5f633fa8e255f74c42f5e87c96b7b013a..ec20a5a6d7c8f65abda528fec36bec7bc71117f6 100644
--- a/net/minecraft/world/entity/AreaEffectCloud.java
+++ b/net/minecraft/world/entity/AreaEffectCloud.java
@@ -142,6 +142,16 @@ public class AreaEffectCloud extends Entity implements TraceableEntity {
         this.duration = duration;
     }
 
+    // Paper start - EAR 2
+    @Override
+    public void inactiveTick() {
+        super.inactiveTick();
+        if (this.tickCount >= this.waitTime + this.duration) {
+            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+        }
+    }
+    // Paper end - EAR 2
+
     @Override
     public void tick() {
         super.tick();
diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java
index 75f81a6bc156a6455a616b8de0d7701fd2255a2d..b3d951670b3a097d04cfe347d7df496b1d0a0e09 100644
--- a/net/minecraft/world/entity/Entity.java
+++ b/net/minecraft/world/entity/Entity.java
@@ -409,6 +409,15 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
     private final int despawnTime; // Paper - entity despawn time limit
     public int totalEntityAge; // Paper - age-like counter for all entities
     public final io.papermc.paper.entity.activation.ActivationType activationType = io.papermc.paper.entity.activation.ActivationType.activationTypeFor(this); // Paper - EAR 2/tracking ranges
+    // Paper start - EAR 2
+    public final boolean defaultActivationState;
+    public long activatedTick = Integer.MIN_VALUE;
+    public boolean isTemporarilyActive;
+    public long activatedImmunityTick = Integer.MIN_VALUE;
+
+    public void inactiveTick() {
+    }
+    // Paper end - EAR 2
     // CraftBukkit end
 
     // Paper start
@@ -424,6 +433,13 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
         this.position = Vec3.ZERO;
         this.blockPosition = BlockPos.ZERO;
         this.chunkPosition = ChunkPos.ZERO;
+        // Paper start - EAR 2
+        if (level != null) {
+            this.defaultActivationState = io.papermc.paper.entity.activation.ActivationRange.initializeEntityActivationState(this, level.spigotConfig);
+        } else {
+            this.defaultActivationState = false;
+        }
+        // Paper end - EAR 2
         SynchedEntityData.Builder builder = new SynchedEntityData.Builder(this);
         builder.define(DATA_SHARED_FLAGS_ID, (byte)0);
         builder.define(DATA_AIR_SUPPLY_ID, this.getMaxAirSupply());
@@ -984,6 +1000,10 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
             this.setPos(this.getX() + movement.x, this.getY() + movement.y, this.getZ() + movement.z);
         } else {
             if (type == MoverType.PISTON) {
+                // Paper start - EAR 2
+                this.activatedTick = Math.max(this.activatedTick, MinecraftServer.currentTick + 20);
+                this.activatedImmunityTick = Math.max(this.activatedImmunityTick, MinecraftServer.currentTick + 20);
+                // Paper end - EAR 2
                 movement = this.limitPistonMovement(movement);
                 if (movement.equals(Vec3.ZERO)) {
                     return;
@@ -997,6 +1017,13 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
                 this.stuckSpeedMultiplier = Vec3.ZERO;
                 this.setDeltaMovement(Vec3.ZERO);
             }
+            // Paper start - ignore movement changes while inactive.
+            if (isTemporarilyActive && !(this instanceof ItemEntity) && movement == getDeltaMovement() && type == MoverType.SELF) {
+                setDeltaMovement(Vec3.ZERO);
+                profilerFiller.pop();
+                return;
+            }
+            // Paper end
 
             movement = this.maybeBackOffFromEdge(movement, type);
             Vec3 vec3 = this.collide(movement);
diff --git a/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java
index 13cf40918ead3e2cb2699398ac659a3e1806294b..1ba342a1a60951f828034d3ed535b577b3990bf6 100644
--- a/net/minecraft/world/entity/LivingEntity.java
+++ b/net/minecraft/world/entity/LivingEntity.java
@@ -3215,6 +3215,14 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin
         return false;
     }
 
+    // Paper start - EAR 2
+    @Override
+    public void inactiveTick() {
+        super.inactiveTick();
+        ++this.noActionTime; // Above all the floats
+    }
+    // Paper end - EAR 2
+
     @Override
     public void tick() {
         super.tick();
diff --git a/net/minecraft/world/entity/Mob.java b/net/minecraft/world/entity/Mob.java
index 3047763580fabd069db5e90a9a854396c6243763..0470c4bbf8be7e48ce8dfa4910c3b9f5ebb23360 100644
--- a/net/minecraft/world/entity/Mob.java
+++ b/net/minecraft/world/entity/Mob.java
@@ -206,6 +206,19 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab
         return this.lookControl;
     }
 
+    // Paper start
+    @Override
+    public void inactiveTick() {
+        super.inactiveTick();
+        if (this.goalSelector.inactiveTick()) {
+            this.goalSelector.tick();
+        }
+        if (this.targetSelector.inactiveTick()) {
+            this.targetSelector.tick();
+        }
+    }
+    // Paper end
+
     public MoveControl getMoveControl() {
         return this.getControlledVehicle() instanceof Mob mob ? mob.getMoveControl() : this.moveControl;
     }
diff --git a/net/minecraft/world/entity/PathfinderMob.java b/net/minecraft/world/entity/PathfinderMob.java
index 60cc5cf40967eee8988cda8a45ff098e66e8ddf2..741b5078ab9ee9b8463c61ab66ba9b5c9cda0974 100644
--- a/net/minecraft/world/entity/PathfinderMob.java
+++ b/net/minecraft/world/entity/PathfinderMob.java
@@ -17,6 +17,8 @@ public abstract class PathfinderMob extends Mob {
         super(entityType, level);
     }
 
+    public BlockPos movingTarget; public BlockPos getMovingTarget() { return movingTarget; } // Paper
+
     public float getWalkTargetValue(BlockPos pos) {
         return this.getWalkTargetValue(pos, this.level());
     }
diff --git a/net/minecraft/world/entity/ai/goal/GoalSelector.java b/net/minecraft/world/entity/ai/goal/GoalSelector.java
index d552e47aa38c61a8f78ed114a3433603c9658a24..674966c580220a4e0c83a628c763aaea8bfd0b1c 100644
--- a/net/minecraft/world/entity/ai/goal/GoalSelector.java
+++ b/net/minecraft/world/entity/ai/goal/GoalSelector.java
@@ -24,6 +24,7 @@ public class GoalSelector {
     private final Map<Goal.Flag, WrappedGoal> lockedFlags = new EnumMap<>(Goal.Flag.class);
     private final Set<WrappedGoal> availableGoals = new ObjectLinkedOpenHashSet<>();
     private final EnumSet<Goal.Flag> disabledFlags = EnumSet.noneOf(Goal.Flag.class);
+    private int curRate; // Paper - EAR 2
 
     public void addGoal(int priority, Goal goal) {
         this.availableGoals.add(new WrappedGoal(priority, goal));
@@ -33,6 +34,22 @@ public class GoalSelector {
         this.availableGoals.removeIf(wrappedGoal -> filter.test(wrappedGoal.getGoal()));
     }
 
+    // Paper start - EAR 2
+    public boolean inactiveTick() {
+        this.curRate++;
+        return this.curRate % 3 == 0; // TODO newGoalRate was already unused in 1.20.4, check if this is correct
+    }
+
+    public boolean hasTasks() {
+        for (WrappedGoal task : this.availableGoals) {
+            if (task.isRunning()) {
+                return true;
+            }
+        }
+        return false;
+    }
+    // Paper end - EAR 2
+
     public void removeGoal(Goal goal) {
         for (WrappedGoal wrappedGoal : this.availableGoals) {
             if (wrappedGoal.getGoal() == goal && wrappedGoal.isRunning()) {
diff --git a/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java b/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java
index 3f780276be6766ef253c50212e06fd93a96b0caa..7e70c7bee633c54497d1cd2854dd60f4fb5ff160 100644
--- a/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java
+++ b/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java
@@ -23,6 +23,14 @@ public abstract class MoveToBlockGoal extends Goal {
     public MoveToBlockGoal(PathfinderMob mob, double speedModifier, int searchRange) {
         this(mob, speedModifier, searchRange, 1);
     }
+    // Paper start - activation range improvements
+    @Override
+    public void stop() {
+        super.stop();
+        this.blockPos = BlockPos.ZERO;
+        this.mob.movingTarget = null;
+    }
+    // Paper end
 
     public MoveToBlockGoal(PathfinderMob mob, double speedModifier, int searchRange, int verticalSearchRange) {
         this.mob = mob;
@@ -113,6 +121,7 @@ public abstract class MoveToBlockGoal extends Goal {
                         mutableBlockPos.setWithOffset(blockPos, i4, i2 - 1, i5);
                         if (this.mob.isWithinHome(mutableBlockPos) && this.isValidTarget(this.mob.level(), mutableBlockPos)) {
                             this.blockPos = mutableBlockPos;
+                            this.mob.movingTarget = mutableBlockPos == BlockPos.ZERO ? null : mutableBlockPos.immutable(); // Paper
                             return true;
                         }
                     }
diff --git a/net/minecraft/world/entity/item/ItemEntity.java b/net/minecraft/world/entity/item/ItemEntity.java
index fca31bbab8e7830933ceffcf992ff56ccc84414c..51804b611f469f2ab53e455e8c633b867b00cc88 100644
--- a/net/minecraft/world/entity/item/ItemEntity.java
+++ b/net/minecraft/world/entity/item/ItemEntity.java
@@ -129,6 +129,29 @@ public class ItemEntity extends Entity implements TraceableEntity {
         return 0.04;
     }
 
+    // Paper start - EAR 2
+    @Override
+    public void inactiveTick() {
+        super.inactiveTick();
+        if (this.pickupDelay > 0 && this.pickupDelay != 32767) {
+            this.pickupDelay--;
+        }
+        if (this.age != -32768) {
+            this.age++;
+        }
+
+        if (!this.level().isClientSide && this.age >= this.despawnRate) {// Paper - Alternative item-despawn-rate
+            // CraftBukkit start - fire ItemDespawnEvent
+            if (org.bukkit.craftbukkit.event.CraftEventFactory.callItemDespawnEvent(this).isCancelled()) {
+                this.age = 0;
+                return;
+            }
+            // CraftBukkit end
+            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
+        }
+    }
+    // Paper end - EAR 2
+
     @Override
     public void tick() {
         if (this.getItem().isEmpty()) {
diff --git a/net/minecraft/world/entity/npc/Villager.java b/net/minecraft/world/entity/npc/Villager.java
index 1a6cfe566d84048c4e70e835f57d18b70f8783f0..e1e2bdb35866a8f32a41f6efd24ad77cf916b2e9 100644
--- a/net/minecraft/world/entity/npc/Villager.java
+++ b/net/minecraft/world/entity/npc/Villager.java
@@ -268,11 +268,35 @@ public class Villager extends AbstractVillager implements ReputationEventHandler
         return this.assignProfessionWhenSpawned;
     }
 
+    // Paper start - EAR 2
+    @Override
+    public void inactiveTick() {
+        // SPIGOT-3874, SPIGOT-3894, SPIGOT-3846, SPIGOT-5286 :(
+        if (this.getUnhappyCounter() > 0) {
+            this.setUnhappyCounter(this.getUnhappyCounter() - 1);
+        }
+        if (this.isEffectiveAi()) {
+            if (this.level().spigotConfig.tickInactiveVillagers) {
+                this.customServerAiStep(this.level().getMinecraftWorld());
+            } else {
+                this.customServerAiStep(this.level().getMinecraftWorld(), true);
+            }
+        }
+        this.maybeDecayGossip();
+        super.inactiveTick();
+    }
+    // Paper end - EAR 2
+
     @Override
     protected void customServerAiStep(ServerLevel level) {
+        // Paper start - EAR 2
+        this.customServerAiStep(level, false);
+    }
+    protected void customServerAiStep(ServerLevel level, final boolean inactive) {
+        // Paper end - EAR 2
         ProfilerFiller profilerFiller = Profiler.get();
         profilerFiller.push("villagerBrain");
-        this.getBrain().tick(level, this);
+        if (!inactive) this.getBrain().tick(level, this); // Paper - EAR 2
         profilerFiller.pop();
         if (this.assignProfessionWhenSpawned) {
             this.assignProfessionWhenSpawned = false;
@@ -296,7 +320,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler
             this.lastTradedPlayer = null;
         }
 
-        if (!this.isNoAi() && this.random.nextInt(100) == 0) {
+        if (!inactive && !this.isNoAi() && this.random.nextInt(100) == 0) { // Paper - EAR 2
             Raid raidAt = level.getRaidAt(this.blockPosition());
             if (raidAt != null && raidAt.isActive() && !raidAt.isOver()) {
                 level.broadcastEntityEvent(this, (byte)42);
@@ -307,6 +331,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler
             this.stopTrading();
         }
 
+        if (inactive) return; // Paper - EAR 2
         super.customServerAiStep(level);
     }
 
diff --git a/net/minecraft/world/entity/projectile/Arrow.java b/net/minecraft/world/entity/projectile/Arrow.java
index 1f22f44abb21d1ed9a4870f668779efb8fd9b295..91ead824718eeea2afba3bc0ef619b8a24bb24b9 100644
--- a/net/minecraft/world/entity/projectile/Arrow.java
+++ b/net/minecraft/world/entity/projectile/Arrow.java
@@ -70,6 +70,16 @@ public class Arrow extends AbstractArrow {
         builder.define(ID_EFFECT_COLOR, -1);
     }
 
+    // Paper start - EAR 2
+    @Override
+    public void inactiveTick() {
+        if (this.isInGround()) {
+            this.life++;
+        }
+        super.inactiveTick();
+    }
+    // Paper end
+
     @Override
     public void tick() {
         super.tick();
diff --git a/net/minecraft/world/entity/projectile/FireworkRocketEntity.java b/net/minecraft/world/entity/projectile/FireworkRocketEntity.java
index 7d5c6eec8e7c2448a3502255135dd9154b6ed2d8..d8dc196ef92e97f831cf97cd1536a46f81f9d5d1 100644
--- a/net/minecraft/world/entity/projectile/FireworkRocketEntity.java
+++ b/net/minecraft/world/entity/projectile/FireworkRocketEntity.java
@@ -107,6 +107,21 @@ public class FireworkRocketEntity extends Projectile implements ItemSupplier {
         return super.shouldRender(x, y, z) && !this.isAttachedToEntity();
     }
 
+    // Paper start - EAR 2
+    @Override
+    public void inactiveTick() {
+        this.life++;
+        if (this.life > this.lifetime && this.level() instanceof ServerLevel serverLevel) {
+            // Paper start - Call FireworkExplodeEvent
+            if (org.bukkit.craftbukkit.event.CraftEventFactory.callFireworkExplodeEvent(this)) {
+                this.explode(serverLevel);
+            }
+            // Paper end - Call FireworkExplodeEvent
+        }
+        super.inactiveTick();
+    }
+    // Paper end - EAR 2
+
     @Override
     public void tick() {
         super.tick();
diff --git a/net/minecraft/world/entity/vehicle/MinecartHopper.java b/net/minecraft/world/entity/vehicle/MinecartHopper.java
index 52acc72841f0c6980f5f3f8ef21d0b29dd472ce3..41a6ec508a10a49a37539d2f10171d15c233b280 100644
--- a/net/minecraft/world/entity/vehicle/MinecartHopper.java
+++ b/net/minecraft/world/entity/vehicle/MinecartHopper.java
@@ -49,6 +49,7 @@ public class MinecartHopper extends AbstractMinecartContainer implements Hopper
         if (flag != this.isEnabled()) {
             this.setEnabled(flag);
         }
+        this.immunize(); // Paper
     }
 
     public boolean isEnabled() {
@@ -102,11 +103,13 @@ public class MinecartHopper extends AbstractMinecartContainer implements Hopper
 
     public boolean suckInItems() {
         if (HopperBlockEntity.suckInItems(this.level(), this)) {
+            this.immunize(); // Paper
             return true;
         } else {
             for (ItemEntity itemEntity : this.level()
                 .getEntitiesOfClass(ItemEntity.class, this.getBoundingBox().inflate(0.25, 0.0, 0.25), EntitySelector.ENTITY_STILL_ALIVE)) {
                 if (HopperBlockEntity.addItem(this, itemEntity)) {
+                    this.immunize(); // Paper
                     return true;
                 }
             }
@@ -141,4 +144,11 @@ public class MinecartHopper extends AbstractMinecartContainer implements Hopper
     public AbstractContainerMenu createMenu(int id, Inventory playerInventory) {
         return new HopperMenu(id, playerInventory, this);
     }
+
+    // Paper start
+    public void immunize() {
+        this.activatedImmunityTick = Math.max(this.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 20);
+    }
+    // Paper end
+
 }
diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java
index 06069d3ac598f5f12feab038de4f1199794298f6..980eaba27ce2616c1573a4760cf4acc2dd251190 100644
--- a/net/minecraft/world/level/Level.java
+++ b/net/minecraft/world/level/Level.java
@@ -143,6 +143,12 @@ public abstract class Level implements LevelAccessor, UUIDLookup<Entity>, AutoCl
     @Nullable
     public List<net.minecraft.world.entity.item.ItemEntity> captureDrops;
     public final it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<SpawnCategory> ticksPerSpawnCategory = new it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<>();
+    // Paper start - EAR 2
+    public int wakeupInactiveRemainingAnimals;
+    public int wakeupInactiveRemainingFlying;
+    public int wakeupInactiveRemainingMonsters;
+    public int wakeupInactiveRemainingVillagers;
+    // Paper end - EAR 2
     public boolean populating;
     public final org.spigotmc.SpigotWorldConfig spigotConfig; // Spigot
     // Paper start - add paper world config
diff --git a/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java b/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java
index ba757b3accf0ee7e57f82507a6c04b90ae850d55..f1ce4cff1c03a0037ade2c8ef989cf327c973a7e 100644
--- a/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java
+++ b/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java
@@ -152,6 +152,10 @@ public class PistonMovingBlockEntity extends BlockEntity {
                                 }
 
                                 entity.setDeltaMovement(d1, d2, d3);
+                                // Paper - EAR items stuck in slime pushed by a piston
+                                entity.activatedTick = Math.max(entity.activatedTick, net.minecraft.server.MinecraftServer.currentTick + 10);
+                                entity.activatedImmunityTick = Math.max(entity.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 10);
+                                // Paper end
                                 break;
                             }
                         }
