From 8d45ed0c79ed384bbc463b0a49f2cecff6189c41 Mon Sep 17 00:00:00 2001
From: Aikar <aikar@aikar.co>
Date: Sun, 3 Feb 2013 05:10:21 -0500
Subject: [PATCH] Entity Activation Range

This feature gives 3 new configurable ranges that if an entity of the matching type is outside of this radius of any player, will tick at 5% of its normal rate.

This will drastically cut down on tick timings for entities that are not in range of a user to actually be "used".
This change can have dramatic impact on gameplay if configured too low. Balance according to your servers desired gameplay.

diff --git a/src/main/java/net/minecraft/server/Entity.java b/src/main/java/net/minecraft/server/Entity.java
index d49a6e4f3..33c9d696b 100644
--- a/src/main/java/net/minecraft/server/Entity.java
+++ b/src/main/java/net/minecraft/server/Entity.java
@@ -110,7 +110,7 @@ public abstract class Entity implements ICommandListener {
     protected Random random;
     public int ticksLived;
     public int fireTicks;
-    public boolean inWater;
+    public boolean inWater; // Spigot - protected -> public // PAIL
     public int noDamageTicks;
     protected boolean justCreated;
     protected boolean fireProof;
@@ -148,6 +148,12 @@ public abstract class Entity implements ICommandListener {
     public org.bukkit.projectiles.ProjectileSource projectileSource; // For projectiles only
     public boolean forceExplosionKnockback; // SPIGOT-949
     public CustomTimingsHandler tickTimer = org.bukkit.craftbukkit.SpigotTimings.getEntityTimings(this); // Spigot
+    // Spigot start
+    public final byte activationType = org.spigotmc.ActivationRange.initializeEntityActivationType(this);
+    public final boolean defaultActivationState;
+    public long activatedTick = Integer.MIN_VALUE;
+    public void inactiveTick() { }
+    // Spigot end
 
     public float getBukkitYaw() {
         return this.yaw;
@@ -174,7 +180,12 @@ public abstract class Entity implements ICommandListener {
         this.setPosition(0.0D, 0.0D, 0.0D);
         if (world != null) {
             this.dimension = world.worldProvider.getDimensionManager().getDimensionID();
+            // Spigot start
+            this.defaultActivationState = org.spigotmc.ActivationRange.initializeEntityActivationState(this, world.spigotConfig);
+        } else {
+            this.defaultActivationState = false;
         }
+        // Spigot end
 
         this.datawatcher = new DataWatcher(this);
         this.datawatcher.register(Entity.Z, Byte.valueOf((byte) 0));
diff --git a/src/main/java/net/minecraft/server/EntityAgeable.java b/src/main/java/net/minecraft/server/EntityAgeable.java
index 2b0a970fd..db23aedc4 100644
--- a/src/main/java/net/minecraft/server/EntityAgeable.java
+++ b/src/main/java/net/minecraft/server/EntityAgeable.java
@@ -12,6 +12,31 @@ public abstract class EntityAgeable extends EntityCreature {
     private float bz;
     public boolean ageLocked; // CraftBukkit
 
+    // Spigot start
+    @Override
+    public void inactiveTick()
+    {
+        super.inactiveTick();
+        if ( this.world.isClientSide || this.ageLocked )
+        { // CraftBukkit
+            this.a( this.isBaby() );
+        } else
+        {
+            int i = this.getAge();
+
+            if ( i < 0 )
+            {
+                ++i;
+                this.setAgeRaw( i );
+            } else if ( i > 0 )
+            {
+                --i;
+                this.setAgeRaw( i );
+            }
+        }
+    }
+    // Spigot end
+
     public EntityAgeable(World world) {
         super(world);
     }
diff --git a/src/main/java/net/minecraft/server/EntityArrow.java b/src/main/java/net/minecraft/server/EntityArrow.java
index 2463a90c3..ba41470d8 100644
--- a/src/main/java/net/minecraft/server/EntityArrow.java
+++ b/src/main/java/net/minecraft/server/EntityArrow.java
@@ -39,6 +39,18 @@ public abstract class EntityArrow extends Entity implements IProjectile {
     private double damage;
     public int knockbackStrength;
 
+    // Spigot Start
+    @Override
+    public void inactiveTick()
+    {
+        if ( this.inGround )
+        {
+            this.ax += 1; // Despawn counter. First int after shooter
+        }
+        super.inactiveTick();
+    }
+    // Spigot End
+
     public EntityArrow(World world) {
         super(world);
         this.h = -1;
diff --git a/src/main/java/net/minecraft/server/EntityFireworks.java b/src/main/java/net/minecraft/server/EntityFireworks.java
index f1f479623..48cdef5db 100644
--- a/src/main/java/net/minecraft/server/EntityFireworks.java
+++ b/src/main/java/net/minecraft/server/EntityFireworks.java
@@ -17,6 +17,14 @@ public class EntityFireworks extends Entity {
         this.setSize(0.25F, 0.25F);
     }
 
+    // Spigot Start
+    @Override
+    public void inactiveTick() {
+        this.ticksFlown += 1;
+        super.inactiveTick();
+    }
+    // Spigot End
+
     protected void i() {
         this.datawatcher.register(EntityFireworks.FIREWORK_ITEM, ItemStack.a);
         this.datawatcher.register(EntityFireworks.b, Integer.valueOf(0));
diff --git a/src/main/java/net/minecraft/server/EntityItem.java b/src/main/java/net/minecraft/server/EntityItem.java
index 3f51880ab..4d3aef96b 100644
--- a/src/main/java/net/minecraft/server/EntityItem.java
+++ b/src/main/java/net/minecraft/server/EntityItem.java
@@ -143,6 +143,28 @@ public class EntityItem extends Entity {
         }
     }
 
+    // Spigot start - copied from above
+    @Override
+    public void inactiveTick() {
+        // CraftBukkit start - Use wall time for pickup and despawn timers
+        int elapsedTicks = MinecraftServer.currentTick - this.lastTick;
+        if (this.pickupDelay != 32767) this.pickupDelay -= elapsedTicks;
+        if (this.age != -32768) this.age += elapsedTicks;
+        this.lastTick = MinecraftServer.currentTick;
+        // CraftBukkit end
+
+        if (!this.world.isClientSide && this.age >= world.spigotConfig.itemDespawnRate) { // Spigot
+            // CraftBukkit start - fire ItemDespawnEvent
+            if (org.bukkit.craftbukkit.event.CraftEventFactory.callItemDespawnEvent(this).isCancelled()) {
+                this.age = 0;
+                return;
+            }
+            // CraftBukkit end
+            this.die();
+        }
+    }
+    // Spigot end
+
     private void x() {
         // Spigot start
         double radius = world.spigotConfig.itemMerge;
diff --git a/src/main/java/net/minecraft/server/EntityLiving.java b/src/main/java/net/minecraft/server/EntityLiving.java
index f8007e946..ecff5fce3 100644
--- a/src/main/java/net/minecraft/server/EntityLiving.java
+++ b/src/main/java/net/minecraft/server/EntityLiving.java
@@ -122,6 +122,13 @@ public abstract class EntityLiving extends Entity {
         return getHeadRotation();
     }
     // CraftBukkit end
+    // Spigot start
+    public void inactiveTick()
+    {
+        super.inactiveTick();
+        ++this.ticksFarFromPlayer; // Above all the floats
+    }
+    // Spigot end
 
     public void killEntity() {
         this.damageEntity(DamageSource.OUT_OF_WORLD, Float.MAX_VALUE);
diff --git a/src/main/java/net/minecraft/server/EntityVillager.java b/src/main/java/net/minecraft/server/EntityVillager.java
index fe137ed06..2795447b5 100644
--- a/src/main/java/net/minecraft/server/EntityVillager.java
+++ b/src/main/java/net/minecraft/server/EntityVillager.java
@@ -97,6 +97,14 @@ public class EntityVillager extends EntityAgeable implements NPC, IMerchant {
         this.getAttributeInstance(GenericAttributes.MOVEMENT_SPEED).setValue(0.5D);
     }
 
+    // Spigot Start
+    @Override
+    public void inactiveTick() {
+        this.M(); // SPIGOT-3846
+        super.inactiveTick();
+    }
+    // Spigot End
+
     protected void M() {
         if (--this.profession <= 0) {
             BlockPosition blockposition = new BlockPosition(this);
diff --git a/src/main/java/net/minecraft/server/World.java b/src/main/java/net/minecraft/server/World.java
index 73e951673..04f63941c 100644
--- a/src/main/java/net/minecraft/server/World.java
+++ b/src/main/java/net/minecraft/server/World.java
@@ -1350,6 +1350,7 @@ public abstract class World implements IBlockAccess {
         CrashReportSystemDetails crashreportsystemdetails1;
         CrashReport crashreport1;
 
+        org.spigotmc.ActivationRange.activateEntities(this); // Spigot
         timings.entityTick.startTiming(); // Spigot
         // CraftBukkit start - Use field for loop variable
         for (this.tickPosition = 0; this.tickPosition < this.entityList.size(); ++this.tickPosition) {
@@ -1535,8 +1536,12 @@ public abstract class World implements IBlockAccess {
 
         // CraftBukkit start - check if chunks are loaded as done in previous versions
         // TODO: Go back to Vanilla behaviour when comfortable
-        Chunk startingChunk = this.getChunkIfLoaded(MathHelper.floor(entity.locX) >> 4, MathHelper.floor(entity.locZ) >> 4);
-        if (flag && !(startingChunk != null && startingChunk.areNeighborsLoaded(2))) {
+        // Spigot start
+        // Chunk startingChunk = this.getChunkIfLoaded(MathHelper.floor(entity.locX) >> 4, MathHelper.floor(entity.locZ) >> 4);
+        if (flag && !org.spigotmc.ActivationRange.checkIfActive(entity)) {
+            entity.ticksLived++;
+            entity.inactiveTick();
+            // Spigot end
             return;
         }
         // CraftBukkit end
diff --git a/src/main/java/org/bukkit/craftbukkit/SpigotTimings.java b/src/main/java/org/bukkit/craftbukkit/SpigotTimings.java
index fa88be0db..4c8ab2bc9 100644
--- a/src/main/java/org/bukkit/craftbukkit/SpigotTimings.java
+++ b/src/main/java/org/bukkit/craftbukkit/SpigotTimings.java
@@ -40,6 +40,9 @@ public class SpigotTimings {
 
     public static final CustomTimingsHandler playerCommandTimer = new CustomTimingsHandler("** playerCommand");
 
+    public static final CustomTimingsHandler entityActivationCheckTimer = new CustomTimingsHandler("entityActivationCheck");
+    public static final CustomTimingsHandler checkIfActiveTimer = new CustomTimingsHandler("** checkIfActive");
+
     public static final HashMap<String, CustomTimingsHandler> entityTypeTimingMap = new HashMap<String, CustomTimingsHandler>();
     public static final HashMap<String, CustomTimingsHandler> tileEntityTypeTimingMap = new HashMap<String, CustomTimingsHandler>();
     public static final HashMap<String, CustomTimingsHandler> pluginTaskTimingMap = new HashMap<String, CustomTimingsHandler>();
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
index 364b8e369..11c793404 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
@@ -287,6 +287,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
         entity.setLocation(location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch());
         // SPIGOT-619: Force sync head rotation also
         entity.setHeadRotation(location.getYaw());
+        entity.world.entityJoinedWorld(entity, false); // Spigot - register to new chunk
 
         return true;
     }
diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java
new file mode 100644
index 000000000..218a2378e
--- /dev/null
+++ b/src/main/java/org/spigotmc/ActivationRange.java
@@ -0,0 +1,286 @@
+package org.spigotmc;
+
+import java.util.Set;
+import net.minecraft.server.AxisAlignedBB;
+import net.minecraft.server.Chunk;
+import net.minecraft.server.Entity;
+import net.minecraft.server.EntityAmbient;
+import net.minecraft.server.EntityAnimal;
+import net.minecraft.server.EntityArrow;
+import net.minecraft.server.EntityComplexPart;
+import net.minecraft.server.EntityCreature;
+import net.minecraft.server.EntityCreeper;
+import net.minecraft.server.EntityEnderCrystal;
+import net.minecraft.server.EntityEnderDragon;
+import net.minecraft.server.EntityFireball;
+import net.minecraft.server.EntityFireworks;
+import net.minecraft.server.EntityHuman;
+import net.minecraft.server.EntityLiving;
+import net.minecraft.server.EntityMonster;
+import net.minecraft.server.EntityProjectile;
+import net.minecraft.server.EntitySheep;
+import net.minecraft.server.EntitySlice;
+import net.minecraft.server.EntitySlime;
+import net.minecraft.server.EntityTNTPrimed;
+import net.minecraft.server.EntityVillager;
+import net.minecraft.server.EntityWeather;
+import net.minecraft.server.EntityWither;
+import net.minecraft.server.MathHelper;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.World;
+import org.bukkit.craftbukkit.SpigotTimings;
+
+public class ActivationRange
+{
+
+    static AxisAlignedBB maxBB = new AxisAlignedBB( 0, 0, 0, 0, 0, 0 );
+    static AxisAlignedBB miscBB = new AxisAlignedBB( 0, 0, 0, 0, 0, 0 );
+    static AxisAlignedBB animalBB = new AxisAlignedBB( 0, 0, 0, 0, 0, 0 );
+    static AxisAlignedBB monsterBB = new AxisAlignedBB( 0, 0, 0, 0, 0, 0 );
+
+    /**
+     * Initializes an entities type on construction to specify what group this
+     * entity is in for activation ranges.
+     *
+     * @param entity
+     * @return group id
+     */
+    public static byte initializeEntityActivationType(Entity entity)
+    {
+        if ( entity instanceof EntityMonster || entity instanceof EntitySlime )
+        {
+            return 1; // Monster
+        } else if ( entity instanceof EntityCreature || entity instanceof EntityAmbient )
+        {
+            return 2; // Animal
+        } else
+        {
+            return 3; // Misc
+        }
+    }
+
+    /**
+     * These entities are excluded from Activation range checks.
+     *
+     * @param entity
+     * @param world
+     * @return boolean If it should always tick.
+     */
+    public static boolean initializeEntityActivationState(Entity entity, SpigotWorldConfig config)
+    {
+        if ( ( entity.activationType == 3 && config.miscActivationRange == 0 )
+                || ( entity.activationType == 2 && config.animalActivationRange == 0 )
+                || ( entity.activationType == 1 && config.monsterActivationRange == 0 )
+                || entity instanceof EntityHuman
+                || entity instanceof EntityProjectile
+                || entity instanceof EntityEnderDragon
+                || entity instanceof EntityComplexPart
+                || entity instanceof EntityWither
+                || entity instanceof EntityFireball
+                || entity instanceof EntityWeather
+                || entity instanceof EntityTNTPrimed
+                || entity instanceof EntityEnderCrystal
+                || entity instanceof EntityFireworks )
+        {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Find what entities are in range of the players in the world and set
+     * active if in range.
+     *
+     * @param world
+     */
+    public static void activateEntities(World world)
+    {
+        SpigotTimings.entityActivationCheckTimer.startTiming();
+        final int miscActivationRange = world.spigotConfig.miscActivationRange;
+        final int animalActivationRange = world.spigotConfig.animalActivationRange;
+        final int monsterActivationRange = world.spigotConfig.monsterActivationRange;
+
+        int maxRange = Math.max( monsterActivationRange, animalActivationRange );
+        maxRange = Math.max( maxRange, miscActivationRange );
+        maxRange = Math.min( ( world.spigotConfig.viewDistance << 4 ) - 8, maxRange );
+
+        for ( EntityHuman player : world.players )
+        {
+
+            player.activatedTick = MinecraftServer.currentTick;
+            maxBB = player.getBoundingBox().grow( maxRange, 256, maxRange );
+            miscBB = player.getBoundingBox().grow( miscActivationRange, 256, miscActivationRange );
+            animalBB = player.getBoundingBox().grow( animalActivationRange, 256, animalActivationRange );
+            monsterBB = player.getBoundingBox().grow( monsterActivationRange, 256, monsterActivationRange );
+
+            int i = MathHelper.floor( maxBB.a / 16.0D );
+            int j = MathHelper.floor( maxBB.d / 16.0D );
+            int k = MathHelper.floor( maxBB.c / 16.0D );
+            int l = MathHelper.floor( maxBB.f / 16.0D );
+
+            for ( int i1 = i; i1 <= j; ++i1 )
+            {
+                for ( int j1 = k; j1 <= l; ++j1 )
+                {
+                    if ( world.getWorld().isChunkLoaded( i1, j1 ) )
+                    {
+                        activateChunkEntities( world.getChunkAt( i1, j1 ) );
+                    }
+                }
+            }
+        }
+        SpigotTimings.entityActivationCheckTimer.stopTiming();
+    }
+
+    /**
+     * Checks for the activation state of all entities in this chunk.
+     *
+     * @param chunk
+     */
+    private static void activateChunkEntities(Chunk chunk)
+    {
+        for ( EntitySlice slice : chunk.entitySlices )
+        {
+            for ( Entity entity : (Set<Entity>) slice )
+            {
+                if ( MinecraftServer.currentTick > entity.activatedTick )
+                {
+                    if ( entity.defaultActivationState )
+                    {
+                        entity.activatedTick = MinecraftServer.currentTick;
+                        continue;
+                    }
+                    switch ( entity.activationType )
+                    {
+                        case 1:
+                            if ( monsterBB.c( entity.getBoundingBox() ) )
+                            {
+                                entity.activatedTick = MinecraftServer.currentTick;
+                            }
+                            break;
+                        case 2:
+                            if ( animalBB.c( entity.getBoundingBox() ) )
+                            {
+                                entity.activatedTick = MinecraftServer.currentTick;
+                            }
+                            break;
+                        case 3:
+                        default:
+                            if ( miscBB.c( 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 boolean checkEntityImmunities(Entity entity)
+    {
+        // quick checks.
+        if ( entity.inWater || entity.fireTicks > 0 )
+        {
+            return true;
+        }
+        if ( !( entity instanceof EntityArrow ) )
+        {
+            if ( !entity.onGround || !entity.passengers.isEmpty() || entity.isPassenger() )
+            {
+                return true;
+            }
+        } else if ( !( (EntityArrow) entity ).inGround )
+        {
+            return true;
+        }
+        // special cases.
+        if ( entity instanceof EntityLiving )
+        {
+            EntityLiving living = (EntityLiving) entity;
+            if ( /*TODO: Missed mapping? living.attackTicks > 0 || */ living.hurtTicks > 0 || living.effects.size() > 0 )
+            {
+                return true;
+            }
+            if ( entity instanceof EntityCreature && ( (EntityCreature) entity ).getGoalTarget() != null )
+            {
+                return true;
+            }
+            if ( entity instanceof EntityVillager && ( (EntityVillager) entity ).isInLove() )
+            {
+                return true;
+            }
+            if ( entity instanceof EntityAnimal )
+            {
+                EntityAnimal animal = (EntityAnimal) entity;
+                if ( animal.isBaby() || animal.isInLove() )
+                {
+                    return true;
+                }
+                if ( entity instanceof EntitySheep && ( (EntitySheep) entity ).isSheared() )
+                {
+                    return true;
+                }
+            }
+            if (entity instanceof EntityCreeper && ((EntityCreeper) entity).isIgnited()) { // isExplosive
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Checks if the entity is active for this tick.
+     *
+     * @param entity
+     * @return
+     */
+    public static boolean checkIfActive(Entity entity)
+    {
+        SpigotTimings.checkIfActiveTimer.startTiming();
+        // Never safe to skip fireworks or entities not yet added to chunk
+        // PAIL: inChunk - boolean under datawatchers
+        if ( !entity.aa || entity instanceof EntityFireworks ) {
+            SpigotTimings.checkIfActiveTimer.stopTiming();
+            return true;
+        }
+
+        boolean isActive = entity.activatedTick >= MinecraftServer.currentTick || entity.defaultActivationState;
+
+        // Should this entity tick?
+        if ( !isActive )
+        {
+            if ( ( MinecraftServer.currentTick - entity.activatedTick - 1 ) % 20 == 0 )
+            {
+                // Check immunities every 20 ticks.
+                if ( checkEntityImmunities( entity ) )
+                {
+                    // Triggered some sort of immunity, give 20 full ticks before we check again.
+                    entity.activatedTick = MinecraftServer.currentTick + 20;
+                }
+                isActive = true;
+            }
+            // Add a little performance juice to active entities. Skip 1/4 if not immune.
+        } else if ( !entity.defaultActivationState && entity.ticksLived % 4 == 0 && !checkEntityImmunities( entity ) )
+        {
+            isActive = false;
+        }
+        int x = MathHelper.floor( entity.locX );
+        int z = MathHelper.floor( entity.locZ );
+        // Make sure not on edge of unloaded chunk
+        Chunk chunk = entity.world.getChunkIfLoaded( x >> 4, z >> 4 );
+        if ( isActive && !( chunk != null && chunk.areNeighborsLoaded( 1 ) ) )
+        {
+            isActive = false;
+        }
+        SpigotTimings.checkIfActiveTimer.stopTiming();
+        return isActive;
+    }
+}
diff --git a/src/main/java/org/spigotmc/SpigotWorldConfig.java b/src/main/java/org/spigotmc/SpigotWorldConfig.java
index 6a8b5cd32..82f821208 100644
--- a/src/main/java/org/spigotmc/SpigotWorldConfig.java
+++ b/src/main/java/org/spigotmc/SpigotWorldConfig.java
@@ -140,4 +140,15 @@ public class SpigotWorldConfig
         itemDespawnRate = getInt( "item-despawn-rate", 6000 );
         log( "Item Despawn Rate: " + itemDespawnRate );
     }
+
+    public int animalActivationRange = 32;
+    public int monsterActivationRange = 32;
+    public int miscActivationRange = 16;
+    private void activationRange()
+    {
+        animalActivationRange = getInt( "entity-activation-range.animals", animalActivationRange );
+        monsterActivationRange = getInt( "entity-activation-range.monsters", monsterActivationRange );
+        miscActivationRange = getInt( "entity-activation-range.misc", miscActivationRange );
+        log( "Entity Activation Range: An " + animalActivationRange + " / Mo " + monsterActivationRange + " / Mi " + miscActivationRange );
+    }
 }
-- 
2.14.1