From fbea9dfe0833c9e06057e8b325bdda7fc73704c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Wed, 4 Nov 2026 14:26:21 +0100 Subject: [PATCH] match vanilla arrow/trident orientation incl. arrow impact shake, add projectile test scenes --- .../pixelpics/render/entity/cem/CemBaker.java | 3 + .../snapshot/EntitySnapshotBuilder.java | 34 ++++- tools/edgecase-plugin/pptest/PPTest.java | 120 +++++++++++++++++- 3 files changed, 154 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/CemBaker.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/CemBaker.java index 60f4ad1..e1223ec 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/CemBaker.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/CemBaker.java @@ -134,6 +134,9 @@ public final class CemBaker implements EntityBaker { 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()))); diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/EntitySnapshotBuilder.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/EntitySnapshotBuilder.java index 132bd0c..2397656 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/EntitySnapshotBuilder.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/EntitySnapshotBuilder.java @@ -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 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 VARIANT_GETTER = new ClassValue<>() { @Override diff --git a/tools/edgecase-plugin/pptest/PPTest.java b/tools/edgecase-plugin/pptest/PPTest.java index 82c354b..42dcd01 100644 --- a/tools/edgecase-plugin/pptest/PPTest.java +++ b/tools/edgecase-plugin/pptest/PPTest.java @@ -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 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) (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) (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); + } + } + } }