christmas chest textures (24-26 december, chests and trapped chests, local_time item models, pixelpics.testDate test property), chest items from the item definition texture, shield banner patterns, item displays turned 180° like vanilla, dropped item lift from the ground model bounds, baby held-item offset, map decorations on framed vanilla maps (banners, frames, markers; none on photo maps), inverted block elements in items and displays (spawner, vault inner faces), deterministic test scenes, items/xmas/marker test scenes

This commit is contained in:
MineTec committed 2026-11-04 21:15:38 +01:00
1 parent 9e48e35680
commit 9093c1ccfb
17 files changed
+681 -74

No files matched your search

+231 -20
View File
@@ -198,6 +198,160 @@ public class PPTest extends JavaPlugin {
scenes.put("be_contents_close", new Scene("be_contents_close", this::beContents, 1.2, 1.8, 2.2, 1.5, 0.6, 0));
scenes.put("be_contents_beam", new Scene("be_contents_beam", this::beContents, 4, 6, 26, 10, 9, 6));
scenes.put("be_contents_portal", new Scene("be_contents_portal", this::beContents, 11.5, 3.6, -1.8, 11.5, 0, 1.2));
// Items in detail (decompiled 26.3 special renderers): shields (plain, base colour, banner patterns), chests,
// spawner/vault held by armor stands (+ a baby zombie: baby hand offset), item displays (fixed), item frames
// (rotation 0/1) and dropped (ground: lift = 0.1625 - model min y). items_inverted: spawner, trial spawner and
// vault as block, block display, item display and dropped item (inverted inner faces). xmas_chests: chests,
// double/trapped/ender/copper chests + chest items; render it once normally and once with the server
// started with -Dpixelpics.testDate=2026-12-25 (christmas textures for chests and trapped chests).
// frames_markers: map decorations on framed maps (banner markers via toggleBanner, frame/target markers,
// an invisible-on-frames player marker; rotations 0/1/3), a PixelPics photo map and an undecorated map.
scenes.put("items_detail", new Scene("items_detail", this::itemsDetail, 4.5, 3.2, 13, 4.5, 0.9, 2));
scenes.put("items_detail_close", new Scene("items_detail_close", this::itemsDetail, 2.2, 1.7, 7.4, 2.2, 0.9, 2));
scenes.put("items_detail_side", new Scene("items_detail_side", this::itemsDetail, -4.5, 2.4, 4.5, 5, 0.9, 3));
scenes.put("items_inverted", new Scene("items_inverted", this::itemsInverted, 3, 2.4, 7, 3, 0.6, 1.5));
scenes.put("xmas_chests", new Scene("xmas_chests", this::xmasChests, 6, 3.6, 9.5, 6, 0.5, 1.5));
scenes.put("frames_markers", new Scene("frames_markers", this::framesMarkers, 2.5, 2.4, 4.2, 2.5, 2.4, 0));
}
private static ItemStack shield(DyeColor base, Object... patterns) {
ItemStack s = it(Material.SHIELD);
org.bukkit.inventory.meta.ShieldMeta m = (org.bukkit.inventory.meta.ShieldMeta) s.getItemMeta();
m.setBaseColor(base);
for (int i = 0; i + 1 < patterns.length; i += 2)
m.addPattern(new org.bukkit.block.banner.Pattern((DyeColor) patterns[i], (org.bukkit.block.banner.PatternType) patterns[i + 1]));
s.setItemMeta(m);
return s;
}
private void itemsDetail(Location o) {
for (int x = -1; x <= 11; x++) for (int y = 0; y <= 3; y++) put(o, x, y, -1, "stone");
ItemStack[] items = {it(Material.SHIELD), shield(DyeColor.RED),
shield(DyeColor.BLUE, DyeColor.YELLOW, org.bukkit.block.banner.PatternType.CREEPER,
DyeColor.WHITE, org.bukkit.block.banner.PatternType.BORDER),
it(Material.CHEST), it(Material.TRAPPED_CHEST), it(Material.ENDER_CHEST), it(Material.COPPER_CHEST),
it(Material.SPAWNER), it(Material.VAULT), it(Material.RED_BANNER)};
for (int i = 0; i < items.length; i++) {
frame(o, i, 1, 0, BlockFace.SOUTH, false, items[i], 0, true);
frame(o, i, 2, 0, BlockFace.SOUTH, false, items[i], 1, true);
ArmorStand as = spawn(o, i + 0.5, 0, 2.5, 0, ArmorStand.class);
as.setArms(true);
as.getEquipment().setItemInMainHand(items[i]);
if (i < 3) as.getEquipment().setItemInOffHand(items[i]);
ItemDisplay d = spawn(o, i + 0.5, 0.6, 4.5, 0, ItemDisplay.class);
d.setItemStack(items[i]);
d.setItemDisplayTransform(ItemDisplay.ItemDisplayTransform.FIXED);
d.setTransformation(new Transformation(new Vector3f(), new Quaternionf(), new Vector3f(0.6f), new Quaternionf()));
drop(o, i + 0.5, 0, 6.5, items[i], i * 20);
}
Zombie baby = spawn(o, 10.5, 0, 2.5, 0, Zombie.class);
baby.setBaby();
baby.getEquipment().setItemInMainHand(it(Material.CHEST));
baby.getEquipment().setItemInOffHand(items[2]);
Zombie z = spawn(o, 11.5, 0, 4, 0, Zombie.class);
z.setAdult();
z.getEquipment().setItemInMainHand(it(Material.TRAPPED_CHEST));
z.getEquipment().setItemInOffHand(items[2]);
}
private void itemsInverted(Location o) {
String[] blocks = {"spawner", "trial_spawner", "vault[facing=south]"};
Material[] mats = {Material.SPAWNER, Material.TRIAL_SPAWNER, Material.VAULT};
for (int i = 0; i < blocks.length; i++) {
put(o, i * 2, 0, 0, blocks[i]);
BlockDisplay b = spawn(o, i * 2, 0, 2, 0, BlockDisplay.class);
b.setBlock(Bukkit.createBlockData("minecraft:" + blocks[i]));
ItemDisplay d = spawn(o, i * 2 + 0.5, 0.5, 4.5, 0, ItemDisplay.class);
d.setItemStack(it(mats[i]));
drop(o, i * 2 + 0.5, 0, 6, it(mats[i]), 15);
}
}
private void xmasChests(Location o) {
put(o, 0, 0, 0, "chest[facing=south]");
put(o, 3, 0, 0, "chest[facing=south,type=left]");
put(o, 2, 0, 0, "chest[facing=south,type=right]");
put(o, 5, 0, 0, "trapped_chest[facing=south]");
put(o, 8, 0, 0, "trapped_chest[facing=south,type=left]");
put(o, 7, 0, 0, "trapped_chest[facing=south,type=right]");
put(o, 10, 0, 0, "ender_chest[facing=south]");
put(o, 12, 0, 0, "copper_chest[facing=south]");
Material[] mats = {Material.CHEST, Material.TRAPPED_CHEST};
for (int i = 0; i < 2; i++) {
ItemDisplay d = spawn(o, 0.5 + i * 1.5, 0.6, 3.5, 0, ItemDisplay.class);
d.setItemStack(it(mats[i]));
d.setItemDisplayTransform(ItemDisplay.ItemDisplayTransform.FIXED);
ArmorStand as = spawn(o, 4 + i * 1.5, 0, 3.5, 0, ArmorStand.class);
as.setArms(true);
as.getEquipment().setItemInMainHand(it(mats[i]));
drop(o, 7.5 + i, 0, 3.5, it(mats[i]), 30);
put(o, 10 + i, 0, 3, "stone");
frame(o, 10 + i, 1, 3, BlockFace.UP, false, it(mats[i]), 0, true);
}
}
private void framesMarkers(Location o) {
for (int x = -2; x <= 7; x++) for (int y = 0; y <= 4; y++) put(o, x, y, -1, "stone");
ItemStack marked = vanillaMap();
org.bukkit.map.MapView v = ((org.bukkit.inventory.meta.MapMeta) marked.getItemMeta()).getMapView();
v.setScale(org.bukkit.map.MapView.Scale.CLOSEST);
v.setCenterX(o.getBlockX());
v.setCenterZ(o.getBlockZ());
put(o, -5, 0, -10, "red_banner[rotation=0]");
put(o, 16, 0, -11, "lime_banner[rotation=4]");
mapBanner(v, o.clone().add(-5, 0, -10));
mapBanner(v, o.clone().add(16, 0, -11));
ItemFrame f0 = frame(o, 0, 2, 0, BlockFace.SOUTH, false, marked, 0, true);
frame(o, 1, 2, 0, BlockFace.SOUTH, false, marked, 1, true);
frame(o, 2, 2, 0, BlockFace.SOUTH, false, synthMap(), 0, true);
frame(o, 3, 2, 0, BlockFace.SOUTH, false, vanillaMap(), 0, true);
frame(o, 4, 2, 0, BlockFace.SOUTH, true, marked, 3, true);
// Frame markers like MapItemSavedData.tickCarriedBy (key frame-<id>, frame block pos, rot = 2D facing · 90):
// the real frame, plus fictitious ones turned east/north; a red X and a player marker (not on frames).
mapDecoration(v, "FRAME", "frame-" + f0.getEntityId(), o.getBlockX(), o.getBlockZ(), 0);
mapDecoration(v, "FRAME", "frame-pptest-1", o.getBlockX() + 30, o.getBlockZ() - 25, 270);
mapDecoration(v, "FRAME", "frame-pptest-2", o.getBlockX() - 28, o.getBlockZ() + 30, 180);
mapDecoration(v, "RED_X", "pptest-x", o.getBlockX() + 40, o.getBlockZ() + 40, 180);
mapDecoration(v, "PLAYER", "pptest-player", o.getBlockX() + 10, o.getBlockZ() + 10, 0);
}
/** MapItemSavedData.toggleBanner(level, pos) on the map's server data (reflective: no API). */
private void mapBanner(org.bukkit.map.MapView v, Location banner) {
try {
Object data = mapData(v);
Object level = w.getClass().getMethod("getHandle").invoke(w);
Class<?> bp = Class.forName("net.minecraft.core.BlockPos", true, data.getClass().getClassLoader());
Object pos = bp.getConstructor(int.class, int.class, int.class)
.newInstance(banner.getBlockX(), banner.getBlockY(), banner.getBlockZ());
for (java.lang.reflect.Method m : data.getClass().getMethods())
if (m.getName().equals("toggleBanner") && m.getParameterCount() == 2)
getLogger().info("toggleBanner " + banner.toVector() + " -> " + m.invoke(data, level, pos));
} catch (ReflectiveOperationException e) {
getLogger().warning("mapBanner: " + e);
}
}
/** MapItemSavedData.addDecoration(MapDecorationTypes.<type>, level, key, x, z, yRot, null) (private, reflective). */
private void mapDecoration(org.bukkit.map.MapView v, String type, String key, double x, double z, double rot) {
try {
Object data = mapData(v);
Object level = w.getClass().getMethod("getHandle").invoke(w);
Object holder = Class.forName("net.minecraft.world.level.saveddata.maps.MapDecorationTypes", true,
data.getClass().getClassLoader()).getField(type).get(null);
for (java.lang.reflect.Method m : data.getClass().getDeclaredMethods())
if (m.getName().equals("addDecoration") && m.getParameterCount() == 7) {
m.setAccessible(true);
m.invoke(data, holder, level, key, x, z, rot, null);
}
} catch (ReflectiveOperationException e) {
getLogger().warning("mapDecoration: " + e);
}
}
private static Object mapData(org.bukkit.map.MapView v) throws ReflectiveOperationException {
java.lang.reflect.Field wm = v.getClass().getDeclaredField("worldMap");
wm.setAccessible(true);
return wm.get(v);
}
private void beContents(Location o) {
@@ -212,6 +366,7 @@ public class PPTest extends JavaPlugin {
c.setItem(i, it(food[i]));
c.setCookTimeTotal(i, 1_000_000); // else the lit campfire pops them out at once (total 0)
}
c.stopCooking(); // keep the food raw and in place while the scene waits
c.update(true, false);
}
if (o.clone().add(4, 0, 0).getBlock().getState() instanceof org.bukkit.block.Campfire c) {
@@ -561,7 +716,9 @@ public class PPTest extends JavaPlugin {
put(o, 8, 0, 1, "water");
Mannequin m3 = rfSpawn(o, 8.5, 0.1, 0.5, 180, Mannequin.class);
m3.setPose(Pose.SWIMMING, true);
m3.setGravity(false); // else it floats up in the water while the scene waits (non-deterministic)
Mannequin m4 = rfSpawn(o, 10, 1.0, 0, 180, Mannequin.class);
m4.setGravity(false); // keeps gliding at y 1 instead of falling
m4.getEquipment().setChestplate(it(Material.ELYTRA));
m4.setPose(Pose.FALL_FLYING, true);
Mannequin m5 = rfSpawn(o, 12, 0, 0, 180, Mannequin.class);
@@ -649,7 +806,30 @@ public class PPTest extends JavaPlugin {
return true;
}
/**
* Loads the scene's chunks and waits until they are entity-ticking before building: entities spawned into a
* chunk that only becomes entity-ticking a few (variable) ticks later start moving late (projectile scenes were
* not reproducible).
*/
private void run(Scene sc, Location o) {
loadArea(o);
whenEntityTicking(sc, o, 0);
}
private void whenEntityTicking(Scene sc, Location o, int waited) {
boolean ready = true;
for (int cx = -1; cx <= 2; cx++)
for (int cz = -2; cz <= 2; cz++)
ready &= w.getChunkAt((o.getBlockX() >> 4) + cx, (o.getBlockZ() >> 4) + cz).getLoadLevel() == Chunk.LoadLevel.ENTITY_TICKING;
if (!ready && waited < 200) {
Bukkit.getScheduler().runTaskLater(this, () -> whenEntityTicking(sc, o, waited + 1), 1);
return;
}
if (!ready) getLogger().warning("scene " + sc.name + ": chunks not entity-ticking after 200 ticks");
build(sc, o);
}
private void build(Scene sc, Location o) {
try {
prepareArea(o);
sc.build.accept(o.clone());
@@ -666,7 +846,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, sc.name.endsWith("_sq") ? new Resolution(384, 384) : sc.name.startsWith("proj_") || sc.name.startsWith("rf_") || sc.name.startsWith("pose_") || sc.name.startsWith("frames") || sc.name.startsWith("geo_") ? new Resolution(960, 640) : new Resolution(480, 320), null);
RenderJob job = r.prepare(eye, sc.name.endsWith("_sq") ? new Resolution(384, 384) : sc.name.startsWith("proj_") || sc.name.startsWith("rf_") || sc.name.startsWith("pose_") || sc.name.startsWith("frames") || sc.name.startsWith("geo_") || sc.name.startsWith("items_") || sc.name.startsWith("xmas_") ? new Resolution(960, 640) : new Resolution(480, 320), null);
if (sc.name.equals("dbg")) {
try {
java.lang.reflect.Field f = DefaultScreenRenderer.class.getDeclaredField("objectBaker");
@@ -730,43 +910,74 @@ public class PPTest extends JavaPlugin {
});
}
/** Clears the area (entities + blocks) and lays a 14x14 stone floor at the origin's y-1. */
private void prepareArea(Location o) {
/** Loads the scene's chunks and keeps them loaded (plugin chunk tickets). */
private void loadArea(Location o) {
for (int cx = -1; cx <= 2; cx++)
for (int cz = -2; cz <= 2; cz++) {
Chunk c = w.getChunkAt((o.getBlockX() >> 4) + cx, (o.getBlockZ() >> 4) + cz);
c.load(true);
c.addPluginChunkTicket(this);
}
for (Entity e : w.getNearbyEntities(o.clone().add(6, 5, 2), 20, 20, 20)) if (!(e instanceof Player)) e.remove();
}
/** Clears the area (blocks + entities) and lays a 24x24 stone floor at the origin's y-1. */
private void prepareArea(Location o) {
// Blocks first: removing a campfire/shelf/container drops its items, which the entity sweep then removes
// (before, the previous run's cooked campfire food lay around at random).
for (int x = -6; x < 18; x++)
for (int z = -12; z < 12; z++)
for (int y = -1; y < 12; y++)
w.getBlockAt(o.getBlockX() + x, o.getBlockY() + y, o.getBlockZ() + z)
.setType(y == -1 ? Material.SMOOTH_STONE : Material.AIR, false);
for (Entity e : w.getNearbyEntities(o.clone().add(6, 5, 2), 20, 20, 20)) if (!(e instanceof Player)) e.remove();
}
// --- helpers ---------------------------------------------------------------------------------
/**
* Deterministic spawn (pixel-diffable between plugin builds): randomizeData=false (no finalizeSpawn — no random
* variants, sizes, babies/jockeys or equipment) and the body turned to {@code yaw} (spawned mobs otherwise keep a
* random body yaw). Scenes set every variant/equipment they show explicitly.
*/
private <T extends Entity> T spawn(Location o, double x, double y, double z, float yaw, Class<T> type) {
Location l = o.clone().add(x, y, z);
l.setYaw(yaw);
T e = w.spawn(l, type, en -> {
if (en instanceof Mob m) m.setAI(false);
en.setPersistent(false);
});
if (e instanceof LivingEntity le) le.setRemoveWhenFarAway(false);
T e = rfSpawn(o, x, y, z, yaw, type);
if (e instanceof LivingEntity le) le.setBodyYaw(yaw);
return e;
}
private Entity spawn(Location o, double x, double y, double z, float yaw, EntityType type) {
Location l = o.clone().add(x, y, z);
l.setYaw(yaw);
Entity e = w.spawnEntity(l, type);
if (e instanceof Mob m) m.setAI(false);
Entity e = rfSpawn(o, x, y, z, yaw, type);
e.setPersistent(false);
if (e instanceof LivingEntity le) {
le.setRemoveWhenFarAway(false);
le.setBodyYaw(yaw);
}
return e;
}
/**
* Respawns until the entity id is even: a full boat turns an animal passenger by 90° or 270° depending on the
* id's parity (AbstractBoat.positionRider), and ids depend on the server's history.
*/
private <T extends Entity> T evenId(java.util.function.Supplier<T> spawner) {
T e = spawner.get();
while (e.getEntityId() % 2 != 0) {
e.remove();
e = spawner.get();
}
return e;
}
/** A dropped item at rest: no velocity/gravity, fixed yaw (PixelPics freezes the item spin from the yaw). */
private Item drop(Location o, double x, double y, double z, ItemStack stack, float yaw) {
return w.dropItem(o.clone().add(x, y, z), stack, i -> {
i.setVelocity(new Vector());
i.setGravity(false);
i.setRotation(yaw, 0);
i.setPersistent(false);
});
}
private static ItemStack it(Material m) {
return new ItemStack(m);
}
@@ -839,7 +1050,7 @@ public class PPTest extends JavaPlugin {
mc.addPassenger(spawn(o, 7, 1, 0, 180, Villager.class));
Boat b = (Boat) spawn(o, 9.5, 0, 0, 180, EntityType.OAK_BOAT);
b.addPassenger(spawn(o, 9.5, 1, 0, 180, Zombie.class));
b.addPassenger(spawn(o, 9.5, 1, 0, 180, Pig.class));
b.addPassenger(evenId(() -> spawn(o, 9.5, 1, 0, 180, Pig.class)));
Camel c = spawn(o, 2, 0, 3.5, 90, Camel.class);
c.getInventory().setSaddle(it(Material.SADDLE));
c.addPassenger(spawn(o, 2, 2, 3.5, 90, EntityType.MANNEQUIN));
@@ -955,10 +1166,10 @@ public class PPTest extends JavaPlugin {
}
private void misc(Location o) {
w.dropItem(o.clone().add(0, 0.2, 0), it(Material.DIAMOND_SWORD), i -> i.setVelocity(new Vector()));
w.dropItem(o.clone().add(1, 0.2, 0), it(Material.OAK_LOG), i -> i.setVelocity(new Vector()));
w.dropItem(o.clone().add(2, 0.2, 0), new ItemStack(Material.COBBLESTONE, 32), i -> i.setVelocity(new Vector()));
w.dropItem(o.clone().add(3, 0.2, 0), it(Material.APPLE), i -> i.setVelocity(new Vector()));
drop(o, 0, 0, 0, it(Material.DIAMOND_SWORD), 30);
drop(o, 1, 0, 0, it(Material.OAK_LOG), 60);
drop(o, 2, 0, 0, new ItemStack(Material.COBBLESTONE, 32), 0);
drop(o, 3, 0, 0, it(Material.APPLE), 90);
FallingBlock fb = w.spawn(o.clone().add(4.5, 0.5, 0.5), FallingBlock.class, f -> f.setGravity(false));
fb.setBlockData(Bukkit.createBlockData("minecraft:sand"));
TNTPrimed tnt = w.spawn(o.clone().add(6, 0, 0.5), TNTPrimed.class);