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
+118 -2
View File
@@ -65,6 +65,21 @@ public class PPTest extends JavaPlugin {
scenes.put("chests_e", new Scene("chests_e", this::chests, 16, 5, 3, 4.5, 0, 3));
scenes.put("chests_w", new Scene("chests_w", this::chests, -7, 5, 3, 4.5, 0, 3));
scenes.put("translucent", new Scene("translucent", this::translucent, 4.5, 2.8, -5.5, 4.5, 0.8, 2));
// Projectile orientation (vanilla ArrowRenderer/ThrownTridentRenderer): flying stars around gold pillars
// (tips point radially outward, rings level / +35 deg / -35 deg) and arrows/tridents stuck in stone pillars.
scenes.put("proj_fly_top", new Scene("proj_fly_top", this::projFly, 6.5, 13, 2.2, 6.5, 0, 2));
scenes.put("proj_fly_side", new Scene("proj_fly_side", this::projFly, 6.5, 2.6, -8, 6.5, 2.4, 2));
scenes.put("proj_fly_obl", new Scene("proj_fly_obl", this::projFly, -2, 6, -6, 6.5, 2, 2));
scenes.put("proj_stuck_nw", new Scene("proj_stuck_nw", this::projStuck, -3, 6, -6, 8, 1.5, 2));
scenes.put("proj_stuck_se", new Scene("proj_stuck_se", this::projStuck, 19, 6, 10, 8, 1.5, 2));
scenes.put("proj_stuck_ne", new Scene("proj_stuck_ne", this::projStuck, 19, 6, -6, 8, 1.5, 2));
scenes.put("proj_fly_gold", new Scene("proj_fly_gold", this::projFly, 2.6, 8.5, 2.5, 2.5, 0, 2.5));
scenes.put("proj_fly_gold_side", new Scene("proj_fly_gold_side", this::projFly, 2.5, 2.6, -3.5, 2.5, 2.4, 2.5));
scenes.put("proj_stuck_close_a", new Scene("proj_stuck_close_a", this::projStuck, -2.5, 2.4, -2.5, 2.5, 1.6, 1));
scenes.put("proj_stuck_close_t", new Scene("proj_stuck_close_t", this::projStuck, 18.5, 2.6, -3, 13.5, 1.6, 1));
// Rendered 4 ticks after the shots: freshly stuck arrows still wobble (vanilla shakeTime -> pitch offset).
scenes.put("proj_shake", new Scene("proj_shake", this::projStuck, -2.5, 2.4, -2.5, 2.5, 1.6, 1));
scenes.put("proj_stuck_sw", new Scene("proj_stuck_sw", this::projStuck, -3, 6, 10, 8, 1.5, 2));
}
@Override
@@ -94,7 +109,7 @@ public class PPTest extends JavaPlugin {
t.printStackTrace();
}
// Let one tick pass (passengers/leashes settle), then render.
Bukkit.getScheduler().runTaskLater(this, () -> render(sc, o), 10);
Bukkit.getScheduler().runTaskLater(this, () -> render(sc, o), sc.name.equals("proj_shake") ? 4 : sc.name.startsWith("proj_") ? 30 : 10);
}
private void render(Scene sc, Location o) {
@@ -102,7 +117,7 @@ public class PPTest extends JavaPlugin {
Location eye = o.clone().add(sc.camX, sc.camY, sc.camZ);
Vector look = o.clone().add(sc.lookX, sc.lookY, sc.lookZ).toVector().subtract(eye.toVector());
eye.setDirection(look);
RenderJob job = r.prepare(eye, new Resolution(480, 320), null);
RenderJob job = r.prepare(eye, sc.name.startsWith("proj_") ? new Resolution(960, 640) : new Resolution(480, 320), null);
if (sc.name.equals("dbg")) {
try {
java.lang.reflect.Field f = DefaultScreenRenderer.class.getDeclaredField("objectBaker");
@@ -124,6 +139,30 @@ public class PPTest extends JavaPlugin {
getLogger().warning("dbg: " + t);
}
}
if (sc.name.startsWith("proj_"))
for (Entity e : w.getNearbyEntities(o.clone().add(8, 3, 2), 12, 8, 12))
if (e instanceof Projectile pr) {
Vector v = pr.getVelocity();
getLogger().info(String.format("proj %s yaw=%.1f pitch=%.1f velYaw=%.1f velPitch=%.1f ground=%s",
e.getType(), e.getLocation().getYaw(), e.getLocation().getPitch(),
Math.toDegrees(Math.atan2(v.getX(), v.getZ())),
Math.toDegrees(Math.atan2(v.getY(), Math.hypot(v.getX(), v.getZ()))),
pr instanceof AbstractArrow a && a.isInBlock()));
}
if (sc.name.equals("proj_shake"))
for (var st : job.entities())
if (st.typeKey().contains("arrow"))
getLogger().info(String.format("state %s pitch=%.2f", st.typeKey(), st.pitch()));
if (sc.name.equals("proj_shake"))
for (Entity e : w.getNearbyEntities(o.clone().add(8, 3, 2), 12, 8, 12))
if (e instanceof AbstractArrow a && !(e instanceof Trident))
try {
Object h = e.getClass().getMethod("getHandle").invoke(e);
getLogger().info(String.format("shake %s pitch=%.2f shakeTime=%d", e.getType(), e.getLocation().getPitch(),
h.getClass().getField("shakeTime").getInt(h)));
} catch (ReflectiveOperationException ex) {
getLogger().warning("shake: " + ex);
}
getLogger().info("scene " + sc.name + ": " + job.entities().size() + " entities, " + job.objects().size()
+ " objects, " + job.blockEntities().size() + " block entities");
Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
@@ -573,4 +612,81 @@ public class PPTest extends JavaPlugin {
t.text(Component.text("Default background"));
t.setBillboard(Display.Billboard.CENTER);
}
// --- projectiles -----------------------------------------------------------------------------
/** Spawns a projectile at p moving along dir, rotated like vanilla Projectile#shoot (yaw atan2(x,z), pitch up). */
private Projectile projectile(Location p, Vector dir, double speed, boolean gravity, Class<? extends Projectile> type) {
Vector d = dir.clone().normalize();
Location l = p.clone();
l.setYaw((float) Math.toDegrees(Math.atan2(d.getX(), d.getZ())));
l.setPitch((float) Math.toDegrees(Math.atan2(d.getY(), Math.hypot(d.getX(), d.getZ()))));
Projectile pr = w.spawn(l, type, e -> {
e.setPersistent(false);
e.setGravity(gravity);
e.setVelocity(d.clone().multiply(speed));
if (e instanceof AbstractArrow a) a.setPickupStatus(AbstractArrow.PickupStatus.DISALLOWED);
});
pr.setRotation(l.getYaw(), l.getPitch()); // spawning resets it to 0 (it would only lerp over ~20 ticks)
return pr;
}
private static Vector dir(double yawDeg, double pitchDeg) {
double y = Math.toRadians(yawDeg), p = Math.toRadians(pitchDeg);
return new Vector(Math.sin(y) * Math.cos(p), Math.sin(p), Math.cos(y) * Math.cos(p));
}
/** Hovering (almost still, no gravity) arrows, spectral arrows and tridents pointing radially away from a pillar. */
private void projFly(Location o) {
for (int y = 0; y <= 4; y++) {
o.clone().add(2, y, 2).getBlock().setType(Material.GOLD_BLOCK, false);
o.clone().add(11, y, 2).getBlock().setType(Material.DIAMOND_BLOCK, false);
}
double[][] rings = {{2.5, 0}, {4.3, 35}, {0.9, -35}}; // height, pitch
for (double[] ring : rings)
for (int i = 0; i < 8; i++) {
double yaw = i * 45;
Vector d = dir(yaw, ring[1]);
Vector h = dir(yaw, 0);
Location a = o.clone().add(2.5 + h.getX() * 1.3, ring[0], 2.5 + h.getZ() * 1.3);
projectile(a, d, 1e-4, false, (Class<? extends AbstractArrow>) (i % 2 == 0 ? Arrow.class : SpectralArrow.class));
Location t = o.clone().add(11.5 + h.getX() * 2.1, ring[0], 2.5 + h.getZ() * 2.1);
projectile(t, d, 1e-4, false, Trident.class);
}
}
/** Arrows and tridents shot into stone pillars from all four sides (rising, level+sideways, falling) and the floor. */
private void projStuck(Location o) {
for (int x = 0; x < 3; x++)
for (int z = 0; z < 3; z++)
for (int y = 0; y < 4; y++) {
o.clone().add(1 + x, y, 1 + z).getBlock().setType(Material.STONE, false);
o.clone().add(12 + x, y, 1 + z).getBlock().setType(Material.STONE, false);
}
double[][] pillars = {{2.5, 2.5}, {13.5, 2.5}}; // centres; arrows left, tridents right
for (int pi = 0; pi < 2; pi++) {
boolean trident = pi == 1;
double cx = pillars[pi][0], cz = pillars[pi][1];
for (int side = 0; side < 4; side++) {
double out = side * 90; // yaw (projectile convention) pointing away from the pillar
Vector n = dir(out, 0);
Vector t = new Vector(n.getZ(), 0, -n.getX());
double dist = 1.5 + 3.5;
// rising from low, level but 20 deg sideways, falling from high
double[][] shots = {{-1, 0.6, 25, 0}, {0, 1.6, 0, 20}, {1, 3.4, -25, 0}};
for (double[] sh : shots) {
Location p = o.clone().add(cx + n.getX() * dist + t.getX() * sh[0], sh[1], cz + n.getZ() * dist + t.getZ() * sh[0]);
Vector d = dir(out + 180 + sh[3], sh[2]);
if (trident) projectile(p, d, 2.0, true, Trident.class);
else projectile(p, d, 1.5, true, (Class<? extends AbstractArrow>) (side % 2 == 0 ? Arrow.class : SpectralArrow.class));
}
// steep shot into the floor, diagonally away from the pillar
Vector diag = dir(out + 45, 0);
Location p = o.clone().add(cx + diag.getX() * 2.6, 3, cz + diag.getZ() * 2.6);
Vector d = dir(out + 45, trident ? -70 : -60);
if (trident) projectile(p, d, 2.0, true, Trident.class);
else projectile(p, d, 1.5, true, Arrow.class);
}
}
}
}