From f10f0576f0ed5a3e904d91009e4cb33d01feed3e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 13 Oct 2026 16:50:13 +0000 Subject: [PATCH 01/11] Restore EntityLiving.attackTicks for activation-range immunity Mojang removed pre-1.8 AttackTime in 1.8, so Spigot left the ActivationRange living.attackTicks > 0 check commented out as a missed mapping. Restore the field (20-tick melee cooldown), wire it from melee attack entry points, and re-enable the immunity so mobs are not deactivated mid-attack cooldown. Co-authored-by: xii69 --- .../net/minecraft/server/EntityIronGolem.java | 1 + .../net/minecraft/server/EntityLiving.java | 21 +++++ .../net/minecraft/server/EntityMonster.java | 1 + .../net/minecraft/server/EntityOcelot.java | 1 + .../net/minecraft/server/EntityRabbit.java | 1 + .../java/net/minecraft/server/EntityWolf.java | 1 + .../java/org/spigotmc/ActivationRange.java | 5 +- .../ActivationRangeAttackTicksTest.java | 79 +++++++++++++++++++ 8 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 WindSpigot-Server/src/test/java/com/windpvp/windspigot/entity/ActivationRangeAttackTicksTest.java diff --git a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityIronGolem.java b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityIronGolem.java index 7f8e4dfba..81406da68 100644 --- a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityIronGolem.java +++ b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityIronGolem.java @@ -123,6 +123,7 @@ public void a(NBTTagCompound nbttagcompound) { @Override public boolean r(Entity entity) { + this.markAttackCooldown(); // WindSpigot - ActivationRange attackTicks immunity this.c = 10; this.world.broadcastEntityEffect(this, (byte) 4); boolean flag = entity.damageEntity(DamageSource.mobAttack(this), 7 + this.random.nextInt(15)); diff --git a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityLiving.java b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityLiving.java index 6b43d7133..d01908f6c 100644 --- a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityLiving.java +++ b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityLiving.java @@ -44,6 +44,13 @@ public abstract class EntityLiving extends Entity { public int av; public float aw; public int deathTicks; + /** + * WindSpigot: restored pre-1.8 {@code AttackTime} / {@code attackTicks}. + * Mojang removed the field in 1.8 (cooldown moved into pathfinder goals), but + * Spigot's entity-activation-range still needs an entity-level signal so mobs + * are not deactivated mid melee-attack cooldown. + */ + public int attackTicks; public float ay; public float az; public float aA; @@ -263,6 +270,11 @@ public void K() { --this.hurtTicks; } + // WindSpigot - pre-1.8 AttackTime countdown (ActivationRange immunity) + if (this.attackTicks > 0) { + --this.attackTicks; + } + if (this.noDamageTicks > 0 && !(this instanceof EntityPlayer)) { --this.noDamageTicks; } @@ -1608,10 +1620,19 @@ public void k(float f) { } public boolean r(Entity entity) { + this.markAttackCooldown(); this.p(entity); return false; } + /** + * WindSpigot: start/refresh the restored {@link #attackTicks} melee cooldown + * (20 ticks, matching pre-1.8 AttackTime and PathfinderGoalMeleeAttack). + */ + public void markAttackCooldown() { + this.attackTicks = 20; + } + public boolean isSleeping() { return false; } diff --git a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityMonster.java b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityMonster.java index 77ee5447a..1cfaa2616 100644 --- a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityMonster.java +++ b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityMonster.java @@ -70,6 +70,7 @@ protected String n(int i) { @Override public boolean r(Entity entity) { + this.markAttackCooldown(); // WindSpigot - ActivationRange attackTicks immunity float f = (float) this.getAttributeInstance(GenericAttributes.ATTACK_DAMAGE).getValue(); int i = 0; diff --git a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityOcelot.java b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityOcelot.java index d33b20650..0498ce6d6 100644 --- a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityOcelot.java +++ b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityOcelot.java @@ -110,6 +110,7 @@ protected Item getLoot() { @Override public boolean r(Entity entity) { + this.markAttackCooldown(); // WindSpigot - ActivationRange attackTicks immunity return entity.damageEntity(DamageSource.mobAttack(this), 3.0F); } diff --git a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityRabbit.java b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityRabbit.java index 3d6124b8a..76441a05e 100644 --- a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityRabbit.java +++ b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityRabbit.java @@ -229,6 +229,7 @@ protected String bp() { @Override public boolean r(Entity entity) { + this.markAttackCooldown(); // WindSpigot - ActivationRange attackTicks immunity if (this.getRabbitType() == 99) { this.makeSound("mob.attack", 1.0F, (this.random.nextFloat() - this.random.nextFloat()) * 0.2F + 1.0F); return entity.damageEntity(DamageSource.mobAttack(this), 8.0F); diff --git a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityWolf.java b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityWolf.java index 333e4078f..9de6c975f 100644 --- a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityWolf.java +++ b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityWolf.java @@ -241,6 +241,7 @@ public boolean damageEntity(DamageSource damagesource, float f) { @Override public boolean r(Entity entity) { + this.markAttackCooldown(); // WindSpigot - ActivationRange attackTicks immunity boolean flag = entity.damageEntity(DamageSource.mobAttack(this), ((int) this.getAttributeInstance(GenericAttributes.ATTACK_DAMAGE).getValue())); diff --git a/WindSpigot-Server/src/main/java/org/spigotmc/ActivationRange.java b/WindSpigot-Server/src/main/java/org/spigotmc/ActivationRange.java index fa7b0bebb..44a197034 100644 --- a/WindSpigot-Server/src/main/java/org/spigotmc/ActivationRange.java +++ b/WindSpigot-Server/src/main/java/org/spigotmc/ActivationRange.java @@ -176,8 +176,9 @@ public static boolean checkEntityImmunities(Entity entity) { // special cases. if (entity instanceof EntityLiving) { EntityLiving living = (EntityLiving) entity; - if ( /* TODO: Missed mapping? living.attackTicks > 0 || */ living.hurtTicks > 0 - || living.effects.size() > 0) { + // WindSpigot: attackTicks restored on EntityLiving (pre-1.8 AttackTime). + // Keep entities active while melee-attack cooldown is counting down. + if (living.attackTicks > 0 || living.hurtTicks > 0 || living.effects.size() > 0) { return true; } if (entity instanceof EntityCreature && ((EntityCreature) entity).getGoalTarget() != null) { diff --git a/WindSpigot-Server/src/test/java/com/windpvp/windspigot/entity/ActivationRangeAttackTicksTest.java b/WindSpigot-Server/src/test/java/com/windpvp/windspigot/entity/ActivationRangeAttackTicksTest.java new file mode 100644 index 000000000..2b3d27dd3 --- /dev/null +++ b/WindSpigot-Server/src/test/java/com/windpvp/windspigot/entity/ActivationRangeAttackTicksTest.java @@ -0,0 +1,79 @@ +package com.windpvp.windspigot.entity; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import org.junit.Assert; +import org.junit.Test; +import org.spigotmc.ActivationRange; + +import net.minecraft.server.EntityLiving; + +/** + * Regression for ActivationRange {@code attackTicks} immunity (TODO #9). + *

+ * Pre-1.8 {@code EntityLiving.attackTicks} (NBT {@code AttackTime}) was removed by + * Mojang in 1.8, so Spigot left {@code living.attackTicks > 0} commented out as a + * "missed mapping". Without it, mobs can deactivate while still in melee-attack + * cooldown. WindSpigot restores the field and the immunity check. + */ +public class ActivationRangeAttackTicksTest { + + /** Pre-1.8 AttackTime / PathfinderGoalMeleeAttack melee cooldown length. */ + private static final int MELEE_ATTACK_COOLDOWN_TICKS = 20; + + /** + * Mirrors {@link ActivationRange#checkEntityImmunities} living special-case + * gate for combat-related fields (attack / hurt / effects). + */ + static boolean hasLivingCombatImmunity(int attackTicks, int hurtTicks, int effectCount) { + return attackTicks > 0 || hurtTicks > 0 || effectCount > 0; + } + + @Test + public void attackTicksGreaterThanZeroGrantsImmunity() { + Assert.assertTrue(hasLivingCombatImmunity(MELEE_ATTACK_COOLDOWN_TICKS, 0, 0)); + Assert.assertTrue(hasLivingCombatImmunity(1, 0, 0)); + Assert.assertFalse("Zero attackTicks alone must not grant immunity", + hasLivingCombatImmunity(0, 0, 0)); + } + + @Test + public void hurtTicksAndEffectsStillGrantImmunity() { + Assert.assertTrue(hasLivingCombatImmunity(0, 1, 0)); + Assert.assertTrue(hasLivingCombatImmunity(0, 0, 1)); + Assert.assertTrue(hasLivingCombatImmunity(5, 3, 2)); + } + + @Test + public void entityLivingExposesRestoredAttackTicksField() throws Exception { + Field field = EntityLiving.class.getField("attackTicks"); + Assert.assertEquals(int.class, field.getType()); + Assert.assertTrue(Modifier.isPublic(field.getModifiers())); + } + + @Test + public void markAttackCooldownSetsPre18AttackTime() throws Exception { + Method mark = EntityLiving.class.getMethod("markAttackCooldown"); + Assert.assertEquals(void.class, mark.getReturnType()); + + // Contract: cooldown length matches pre-1.8 AttackTime / melee goal (20). + int ticks = MELEE_ATTACK_COOLDOWN_TICKS; + Assert.assertTrue(hasLivingCombatImmunity(ticks, 0, 0)); + for (int i = 0; i < MELEE_ATTACK_COOLDOWN_TICKS; i++) { + ticks--; + } + Assert.assertEquals(0, ticks); + Assert.assertFalse(hasLivingCombatImmunity(ticks, 0, 0)); + } + + @Test + public void activationRangeSourceUsesAttackTicks() throws Exception { + // Compile-time: ActivationRange references EntityLiving.attackTicks. + // Runtime: ensure the public field name the immunity check uses is present. + Assert.assertNotNull(EntityLiving.class.getField("attackTicks")); + Assert.assertNotNull(ActivationRange.class.getMethod("checkEntityImmunities", + net.minecraft.server.Entity.class)); + } +} From a9159fa11972b9bf688ec2ad500be13d22a86525 Mon Sep 17 00:00:00 2001 From: Elias Date: Wed, 14 Oct 2026 19:57:08 +0200 Subject: [PATCH 02/11] Update ActivationRange.java --- .../src/main/java/org/spigotmc/ActivationRange.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/WindSpigot-Server/src/main/java/org/spigotmc/ActivationRange.java b/WindSpigot-Server/src/main/java/org/spigotmc/ActivationRange.java index 44a197034..cec74eb1c 100644 --- a/WindSpigot-Server/src/main/java/org/spigotmc/ActivationRange.java +++ b/WindSpigot-Server/src/main/java/org/spigotmc/ActivationRange.java @@ -176,9 +176,8 @@ public static boolean checkEntityImmunities(Entity entity) { // special cases. if (entity instanceof EntityLiving) { EntityLiving living = (EntityLiving) entity; - // WindSpigot: attackTicks restored on EntityLiving (pre-1.8 AttackTime). - // Keep entities active while melee-attack cooldown is counting down. - if (living.attackTicks > 0 || living.hurtTicks > 0 || living.effects.size() > 0) { + if (living.attackTicks > 0 || living.hurtTicks > 0 + || !living.effects.isEmpty()) { return true; } if (entity instanceof EntityCreature && ((EntityCreature) entity).getGoalTarget() != null) { From 5a89489261f81118dc03537eea52abe38da2d506 Mon Sep 17 00:00:00 2001 From: Elias Date: Wed, 14 Oct 2026 19:57:57 +0200 Subject: [PATCH 03/11] Update EntityIronGolem.java --- .../src/main/java/net/minecraft/server/EntityIronGolem.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityIronGolem.java b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityIronGolem.java index 81406da68..22e24f24a 100644 --- a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityIronGolem.java +++ b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityIronGolem.java @@ -123,7 +123,7 @@ public void a(NBTTagCompound nbttagcompound) { @Override public boolean r(Entity entity) { - this.markAttackCooldown(); // WindSpigot - ActivationRange attackTicks immunity + attackTicks = 20; // WindSpigot this.c = 10; this.world.broadcastEntityEffect(this, (byte) 4); boolean flag = entity.damageEntity(DamageSource.mobAttack(this), 7 + this.random.nextInt(15)); From 597de540aaa893c46210e19b84f218bb69f5e9e6 Mon Sep 17 00:00:00 2001 From: Elias Date: Wed, 14 Oct 2026 19:59:44 +0200 Subject: [PATCH 04/11] Update EntityLiving.java --- .../net/minecraft/server/EntityLiving.java | 31 ++++++------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityLiving.java b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityLiving.java index d01908f6c..f48510a94 100644 --- a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityLiving.java +++ b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityLiving.java @@ -44,13 +44,7 @@ public abstract class EntityLiving extends Entity { public int av; public float aw; public int deathTicks; - /** - * WindSpigot: restored pre-1.8 {@code AttackTime} / {@code attackTicks}. - * Mojang removed the field in 1.8 (cooldown moved into pathfinder goals), but - * Spigot's entity-activation-range still needs an entity-level signal so mobs - * are not deactivated mid melee-attack cooldown. - */ - public int attackTicks; + public int attackTicks; // WindSpigot - restore attackTicks public float ay; public float az; public float aA; @@ -266,13 +260,14 @@ public void K() { } this.aE = this.aF; - if (this.hurtTicks > 0) { - --this.hurtTicks; + // WindSpigot start + if (attackTicks > 0) { + --attackTicks; } + // WindSpigot end - // WindSpigot - pre-1.8 AttackTime countdown (ActivationRange immunity) - if (this.attackTicks > 0) { - --this.attackTicks; + if (this.hurtTicks > 0) { + --this.hurtTicks; } if (this.noDamageTicks > 0 && !(this instanceof EntityPlayer)) { @@ -424,6 +419,7 @@ public void b(NBTTagCompound nbttagcompound) { nbttagcompound.setShort("HurtTime", (short) this.hurtTicks); nbttagcompound.setInt("HurtByTimestamp", this.hurtTimestamp); nbttagcompound.setShort("DeathTime", (short) this.deathTicks); + nbttagcompound.setShort("AttackTime", (short) attackTicks); // WindSpigot nbttagcompound.setFloat("AbsorptionAmount", this.getAbsorptionHearts()); ItemStack[] aitemstack = this.getEquipment(); int i = aitemstack.length; @@ -511,6 +507,7 @@ public void a(NBTTagCompound nbttagcompound) { this.hurtTicks = nbttagcompound.getShort("HurtTime"); this.deathTicks = nbttagcompound.getShort("DeathTime"); + attackTicks = nbttagcompound.getShort("AttackTime"); // WindSpigot this.hurtTimestamp = nbttagcompound.getInt("HurtByTimestamp"); } @@ -1620,19 +1617,11 @@ public void k(float f) { } public boolean r(Entity entity) { - this.markAttackCooldown(); + attackTicks = 20; // WindSpigot this.p(entity); return false; } - /** - * WindSpigot: start/refresh the restored {@link #attackTicks} melee cooldown - * (20 ticks, matching pre-1.8 AttackTime and PathfinderGoalMeleeAttack). - */ - public void markAttackCooldown() { - this.attackTicks = 20; - } - public boolean isSleeping() { return false; } From 9965779c7fd2fa9d68c4a83cc22d825499d1f7cb Mon Sep 17 00:00:00 2001 From: Elias Date: Wed, 14 Oct 2026 20:00:04 +0200 Subject: [PATCH 05/11] Update EntityMonster.java --- .../src/main/java/net/minecraft/server/EntityMonster.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityMonster.java b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityMonster.java index 1cfaa2616..65d44f347 100644 --- a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityMonster.java +++ b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityMonster.java @@ -70,7 +70,7 @@ protected String n(int i) { @Override public boolean r(Entity entity) { - this.markAttackCooldown(); // WindSpigot - ActivationRange attackTicks immunity + attackTicks = 20; // WindSpigot float f = (float) this.getAttributeInstance(GenericAttributes.ATTACK_DAMAGE).getValue(); int i = 0; From d8b9143da46089e19902a407291f959cb8d52d6b Mon Sep 17 00:00:00 2001 From: Elias Date: Wed, 14 Oct 2026 20:00:31 +0200 Subject: [PATCH 06/11] Update EntityOcelot.java --- .../src/main/java/net/minecraft/server/EntityOcelot.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityOcelot.java b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityOcelot.java index 0498ce6d6..6b4558983 100644 --- a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityOcelot.java +++ b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityOcelot.java @@ -110,7 +110,7 @@ protected Item getLoot() { @Override public boolean r(Entity entity) { - this.markAttackCooldown(); // WindSpigot - ActivationRange attackTicks immunity + attackTicks = 20; // WindSpigot return entity.damageEntity(DamageSource.mobAttack(this), 3.0F); } From 328b08c5bf22467af174d466a5f3e5e493d240e9 Mon Sep 17 00:00:00 2001 From: Elias Date: Wed, 14 Oct 2026 20:00:58 +0200 Subject: [PATCH 07/11] Update EntityRabbit.java --- .../src/main/java/net/minecraft/server/EntityRabbit.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityRabbit.java b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityRabbit.java index 76441a05e..5e5b5b414 100644 --- a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityRabbit.java +++ b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityRabbit.java @@ -229,7 +229,7 @@ protected String bp() { @Override public boolean r(Entity entity) { - this.markAttackCooldown(); // WindSpigot - ActivationRange attackTicks immunity + attackTicks = 20; // WindSpigot if (this.getRabbitType() == 99) { this.makeSound("mob.attack", 1.0F, (this.random.nextFloat() - this.random.nextFloat()) * 0.2F + 1.0F); return entity.damageEntity(DamageSource.mobAttack(this), 8.0F); From d223cffd05354fc4ac2f16b7e666a8f94f809f44 Mon Sep 17 00:00:00 2001 From: Elias Date: Wed, 14 Oct 2026 20:01:16 +0200 Subject: [PATCH 08/11] Update EntityWolf.java --- .../src/main/java/net/minecraft/server/EntityWolf.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityWolf.java b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityWolf.java index 9de6c975f..7a9919384 100644 --- a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityWolf.java +++ b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityWolf.java @@ -241,7 +241,7 @@ public boolean damageEntity(DamageSource damagesource, float f) { @Override public boolean r(Entity entity) { - this.markAttackCooldown(); // WindSpigot - ActivationRange attackTicks immunity + attackTicks = 20; // WindSpigot boolean flag = entity.damageEntity(DamageSource.mobAttack(this), ((int) this.getAttributeInstance(GenericAttributes.ATTACK_DAMAGE).getValue())); From e07b4ecfec9d961e12b529c2b2eb6971eebb188e Mon Sep 17 00:00:00 2001 From: Elias Date: Wed, 14 Oct 2026 20:03:15 +0200 Subject: [PATCH 09/11] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3c5b175d0..50254f383 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ See the patches list below. [WindSpigot-0015] Configurable explosion animations and sound [WindSpigot-0016] Configurable weather changes [WindSpigot-0017] Configurable fishing rod speed multiplier +[WindSpigot-0018] Restore attackTicks [PandaSpigot-0115] Break up and make tab spam limits configurable [PandaSpigot-0105] Fix SPIGOT-2622: Only one player receiving the server shutdown message From afe69cc9825d6f4490644657b8ea7b8bc7d2a2a7 Mon Sep 17 00:00:00 2001 From: Elias Date: Wed, 14 Oct 2026 20:16:48 +0200 Subject: [PATCH 10/11] Delete WindSpigot-Server/src/test/java/com/windpvp/windspigot/entity/ActivationRangeAttackTicksTest.java --- .../ActivationRangeAttackTicksTest.java | 79 ------------------- 1 file changed, 79 deletions(-) delete mode 100644 WindSpigot-Server/src/test/java/com/windpvp/windspigot/entity/ActivationRangeAttackTicksTest.java diff --git a/WindSpigot-Server/src/test/java/com/windpvp/windspigot/entity/ActivationRangeAttackTicksTest.java b/WindSpigot-Server/src/test/java/com/windpvp/windspigot/entity/ActivationRangeAttackTicksTest.java deleted file mode 100644 index 2b3d27dd3..000000000 --- a/WindSpigot-Server/src/test/java/com/windpvp/windspigot/entity/ActivationRangeAttackTicksTest.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.windpvp.windspigot.entity; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; - -import org.junit.Assert; -import org.junit.Test; -import org.spigotmc.ActivationRange; - -import net.minecraft.server.EntityLiving; - -/** - * Regression for ActivationRange {@code attackTicks} immunity (TODO #9). - *

- * Pre-1.8 {@code EntityLiving.attackTicks} (NBT {@code AttackTime}) was removed by - * Mojang in 1.8, so Spigot left {@code living.attackTicks > 0} commented out as a - * "missed mapping". Without it, mobs can deactivate while still in melee-attack - * cooldown. WindSpigot restores the field and the immunity check. - */ -public class ActivationRangeAttackTicksTest { - - /** Pre-1.8 AttackTime / PathfinderGoalMeleeAttack melee cooldown length. */ - private static final int MELEE_ATTACK_COOLDOWN_TICKS = 20; - - /** - * Mirrors {@link ActivationRange#checkEntityImmunities} living special-case - * gate for combat-related fields (attack / hurt / effects). - */ - static boolean hasLivingCombatImmunity(int attackTicks, int hurtTicks, int effectCount) { - return attackTicks > 0 || hurtTicks > 0 || effectCount > 0; - } - - @Test - public void attackTicksGreaterThanZeroGrantsImmunity() { - Assert.assertTrue(hasLivingCombatImmunity(MELEE_ATTACK_COOLDOWN_TICKS, 0, 0)); - Assert.assertTrue(hasLivingCombatImmunity(1, 0, 0)); - Assert.assertFalse("Zero attackTicks alone must not grant immunity", - hasLivingCombatImmunity(0, 0, 0)); - } - - @Test - public void hurtTicksAndEffectsStillGrantImmunity() { - Assert.assertTrue(hasLivingCombatImmunity(0, 1, 0)); - Assert.assertTrue(hasLivingCombatImmunity(0, 0, 1)); - Assert.assertTrue(hasLivingCombatImmunity(5, 3, 2)); - } - - @Test - public void entityLivingExposesRestoredAttackTicksField() throws Exception { - Field field = EntityLiving.class.getField("attackTicks"); - Assert.assertEquals(int.class, field.getType()); - Assert.assertTrue(Modifier.isPublic(field.getModifiers())); - } - - @Test - public void markAttackCooldownSetsPre18AttackTime() throws Exception { - Method mark = EntityLiving.class.getMethod("markAttackCooldown"); - Assert.assertEquals(void.class, mark.getReturnType()); - - // Contract: cooldown length matches pre-1.8 AttackTime / melee goal (20). - int ticks = MELEE_ATTACK_COOLDOWN_TICKS; - Assert.assertTrue(hasLivingCombatImmunity(ticks, 0, 0)); - for (int i = 0; i < MELEE_ATTACK_COOLDOWN_TICKS; i++) { - ticks--; - } - Assert.assertEquals(0, ticks); - Assert.assertFalse(hasLivingCombatImmunity(ticks, 0, 0)); - } - - @Test - public void activationRangeSourceUsesAttackTicks() throws Exception { - // Compile-time: ActivationRange references EntityLiving.attackTicks. - // Runtime: ensure the public field name the immunity check uses is present. - Assert.assertNotNull(EntityLiving.class.getField("attackTicks")); - Assert.assertNotNull(ActivationRange.class.getMethod("checkEntityImmunities", - net.minecraft.server.Entity.class)); - } -} From 18eef7ad4f1e74520f56bf134d18a731e59b67b4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 14 Oct 2026 21:11:41 +0000 Subject: [PATCH 11/11] Restore EntityLiving.attackTicks for activation-range immunity Co-authored-by: xii69 --- README.md | 1 + .../java/net/minecraft/server/EntityIronGolem.java | 1 + .../main/java/net/minecraft/server/EntityLiving.java | 10 ++++++++++ .../main/java/net/minecraft/server/EntityMonster.java | 1 + .../main/java/net/minecraft/server/EntityOcelot.java | 1 + .../main/java/net/minecraft/server/EntityRabbit.java | 1 + .../src/main/java/net/minecraft/server/EntityWolf.java | 1 + .../src/main/java/org/spigotmc/ActivationRange.java | 4 ++-- 8 files changed, 18 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3c5b175d0..50254f383 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ See the patches list below. [WindSpigot-0015] Configurable explosion animations and sound [WindSpigot-0016] Configurable weather changes [WindSpigot-0017] Configurable fishing rod speed multiplier +[WindSpigot-0018] Restore attackTicks [PandaSpigot-0115] Break up and make tab spam limits configurable [PandaSpigot-0105] Fix SPIGOT-2622: Only one player receiving the server shutdown message diff --git a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityIronGolem.java b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityIronGolem.java index 7f8e4dfba..22e24f24a 100644 --- a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityIronGolem.java +++ b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityIronGolem.java @@ -123,6 +123,7 @@ public void a(NBTTagCompound nbttagcompound) { @Override public boolean r(Entity entity) { + attackTicks = 20; // WindSpigot this.c = 10; this.world.broadcastEntityEffect(this, (byte) 4); boolean flag = entity.damageEntity(DamageSource.mobAttack(this), 7 + this.random.nextInt(15)); diff --git a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityLiving.java b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityLiving.java index 6b43d7133..f48510a94 100644 --- a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityLiving.java +++ b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityLiving.java @@ -44,6 +44,7 @@ public abstract class EntityLiving extends Entity { public int av; public float aw; public int deathTicks; + public int attackTicks; // WindSpigot - restore attackTicks public float ay; public float az; public float aA; @@ -259,6 +260,12 @@ public void K() { } this.aE = this.aF; + // WindSpigot start + if (attackTicks > 0) { + --attackTicks; + } + // WindSpigot end + if (this.hurtTicks > 0) { --this.hurtTicks; } @@ -412,6 +419,7 @@ public void b(NBTTagCompound nbttagcompound) { nbttagcompound.setShort("HurtTime", (short) this.hurtTicks); nbttagcompound.setInt("HurtByTimestamp", this.hurtTimestamp); nbttagcompound.setShort("DeathTime", (short) this.deathTicks); + nbttagcompound.setShort("AttackTime", (short) attackTicks); // WindSpigot nbttagcompound.setFloat("AbsorptionAmount", this.getAbsorptionHearts()); ItemStack[] aitemstack = this.getEquipment(); int i = aitemstack.length; @@ -499,6 +507,7 @@ public void a(NBTTagCompound nbttagcompound) { this.hurtTicks = nbttagcompound.getShort("HurtTime"); this.deathTicks = nbttagcompound.getShort("DeathTime"); + attackTicks = nbttagcompound.getShort("AttackTime"); // WindSpigot this.hurtTimestamp = nbttagcompound.getInt("HurtByTimestamp"); } @@ -1608,6 +1617,7 @@ public void k(float f) { } public boolean r(Entity entity) { + attackTicks = 20; // WindSpigot this.p(entity); return false; } diff --git a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityMonster.java b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityMonster.java index 77ee5447a..65d44f347 100644 --- a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityMonster.java +++ b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityMonster.java @@ -70,6 +70,7 @@ protected String n(int i) { @Override public boolean r(Entity entity) { + attackTicks = 20; // WindSpigot float f = (float) this.getAttributeInstance(GenericAttributes.ATTACK_DAMAGE).getValue(); int i = 0; diff --git a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityOcelot.java b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityOcelot.java index d33b20650..6b4558983 100644 --- a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityOcelot.java +++ b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityOcelot.java @@ -110,6 +110,7 @@ protected Item getLoot() { @Override public boolean r(Entity entity) { + attackTicks = 20; // WindSpigot return entity.damageEntity(DamageSource.mobAttack(this), 3.0F); } diff --git a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityRabbit.java b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityRabbit.java index 3d6124b8a..5e5b5b414 100644 --- a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityRabbit.java +++ b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityRabbit.java @@ -229,6 +229,7 @@ protected String bp() { @Override public boolean r(Entity entity) { + attackTicks = 20; // WindSpigot if (this.getRabbitType() == 99) { this.makeSound("mob.attack", 1.0F, (this.random.nextFloat() - this.random.nextFloat()) * 0.2F + 1.0F); return entity.damageEntity(DamageSource.mobAttack(this), 8.0F); diff --git a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityWolf.java b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityWolf.java index 333e4078f..7a9919384 100644 --- a/WindSpigot-Server/src/main/java/net/minecraft/server/EntityWolf.java +++ b/WindSpigot-Server/src/main/java/net/minecraft/server/EntityWolf.java @@ -241,6 +241,7 @@ public boolean damageEntity(DamageSource damagesource, float f) { @Override public boolean r(Entity entity) { + attackTicks = 20; // WindSpigot boolean flag = entity.damageEntity(DamageSource.mobAttack(this), ((int) this.getAttributeInstance(GenericAttributes.ATTACK_DAMAGE).getValue())); diff --git a/WindSpigot-Server/src/main/java/org/spigotmc/ActivationRange.java b/WindSpigot-Server/src/main/java/org/spigotmc/ActivationRange.java index fa7b0bebb..cec74eb1c 100644 --- a/WindSpigot-Server/src/main/java/org/spigotmc/ActivationRange.java +++ b/WindSpigot-Server/src/main/java/org/spigotmc/ActivationRange.java @@ -176,8 +176,8 @@ public static boolean checkEntityImmunities(Entity entity) { // special cases. if (entity instanceof EntityLiving) { EntityLiving living = (EntityLiving) entity; - if ( /* TODO: Missed mapping? living.attackTicks > 0 || */ living.hurtTicks > 0 - || living.effects.size() > 0) { + if (living.attackTicks > 0 || living.hurtTicks > 0 + || !living.effects.isEmpty()) { return true; } if (entity instanceof EntityCreature && ((EntityCreature) entity).getGoalTarget() != null) {