match vanilla arrow/trident orientation incl. arrow impact shake, add projectile test scenes

This commit is contained in:
MineTec committed 2026-11-04 14:26:21 +01:00
1 parent addebffba1
commit fbea9dfe08
3 files changed
+154 -3

No files matched your search

@@ -134,6 +134,9 @@ public final class CemBaker implements EntityBaker<EntityState> {
place = Affine.translation(s.x(), s.y(), s.z())
.mul(Affine.scale(extra))
.mul(Affine.rotY(yawSign * Math.toRadians(s.bodyYaw())));
// Projectiles (verified against ArrowRenderer/ThrownTridentRenderer 26.3): T0 = rotY(-90)[·rotZ(90)],
// pitchAxis = -X, so rotY(yaw)·rotAxis(-X, pitch)·T0 == vanilla rotY(yRot-90)·rotZ(xRot[+90]) with the
// raw projectile rotation (yaw atan2(dx,dz), pitch positive up); origin = entity position, no offset.
if (info != null && info.pitchAxis() != null && s.pitch() != 0) {
double[] a = info.pitchAxis();
place = place.mul(Affine.rotAxis(a[0], a[1], a[2], Math.toRadians(s.pitch())));
@@ -19,6 +19,7 @@ import java.util.function.DoubleSupplier;
import org.bukkit.Keyed;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.AbstractArrow;
import org.bukkit.entity.AbstractHorse;
import org.bukkit.entity.Ageable;
import org.bukkit.entity.ArmorStand;
@@ -47,6 +48,7 @@ import org.bukkit.entity.Shulker;
import org.bukkit.entity.Slime;
import org.bukkit.entity.Snowman;
import org.bukkit.entity.TraderLlama;
import org.bukkit.entity.Trident;
import org.bukkit.entity.TropicalFish;
import org.bukkit.entity.Villager;
import org.bukkit.entity.Wolf;
@@ -304,7 +306,7 @@ public final class EntitySnapshotBuilder {
bodyEquip,
equipment,
invisible,
loc.getPitch(),
loc.getPitch() + arrowShake(e),
bodyDye,
tint2,
e.isInsideVehicle(),
@@ -406,6 +408,36 @@ public final class EntitySnapshotBuilder {
}
}
/** Per NMS entity class: its public {@code shakeTime} field (AbstractArrow), or null. */
private static final ClassValue<java.lang.reflect.Field> SHAKE_FIELD = new ClassValue<>() {
@Override
protected java.lang.reflect.Field computeValue(Class<?> c) {
try {
return c.getField("shakeTime");
} catch (NoSuchFieldException ex) {
return null;
}
}
};
/**
* Vanilla impact wobble of a freshly stuck arrow (ArrowModel#setupAnim, partialTick 0): the model root turns
* by {@code -sin(3·shake)·shake} degrees about its z axis — the same axis as the pitch, so it is added to it.
* {@code shakeTime} (7 → 0 ticks after hitting a block) has no API; read from the server entity. Tridents
* ignore it (ThrownTridentRenderer has no shake).
*/
private static float arrowShake(Entity e) {
if (!(e instanceof AbstractArrow) || e instanceof Trident) return 0;
try {
Object handle = e.getClass().getMethod("getHandle").invoke(e);
java.lang.reflect.Field f = SHAKE_FIELD.get(handle.getClass());
int shake = f == null ? 0 : f.getInt(handle);
return shake <= 0 ? 0 : (float) (-Math.sin(shake * 3.0) * shake);
} catch (ReflectiveOperationException | RuntimeException ex) {
return 0;
}
}
/** Per entity class: its {@code getVariant()} / {@code getWeatheringState()} getter, or null. */
private static final ClassValue<java.lang.reflect.Method> VARIANT_GETTER = new ClassValue<>() {
@Override