Skip to content

Commit be847f2

Browse files
committed
Implement EntityType checking for EntityClassGroup
Add EntityClassGroups for future Projectile optimizations
1 parent ef9b40f commit be847f2

File tree

8 files changed

+195
-61
lines changed

8 files changed

+195
-61
lines changed

common/src/main/java/net/caffeinemc/mods/lithium/common/entity/EntityClassGroup.java

Lines changed: 79 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,134 @@
11
package net.caffeinemc.mods.lithium.common.entity;
22

3+
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
34
import it.unimi.dsi.fastutil.objects.Reference2ByteOpenHashMap;
5+
import it.unimi.dsi.fastutil.objects.ReferenceReferenceImmutablePair;
46
import net.caffeinemc.mods.lithium.common.reflection.ReflectionUtil;
57
import net.caffeinemc.mods.lithium.common.services.PlatformMappingInformation;
68
import net.minecraft.world.entity.Entity;
9+
import net.minecraft.world.entity.EntityType;
710
import net.minecraft.world.entity.boss.enderdragon.EnderDragon;
811
import net.minecraft.world.entity.monster.Shulker;
912
import net.minecraft.world.entity.projectile.windcharge.BreezeWindCharge;
1013
import net.minecraft.world.entity.projectile.windcharge.WindCharge;
1114
import net.minecraft.world.entity.vehicle.Minecart;
15+
import org.jetbrains.annotations.Nullable;
1216

13-
import java.util.Objects;
14-
import java.util.function.Predicate;
17+
import java.util.function.BiPredicate;
18+
import java.util.function.Supplier;
1519
import java.util.logging.Logger;
1620

1721
/**
18-
* Class for grouping Entity classes by some property for use in TypeFilterableList
22+
* Class for grouping Entity classes and Entity types by some property for use in TypeFilterableList
1923
* It is intended that an EntityClassGroup acts as if it was immutable, however we cannot predict which subclasses of
20-
* Entity might appear. Therefore we evaluate whether a class belongs to the class group when it is first seen.
24+
* Entity might appear. Therefore, we evaluate whether a class belongs to the class group when it is first seen.
2125
* Once a class was evaluated the result of it is cached and cannot be changed.
2226
*
2327
* @author 2No2Name
2428
*/
2529
public class EntityClassGroup {
30+
31+
private static final byte ABSENT_VALUE = (byte) 3;
32+
2633
public static final EntityClassGroup CUSTOM_COLLIDE_LIKE_MINECART_BOAT_WINDCHARGE; //aka entities that will attempt to collide with all other entities when moving
2734

2835
static {
2936
String remapped_collidesWith = PlatformMappingInformation.INSTANCE.mapMethodName("intermediary", "net.minecraft.class_1297", "method_30949", "(Lnet/minecraft/class_1297;)Z", "canCollideWith");
3037
CUSTOM_COLLIDE_LIKE_MINECART_BOAT_WINDCHARGE = new EntityClassGroup(
31-
(Class<?> entityClass) -> ReflectionUtil.hasMethodOverride(entityClass, Entity.class, true, remapped_collidesWith, Entity.class));
38+
(Class<?> entityClass, Supplier<EntityType<?>> entityType) -> ReflectionUtil.hasMethodOverride(entityClass, Entity.class, true, remapped_collidesWith, Entity.class));
3239

3340
//sanity check: in case intermediary mappings changed, we fail
34-
if ((!CUSTOM_COLLIDE_LIKE_MINECART_BOAT_WINDCHARGE.contains(Minecart.class))) {
41+
if ((!CUSTOM_COLLIDE_LIKE_MINECART_BOAT_WINDCHARGE.contains(Minecart.class, EntityType.MINECART))) {
3542
throw new AssertionError();
3643
}
37-
if ((!CUSTOM_COLLIDE_LIKE_MINECART_BOAT_WINDCHARGE.contains(WindCharge.class)) || (!CUSTOM_COLLIDE_LIKE_MINECART_BOAT_WINDCHARGE.contains(BreezeWindCharge.class))) {
44+
if ((!CUSTOM_COLLIDE_LIKE_MINECART_BOAT_WINDCHARGE.contains(WindCharge.class, EntityType.WIND_CHARGE)) || (!CUSTOM_COLLIDE_LIKE_MINECART_BOAT_WINDCHARGE.contains(BreezeWindCharge.class, EntityType.BREEZE_WIND_CHARGE))) {
3845
throw new AssertionError();
3946
}
40-
if ((CUSTOM_COLLIDE_LIKE_MINECART_BOAT_WINDCHARGE.contains(Shulker.class))) {
41-
//should not throw an Error here, because another mod *could* add the method to ShulkerEntity. Wwarning when this sanity check fails.
47+
if ((CUSTOM_COLLIDE_LIKE_MINECART_BOAT_WINDCHARGE.contains(Shulker.class, EntityType.SHULKER))) {
48+
//should not throw an Error here, because another mod *could* add the method to ShulkerEntity. Warning when this sanity check fails.
4249
Logger.getLogger("Lithium EntityClassGroup").warning("Either Lithium EntityClassGroup is broken or something else gave Shulkers the minecart-like collision behavior.");
4350
}
4451
CUSTOM_COLLIDE_LIKE_MINECART_BOAT_WINDCHARGE.clear();
4552
}
4653

47-
private final Predicate<Class<?>> classFitEvaluator;
48-
private volatile Reference2ByteOpenHashMap<Class<?>> class2GroupContains;
54+
private final BiPredicate<Class<?>, Supplier<EntityType<?>>> classAndTypeFitEvaluator;
55+
private volatile Reference2ByteOpenHashMap<Class<?>> class2GroupContains; // 0: Not contained (decision based on class only), 1: Contained (decision based on class only), 2: Check containedClassAndTypePairs (decision based on entity type)
56+
private volatile @Nullable ObjectOpenHashSet<ReferenceReferenceImmutablePair<Class<?>, EntityType<?>>> containedClassAndTypePairs; //only used if decision is based on entity type
4957

50-
public EntityClassGroup(Predicate<Class<?>> classFitEvaluator) {
51-
this.class2GroupContains = new Reference2ByteOpenHashMap<>();
52-
Objects.requireNonNull(classFitEvaluator);
53-
this.classFitEvaluator = classFitEvaluator;
58+
public EntityClassGroup(BiPredicate<Class<?>, Supplier<EntityType<?>>> classAndTypeFitEvaluator) {
59+
this.classAndTypeFitEvaluator = classAndTypeFitEvaluator;
60+
this.clear();
5461
}
5562

5663
public void clear() {
5764
this.class2GroupContains = new Reference2ByteOpenHashMap<>();
65+
this.class2GroupContains.defaultReturnValue(ABSENT_VALUE);
66+
this.containedClassAndTypePairs = null;
5867
}
5968

60-
public boolean contains(Class<?> entityClass) {
61-
byte contains = this.class2GroupContains.getOrDefault(entityClass, (byte) 2);
62-
if (contains != 2) {
69+
public boolean contains(Entity entity) {
70+
return this.contains(entity.getClass(), entity.getType());
71+
}
72+
73+
public boolean contains(Class<?> entityClass, EntityType<?> entityType) {
74+
byte contains = this.class2GroupContains.getByte(entityClass);
75+
if (contains < 2) {
6376
return contains == 1;
77+
}
78+
return checkDetailedContains(entityClass, entityType, contains);
79+
}
80+
81+
private boolean checkDetailedContains(Class<?> entityClass, EntityType<?> entityType, byte contains) {
82+
if (contains == ABSENT_VALUE) {
83+
return this.testAndAddClass(entityClass, entityType);
6484
} else {
65-
return this.testAndAddClass(entityClass);
85+
var containedPairs = this.containedClassAndTypePairs;
86+
return containedPairs != null && containedPairs.contains(ReferenceReferenceImmutablePair.of(entityClass, entityType));
6687
}
6788
}
6889

69-
boolean testAndAddClass(Class<?> entityClass) {
70-
byte contains;
90+
boolean testAndAddClass(Class<?> entityClass, EntityType<?> entityType) {
91+
boolean contains;
7192
//synchronizing here to avoid multiple threads replacing the map at the same time, and therefore possibly undoing progress
7293
//it could also be fixed by using an AtomicReference's CAS, but we are writing very rarely (less than 150 times for the total game runtime in vanilla)
7394
synchronized (this) {
7495
//test the same condition again after synchronizing, as the collection might have been updated while this thread blocked
75-
contains = this.class2GroupContains.getOrDefault(entityClass, (byte) 2);
76-
if (contains != 2) {
77-
return contains == 1;
96+
if (this.class2GroupContains.containsKey(entityClass)) {
97+
return this.contains(entityClass, entityType);
7898
}
99+
79100
//construct new map instead of updating the old map to avoid thread safety problems
80101
//the map is not modified after publication
81102
Reference2ByteOpenHashMap<Class<?>> newMap = this.class2GroupContains.clone();
82-
contains = this.classFitEvaluator.test(entityClass) ? (byte) 1 : (byte) 0;
83-
newMap.put(entityClass, contains);
84-
//publish the new map in a volatile field, so that all threads reading after this write can also see all changes to the map done before the write
103+
boolean[] accessedEntityType = new boolean[1];
104+
Supplier<EntityType<?>> entityTypeSupplier = () -> {
105+
accessedEntityType[0] = true;
106+
return entityType;
107+
};
108+
contains = this.classAndTypeFitEvaluator.test(entityClass, entityTypeSupplier);
109+
byte containsInfo = contains ? (byte) 1 : (byte) 0;
110+
if (accessedEntityType[0]) {
111+
containsInfo = 2; //2: The class group decision is based on both class and type
112+
113+
ObjectOpenHashSet<ReferenceReferenceImmutablePair<Class<?>, EntityType<?>>> newPairSet = this.containedClassAndTypePairs;
114+
newPairSet = newPairSet == null ? new ObjectOpenHashSet<>() : newPairSet.clone();
115+
if (contains) {
116+
newPairSet.add(ReferenceReferenceImmutablePair.of(entityClass, entityType));
117+
//publish the new set in a volatile field, so that all threads reading after this write can also see all changes to the map done beforehand
118+
//since modification on happens in the synchronized block, progress won't be lost
119+
this.containedClassAndTypePairs = newPairSet;
120+
}
121+
}
122+
123+
byte previousContainsInfo = newMap.put(entityClass, containsInfo);
124+
if (previousContainsInfo != ABSENT_VALUE && previousContainsInfo != containsInfo) {
125+
throw new IllegalStateException("Entity class group class fit evaluator must be a pure function! Class fit for " + entityClass + " changed from " + previousContainsInfo + " to " + containsInfo + " when evaluating for " + entityType + "!");
126+
}
127+
128+
//publish the new map in a volatile field, so that all threads reading after this write can also see all changes to the map done beforehand
85129
this.class2GroupContains = newMap;
86130
}
87-
return contains == 1;
131+
return contains;
88132
}
89133

90134
public static class NoDragonClassGroup extends EntityClassGroup {
@@ -93,17 +137,19 @@ public static class NoDragonClassGroup extends EntityClassGroup {
93137
static {
94138
String remapped_canBeCollidedWith = PlatformMappingInformation.INSTANCE.mapMethodName("intermediary", "net.minecraft.class_1297", "method_30948", "()Z", "canBeCollidedWith");
95139
BOAT_SHULKER_LIKE_COLLISION = new NoDragonClassGroup(
96-
(Class<?> entityClass) -> ReflectionUtil.hasMethodOverride(entityClass, Entity.class, true, remapped_canBeCollidedWith));
140+
(Class<?> entityClass, Supplier<EntityType<?>> entityType) -> ReflectionUtil.hasMethodOverride(entityClass, Entity.class, true, remapped_canBeCollidedWith));
97141

98-
if ((!BOAT_SHULKER_LIKE_COLLISION.contains(Shulker.class))) {
142+
if ((!BOAT_SHULKER_LIKE_COLLISION.contains(Shulker.class, EntityType.SHULKER))) {
99143
throw new AssertionError();
100144
}
101145
BOAT_SHULKER_LIKE_COLLISION.clear();
102146
}
103147

104-
public NoDragonClassGroup(Predicate<Class<?>> classFitEvaluator) {
105-
super(classFitEvaluator);
106-
if (classFitEvaluator.test(EnderDragon.class)) {
148+
public NoDragonClassGroup(BiPredicate<Class<?>, Supplier<EntityType<?>>> classAndTypeFitEvaluator) {
149+
super(classAndTypeFitEvaluator);
150+
if (classAndTypeFitEvaluator.test(EnderDragon.class, () -> {
151+
throw new IllegalArgumentException("EntityClassGroup.NoDragonClassGroup cannot be initialized: Must exclude EnderDragonEntity without checking entity type!");
152+
})) {
107153
throw new IllegalArgumentException("EntityClassGroup.NoDragonClassGroup cannot be initialized: Must exclude EnderDragonEntity!");
108154
}
109155
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package net.caffeinemc.mods.lithium.common.entity.projectile;
2+
3+
import net.caffeinemc.mods.lithium.common.entity.EntityClassGroup;
4+
import net.caffeinemc.mods.lithium.common.reflection.ReflectionUtil;
5+
import net.caffeinemc.mods.lithium.common.services.PlatformMappingInformation;
6+
import net.minecraft.tags.EntityTypeTags;
7+
import net.minecraft.world.entity.Entity;
8+
import net.minecraft.world.entity.EntityType;
9+
import net.minecraft.world.entity.Interaction;
10+
import net.minecraft.world.entity.boss.enderdragon.EnderDragon;
11+
import net.minecraft.world.entity.projectile.AbstractArrow;
12+
import net.minecraft.world.entity.projectile.AbstractHurtingProjectile;
13+
import net.minecraft.world.entity.projectile.Projectile;
14+
import net.minecraft.world.entity.projectile.ShulkerBullet;
15+
import net.minecraft.world.entity.projectile.windcharge.AbstractWindCharge;
16+
17+
import java.util.function.Supplier;
18+
19+
public class ProjectileEntityClassGroup {
20+
21+
/**
22+
* Projectiles that do not override {@link net.minecraft.world.entity.projectile.Projectile#canHitEntity(Entity)} or only do so to restrict the set of hittable entities
23+
*/
24+
@SuppressWarnings("JavadocReference")
25+
public static final EntityClassGroup OPTIMIZED_PROJECTILES;
26+
/**
27+
* Projectiles that override {@link Entity#canBeHitByProjectile()} or override {@link Entity#isPickable()} in a way that makes them hittable by projectiles.
28+
*/
29+
public static final EntityClassGroup CAN_MAYBE_BE_HIT_BY_OPTIMIZED_PROJECTILE;
30+
31+
32+
static {
33+
String remapped_canHitEntity = PlatformMappingInformation.INSTANCE.mapMethodName("intermediary", "net.minecraft.class_1676", "method_26958", "(Lnet/minecraft/class_1297;)Z", "canHitEntity");
34+
OPTIMIZED_PROJECTILES = new EntityClassGroup(
35+
(Class<?> entityClass, Supplier<EntityType<?>> entityType) -> {
36+
Class<?> parentClass = Projectile.class;
37+
if (AbstractHurtingProjectile.class.isAssignableFrom(entityClass)) {
38+
parentClass = AbstractHurtingProjectile.class;
39+
if (AbstractWindCharge.class.isAssignableFrom(entityClass)) {
40+
parentClass = AbstractWindCharge.class;
41+
}
42+
} else if (AbstractArrow.class.isAssignableFrom(entityClass)) {
43+
parentClass = AbstractArrow.class;
44+
} else if (ShulkerBullet.class.isAssignableFrom(entityClass)) {
45+
parentClass = ShulkerBullet.class;
46+
}
47+
48+
return !ReflectionUtil.hasMethodOverride(entityClass, parentClass, true, remapped_canHitEntity, Entity.class);
49+
});
50+
51+
String remapped_canBeHitByProjectile = PlatformMappingInformation.INSTANCE.mapMethodName("intermediary", "net.minecraft.class_1297", "method_49108", "()Z", "canBeHitByProjectile");
52+
String remapped_isPickable = PlatformMappingInformation.INSTANCE.mapMethodName("intermediary", "net.minecraft.class_1297", "method_5863", "()Z", "isPickable");
53+
CAN_MAYBE_BE_HIT_BY_OPTIMIZED_PROJECTILE = new EntityClassGroup(
54+
(entityClass, entityType) -> {
55+
Class<?> parentClass_isPickable = Entity.class;
56+
if (Interaction.class == entityClass) {
57+
return false;
58+
}
59+
if (ReflectionUtil.hasMethodOverride(entityClass, Entity.class, true, remapped_canBeHitByProjectile)) {
60+
return true;
61+
}
62+
if (EnderDragon.class == entityClass) {
63+
return false;
64+
}
65+
if (Projectile.class.isAssignableFrom(entityClass)) {
66+
parentClass_isPickable = Projectile.class;
67+
if (AbstractArrow.class.isAssignableFrom(entityClass)) {
68+
parentClass_isPickable = AbstractArrow.class;
69+
}
70+
if (entityType.get().is(EntityTypeTags.REDIRECTABLE_PROJECTILE)) {
71+
return true;
72+
}
73+
}
74+
return ReflectionUtil.hasMethodOverride(entityClass, parentClass_isPickable, true, remapped_isPickable);
75+
});
76+
}
77+
}

common/src/main/java/net/caffeinemc/mods/lithium/common/entity/pushable/PushableEntityClassGroup.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,19 @@
44
import net.caffeinemc.mods.lithium.common.reflection.ReflectionUtil;
55
import net.caffeinemc.mods.lithium.common.services.PlatformMappingInformation;
66
import net.minecraft.world.entity.Entity;
7+
import net.minecraft.world.entity.EntityType;
78
import net.minecraft.world.entity.LivingEntity;
89
import net.minecraft.world.entity.ambient.Bat;
910
import net.minecraft.world.entity.boss.enderdragon.EnderDragon;
1011
import net.minecraft.world.entity.decoration.ArmorStand;
1112
import net.minecraft.world.entity.player.Player;
1213

14+
import java.util.function.Supplier;
15+
1316
public class PushableEntityClassGroup {
1417

1518
/**
16-
* Contains Entity Classes that use {@link LivingEntity#isPushable()} ()} to determine their pushability state
19+
* Contains Entity Classes that use {@link LivingEntity#isPushable()} to determine their pushability state
1720
* and use {@link LivingEntity#onClimbable()} to determine their climbing state and are never spectators (no players).
1821
* <p>
1922
* LivingEntity, but not Players and not Subclasses with different pushability calculations
@@ -31,7 +34,7 @@ public class PushableEntityClassGroup {
3134
String remapped_isClimbing = PlatformMappingInformation.INSTANCE.mapMethodName("intermediary", "net.minecraft.class_1309", "method_6101", "()Z", "onClimbable");
3235
String remapped_isPushable = PlatformMappingInformation.INSTANCE.mapMethodName("intermediary", "net.minecraft.class_1297", "method_5810", "()Z", "isPushable");
3336
CACHABLE_UNPUSHABILITY = new EntityClassGroup(
34-
(Class<?> entityClass) -> {
37+
(Class<?> entityClass, Supplier<EntityType<?>> entityType) -> {
3538
if (LivingEntity.class.isAssignableFrom(entityClass) && !Player.class.isAssignableFrom(entityClass)) {
3639
if (!ReflectionUtil.hasMethodOverride(entityClass, LivingEntity.class, true, remapped_isPushable)) {
3740
if (!ReflectionUtil.hasMethodOverride(entityClass, LivingEntity.class, true, remapped_isClimbing)) {
@@ -42,7 +45,7 @@ public class PushableEntityClassGroup {
4245
return false;
4346
});
4447
MAYBE_PUSHABLE = new EntityClassGroup(
45-
(Class<?> entityClass) -> {
48+
(Class<?> entityClass, Supplier<EntityType<?>> entityType) -> {
4649
if (ReflectionUtil.hasMethodOverride(entityClass, Entity.class, true, remapped_isPushable)) {
4750
if (EnderDragon.class.isAssignableFrom(entityClass)) {
4851
return false;

0 commit comments

Comments
 (0)