diff --git a/build.gradle b/build.gradle index c5016ea..435cb77 100644 --- a/build.gradle +++ b/build.gradle @@ -60,3 +60,9 @@ spotless { if (file("local.gradle").exists()) { apply from: "local.gradle" } + +// Classpath for the standalone tools (tools/*.java harnesses, tools/edgecase-plugin/build.sh). +tasks.register('printCompileClasspath') { + def cp = sourceSets.main.compileClasspath + doLast { println cp.asPath } +} diff --git a/tools/BlockEntityTestRender.java b/tools/BlockEntityTestRender.java index 41ac684..a226188 100644 --- a/tools/BlockEntityTestRender.java +++ b/tools/BlockEntityTestRender.java @@ -1,35 +1,38 @@ -import eu.mhsl.minecraft.pixelpics.assets.*; import eu.mhsl.minecraft.pixelpics.render.entity.*; import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.*; import eu.mhsl.minecraft.pixelpics.render.entity.cem.*; -import eu.mhsl.minecraft.pixelpics.render.render.*; -import eu.mhsl.minecraft.pixelpics.render.sky.SkyContext; +import eu.mhsl.minecraft.pixelpics.render.render.RenderJob; import eu.mhsl.minecraft.pixelpics.render.snapshot.WorldSnapshot; -import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.block.data.BlockData; +import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil; +import org.bukkit.DyeColor; import org.bukkit.util.Vector; import javax.imageio.ImageIO; -import java.awt.*; import java.awt.image.BufferedImage; import java.io.File; -import java.lang.reflect.Proxy; import java.util.*; import java.util.List; -import java.util.logging.Logger; -/** Standalone (no server) block-entity renderer: each scene framed and labelled into a contact sheet. */ +/** + * Standalone (no server, no block models: sign boards, tables, campfires, cages are absent) block-entity and + * decoration renderer: each scene framed and labelled -> {@code .png} + {@code sheet.png} in {@code -Dout} + * (default ~/Dokumente/PixelPics-BlockEntity-Renders). Christmas chests: {@code -Dpixelpics.testDate=2026-12-25}. + * Build: see {@link Harness}. + */ public class BlockEntityTestRender { - static final String ROOT = "/home/elias/Dokumente/mcTestServer/plugins/PixelPics"; - static final double V_FOV_HALF = Math.toRadians(eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer.V_FOV_DEG / 2); - static final int SSAA = 3; static final int TW = 220, TH = 240; + static final String[] D = {"S", "W", "N", "E"}; // facing yaw i·90 - static BlockEntityBaker beBaker; - static DecorationBaker decoBaker; + /** Block entities, viewed from the front ({@code frontYaw}: facing yaw, null = fixed diagonal view). */ + record Scene(List states, Float frontYaw) {} + + static final Map scenes = new LinkedHashMap<>(); + static final Map> decoScenes = new LinkedHashMap<>(); + + static void scene(String name, Float frontYaw, BlockEntityState... states) { + scenes.put(name, new Scene(List.of(states), frontYaw)); + } /** A block-entity state at (bx, 0, bz); unused fields default. */ static BlockEntityState st(Kind kind, int bx, int bz, float yaw, String direction, boolean active, String color, @@ -42,224 +45,177 @@ public class BlockEntityTestRender { return st(kind, 0, 0, yaw, null, false, null, 0, null, List.of(), List.of()); } + static BlockEntityState head(Kind kind, String type, float yaw) { + return st(kind, 0, 0, yaw, null, false, null, 0, type, List.of(), List.of()); + } + + static BlockEntityState chest(Kind kind, int bx, int bz, float yaw, ChestKind ck, String variant) { + return new BlockEntityState(kind, bx, 0, bz, yaw, null, false, ck, 0, variant, null, null, + List.of(), List.of(), null, null); + } + static BlockEntityState sign(Kind kind, float yaw, SignText front, SignText back) { return new BlockEntityState(kind, 0, 0, 0, yaw, null, false, null, 0, null, null, null, List.of(), List.of(), front, back); } - static SignText txt(org.bukkit.DyeColor dye, boolean glow, String... lines) { - return new SignText(List.of(lines), - eu.mhsl.minecraft.pixelpics.render.util.ColorUtil.signFillArgb(dye, glow), - eu.mhsl.minecraft.pixelpics.render.util.ColorUtil.signOutlineArgb(dye), glow); + static SignText txt(DyeColor dye, boolean glow, String... lines) { + return new SignText(List.of(lines), ColorUtil.signFillArgb(dye, glow), ColorUtil.signOutlineArgb(dye), glow); } - public static void main(String[] args) throws Exception { - Logger log = Logger.getLogger("test"); - ResourcePack pack = ResourcePackLoader.load(new File(ROOT, "resourcepack"), log).orElseThrow(); - AssetReader reader = new AssetReader(pack); - TextureCache textures = new TextureCache(pack); - BlockModelRegistry registry = new BlockModelRegistry(reader, textures); - BiomeTintProvider tint = new BiomeTintProvider(textures); - CemModelLoader geo = new CemModelLoader(); - geo.load(new java.io.FileInputStream("src/main/resources/cem/cem_template_models.json"), log); // entity fallback only - geo.load(new java.io.FileInputStream("src/main/resources/cem/vanilla_entity_models.json"), log); - SkinCache skins = new SkinCache(); - eu.mhsl.minecraft.pixelpics.assets.font.BitmapFont font = - eu.mhsl.minecraft.pixelpics.assets.font.FontLoader.load(pack, textures, log); - log.info("font glyphs loaded: " + (font.isEmpty() ? "NONE" : "ok")); - CemBaker baker = new CemBaker(geo, textures, skins); - beBaker = new BlockEntityBaker(geo, textures, skins, font); - decoBaker = new DecorationBaker(textures, registry, - new eu.mhsl.minecraft.pixelpics.render.entity.cem.ItemGeometry(reader, registry, textures, geo, beBaker)); - DefaultScreenRenderer renderer = new DefaultScreenRenderer(registry, tint, textures, baker, beBaker, log); + /** A contents-bearing block entity at the origin (items / spawner mob / beacon beam / statue pose). */ + static BlockEntityState contents(Kind kind, float yaw, List items, EntityState mob, Beam beam, + String pose, String color) { + return new BlockEntityState(kind, 0, 0, 0, yaw, null, false, null, 0, color, null, null, + List.of(), List.of(), null, null, items, mob, beam, pose); + } - BlockData air = (BlockData) Proxy.newProxyInstance(BlockEntityTestRender.class.getClassLoader(), - new Class[]{BlockData.class}, (p, m, a) -> { - switch (m.getName()) { - case "getMaterial": return Material.AIR; - case "equals": return p == a[0]; - case "hashCode": return System.identityHashCode(p); - case "toString": return "air"; - } - Class rt = m.getReturnType(); - if (rt == boolean.class) return false; - if (rt.isPrimitive()) return 0; - return null; - }); - WorldSnapshot empty = new WorldSnapshot(Map.of(), -64, 320, air); - SkyContext sky = new SkyContext(6000, 0, 6000); + static List items(String... ids) { + List out = new ArrayList<>(); + for (String id : ids) out.add(id == null ? null : Harness.item(id)); + return out; + } - // Scenes: label -> list of block-entity states. - LinkedHashMap> scenes = new LinkedHashMap<>(); - // Chests: single + double (all four facings), trapped/ender/copper. A double chest's LEFT half has - // its partner (RIGHT) on facing.getClockWise(): S→west, W→north, N→east, E→south. - String[] dirs = {"S", "W", "N", "E"}; + /** EntityTestRender's default state of a mob with its real bounding box (spawner scale depends on it). */ + static EntityState spawnerMob(String type) throws Exception { + double[] dims = switch (type) { + case "zombie" -> new double[] {0.6, 1.95}; + case "pig" -> new double[] {0.9, 0.9}; + case "spider" -> new double[] {1.4, 0.9}; + default -> new double[] {0.6, 1.8}; + }; + return Harness.copy(EntityTestRender.state(type, 0), Map.of("width", dims[0], "height", dims[1])); + } + + /** Item frame on the south side of the block at the origin. */ + static DecorationState southFrame(ItemSpec item) { + return new DecorationState(DecorationState.Kind.ITEM_FRAME, 0.0625, 0.0625, 0, 0.9375, 0.9375, 0.0625, + DecorationState.Facing.SOUTH, null, item, null, 0, false, false); + } + + static void buildScenes() throws Exception { + // Chests: single + double (all four facings), trapped/ender/copper. A double chest's LEFT half has its + // partner (RIGHT) on facing.getClockWise(): S→west, W→north, N→east, E→south. int[][] partner = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}}; for (int i = 0; i < 4; i++) { float yaw = i * 90; - scenes.put("chest_" + dirs[i], List.of(chest(Kind.CHEST, 0, 0, yaw, ChestKind.SINGLE, null))); - frontCam.put("chest_" + dirs[i], yaw); - scenes.put("double_chest_" + dirs[i], List.of( - chest(Kind.CHEST, 0, 0, yaw, ChestKind.LEFT, null), - chest(Kind.CHEST, partner[i][0], partner[i][1], yaw, ChestKind.RIGHT, null))); - frontCam.put("double_chest_" + dirs[i], yaw); + scene("chest_" + D[i], yaw, chest(Kind.CHEST, 0, 0, yaw, ChestKind.SINGLE, null)); + scene("double_chest_" + D[i], yaw, chest(Kind.CHEST, 0, 0, yaw, ChestKind.LEFT, null), + chest(Kind.CHEST, partner[i][0], partner[i][1], yaw, ChestKind.RIGHT, null)); } - scenes.put("double_chest_back", scenes.get("double_chest_S")); - frontCam.put("double_chest_back", 180f); - scenes.put("trapped_double", List.of( - chest(Kind.TRAPPED_CHEST, 0, 0, 0, ChestKind.LEFT, null), - chest(Kind.TRAPPED_CHEST, -1, 0, 0, ChestKind.RIGHT, null))); - frontCam.put("trapped_double", 0f); - scenes.put("ender_chest", List.of(chest(Kind.ENDER_CHEST, 0, 0, 0, ChestKind.SINGLE, null))); - frontCam.put("ender_chest", 0f); - scenes.put("copper_chest", List.of(chest(Kind.COPPER_CHEST, 0, 0, 0, ChestKind.SINGLE, null))); - frontCam.put("copper_chest", 0f); - scenes.put("copper_oxidized_double", List.of( - chest(Kind.COPPER_CHEST, 0, 0, 0, ChestKind.LEFT, "oxidized"), - chest(Kind.COPPER_CHEST, -1, 0, 0, ChestKind.RIGHT, "oxidized"))); - frontCam.put("copper_oxidized_double", 0f); - // --- sign text (boards are block models, not drawn here) --- facingDeg 180 → front faces north. - org.bukkit.DyeColor B = org.bukkit.DyeColor.BLACK; - String[] D = {"S", "W", "N", "E"}; + scenes.put("double_chest_back", new Scene(scenes.get("double_chest_S").states(), 180f)); + scene("trapped_double", 0f, chest(Kind.TRAPPED_CHEST, 0, 0, 0, ChestKind.LEFT, null), + chest(Kind.TRAPPED_CHEST, -1, 0, 0, ChestKind.RIGHT, null)); + scene("ender_chest", 0f, chest(Kind.ENDER_CHEST, 0, 0, 0, ChestKind.SINGLE, null)); + scene("copper_chest", 0f, chest(Kind.COPPER_CHEST, 0, 0, 0, ChestKind.SINGLE, null)); + scene("copper_oxidized_double", 0f, chest(Kind.COPPER_CHEST, 0, 0, 0, ChestKind.LEFT, "oxidized"), + chest(Kind.COPPER_CHEST, -1, 0, 0, ChestKind.RIGHT, "oxidized")); + + // Sign text (boards are block models, not drawn here); facing 180 → front faces north. + DyeColor b = DyeColor.BLACK; for (int i = 0; i < 4; i++) { - float yaw = i * 90; - scenes.put("sign_text_" + D[i], List.of(sign(Kind.SIGN, yaw, txt(B, false, "Hello World", "zweite Zeile", "Gruesse: AÖÜ", "ßäöü 12345"), null))); - frontCam.put("sign_text_" + D[i], yaw); + scene("sign_text_" + D[i], i * 90f, + sign(Kind.SIGN, i * 90, txt(b, false, "Hello World", "zweite Zeile", "Gruesse: AÖÜ", "ßäöü 12345"), null)); } - scenes.put("sign_text_diag", List.of(sign(Kind.SIGN, 22.5f * 3, txt(B, false, "rot 3", "67.5 deg", "", ""), null))); - frontCam.put("sign_text_diag", 22.5f * 3); - scenes.put("wall_sign_text", List.of(sign(Kind.WALL_SIGN, 180, txt(B, false, "Wall Sign", "Line 2", "Line 3", "Line 4"), null))); - frontCam.put("wall_sign_text", 180f); - scenes.put("wall_sign_text_E", List.of(sign(Kind.WALL_SIGN, 270, txt(B, false, "Wall East", "Line 2", "", ""), null))); - frontCam.put("wall_sign_text_E", 270f); - scenes.put("hanging_sign_text", List.of(sign(Kind.HANGING_SIGN, 180, txt(B, false, "Hanging", "sign text", "row 3", "row 4"), null))); - frontCam.put("hanging_sign_text", 180f); - scenes.put("sign_text_red", List.of(sign(Kind.SIGN, 180, txt(org.bukkit.DyeColor.RED, false, "RED INK", "colored", "sign text", ""), null))); - frontCam.put("sign_text_red", 180f); - scenes.put("sign_text_glow", List.of(sign(Kind.SIGN, 180, txt(org.bukkit.DyeColor.LIME, true, "GLOWING", "lime glow", "outline!", ""), null))); - frontCam.put("sign_text_glow", 180f); - // Back side viewed head-on: the camera looks at the side opposite the front. - scenes.put("sign_view_back", List.of(sign(Kind.SIGN, 180, txt(B, false, "FRONTXY", "f2", "", ""), txt(org.bukkit.DyeColor.BLUE, false, "BACKXY", "b2", "", "")))); - frontCam.put("sign_view_back", 0f); - scenes.put("sign_1line", List.of(sign(Kind.SIGN, 180, txt(B, false, "", "SHOP", "", ""), null))); - frontCam.put("sign_1line", 180f); - scenes.put("sign_long", List.of(sign(Kind.SIGN, 180, txt(B, false, "a very long line here", "", "", ""), null))); - frontCam.put("sign_long", 180f); - // --- banners: standing (rotation 0/4/6), wall (4 facings), patterned --- - List pats = List.of(new BannerPattern("stripe_bottom", 0xFFCC2020), new BannerPattern("cross", 0xFF2040CC), new BannerPattern("border", 0xFF20A020)); + scene("sign_text_diag", 22.5f * 3, sign(Kind.SIGN, 22.5f * 3, txt(b, false, "rot 3", "67.5 deg", "", ""), null)); + scene("wall_sign_text", 180f, sign(Kind.WALL_SIGN, 180, txt(b, false, "Wall Sign", "Line 2", "Line 3", "Line 4"), null)); + scene("wall_sign_text_E", 270f, sign(Kind.WALL_SIGN, 270, txt(b, false, "Wall East", "Line 2", "", ""), null)); + scene("hanging_sign_text", 180f, sign(Kind.HANGING_SIGN, 180, txt(b, false, "Hanging", "sign text", "row 3", "row 4"), null)); + scene("sign_text_red", 180f, sign(Kind.SIGN, 180, txt(DyeColor.RED, false, "RED INK", "colored", "sign text", ""), null)); + scene("sign_text_glow", 180f, sign(Kind.SIGN, 180, txt(DyeColor.LIME, true, "GLOWING", "lime glow", "outline!", ""), null)); + // Back side viewed head-on. + scene("sign_view_back", 0f, sign(Kind.SIGN, 180, txt(b, false, "FRONTXY", "f2", "", ""), + txt(DyeColor.BLUE, false, "BACKXY", "b2", "", ""))); + scene("sign_1line", 180f, sign(Kind.SIGN, 180, txt(b, false, "", "SHOP", "", ""), null)); + scene("sign_long", 180f, sign(Kind.SIGN, 180, txt(b, false, "a very long line here", "", "", ""), null)); + + // Banners: standing (rotation 0/4/6), patterned (front + side), wall (4 facings). + List pats = List.of(new BannerPattern("stripe_bottom", 0xFFCC2020), + new BannerPattern("cross", 0xFF2040CC), new BannerPattern("border", 0xFF20A020)); for (float yaw : new float[] {0, 90, 135}) { - String k = "banner_" + (int) yaw; - scenes.put(k, List.of(st(Kind.BANNER, 0, 0, yaw, null, false, null, 0xFFCC2020, null, List.of(), List.of()))); - frontCam.put(k, yaw); + scene("banner_" + (int) yaw, yaw, st(Kind.BANNER, 0, 0, yaw, null, false, null, 0xFFCC2020, null, List.of(), List.of())); } - scenes.put("banner_patterned", List.of(st(Kind.BANNER, 0, 0, 0, null, false, null, 0xFFFFFFFF, null, pats, List.of()))); - frontCam.put("banner_patterned", 0f); - scenes.put("banner_side", scenes.get("banner_patterned")); - frontCam.put("banner_side", 90f); + BlockEntityState patterned = st(Kind.BANNER, 0, 0, 0, null, false, null, 0xFFFFFFFF, null, pats, List.of()); + scene("banner_patterned", 0f, patterned); + scene("banner_side", 90f, patterned); for (int i = 0; i < 4; i++) { - scenes.put("wall_banner_" + D[i], List.of(st(Kind.WALL_BANNER, 0, 0, i * 90, null, false, null, 0xFF2040CC, null, pats, List.of()))); - frontCam.put("wall_banner_" + D[i], i * 90f); + scene("wall_banner_" + D[i], i * 90f, + st(Kind.WALL_BANNER, 0, 0, i * 90, null, false, null, 0xFF2040CC, null, pats, List.of())); } - // --- shulker boxes: 6 facings --- + for (String d : new String[] {"up", "down", "north", "south", "east", "west"}) { - scenes.put("shulker_" + d, List.of(st(Kind.SHULKER_BOX, 0, 0, 0, d, false, d.equals("up") ? null : "red", 0, null, List.of(), List.of()))); - frontCam.put("shulker_" + d, 0f); + scene("shulker_" + d, 0f, + st(Kind.SHULKER_BOX, 0, 0, 0, d, false, d.equals("up") ? null : "red", 0, null, List.of(), List.of())); } - scenes.put("conduit", List.of(be(Kind.CONDUIT, 0))); - scenes.put("conduit_active", List.of(st(Kind.CONDUIT, 0, 0, 0, null, true, null, 0, null, List.of(), List.of()))); + scene("conduit", null, be(Kind.CONDUIT, 0)); + scene("conduit_active", null, st(Kind.CONDUIT, 0, 0, 0, null, true, null, 0, null, List.of(), List.of())); List sherds = List.of("heart_pottery_sherd", "skull_pottery_sherd", "angler_pottery_sherd", "brick"); for (int i = 0; i < 4; i++) { - scenes.put("pot_" + D[i], List.of(st(Kind.DECORATED_POT, 0, 0, i * 90, null, false, null, 0, null, List.of(), sherds))); - frontCam.put("pot_" + D[i], i * 90f); + scene("pot_" + D[i], i * 90f, st(Kind.DECORATED_POT, 0, 0, i * 90, null, false, null, 0, null, List.of(), sherds)); } - scenes.put("pot_plain", List.of(be(Kind.DECORATED_POT, 0))); - frontCam.put("pot_plain", 0f); - scenes.put("bell", List.of(be(Kind.BELL, 0))); + scene("pot_plain", 0f, be(Kind.DECORATED_POT, 0)); + scene("bell", null, be(Kind.BELL, 0)); for (int i = 0; i < 4; i++) { - scenes.put("lectern_book_" + D[i], List.of(st(Kind.LECTERN, 0, 0, i * 90, null, true, null, 0, null, List.of(), List.of()))); - frontCam.put("lectern_book_" + D[i], i * 90f); + scene("lectern_book_" + D[i], i * 90f, st(Kind.LECTERN, 0, 0, i * 90, null, true, null, 0, null, List.of(), List.of())); } - // --- heads: every type facing the camera (rotation 0), plus rotations and wall mounts --- - for (String t : new String[] {"skeleton", "wither_skeleton", "zombie", "creeper", "player", "piglin", "dragon"}) { - scenes.put("head_" + t, List.of(head(Kind.HEAD, t, 0))); - frontCam.put("head_" + t, 0f); - } - scenes.put("head_dragon_side", List.of(head(Kind.HEAD, "dragon", 0))); - frontCam.put("head_dragon_side", 90f); - for (float yaw : new float[] {90, 157.5f, 270}) { - scenes.put("head_creeper_" + yaw, List.of(head(Kind.HEAD, "creeper", yaw))); - frontCam.put("head_creeper_" + yaw, yaw); - } - for (int i = 0; i < 4; i++) { - scenes.put("wall_head_" + D[i], List.of(head(Kind.WALL_HEAD, "skeleton", i * 90))); - frontCam.put("wall_head_" + D[i], i * 90f); - } - scenes.put("wall_dragon_N", List.of(head(Kind.WALL_HEAD, "dragon", 180))); - frontCam.put("wall_dragon_N", 180f); - scenes.put("wall_piglin_E", List.of(head(Kind.WALL_HEAD, "piglin", 270))); - frontCam.put("wall_piglin_E", 270f); - // --- block-entity contents (no block models in this harness: tables/campfires/shelves/cages absent) --- - scenes.put("enchant_book_closed", List.of(be(Kind.ENCHANTING_TABLE, 0))); - scenes.put("enchant_book_open", List.of(st(Kind.ENCHANTING_TABLE, 0, 0, 90, null, true, null, 0, null, List.of(), List.of()))); - frontCam.put("enchant_book_open", 0f); - scenes.put("campfire_items", List.of(contents(Kind.CAMPFIRE, 0, items("beef", "cod", "porkchop", null), null, null, null, null))); - scenes.put("campfire_items_E", List.of(contents(Kind.CAMPFIRE, 270, items("beef", "cod", "porkchop", "salmon"), null, null, null, null))); - scenes.put("shelf_items_S", List.of(contents(Kind.SHELF, 0, items("diamond", "book", "golden_apple"), null, null, null, null))); - frontCam.put("shelf_items_S", 0f); - scenes.put("shelf_items_E", List.of(contents(Kind.SHELF, 270, items("stick", null, "emerald"), null, null, null, null))); - frontCam.put("shelf_items_E", 270f); - scenes.put("vault_item", List.of(contents(Kind.VAULT, 0, items("trial_key"), null, null, null, null))); - frontCam.put("vault_item", 0f); + // Heads: every type facing the camera, rotations and wall mounts. + for (String t : new String[] {"skeleton", "wither_skeleton", "zombie", "creeper", "player", "piglin", "dragon"}) { + scene("head_" + t, 0f, head(Kind.HEAD, t, 0)); + } + scene("head_dragon_side", 90f, head(Kind.HEAD, "dragon", 0)); + for (float yaw : new float[] {90, 157.5f, 270}) scene("head_creeper_" + yaw, yaw, head(Kind.HEAD, "creeper", yaw)); + for (int i = 0; i < 4; i++) scene("wall_head_" + D[i], i * 90f, head(Kind.WALL_HEAD, "skeleton", i * 90)); + scene("wall_dragon_N", 180f, head(Kind.WALL_HEAD, "dragon", 180)); + scene("wall_piglin_E", 270f, head(Kind.WALL_HEAD, "piglin", 270)); + + // Block-entity contents. + scene("enchant_book_closed", null, be(Kind.ENCHANTING_TABLE, 0)); + scene("enchant_book_open", 0f, st(Kind.ENCHANTING_TABLE, 0, 0, 90, null, true, null, 0, null, List.of(), List.of())); + scene("campfire_items", null, contents(Kind.CAMPFIRE, 0, items("beef", "cod", "porkchop", null), null, null, null, null)); + scene("campfire_items_E", null, contents(Kind.CAMPFIRE, 270, items("beef", "cod", "porkchop", "salmon"), null, null, null, null)); + scene("shelf_items_S", 0f, contents(Kind.SHELF, 0, items("diamond", "book", "golden_apple"), null, null, null, null)); + scene("shelf_items_E", 270f, contents(Kind.SHELF, 270, items("stick", null, "emerald"), null, null, null, null)); + scene("vault_item", 0f, contents(Kind.VAULT, 0, items("trial_key"), null, null, null, null)); String[] poses = {"standing", "running", "sitting", "star"}; String[] stages = {null, "exposed", "weathered", "oxidized"}; for (int i = 0; i < 4; i++) { - scenes.put("statue_" + poses[i], List.of(contents(Kind.COPPER_GOLEM_STATUE, i * 90, List.of(), null, null, poses[i], stages[i]))); - frontCam.put("statue_" + poses[i], i * 90f); + scene("statue_" + poses[i], i * 90f, + contents(Kind.COPPER_GOLEM_STATUE, i * 90, List.of(), null, null, poses[i], stages[i])); } for (String mob : new String[] {"zombie", "pig", "spider", "blaze"}) { - scenes.put("spawner_" + mob, List.of(contents(Kind.SPAWNER, 0, List.of(), spawnerMob(mob), null, null, null))); - frontCam.put("spawner_" + mob, 0f); + scene("spawner_" + mob, 0f, contents(Kind.SPAWNER, 0, List.of(), spawnerMob(mob), null, null, null)); } - scenes.put("beacon_beam", List.of(contents(Kind.BEACON, 0, List.of(), null, - new Beam(List.of(new BeamSection(0xFFF9FFFE, 1), new BeamSection(0xFFB02E26, 2), new BeamSection(0xFF3C44AA, 1)), 1f), null, null))); + scene("beacon_beam", null, contents(Kind.BEACON, 0, List.of(), null, new Beam(List.of( + new BeamSection(0xFFF9FFFE, 1), new BeamSection(0xFFB02E26, 2), new BeamSection(0xFF3C44AA, 1)), 1f), null, null)); List portal = new ArrayList<>(); for (int x = 0; x < 3; x++) for (int z = 0; z < 3; z++) portal.add(st(Kind.END_PORTAL, x, z, 0, null, false, null, 0, null, List.of(), List.of())); - scenes.put("end_portal", portal); - scenes.put("end_gateway", List.of(st(Kind.END_GATEWAY, 0, 0, 0, "down+up+north+south+west+east", false, null, 0, null, List.of(), List.of()))); + scene("end_portal", null, portal.toArray(new BlockEntityState[0])); + scene("end_gateway", null, st(Kind.END_GATEWAY, 0, 0, 0, "down+up+north+south+west+east", false, null, 0, null, List.of(), List.of())); - // Decoration scenes (paintings + item frames), rendered via the same scene path. - LinkedHashMap> decoScenes = new LinkedHashMap<>(); - // kebab is 1x1; place a thin quad facing south spanning the block at z=0..0.0625. + // Paintings (thin quads facing south / north) and item frames, rendered via the same scene path. decoScenes.put("painting_kebab_S", List.of(new DecorationState(DecorationState.Kind.PAINTING, 0, 0, 0, 1, 1, 0.0625, DecorationState.Facing.SOUTH, "kebab", null, null, 0, false, false))); decoScenes.put("painting_kebab_N", List.of(new DecorationState(DecorationState.Kind.PAINTING, 0, 0, 0.9375, 1, 1, 1, DecorationState.Facing.NORTH, "kebab", null, null, 0, false, false))); decoScenes.put("painting_pool_S", List.of(new DecorationState(DecorationState.Kind.PAINTING, 0, 0, 0, 2, 1, 0.0625, DecorationState.Facing.SOUTH, "pool", null, null, 0, false, false))); - decoScenes.put("item_frame", List.of(new DecorationState(DecorationState.Kind.ITEM_FRAME, - 0.0625, 0.0625, 0, 0.9375, 0.9375, 0.0625, DecorationState.Facing.SOUTH, null, new eu.mhsl.minecraft.pixelpics.render.entity.ItemSpec("diamond", null, null, false), null, 0, false, false))); - decoScenes.put("item_frame_empty", List.of(new DecorationState(DecorationState.Kind.ITEM_FRAME, - 0.0625, 0.0625, 0, 0.9375, 0.9375, 0.0625, DecorationState.Facing.SOUTH, null, null, null, 0, false, false))); + decoScenes.put("item_frame", List.of(southFrame(Harness.item("diamond")))); + decoScenes.put("item_frame_empty", List.of(southFrame(null))); decoScenes.put("item_frame_E", List.of(new DecorationState(DecorationState.Kind.ITEM_FRAME, - 0.9375, 0.0625, 0.0625, 1.0, 0.9375, 0.9375, DecorationState.Facing.EAST, null, new eu.mhsl.minecraft.pixelpics.render.entity.ItemSpec("diamond", null, null, false), null, 0, false, false))); - - // Shields (plain / red base / blue + creeper + border patterns), chests (christmas with - // -Dpixelpics.testDate=2026-12-25) and a decorated map in frames (map/decorations sprites as MapRenderer). + 0.9375, 0.0625, 0.0625, 1.0, 0.9375, 0.9375, DecorationState.Facing.EAST, null, Harness.item("diamond"), null, 0, false, false))); + // Shields (plain / red base / blue + creeper + border patterns) and chests (christmas with testDate). BlockEntityState shieldPats = new BlockEntityState(Kind.BANNER, 0, 0, 0, 0, null, false, null, 0xFF3C44AA, null, null, null, List.of(new BannerPattern("creeper", 0xFFFED83D), new BannerPattern("border", 0xFFF9FFFE)), List.of(), null, null); BlockEntityState shieldRed = new BlockEntityState(Kind.BANNER, 0, 0, 0, 0, null, false, null, 0xFFB02E26, null, null, null, List.of(), List.of(), null, null); - String[] frameItems = {"shield", "shield_red", "shield_patterns", "chest", "trapped_chest", "ender_chest"}; - for (String id : frameItems) { - ItemSpec spec = switch (id) { - case "shield_red" -> new ItemSpec("shield", null, shieldRed, false); - case "shield_patterns" -> new ItemSpec("shield", null, shieldPats, false); - default -> new ItemSpec(id, null, null, false); - }; - decoScenes.put("item_frame_" + id, List.of(new DecorationState(DecorationState.Kind.ITEM_FRAME, - 0.0625, 0.0625, 0, 0.9375, 0.9375, 0.0625, DecorationState.Facing.SOUTH, null, spec, null, 0, false, false))); + decoScenes.put("item_frame_shield", List.of(southFrame(Harness.item("shield")))); + decoScenes.put("item_frame_shield_red", List.of(southFrame(new ItemSpec("shield", null, shieldRed, false)))); + decoScenes.put("item_frame_shield_patterns", List.of(southFrame(new ItemSpec("shield", null, shieldPats, false)))); + for (String id : new String[] {"chest", "trapped_chest", "ender_chest"}) { + decoScenes.put("item_frame_" + id, List.of(southFrame(Harness.item(id)))); } + // A checkered map with decorations (map/decorations sprites as MapRenderer), rotation 0 and 1. int[][] mapPx = new int[128][128]; for (int y = 0; y < 128; y++) for (int x = 0; x < 128; x++) mapPx[y][x] = ((x / 16 + y / 16) & 1) == 0 ? 0xFF7FB238 : 0xFF3F76E4; List markers = List.of( @@ -271,157 +227,74 @@ public class BlockEntityTestRender { for (int rot : new int[] {0, 1}) { decoScenes.put("item_frame_map_markers_r" + rot, List.of(new DecorationState(DecorationState.Kind.ITEM_FRAME, 0.0625, 0.0625, 0, 0.9375, 0.9375, 0.0625, DecorationState.Facing.SOUTH, null, - new ItemSpec("filled_map", null, null, false), mapPx, rot, false, false, markers))); + Harness.item("filled_map"), mapPx, rot, false, false, markers))); } + } + + public static void main(String[] args) throws Exception { + Harness h = new Harness("test"); + DecorationBaker decoBaker = new DecorationBaker(h.textures, h.registry, + new ItemGeometry(h.reader, h.registry, h.textures, h.geo, h.beBaker)); + WorldSnapshot empty = Harness.emptyWorld(-64, 320); + buildScenes(); List cells = new ArrayList<>(); - List names = new ArrayList<>(scenes.keySet()); - for (String name : names) { - cells.add(labelCell(name, renderScene(renderer, empty, sky, scenes.get(name), frontCam.get(name)))); - log.info("rendered " + name); + List names = new ArrayList<>(); + for (Map.Entry e : scenes.entrySet()) { + Scene sc = e.getValue(); + List baked = new ArrayList<>(); + for (BlockEntityState s : sc.states()) baked.add(h.beBaker.bake(s)); + double[][] box = bounds(baked); + // Frame the beacon beam's foot, not 2048 blocks. + if (sc.states().get(0).kind() == Kind.BEACON) box[1][1] = box[0][1] + 4; + Vector center = Harness.center(box[0], box[1]); + double dist = Harness.frameDistance(box[0], box[1], 1.4, 0.6); + Vector cam = new Vector(center.getX() - dist, center.getY() + dist * 0.45, center.getZ() - dist * 0.35); + if (sc.frontYaw() != null) { + // Facing yaw (0 = south, 90 = west) → front direction (-sin, cos); offset 25° to the side. + double a = Math.toRadians(sc.frontYaw() + 25); + cam = new Vector(center.getX() - Math.sin(a) * dist, center.getY() + dist * 0.55, center.getZ() + Math.cos(a) * dist); + } + render(h, e.getKey(), Harness.job(empty, cam, center, TW, TH, List.of(), sc.states(), List.of(), List.of()), cells, names); } - for (String name : decoScenes.keySet()) { - cells.add(labelCell(name, renderDeco(renderer, empty, sky, decoScenes.get(name)))); - names.add(name); - log.info("rendered " + name); + for (Map.Entry> e : decoScenes.entrySet()) { + List baked = new ArrayList<>(); + for (DecorationState s : e.getValue()) baked.add(decoBaker.bake(s)); + double[][] box = bounds(baked); + Vector center = Harness.center(box[0], box[1]); + double dist = Harness.frameDistance(box[0], box[1], 1.4, 0.6); + // View from the front side (along +facing) so the picture is visible, not the back. + DecorationState.Facing f = e.getValue().get(0).facing(); + double fx = f.axis() == 0 ? f.sign() : 0, fy = f.axis() == 1 ? f.sign() : 0, fz = f.axis() == 2 ? f.sign() : 0; + Vector cam = new Vector(center.getX() + fx * dist + 0.1, center.getY() + fy * dist + 0.1, center.getZ() + fz * dist + 0.1); + render(h, e.getKey(), Harness.job(empty, cam, center, TW, TH, List.of(), List.of(), e.getValue(), List.of()), cells, names); } File outDir = new File(System.getProperty("out", "/home/elias/Dokumente/PixelPics-BlockEntity-Renders")); outDir.mkdirs(); for (int i = 0; i < names.size(); i++) ImageIO.write(cells.get(i), "png", new File(outDir, names.get(i) + ".png")); - - int cols = 4, cw = cells.get(0).getWidth(), ch = cells.get(0).getHeight(); - int rows = (cells.size() + cols - 1) / cols; - BufferedImage sheet = new BufferedImage(cols * cw, rows * ch, BufferedImage.TYPE_INT_RGB); - Graphics2D g = sheet.createGraphics(); - g.setColor(new Color(40, 40, 48)); - g.fillRect(0, 0, sheet.getWidth(), sheet.getHeight()); - for (int i = 0; i < cells.size(); i++) g.drawImage(cells.get(i), (i % cols) * cw, (i / cols) * ch, null); - g.dispose(); - File f = new File(outDir, "sheet.png"); - ImageIO.write(sheet, "png", f); - System.out.println("WROTE " + f); + Harness.writeSheets(cells, 4, cells.size(), outDir, page -> "sheet.png"); } - /** Scenes viewed from the front (camera along the facing yaw, raised, slightly to the side). */ - static final Map frontCam = new HashMap<>(); - - static BlockEntityState chest(Kind kind, int bx, int bz, float yaw, ChestKind ck, String variant) { - return new BlockEntityState(kind, bx, 0, bz, yaw, null, false, ck, 0, variant, null, null, - List.of(), List.of(), null, null); + static void render(Harness h, String name, RenderJob job, + List cells, List names) { + cells.add(Harness.labelCell(name, h.renderer.execute(job))); + names.add(name); + h.log.info("rendered " + name); } - /** A contents-bearing block-entity at the origin (items / spawner mob / beacon beam / statue pose). */ - static BlockEntityState contents(Kind kind, float yaw, List items, - EntityState mob, Beam beam, String pose, String color) { - return new BlockEntityState(kind, 0, 0, 0, yaw, null, false, null, 0, color, null, null, - List.of(), List.of(), null, null, items, mob, beam, pose); - } - - static List items(String... ids) { - List out = new ArrayList<>(); - for (String id : ids) out.add(id == null ? null : new eu.mhsl.minecraft.pixelpics.render.entity.ItemSpec(id, null, null, false)); - return out; - } - - /** EntityTestRender's default state of a mob with its real bounding box (spawner scale depends on it). */ - static EntityState spawnerMob(String type) throws Exception { - EntityState e = EntityTestRender.state(type, 0); - double[] dims = switch (type) { - case "zombie" -> new double[] {0.6, 1.95}; - case "pig" -> new double[] {0.9, 0.9}; - case "spider" -> new double[] {1.4, 0.9}; - default -> new double[] {0.6, 1.8}; - }; - var comps = EntityState.class.getRecordComponents(); - Object[] vals = new Object[comps.length]; - Class[] types = new Class[comps.length]; - for (int i = 0; i < comps.length; i++) { - types[i] = comps[i].getType(); - vals[i] = switch (comps[i].getName()) { - case "width" -> dims[0]; - case "height" -> dims[1]; - default -> comps[i].getAccessor().invoke(e); - }; - } - return EntityState.class.getDeclaredConstructor(types).newInstance(vals); - } - - static BlockEntityState head(Kind kind, String type, float yaw) { - return st(kind, 0, 0, yaw, null, false, null, 0, type, List.of(), List.of()); - } - - static BufferedImage renderScene(DefaultScreenRenderer renderer, WorldSnapshot world, SkyContext sky, - List states, Float frontYaw) { + /** {min, max} over the baked boxes (null entries skipped); the unit cube when nothing baked. */ + static double[][] bounds(List baked) { double[] min = {Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE}; double[] max = {-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE}; - for (BlockEntityState s : states) { - RenderedEntity re = beBaker.bake(s); + for (RenderedEntity re : baked) { if (re == null) continue; - for (int a = 0; a < 3; a++) { min[a] = Math.min(min[a], re.aabbMin[a]); max[a] = Math.max(max[a], re.aabbMax[a]); } + for (int a = 0; a < 3; a++) { + min[a] = Math.min(min[a], re.aabbMin[a]); + max[a] = Math.max(max[a], re.aabbMax[a]); + } } - if (min[0] > max[0]) { min = new double[]{0, 0, 0}; max = new double[]{1, 1, 1}; } - if (states.get(0).kind() == Kind.BEACON) max[1] = min[1] + 4; // frame the beam's foot, not 2048 blocks - double cx = (min[0] + max[0]) / 2, cy = (min[1] + max[1]) / 2, cz = (min[2] + max[2]) / 2; - double ext = 0.5; - for (int a = 0; a < 3; a++) ext = Math.max(ext, max[a] - min[a]); - double dist = ext / (2 * Math.tan(V_FOV_HALF)) * 1.4 + 0.6; - - Vector center = new Vector(cx, cy, cz); - Vector cam = new Vector(cx - dist, cy + dist * 0.45, cz - dist * 0.35); - if (frontYaw != null) { - // Facing yaw (0 = south, 90 = west) → front direction (-sin, cos); offset 25° to the side. - double a = Math.toRadians(frontYaw + 25); - cam = new Vector(cx - Math.sin(a) * dist, cy + dist * 0.55, cz + Math.cos(a) * dist); - } - Location loc = new Location(null, cam.getX(), cam.getY(), cam.getZ()); - loc.setDirection(center.clone().subtract(cam)); - List rayMap = buildRayMap(loc, TW * SSAA, TH * SSAA); - RenderJob job = new RenderJob(world, rayMap, cam, TW, TH, sky, List.of(), states, List.of(), List.of()); - return renderer.execute(job); - } - - static BufferedImage renderDeco(DefaultScreenRenderer renderer, WorldSnapshot world, SkyContext sky, - List states) { - double[] min = {Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE}; - double[] max = {-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE}; - for (DecorationState s : states) { - RenderedEntity re = decoBaker.bake(s); - if (re == null) continue; - for (int a = 0; a < 3; a++) { min[a] = Math.min(min[a], re.aabbMin[a]); max[a] = Math.max(max[a], re.aabbMax[a]); } - } - if (min[0] > max[0]) { min = new double[]{0, 0, 0}; max = new double[]{1, 1, 1}; } - double cx = (min[0] + max[0]) / 2, cy = (min[1] + max[1]) / 2, cz = (min[2] + max[2]) / 2; - double ext = 0.5; - for (int a = 0; a < 3; a++) ext = Math.max(ext, max[a] - min[a]); - double dist = ext / (2 * Math.tan(V_FOV_HALF)) * 1.4 + 0.6; - // View from the front side (along +facing) so the picture is visible, not the back. - DecorationState.Facing f = states.get(0).facing(); - double fx = f.axis() == 0 ? f.sign() : 0, fy = f.axis() == 1 ? f.sign() : 0, fz = f.axis() == 2 ? f.sign() : 0; - Vector center = new Vector(cx, cy, cz); - Vector cam = new Vector(cx + fx * dist + 0.1, cy + fy * dist + 0.1, cz + fz * dist + 0.1); - Location loc = new Location(null, cam.getX(), cam.getY(), cam.getZ()); - loc.setDirection(center.clone().subtract(cam)); - List rayMap = buildRayMap(loc, TW * SSAA, TH * SSAA); - RenderJob job = new RenderJob(world, rayMap, cam, TW, TH, sky, List.of(), List.of(), states, List.of()); - return renderer.execute(job); - } - - static BufferedImage labelCell(String key, BufferedImage v) { - int labelH = 20; - BufferedImage cell = new BufferedImage(v.getWidth(), v.getHeight() + labelH, BufferedImage.TYPE_INT_RGB); - Graphics2D g = cell.createGraphics(); - g.setColor(new Color(25, 25, 30)); - g.fillRect(0, 0, cell.getWidth(), cell.getHeight()); - g.drawImage(v, 0, labelH, null); - g.setColor(Color.WHITE); - g.setFont(new Font("SansSerif", Font.BOLD, 13)); - g.drawString(key, 4, 15); - g.dispose(); - return cell; - } - - static List buildRayMap(Location eye, int width, int height) { - return eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer.buildRayMap(eye, width, height); + if (min[0] > max[0]) return new double[][] {{0, 0, 0}, {1, 1, 1}}; + return new double[][] {min, max}; } } diff --git a/tools/EntityBenchmark.java b/tools/EntityBenchmark.java index 0c34daf..b917d66 100644 --- a/tools/EntityBenchmark.java +++ b/tools/EntityBenchmark.java @@ -1,11 +1,7 @@ -import eu.mhsl.minecraft.pixelpics.assets.*; import eu.mhsl.minecraft.pixelpics.render.entity.*; -import eu.mhsl.minecraft.pixelpics.render.render.*; -import eu.mhsl.minecraft.pixelpics.render.sky.SkyContext; +import eu.mhsl.minecraft.pixelpics.render.render.RenderJob; import eu.mhsl.minecraft.pixelpics.render.snapshot.WorldSnapshot; -import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider; import org.bukkit.ChunkSnapshot; -import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.block.data.BlockData; import org.bukkit.util.Vector; @@ -16,41 +12,24 @@ import java.io.File; import java.lang.reflect.Proxy; import java.util.*; import java.util.List; -import java.util.logging.Logger; /** - * Reproducible render benchmark (no server): farm scenes with hundreds of mobs / dropped items on a block - * floor plus a "normal" scene (few mobs, translucent + additive layers). Renders every scene {@code -Druns} - * times (after {@code -Dwarmup} runs), prints the median wall time and writes one PNG per scene to - * {@code -Dout} (default ~/Dokumente/PixelPics-Entity-Renders/bench) for before/after pixel diffs. - * Args: scene names to run (default all): farm_mobs, farm_empty (same view, no entities), farm_mobs_close, - * farm_items, village, normal. {@code -Djava.util.concurrent.ForkJoinPool.common.parallelism=N} pins the thread count. + * Reproducible render benchmark (no server): farm scenes with hundreds of mobs / dropped items on a grass floor + * plus a "normal" scene (few mobs, translucent + additive layers). Renders every scene {@code -Druns} times (after + * {@code -Dwarmup} runs), prints median/min wall time and writes one PNG per scene to {@code -Dout} (default + * ~/Dokumente/PixelPics-Entity-Renders/bench) for before/after pixel diffs. Args: scene names (default all): + * farm_mobs, farm_empty (same view, no entities), farm_mobs_close, farm_items, village, normal. + * {@code -Djava.util.concurrent.ForkJoinPool.common.parallelism=N} pins the thread count. Build: see {@link Harness}. */ public class EntityBenchmark { - static final String ROOT = "/home/elias/Dokumente/mcTestServer/plugins/PixelPics"; - static final int W = 320, H = 240, SSAA = 3; + static final int W = 320, H = 240; record Scene(String name, Vector cam, Vector look, List mobs, List objects) {} public static void main(String[] args) throws Exception { - Logger log = Logger.getLogger("bench"); - ResourcePack pack = ResourcePackLoader.load(new File(ROOT, "resourcepack"), log).orElseThrow(); - AssetReader reader = new AssetReader(pack); - TextureCache textures = new TextureCache(pack); - BlockModelRegistry registry = new BlockModelRegistry(reader, textures); - BiomeTintProvider tint = new BiomeTintProvider(textures); - var geo = new eu.mhsl.minecraft.pixelpics.render.entity.cem.CemModelLoader(); - geo.load(new java.io.FileInputStream("src/main/resources/cem/cem_template_models.json"), log); - geo.load(new java.io.FileInputStream("src/main/resources/cem/vanilla_entity_models.json"), log); - var font = eu.mhsl.minecraft.pixelpics.assets.font.FontLoader.load(pack, textures, log); - SkinCache skins = new SkinCache(); - var baker = new eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker(geo, textures, skins); - var beBaker = new eu.mhsl.minecraft.pixelpics.render.entity.cem.BlockEntityBaker(geo, textures, skins, font); - DefaultScreenRenderer renderer = new DefaultScreenRenderer(registry, tint, textures, baker, beBaker, log); - + Harness h = new Harness("bench"); WorldSnapshot world = farmWorld(); - SkyContext sky = new SkyContext(6000, 0, 6000); int runs = Integer.getInteger("runs", 5), warmup = Integer.getInteger("warmup", 2); File outDir = new File(System.getProperty("out", "/home/elias/Dokumente/PixelPics-Entity-Renders/bench")); outDir.mkdirs(); @@ -58,16 +37,13 @@ public class EntityBenchmark { for (Scene sc : scenes()) { if (!only.isEmpty() && !only.contains(sc.name)) continue; - Location loc = new Location(null, sc.cam.getX(), sc.cam.getY(), sc.cam.getZ()); - loc.setDirection(sc.look.clone().subtract(sc.cam)); - List rayMap = EntityTestRender.buildRayMap(loc, W * SSAA, H * SSAA); - RenderJob job = new RenderJob(world, rayMap, sc.cam, W, H, sky, sc.mobs, List.of(), List.of(), sc.objects); + RenderJob job = Harness.job(world, sc.cam, sc.look, W, H, sc.mobs, List.of(), List.of(), sc.objects); BufferedImage img = null; - for (int i = 0; i < warmup; i++) img = renderer.execute(job); + for (int i = 0; i < warmup; i++) img = h.renderer.execute(job); long[] times = new long[runs]; for (int i = 0; i < runs; i++) { long t0 = System.nanoTime(); - img = renderer.execute(job); + img = h.renderer.execute(job); times[i] = System.nanoTime() - t0; } Arrays.sort(times); @@ -77,7 +53,7 @@ public class EntityBenchmark { } } - static List scenes() { + static List scenes() throws ReflectiveOperationException { List out = new ArrayList<>(); // Mob farm: 400 animals in a 20x20 pen (1.4 block spacing), seen from an elevated corner. List farm = new ArrayList<>(); @@ -106,9 +82,9 @@ public class EntityBenchmark { ItemSpec spec; if (r.nextInt(4) == 0) { int b = r.nextInt(blocks.length); - spec = new ItemSpec(blocks[b].getKey().getKey(), ObjectTestRender.block(blocks[b], blockState[b]), null, false); + spec = new ItemSpec(blocks[b].getKey().getKey(), Harness.block(blocks[b], blockState[b]), null, false); } else { - spec = new ItemSpec(flat[r.nextInt(flat.length)], null, null, false); + spec = Harness.item(flat[r.nextInt(flat.length)]); } items.add(ObjectState.item(a, spec, "ground", true)); } @@ -149,11 +125,10 @@ public class EntityBenchmark { few.add(mob("villager", 4, 1, 6, 160, false)); few.add(mob("zombie", 6.5, 1, 6, 220, false)); few.add(mob("slime", 2, 1, 5.5, 120, false)); - EntityState creeper = mob("creeper", 7, 1, 3.5, 250, false); - few.add(withPowered(creeper)); + few.add(Harness.copy(mob("creeper", 7, 1, 3.5, 250, false), Map.of("powered", true))); few.add(mob("chicken", 3.5, 1, 1.5, 90, false)); List objs = new ArrayList<>(); - objs.add(ObjectState.item(Affine.translation(4.5, 1.2, 2).mul(Affine.scale(0.5)), new ItemSpec("apple", null, null, false), "ground", true)); + objs.add(ObjectState.item(Affine.translation(4.5, 1.2, 2).mul(Affine.scale(0.5)), Harness.item("apple"), "ground", true)); objs.add(ObjectState.text(Affine.translation(4.5, 3.2, 4).mul(Affine.rotY(Math.PI)).mul(Affine.scale(0.025)), List.of("PixelPics bench"), 200, 0xFFFFFFFF, 0x40000000, true)); out.add(new Scene("normal", new Vector(0, 3.5, -2.5), new Vector(4.5, 1.5, 4), few, objs)); @@ -173,20 +148,20 @@ public class EntityBenchmark { static final int[] DYES = {0xFFF9FFFE, 0xFFF9801D, 0xFFC74EBD, 0xFF3AB3DA, 0xFFFED83D, 0xFF80C71F, 0xFFF38BAA, 0xFF474F52, 0xFF9D9D97, 0xFF169C9C, 0xFF8932B8, 0xFF3C44AA, 0xFF835432, 0xFF5E7C16, 0xFFB02E26, 0xFF1D1D21}; - static EntityState villager(double x, double z, float yaw, String type, String prof, int level, boolean baby) { - return new EntityState("villager", x, 1, z, yaw, baby, 0.6, 1.95, - false, null, false, type, 0, 1.0, prof, level, - null, false, false, false, null, null, - false, 0, 0, 0, false, null, null, null, 0, false, null, null, null, null, null); + static EntityState villager(double x, double z, float yaw, String type, String prof, int level, boolean baby) + throws ReflectiveOperationException { + return Harness.copy(mob("villager", x, 1, z, yaw, baby), Map.of("width", 0.6, "height", 1.95, "variant", type, + "profession", prof, "villagerLevel", level)); } /** {@code s} with wool tint, humanoid armor and collar dye replaced. */ - static EntityState with(EntityState s, int tint, EntityState.Equipment eq, int collar) { - return new EntityState(s.typeKey(), s.x(), s.y(), s.z(), s.bodyYaw(), s.baby(), s.width(), s.height(), - s.player(), s.skinUrl(), s.slim(), s.variant(), tint, s.sizeScale(), s.profession(), s.villagerLevel(), - s.markings(), s.saddle(), s.chest(), s.arms(), s.bodyEquip(), eq, s.invisible(), s.pitch(), - s.bodyDye(), s.tint2(), s.passenger(), s.rightHand(), s.leftHand(), s.headItem(), collar, s.powered(), - s.mouthItem(), s.pose(), s.partPoses(), s.shoulderLeft(), s.shoulderRight()); + static EntityState with(EntityState s, int tint, EntityState.Equipment eq, int collar) + throws ReflectiveOperationException { + Map o = new HashMap<>(); + o.put("tint", tint); + o.put("equipment", eq); + o.put("collar", collar); + return Harness.copy(s, o); } /** The variant the game always supplies (cow/pig/chicken have no texture without one). */ @@ -199,41 +174,23 @@ public class EntityBenchmark { }; } - static EntityState withPowered(EntityState s) { - return new EntityState(s.typeKey(), s.x(), s.y(), s.z(), s.bodyYaw(), s.baby(), s.width(), s.height(), - s.player(), s.skinUrl(), s.slim(), s.variant(), s.tint(), s.sizeScale(), s.profession(), s.villagerLevel(), - s.markings(), s.saddle(), s.chest(), s.arms(), s.bodyEquip(), s.equipment(), s.invisible(), s.pitch(), - s.bodyDye(), s.tint2(), s.passenger(), s.rightHand(), s.leftHand(), s.headItem(), s.collar(), true, - s.mouthItem(), s.pose(), s.partPoses(), s.shoulderLeft(), s.shoulderRight()); - } - /** * Grass floor at y=0 over [-48, 48)². Nothing stacked on it: ambient occlusion asks Material.isOccluding() * for non-air neighbours, which needs a server registry (not available standalone). */ static WorldSnapshot farmWorld() { - BlockData air = ObjectTestRender.block(Material.AIR, null); - BlockData grass = ObjectTestRender.block(Material.GRASS_BLOCK, "snowy=false"); + BlockData air = Harness.block(Material.AIR, null); + BlockData grass = Harness.block(Material.GRASS_BLOCK, "snowy=false"); Map chunks = new HashMap<>(); for (int cx = -3; cx < 3; cx++) { for (int cz = -3; cz < 3; cz++) { - final int bx0 = cx * 16, bz0 = cz * 16; ChunkSnapshot cs = (ChunkSnapshot) Proxy.newProxyInstance(EntityBenchmark.class.getClassLoader(), - new Class[]{ChunkSnapshot.class}, (p, m, a) -> { - switch (m.getName()) { - case "getBlockData": { - int y = (int) a[1]; - return y == 0 ? grass : air; - } - case "getBiome": return null; - case "equals": return p == a[0]; - case "hashCode": return System.identityHashCode(p); - case "toString": return "chunk"; - } - Class rt = m.getReturnType(); - if (rt == boolean.class) return false; - if (rt.isPrimitive()) return 0; - return null; + new Class[]{ChunkSnapshot.class}, (p, m, a) -> switch (m.getName()) { + case "getBlockData" -> (int) a[1] == 0 ? grass : air; + case "equals" -> p == a[0]; + case "hashCode" -> System.identityHashCode(p); + case "toString" -> "chunk"; + default -> Harness.defaultValue(m.getReturnType()); }); chunks.put(WorldSnapshot.chunkKey(cx, cz), cs); } diff --git a/tools/EntityTestRender.java b/tools/EntityTestRender.java index 97420e3..92f207e 100644 --- a/tools/EntityTestRender.java +++ b/tools/EntityTestRender.java @@ -1,32 +1,23 @@ -import eu.mhsl.minecraft.pixelpics.assets.*; import eu.mhsl.minecraft.pixelpics.render.entity.*; -import eu.mhsl.minecraft.pixelpics.render.render.*; -import eu.mhsl.minecraft.pixelpics.render.sky.SkyContext; import eu.mhsl.minecraft.pixelpics.render.snapshot.WorldSnapshot; -import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.block.data.BlockData; import org.bukkit.util.Vector; import javax.imageio.ImageIO; -import java.awt.*; import java.awt.image.BufferedImage; import java.io.File; -import java.lang.reflect.Proxy; import java.util.*; import java.util.List; -import java.util.logging.Logger; -/** Standalone (no server) renderer: every entity twice (yaw 45° / 225°) against empty sky -> contact sheets. */ +/** + * Standalone (no server) renderer: every entity twice (yaw 45° / 225°) against empty sky -> contact sheets + * ({@code page_.png}) + {@code einzeln/.png} in {@code -Dout} (default ~/Dokumente/PixelPics-Entity-Renders). + * Args: case specs (default {@link #ENTITIES}), or {@code @variants} for {@link #VARIANT_CASES}. Build: see {@link Harness}. + */ public class EntityTestRender { - static final String ROOT = "/home/elias/Dokumente/mcTestServer/plugins/PixelPics"; - static final double V_FOV_HALF = Math.toRadians(eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer.V_FOV_DEG / 2); - static final int SSAA = 3; static final int TW = 200, TH = 230; // per-view tile size - // Entity type keys to render (current vanilla + bundled bedrock specials). variant=null (base look). + // Default case list: every entity type in its base look ("plain": the per-type tables below apply). static final String[] ENTITIES = { "allay","armadillo","armor_stand","axolotl","bat","bee","blaze","bogged","breeze","camel", "cat","cave_spider","chicken","cod","copper_golem","cow","creaking","creeper","dolphin","donkey", @@ -42,8 +33,10 @@ public class EntityTestRender { "minecart","end_crystal","arrow","trident","leash_knot","evoker_fangs","wither_skull","shulker_bullet","cushion" }; - // "@variants" case set (before/after pixel diffs of variant / pose-flag / armor handling). Spec syntax: - // [baby:]type[@variant][+flag…], flags: chest, arms, tame (red collar), saddle, armor (full diamond), noarmor. + // "@variants" case set (before/after pixel diffs of refactors). Spec: [baby:]type[@variant][+flag…], flags: + // chest, arms, saddle, tame (red collar), armor (full diamond), elytra, red (sheep wool), sword / crossbow + // (right hand), crossbowl (left hand), mouth (sweet berries), passenger, shoulder (red + blue parrot), stand + // (custom armor-stand part poses), pitch:, p: (sleeping, sitting, eyes_glowing, …). static final List VARIANT_CASES = new ArrayList<>(); static { for (String v : new String[] {"tabby", "black", "red", "siamese", "british_shorthair", "calico", "persian", @@ -68,13 +61,11 @@ public class EntityTestRender { VARIANT_CASES.add("mooshroom@red"); VARIANT_CASES.add("mooshroom@brown"); VARIANT_CASES.add("baby:mooshroom@brown"); - // Armor stand with/without arms, small, with armor. - for (String a : new String[] {"armor_stand", "armor_stand+arms", "baby:armor_stand", "baby:armor_stand+arms", - "armor_stand+arms+armor", "baby:armor_stand+arms+armor"}) VARIANT_CASES.add(a); - // Chests. - for (String c : new String[] {"llama@creamy", "llama@creamy+chest", "llama@brown+chest+saddle", "llama@gray+chest", + VARIANT_CASES.addAll(List.of("armor_stand", "armor_stand+arms", "baby:armor_stand", "baby:armor_stand+arms", + "armor_stand+arms+armor", "baby:armor_stand+arms+armor")); + VARIANT_CASES.addAll(List.of("llama@creamy", "llama@creamy+chest", "llama@brown+chest+saddle", "llama@gray+chest", "trader_llama@white+chest", "baby:llama@creamy+chest", "donkey", "donkey+chest", "donkey+chest+saddle", - "mule", "mule+chest", "mule+chest+saddle", "baby:donkey+chest"}) VARIANT_CASES.add(c); + "mule", "mule+chest", "mule+chest+saddle", "baby:donkey+chest")); // Armor wearers (and non-wearers given armor: nothing must appear). for (String t : new String[] {"zombie", "husk", "drowned", "zombie_villager", "zombified_piglin", "skeleton", "stray", "wither_skeleton", "bogged", "parched", "piglin", "piglin_brute", "giant", "player", @@ -84,7 +75,7 @@ public class EntityTestRender { VARIANT_CASES.add(t + "+armor"); // Layer / texture states: emissive eyes, sheep undercoat, angry wolves, tipped arrows, invulnerable // wither, partial-alpha cutout textures, held items (piglin arm poses), elytra. - for (String c : new String[] {"spider", "cave_spider", "enderman", "phantom", "breeze", "warden", + VARIANT_CASES.addAll(List.of("spider", "cave_spider", "enderman", "phantom", "breeze", "warden", "copper_golem@oxidized", "creaking", "creaking+p:eyes_glowing", "sheep@sheared", "sheep+red", "sheep@sheared+red", "baby:sheep+red", "sheep+p:jeb", "wolf@pale+p:angry", "baby:wolf@pale+p:angry", "wolf@pale+tame+p:angry", "arrow+p:tipped", "spectral_arrow", "wither+p:invulnerable", @@ -92,12 +83,11 @@ public class EntityTestRender { "piglin+sword+crossbowl", "piglin+crossbow+p:arms_crossbow_hold", "piglin+sword+p:arms_attacking_with_melee_weapon", "zombified_piglin+sword", "piglin_brute+sword", "pillager+crossbow", "vindicator+sword", "armor_stand+elytra", "zombie+elytra", "baby:armor_stand+armor", - "baby:armor_stand+arms+elytra"}) - VARIANT_CASES.add(c); + "baby:armor_stand+arms+elytra")); VARIANT_CASES.addAll(List.of(ENTITIES)); } - // Representative variants (the game always supplies these; null base textures wouldn't exist otherwise). + // Variants the game always supplies (several types have no texture without one); villager profession/level. static final Map VAR = Map.ofEntries( Map.entry("cat", "tabby"), Map.entry("wolf", "pale"), Map.entry("axolotl", "lucy"), Map.entry("parrot", "red"), Map.entry("rabbit", "brown"), Map.entry("horse", "white"), @@ -108,7 +98,6 @@ public class EntityTestRender { Map.entry("tropical_fish", "betty"), Map.entry("pufferfish", "big"), Map.entry("zombie_nautilus", "warm") ); - // Villager profession / level for the standalone render (biome type comes from VAR above). static final Map PROF = Map.ofEntries( Map.entry("villager", "librarian"), Map.entry("zombie_villager", "farmer") ); @@ -116,17 +105,17 @@ public class EntityTestRender { Map.entry("villager", 5) ); - // Horse/llama/donkey equipment for the standalone render. - static final Map MARK = Map.of("horse", "blackdots"); // coat markings - static final java.util.Set SADDLE = java.util.Set.of("horse", "donkey", "mule", "nautilus", "pig", "strider", "camel"); - static final java.util.Set CHEST = java.util.Set.of("llama", "donkey"); - static final Map EQUIP = Map.ofEntries( // armor / carpet + // Plain cases: horse markings, saddles, chests, body armor / carpet / harness. + static final Map MARK = Map.of("horse", "blackdots"); + static final Set SADDLE = Set.of("horse", "donkey", "mule", "nautilus", "pig", "strider", "camel"); + static final Set CHEST = Set.of("llama", "donkey"); + static final Map EQUIP = Map.ofEntries( Map.entry("horse", "diamond"), Map.entry("llama", "red"), Map.entry("happy_ghast", "blue_harness"), Map.entry("wolf", "armadillo_scute"), Map.entry("trader_llama", "trader_llama"), Map.entry("nautilus", "diamond") ); - // Humanoid armor test cases (worn equipment, trims, glint, elytra). + // Plain cases: humanoid armor (diamond, dyed leather, trimmed iron, enchanted netherite, glint helmet + elytra). static EntityState.EquipPiece P(String asset) { return new EntityState.EquipPiece(asset, 0, null, null, false); } static EntityState.EquipPiece P(String asset, int dye, String trimMat, String trimPat, boolean glint) { return new EntityState.EquipPiece(asset, dye, trimMat, trimPat, glint); @@ -134,124 +123,50 @@ public class EntityTestRender { static final Map ARMOR = new HashMap<>(); static { - // Full diamond armor. ARMOR.put("skeleton", new EntityState.Equipment(P("diamond"), P("diamond"), P("diamond"), P("diamond"))); - // Dyed (orange) leather armor. EntityState.EquipPiece le = P("leather", 0xFFFF8000, null, null, false); ARMOR.put("zombie", new EntityState.Equipment(le, le, le, le)); - // Iron armor with a coast/diamond trim. EntityState.EquipPiece ir = P("iron", 0, "diamond", "coast", false); ARMOR.put("armor_stand", new EntityState.Equipment(ir, ir, ir, ir)); - // Enchanted netherite armor (glint). EntityState.EquipPiece ne = P("netherite", 0, null, null, true); ARMOR.put("wither_skeleton", new EntityState.Equipment(ne, ne, ne, ne)); - // Enchanted diamond helmet + elytra in the chest slot. ARMOR.put("player", new EntityState.Equipment(P("diamond", 0, null, null, true), P("elytra"), null, null)); - // Gold helmet. ARMOR.put("piglin", new EntityState.Equipment(P("gold"), null, null, null)); } // Invisible test cases: armor_stand -> only its (trimmed iron) armor floats; creeper -> nothing renders. - static final java.util.Set INVISIBLE = java.util.Set.of("armor_stand", "creeper"); + static final Set INVISIBLE = Set.of("armor_stand", "creeper"); public static void main(String[] args) throws Exception { - Logger log = Logger.getLogger("test"); - ResourcePack pack = ResourcePackLoader.load(new File(ROOT, "resourcepack"), log).orElseThrow(); - AssetReader reader = new AssetReader(pack); - TextureCache textures = new TextureCache(pack); - BlockModelRegistry registry = new BlockModelRegistry(reader, textures); - BiomeTintProvider tint = new BiomeTintProvider(textures); - eu.mhsl.minecraft.pixelpics.render.entity.cem.CemModelLoader geo = new eu.mhsl.minecraft.pixelpics.render.entity.cem.CemModelLoader(); - int n = geo.load(new java.io.FileInputStream("src/main/resources/cem/cem_template_models.json"), log); - n = geo.load(new java.io.FileInputStream("src/main/resources/cem/vanilla_entity_models.json"), log); - log.info("Loaded " + n + " geometries"); - SkinCache skins = new SkinCache(); - eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker baker = new eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker(geo, textures, skins); - eu.mhsl.minecraft.pixelpics.assets.font.BitmapFont font = - eu.mhsl.minecraft.pixelpics.assets.font.FontLoader.load(pack, textures, log); - eu.mhsl.minecraft.pixelpics.render.entity.cem.BlockEntityBaker beBaker = new eu.mhsl.minecraft.pixelpics.render.entity.cem.BlockEntityBaker(geo, textures, skins, font); - DefaultScreenRenderer renderer = new DefaultScreenRenderer(registry, tint, textures, baker, beBaker, log); - - BlockData air = (BlockData) Proxy.newProxyInstance(EntityTestRender.class.getClassLoader(), - new Class[]{BlockData.class}, (p, m, a) -> { - switch (m.getName()) { - case "getMaterial": return Material.AIR; - case "equals": return p == a[0]; - case "hashCode": return System.identityHashCode(p); - case "toString": return "air"; - } - Class rt = m.getReturnType(); - if (rt == boolean.class) return false; - if (rt.isPrimitive()) return 0; - return null; - }); - WorldSnapshot empty = new WorldSnapshot(Map.of(), 0, 1, air); - SkyContext sky = new SkyContext(6000, 0, 6000); - + Harness h = new Harness("test"); + WorldSnapshot empty = Harness.emptyWorld(0, 1); String[] list = args.length == 0 ? ENTITIES : args[0].equals("@variants") ? VARIANT_CASES.toArray(new String[0]) : args; List cells = new ArrayList<>(); for (String key : list) { - BufferedImage v1 = renderEntity(renderer, baker, empty, sky, key, 45); - BufferedImage v2 = renderEntity(renderer, baker, empty, sky, key, 225); - cells.add(labelCell(key, v1, v2)); - log.info("rendered " + key); + cells.add(Harness.labelCell(key, renderEntity(h, empty, key, 45), renderEntity(h, empty, key, 225))); + h.log.info("rendered " + key); } File outDir = new File(System.getProperty("out", "/home/elias/Dokumente/PixelPics-Entity-Renders")); - outDir.mkdirs(); - // One image per entity (both 45°/225° views side by side), named by entity key. File single = new File(outDir, "einzeln"); single.mkdirs(); for (int i = 0; i < list.length; i++) { ImageIO.write(cells.get(i), "png", new File(single, list[i].replaceAll("[:@+]", "_") + ".png")); } - - // Compose contact-sheet pages: 2 columns. - int cols = 2, cellW = cells.get(0).getWidth(), cellH = cells.get(0).getHeight(); - int perPage = cols * 5; - for (int page = 0, idx = 0; idx < cells.size(); page++) { - int count = Math.min(perPage, cells.size() - idx); - int rows = (count + cols - 1) / cols; - BufferedImage sheet = new BufferedImage(cols * cellW, rows * cellH, BufferedImage.TYPE_INT_RGB); - Graphics2D g = sheet.createGraphics(); - g.setColor(new Color(40, 40, 48)); - g.fillRect(0, 0, sheet.getWidth(), sheet.getHeight()); - for (int i = 0; i < count; i++) { - int r = i / cols, c = i % cols; - g.drawImage(cells.get(idx + i), c * cellW, r * cellH, null); - } - g.dispose(); - File f = new File(outDir, String.format("page_%d.png", page)); - ImageIO.write(sheet, "png", f); - System.out.println("WROTE " + f); - idx += count; - } + Harness.writeSheets(cells, 2, 10, outDir, page -> "page_" + page + ".png"); } - static BufferedImage renderEntity(DefaultScreenRenderer renderer, eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker baker, WorldSnapshot world, - SkyContext sky, String key, float yaw) { + static BufferedImage renderEntity(Harness h, WorldSnapshot world, String key, float yaw) { EntityState s = state(key, yaw); - RenderedEntity re = baker.bake(s); + RenderedEntity re = h.baker.bake(s); if (re == null || re.cubes.isEmpty()) { // invisible with no equipment -> nothing renders return new BufferedImage(TW, TH, BufferedImage.TYPE_INT_RGB); } - double cx = (re.aabbMin[0] + re.aabbMax[0]) / 2; - double cy = (re.aabbMin[1] + re.aabbMax[1]) / 2; - double cz = (re.aabbMin[2] + re.aabbMax[2]) / 2; - double ext = 0; - for (int a = 0; a < 3; a++) ext = Math.max(ext, re.aabbMax[a] - re.aabbMin[a]); - if (ext < 0.5) ext = 0.5; - double dist = ext / (2 * Math.tan(V_FOV_HALF)) * 1.25 + 0.4; - - Vector center = new Vector(cx, cy, cz); - Vector cam = new Vector(cx - dist, cy + dist * 0.42, cz - dist * 0.15); - Location loc = new Location(null, cam.getX(), cam.getY(), cam.getZ()); - loc.setDirection(center.clone().subtract(cam)); - - List rayMap = buildRayMap(loc, TW * SSAA, TH * SSAA); - RenderJob job = new RenderJob(world, rayMap, cam, TW, TH, sky, List.of(s), List.of(), List.of(), List.of()); - return renderer.execute(job); + Vector center = Harness.center(re.aabbMin, re.aabbMax); + double dist = Harness.frameDistance(re.aabbMin, re.aabbMax, 1.25, 0.4); + Vector cam = new Vector(center.getX() - dist, center.getY() + dist * 0.42, center.getZ() - dist * 0.15); + return h.renderer.execute(Harness.job(world, cam, center, TW, TH, List.of(s), List.of(), List.of(), List.of())); } /** Entity state for a case spec ({@code [baby:]type[@variant][+flag…]}, see VARIANT_CASES). */ @@ -266,7 +181,7 @@ public class EntityTestRender { variant = key.substring(key.indexOf('@') + 1); key = key.substring(0, key.indexOf('@')); } - boolean plain = flagSplit.length == 1 && variant == null; // default ENTITIES entry: legacy test setup + boolean plain = flagSplit.length == 1 && variant == null; if (variant == null) variant = VAR.get(key); boolean isPlayer = key.equals("player"); int tint = key.equals("tropical_fish") ? 0xFFF9801D : (key.equals("sheep") ? 0xFFF9FFFE : 0); @@ -283,8 +198,6 @@ public class EntityTestRender { String equip = plain ? EQUIP.get(key) : null; boolean arms = flags.contains("arms"); int collar = flags.contains("tame") ? 0xFFB02E26 : 0; - // Pose flags: "p:" (sleeping, sitting, lying, …), "pitch:", "passenger", "shoulder" - // (red + blue parrot on the shoulders), "stand" (custom armor-stand part poses), "mouth" (sweet berries). List poses = new ArrayList<>(); float pitch = 0; for (String f : flags) { @@ -306,11 +219,10 @@ public class EntityTestRender { partPoses.put("rightLegPose", new float[] {-40, 0, 0}); } boolean shoulder = flags.contains("shoulder"); - ItemSpec mouth = flags.contains("mouth") ? new ItemSpec("sweet_berries", null, null, false) : null; - // Held items: "sword" (right hand), "crossbow" (right hand), "crossbowl" (left hand). - ItemSpec right = flags.contains("sword") ? new ItemSpec("iron_sword", null, null, false) - : flags.contains("crossbow") ? new ItemSpec("crossbow", null, null, false) : null; - ItemSpec left = flags.contains("crossbowl") ? new ItemSpec("crossbow", null, null, false) : null; + ItemSpec mouth = flags.contains("mouth") ? Harness.item("sweet_berries") : null; + ItemSpec right = flags.contains("sword") ? Harness.item("iron_sword") + : flags.contains("crossbow") ? Harness.item("crossbow") : null; + ItemSpec left = flags.contains("crossbowl") ? Harness.item("crossbow") : null; return new EntityState(key, 0, 0, 0, yaw, baby, 0.8, 1.0, isPlayer, null, false, variant, tint, 1.0, PROF.get(key), LVL.getOrDefault(key, 0), plain ? MARK.get(key) : null, saddle, chest, arms, equip, armor, @@ -318,24 +230,4 @@ public class EntityTestRender { collar, false, mouth, poses.isEmpty() ? null : String.join("+", poses), partPoses, shoulder ? "red_blue" : null, shoulder ? "blue" : null); } - - static BufferedImage labelCell(String key, BufferedImage v1, BufferedImage v2) { - int w = v1.getWidth() + v2.getWidth(); - int labelH = 20; - BufferedImage cell = new BufferedImage(w, v1.getHeight() + labelH, BufferedImage.TYPE_INT_RGB); - Graphics2D g = cell.createGraphics(); - g.setColor(new Color(25, 25, 30)); - g.fillRect(0, 0, cell.getWidth(), cell.getHeight()); - g.drawImage(v1, 0, labelH, null); - g.drawImage(v2, v1.getWidth(), labelH, null); - g.setColor(Color.WHITE); - g.setFont(new Font("SansSerif", Font.BOLD, 13)); - g.drawString(key, 4, 15); - g.dispose(); - return cell; - } - - static List buildRayMap(Location eye, int width, int height) { - return eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer.buildRayMap(eye, width, height); - } } diff --git a/tools/Harness.java b/tools/Harness.java new file mode 100644 index 0000000..331c95a --- /dev/null +++ b/tools/Harness.java @@ -0,0 +1,170 @@ +import eu.mhsl.minecraft.pixelpics.assets.*; +import eu.mhsl.minecraft.pixelpics.assets.font.FontLoader; +import eu.mhsl.minecraft.pixelpics.render.entity.*; +import eu.mhsl.minecraft.pixelpics.render.entity.cem.BlockEntityBaker; +import eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker; +import eu.mhsl.minecraft.pixelpics.render.entity.cem.CemModelLoader; +import eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer; +import eu.mhsl.minecraft.pixelpics.render.render.RenderJob; +import eu.mhsl.minecraft.pixelpics.render.sky.SkyContext; +import eu.mhsl.minecraft.pixelpics.render.snapshot.WorldSnapshot; +import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.data.BlockData; +import org.bukkit.util.Vector; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Proxy; +import java.util.List; +import java.util.Map; +import java.util.function.IntFunction; +import java.util.logging.Logger; + +/** + * Shared setup of the standalone render tools (no server: EntityTestRender, BlockEntityTestRender, + * ObjectTestRender, EntityBenchmark). Renders with the test server's resource pack and the repo's model JSONs. + * + *

Build/run from the repo root with a JDK 25: {@code CP=build/classes/java/main:build/resources/main:$(./gradlew -q + * printCompileClasspath)}, {@code javac -d -cp $CP $(ls tools/*.java | grep -v VanillaModelDump)}, + * {@code java -cp :$CP [-Dout=

] [args]}. + */ +final class Harness { + + static final String PACK_ROOT = "/home/elias/Dokumente/mcTestServer/plugins/PixelPics"; + static final int SSAA = 3; + static final double V_FOV_HALF = Math.toRadians(DefaultScreenRenderer.V_FOV_DEG / 2); + static final SkyContext NOON = new SkyContext(6000, 0, 6000); + + final Logger log; + final ResourcePack pack; + final AssetReader reader; + final TextureCache textures; + final BlockModelRegistry registry; + final CemModelLoader geo = new CemModelLoader(); + final CemBaker baker; + final BlockEntityBaker beBaker; + final DefaultScreenRenderer renderer; + + Harness(String logName) throws IOException { + log = Logger.getLogger(logName); + pack = ResourcePackLoader.load(new File(PACK_ROOT, "resourcepack"), log).orElseThrow(); + reader = new AssetReader(pack); + textures = new TextureCache(pack); + registry = new BlockModelRegistry(reader, textures); + // Template set = entity fallback only; the vanilla dump is the primary model source. + for (String f : new String[] {"cem_template_models.json", "vanilla_entity_models.json"}) { + try (InputStream in = new FileInputStream("src/main/resources/cem/" + f)) { + geo.load(in, log); + } + } + SkinCache skins = new SkinCache(); + baker = new CemBaker(geo, textures, skins); + beBaker = new BlockEntityBaker(geo, textures, skins, FontLoader.load(pack, textures, log)); + renderer = new DefaultScreenRenderer(registry, new BiomeTintProvider(textures), textures, baker, beBaker, log); + } + + /** + * Server-free BlockData stand-in: material + blockstate string are all the model registry needs. (Each proxy + * kind gets its own handler class: a shared generic one made the hot getMaterial/hashCode calls megamorphic and + * EntityBenchmark measurably slower.) + */ + static BlockData block(Material m, String state) { + String s = "minecraft:" + m.getKey().getKey() + (state == null ? "" : "[" + state + "]"); + return (BlockData) Proxy.newProxyInstance(Harness.class.getClassLoader(), + new Class[]{BlockData.class}, (p, meth, a) -> switch (meth.getName()) { + case "getMaterial" -> m; + case "getAsString", "toString" -> s; + case "equals" -> p == a[0]; + case "hashCode" -> System.identityHashCode(p); + default -> defaultValue(meth.getReturnType()); + }); + } + + /** false / 0 / null for an unstubbed proxy method. */ + static Object defaultValue(Class rt) { + if (rt == boolean.class) return false; + return rt.isPrimitive() ? 0 : null; + } + + static WorldSnapshot emptyWorld(int minY, int maxY) { + return new WorldSnapshot(Map.of(), minY, maxY, block(Material.AIR, null)); + } + + /** Render job looking from {@code cam} at {@code look}, {@link #SSAA}x supersampled. */ + static RenderJob job(WorldSnapshot world, Vector cam, Vector look, int w, int h, List mobs, + List bes, List decos, List objects) { + Location loc = new Location(null, cam.getX(), cam.getY(), cam.getZ()); + loc.setDirection(look.clone().subtract(cam)); + List rayMap = DefaultScreenRenderer.buildRayMap(loc, w * SSAA, h * SSAA); + return new RenderJob(world, rayMap, cam, w, h, NOON, mobs, bes, decos, objects); + } + + /** Distance at which a box of extent {@code ext} (at least 0.5) fills the view, scaled {@code mul}, plus {@code add}. */ + static double frameDistance(double[] min, double[] max, double mul, double add) { + double ext = 0.5; + for (int a = 0; a < 3; a++) ext = Math.max(ext, max[a] - min[a]); + return ext / (2 * Math.tan(V_FOV_HALF)) * mul + add; + } + + static Vector center(double[] min, double[] max) { + return new Vector((min[0] + max[0]) / 2, (min[1] + max[1]) / 2, (min[2] + max[2]) / 2); + } + + /** Copy of an entity state with some record components replaced (by name). */ + static EntityState copy(EntityState s, Map overrides) throws ReflectiveOperationException { + var comps = EntityState.class.getRecordComponents(); + Object[] vals = new Object[comps.length]; + Class[] types = new Class[comps.length]; + for (int i = 0; i < comps.length; i++) { + types[i] = comps[i].getType(); + String n = comps[i].getName(); + vals[i] = overrides.containsKey(n) ? overrides.get(n) : comps[i].getAccessor().invoke(s); + } + return EntityState.class.getDeclaredConstructor(types).newInstance(vals); + } + + static ItemSpec item(String id) { + return new ItemSpec(id, null, null, false); + } + + /** The views side by side under a 20 px label. */ + static BufferedImage labelCell(String key, BufferedImage... views) { + int w = 0, labelH = 20; + for (BufferedImage v : views) w += v.getWidth(); + BufferedImage cell = new BufferedImage(w, views[0].getHeight() + labelH, BufferedImage.TYPE_INT_RGB); + Graphics2D g = cell.createGraphics(); + g.setColor(new Color(25, 25, 30)); + g.fillRect(0, 0, cell.getWidth(), cell.getHeight()); + for (int i = 0, x = 0; i < views.length; x += views[i].getWidth(), i++) g.drawImage(views[i], x, labelH, null); + g.setColor(Color.WHITE); + g.setFont(new Font("SansSerif", Font.BOLD, 13)); + g.drawString(key, 4, 15); + g.dispose(); + return cell; + } + + /** Contact sheets of {@code cols} columns, {@code perPage} cells each, named {@code name(page)}. */ + static void writeSheets(List cells, int cols, int perPage, File dir, IntFunction name) + throws IOException { + int cw = cells.get(0).getWidth(), ch = cells.get(0).getHeight(); + for (int page = 0, idx = 0; idx < cells.size(); page++, idx += perPage) { + int count = Math.min(perPage, cells.size() - idx); + BufferedImage sheet = new BufferedImage(cols * cw, (count + cols - 1) / cols * ch, BufferedImage.TYPE_INT_RGB); + Graphics2D g = sheet.createGraphics(); + g.setColor(new Color(40, 40, 48)); + g.fillRect(0, 0, sheet.getWidth(), sheet.getHeight()); + for (int i = 0; i < count; i++) g.drawImage(cells.get(idx + i), (i % cols) * cw, (i / cols) * ch, null); + g.dispose(); + File f = new File(dir, name.apply(page)); + ImageIO.write(sheet, "png", f); + System.out.println("WROTE " + f); + } + } +} diff --git a/tools/ObjectTestRender.java b/tools/ObjectTestRender.java index 88df7ff..c71960d 100644 --- a/tools/ObjectTestRender.java +++ b/tools/ObjectTestRender.java @@ -1,105 +1,50 @@ -import eu.mhsl.minecraft.pixelpics.assets.*; import eu.mhsl.minecraft.pixelpics.render.entity.*; -import eu.mhsl.minecraft.pixelpics.render.render.*; -import eu.mhsl.minecraft.pixelpics.render.sky.SkyContext; -import eu.mhsl.minecraft.pixelpics.render.snapshot.WorldSnapshot; -import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider; -import org.bukkit.Location; import org.bukkit.Material; -import org.bukkit.block.data.BlockData; import org.bukkit.util.Vector; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.File; -import java.lang.reflect.Proxy; import java.util.*; import java.util.List; -import java.util.logging.Logger; /** - * Standalone (no server) render of {@link ObjectState}s — block/item/text displays, dropped items, - * falling blocks — in one scene, to check the ObjectBaker geometry, UV orientation and transforms. - * Output: ~/Dokumente/PixelPics-Entity-Renders/objects.png + * Standalone (no server) render of {@link ObjectState}s (block/item/text displays, dropped items) in one scene, + * to check the ObjectBaker geometry, UV orientation and transforms. Output: {@code -Dout} (default + * ~/Dokumente/PixelPics-Entity-Renders/objects.png). Build: see {@link Harness}. */ public class ObjectTestRender { - static final String ROOT = "/home/elias/Dokumente/mcTestServer/plugins/PixelPics"; - - /** Server-free BlockData stand-in: material + blockstate string are all the model registry needs. */ - static BlockData block(Material m, String state) { - String s = "minecraft:" + m.getKey().getKey() + (state == null ? "" : "[" + state + "]"); - return (BlockData) Proxy.newProxyInstance(ObjectTestRender.class.getClassLoader(), - new Class[]{BlockData.class}, (p, meth, a) -> switch (meth.getName()) { - case "getMaterial" -> m; - case "getAsString" -> s; - case "equals" -> p == a[0]; - case "hashCode" -> System.identityHashCode(p); - case "toString" -> s; - default -> { - Class rt = meth.getReturnType(); - yield rt == boolean.class ? false : (rt.isPrimitive() ? 0 : null); - } - }); + static ObjectState item(Affine a, String id, Material block, String state) { + return ObjectState.item(a, new ItemSpec(id, block == null ? null : Harness.block(block, state), null, false), "none", false); } - static ObjectState obj(ObjectState.Kind k, Affine a, BlockData b, String item, List lines, int bg) { - return switch (k) { - case BLOCK -> ObjectState.block(a, b, null); - case ITEM -> ObjectState.item(a, new ItemSpec(item, b, null, false), "none", false); - case TEXT -> ObjectState.text(a, lines, 200, 0xFFFFFFFF, bg, true); - case LEASH -> ObjectState.leash(new double[6]); - }; + static ObjectState text(Affine a, int background, String... lines) { + return ObjectState.text(a, List.of(lines), 200, 0xFFFFFFFF, background, true); } public static void main(String[] args) throws Exception { - Logger log = Logger.getLogger("test"); - ResourcePack pack = ResourcePackLoader.load(new File(ROOT, "resourcepack"), log).orElseThrow(); - AssetReader reader = new AssetReader(pack); - TextureCache textures = new TextureCache(pack); - BlockModelRegistry registry = new BlockModelRegistry(reader, textures); - BiomeTintProvider tint = new BiomeTintProvider(textures); - var geo = new eu.mhsl.minecraft.pixelpics.render.entity.cem.CemModelLoader(); - var font = eu.mhsl.minecraft.pixelpics.assets.font.FontLoader.load(pack, textures, log); - SkinCache skins = new SkinCache(); - var baker = new eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker(geo, textures, skins); - var beBaker = new eu.mhsl.minecraft.pixelpics.render.entity.cem.BlockEntityBaker(geo, textures, skins, font); - geo.load(new java.io.FileInputStream("src/main/resources/cem/cem_template_models.json"), log); - geo.load(new java.io.FileInputStream("src/main/resources/cem/vanilla_entity_models.json"), log); - DefaultScreenRenderer renderer = new DefaultScreenRenderer(registry, tint, textures, baker, beBaker, log); - + Harness h = new Harness("test"); List objs = new ArrayList<>(); // Block displays: plain grass block (tinted), rotated + scaled stairs. - objs.add(obj(ObjectState.Kind.BLOCK, Affine.translation(0, 0, 0), block(Material.GRASS_BLOCK, "snowy=false"), null, List.of(), 0)); - objs.add(obj(ObjectState.Kind.BLOCK, - Affine.translation(2, 0, 0).mul(Affine.rotY(Math.toRadians(30))).mul(Affine.scale(0.75)), - block(Material.OAK_STAIRS, "facing=north,half=bottom,shape=straight,waterlogged=false"), null, List.of(), 0)); - // Item displays: a flat item and a block item (centred). - objs.add(obj(ObjectState.Kind.ITEM, Affine.translation(0.5, 2.0, 0.5), null, "diamond_sword", List.of(), 0)); - objs.add(obj(ObjectState.Kind.ITEM, Affine.translation(2.5, 2.0, 0.5).mul(Affine.scale(0.6)), - block(Material.OAK_LOG, "axis=y"), "oak_log", List.of(), 0)); - // Text display (facing the camera side), opaque background. - objs.add(obj(ObjectState.Kind.TEXT, - Affine.translation(1.5, 3.0, 0).mul(Affine.rotY(Math.PI)).mul(Affine.scale(0.025)), null, null, - List.of("Hallo PixelPics!", "Zeile zwei äöü"), 0xFF203040)); - // Text seen from the BACK (front faces +Z, camera is on -Z) on the vanilla default 25% black - // background (blended), and a shield item. - objs.add(obj(ObjectState.Kind.TEXT, Affine.translation(-0.5, 3.0, 0).mul(Affine.scale(0.025)), null, null, - List.of("Rueckseite"), 0x40000000)); - objs.add(obj(ObjectState.Kind.ITEM, Affine.translation(4, 1.0, 0.5), null, "shield", List.of(), 0)); + objs.add(ObjectState.block(Affine.translation(0, 0, 0), Harness.block(Material.GRASS_BLOCK, "snowy=false"), null)); + objs.add(ObjectState.block(Affine.translation(2, 0, 0).mul(Affine.rotY(Math.toRadians(30))).mul(Affine.scale(0.75)), + Harness.block(Material.OAK_STAIRS, "facing=north,half=bottom,shape=straight,waterlogged=false"), null)); + // Item displays: a flat item and a block item. + objs.add(item(Affine.translation(0.5, 2.0, 0.5), "diamond_sword", null, null)); + objs.add(item(Affine.translation(2.5, 2.0, 0.5).mul(Affine.scale(0.6)), "oak_log", Material.OAK_LOG, "axis=y")); + // Text facing the camera on an opaque background, and seen from the BACK (front faces +Z, camera is on -Z) + // on the vanilla default 25% black background (blended); a shield item. + objs.add(text(Affine.translation(1.5, 3.0, 0).mul(Affine.rotY(Math.PI)).mul(Affine.scale(0.025)), 0xFF203040, + "Hallo PixelPics!", "Zeile zwei äöü")); + objs.add(text(Affine.translation(-0.5, 3.0, 0).mul(Affine.scale(0.025)), 0x40000000, "Rueckseite")); + objs.add(item(Affine.translation(4, 1.0, 0.5), "shield", null, null)); // Dropped item (flat) on the ground. - objs.add(obj(ObjectState.Kind.ITEM, Affine.translation(-1, 0.3, 0.5).mul(Affine.scale(0.5)), null, "apple", List.of(), 0)); + objs.add(item(Affine.translation(-1, 0.3, 0.5).mul(Affine.scale(0.5)), "apple", null, null)); - BlockData air = block(Material.AIR, null); - WorldSnapshot empty = new WorldSnapshot(Map.of(), 0, 1, air); - SkyContext sky = new SkyContext(6000, 0, 6000); - int W = 480, H = 360; - Vector cam = new Vector(1.5, 3.2, -6.5); - Location loc = new Location(null, cam.getX(), cam.getY(), cam.getZ()); - loc.setDirection(new Vector(1.2, 1.3, 0.5).subtract(cam)); - List rayMap = EntityTestRender.buildRayMap(loc, W * 3, H * 3); - RenderJob job = new RenderJob(empty, rayMap, cam, W, H, sky, List.of(), List.of(), List.of(), objs); - BufferedImage img = renderer.execute(job); + int w = 480, ht = 360; + BufferedImage img = h.renderer.execute(Harness.job(Harness.emptyWorld(0, 1), new Vector(1.5, 3.2, -6.5), + new Vector(1.2, 1.3, 0.5), w, ht, List.of(), List.of(), List.of(), objs)); File out = new File(System.getProperty("out", "/home/elias/Dokumente/PixelPics-Entity-Renders/objects.png")); ImageIO.write(img, "png", out); System.out.println("WROTE " + out); diff --git a/tools/VanillaModelDump.java b/tools/VanillaModelDump.java index 1a6bc47..b2af8f1 100644 --- a/tools/VanillaModelDump.java +++ b/tools/VanillaModelDump.java @@ -1,51 +1,85 @@ import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.renderpearl.api.pipeline.BlendFunction; +import com.mojang.renderpearl.api.pipeline.ColorTargetState; +import com.mojang.renderpearl.api.pipeline.RenderPipeline; +import java.io.File; import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashSet; import java.util.IdentityHashMap; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; import net.minecraft.SharedConstants; import net.minecraft.client.model.Model; import net.minecraft.client.model.geom.EntityModelSet; import net.minecraft.client.model.geom.LayerDefinitions; import net.minecraft.client.model.geom.ModelLayerLocation; +import net.minecraft.client.model.geom.ModelLayers; import net.minecraft.client.model.geom.ModelPart; import net.minecraft.client.model.geom.builders.CubeDefinition; import net.minecraft.client.model.geom.builders.LayerDefinition; +import net.minecraft.client.model.geom.builders.MeshDefinition; import net.minecraft.client.model.geom.builders.PartDefinition; +import net.minecraft.client.model.object.banner.BannerFlagModel; +import net.minecraft.client.model.object.book.BookModel; +import net.minecraft.client.model.object.skull.DragonHeadModel; +import net.minecraft.client.model.object.skull.PiglinHeadModel; +import net.minecraft.client.model.object.skull.SkullModelBase; +import net.minecraft.client.model.object.statue.CopperGolemStatueModel; +import net.minecraft.client.renderer.ShaderDefines; import net.minecraft.client.renderer.entity.EntityRenderer; import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.layers.HumanoidArmorLayer; +import net.minecraft.client.renderer.entity.player.AvatarRenderer; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.rendertype.RenderType; +import net.minecraft.client.renderer.rendertype.RenderTypes; import net.minecraft.core.Direction; import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.Identifier; import net.minecraft.server.Bootstrap; +import net.minecraft.util.Unit; import net.minecraft.world.entity.EntityType; +import net.minecraft.world.phys.Vec3; +import org.joml.Matrix3f; +import org.joml.Matrix4f; +import org.joml.Vector3f; +import org.joml.Vector3fc; /** - * Dumps the vanilla Java entity models straight from an (unobfuscated, 26.x+) Minecraft CLIENT jar into - * the CEM-template JSON layout PixelPics loads ({@code {"models": {name: {"model": {}}}}}; the jem is a - * nested object, not the template set's embedded JSON string, so the plugin parses the file only once). + * Dumps the vanilla Java entity models from an (unobfuscated, 26.x+) Minecraft CLIENT jar into the CEM-template + * JSON layout PixelPics loads ({@code {"models": {name: {"model": {}}}}}, the jem as a nested object). * - *

Unlike the third-party CEM template set (rest geometry only), every entity model is instantiated - * through its real renderer and posed by its real {@code setupAnim} with a default (idle, adult, not - * aggressive) render state — so zombie arms are raised, spider legs splayed, alternate-state parts - * (rolled-up armadillo, crossed/straight illager arms, chests) carry their vanilla visibility, and new - * mobs of the jar's version are included automatically. + *

Every model is instantiated through its real renderer and posed by its real {@code setupAnim} with a default + * (idle, adult, not aggressive) render state. Names: {@code } (main model) plus {@code /} for + * the renderer's other models and render-layer models. Part coordinates are ABSOLUTE in the CEM frame (x negated, + * y up with the ground at 24), every part carries its absolute pivot ({@code "absolute": true}). * - *

Output per entity type key: {@code } (main model) plus {@code /} for the - * renderer's additional models and render-layer models (outer layers, armor sets, saddles, …). - * Part coordinates are ABSOLUTE in the CEM frame (x negated, y up with the ground at 24, z kept); - * every part carries its absolute pivot ({@code "pivot"}), so the jem is flagged {@code "absolute": true}. - * - *

Run (classpath = client jar + its libraries from the version json): - * {@code java -cp tools/VanillaModelDump.java } + *

Run via {@code tools/update-vanilla-models.sh }, or directly: + * {@code java -cp tools/VanillaModelDump.java } */ public class VanillaModelDump { @@ -56,6 +90,7 @@ public class VanillaModelDump { SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); roots = LayerDefinitions.createRoots(); + // Records which layer every baked root came from (models only keep the ModelPart). EntityModelSet set = new EntityModelSet(roots) { @Override public ModelPart bakeLayer(ModelLayerLocation loc) { @@ -64,11 +99,11 @@ public class VanillaModelDump { return p; } }; + // Renderers only need the model set at construction; everything else may stay null. EntityRendererProvider.Context ctx = new EntityRendererProvider.Context(null, null, null, null, null, set, null, null, null, null, null); - - Field provField = Class.forName("net.minecraft.client.renderer.entity.EntityRenderers") - .getDeclaredField("PROVIDERS"); + Class renderers = Class.forName("net.minecraft.client.renderer.entity.EntityRenderers"); + Field provField = renderers.getDeclaredField("PROVIDERS"); provField.setAccessible(true); @SuppressWarnings("unchecked") Map, EntityRendererProvider> providers = @@ -86,128 +121,27 @@ public class VanillaModelDump { failed.add(key + " (renderer: " + t + ")"); continue; } - Object state; - try { - state = r.createRenderState(); - } catch (Throwable t) { - state = null; - } - idleTweaks(state); - Map> found = new LinkedHashMap<>(); - Map textures = new LinkedHashMap<>(); - // The renderer's current model is the main one, whatever field else also holds it (bigModel, …). - if (fieldValue(r, "model") instanceof Model current) found.put("model", current); - collectModels(r, "", found, 0); - Object layers = fieldValue(r, "layers"); - if (layers instanceof List ls) { - for (Object layer : ls) collectLayer(layer, found, textures); - } - JsonObject info = entityInfo(r, state); - addLayerInfo(info, layers, r); - addTransform(info, r); - addPoseVariants(info, found, key, r); - addRotationBindings(info, found, r); - Map, String> types = addRenderInfo(info, layers, found, key, r); - entities.put(key, info); - emit(key, found, textures, state, types, out, failed); + dumpRenderer(key, r, true, out, entities, failed); } // Players & mannequins use the avatar renderers (one per skin model type: wide/slim). - Map avatars = (Map) Class.forName("net.minecraft.client.renderer.entity.EntityRenderers") + Map avatars = (Map) renderers .getMethod("createAvatarRenderers", EntityRendererProvider.Context.class) .invoke(null, ctx); for (Map.Entry a : avatars.entrySet()) { - EntityRenderer r = (EntityRenderer) a.getValue(); String key = String.valueOf(a.getKey()).toLowerCase().equals("slim") ? "player_slim" : "player"; - Map> found = new LinkedHashMap<>(); - Map textures = new LinkedHashMap<>(); - collectModels(r, "", found, 0); - Object layers = fieldValue(r, "layers"); - if (layers instanceof List ls) { - for (Object layer : ls) collectLayer(layer, found, textures); - } - JsonObject info = new JsonObject(); - addLayerInfo(info, layers, r); - addTransform(info, r); - addPoseVariants(info, found, key, r); - addRotationBindings(info, found, r); - Map, String> types = addRenderInfo(info, layers, found, key, r); - entities.put(key, info); - Object avatarState = r.createRenderState(); - idleTweaks(avatarState); - emit(key, found, textures, avatarState, types, out, failed); + dumpRenderer(key, (EntityRenderer) a.getValue(), false, out, entities, failed); } - // Every layer definition in its rest pose as "layer/#" (special item models such as the - // shield, block-entity models, …) — same absolute CEM frame as the entity models. + // Every layer definition in its rest pose as "layer/#" (special item models, block entities). for (Map.Entry le : roots.entrySet()) { String name = "layer/" + le.getKey().model().getPath() + "#" + le.getKey().layer(); try { - ModelPart baked = le.getValue().bakeRoot(); - PartDefinition rootDef = ((net.minecraft.client.model.geom.builders.MeshDefinition) - fieldValue(le.getValue(), "mesh")) - .getRoot(); - Object mat = fieldValue(le.getValue(), "material"); - JsonObject jem = new JsonObject(); - JsonArray ts = new JsonArray(); - ts.add((int) fieldValue(mat, "xTexSize")); - ts.add((int) fieldValue(mat, "yTexSize")); - jem.add("textureSize", ts); - jem.addProperty("absolute", true); - JsonArray parts = new JsonArray(); - parts.add(part("root", rootDef, baked, 0, 0, 0)); - jem.add("models", parts); - JsonObject entry = new JsonObject(); - entry.add("model", jem); // nested object, not a JSON string: parsed once at plugin start - out.putIfAbsent(name, entry); + out.putIfAbsent(name, entry(jem(le.getValue(), List.of(), le.getValue().bakeRoot(), null))); } catch (Throwable t) { failed.add(name + " (layer: " + t + ")"); } } - // Block-entity models posed by their own setupAnim with the static state the vanilla - // BlockEntityRenderer uses ("posed/"): skulls at animation 0 (dragon jaw, piglin ears), banner - // flags at wave phase 0 (item/static pose), the lectern's open book (LecternRenderer.BOOK_STATE). - // Rest-pose-only models (chest, bell, shulker box, pot, conduit) use "layer/" directly. - posedBlockEntity(out, failed, set, net.minecraft.client.model.geom.ModelLayers.DRAGON_SKULL, - m -> new net.minecraft.client.model.object.skull.DragonHeadModel(m), - new net.minecraft.client.model.object.skull.SkullModelBase.State()); - posedBlockEntity(out, failed, set, net.minecraft.client.model.geom.ModelLayers.PIGLIN_HEAD, - m -> new net.minecraft.client.model.object.skull.PiglinHeadModel(m), - new net.minecraft.client.model.object.skull.SkullModelBase.State()); - posedBlockEntity(out, failed, set, net.minecraft.client.model.geom.ModelLayers.STANDING_BANNER_FLAG, - m -> new net.minecraft.client.model.object.banner.BannerFlagModel(m), 0.0F); - posedBlockEntity(out, failed, set, net.minecraft.client.model.geom.ModelLayers.WALL_BANNER_FLAG, - m -> new net.minecraft.client.model.object.banner.BannerFlagModel(m), 0.0F); - try { - Field bookState = Class.forName("net.minecraft.client.renderer.blockentity.LecternRenderer") - .getDeclaredField("BOOK_STATE"); - bookState.setAccessible(true); - posedBlockEntity(out, failed, set, net.minecraft.client.model.geom.ModelLayers.BOOK, - m -> new net.minecraft.client.model.object.book.BookModel(m), bookState.get(null)); - } catch (ReflectiveOperationException e) { - failed.add("posed/book (" + e + ")"); - } - // EnchantTableRenderer: BookModel.State.forAnimation(time 0, flip 0 → pages 0.1/0.9, open) — closed - // (idle, no player within 3 blocks) and fully open (a player nearby). - for (float open : new float[] {0F, 1F}) { - posedBlockEntity(out, failed, set, net.minecraft.client.model.geom.ModelLayers.BOOK, - "posed/enchanting_book_" + (open == 0 ? "closed" : "open") + "#main", - m -> new net.minecraft.client.model.object.book.BookModel(m), - net.minecraft.client.model.object.book.BookModel.State.forAnimation(0F, 0.1F, 0.9F, open)); - } - // CopperGolemStatueBlockRenderer: one baked layer per statue pose, CopperGolemStatueModel.setupAnim - // (root.y = 0, root.zRot = π). - String[][] statues = {{"standing", "COPPER_GOLEM"}, {"running", "COPPER_GOLEM_RUNNING"}, - {"sitting", "COPPER_GOLEM_SITTING"}, {"star", "COPPER_GOLEM_STAR"}}; - for (String[] st : statues) { - try { - ModelLayerLocation loc = (ModelLayerLocation) net.minecraft.client.model.geom.ModelLayers.class - .getField(st[1]).get(null); - posedBlockEntity(out, failed, set, loc, "posed/copper_golem_statue_" + st[0] + "#main", - m -> new net.minecraft.client.model.object.statue.CopperGolemStatueModel(m), - net.minecraft.util.Unit.INSTANCE); - } catch (ReflectiveOperationException e) { - failed.add("posed/copper_golem_statue_" + st[0] + " (" + e + ")"); - } - } + dumpPosedBlockEntities(set, out, failed); + JsonObject root = new JsonObject(); root.addProperty("version", SharedConstants.getCurrentVersion().name()); JsonObject models = new JsonObject(); @@ -228,10 +162,91 @@ public class VanillaModelDump { } /** - * The renderer's layers: {@code layers: {LayerClass: drawnByDefault}} — whether the layer submits anything - * for the default (idle, adult, untamed, unequipped, …) render state, measured by running its real - * {@code submit} against a recording collector — plus {@code humanoidArmor: true} when one of them is a - * HumanoidArmorLayer (the entity wears humanoid armor). + * Models and entity info of one renderer. {@code mob}: the renderer's current {@code model} field is the main + * model whatever else holds it (bigModel, …), plus scale/default textures ({@link #entityInfo}); avatars skip both. + */ + static void dumpRenderer( + String key, + EntityRenderer r, + boolean mob, + Map out, + Map entities, + List failed) { + Object state = freshState(r, List.of()); + Map> found = new LinkedHashMap<>(); + Map textures = new LinkedHashMap<>(); + if (mob && fieldValue(r, "model") instanceof Model current) found.put("model", current); + collectModels(r, "", found, 0); + Object layers = fieldValue(r, "layers"); + if (layers instanceof List ls) { + for (Object layer : ls) collectLayer(layer, found, textures); + } + JsonObject info = mob ? entityInfo(r, state) : new JsonObject(); + addLayerInfo(info, layers, r); + addTransform(info, r); + addPoseVariants(info, found, key, r); + addRotationBindings(info, found, r); + Map, String> types = addRenderInfo(info, layers, found, key, r); + entities.put(key, info); + emit(key, found, textures, state, types, out, failed); + } + + /** + * Block-entity models posed by their own setupAnim with the static state the vanilla BlockEntityRenderer uses + * ({@code posed/}); rest-pose-only models (chest, bell, shulker box, pot, conduit) use {@code layer/…}. + */ + static void dumpPosedBlockEntities(EntityModelSet set, Map out, List failed) { + // Skulls at animation 0 (dragon jaw, piglin ears), banner flags at wave phase 0 (item/static pose). + posed(out, failed, set, ModelLayers.DRAGON_SKULL, null, DragonHeadModel::new, new SkullModelBase.State()); + posed(out, failed, set, ModelLayers.PIGLIN_HEAD, null, PiglinHeadModel::new, new SkullModelBase.State()); + posed(out, failed, set, ModelLayers.STANDING_BANNER_FLAG, null, BannerFlagModel::new, 0.0F); + posed(out, failed, set, ModelLayers.WALL_BANNER_FLAG, null, BannerFlagModel::new, 0.0F); + try { + Field bookState = Class.forName("net.minecraft.client.renderer.blockentity.LecternRenderer") + .getDeclaredField("BOOK_STATE"); + bookState.setAccessible(true); + posed(out, failed, set, ModelLayers.BOOK, null, BookModel::new, bookState.get(null)); + } catch (ReflectiveOperationException e) { + failed.add("posed/book (" + e + ")"); + } + // EnchantTableRenderer: BookModel.State.forAnimation(time 0, flip 0 → pages 0.1/0.9, open), closed (no + // player within 3 blocks) and fully open. + for (float open : new float[] {0F, 1F}) { + posed(out, failed, set, ModelLayers.BOOK, + "posed/enchanting_book_" + (open == 0 ? "closed" : "open") + "#main", + BookModel::new, BookModel.State.forAnimation(0F, 0.1F, 0.9F, open)); + } + // CopperGolemStatueBlockRenderer: one baked layer per statue pose. + Map statues = new LinkedHashMap<>(); + statues.put("standing", ModelLayers.COPPER_GOLEM); + statues.put("running", ModelLayers.COPPER_GOLEM_RUNNING); + statues.put("sitting", ModelLayers.COPPER_GOLEM_SITTING); + statues.put("star", ModelLayers.COPPER_GOLEM_STAR); + statues.forEach((pose, loc) -> posed(out, failed, set, loc, "posed/copper_golem_statue_" + pose + "#main", + CopperGolemStatueModel::new, Unit.INSTANCE)); + } + + /** One block-entity model posed with {@code state}, stored as {@code name} (default {@code posed/}). */ + static void posed( + Map out, + List failed, + EntityModelSet set, + ModelLayerLocation loc, + String name, + Function> factory, + Object state) { + if (name == null) name = "posed/" + loc.model().getPath() + "#" + loc.layer(); + try { + JsonObject jem = dump(factory.apply(set.bakeLayer(loc)), state, name, failed); + if (jem != null) out.put(name, entry(jem)); + } catch (Throwable t) { + failed.add(name + " (" + t + ")"); + } + } + + /** + * {@code layers: {LayerClass: drawnByDefault}}, measured by running each layer's real {@code submit} for the + * default state, plus {@code humanoidArmor: true} when one of them is a HumanoidArmorLayer. */ static void addLayerInfo(JsonObject info, Object layers, EntityRenderer r) { if (!(layers instanceof List ls) || ls.isEmpty()) return; @@ -240,9 +255,7 @@ public class VanillaModelDump { Class lc = layer.getClass(); while (lc.getSimpleName().isEmpty()) lc = lc.getSuperclass(); // anonymous layer -> its base class String name = lc.getSimpleName(); - if (layer instanceof net.minecraft.client.renderer.entity.layers.HumanoidArmorLayer) { - info.addProperty("humanoidArmor", true); - } + if (layer instanceof HumanoidArmorLayer) info.addProperty("humanoidArmor", true); Boolean drawn = layerDrawsByDefault(layer, r); if (drawn == null) { System.out.println("layer " + name + " (" + r.getClass().getSimpleName() + "): submit failed"); @@ -253,41 +266,27 @@ public class VanillaModelDump { info.add("layers", out); } - /** True/false = the layer submits / doesn't submit geometry for the default state; null = submit threw. */ + /** Whether the layer submits anything for the default state; null = no state or submit threw before. */ static Boolean layerDrawsByDefault(Object layer, EntityRenderer r) { - Object state; - try { - state = r.createRenderState(); - } catch (Throwable t) { - return null; - } - idleTweaks(state); + Object state = freshState(r, List.of()); + if (state == null) return null; boolean[] submitted = {false}; - Object proxy = recordingCollector((m, a) -> { + Boolean ok = submit(layer, state, recordingCollector((m, a) -> { if (m.getName().startsWith("submit")) submitted[0] = true; - }); - Method submit = null; - for (Method m : layer.getClass().getMethods()) { - if (m.getName().equals("submit") && m.getParameterCount() == 6 && !m.isBridge()) submit = m; - } - if (submit == null) return null; - try { - submit.invoke(layer, new com.mojang.blaze3d.vertex.PoseStack(), proxy, 0xF000F0, state, 0f, 0f); - } catch (Throwable t) { - return submitted[0] ? Boolean.TRUE : null; - } + })); + if (ok == null || (!ok && !submitted[0])) return null; return submitted[0]; } /** A SubmitNodeCollector proxy that reports every call and otherwise does nothing. */ - static Object recordingCollector(java.util.function.BiConsumer onCall) { + static Object recordingCollector(BiConsumer onCall) { Class collector; try { collector = Class.forName("net.minecraft.client.renderer.SubmitNodeCollector"); } catch (ClassNotFoundException e) { throw new IllegalStateException(e); } - return java.lang.reflect.Proxy.newProxyInstance( + return Proxy.newProxyInstance( VanillaModelDump.class.getClassLoader(), new Class[] {collector}, (px, m, a) -> { if (m.getName().equals("order")) return px; onCall.accept(m, a); @@ -298,26 +297,41 @@ public class VanillaModelDump { }); } + /** + * Runs the real {@code submit} of a renderer (yaw/pitch from the state) or of one of its render layers against + * {@code collector}. Null = no submit method, false = it threw (submissions before the throw still count: some + * layers fail on the null context). + */ + static Boolean submit(Object target, Object state, Object collector) { + boolean layer = !(target instanceof EntityRenderer); + Method submit = null; + for (Method m : target.getClass().getMethods()) + if (m.getName().equals("submit") && m.getParameterCount() == (layer ? 6 : 4) && !m.isBridge()) submit = m; + if (submit == null) return null; + try { + if (layer) submit.invoke(target, new PoseStack(), collector, 0xF000F0, state, 0f, 0f); + else submit.invoke(target, state, new PoseStack(), collector, null); + return true; + } catch (Throwable t) { + return false; + } + } + // ---- render types, emissive layers, state textures ---------------------------------------------------- - /** Render type name -> how its pipeline draws texels (see {@link #renderTypeJson}); written as root "renderTypes". */ + /** Render type name -> pipeline properties ({@link #renderTypeJson}); written as root "renderTypes". */ static final Map RENDER_TYPES = new TreeMap<>(); /** - * Named render-state assignments ({@link #STATE_POSES} format) whose effect is a different body texture - * ({@code getTextureLocation}): {@code stateTextures: {name: texture}}. Tipped arrows (TippableArrowRenderer: - * isTipped = Arrow.getColor() > 0), the wither while invulnerable (WitherBossRenderer: invulnerableTicks > 80, - * or blinking below). + * {@link #STATE_POSES}-style assignments whose effect is a different body texture ({@code stateTextures}): + * TippableArrowRenderer isTipped (Arrow.getColor() > 0), WitherBossRenderer invulnerableTicks > 80. */ static final String[][] TEXTURE_STATES = { {"tipped", "isTipped", "true"}, {"invulnerable", "invulnerableTicks", "100"}, }; - /** - * Assignments that switch an emissive layer on that the default state leaves off: creaking eyes glow while the - * creaking is active (CreakingRenderState.eyesGlowing). - */ + /** Assignments that switch on an emissive layer the default state leaves off (creaking eyes while active). */ static final String[][] LAYER_STATES = { {"eyes_glowing", "eyesGlowing", "true"}, }; @@ -325,10 +339,7 @@ public class VanillaModelDump { /** One recorded {@code submitModel} call: model, render type, vertex colour (ARGB, -1 = white). */ record Submission(Model model, Object renderType, int color) {} - /** - * Runs the renderer's (or, with {@code layer}, only that layer's) real {@code submit} for {@code state} against - * a recording collector and returns its model submissions. - */ + /** The model submissions of the renderer's ({@code layer == null}) or one layer's {@code submit}. */ @SuppressWarnings({"unchecked", "rawtypes"}) static List submissions(EntityRenderer r, Object layer, Object state) { List out = new ArrayList<>(); @@ -338,30 +349,14 @@ public class VanillaModelDump { Object rt = null; for (Object arg : a) { if (arg instanceof Model mm && model == null) model = mm; - else if (arg instanceof net.minecraft.client.renderer.rendertype.RenderType t && rt == null) rt = t; - else if (arg instanceof net.minecraft.resources.Identifier id && rt == null && model != null) + else if (arg instanceof RenderType t && rt == null) rt = t; + else if (arg instanceof Identifier id && rt == null && model != null) rt = ((Model) model).renderType(id); } int color = a.length >= 9 && a[6] instanceof Integer c ? c : -1; if (model != null && rt != null) out.add(new Submission(model, rt, color)); }); - try { - if (layer == null) { - Method submit = null; - for (Method m : r.getClass().getMethods()) - if (m.getName().equals("submit") && m.getParameterCount() == 4 && !m.isBridge()) submit = m; - if (submit != null) submit.invoke(r, state, new com.mojang.blaze3d.vertex.PoseStack(), proxy, null); - } else { - Method submit = null; - for (Method m : layer.getClass().getMethods()) - if (m.getName().equals("submit") && m.getParameterCount() == 6 && !m.isBridge()) submit = m; - if (submit != null) { - submit.invoke(layer, new com.mojang.blaze3d.vertex.PoseStack(), proxy, 0xF000F0, state, 0f, 0f); - } - } - } catch (Throwable ignored) { - // partial submissions still count (some layers fail on the null context) - } + submit(layer == null ? r : layer, state, proxy); return out; } @@ -377,40 +372,35 @@ public class VanillaModelDump { @SuppressWarnings({"unchecked", "rawtypes"}) static String renderTypeName(Model model) { try { - return renderTypeName(((Model) model).renderType( - net.minecraft.resources.Identifier.withDefaultNamespace("textures/entity/dummy.png"))); + return renderTypeName(((Model) model).renderType(Identifier.withDefaultNamespace("textures/entity/dummy.png"))); } catch (Throwable t) { return null; } } /** - * Pipeline properties of a render type: {@code cutout} (shader ALPHA_CUTOUT threshold; absent = none), - * {@code blend} ("translucent" = src-alpha blending, "additive"; absent = opaque write), {@code emissive} - * (EMISSIVE: no lightmap) and {@code shaded} (false with NO_CARDINAL_LIGHTING: no directional face shading). + * Pipeline properties: {@code cutout} (shader ALPHA_CUTOUT threshold), {@code blend} ("translucent" or + * "additive"; absent = opaque), {@code emissive} (no lightmap) and {@code shaded} (false with + * NO_CARDINAL_LIGHTING: no directional face shading). */ static JsonObject renderTypeJson(Object renderType) { JsonObject o = new JsonObject(); - Object setup = fieldValue(renderType, "state"); - com.mojang.renderpearl.api.pipeline.RenderPipeline p = - (com.mojang.renderpearl.api.pipeline.RenderPipeline) fieldValue(setup, "pipeline"); + RenderPipeline p = (RenderPipeline) fieldValue(fieldValue(renderType, "state"), "pipeline"); if (p == null) return o; o.addProperty("pipeline", p.getLocation().getPath()); - net.minecraft.client.renderer.ShaderDefines defs = p.getShaderDefines(); + ShaderDefines defs = p.getShaderDefines(); String cut = defs.values().get("ALPHA_CUTOUT"); if (cut != null) o.addProperty("cutout", Float.parseFloat(cut.replace("f", "").replace("F", ""))); - for (com.mojang.renderpearl.api.pipeline.ColorTargetState c : p.getColorTargetStates()) { + for (ColorTargetState c : p.getColorTargetStates()) { if (c == null || c.blendFunction().isEmpty()) continue; - Object bf = c.blendFunction().get(); - if (bf.equals(com.mojang.renderpearl.api.pipeline.BlendFunction.ADDITIVE)) o.addProperty("blend", "additive"); - else o.addProperty("blend", "translucent"); + o.addProperty("blend", c.blendFunction().get().equals(BlendFunction.ADDITIVE) ? "additive" : "translucent"); } o.addProperty("emissive", defs.flags().contains("EMISSIVE")); o.addProperty("shaded", !defs.flags().contains("NO_CARDINAL_LIGHTING")); return o; } - /** Texture (path without "textures/" and ".png") a render type samples as Sampler0, or null. */ + /** Texture a render type samples as Sampler0, or null. */ @SuppressWarnings("unchecked") static String renderTypeTexture(Object renderType) { Object setup = fieldValue(renderType, "state"); @@ -420,6 +410,7 @@ public class VanillaModelDump { return id == null ? null : texturePath(id); } + /** Texture path without namespace, "textures/" and ".png". */ static String texturePath(Object id) { return id.toString().replaceFirst("^minecraft:", "").replaceFirst("^textures/", "").replaceFirst("\\.png$", ""); } @@ -431,30 +422,25 @@ public class VanillaModelDump { /** * Render info of one renderer: the render type of every model the default state submits (returned, written - * per model as {@code renderType} by {@link #emit}; other models use their own {@code Model.renderType}), - * {@code emissive} layers and {@code stateTextures} ({@link #TEXTURE_STATES}). + * per model by {@link #emit}), {@code emissive} layers and {@code stateTextures} ({@link #TEXTURE_STATES}). * *

{@code emissive: [{model, texture, renderType, alpha[, textures{: texture}][, when][, - * invisible][, animated]}]}: every layer that submits with an EMISSIVE pipeline (EyesLayer = RenderTypes.eyes, - * BreezeEyesLayer, LivingEntityEmissiveLayer: warden glow, copper-golem / creaking eyes), with its texture, the - * vertex alpha of its alpha function, texture variants per render-state enum constant (copper golem - * weathering), the {@link #LAYER_STATES} flag it needs, whether it still draws on an invisible entity - * (EyesLayer: yes) and whether its alpha changes with the entity age (warden pulsating spots, …). + * invisible][, animated]}]}: every layer that submits with an EMISSIVE pipeline (eyes, warden glow, copper-golem / + * creaking eyes) with the vertex alpha of its alpha function, texture variants per render-state enum constant + * (copper weathering), the {@link #LAYER_STATES} flag it needs, whether it still draws on an invisible entity + * and whether its alpha changes with the entity age (warden pulsating spots). */ static Map, String> addRenderInfo( JsonObject info, Object layers, Map> found, String key, EntityRenderer r) { Map, String> types = new IdentityHashMap<>(); Object st0 = freshState(r, List.of()); if (st0 == null) return types; - for (Submission sub : submissions(r, null, st0)) { - types.putIfAbsent(sub.model(), renderTypeName(sub.renderType())); - } - // Equipment (humanoid armor, elytra, saddles, body armor, harnesses) is drawn by the EquipmentLayerRenderer - // with RenderTypes.armorCutoutNoCull, whatever the model's own render type (PlayerModel/PiglinModel: - // translucent). + for (Submission sub : submissions(r, null, st0)) types.putIfAbsent(sub.model(), renderTypeName(sub.renderType())); + // Equipment (armor, elytra, saddles, body armor, harnesses) is drawn by the EquipmentLayerRenderer with + // RenderTypes.armorCutoutNoCull, whatever the model's own render type (PlayerModel/PiglinModel: translucent). if (layers instanceof List ls) { - String armor = renderTypeName((Object) net.minecraft.client.renderer.rendertype.RenderTypes.armorCutoutNoCull( - net.minecraft.resources.Identifier.withDefaultNamespace("textures/entity/dummy.png"))); + String armor = renderTypeName((Object) RenderTypes.armorCutoutNoCull( + Identifier.withDefaultNamespace("textures/entity/dummy.png"))); for (Object layer : ls) { boolean equipment = false; for (Class c = layer.getClass(); c != null && c != Object.class; c = c.getSuperclass()) @@ -470,21 +456,22 @@ public class VanillaModelDump { for (Map.Entry> m : found.entrySet()) names.putIfAbsent(m.getValue(), modelName(key, m.getKey())); JsonArray emissive = new JsonArray(); - java.util.Set seen = new java.util.HashSet<>(); + Set seen = new HashSet<>(); if (layers instanceof List ls) { for (Object layer : ls) { - String when = null; + String[] when = null; List subs = emissiveSubmissions(r, layer, freshState(r, List.of())); if (subs.isEmpty()) { for (String[] a : LAYER_STATES) { if (!assignmentApplies(st0, a)) continue; subs = emissiveSubmissions(r, layer, freshState(r, List.of(a))); if (!subs.isEmpty()) { - when = a[0]; + when = a; break; } } } + List base = when == null ? List.of() : List.of(when); for (Submission sub : subs) { String name = names.get(sub.model()); String tex = renderTypeTexture(sub.renderType()); @@ -494,10 +481,7 @@ public class VanillaModelDump { e.addProperty("texture", tex); e.addProperty("renderType", renderTypeName(sub.renderType())); e.addProperty("alpha", Math.round((sub.color() >>> 24) / 255f * 1000f) / 1000f); - if (when != null) e.addProperty("when", when); - String[] whenA = when == null ? null : java.util.Arrays.stream(LAYER_STATES) - .filter(a -> a[0].equals(e.get("when").getAsString())).findFirst().orElse(null); - List base = whenA == null ? List.of() : List.of(whenA); + if (when != null) e.addProperty("when", when[0]); // Animated alpha (pulsating warden spots, …): a photo has no age, such layers are skipped. boolean animated = false; for (float age : new float[] {10, 25, 40, 63, 90}) { @@ -538,7 +522,6 @@ public class VanillaModelDump { } /** Textures an emissive layer uses per constant of the render state's enum fields (when they differ). */ - @SuppressWarnings({"unchecked", "rawtypes"}) static JsonObject textureVariants(EntityRenderer r, Object layer, Model model, List base, String def) { JsonObject out = new JsonObject(); Object probe = freshState(r, base); @@ -565,14 +548,8 @@ public class VanillaModelDump { static String textureOf(EntityRenderer r, Object state) { if (state == null) return null; try { - Method gt = null; - for (Class c = r.getClass(); c != null && gt == null; c = c.getSuperclass()) { - for (Method dm : c.getDeclaredMethods()) - if (dm.getName().equals("getTextureLocation") && dm.getParameterCount() == 1 && !dm.isBridge()) - gt = dm; - } + Method gt = declaredMethod(r, m -> m.getName().equals("getTextureLocation") && m.getParameterCount() == 1); if (gt == null) return null; - gt.setAccessible(true); Object id = gt.invoke(r, state); return id == null ? null : texturePath(id); } catch (Throwable t) { @@ -580,35 +557,42 @@ public class VanillaModelDump { } } - static void setBoolean(Object o, String field, boolean v) { - try { - Field f = o.getClass().getField(field); - if (f.getType() == boolean.class) f.setBoolean(o, v); - } catch (Throwable ignored) { + /** + * Non-bridge method matching {@code test} in the nearest class (from the object's class up) that declares one; + * of several there the LAST in declaration order (ShulkerRenderer also has a static + * getTextureLocation(DyeColor), which then wins and fails: the dump has no shulker texture). + */ + static Method declaredMethod(Object o, java.util.function.Predicate test) { + for (Class c = o.getClass(); c != null; c = c.getSuperclass()) { + Method found = null; + for (Method m : c.getDeclaredMethods()) if (!m.isBridge() && test.test(m)) found = m; + if (found != null) { + found.setAccessible(true); + return found; + } } + return null; } /** - * The built-in data-driven mob variants ({@code data/minecraft/_variant/.json}, cat/wolf/frog/ - * cow/pig/chicken/zombie_nautilus …) with their client assets: {@code variants: {: {: {asset_id, - * baby_asset_id, assets{wild,tame,angry}, baby_assets{…}, model}}}} (spawn conditions dropped). + * The built-in data-driven mob variants ({@code data/minecraft/_variant/.json}) with their client + * assets: {@code variants: {: {: {asset_id, baby_asset_id, assets, baby_assets, model}}}}. */ - static JsonObject registryVariants() throws java.io.IOException { + static JsonObject registryVariants() throws IOException { JsonObject out = new JsonObject(); - java.net.URL jar = SharedConstants.class.getProtectionDomain().getCodeSource().getLocation(); - java.util.regex.Pattern p = - java.util.regex.Pattern.compile("data/minecraft/([a-z0-9_]+)_variant/([a-z0-9_/.-]+)\\.json"); - try (java.util.zip.ZipFile zf = new java.util.zip.ZipFile(new java.io.File(jar.getPath()))) { - List entries = - zf.stream().sorted(java.util.Comparator.comparing(java.util.zip.ZipEntry::getName)).toList(); - for (java.util.zip.ZipEntry ze : entries) { - java.util.regex.Matcher m = p.matcher(ze.getName()); + String jar = SharedConstants.class.getProtectionDomain().getCodeSource().getLocation().getPath(); + Pattern p = Pattern.compile("data/minecraft/([a-z0-9_]+)_variant/([a-z0-9_/.-]+)\\.json"); + try (ZipFile zf = new ZipFile(new File(jar))) { + List entries = + zf.stream().sorted(Comparator.comparing(ZipEntry::getName)).toList(); + for (ZipEntry ze : entries) { + Matcher m = p.matcher(ze.getName()); if (!m.matches()) continue; String type = m.group(1); if (type.endsWith("_sound") || type.equals("painting")) continue; // not mob textures JsonObject v; - try (java.io.Reader rd = new java.io.InputStreamReader(zf.getInputStream(ze))) { - v = com.google.gson.JsonParser.parseReader(rd).getAsJsonObject(); + try (Reader rd = new InputStreamReader(zf.getInputStream(ze))) { + v = JsonParser.parseReader(rd).getAsJsonObject(); } JsonObject keep = new JsonObject(); for (String k : new String[] {"asset_id", "baby_asset_id", "assets", "baby_assets", "model"}) { @@ -622,52 +606,18 @@ public class VanillaModelDump { return out; } - /** One block-entity model posed with {@code state} (see main), stored as {@code posed/#}. */ - static void posedBlockEntity( - Map out, - List failed, - EntityModelSet set, - ModelLayerLocation loc, - java.util.function.Function> factory, - Object state) { - posedBlockEntity(out, failed, set, loc, "posed/" + loc.model().getPath() + "#" + loc.layer(), factory, state); - } - - /** {@link #posedBlockEntity} under an explicit name (several static states of one layer). */ - static void posedBlockEntity( - Map out, - List failed, - EntityModelSet set, - ModelLayerLocation loc, - String name, - java.util.function.Function> factory, - Object state) { - try { - JsonObject jem = dump(factory.apply(set.bakeLayer(loc)), state, name, failed); - if (jem == null) return; - JsonObject entry = new JsonObject(); - entry.add("model", jem); - out.put(name, entry); - } catch (Throwable t) { - failed.add(name + " (" + t + ")"); - } - } - /** - * Small, name-based tweaks so the default render state shows the typical idle look: illagers stand - * with crossed arms (their peaceful pose) instead of the empty-handed "neutral" pose; worn elytra rest at - * the ElytraAnimationState idle targets (xRot π/12, zRot -π/12; the render state's 0 is a never-ticked - * entity, the wings would hang flat). + * Name-based tweaks so the default render state shows the idle look: size 1 (slimes/magma cubes: a fresh state + * has 0), illagers with crossed arms (their peaceful pose), worn elytra at the ElytraAnimationState idle + * targets (a never-ticked state leaves the wings flat). */ static void idleTweaks(Object state) { if (state == null) return; - for (String sizeField : new String[] {"size"}) { - try { - Field f = state.getClass().getField(sizeField); - if (f.getType() == int.class && f.getInt(state) == 0) f.setInt(state, 1); - if (f.getType() == float.class && f.getFloat(state) == 0) f.setFloat(state, 1); - } catch (Throwable ignored) { - } + try { + Field f = state.getClass().getField("size"); + if (f.getType() == int.class && f.getInt(state) == 0) f.setInt(state, 1); + if (f.getType() == float.class && f.getFloat(state) == 0) f.setFloat(state, 1); + } catch (Throwable ignored) { } try { Field f = state.getClass().getField("armPose"); @@ -683,7 +633,7 @@ public class VanillaModelDump { } } - /** Writes every collected model of one entity type; adult/normal/plain "model" fields become the main model. */ + /** Writes every collected model of one entity type. */ static void emit( String key, Map> found, @@ -693,30 +643,40 @@ public class VanillaModelDump { Map out, List failed) { for (Map.Entry> m : found.entrySet()) { - String f = m.getKey(); - String name = modelName(key, f); + String name = modelName(key, m.getKey()); if (out.containsKey(name)) continue; JsonObject jem = dump(m.getValue(), state, name, failed); - if (jem != null) { - String tex = textures.get(f); - if (tex != null) jem.addProperty("texture", tex); - String rt = renderTypes.get(m.getValue()); - if (rt == null) rt = renderTypeName(m.getValue()); - if (rt != null) jem.addProperty("renderType", rt); - JsonObject entry = new JsonObject(); - entry.add("model", jem); - out.put(name, entry); - } + if (jem == null) continue; + String tex = textures.get(m.getKey()); + if (tex != null) jem.addProperty("texture", tex); + String rt = renderTypes.get(m.getValue()); + if (rt == null) rt = renderTypeName(m.getValue()); + if (rt != null) jem.addProperty("renderType", rt); + out.put(name, entry(jem)); } } - /** Dump name of a collected model field of entity type {@code key} (see {@link #emit}). */ + static boolean isMainField(String field) { + return field.equals("model") || field.equals("adultModel") || field.equals("normalModel"); + } + + static Model mainModel(Map> found) { + return found.containsKey("model") ? found.get("model") : found.get("adultModel"); + } + + /** Render-layer models of the adult or the baby body. */ + static List> layerModels(Map> found, boolean baby) { + List> out = new ArrayList<>(); + for (Map.Entry> lm : found.entrySet()) + if (lm.getKey().contains("Layer.") && lm.getKey().toLowerCase().contains("baby") == baby) + out.add(lm.getValue()); + return out; + } + + /** Dump name of a collected model field of entity type {@code key}. */ static String modelName(String key, String field) { - return switch (field) { - case "model", "adultModel", "normalModel" -> key; - case "babyModel" -> key + "/baby"; - default -> key + "/" + field; - }; + if (isMainField(field)) return key; + return field.equals("babyModel") ? key + "/baby" : key + "/" + field; } /** @@ -725,9 +685,8 @@ public class VanillaModelDump { */ static void collectLayer(Object layer, Map> found, Map textures) { String prefix = layer.getClass().getSimpleName() + "."; - Object type = fieldValue(layer, "layer"); - if (type instanceof Enum en) prefix = "equip." + en.name().toLowerCase() + "."; - // A renderer with several layers of one class (warden: 5 LivingEntityEmissiveLayers): "#2.", … + if (fieldValue(layer, "layer") instanceof Enum en) prefix = "equip." + en.name().toLowerCase() + "."; + // Several layers of one class (warden: 5 LivingEntityEmissiveLayers): "#2.", … String base = prefix; for (int n = 2; !prefix.startsWith("equip.") && hasPrefix(found, prefix); n++) { prefix = base.substring(0, base.length() - 1) + "#" + n + "."; @@ -743,12 +702,9 @@ public class VanillaModelDump { fl.setAccessible(true); Object v = fl.get(Modifier.isStatic(fl.getModifiers()) ? null : layer); if (v == null) continue; - String path = v.toString().replaceFirst("^minecraft:", "") - .replaceFirst("^textures/", "") - .replaceFirst("\\.png$", ""); if (fl.getName().toLowerCase().contains("baby")) { - if (baby == null) baby = path; - } else if (adult == null) adult = path; + if (baby == null) baby = texturePath(v); + } else if (adult == null) adult = texturePath(v); } catch (Throwable ignored) { } } @@ -767,42 +723,65 @@ public class VanillaModelDump { } /** - * Runs the renderer's real {@code submit} (yaw 0, pitch 0) against a recording collector and returns the - * pose matrix (column-major 4x4, Java model units -> entity-local blocks) of its first model submission: - * the renderer's own translate/rotate/scale (end-crystal float, minecart offsets, wither x2, …), so no - * per-entity placement table is needed. - */ - /** - * Stores the renderer transform at yaw=pitch=0 ({@code transform}, column-major 4x4) plus how yaw and - * pitch enter it, measured by re-rendering at yaw 90 / pitch 90: {@code yawSign} (+1/-1, 0 = ignores yaw) - * and {@code pitchAxis} (world-frame axis for +pitch; absent = ignores pitch). + * Renderer transform at yaw=pitch=0 ({@code transform}: the pose of its first model submission, column-major, + * Java model units -> entity-local blocks: end-crystal float, minecart offsets, wither x2, …) plus how yaw and + * pitch enter it, measured at yaw 90 / pitch 90: {@code yawSign} (+1/-1, 0 = ignores yaw) and + * {@code pitchAxis} (world-frame axis for +pitch; absent = ignores pitch). */ static void addTransform(JsonObject info, EntityRenderer r) { - float[] t0 = captureTransform(r, 0, 0); - if (t0 == null) return; - JsonArray ta = new JsonArray(); - for (float v : t0) ta.add(Math.round(v * 100000f) / 100000f); - info.add("transform", ta); - org.joml.Matrix3f inv0 = new org.joml.Matrix4f().set(t0).get3x3(new org.joml.Matrix3f()).invert(); - float[] ty = captureTransform(r, 90, 0); - if (ty != null) { - org.joml.Matrix3f rel = new org.joml.Matrix4f().set(ty).get3x3(new org.joml.Matrix3f()).mul(inv0); - // rel ≈ RotY(±90): m20 (column 0, row 2) = -sin(angle) - float sinA = -rel.m02; - info.addProperty("yawSign", Math.abs(sinA) < 0.5f ? 0 : (sinA > 0 ? 1 : -1)); + Transform t = transform(r, null, false); + if (t != null) t.write(info); + } + + /** {@code yawSign} null = the yaw-90 capture failed. */ + record Transform(float[] matrix, Integer yawSign, float[] pitchAxis) { + int yaw() { + return yawSign == null ? 0 : yawSign; } - float[] tp = captureTransform(r, 0, 90); - if (tp != null) { - org.joml.Matrix3f rel = new org.joml.Matrix4f().set(tp).get3x3(new org.joml.Matrix3f()).mul(inv0); - // axis of a 90° rotation: the antisymmetric part (R - R^T)/2 - float ax = (rel.m12 - rel.m21) / 2, ay = (rel.m20 - rel.m02) / 2, az = (rel.m01 - rel.m10) / 2; - float len = (float) Math.sqrt(ax * ax + ay * ay + az * az); - if (len > 0.5f) info.add("pitchAxis", arr(ax / len, ay / len, az / len)); + + boolean same(Transform o) { + for (int i = 0; i < 16; i++) if (Math.abs(matrix[i] - o.matrix[i]) > 1e-4f) return false; + if (yaw() != o.yaw() || (pitchAxis == null) != (o.pitchAxis == null)) return false; + if (pitchAxis != null) + for (int i = 0; i < 3; i++) if (Math.abs(pitchAxis[i] - o.pitchAxis[i]) > 1e-3f) return false; + return true; + } + + void write(JsonObject o) { + JsonArray ta = new JsonArray(); + for (float v : matrix) ta.add(Math.round(v * 100000f) / 100000f); + o.add("transform", ta); + if (yawSign != null) o.addProperty("yawSign", yawSign); + if (pitchAxis != null) o.add("pitchAxis", arr(pitchAxis)); } } + /** Renderer transform for a state ({@code assigns}, see {@link #captureTransform}); null = none. */ + static Transform transform(EntityRenderer r, List assigns, boolean baby) { + float[] t = captureTransform(r, 0, 0, assigns, baby); + if (t == null) return null; + Matrix3f inv0 = new Matrix4f().set(t).get3x3(new Matrix3f()).invert(); + Integer yaw = null; + float[] ty = captureTransform(r, 90, 0, assigns, baby); + if (ty != null) { + Matrix3f rel = new Matrix4f().set(ty).get3x3(new Matrix3f()).mul(inv0); + float sinA = -rel.m02; // rel ≈ RotY(±90) + yaw = Math.abs(sinA) < 0.5f ? 0 : (sinA > 0 ? 1 : -1); + } + float[] axis = null; + float[] tp = captureTransform(r, 0, 90, assigns, baby); + if (tp != null) { + Matrix3f rel = new Matrix4f().set(tp).get3x3(new Matrix3f()).mul(inv0); + // axis of a 90° rotation: the antisymmetric part (R - R^T)/2 + float ax = (rel.m12 - rel.m21) / 2, ay = (rel.m20 - rel.m02) / 2, az = (rel.m01 - rel.m10) / 2; + float len = (float) Math.sqrt(ax * ax + ay * ay + az * az); + if (len > 0.5f) axis = new float[] {ax / len, ay / len, az / len}; + } + return new Transform(t, yaw, axis); + } + /** - * Named STATE poses (the render-state fields vanilla's extractRenderState fills from the entity's pose/flags): + * Named STATE poses (render-state fields vanilla's extractRenderState fills from the entity's pose/flags): * {@code {pose, field, value[, "unless:"]}}. A pose applies to a renderer when at least one of its * assignments finds its field; the others are skipped (one name covers fox/cat/wolf/panda/parrot/camel * sitting, …). Values: boolean, float, enum constant name, or {@code anim:} = start that @@ -857,131 +836,120 @@ public class VanillaModelDump { static final List> POSE_GROUPS = List.of(List.of("crouching", "pouncing"), List.of("head_up", "lying", "on_player")); + /** Every subset (in bitmask order, elements in list order) with at least {@code minSize} elements. */ + static List> subsets(List items, int minSize) { + List> out = new ArrayList<>(); + for (int mask = 0; mask < (1 << items.size()); mask++) { + if (Integer.bitCount(mask) < minSize) continue; + List sub = new ArrayList<>(); + for (int i = 0; i < items.size(); i++) if ((mask & (1 << i)) != 0) sub.add(items.get(i)); + out.add(sub); + } + return out; + } + + static List flatten(List> groups) { + List out = new ArrayList<>(); + for (List g : groups) out.addAll(g); + return out; + } + /** * State-dependent poses as per-part overrides (only parts that differ from the idle pose): sitting as a * passenger ({@code isPassenger}/{@code isRiding}), holding an item in the right/left hand - * ({@code right/leftArmPose = ITEM}) and their combinations, illager arm poses, and the {@link #STATE_POSES} - * (alone, in {@link #POSE_GROUPS} combinations and with held items). Stored per entity type as + * ({@code right/leftArmPose = ITEM}), armor-stand arms / chests and their combinations, mob arm poses, and the + * {@link #STATE_POSES} (alone, in {@link #POSE_GROUPS} combinations and with held items). Stored as * {@code poses: {variant: {part: {pivot, rotate, visible[, scale]}}}} for the main body model and * {@code modelPoses: {: {poses, poseTransforms}}} for the other body models (baby, small, …: own - * pivots); the baker applies them by part name to the body AND its layer models (armor follows the arms, - * like vanilla's copyPropertiesTo). State poses also carry the parts of layer models the body lacks (elytra - * wings) and, when the renderer transform changes (sleeping, swimming, fish in water, …), - * {@code poseTransforms: {variant: {transform, yawSign[, pitchAxis]}}} measured like {@link #addTransform}. + * pivots); the baker applies them by part name to the body AND its layer models (like vanilla's + * copyPropertiesTo). State poses also carry the parts of layer models the body lacks (elytra wings) and, when + * the renderer transform changes (sleeping, swimming, fish in water, …), {@code poseTransforms}. */ - static void addPoseVariants( - JsonObject info, Map> found, String key, EntityRenderer r) { + static void addPoseVariants(JsonObject info, Map> found, String key, EntityRenderer r) { Object probe; try { probe = r.createRenderState(); } catch (Throwable t) { return; } - // Toggle groups: {name, field, value} entries sharing a name are applied together. + // Toggle groups: {name, field, value} entries of one group are applied together. List> toggles = new ArrayList<>(); List passenger = new ArrayList<>(); for (String f : new String[] {"isPassenger", "isRiding"}) if (hasField(probe, f)) passenger.add(new String[] {"passenger", f, "true"}); if (!passenger.isEmpty()) toggles.add(passenger); // HumanoidModel.ArmPose.ITEM (arm raised by a held item) comes only from AvatarRenderer.getArmPose - // (players, mannequins): HumanoidMobRenderer (zombies, skeletons, piglins, …) extracts EMPTY (SPEAR, - // aggressive bow/trident aside), other renderers never set it. - boolean avatar = r instanceof net.minecraft.client.renderer.entity.player.AvatarRenderer; + // (players, mannequins): HumanoidMobRenderer extracts EMPTY (spear, aggressive bow/trident aside). List> items = new ArrayList<>(); - if (avatar && hasField(probe, "rightArmPose") && probeEnum(probe, "rightArmPose")) - items.add(java.util.Collections.singletonList(new String[] {"item_r", "rightArmPose", "ITEM"})); - if (avatar && hasField(probe, "leftArmPose") && probeEnum(probe, "leftArmPose")) - items.add(java.util.Collections.singletonList(new String[] {"item_l", "leftArmPose", "ITEM"})); + if (r instanceof AvatarRenderer) { + for (String[] hand : new String[][] {{"item_r", "rightArmPose"}, {"item_l", "leftArmPose"}}) + if (isEnumField(probe, hand[1])) items.add(List.of(new String[] {hand[0], hand[1], "ITEM"})); + } toggles.addAll(items); // State flags that switch model parts on in setupAnim: armor-stand arms, donkey/mule/llama chests. for (String[] f : new String[][] {{"arms", "showArms"}, {"chest", "hasChest"}}) { - if (hasField(probe, f[1])) - toggles.add(java.util.Collections.singletonList(new String[] {f[0], f[1], "true"})); + if (hasField(probe, f[1])) toggles.add(List.of(new String[] {f[0], f[1], "true"})); } List> combos = new ArrayList<>(); - for (int mask = 1; mask < (1 << toggles.size()); mask++) { - List c = new ArrayList<>(); - for (int i = 0; i < toggles.size(); i++) if ((mask & (1 << i)) != 0) c.addAll(toggles.get(i)); - combos.add(c); - } + for (List> sub : subsets(toggles, 1)) combos.add(flatten(sub)); // Mob arm-pose enums (illagers: IllagerArmPose, idle CROSSED; piglins: PiglinArmPose, idle DEFAULT): every // other constant (crossbow hold, attacking, admiring, …) as "arms_". - if (hasField(probe, "armPose") && probeEnum(probe, "armPose")) { + if (isEnumField(probe, "armPose")) { Object idle = fieldValue(freshState(r, List.of()), "armPose"); - List armPoses = new ArrayList<>(); - Object[] constants; - try { - constants = probe.getClass().getField("armPose").getType().getEnumConstants(); - } catch (NoSuchFieldException e) { - constants = new Object[0]; - } - for (Object c : constants) if (c != idle) armPoses.add(((Enum) c).name()); - for (String v : armPoses) { - List c = new ArrayList<>(); - c.add(new String[] {"arms_" + v.toLowerCase(), "armPose", v}); - combos.add(c); + for (Object c : fieldType(probe, "armPose").getEnumConstants()) { + if (c == idle) continue; + String v = ((Enum) c).name(); + List combo = new ArrayList<>(); + combo.add(new String[] {"arms_" + v.toLowerCase(), "armPose", v}); + combos.add(combo); if (!passenger.isEmpty()) { - List cr = new ArrayList<>(c); - cr.addAll(passenger); - combos.add(cr); + List riding = new ArrayList<>(combo); + riding.addAll(passenger); + combos.add(riding); } } } // State poses: alone, grouped, each with every held-item subset. - List> stateCombos = new ArrayList<>(); - java.util.Set applicable = new java.util.LinkedHashSet<>(); + Set applicable = new LinkedHashSet<>(); for (String[] a : STATE_POSES) if (assignmentApplies(probe, a)) applicable.add(a[0]); List> stateNames = new ArrayList<>(); for (String n : applicable) stateNames.add(List.of(n)); - for (List g : POSE_GROUPS) { - List members = g.stream().filter(applicable::contains).toList(); - for (int mask = 1; mask < (1 << members.size()); mask++) { - if (Integer.bitCount(mask) < 2) continue; - List sub = new ArrayList<>(); - for (int i = 0; i < members.size(); i++) if ((mask & (1 << i)) != 0) sub.add(members.get(i)); - stateNames.add(sub); - } - } + for (List g : POSE_GROUPS) + stateNames.addAll(subsets(g.stream().filter(applicable::contains).toList(), 2)); + List> stateCombos = new ArrayList<>(); for (List names : stateNames) { List base = new ArrayList<>(); for (String n : names) for (String[] a : STATE_POSES) if (a[0].equals(n)) base.add(a); - for (int mask = 0; mask < (1 << items.size()); mask++) { + for (List> held : subsets(items, 0)) { List c = new ArrayList<>(base); - for (int i = 0; i < items.size(); i++) if ((mask & (1 << i)) != 0) c.addAll(items.get(i)); + c.addAll(flatten(held)); stateCombos.add(c); } } if (combos.isEmpty() && stateCombos.isEmpty()) return; // Every body model (main, baby, small, variant models) gets its own overrides: pivots differ per model. - float[][] t0 = transformTriple(r, List.of(), false); + Transform t0 = transform(r, List.of(), false); for (Map.Entry> bm : found.entrySet()) { String f = bm.getKey(); if (f.contains("Layer.") || f.startsWith("equip.")) continue; - boolean main = f.equals("model") || f.equals("adultModel") || f.equals("normalModel"); - if (main && bm.getValue() != (found.containsKey("model") ? found.get("model") : found.get("adultModel"))) - continue; + boolean main = isMainField(f); + if (main && bm.getValue() != mainModel(found)) continue; boolean baby = f.toLowerCase().contains("baby"); - List> layerModels = new ArrayList<>(); - for (Map.Entry> lm : found.entrySet()) { - String ln = lm.getKey(); - if (!ln.contains("Layer.") || ln.toLowerCase().contains("baby") != baby) continue; - layerModels.add(lm.getValue()); - } + List> layerModels = layerModels(found, baby); JsonObject poses = new JsonObject(); JsonObject transforms = new JsonObject(); Model model = bm.getValue(); - // Main model: the dumped idle state (unchanged poses). Baby models: a baby's state (isBaby, ageScale - // 0.5 — e.g. the wolf's sitting offsets scale with it). + // Baby models are posed with a baby's state (isBaby, ageScale 0.5: e.g. wolf sitting offsets scale). boolean babyState = baby && !main; Map base = poseOf(model, freshState(r, List.of(), babyState)); if (base == null) continue; for (List c : combos) { if (!c.stream().allMatch(a -> assignmentApplies(probe, a))) continue; // e.g. piglin arm poses - addPose(poses, c, model, base, poseOf(model, freshState(r, c, babyState)), false); + addPose(poses, c, base, poseOf(model, freshState(r, c, babyState)), false); } Map baseAll = withLayers(base, layerModels, freshState(r, List.of(), babyState)); - java.util.Set emitted = new java.util.HashSet<>(); + Set emitted = new HashSet<>(); for (List c : stateCombos) { // Held-item variants only for state poses that exist on their own (no item_r+standing duplicates). String states = c.stream().filter(a -> !a[0].startsWith("item_")).map(a -> a[0]).distinct().sorted() @@ -990,11 +958,16 @@ public class VanillaModelDump { if (withItems && !emitted.contains(states)) continue; Map v = withLayers( poseOf(model, freshState(r, c, babyState)), layerModels, freshState(r, c, babyState)); - float[][] tr = transformTriple(r, c, baby); - boolean newTransform = tr != null && t0 != null && !sameTriple(tr, t0); - String name = addPose(poses, c, model, baseAll, v, newTransform); - if (name != null) emitted.add(name); - if (name != null && newTransform) transforms.add(name, tripleJson(tr)); + Transform tr = transform(r, c, baby); + boolean newTransform = tr != null && t0 != null && !tr.same(t0); + String name = addPose(poses, c, baseAll, v, newTransform); + if (name == null) continue; + emitted.add(name); + if (newTransform) { + JsonObject o = new JsonObject(); + new Transform(tr.matrix(), tr.yaw(), tr.pitchAxis()).write(o); + transforms.add(name, o); + } } poseOf(model, freshState(r, List.of())); // leave the model in its idle pose for the dump if (main) { @@ -1002,12 +975,11 @@ public class VanillaModelDump { if (!transforms.isEmpty()) info.add("poseTransforms", transforms); } else { // Always present (also when empty): the baker then never falls back to the main model's pivots. - String sub = f.equals("babyModel") ? "baby" : f; if (!info.has("modelPoses")) info.add("modelPoses", new JsonObject()); JsonObject mp = new JsonObject(); mp.add("poses", poses); if (!transforms.isEmpty()) mp.add("poseTransforms", transforms); - info.getAsJsonObject("modelPoses").add(sub, mp); + info.getAsJsonObject("modelPoses").add(f.equals("babyModel") ? "baby" : f, mp); } } for (Model lm : found.values()) poseOf(lm, freshState(r, List.of())); @@ -1018,12 +990,7 @@ public class VanillaModelDump { * nothing differs (and {@code keepEmpty} is false — state poses that only move the renderer transform). */ static String addPose( - JsonObject poses, - List c, - Model model, - Map base, - Map v, - boolean keepEmpty) { + JsonObject poses, List c, Map base, Map v, boolean keepEmpty) { if (v == null) return null; JsonObject diff = new JsonObject(); for (Map.Entry e : v.entrySet()) { @@ -1036,22 +1003,21 @@ public class VanillaModelDump { boolean moved = b == null; for (int i = 0; !moved && i < 6; i++) moved = Math.abs(b[i] - x[i]) >= 1e-4f; if (moved) { - // Absolute pivot + rotation; omitted for pure visibility toggles (arms, chests), which then - // apply to every model of the type (small/baby models have other pivots). + // Omitted for pure visibility toggles (arms, chests), which then apply to every model of the type + // (small/baby models have other pivots). po.add("pivot", arr(x[0], x[1], x[2])); po.add("rotate", arr(x[3], x[4], x[5])); } po.addProperty("visible", x[6] > 0.5f); - boolean scaled = b == null ? (x[7] != 1 || x[8] != 1 || x[9] != 1) : false; + boolean scaled = b == null && (x[7] != 1 || x[8] != 1 || x[9] != 1); for (int i = 7; b != null && i < 10; i++) scaled |= Math.abs(b[i] - x[i]) >= 1e-4f; if (scaled) po.add("scale", arr(x[7], x[8], x[9])); diff.add(e.getKey(), po); } if (diff.isEmpty() && !keepEmpty) return null; - StringBuilder name = new StringBuilder(); - c.stream().map(t -> t[0]).distinct().sorted().forEach(n -> name.append(name.isEmpty() ? "" : "+").append(n)); - poses.add(name.toString(), diff); - return name.toString(); + String name = String.join("+", c.stream().map(t -> t[0]).distinct().sorted().toList()); + poses.add(name, diff); + return name; } /** {@code parts} plus the posed parts of layer models that the body model lacks (elytra wings, …). */ @@ -1067,55 +1033,13 @@ public class VanillaModelDump { return out; } - /** Renderer transform, yaw sign and pitch axis for a posed state (see {@link #addTransform}); null = none. */ - static float[][] transformTriple(EntityRenderer r, List assigns, boolean baby) { - float[] t = captureTransform(r, 0, 0, assigns, baby); - if (t == null) return null; - org.joml.Matrix3f inv0 = new org.joml.Matrix4f().set(t).get3x3(new org.joml.Matrix3f()).invert(); - float[] yaw = {0}; - float[] ty = captureTransform(r, 90, 0, assigns, baby); - if (ty != null) { - org.joml.Matrix3f rel = new org.joml.Matrix4f().set(ty).get3x3(new org.joml.Matrix3f()).mul(inv0); - float sinA = -rel.m02; - yaw[0] = Math.abs(sinA) < 0.5f ? 0 : (sinA > 0 ? 1 : -1); - } - float[] axis = null; - float[] tp = captureTransform(r, 0, 90, assigns, baby); - if (tp != null) { - org.joml.Matrix3f rel = new org.joml.Matrix4f().set(tp).get3x3(new org.joml.Matrix3f()).mul(inv0); - float ax = (rel.m12 - rel.m21) / 2, ay = (rel.m20 - rel.m02) / 2, az = (rel.m01 - rel.m10) / 2; - float len = (float) Math.sqrt(ax * ax + ay * ay + az * az); - if (len > 0.5f) axis = new float[] {ax / len, ay / len, az / len}; - } - return new float[][] {t, yaw, axis}; - } - - static boolean sameTriple(float[][] a, float[][] b) { - for (int i = 0; i < 16; i++) if (Math.abs(a[0][i] - b[0][i]) > 1e-4f) return false; - if (a[1][0] != b[1][0]) return false; - if ((a[2] == null) != (b[2] == null)) return false; - if (a[2] != null) for (int i = 0; i < 3; i++) if (Math.abs(a[2][i] - b[2][i]) > 1e-3f) return false; - return true; - } - - static JsonObject tripleJson(float[][] tr) { - JsonObject o = new JsonObject(); - JsonArray ta = new JsonArray(); - for (float v : tr[0]) ta.add(Math.round(v * 100000f) / 100000f); - o.add("transform", ta); - o.addProperty("yawSign", (int) tr[1][0]); - if (tr[2] != null) o.add("pitchAxis", arr(tr[2][0], tr[2][1], tr[2][2])); - return o; - } - /** * Continuous state inputs bound linearly to part rotations: every {@code Rotations} field (armor-stand - * head/body/arm/leg poses) and {@code xRot} (entity pitch: dolphin body, heads, …), measured by - * re-running setupAnim with each component +10°. Stored as {@code rotationBindings: {field: {default: - * [x,y,z], parts: {part: [9 coefficients, row = CEM rotate axis, column = field component]}}}}; the baker - * adds {@code coeff · (value − default)} to each named part's rotation (body and layer models alike). + * head/body/arm/leg poses) and {@code xRot} (entity pitch: dolphin body, …), measured by re-running setupAnim + * with each component +10°. Stored as {@code rotationBindings: {field: {default: [x,y,z], parts: {part: + * [9 coefficients, row = CEM rotate axis, column = field component]}}}}; the baker adds + * {@code coeff · (value − default)} to each named part's rotation (body and layer models alike). */ - @SuppressWarnings({"unchecked", "rawtypes"}) static void addRotationBindings(JsonObject info, Map> found, EntityRenderer r) { Object probe; try { @@ -1123,11 +1047,9 @@ public class VanillaModelDump { } catch (Throwable t) { return; } - Model main = found.containsKey("model") ? found.get("model") : found.get("adultModel"); + Model main = mainModel(found); if (main == null) return; - List> layers = new ArrayList<>(); - for (Map.Entry> lm : found.entrySet()) - if (lm.getKey().contains("Layer.") && !lm.getKey().toLowerCase().contains("baby")) layers.add(lm.getValue()); + List> layers = layerModels(found, false); JsonObject out = new JsonObject(); for (Field f : probe.getClass().getFields()) { if (Modifier.isStatic(f.getModifiers())) continue; @@ -1169,7 +1091,7 @@ public class VanillaModelDump { // Pitch tilts whole bodies only (dolphin, strider, nautilus): head look is left out, PixelPics has // no head yaw to go with it. if (!rot) { - java.util.Set heads = new java.util.HashSet<>(); + Set heads = new HashSet<>(); headParts(main.root(), "root", false, heads); for (Model lm : layers) headParts(lm.root(), "root", false, heads); coeff.keySet().removeAll(heads); @@ -1191,55 +1113,55 @@ public class VanillaModelDump { } /** Names of every part named "*head*" and of everything below one. */ - @SuppressWarnings("unchecked") - static void headParts(ModelPart p, String name, boolean inHead, java.util.Set out) { + static void headParts(ModelPart p, String name, boolean inHead, Set out) { inHead |= name.contains("head"); if (inHead) out.add(name); - Map children = (Map) fieldValue(p, "children"); - if (children == null) return; - for (Map.Entry c : children.entrySet()) headParts(c.getValue(), c.getKey(), inHead, out); + for (Map.Entry c : children(p).entrySet()) headParts(c.getValue(), c.getKey(), inHead, out); + } + + @SuppressWarnings("unchecked") + static Map children(ModelPart p) { + Map c = (Map) fieldValue(p, "children"); + return c == null ? Map.of() : c; } /** Whether a {@link #STATE_POSES} assignment finds its field (and an enum value its constant). */ static boolean assignmentApplies(Object st, String[] a) { if (a.length > 3 && a[3].startsWith("unless:") && hasField(st, a[3].substring(7))) return false; - Field f; - try { - f = st.getClass().getField(a[1]); - } catch (NoSuchFieldException e) { + Class type = fieldType(st, a[1]); + if (type == null) return false; + if (a[2].startsWith("anim:")) return type.getSimpleName().equals("AnimationState"); + if (type.isEnum()) { + for (Object c : type.getEnumConstants()) if (((Enum) c).name().equals(a[2])) return true; return false; } - if (a[2].startsWith("anim:")) return f.getType().getSimpleName().equals("AnimationState"); - if (f.getType().isEnum()) { - for (Object c : f.getType().getEnumConstants()) if (((Enum) c).name().equals(a[2])) return true; - return false; - } - return f.getType() == boolean.class || f.getType() == float.class || f.getType() == int.class; + return type == boolean.class || type == float.class || type == int.class; } - /** Whether the state field is an enum (HumanoidModel.ArmPose), not e.g. an armor stand's Rotations. */ - static boolean probeEnum(Object o, String f) { + /** Type of a public field, or null. */ + static Class fieldType(Object o, String f) { try { - return o.getClass().getField(f).getType().isEnum(); + return o.getClass().getField(f).getType(); } catch (NoSuchFieldException e) { - return false; + return null; } } static boolean hasField(Object o, String f) { - try { - o.getClass().getField(f); - return true; - } catch (NoSuchFieldException e) { - return false; - } + return fieldType(o, f) != null; + } + + /** An enum field (HumanoidModel.ArmPose), not e.g. an armor stand's Rotations. */ + static boolean isEnumField(Object o, String f) { + Class type = fieldType(o, f); + return type != null && type.isEnum(); } static Object freshState(EntityRenderer r, List sets) { return freshState(r, sets, false); } - /** {@code baby}: also isBaby + ageScale 0.5 like a baby's extracted state (baby models' pose offsets). */ + /** A fresh idle render state with {@code sets} applied; {@code baby}: also isBaby + ageScale 0.5. */ static Object freshState(EntityRenderer r, List sets, boolean baby) { try { Object st = r.createRenderState(); @@ -1270,7 +1192,7 @@ public class VanillaModelDump { else if (f.getType().isEnum()) f.set(st, Enum.valueOf((Class) f.getType(), t[2])); } - /** setupAnim with the state, then every part's absolute CEM pivot, CEM rotation and visibility, by name. */ + /** setupAnim with the state, then every part's absolute CEM pivot, rotation, visibility and scale, by name. */ @SuppressWarnings({"unchecked", "rawtypes"}) static Map poseOf(Model model, Object state) { if (state == null) return null; @@ -1284,7 +1206,6 @@ public class VanillaModelDump { return out; } - @SuppressWarnings("unchecked") static void collectPose(ModelPart p, String name, float px, float py, float pz, Map out) { float ax = px + p.x, ay = py + p.y, az = pz + p.z; out.put(name, new float[] { @@ -1292,8 +1213,7 @@ public class VanillaModelDump { (float) Math.toDegrees(-p.xRot), (float) Math.toDegrees(-p.yRot), (float) Math.toDegrees(p.zRot), p.visible ? 1 : 0, p.xScale, p.yScale, p.zScale }); - Map children = (Map) fieldValue(p, "children"); - if (children != null) children.forEach((n, c) -> collectPose(c, n, ax, ay, az, out)); + children(p).forEach((n, c) -> collectPose(c, n, ax, ay, az, out)); } static void setFloat(Object o, String field, float v) { @@ -1304,17 +1224,21 @@ public class VanillaModelDump { } } - static float[] captureTransform(EntityRenderer r, float yaw, float pitch) { - return captureTransform(r, yaw, pitch, null, false); + static void setBoolean(Object o, String field, boolean v) { + try { + Field f = o.getClass().getField(field); + if (f.getType() == boolean.class) f.setBoolean(o, v); + } catch (Throwable ignored) { + } } /** - * {@code assigns != null}: a posed state ({@link #STATE_POSES}, {@code isBaby} when {@code baby}); the - * renderer's {@code getRenderOffset} (crouching players sink 2 px) is then included like the dispatcher does. + * Pose matrix (column-major) of the renderer's first model submission at the given yaw/pitch. + * {@code assigns != null}: a posed state ({@link #STATE_POSES}, isBaby when {@code baby}), then also the + * renderer's {@code getRenderOffset} (crouching players sink 2 px) like the EntityRenderDispatcher adds it. */ @SuppressWarnings({"unchecked", "rawtypes"}) - static float[] captureTransform( - EntityRenderer r, float yaw, float pitch, List assigns, boolean baby) { + static float[] captureTransform(EntityRenderer r, float yaw, float pitch, List assigns, boolean baby) { Object state; try { state = r.createRenderState(); @@ -1330,43 +1254,20 @@ public class VanillaModelDump { setFloat(state, "yRot", yaw); setFloat(state, "xRot", pitch); float[][] first = new float[1][]; - Class collector; - try { - collector = Class.forName("net.minecraft.client.renderer.SubmitNodeCollector"); - } catch (ClassNotFoundException e) { - return null; - } - Object proxy = java.lang.reflect.Proxy.newProxyInstance( - VanillaModelDump.class.getClassLoader(), new Class[] {collector}, (px, m, a) -> { - if (m.getName().equals("order")) return px; - if (a != null && first[0] == null && m.getName().startsWith("submitModel")) { - for (Object arg : a) { - if (arg instanceof com.mojang.blaze3d.vertex.PoseStack ps) { - first[0] = ps.last().pose().get(new float[16]); - break; - } - } - } - Class rt = m.getReturnType(); - if (rt == boolean.class) return false; - if (rt.isPrimitive() && rt != void.class) return 0; - return null; - }); - try { - Method submit = null; - for (Method m : r.getClass().getMethods()) { - if (m.getName().equals("submit") && m.getParameterCount() == 4 && !m.isBridge()) submit = m; + submit(r, state, recordingCollector((m, a) -> { + if (a == null || first[0] != null || !m.getName().startsWith("submitModel")) return; + for (Object arg : a) { + if (arg instanceof PoseStack ps) { + first[0] = ps.last().pose().get(new float[16]); + break; + } } - if (submit == null) return null; - submit.invoke(r, state, new com.mojang.blaze3d.vertex.PoseStack(), proxy, null); - } catch (Throwable ignored) { - // partial submissions still count (layers may fail on the null context) - } + })); if (assigns != null && first[0] != null) { try { - net.minecraft.world.phys.Vec3 off = ((EntityRenderer) r).getRenderOffset((net.minecraft.client.renderer.entity.state.EntityRenderState) state); + Vec3 off = ((EntityRenderer) r).getRenderOffset((EntityRenderState) state); if (off != null && off.lengthSqr() > 0) { - first[0] = new org.joml.Matrix4f().set(first[0]) + first[0] = new Matrix4f().set(first[0]) .translateLocal((float) off.x, (float) off.y, (float) off.z) .get(new float[16]); } @@ -1382,52 +1283,28 @@ public class VanillaModelDump { JsonObject o = new JsonObject(); if (state == null) return o; try { - Method m = null; - for (Class c = r.getClass(); c != null && m == null; c = c.getSuperclass()) { - for (Method dm : c.getDeclaredMethods()) { - if (dm.getName().equals("scale") - && dm.getParameterCount() == 2 - && dm.getParameterTypes()[1].getSimpleName().equals("PoseStack") - && !dm.isBridge()) { - m = dm; - break; - } - } - } + Method m = declaredMethod(r, dm -> dm.getName().equals("scale") + && dm.getParameterCount() == 2 + && dm.getParameterTypes()[1].getSimpleName().equals("PoseStack")); if (m != null) { - m.setAccessible(true); - com.mojang.blaze3d.vertex.PoseStack ps = new com.mojang.blaze3d.vertex.PoseStack(); + PoseStack ps = new PoseStack(); m.invoke(r, state, ps); - float sc = ps.last().pose().getScale(new org.joml.Vector3f()).y; + float sc = ps.last().pose().getScale(new Vector3f()).y; if (sc > 0.05f && Math.abs(sc - 1) > 1e-3) o.addProperty("scale", Math.round(sc * 1000f) / 1000f); } } catch (Throwable ignored) { } for (boolean baby : new boolean[] {false, true}) { + Object st; try { - Object st = r.createRenderState(); - try { - st.getClass().getField("isBaby").setBoolean(st, baby); - } catch (NoSuchFieldException e) { - if (baby) continue; - } - Method gt = null; - for (Class c = r.getClass(); c != null && gt == null; c = c.getSuperclass()) { - for (Method dm : c.getDeclaredMethods()) - if (dm.getName().equals("getTextureLocation") && dm.getParameterCount() == 1 && !dm.isBridge()) - gt = dm; - } - if (gt == null) break; - gt.setAccessible(true); - Object id = gt.invoke(r, st); - if (id != null) { - String path = id.toString().replaceFirst("^minecraft:", "") - .replaceFirst("^textures/", "") - .replaceFirst("\\.png$", ""); - o.addProperty(baby ? "babyTexture" : "texture", path); - } - } catch (Throwable ignored) { + st = r.createRenderState(); + } catch (Throwable t) { + continue; } + if (baby && !hasField(st, "isBaby")) continue; + setBoolean(st, "isBaby", baby); + String tex = textureOf(r, st); + if (tex != null) o.addProperty(baby ? "babyTexture" : "texture", tex); } return o; } @@ -1466,12 +1343,9 @@ public class VanillaModelDump { } /** Path of child names from {@code from} down to the identical part {@code target}, or null. */ - @SuppressWarnings("unchecked") static List findPath(ModelPart from, ModelPart target, List path) { if (from == target) return path; - Map children = (Map) fieldValue(from, "children"); - if (children == null) return null; - for (Map.Entry c : children.entrySet()) { + for (Map.Entry c : children(from).entrySet()) { List p = new ArrayList<>(path); p.add(c.getKey()); List r = findPath(c.getValue(), target, p); @@ -1480,6 +1354,7 @@ public class VanillaModelDump { return null; } + /** A (private) field's value, searched from the object's class up; null when missing or unreadable. */ static Object fieldValue(Object o, String name) { for (Class c = o.getClass(); c != null; c = c.getSuperclass()) { try { @@ -1494,6 +1369,13 @@ public class VanillaModelDump { return null; } + static JsonObject entry(JsonObject jem) { + JsonObject entry = new JsonObject(); + entry.add("model", jem); + return entry; + } + + /** One model posed with {@code state} (fallback: a fresh state of the model's own type) as a jem. */ @SuppressWarnings({"unchecked", "rawtypes"}) static JsonObject dump(Model model, Object state, String name, List failed) { ModelPart root = model.root(); @@ -1514,7 +1396,6 @@ public class VanillaModelDump { failed.add(name + " (no layer)"); return null; } - // Pose with the renderer's default state; fall back to a fresh state of the model's own type. boolean posed = false; if (state != null) { try { @@ -1528,8 +1409,7 @@ public class VanillaModelDump { for (Method m : model.getClass().getMethods()) { if (!m.getName().equals("setupAnim") || m.getParameterCount() != 1) continue; try { - Object st = m.getParameterTypes()[0].getDeclaredConstructor().newInstance(); - m.invoke(model, st); + m.invoke(model, m.getParameterTypes()[0].getDeclaredConstructor().newInstance()); posed = true; break; } catch (Throwable ignored) { @@ -1537,22 +1417,21 @@ public class VanillaModelDump { } } if (!posed) failed.add(name + " (rest pose: setupAnim failed)"); + return jem(roots.get(loc), subPath, root, loc.model().getPath() + "#" + loc.layer()); + } - LayerDefinition def = roots.get(loc); - PartDefinition rootDef = ((net.minecraft.client.model.geom.builders.MeshDefinition) - fieldValue(def, "mesh")) - .getRoot(); + /** The jem of a (sub-tree of a) layer definition with the parts' current pose; {@code layer} may be null. */ + static JsonObject jem(LayerDefinition def, List subPath, ModelPart root, String layer) { + PartDefinition rootDef = ((MeshDefinition) fieldValue(def, "mesh")).getRoot(); for (String seg : subPath) rootDef = rootDef.getChild(seg); Object mat = fieldValue(def, "material"); - int tw = (int) fieldValue(mat, "xTexSize"), th = (int) fieldValue(mat, "yTexSize"); - JsonObject jem = new JsonObject(); JsonArray ts = new JsonArray(); - ts.add(tw); - ts.add(th); + ts.add((int) fieldValue(mat, "xTexSize")); + ts.add((int) fieldValue(mat, "yTexSize")); jem.add("textureSize", ts); jem.addProperty("absolute", true); - jem.addProperty("layer", loc.model().getPath() + "#" + loc.layer()); + if (layer != null) jem.addProperty("layer", layer); JsonArray parts = new JsonArray(); parts.add(part(subPath.isEmpty() ? "root" : subPath.getLast(), rootDef, root, 0, 0, 0)); jem.add("models", parts); @@ -1574,13 +1453,12 @@ public class VanillaModelDump { if (p.skipDraw) o.addProperty("skipDraw", true); JsonArray boxes = new JsonArray(); for (CubeDefinition c : (List) fieldValue(def, "cubes")) { - org.joml.Vector3fc org = (org.joml.Vector3fc) fieldValue(c, "origin"); - org.joml.Vector3fc dim = (org.joml.Vector3fc) fieldValue(c, "dimensions"); + Vector3fc org = (Vector3fc) fieldValue(c, "origin"); + Vector3fc dim = (Vector3fc) fieldValue(c, "dimensions"); Object grow = fieldValue(c, "grow"); float gx = (float) fieldValue(grow, "growX"), gy = (float) fieldValue(grow, "growY"), gz = (float) fieldValue(grow, "growZ"); Object tex = fieldValue(c, "texCoord"), texScale = fieldValue(c, "texScale"); - boolean mirror = (boolean) fieldValue(c, "mirror"); Set faces = (Set) fieldValue(c, "visibleFaces"); float x0 = ax + org.x(), y0 = ay + org.y(), z0 = az + org.z(); JsonObject b = new JsonObject(); @@ -1593,7 +1471,7 @@ public class VanillaModelDump { } else { b.add("sizeAddXYZ", arr(gx, gy, gz)); } - if (mirror) b.addProperty("mirrorTexture", "u"); + if ((boolean) fieldValue(c, "mirror")) b.addProperty("mirrorTexture", "u"); if (faces != null && faces.size() < 6) { JsonArray hide = new JsonArray(); // Named in the CEM frame (x and y flipped): Java east is CEM west, Java up is CEM down. @@ -1614,8 +1492,7 @@ public class VanillaModelDump { if (!boxes.isEmpty()) o.add("boxes", boxes); JsonArray subs = new JsonArray(); for (Map.Entry ce : def.getChildren()) { - ModelPart child = p.getChild(ce.getKey()); - subs.add(part(ce.getKey(), ce.getValue(), child, ax, ay, az)); + subs.add(part(ce.getKey(), ce.getValue(), p.getChild(ce.getKey()), ax, ay, az)); } if (!subs.isEmpty()) o.add("submodels", subs); return o; diff --git a/tools/edgecase-plugin/build.sh b/tools/edgecase-plugin/build.sh index 0879747..0b6fb90 100755 --- a/tools/edgecase-plugin/build.sh +++ b/tools/edgecase-plugin/build.sh @@ -1,18 +1,19 @@ #!/usr/bin/env bash # Builds the edge-case test plugin (PPTest.jar) against the compiled PixelPics classes. -# Usage: ./gradlew compileJava && tools/edgecase-plugin/build.sh [out.jar] -# (classpath = paper-api + its deps, e.g. the Gradle compileClasspath joined with ":") -# Then drop the jar into the TEST server's plugins/ (never production), start the server and run -# `pptest all` (or `pptest `) in its console; renders land in plugins/PPTest/.png. -# Scenes are built at world (5000 + i*64, 200, 5000) on stone platforms; see PPTest#onEnable. +# Usage: ./gradlew compileJava && tools/edgecase-plugin/build.sh [compile-classpath] [out.jar] +# classpath default: ./gradlew -q printCompileClasspath; JAVAC must be a JDK >= 25 (default: javac). +# Drop the jar into the TEST server's plugins/ (never production), start the server and run `pptest all` +# (or `pptest ...`) in its console; renders land in plugins/PPTest/.png. set -euo pipefail ROOT="$(cd "$(dirname "$0")/../.." && pwd)" -API="${1:?compile classpath (paper-api + deps)}" +API="${1:-$(cd "$ROOT" && ./gradlew -q printCompileClasspath)}" OUT="${2:-$ROOT/build/PPTest.jar}" JAVAC="${JAVAC:-javac}" TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT "$JAVAC" -nowarn -d "$TMP" -cp "$ROOT/build/classes/java/main:$API" "$ROOT/tools/edgecase-plugin/pptest/PPTest.java" cp "$ROOT/tools/edgecase-plugin/plugin.yml" "$TMP/" +mkdir -p "$(dirname "$OUT")" +rm -f "$OUT" # zip would update an existing jar and keep stale classes (cd "$TMP" && zip -q -r "$OUT" .) echo "Wrote $OUT" diff --git a/tools/edgecase-plugin/pptest/PPTest.java b/tools/edgecase-plugin/pptest/PPTest.java index 1b701e0..5aa66c4 100644 --- a/tools/edgecase-plugin/pptest/PPTest.java +++ b/tools/edgecase-plugin/pptest/PPTest.java @@ -4,6 +4,38 @@ import eu.mhsl.minecraft.pixelpics.Main; import eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer; import eu.mhsl.minecraft.pixelpics.render.render.RenderJob; import eu.mhsl.minecraft.pixelpics.render.render.Resolution; +import eu.mhsl.minecraft.pixelpics.render.entity.ObjectBaker; +import eu.mhsl.minecraft.pixelpics.survival.CameraItems; +import eu.mhsl.minecraft.pixelpics.utils.ImageMapRenderer; +import eu.mhsl.minecraft.pixelpics.utils.MapImageDither; +import eu.mhsl.minecraft.pixelpics.utils.MapManager; +import io.papermc.paper.registry.RegistryAccess; +import io.papermc.paper.registry.RegistryKey; +import io.papermc.paper.world.WeatheringCopperState; +import java.awt.Graphics2D; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import org.bukkit.attribute.Attribute; +import org.bukkit.block.Banner; +import org.bukkit.block.Campfire; +import org.bukkit.block.CreatureSpawner; +import org.bukkit.block.DecoratedPot; +import org.bukkit.block.Shelf; +import org.bukkit.block.Sign; +import org.bukkit.block.TrialSpawner; +import org.bukkit.block.banner.Pattern; +import org.bukkit.block.banner.PatternType; +import org.bukkit.block.sign.Side; +import org.bukkit.inventory.meta.BannerMeta; +import org.bukkit.inventory.meta.CrossbowMeta; +import org.bukkit.inventory.meta.LeatherArmorMeta; +import org.bukkit.inventory.meta.MapMeta; +import org.bukkit.inventory.meta.ShieldMeta; +import org.bukkit.map.MapView; +import org.bukkit.potion.PotionType; +import org.bukkit.util.EulerAngle; import java.awt.image.BufferedImage; import java.io.File; import java.util.*; @@ -16,7 +48,6 @@ import org.bukkit.block.Biome; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.block.data.BlockData; -import org.bukkit.block.data.type.Bed; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.entity.*; @@ -32,173 +63,180 @@ import org.joml.Vector3f; /** Test-only plugin: builds edge-case scenes and renders them through PixelPics into PNGs. */ public class PPTest extends JavaPlugin { - record Scene(String name, Consumer build, double camX, double camY, double camZ, double lookX, double lookY, double lookZ) {} + /** {@code index}: the scene is built at world (5000 + index·64, 200, 5000); camera relative to that origin. */ + record Scene(String name, int index, Consumer build, double camX, double camY, double camZ, + double lookX, double lookY, double lookZ) {} private final Map scenes = new LinkedHashMap<>(); private World w; + private void scene(String name, Consumer build, double camX, double camY, double camZ, double lookX, + double lookY, double lookZ) { + scenes.put(name, new Scene(name, scenes.size(), build, camX, camY, camZ, lookX, lookY, lookZ)); + } + @Override public void onEnable() { // Each scene sits on its own stone platform; the camera is placed relative to the platform origin. - scenes.put("held", new Scene("held", this::held, 4.4, 2.6, -8.5, 4.4, 1.0, 1)); - scenes.put("equip", new Scene("equip", this::equip, 4, 3, -8.5, 4, 1.0, 1.5)); - scenes.put("riding", new Scene("riding", this::riding, 5, 3, -7.5, 5, 1.2, 1)); - scenes.put("leash", new Scene("leash", this::leash, 5, 4, -9, 5, 2.5, 2)); - scenes.put("displays", new Scene("displays", this::displays, 5, 3, -9, 5, 1.0, 1.5)); - scenes.put("pixelblock", new Scene("pixelblock", this::pixelblock, 1.5, 1.9, -1.3, 0.5, 0.5, 0.5)); - scenes.put("misc", new Scene("misc", this::misc, 5, 3, -7, 5, 0.8, 1.5)); - scenes.put("dbg", new Scene("dbg", this::dbg, 1, 1.6, -3, 1, 0.8, 0.5)); - scenes.put("shelf", new Scene("shelf", this::shelf, 3.5, 3.2, -2.5, 0.5, 1.5, 0.5)); - scenes.put("shelftop", new Scene("shelftop", this::shelf, 0.52, 6, 0.5, 0.5, 0, 0.5)); - scenes.put("rotated", new Scene("rotated", this::rotated, 3, 4.5, -4, 3, 0.5, 2)); - scenes.put("rotated2", new Scene("rotated2", this::rotated, -0.5, 3.5, 9.5, -0.5, 0.5, 5)); - scenes.put("redstone", new Scene("redstone", this::redstone, 3, 3, -3, 3, 0, 2)); - scenes.put("piston", new Scene("piston", this::piston, 4.5, 3.2, -3.5, 5, 1, 1)); - scenes.put("pistonback", new Scene("pistonback", this::piston, 5.5, 3.2, 5.5, 5, 1, 1)); - scenes.put("pistondown", new Scene("pistondown", this::piston, -1.6, 2.6, -1.6, 0.5, 1.4, 0.5)); - scenes.put("pistonup", new Scene("pistonup", this::piston, 0.4, 2.6, -1.6, 2.5, 1.4, 0.5)); - scenes.put("vehicles", new Scene("vehicles", this::vehicles, 5, 3, -7, 5, 0.6, 1.5)); + scene("held", this::held, 4.4, 2.6, -8.5, 4.4, 1.0, 1); + scene("equip", this::equip, 4, 3, -8.5, 4, 1.0, 1.5); + scene("riding", this::riding, 5, 3, -7.5, 5, 1.2, 1); + scene("leash", this::leash, 5, 4, -9, 5, 2.5, 2); + scene("displays", this::displays, 5, 3, -9, 5, 1.0, 1.5); + scene("pixelblock", this::pixelblock, 1.5, 1.9, -1.3, 0.5, 0.5, 0.5); + scene("misc", this::misc, 5, 3, -7, 5, 0.8, 1.5); + scene("dbg", this::dbg, 1, 1.6, -3, 1, 0.8, 0.5); + scene("shelf", this::shelf, 3.5, 3.2, -2.5, 0.5, 1.5, 0.5); + scene("shelftop", this::shelf, 0.52, 6, 0.5, 0.5, 0, 0.5); + scene("rotated", this::rotated, 3, 4.5, -4, 3, 0.5, 2); + scene("rotated2", this::rotated, -0.5, 3.5, 9.5, -0.5, 0.5, 5); + scene("redstone", this::redstone, 3, 3, -3, 3, 0, 2); + scene("piston", this::piston, 4.5, 3.2, -3.5, 5, 1, 1); + scene("pistonback", this::piston, 5.5, 3.2, 5.5, 5, 1, 1); + scene("pistondown", this::piston, -1.6, 2.6, -1.6, 0.5, 1.4, 0.5); + scene("pistonup", this::piston, 0.4, 2.6, -1.6, 2.5, 1.4, 0.5); + scene("vehicles", this::vehicles, 5, 3, -7, 5, 0.6, 1.5); // Blockstate x rotations (+ x/y combined, uvlock): lying pillars, vertical observers/dispensers, top stairs. - scenes.put("xrot", new Scene("xrot", this::xrot, 3.5, 5.5, -4.5, 6, 0.5, 3.5)); - scenes.put("xrot2", new Scene("xrot2", this::xrot, 11, 5, 10.5, 6, 0.5, 3.5)); + scene("xrot", this::xrot, 3.5, 5.5, -4.5, 6, 0.5, 3.5); + scene("xrot2", this::xrot, 11, 5, 10.5, 6, 0.5, 3.5); // Chests: same layout seen from all four sides (single/double per facing, trapped, ender, copper). - scenes.put("chests_s", new Scene("chests_s", this::chests, 4.5, 5, 13, 4.5, 0, 3)); - scenes.put("chests_n", new Scene("chests_n", this::chests, 4.5, 5, -7, 4.5, 0, 3)); - 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)); + scene("chests_s", this::chests, 4.5, 5, 13, 4.5, 0, 3); + scene("chests_n", this::chests, 4.5, 5, -7, 4.5, 0, 3); + scene("chests_e", this::chests, 16, 5, 3, 4.5, 0, 3); + scene("chests_w", this::chests, -7, 5, 3, 4.5, 0, 3); + 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)); + scene("proj_fly_top", this::projFly, 6.5, 13, 2.2, 6.5, 0, 2); + scene("proj_fly_side", this::projFly, 6.5, 2.6, -8, 6.5, 2.4, 2); + scene("proj_fly_obl", this::projFly, -2, 6, -6, 6.5, 2, 2); + scene("proj_stuck_nw", this::projStuck, -3, 6, -6, 8, 1.5, 2); + scene("proj_stuck_se", this::projStuck, 19, 6, 10, 8, 1.5, 2); + scene("proj_stuck_ne", this::projStuck, 19, 6, -6, 8, 1.5, 2); + scene("proj_fly_gold", this::projFly, 2.6, 8.5, 2.5, 2.5, 0, 2.5); + scene("proj_fly_gold_side", this::projFly, 2.5, 2.6, -3.5, 2.5, 2.4, 2.5); + scene("proj_stuck_close_a", this::projStuck, -2.5, 2.4, -2.5, 2.5, 1.6, 1); + 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)); + scene("proj_shake", this::projStuck, -2.5, 2.4, -2.5, 2.5, 1.6, 1); // Conditional vanilla layers: collars (tamed only, dye-tinted), charged creeper / wither armor (additive // energy swirl), fox/dolphin mouth items. - scenes.put("layers", new Scene("layers", this::layers, 4.8, 2.4, -5.5, 4.8, 0.6, 1.5)); - scenes.put("layers_side", new Scene("layers_side", this::layers, 12.5, 2.2, 5.5, 5, 0.5, 2.5)); - scenes.put("layers_close", new Scene("layers_close", this::layers, 5.5, 1.3, 0.3, 5.5, 0.4, 2.5)); - scenes.put("layers_pets", new Scene("layers_pets", this::layers, 4.5, 1.6, -3.2, 4.5, 0.3, 0)); - scenes.put("layers_dolphin", new Scene("layers_dolphin", this::layers, 10, 1.0, 1.0, 7.5, 0.4, 2.0)); - scenes.put("layers_cats", new Scene("layers_cats", this::layers, 1.7, 1.1, -1.6, 1.7, 0.3, 0)); - scenes.put("layers_cats_side", new Scene("layers_cats_side", this::layers, 1.7, 1.6, 1.4, 1.7, 0.2, 0)); - scenes.put("layers_top", new Scene("layers_top", this::layers, 5.5, 6, 2.0, 5.5, 0, 2.5)); - scenes.put("layers_wither", new Scene("layers_wither", this::withers, 3.5, 4, -9, 3.5, 2.2, 1)); - scenes.put("layers_wither_close", new Scene("layers_wither_close", this::withers, 6, 3.4, -3.5, 6, 2.6, 1)); + scene("layers", this::layers, 4.8, 2.4, -5.5, 4.8, 0.6, 1.5); + scene("layers_side", this::layers, 12.5, 2.2, 5.5, 5, 0.5, 2.5); + scene("layers_close", this::layers, 5.5, 1.3, 0.3, 5.5, 0.4, 2.5); + scene("layers_pets", this::layers, 4.5, 1.6, -3.2, 4.5, 0.3, 0); + scene("layers_dolphin", this::layers, 10, 1.0, 1.0, 7.5, 0.4, 2.0); + scene("layers_cats", this::layers, 1.7, 1.1, -1.6, 1.7, 0.3, 0); + scene("layers_cats_side", this::layers, 1.7, 1.6, 1.4, 1.7, 0.2, 0); + scene("layers_top", this::layers, 5.5, 6, 2.0, 5.5, 0, 2.5); + scene("layers_wither", this::withers, 3.5, 4, -9, 3.5, 2.2, 1); + scene("layers_wither_close", this::withers, 6, 3.4, -3.5, 6, 2.6, 1); // Block entities (vanilla BlockEntityRenderer poses): signs, banners, heads in all 16 rotations + wall // mounts, and bells/pots/shulker boxes/lecterns/conduits, each layout seen from two sides. - scenes.put("be_signs_s", new Scene("be_signs_s", this::beSigns, 7, 3, 8, 7, 0.5, 1)); - scenes.put("be_signs_n", new Scene("be_signs_n", this::beSigns, 7, 1.3, -8, 7, 0.6, 1)); - scenes.put("be_signs_close", new Scene("be_signs_close", this::beSigns, 3, 1.8, 4.2, 3, 0.8, 2)); - scenes.put("be_signs_wall", new Scene("be_signs_wall", this::beSigns, 4.6, 2.2, 10.2, 3.5, 0.9, 7)); - scenes.put("be_signs_wall_n", new Scene("be_signs_wall_n", this::beSigns, 1.4, 2.2, 3.8, 3.5, 0.9, 7)); - scenes.put("be_signs_wall2", new Scene("be_signs_wall2", this::beSigns, 12.5, 2.4, 10.2, 10, 1.2, 7)); - scenes.put("be_signs_hang", new Scene("be_signs_hang", this::beSigns, 4, 1.3, 0.2, 4, 1.4, -3)); - scenes.put("be_banners_s", new Scene("be_banners_s", this::beBanners, 7, 3, 10, 7, 1, 1.5)); - scenes.put("be_banners_n", new Scene("be_banners_n", this::beBanners, 7, 3, -6, 7, 1, 1.5)); - scenes.put("be_banners_wall", new Scene("be_banners_wall", this::beBanners, 13, 3.5, 9, 16, 1.5, 5)); - scenes.put("be_banners_wall_n", new Scene("be_banners_wall_n", this::beBanners, 13, 3.5, 1, 16, 1.5, 5)); - scenes.put("be_heads_s", new Scene("be_heads_s", this::beHeads, 5, 5, 11, 5, 0, 3)); - scenes.put("be_heads_n", new Scene("be_heads_n", this::beHeads, 5, 5, -6, 5, 0, 3)); - scenes.put("be_heads_close", new Scene("be_heads_close", this::beHeads, 4, 2.2, 9.5, 4, 0.3, 5)); - scenes.put("be_heads_wall_w", new Scene("be_heads_wall_w", this::beHeads, 7, 3, 2.5, 12, 1, 2.5)); - scenes.put("be_heads_wall_e", new Scene("be_heads_wall_e", this::beHeads, 17, 3, 2.5, 12, 1, 2.5)); - scenes.put("be_misc_s", new Scene("be_misc_s", this::beMisc, 6, 5, 11, 6, 0.5, 3)); - scenes.put("be_misc_n", new Scene("be_misc_n", this::beMisc, 6, 5, -6, 6, 0.5, 3)); - scenes.put("be_misc_close", new Scene("be_misc_close", this::beMisc, 3, 2.5, 3, 3, 0.5, 0)); - scenes.put("be_misc_lectern", new Scene("be_misc_lectern", this::beMisc, 3, 2.2, 9.5, 3, 0.6, 6)); - scenes.put("be_misc_shulker", new Scene("be_misc_shulker", this::beMisc, 11, 2.8, 9.5, 11, 0.3, 4)); - scenes.put("be_items", new Scene("be_items", this::beItems, 4.5, 2.6, 11, 4.5, 0.8, 3)); - scenes.put("be_items_back", new Scene("be_items_back", this::beItems, 4.5, 2.6, -5, 4.5, 0.8, 3)); - scenes.put("proj_stuck_sw", new Scene("proj_stuck_sw", this::projStuck, -3, 6, 10, 8, 1.5, 2)); + scene("be_signs_s", this::beSigns, 7, 3, 8, 7, 0.5, 1); + scene("be_signs_n", this::beSigns, 7, 1.3, -8, 7, 0.6, 1); + scene("be_signs_close", this::beSigns, 3, 1.8, 4.2, 3, 0.8, 2); + scene("be_signs_wall", this::beSigns, 4.6, 2.2, 10.2, 3.5, 0.9, 7); + scene("be_signs_wall_n", this::beSigns, 1.4, 2.2, 3.8, 3.5, 0.9, 7); + scene("be_signs_wall2", this::beSigns, 12.5, 2.4, 10.2, 10, 1.2, 7); + scene("be_signs_hang", this::beSigns, 4, 1.3, 0.2, 4, 1.4, -3); + scene("be_banners_s", this::beBanners, 7, 3, 10, 7, 1, 1.5); + scene("be_banners_n", this::beBanners, 7, 3, -6, 7, 1, 1.5); + scene("be_banners_wall", this::beBanners, 13, 3.5, 9, 16, 1.5, 5); + scene("be_banners_wall_n", this::beBanners, 13, 3.5, 1, 16, 1.5, 5); + scene("be_heads_s", this::beHeads, 5, 5, 11, 5, 0, 3); + scene("be_heads_n", this::beHeads, 5, 5, -6, 5, 0, 3); + scene("be_heads_close", this::beHeads, 4, 2.2, 9.5, 4, 0.3, 5); + scene("be_heads_wall_w", this::beHeads, 7, 3, 2.5, 12, 1, 2.5); + scene("be_heads_wall_e", this::beHeads, 17, 3, 2.5, 12, 1, 2.5); + scene("be_misc_s", this::beMisc, 6, 5, 11, 6, 0.5, 3); + scene("be_misc_n", this::beMisc, 6, 5, -6, 6, 0.5, 3); + scene("be_misc_close", this::beMisc, 3, 2.5, 3, 3, 0.5, 0); + scene("be_misc_lectern", this::beMisc, 3, 2.2, 9.5, 3, 0.6, 6); + scene("be_misc_shulker", this::beMisc, 11, 2.8, 9.5, 11, 0.3, 4); + scene("be_items", this::beItems, 4.5, 2.6, 11, 4.5, 0.8, 3); + scene("be_items_back", this::beItems, 4.5, 2.6, -5, 4.5, 0.8, 3); + scene("proj_stuck_sw", this::projStuck, -3, 6, 10, 8, 1.5, 2); // Data-driven variants / pose flags / armor wearers (vanilla dump: asset_id, setupAnim flags, armor layers). - scenes.put("rf_cats", new Scene("rf_cats", this::rfCats, 6.5, 2.6, -4.5, 6.5, 0.2, 1.2)); - scenes.put("rf_wolves", new Scene("rf_wolves", this::rfWolves, 5.5, 3.2, -5.5, 5.5, 0.2, 1.8)); - scenes.put("rf_farm", new Scene("rf_farm", this::rfFarm, 6, 3.4, -6, 6, 0.3, 1.8)); - scenes.put("rf_stands", new Scene("rf_stands", this::rfStands, 5, 2.4, -4.5, 5, 0.9, 0)); - scenes.put("rf_stands_back", new Scene("rf_stands_back", this::rfStands, 5, 2.4, 4.5, 5, 0.9, 0)); - scenes.put("rf_chests", new Scene("rf_chests", this::rfChests, 7, 4, -7, 7, 0.8, 1)); - scenes.put("rf_chests_side", new Scene("rf_chests_side", this::rfChests, 7, 2.5, 7, 7, 0.8, 1)); + scene("rf_cats", this::rfCats, 6.5, 2.6, -4.5, 6.5, 0.2, 1.2); + scene("rf_wolves", this::rfWolves, 5.5, 3.2, -5.5, 5.5, 0.2, 1.8); + scene("rf_farm", this::rfFarm, 6, 3.4, -6, 6, 0.3, 1.8); + scene("rf_stands", this::rfStands, 5, 2.4, -4.5, 5, 0.9, 0); + scene("rf_stands_back", this::rfStands, 5, 2.4, 4.5, 5, 0.9, 0); + scene("rf_chests", this::rfChests, 7, 4, -7, 7, 0.8, 1); + scene("rf_chests_side", this::rfChests, 7, 2.5, 7, 7, 0.8, 1); // Needs a datapack cat variant "pptest:ocelotcat" (asset_id entity/cat/ocelot) in the world's datapacks. - scenes.put("rf_dpcat", new Scene("rf_dpcat", this::rfDatapackCat, 1.2, 1.4, -2.2, 1.2, 0.2, 0.5)); - scenes.put("rf_armor", new Scene("rf_armor", this::rfArmor, 7, 3.2, -6.5, 7, 1.0, 1.5)); + scene("rf_dpcat", this::rfDatapackCat, 1.2, 1.4, -2.2, 1.2, 0.2, 0.5); + scene("rf_armor", this::rfArmor, 7, 3.2, -6.5, 7, 1.0, 1.5); // Item frames (vanilla ItemFrameRenderer): north wall with rotations 0-7, block/flat/tool/block-entity // items, photo + synthetic + vanilla-data maps, invisible and glow frames; frames on all 6 block sides. - scenes.put("frames", new Scene("frames", this::frames, 4.5, 2.8, -3.5, 4.5, 2.6, 4)); - scenes.put("frames_close", new Scene("frames_close", this::frames, 2.6, 2.4, 0.8, 2.6, 2.3, 4)); - scenes.put("frames_side", new Scene("frames_side", this::frames, -1.5, 3.2, -1.0, 4.5, 2.6, 3.6)); - scenes.put("frames_dirs_ne", new Scene("frames_dirs_ne", this::framesDirs, 16, 7.5, -4, 8, 4, 3.5)); - scenes.put("frames_dirs_sw", new Scene("frames_dirs_sw", this::framesDirs, 0, 1.2, 10, 8, 4, 3.5)); + scene("frames", this::frames, 4.5, 2.8, -3.5, 4.5, 2.6, 4); + scene("frames_close", this::frames, 2.6, 2.4, 0.8, 2.6, 2.3, 4); + scene("frames_side", this::frames, -1.5, 3.2, -1.0, 4.5, 2.6, 3.6); + scene("frames_dirs_ne", this::framesDirs, 16, 7.5, -4, 8, 4, 3.5); + scene("frames_dirs_sw", this::framesDirs, 0, 1.2, 10, 8, 4, 3.5); // Camera projection (pinhole, vertical FOV 70): frontal checkerboard wall / item-frame map (squares must be // square, same size everywhere), ceiling straight up, floor straight down at yaw 30, oblique wall with // stripes (edges must stay straight, the vertical stripe through the view centre vertical). *_sq = 384x384. - scenes.put("cam_checker_sq", new Scene("cam_checker_sq", this::camChecker, 5.5, 5.5, 1, 5.5, 5.5, 6)); - scenes.put("cam_checker", new Scene("cam_checker", this::camChecker, 5.5, 5.5, 1, 5.5, 5.5, 6)); - scenes.put("cam_frame_sq", new Scene("cam_frame_sq", this::camFrame, 5.5, 5.5, 3, 5.5, 5.5, 6)); - scenes.put("cam_up_sq", new Scene("cam_up_sq", this::camChecker, 5.5, 6, 0.5, 5.5, 11, 0.5)); - scenes.put("cam_down_sq", new Scene("cam_down_sq", this::camChecker, 5.5, 4, 0.5, 5.4995, 0, 0.500866)); - scenes.put("cam_oblique", new Scene("cam_oblique", this::camStripes, 2, 11, 1.5, 5.5, 5.5, 6)); + scene("cam_checker_sq", this::camChecker, 5.5, 5.5, 1, 5.5, 5.5, 6); + scene("cam_checker", this::camChecker, 5.5, 5.5, 1, 5.5, 5.5, 6); + scene("cam_frame_sq", this::camFrame, 5.5, 5.5, 3, 5.5, 5.5, 6); + scene("cam_up_sq", this::camChecker, 5.5, 6, 0.5, 5.5, 11, 0.5); + scene("cam_down_sq", this::camChecker, 5.5, 4, 0.5, 5.4995, 0, 0.500866); + scene("cam_oblique", this::camStripes, 2, 11, 1.5, 5.5, 5.5, 6); // Performance (entity BVH): a 15x18 animal farm (270 mobs, randomizeData=false, fixed body yaw) and 300 // item displays from a seeded RNG -> deterministic, pixel-diffable; the render time is logged. - scenes.put("perf_farm", new Scene("perf_farm", this::perfFarm, -6, 9, -13, 4, 0, 0)); - scenes.put("perf_farm_close", new Scene("perf_farm_close", this::perfFarm, 0.5, 3, -11.5, 5, 0.5, -4)); - scenes.put("perf_items", new Scene("perf_items", this::perfItems, 0.5, 5, -10, 5, 0, -2)); + scene("perf_farm", this::perfFarm, -6, 9, -13, 4, 0, 0); + scene("perf_farm_close", this::perfFarm, 0.5, 3, -11.5, 5, 0.5, -4); + scene("perf_items", this::perfItems, 0.5, 5, -10, 5, 0, -2); // Per-mob texture compositing (tinted-texture cache): villager type/profession/level, sheep colours, dyed // leather armor, wolf collars and patterned banners, all from a seeded RNG (deterministic). - scenes.put("perf_village", new Scene("perf_village", this::perfVillage, -6, 9, -13, 4, 0, 0)); - scenes.put("perf_village_close", new Scene("perf_village_close", this::perfVillage, 0.5, 3, -11.5, 5, 0.5, -4)); + scene("perf_village", this::perfVillage, -6, 9, -13, 4, 0, 0); + scene("perf_village_close", this::perfVillage, 0.5, 3, -11.5, 5, 0.5, -4); // Entity poses (vanilla setupAnim/renderer state from the dump): sitting/sleeping/crouching pets with mouth // items, cats lying, parrots, pandas; camel/sniffer/bat/dolphin pitch/fish in water/sleeping villager/ // mannequin poses/baby rider; armor stands with custom part poses. All rfSpawn (deterministic). - scenes.put("pose_pets", new Scene("pose_pets", this::posePets, 4.3, 3.6, -6, 4.3, 0.2, 2)); - scenes.put("pose_pets_side", new Scene("pose_pets_side", this::posePets, -3, 3, -3, 4, 0, 2)); - scenes.put("pose_fox_close", new Scene("pose_fox_close", this::posePets, 2.2, 1.4, -2.2, 2.2, 0.2, 0)); - scenes.put("pose_misc", new Scene("pose_misc", this::poseMisc, 6.5, 5, -10, 6.5, 0.5, 0)); - scenes.put("pose_misc_side", new Scene("pose_misc_side", this::poseMisc, -4, 3, -7, 6, 0.5, -1)); - scenes.put("pose_humans", new Scene("pose_humans", this::poseHumans, 6, 3.5, -7, 6, 0.6, 1)); - scenes.put("pose_humans_side", new Scene("pose_humans_side", this::poseHumans, -3, 3, -4, 5, 0.5, 1)); - scenes.put("pose_humans_close", new Scene("pose_humans_close", this::poseHumans, 4.8, 1.6, -5.2, 4.8, 0.5, -2.5)); - scenes.put("pose_beds", new Scene("pose_beds", this::poseHumans, 1.3, 2.6, -3.2, 1.3, 0.4, 0.2)); - scenes.put("pose_stands", new Scene("pose_stands", this::poseStands, 4, 2.4, -5, 4, 0.9, 0)); - scenes.put("pose_stands_side", new Scene("pose_stands_side", this::poseStands, -3, 2.4, -3, 4, 0.9, 0)); + scene("pose_pets", this::posePets, 4.3, 3.6, -6, 4.3, 0.2, 2); + scene("pose_pets_side", this::posePets, -3, 3, -3, 4, 0, 2); + scene("pose_fox_close", this::posePets, 2.2, 1.4, -2.2, 2.2, 0.2, 0); + scene("pose_misc", this::poseMisc, 6.5, 5, -10, 6.5, 0.5, 0); + scene("pose_misc_side", this::poseMisc, -4, 3, -7, 6, 0.5, -1); + scene("pose_humans", this::poseHumans, 6, 3.5, -7, 6, 0.6, 1); + scene("pose_humans_side", this::poseHumans, -3, 3, -4, 5, 0.5, 1); + scene("pose_humans_close", this::poseHumans, 4.8, 1.6, -5.2, 4.8, 0.5, -2.5); + scene("pose_beds", this::poseHumans, 1.3, 2.6, -3.2, 1.3, 0.4, 0.2); + scene("pose_stands", this::poseStands, 4, 2.4, -5, 4, 0.9, 0); + scene("pose_stands_side", this::poseStands, -3, 2.4, -3, 4, 0.9, 0); // Entity texture / layer states (rfSpawn, deterministic): emissive eyes (spider, cave spider, enderman, // phantom, breeze, copper golem, warden), sheep wool colour / undercoat / baby wool / jeb_, angry wolves, // partial-alpha cutout textures (llama, tropical fish); small armor stands with armor, elytra at rest, // piglin / illager arm poses, skeleton holding a sword, invulnerable wither, tipped / potion arrows. - scenes.put("rf_vlayers", new Scene("rf_vlayers", this::rfVisualLayers, 5.5, 3.4, -6, 5.5, 0.6, 2.5)); - scenes.put("rf_vlayers_close", new Scene("rf_vlayers_close", this::rfVisualLayers, 3.2, 1.5, -2.4, 3.2, 0.5, 1)); - scenes.put("rf_vstates", new Scene("rf_vstates", this::rfVisualStates, 5.5, 3.4, -6, 5.5, 0.9, 2.5)); - scenes.put("rf_vstates_close", new Scene("rf_vstates_close", this::rfVisualStates, 3.2, 1.7, -2.6, 3.2, 0.9, 0.5)); + scene("rf_vlayers", this::rfVisualLayers, 5.5, 3.4, -6, 5.5, 0.6, 2.5); + scene("rf_vlayers_close", this::rfVisualLayers, 3.2, 1.5, -2.4, 3.2, 0.5, 1); + scene("rf_vstates", this::rfVisualStates, 5.5, 3.4, -6, 5.5, 0.9, 2.5); + scene("rf_vstates_close", this::rfVisualStates, 3.2, 1.7, -2.6, 3.2, 0.9, 0.5); // Block geometry (vanilla FaceBakery): rescaled element rotations (cross plants, amethyst, campfire, fire, // raised rails, tripwire hooks, coral wall fans, dripleaf, stems, sea pickles), Euler x/y/z element // rotations (hanging sign rot 3/7/11/15 chains), inverted elements (spawner/vault inner faces), 22.5° steps. - scenes.put("geo_plants", new Scene("geo_plants", this::geoPlants, 5.5, 2.6, -3.2, 5.5, 0.4, 2.2)); - scenes.put("geo_plants_close", new Scene("geo_plants_close", this::geoPlants, 2.5, 1.1, -0.9, 2.5, 0.5, 1)); - scenes.put("geo_misc", new Scene("geo_misc", this::geoMisc, 6, 3.4, -4.5, 6, 0.5, 3)); - scenes.put("geo_misc_close", new Scene("geo_misc_close", this::geoMisc, 3, 1.6, -1.6, 3, 0.4, 1)); - scenes.put("geo_misc_signs", new Scene("geo_misc_signs", this::geoMisc, 4, 2.4, 3.5, 4, 2.2, 7)); + scene("geo_plants", this::geoPlants, 5.5, 2.6, -3.2, 5.5, 0.4, 2.2); + scene("geo_plants_close", this::geoPlants, 2.5, 1.1, -0.9, 2.5, 0.5, 1); + scene("geo_misc", this::geoMisc, 6, 3.4, -4.5, 6, 0.5, 3); + scene("geo_misc_close", this::geoMisc, 3, 1.6, -1.6, 3, 0.4, 1); + scene("geo_misc_signs", this::geoMisc, 4, 2.4, 3.5, 4, 2.2, 7); // Water depth vs. translucency: a pool with columns 1-6 blocks deep (x pairs), each with a glass block, // a gold block and white stained glass on the sand bottom; seen from the shore, from above and from inside. - scenes.put("geo_water", new Scene("geo_water", this::geoWater, 6, 3.5, -5, 6, -3, 2.5)); - scenes.put("geo_water_top", new Scene("geo_water_top", this::geoWater, 6, 9, 2.4, 6, -4, 2.5)); - scenes.put("geo_water_under", new Scene("geo_water_under", this::geoWater, 11.5, -3.3, 0.3, 4, -5, 3)); + scene("geo_water", this::geoWater, 6, 3.5, -5, 6, -3, 2.5); + scene("geo_water_top", this::geoWater, 6, 9, 2.4, 6, -4, 2.5); + scene("geo_water_under", this::geoWater, 11.5, -3.3, 0.3, 4, -5, 3); // Block-entity contents: enchanting-table book (closed; open when the camera is within 3 blocks), campfire // and shelf items, copper golem statues (4 poses x stages), spawner/trial-spawner mobs, vault, beacon beam // through red+blue glass, 3x3 end portal and an end gateway. Rendered 100 ticks after building (beacon // levels update every 80 ticks). - scenes.put("be_contents", new Scene("be_contents", this::beContents, 7, 4.5, 13, 7, 0.8, 3)); - scenes.put("be_contents_n", new Scene("be_contents_n", this::beContents, 7, 4.5, -7, 7, 0.8, 3)); - 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)); + scene("be_contents", this::beContents, 7, 4.5, 13, 7, 0.8, 3); + scene("be_contents_n", this::beContents, 7, 4.5, -7, 7, 0.8, 3); + scene("be_contents_close", this::beContents, 1.2, 1.8, 2.2, 1.5, 0.6, 0); + scene("be_contents_beam", this::beContents, 4, 6, 26, 10, 9, 6); + 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 @@ -207,12 +245,12 @@ public class PPTest extends JavaPlugin { // 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)); + scene("items_detail", this::itemsDetail, 4.5, 3.2, 13, 4.5, 0.9, 2); + scene("items_detail_close", this::itemsDetail, 2.2, 1.7, 7.4, 2.2, 0.9, 2); + scene("items_detail_side", this::itemsDetail, -4.5, 2.4, 4.5, 5, 0.9, 3); + scene("items_inverted", this::itemsInverted, 3, 2.4, 7, 3, 0.6, 1.5); + scene("xmas_chests", this::xmasChests, 6, 3.6, 9.5, 6, 0.5, 1.5); + scene("frames_markers", this::framesMarkers, 2.5, 2.4, 4.2, 2.5, 2.4, 0); // Fluids (vanilla FluidRenderer / FogRenderer): liq_flow = water flowing with physics (rendered 200 ticks after // building): a source on a 3-high terrace spreading and falling over its edges, a lone source spreading on // the floor, a sunk 3x3 source pool (surface 8/9 below the rim). liq_logged = waterlogged blocks in a 1-deep @@ -221,30 +259,30 @@ public class PPTest extends JavaPlugin { // the surface (bridge + lily pads above), just under the surface, from above; _swamp/_warm = same tank in a // swamp / warm ocean (water_fog_color, swamp end x0.85, water_color). liq_lava = lava pool, flowing levels // and a falling column; liq_lava_in = camera inside the lava (lava fog). - scenes.put("liq_flow", new Scene("liq_flow", this::liqFlow, -3, 8, -9, 6, 0, 0)); - scenes.put("liq_flow_close", new Scene("liq_flow_close", this::liqFlow, 7.5, 1.6, 9, 11, 0, 5)); - scenes.put("liq_flow_fall", new Scene("liq_flow_fall", this::liqFlow, -4, 3, 7, 2, 1.5, 0)); - scenes.put("liq_logged", new Scene("liq_logged", this::liqLogged, 6, 3.6, -4.5, 6, -0.5, 3.5)); - scenes.put("liq_logged_close", new Scene("liq_logged_close", this::liqLogged, 2.2, 0.7, -1.4, 3, -0.4, 1.5)); - scenes.put("liq_logged_air", new Scene("liq_logged_air", this::liqLogged, 4, 1.6, 2.6, 4.5, 0.4, 5.5)); - scenes.put("liq_under", new Scene("liq_under", o -> liqUnder(o, Biome.PLAINS), -3.5, -3, -8.5, 12, -5, 6)); - scenes.put("liq_under_deep", new Scene("liq_under_deep", o -> liqUnder(o, Biome.PLAINS), -3.5, -7, -8.5, 12, -6, 6)); - scenes.put("liq_under_up", new Scene("liq_under_up", o -> liqUnder(o, Biome.PLAINS), 6.2, -6, -4, 6.4, 0, 0.5)); - scenes.put("liq_under_shallow", new Scene("liq_under_shallow", o -> liqUnder(o, Biome.PLAINS), -3.5, -0.45, -8.5, 12, 0.6, 6)); - scenes.put("liq_under_top", new Scene("liq_under_top", o -> liqUnder(o, Biome.PLAINS), -3, 4, -10, 6, -6, 2)); - scenes.put("liq_under_swamp", new Scene("liq_under_swamp", o -> liqUnder(o, Biome.SWAMP), -3.5, -3, -8.5, 12, -5, 6)); - scenes.put("liq_under_warm", new Scene("liq_under_warm", o -> liqUnder(o, Biome.WARM_OCEAN), -3.5, -3, -8.5, 12, -5, 6)); - scenes.put("liq_under_warm_top", new Scene("liq_under_warm_top", o -> liqUnder(o, Biome.WARM_OCEAN), -3, 4, -10, 6, -6, 2)); - scenes.put("liq_lava", new Scene("liq_lava", this::liqLava, 4, 4.5, -6, 9, 0, 1)); - scenes.put("liq_lava_in", new Scene("liq_lava_in", this::liqLava, 3.5, -0.5, 1.5, 9, -0.5, 1.5)); + scene("liq_flow", this::liqFlow, -3, 8, -9, 6, 0, 0); + scene("liq_flow_close", this::liqFlow, 7.5, 1.6, 9, 11, 0, 5); + scene("liq_flow_fall", this::liqFlow, -4, 3, 7, 2, 1.5, 0); + scene("liq_logged", this::liqLogged, 6, 3.6, -4.5, 6, -0.5, 3.5); + scene("liq_logged_close", this::liqLogged, 2.2, 0.7, -1.4, 3, -0.4, 1.5); + scene("liq_logged_air", this::liqLogged, 4, 1.6, 2.6, 4.5, 0.4, 5.5); + scene("liq_under", o -> liqUnder(o, Biome.PLAINS), -3.5, -3, -8.5, 12, -5, 6); + scene("liq_under_deep", o -> liqUnder(o, Biome.PLAINS), -3.5, -7, -8.5, 12, -6, 6); + scene("liq_under_up", o -> liqUnder(o, Biome.PLAINS), 6.2, -6, -4, 6.4, 0, 0.5); + scene("liq_under_shallow", o -> liqUnder(o, Biome.PLAINS), -3.5, -0.45, -8.5, 12, 0.6, 6); + scene("liq_under_top", o -> liqUnder(o, Biome.PLAINS), -3, 4, -10, 6, -6, 2); + scene("liq_under_swamp", o -> liqUnder(o, Biome.SWAMP), -3.5, -3, -8.5, 12, -5, 6); + scene("liq_under_warm", o -> liqUnder(o, Biome.WARM_OCEAN), -3.5, -3, -8.5, 12, -5, 6); + scene("liq_under_warm_top", o -> liqUnder(o, Biome.WARM_OCEAN), -3, 4, -10, 6, -6, 2); + scene("liq_lava", this::liqLava, 4, 4.5, -6, 9, 0, 1); + scene("liq_lava_in", this::liqLava, 3.5, -0.5, 1.5, 9, -0.5, 1.5); } 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(); + ShieldMeta m = (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])); + m.addPattern(new Pattern((DyeColor) patterns[i], (PatternType) patterns[i + 1])); s.setItemMeta(m); return s; } @@ -252,8 +290,8 @@ public class PPTest extends JavaPlugin { 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), + shield(DyeColor.BLUE, DyeColor.YELLOW, PatternType.CREEPER, + DyeColor.WHITE, 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++) { @@ -318,8 +356,8 @@ public class PPTest extends JavaPlugin { 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); + MapView v = ((MapMeta) marked.getItemMeta()).getMapView(); + v.setScale(MapView.Scale.CLOSEST); v.setCenterX(o.getBlockX()); v.setCenterZ(o.getBlockZ()); put(o, -5, 0, -10, "red_banner[rotation=0]"); @@ -341,14 +379,14 @@ public class PPTest extends JavaPlugin { } /** MapItemSavedData.toggleBanner(level, pos) on the map's server data (reflective: no API). */ - private void mapBanner(org.bukkit.map.MapView v, Location banner) { + private void mapBanner(MapView v, Location banner) { try { Object data = mapData(v); - Object level = w.getClass().getMethod("getHandle").invoke(w); + Object level = handle(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()) + for (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) { @@ -357,13 +395,13 @@ public class PPTest extends JavaPlugin { } /** MapItemSavedData.addDecoration(MapDecorationTypes., 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) { + private void mapDecoration(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 level = handle(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()) + for (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); @@ -373,8 +411,8 @@ public class PPTest extends JavaPlugin { } } - private static Object mapData(org.bukkit.map.MapView v) throws ReflectiveOperationException { - java.lang.reflect.Field wm = v.getClass().getDeclaredField("worldMap"); + private static Object mapData(MapView v) throws ReflectiveOperationException { + Field wm = v.getClass().getDeclaredField("worldMap"); wm.setAccessible(true); return wm.get(v); } @@ -385,7 +423,7 @@ public class PPTest extends JavaPlugin { put(o, 4, 0, 0, "soul_campfire[facing=east,lit=false]"); put(o, 6, 0, 0, "oak_shelf[facing=south]"); put(o, 8, 0, 0, "cherry_shelf[facing=south,powered=false]"); - if (o.clone().add(2, 0, 0).getBlock().getState() instanceof org.bukkit.block.Campfire c) { + if (o.clone().add(2, 0, 0).getBlock().getState() instanceof Campfire c) { Material[] food = {Material.BEEF, Material.COD, Material.PORKCHOP, Material.POTATO}; for (int i = 0; i < 4; i++) { c.setItem(i, it(food[i])); @@ -394,14 +432,14 @@ public class PPTest extends JavaPlugin { 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) { + if (o.clone().add(4, 0, 0).getBlock().getState() instanceof Campfire c) { c.setItem(0, it(Material.SALMON)); c.setItem(2, it(Material.CHICKEN)); c.update(true, false); } Material[][] shelves = {{Material.DIAMOND, Material.BOOK, Material.OAK_LOG}, {Material.GOLDEN_APPLE, null, Material.LANTERN}}; for (int k = 0; k < 2; k++) { - if (o.clone().add(6 + 2 * k, 0, 0).getBlock().getState() instanceof org.bukkit.block.Shelf sh) { + if (o.clone().add(6 + 2 * k, 0, 0).getBlock().getState() instanceof Shelf sh) { for (int i = 0; i < 3; i++) if (shelves[k][i] != null) sh.getSnapshotInventory().setItem(i, it(shelves[k][i])); sh.update(true, false); } @@ -417,12 +455,12 @@ public class PPTest extends JavaPlugin { put(o, 6, 0, 6, "vault[facing=south]"); EntityType[] mobs = {EntityType.ZOMBIE, EntityType.PIG}; for (int i = 0; i < 2; i++) { - if (o.clone().add(i * 2, 0, 6).getBlock().getState() instanceof org.bukkit.block.CreatureSpawner cs) { + if (o.clone().add(i * 2, 0, 6).getBlock().getState() instanceof CreatureSpawner cs) { cs.setSpawnedType(mobs[i]); cs.update(true, false); } } - if (o.clone().add(4, 0, 6).getBlock().getState() instanceof org.bukkit.block.TrialSpawner ts) { + if (o.clone().add(4, 0, 6).getBlock().getState() instanceof TrialSpawner ts) { ts.getNormalConfiguration().setSpawnedType(EntityType.SKELETON); ts.update(true, false); put(o, 4, 0, 6, "trial_spawner[trial_spawner_state=waiting_for_players]"); @@ -445,7 +483,7 @@ public class PPTest extends JavaPlugin { rfSpawn(o, 4.8, 1.2, 0, 180, Phantom.class); rfSpawn(o, 6.4, 0, 0, 180, Breeze.class); rfSpawn(o, 8.0, 0, 0, 180, CopperGolem.class) - .setWeatheringState(io.papermc.paper.world.WeatheringCopperState.OXIDIZED); + .setWeatheringState(WeatheringCopperState.OXIDIZED); rfSpawn(o, 10.2, 0, 0.5, 180, Warden.class); // Row 2: sheep (white, red, red sheared, baby red, jeb_, blue sheared), angry wolves (wild, tame). rfSpawn(o, 0, 0, 2.6, 180, Sheep.class); @@ -495,7 +533,7 @@ public class PPTest extends JavaPlugin { p2.setAggressive(true); Piglin p3 = rfSpawn(o, 7.6, 0, 0, 180, Piglin.class); ItemStack cb = it(Material.CROSSBOW); - org.bukkit.inventory.meta.CrossbowMeta cm = (org.bukkit.inventory.meta.CrossbowMeta) cb.getItemMeta(); + CrossbowMeta cm = (CrossbowMeta) cb.getItemMeta(); cm.addChargedProjectile(it(Material.ARROW)); cb.setItemMeta(cm); p3.getEquipment().setItemInMainHand(cb); @@ -519,7 +557,7 @@ public class PPTest extends JavaPlugin { Arrow tipped = (Arrow) projectile(o.clone().add(0.5, 1.6, 5.5), new Vector(1, 0, 0), 0.0001, false, Arrow.class); tipped.setColor(Color.fromARGB(0x00FF2020)); Arrow potion = (Arrow) projectile(o.clone().add(0.5, 2.2, 5.5), new Vector(1, 0, 0), 0.0001, false, Arrow.class); - potion.setBasePotionType(org.bukkit.potion.PotionType.POISON); + potion.setBasePotionType(PotionType.POISON); projectile(o.clone().add(0.5, 2.8, 5.5), new Vector(1, 0, 0), 0.0001, false, SpectralArrow.class); for (Entity e : w.getNearbyEntities(o.clone().add(6, 1, 2), 9, 4, 6)) { if (e instanceof LivingEntity le && !(e instanceof ArmorStand)) le.setBodyYaw(180); @@ -541,8 +579,8 @@ public class PPTest extends JavaPlugin { private void perfVillage(Location o) { Random r = new Random(11); - List vTypes = registry(io.papermc.paper.registry.RegistryKey.VILLAGER_TYPE); - List profs = registry(io.papermc.paper.registry.RegistryKey.VILLAGER_PROFESSION); + List vTypes = registry(RegistryKey.VILLAGER_TYPE); + List profs = registry(RegistryKey.VILLAGER_PROFESSION); DyeColor[] dyes = DyeColor.values(); for (int i = 0; i < 14; i++) for (int j = 0; j < 16; j++) { @@ -573,8 +611,8 @@ public class PPTest extends JavaPlugin { ItemStack[] armor = new ItemStack[4]; for (int k = 0; k < 4; k++) { armor[k] = it(pieces[k]); - org.bukkit.inventory.meta.LeatherArmorMeta m = - (org.bukkit.inventory.meta.LeatherArmorMeta) armor[k].getItemMeta(); + LeatherArmorMeta m = + (LeatherArmorMeta) armor[k].getItemMeta(); m.setColor(dye.getColor()); armor[k].setItemMeta(m); } @@ -596,9 +634,9 @@ public class PPTest extends JavaPlugin { for (int b = 0; b < 10; b++) { int x = -4 + b * 2; put(o, x, 0, 10, dyes[r.nextInt(dyes.length)].name().toLowerCase() + "_banner[rotation=" + r.nextInt(16) + "]"); - if (o.clone().add(x, 0, 10).getBlock().getState() instanceof org.bukkit.block.Banner bn) { - bn.addPattern(new org.bukkit.block.banner.Pattern(dyes[r.nextInt(dyes.length)], org.bukkit.block.banner.PatternType.STRIPE_TOP)); - bn.addPattern(new org.bukkit.block.banner.Pattern(dyes[r.nextInt(dyes.length)], org.bukkit.block.banner.PatternType.CREEPER)); + if (o.clone().add(x, 0, 10).getBlock().getState() instanceof Banner bn) { + bn.addPattern(new Pattern(dyes[r.nextInt(dyes.length)], PatternType.STRIPE_TOP)); + bn.addPattern(new Pattern(dyes[r.nextInt(dyes.length)], PatternType.CREEPER)); bn.update(true, false); } } @@ -723,7 +761,7 @@ public class PPTest extends JavaPlugin { put(o, 0, 0, -1, "red_bed[facing=south,part=foot]"); Villager v = rfSpawn(o, 0.5, 0, 1.5, 180, Villager.class); v.setProfession(Villager.Profession.LIBRARIAN); - v.sleep(o.clone().add(0, 0, 0)); + v.sleep(o.clone()); put(o, 2, 0, 0, "blue_bed[facing=east,part=head]"); put(o, 1, 0, 0, "blue_bed[facing=east,part=foot]"); Villager vb = rfSpawn(o, 2.5, 0, 1.5, 180, Villager.class); @@ -762,43 +800,39 @@ public class PPTest extends JavaPlugin { // Custom armor-stand poses (head/body/arms/legs, radians), with arms, armor and held items. ArmorStand a = rfSpawn(o, 0, 0, 0, 180, ArmorStand.class); a.setArms(true); - a.setHeadPose(new org.bukkit.util.EulerAngle(Math.toRadians(-20), Math.toRadians(30), 0)); - a.setRightArmPose(new org.bukkit.util.EulerAngle(Math.toRadians(-90), 0, 0)); - a.setLeftArmPose(new org.bukkit.util.EulerAngle(Math.toRadians(-30), 0, Math.toRadians(-40))); - Ah(a); + a.setHeadPose(new EulerAngle(Math.toRadians(-20), Math.toRadians(30), 0)); + a.setRightArmPose(new EulerAngle(Math.toRadians(-90), 0, 0)); + a.setLeftArmPose(new EulerAngle(Math.toRadians(-30), 0, Math.toRadians(-40))); + a.getEquipment().setHelmet(it(Material.GOLDEN_HELMET)); + hands(a, Material.BOW, Material.APPLE); ArmorStand b = rfSpawn(o, 1.6, 0, 0, 180, ArmorStand.class); b.setArms(true); - b.setBodyPose(new org.bukkit.util.EulerAngle(Math.toRadians(15), Math.toRadians(20), 0)); - b.setRightLegPose(new org.bukkit.util.EulerAngle(Math.toRadians(-40), 0, 0)); - b.setLeftLegPose(new org.bukkit.util.EulerAngle(Math.toRadians(30), 0, Math.toRadians(-10))); + b.setBodyPose(new EulerAngle(Math.toRadians(15), Math.toRadians(20), 0)); + b.setRightLegPose(new EulerAngle(Math.toRadians(-40), 0, 0)); + b.setLeftLegPose(new EulerAngle(Math.toRadians(30), 0, Math.toRadians(-10))); diamondArmor(b); hands(b, Material.IRON_SWORD, Material.SHIELD); ArmorStand c = rfSpawn(o, 3.2, 0, 0, 180, ArmorStand.class); c.setArms(true); c.setSmall(true); - c.setRightArmPose(new org.bukkit.util.EulerAngle(Math.toRadians(-120), Math.toRadians(20), 0)); - c.setHeadPose(new org.bukkit.util.EulerAngle(0, 0, Math.toRadians(25))); + c.setRightArmPose(new EulerAngle(Math.toRadians(-120), Math.toRadians(20), 0)); + c.setHeadPose(new EulerAngle(0, 0, Math.toRadians(25))); diamondArmor(c); hands(c, Material.TORCH, null); ArmorStand d = rfSpawn(o, 4.8, 0, 0, 180, ArmorStand.class); - d.setHeadPose(new org.bukkit.util.EulerAngle(Math.toRadians(30), 0, 0)); + d.setHeadPose(new EulerAngle(Math.toRadians(30), 0, 0)); d.getEquipment().setHelmet(it(Material.CARVED_PUMPKIN)); ArmorStand e = rfSpawn(o, 6.4, 0, 0, 180, ArmorStand.class); e.setArms(true); e.setInvisible(true); - e.setRightArmPose(new org.bukkit.util.EulerAngle(Math.toRadians(-60), Math.toRadians(-30), 0)); - e.setBodyPose(new org.bukkit.util.EulerAngle(0, Math.toRadians(30), 0)); + e.setRightArmPose(new EulerAngle(Math.toRadians(-60), Math.toRadians(-30), 0)); + e.setBodyPose(new EulerAngle(0, Math.toRadians(30), 0)); diamondArmor(e); hands(e, Material.DIAMOND_SWORD, null); ArmorStand f = rfSpawn(o, 8, 0, 0, 180, ArmorStand.class); // default pose (must match rf_stands) f.setArms(true); } - private static void Ah(ArmorStand a) { - a.getEquipment().setHelmet(it(Material.GOLDEN_HELMET)); - hands(a, Material.BOW, Material.APPLE); - } - private void perfItems(Location o) { Random r = new Random(7); Material[] mats = {Material.WHEAT, Material.CARROT, Material.EGG, Material.BONE, Material.ROTTEN_FLESH, @@ -823,8 +857,7 @@ public class PPTest extends JavaPlugin { for (String n : todo) { Scene sc = scenes.get(n); if (sc == null) { getLogger().warning("unknown scene " + n); continue; } - int idx = new ArrayList<>(scenes.keySet()).indexOf(n); - Location origin = new Location(w, 5000 + idx * 64, 200, 5000); + Location origin = new Location(w, 5000 + sc.index * 64, 200, 5000); Bukkit.getScheduler().runTaskLater(this, () -> run(sc, origin), 1 + i * 60L); i++; } @@ -843,9 +876,7 @@ public class PPTest extends JavaPlugin { 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; + for (Chunk c : areaChunks(o)) ready &= c.getLoadLevel() == Chunk.LoadLevel.ENTITY_TICKING; if (!ready && waited < 200) { Bukkit.getScheduler().runTaskLater(this, () -> whenEntityTicking(sc, o, waited + 1), 1); return; @@ -862,8 +893,28 @@ public class PPTest extends JavaPlugin { getLogger().severe("build " + sc.name + " failed: " + t); t.printStackTrace(); } - // Let one tick pass (passengers/leashes settle), then render. - Bukkit.getScheduler().runTaskLater(this, () -> render(sc, o), sc.name.equals("proj_shake") ? 4 : sc.name.startsWith("proj_") ? 30 : sc.name.startsWith("be_contents") ? 100 : sc.name.startsWith("liq_flow") ? 200 : 10); + Bukkit.getScheduler().runTaskLater(this, () -> render(sc, o), settleTicks(sc.name)); + } + + /** + * Ticks between building and rendering: passengers/leashes settle (10); projectiles fly and stick (30), proj_shake + * catches freshly stuck arrows still wobbling (4); beacon levels update every 80 ticks (be_contents); flowing + * water spreads (liq_flow). + */ + private static int settleTicks(String name) { + if (name.equals("proj_shake")) return 4; + if (name.startsWith("proj_")) return 30; + if (name.startsWith("be_contents")) return 100; + if (name.startsWith("liq_flow")) return 200; + return 10; + } + + private static final List LARGE = List.of("proj_", "rf_", "pose_", "frames", "geo_", "liq_", "items_", "xmas_"); + + /** *_sq scenes square, newer scene families 960x640, the rest 480x320. */ + private static Resolution resolution(String name) { + if (name.endsWith("_sq")) return new Resolution(384, 384); + return LARGE.stream().anyMatch(name::startsWith) ? new Resolution(960, 640) : new Resolution(480, 320); } private void render(Scene sc, Location o) { @@ -871,19 +922,20 @@ 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_") || sc.name.startsWith("liq_") || sc.name.startsWith("items_") || sc.name.startsWith("xmas_") ? new Resolution(960, 640) : new Resolution(480, 320), null); + RenderJob job = r.prepare(eye, resolution(sc.name), null); if (sc.name.equals("dbg")) { + // Bake log of the display objects near the origin. try { - java.lang.reflect.Field f = DefaultScreenRenderer.class.getDeclaredField("objectBaker"); + Field f = DefaultScreenRenderer.class.getDeclaredField("objectBaker"); f.setAccessible(true); - var ob = (eu.mhsl.minecraft.pixelpics.render.entity.ObjectBaker) f.get(r); + var ob = (ObjectBaker) f.get(r); for (var st : job.objects()) { double[] pos = st.toWorld().t; if (Math.abs(pos[0] - o.getX()) > 5 || Math.abs(pos[2] - o.getZ()) > 5) continue; try { var re = ob.bake(st); getLogger().info("dbg obj " + st.kind() + " item=" + st.item() + " ctx=" + st.context() + " -> " - + (re == null ? "null" : re.cubes.size() + " cubes " + java.util.Arrays.toString(re.aabbMin))); + + (re == null ? "null" : re.cubes.size() + " cubes " + Arrays.toString(re.aabbMin))); } catch (Throwable t) { getLogger().warning("dbg bake failed: " + t); t.printStackTrace(); @@ -893,30 +945,7 @@ 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); - } + if (sc.name.startsWith("proj_")) logProjectiles(sc.name, job, o); getLogger().info("scene " + sc.name + ": " + job.entities().size() + " entities, " + job.objects().size() + " objects, " + job.blockEntities().size() + " block entities"); Bukkit.getScheduler().runTaskAsynchronously(this, () -> { @@ -935,14 +964,51 @@ public class PPTest extends JavaPlugin { }); } + /** Projectile orientation vs. velocity (proj_shake: also the captured arrow pitch and vanilla shakeTime). */ + private void logProjectiles(String name, RenderJob job, Location o) { + boolean shake = name.equals("proj_shake"); + if (shake) + for (var st : job.entities()) + if (st.typeKey().contains("arrow")) + getLogger().info(String.format("state %s pitch=%.2f", st.typeKey(), st.pitch())); + for (Entity e : w.getNearbyEntities(o.clone().add(8, 3, 2), 12, 8, 12)) { + if (!(e instanceof Projectile pr)) continue; + 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 (shake && e instanceof AbstractArrow && !(e instanceof Trident)) { + try { + Object h = handle(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); + } + } + } + } + + private static Object handle(Object craftObject) throws ReflectiveOperationException { + return craftObject.getClass().getMethod("getHandle").invoke(craftObject); + } + + /** The chunks around a scene origin (its platform and camera). */ + private List areaChunks(Location o) { + List out = new ArrayList<>(); + for (int cx = -1; cx <= 2; cx++) + for (int cz = -2; cz <= 2; cz++) out.add(w.getChunkAt((o.getBlockX() >> 4) + cx, (o.getBlockZ() >> 4) + cz)); + return out; + } + /** 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 (Chunk c : areaChunks(o)) { + c.load(true); + c.addPluginChunkTicket(this); + } } /** Clears the area (blocks + entities) and lays a 24x24 stone floor at the origin's y-1. */ @@ -959,11 +1025,7 @@ public class PPTest extends JavaPlugin { // --- 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. - */ + /** {@link #rfSpawn} with the body turned to {@code yaw} (spawned mobs otherwise keep a random body yaw). */ private T spawn(Location o, double x, double y, double z, float yaw, Class type) { T e = rfSpawn(o, x, y, z, yaw, type); if (e instanceof LivingEntity le) le.setBodyYaw(yaw); @@ -984,7 +1046,7 @@ public class PPTest extends JavaPlugin { * 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 evenId(java.util.function.Supplier spawner) { + private T evenId(Supplier spawner) { T e = spawner.get(); while (e.getEntityId() % 2 != 0) { e.remove(); @@ -1088,7 +1150,7 @@ public class PPTest extends JavaPlugin { } private void leash(Location o) { - Block fence = o.clone().add(0, 0, 0).getBlock(); + Block fence = o.getBlock(); fence.setType(Material.OAK_FENCE); LeashHitch knot = w.spawn(fence.getLocation(), LeashHitch.class); Cow cow = spawn(o, 2.5, 0, 0.5, 180, Cow.class); @@ -1226,42 +1288,31 @@ public class PPTest extends JavaPlugin { /** World blocks: a log column with shelf mushrooms (26.x) on all four sides, both growth stages. */ private void shelf(Location o) { - for (int y = 0; y < 3; y++) o.clone().add(0, y, 0).getBlock().setType(Material.OAK_LOG, false); - o.clone().add(0, 0, -3).getBlock().setType(Material.DIAMOND_BLOCK, false); // north marker - o.clone().add(3, 0, 0).getBlock().setType(Material.GOLD_BLOCK, false); // east marker - String[] f = {"north", "south", "east", "west"}; - int[][] off = {{0, -1}, {0, 1}, {1, 0}, {-1, 0}}; - for (int i = 0; i < 4; i++) { - for (int age = 0; age < 2; age++) { - o.clone().add(off[i][0], 1 + age, off[i][1]).getBlock().setBlockData( - Bukkit.createBlockData("minecraft:shelf_mushroom[age=" + age + ",facing=" + f[i] + "]"), false); - } - } + for (int y = 0; y < 3; y++) put(o, 0, y, 0, "oak_log"); + put(o, 0, 0, -3, "diamond_block"); // north marker + put(o, 3, 0, 0, "gold_block"); // east marker + for (int i = 0; i < 4; i++) + for (int age = 0; age < 2; age++) + put(o, STEP[i][0], 1 + age, STEP[i][1], "shelf_mushroom[age=" + age + ",facing=" + DIRS[i] + "]"); } /** Blockstate y-rotations: ladders, stairs, wall torches, furnaces, logs on all sides of stone pillars. */ private void rotated(Location o) { - String[] f = {"north", "south", "east", "west"}; - int[][] off = {{0, -1}, {0, 1}, {1, 0}, {-1, 0}}; String[] kinds = {"ladder[facing=%s]", "wall_torch[facing=%s]", "oak_stairs[facing=%s]", "furnace[facing=%s]"}; for (int k = 0; k < kinds.length; k++) { - Location c = o.clone().add(k * 3 - 1, 0, 2); - c.getBlock().setType(Material.STONE, false); - c.clone().add(0, 1, 0).getBlock().setType(Material.STONE, false); - for (int i = 0; i < 4; i++) { - c.clone().add(off[i][0], k == 2 || k == 3 ? 0 : 1, off[i][1]).getBlock().setBlockData( - Bukkit.createBlockData("minecraft:" + String.format(kinds[k], f[i])), false); - } + int x = k * 3 - 1; + put(o, x, 0, 2, "stone"); + put(o, x, 1, 2, "stone"); + for (int i = 0; i < 4; i++) + put(o, x + STEP[i][0], k == 2 || k == 3 ? 0 : 1, 2 + STEP[i][1], String.format(kinds[k], DIRS[i])); } - // Fence with a single connection to the east (stone post at +x), door facing east, chest-free check. - o.clone().add(-3, 0, 5).getBlock().setBlockData(Bukkit.createBlockData("minecraft:oak_fence[east=true]"), false); - o.clone().add(-2, 0, 5).getBlock().setType(Material.STONE, false); - o.clone().add(1, 0, 5).getBlock().setBlockData( - Bukkit.createBlockData("minecraft:oak_door[facing=east,half=lower,hinge=left,open=false]"), false); - o.clone().add(1, 1, 5).getBlock().setBlockData( - Bukkit.createBlockData("minecraft:oak_door[facing=east,half=upper,hinge=left,open=false]"), false); - o.clone().add(9, 0, 2).getBlock().setBlockData(Bukkit.createBlockData("minecraft:oak_log[axis=x]"), false); - o.clone().add(9, 0, 4).getBlock().setBlockData(Bukkit.createBlockData("minecraft:oak_log[axis=z]"), false); + // Fence with a single connection to the east (stone post at +x), door facing east. + put(o, -3, 0, 5, "oak_fence[east=true]"); + put(o, -2, 0, 5, "stone"); + put(o, 1, 0, 5, "oak_door[facing=east,half=lower,hinge=left,open=false]"); + put(o, 1, 1, 5, "oak_door[facing=east,half=upper,hinge=left,open=false]"); + put(o, 9, 0, 2, "oak_log[axis=x]"); + put(o, 9, 0, 4, "oak_log[axis=z]"); } /** @@ -1272,52 +1323,45 @@ public class PPTest extends JavaPlugin { private void chests(Location o) { String[] f = {"south", "west", "north", "east"}; int[][] cw = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}}; - java.util.function.BiConsumer put = (p, data) -> o.clone().add(p[0], p[1], p[2]).getBlock() - .setBlockData(Bukkit.createBlockData("minecraft:" + data), false); for (int i = 0; i < 4; i++) { - put.accept(new int[] {i * 3, 0, 0}, "chest[facing=" + f[i] + ",type=single]"); - put.accept(new int[] {i * 3, 0, 3}, "chest[facing=" + f[i] + ",type=left]"); - put.accept(new int[] {i * 3 + cw[i][0], 0, 3 + cw[i][1]}, "chest[facing=" + f[i] + ",type=right]"); + put(o, i * 3, 0, 0, "chest[facing=" + f[i] + ",type=single]"); + put(o, i * 3, 0, 3, "chest[facing=" + f[i] + ",type=left]"); + put(o, i * 3 + cw[i][0], 0, 3 + cw[i][1], "chest[facing=" + f[i] + ",type=right]"); } - put.accept(new int[] {0, 0, 6}, "trapped_chest[facing=south,type=left]"); - put.accept(new int[] {-1, 0, 6}, "trapped_chest[facing=south,type=right]"); - put.accept(new int[] {3, 0, 6}, "ender_chest[facing=south]"); - put.accept(new int[] {6, 0, 6}, "copper_chest[facing=east,type=single]"); - put.accept(new int[] {9, 0, 6}, "oxidized_copper_chest[facing=north,type=left]"); - put.accept(new int[] {10, 0, 6}, "oxidized_copper_chest[facing=north,type=right]"); + put(o, 0, 0, 6, "trapped_chest[facing=south,type=left]"); + put(o, -1, 0, 6, "trapped_chest[facing=south,type=right]"); + put(o, 3, 0, 6, "ender_chest[facing=south]"); + put(o, 6, 0, 6, "copper_chest[facing=east,type=single]"); + put(o, 9, 0, 6, "oxidized_copper_chest[facing=north,type=left]"); + put(o, 10, 0, 6, "oxidized_copper_chest[facing=north,type=right]"); } /** Redstone wire (dot, line, corner, climbing a wall, powered/unpowered) next to glass and panes. */ private void redstone(Location o) { - java.util.function.BiConsumer put = (p, data) -> o.clone().add(p[0], p[1], p[2]).getBlock() - .setBlockData(Bukkit.createBlockData("minecraft:" + data), false); - put.accept(new int[] {0, 0, 0}, "redstone_wire[power=0]"); - for (int x = 1; x <= 4; x++) put.accept(new int[] {x, 0, 0}, "redstone_wire[east=side,west=side,power=" + (x * 3) + "]"); - put.accept(new int[] {5, 0, 0}, "redstone_wire[west=side,north=side,power=15]"); - put.accept(new int[] {5, 0, -1}, "redstone_wire[south=side,north=up,power=15]"); - put.accept(new int[] {5, 0, -2}, "stone"); - put.accept(new int[] {5, 1, -2}, "redstone_wire[south=side,power=15]"); - put.accept(new int[] {0, 0, 3}, "glass"); - put.accept(new int[] {1, 0, 3}, "red_stained_glass"); - put.accept(new int[] {2, 0, 3}, "glass_pane[east=true,west=false]"); - put.accept(new int[] {3, 0, 3}, "blue_stained_glass_pane[east=true,west=true]"); - put.accept(new int[] {4, 0, 3}, "blue_stained_glass_pane[west=true,north=true]"); + put(o, 0, 0, 0, "redstone_wire[power=0]"); + for (int x = 1; x <= 4; x++) put(o, x, 0, 0, "redstone_wire[east=side,west=side,power=" + (x * 3) + "]"); + put(o, 5, 0, 0, "redstone_wire[west=side,north=side,power=15]"); + put(o, 5, 0, -1, "redstone_wire[south=side,north=up,power=15]"); + put(o, 5, 0, -2, "stone"); + put(o, 5, 1, -2, "redstone_wire[south=side,power=15]"); + put(o, 0, 0, 3, "glass"); + put(o, 1, 0, 3, "red_stained_glass"); + put(o, 2, 0, 3, "glass_pane[east=true,west=false]"); + put(o, 3, 0, 3, "blue_stained_glass_pane[east=true,west=true]"); + put(o, 4, 0, 3, "blue_stained_glass_pane[west=true,north=true]"); } /** x/y blockstate rotations without uvlock: pistons in all six directions, observers, dispensers, logs. */ private void piston(Location o) { String[] f = {"down", "up", "north", "south", "east", "west"}; for (int i = 0; i < f.length; i++) { - o.clone().add(i * 2, 1, 0).getBlock().setBlockData( - Bukkit.createBlockData("minecraft:piston[facing=" + f[i] + ",extended=false]"), false); - o.clone().add(i * 2, 1, 2).getBlock().setBlockData( - Bukkit.createBlockData("minecraft:observer[facing=" + f[i] + "]"), false); - o.clone().add(i * 2, 0, 2).getBlock().setBlockData( - Bukkit.createBlockData("minecraft:dispenser[facing=" + f[i] + "]"), false); + put(o, i * 2, 1, 0, "piston[facing=" + f[i] + ",extended=false]"); + put(o, i * 2, 1, 2, "observer[facing=" + f[i] + "]"); + put(o, i * 2, 0, 2, "dispenser[facing=" + f[i] + "]"); } - o.clone().add(0, 0, -2).getBlock().setBlockData(Bukkit.createBlockData("minecraft:oak_log[axis=x]"), false); - o.clone().add(2, 0, -2).getBlock().setBlockData(Bukkit.createBlockData("minecraft:oak_log[axis=z]"), false); - o.clone().add(4, 0, -2).getBlock().setBlockData(Bukkit.createBlockData("minecraft:furnace[facing=north]"), false); + put(o, 0, 0, -2, "oak_log[axis=x]"); + put(o, 2, 0, -2, "oak_log[axis=z]"); + put(o, 4, 0, -2, "furnace[facing=north]"); } /** @@ -1326,30 +1370,28 @@ public class PPTest extends JavaPlugin { * (x=180 + uvlock), oak buttons on the ceiling. A stone column at z=-2 marks north. */ private void xrot(Location o) { - java.util.function.BiConsumer put = (p, data) -> o.clone().add(p[0], p[1], p[2]).getBlock() - .setBlockData(Bukkit.createBlockData("minecraft:" + data), false); String[] pillars = {"oak_log", "basalt", "quartz_pillar", "hay_block"}; for (int i = 0; i < pillars.length; i++) { - put.accept(new int[] {i * 3, 0, 0}, pillars[i] + "[axis=x]"); - put.accept(new int[] {i * 3 + 1, 0, 0}, pillars[i] + "[axis=z]"); + put(o, i * 3, 0, 0, pillars[i] + "[axis=x]"); + put(o, i * 3 + 1, 0, 0, pillars[i] + "[axis=z]"); } String[] vertical = {"observer", "dispenser", "dropper"}; for (int i = 0; i < vertical.length; i++) { - put.accept(new int[] {i * 3, 0, 3}, vertical[i] + "[facing=up]"); - put.accept(new int[] {i * 3 + 1, 1, 3}, vertical[i] + "[facing=down]"); - put.accept(new int[] {i * 3 + 1, 0, 3}, "glass"); + put(o, i * 3, 0, 3, vertical[i] + "[facing=up]"); + put(o, i * 3 + 1, 1, 3, vertical[i] + "[facing=down]"); + put(o, i * 3 + 1, 0, 3, "glass"); } String[] f = {"north", "east", "south", "west"}; for (int i = 0; i < f.length; i++) { - put.accept(new int[] {i * 2, 0, 6}, "oak_stairs[facing=" + f[i] + ",half=top]"); - put.accept(new int[] {i * 2 + 1, 0, 6}, "stone_brick_stairs[facing=" + f[i] + ",half=top]"); + put(o, i * 2, 0, 6, "oak_stairs[facing=" + f[i] + ",half=top]"); + put(o, i * 2 + 1, 0, 6, "stone_brick_stairs[facing=" + f[i] + ",half=top]"); } - put.accept(new int[] {9, 1, 6}, "stone"); - put.accept(new int[] {9, 0, 6}, "oak_button[face=ceiling,facing=north]"); - put.accept(new int[] {10, 1, 6}, "stone"); - put.accept(new int[] {10, 0, 6}, "oak_button[face=ceiling,facing=east]"); - put.accept(new int[] {0, 0, -2}, "stone"); - put.accept(new int[] {0, 1, -2}, "stone"); + put(o, 9, 1, 6, "stone"); + put(o, 9, 0, 6, "oak_button[face=ceiling,facing=north]"); + put(o, 10, 1, 6, "stone"); + put(o, 10, 0, 6, "oak_button[face=ceiling,facing=east]"); + put(o, 0, 0, -2, "stone"); + put(o, 0, 1, -2, "stone"); } private void vehicles(Location o) { @@ -1371,24 +1413,22 @@ public class PPTest extends JavaPlugin { * cube (translucent outer skins, one behind glass) and a text display with the default background. */ private void translucent(Location o) { - java.util.function.BiConsumer put = (p, data) -> o.clone().add(p[0], p[1], p[2]).getBlock() - .setBlockData(Bukkit.createBlockData("minecraft:" + data), false); for (int x = -1; x <= 10; x++) - for (int y = 0; y <= 4; y++) put.accept(new int[] {x, y, 5}, (x + y) % 2 == 0 ? "white_wool" : "black_wool"); - for (int x = 0; x <= 2; x++) for (int y = 0; y <= 1; y++) put.accept(new int[] {x, y, 1}, "red_stained_glass"); - put.accept(new int[] {3, 0, 1}, "blue_stained_glass"); - put.accept(new int[] {3, 0, 0}, "yellow_stained_glass"); - put.accept(new int[] {4, 0, 1}, "ice"); - put.accept(new int[] {4, 1, 1}, "tinted_glass"); - put.accept(new int[] {5, 0, 1}, "slime_block"); - put.accept(new int[] {6, 0, 1}, "honey_block"); - put.accept(new int[] {7, 0, 1}, "lime_stained_glass_pane[east=true]"); - put.accept(new int[] {8, 0, 1}, "lime_stained_glass_pane[west=true,east=true]"); - put.accept(new int[] {9, 0, 1}, "lime_stained_glass_pane[west=true]"); - for (int x = 0; x <= 2; x++) put.accept(new int[] {x, -1, -2}, "water"); - put.accept(new int[] {1, -2, -2}, "magenta_stained_glass"); - put.accept(new int[] {0, -2, -2}, "sand"); - put.accept(new int[] {2, -2, -2}, "sand"); + for (int y = 0; y <= 4; y++) put(o, x, y, 5, (x + y) % 2 == 0 ? "white_wool" : "black_wool"); + for (int x = 0; x <= 2; x++) for (int y = 0; y <= 1; y++) put(o, x, y, 1, "red_stained_glass"); + put(o, 3, 0, 1, "blue_stained_glass"); + put(o, 3, 0, 0, "yellow_stained_glass"); + put(o, 4, 0, 1, "ice"); + put(o, 4, 1, 1, "tinted_glass"); + put(o, 5, 0, 1, "slime_block"); + put(o, 6, 0, 1, "honey_block"); + put(o, 7, 0, 1, "lime_stained_glass_pane[east=true]"); + put(o, 8, 0, 1, "lime_stained_glass_pane[west=true,east=true]"); + put(o, 9, 0, 1, "lime_stained_glass_pane[west=true]"); + for (int x = 0; x <= 2; x++) put(o, x, -1, -2, "water"); + put(o, 1, -2, -2, "magenta_stained_glass"); + put(o, 0, -2, -2, "sand"); + put(o, 2, -2, -2, "sand"); Slime slime = spawn(o, 1.5, 0, 3, 200, Slime.class); slime.setSize(2); Slime front = spawn(o, 7.5, 0, -1.5, 160, Slime.class); @@ -1422,9 +1462,6 @@ public class PPTest extends JavaPlugin { kit.setBaby(); hands(kit, Material.SWEET_BERRIES, null); hands(spawn(o, 7.5, 0, 2.5, 180, Dolphin.class), Material.COD, null); - // Spawned mobs get a random body yaw: face them all to the camera (north). - for (Entity e : w.getNearbyEntities(o.clone().add(5, 0, 1), 7, 3, 4)) - if (e instanceof LivingEntity le && !(e instanceof Player)) le.setBodyYaw(180); } private void tame(Tameable t, DyeColor collar) { @@ -1436,20 +1473,22 @@ public class PPTest extends JavaPlugin { private void withers(Location o) { spawn(o, 1, 0, 1, 180, Wither.class); // full health: no armor Wither half = spawn(o, 6, 0, 1, 180, Wither.class); - half.setHealth(half.getAttribute(org.bukkit.attribute.Attribute.MAX_HEALTH).getValue() / 2 - 1); - for (Entity e : w.getNearbyEntities(o, 12, 8, 8)) if (e instanceof Wither wi) wi.setBodyYaw(180); + half.setHealth(half.getAttribute(Attribute.MAX_HEALTH).getValue() / 2 - 1); } // --- data-driven variants / pose flags / armor wearers ------------------------------------------ - private static List registry(io.papermc.paper.registry.RegistryKey key) { + private static List registry(RegistryKey key) { List out = new ArrayList<>(); - io.papermc.paper.registry.RegistryAccess.registryAccess().getRegistry(key).forEach(out::add); + RegistryAccess.registryAccess().getRegistry(key).forEach(out::add); out.sort(Comparator.comparing(k -> k.getKey().toString())); return out; } - /** Spawn without vanilla's random spawn data (baby chance, jockeys, gear, biome variants): reproducible. */ + /** + * Deterministic spawn (pixel-diffable between plugin builds): randomizeData=false (no finalizeSpawn: no random + * variants, sizes, babies/jockeys or equipment), no AI. Scenes set every variant/equipment they show explicitly. + */ private T rfSpawn(Location o, double x, double y, double z, float yaw, Class type) { Location l = o.clone().add(x, y, z); l.setYaw(yaw); @@ -1475,7 +1514,7 @@ public class PPTest extends JavaPlugin { } private void rfCats(Location o) { - List types = registry(io.papermc.paper.registry.RegistryKey.CAT_VARIANT); + List types = registry(RegistryKey.CAT_VARIANT); for (int i = 0; i < types.size(); i++) { rfSpawn(o, i * 1.2, 0, 0, 180, Cat.class).setCatType(types.get(i)); Cat kit = rfSpawn(o, i * 1.2, 0, 1.6, 180, Cat.class); @@ -1487,8 +1526,8 @@ public class PPTest extends JavaPlugin { } private void rfDatapackCat(Location o) { - Cat.Type custom = io.papermc.paper.registry.RegistryAccess.registryAccess() - .getRegistry(io.papermc.paper.registry.RegistryKey.CAT_VARIANT) + Cat.Type custom = RegistryAccess.registryAccess() + .getRegistry(RegistryKey.CAT_VARIANT) .get(new NamespacedKey("pptest", "ocelotcat")); if (custom == null) throw new IllegalStateException("datapack variant pptest:ocelotcat missing"); rfSpawn(o, 0, 0, 0, 180, Cat.class).setCatType(custom); @@ -1500,7 +1539,7 @@ public class PPTest extends JavaPlugin { } private void rfWolves(Location o) { - List vs = registry(io.papermc.paper.registry.RegistryKey.WOLF_VARIANT); + List vs = registry(RegistryKey.WOLF_VARIANT); for (int i = 0; i < vs.size(); i++) { rfSpawn(o, i * 1.3, 0, 0, 180, Wolf.class).setVariant(vs.get(i)); Wolf t = rfSpawn(o, i * 1.3, 0, 1.6, 180, Wolf.class); @@ -1515,11 +1554,11 @@ public class PPTest extends JavaPlugin { } private void rfFarm(Location o) { - List cows = registry(io.papermc.paper.registry.RegistryKey.COW_VARIANT); - List pigs = registry(io.papermc.paper.registry.RegistryKey.PIG_VARIANT); - List chickens = registry(io.papermc.paper.registry.RegistryKey.CHICKEN_VARIANT); - List frogs = registry(io.papermc.paper.registry.RegistryKey.FROG_VARIANT); - List nautili = registry(io.papermc.paper.registry.RegistryKey.ZOMBIE_NAUTILUS_VARIANT); + List cows = registry(RegistryKey.COW_VARIANT); + List pigs = registry(RegistryKey.PIG_VARIANT); + List chickens = registry(RegistryKey.CHICKEN_VARIANT); + List frogs = registry(RegistryKey.FROG_VARIANT); + List nautili = registry(RegistryKey.ZOMBIE_NAUTILUS_VARIANT); for (int i = 0; i < 3; i++) { double x = i * 4; setVar(rfSpawn(o, x, 0, 0, 180, Cow.class), cows.get(i)); @@ -1543,7 +1582,7 @@ public class PPTest extends JavaPlugin { /** setVariant via reflection (the variant setters' declaring interface differs between API and server). */ private static void setVar(Entity e, Object variant) { - for (java.lang.reflect.Method m : e.getClass().getMethods()) + for (Method m : e.getClass().getMethods()) if (m.getName().equals("setVariant") && m.getParameterCount() == 1 && m.getParameterTypes()[0].isInstance(variant)) { try { @@ -1669,8 +1708,8 @@ public class PPTest extends JavaPlugin { /** 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); + put(o, 2, y, 2, "gold_block"); + put(o, 11, y, 2, "diamond_block"); } double[][] rings = {{2.5, 0}, {4.3, 35}, {0.9, -35}}; // height, pitch for (double[] ring : rings) @@ -1690,8 +1729,8 @@ public class PPTest extends JavaPlugin { 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); + put(o, 1 + x, y, 1 + z, "stone"); + put(o, 12 + x, y, 1 + z, "stone"); } double[][] pillars = {{2.5, 2.5}, {13.5, 2.5}}; // centres; arrows left, tridents right for (int pi = 0; pi < 2; pi++) { @@ -1727,10 +1766,10 @@ public class PPTest extends JavaPlugin { } private void signText(Location o, int x, int y, int z, String front, String back) { - if (!(o.clone().add(x, y, z).getBlock().getState() instanceof org.bukkit.block.Sign sg)) return; - sg.getSide(org.bukkit.block.sign.Side.FRONT).setLine(1, front); - sg.getSide(org.bukkit.block.sign.Side.BACK).setLine(1, back); - sg.getSide(org.bukkit.block.sign.Side.BACK).setColor(DyeColor.BLUE); + if (!(o.clone().add(x, y, z).getBlock().getState() instanceof Sign sg)) return; + sg.getSide(Side.FRONT).setLine(1, front); + sg.getSide(Side.BACK).setLine(1, back); + sg.getSide(Side.BACK).setColor(DyeColor.BLUE); sg.update(true, false); } @@ -1769,9 +1808,9 @@ public class PPTest extends JavaPlugin { for (int r = 0; r < 16; r++) { int x = (r % 8) * 2, z = r < 8 ? 0 : 3; put(o, x, 0, z, cols[r].name().toLowerCase() + "_banner[rotation=" + r + "]"); - if (o.clone().add(x, 0, z).getBlock().getState() instanceof org.bukkit.block.Banner b) { - b.addPattern(new org.bukkit.block.banner.Pattern(DyeColor.BLACK, org.bukkit.block.banner.PatternType.STRIPE_TOP)); - b.addPattern(new org.bukkit.block.banner.Pattern(DyeColor.WHITE, org.bukkit.block.banner.PatternType.CREEPER)); + if (o.clone().add(x, 0, z).getBlock().getState() instanceof Banner b) { + b.addPattern(new Pattern(DyeColor.BLACK, PatternType.STRIPE_TOP)); + b.addPattern(new Pattern(DyeColor.WHITE, PatternType.CREEPER)); b.update(true, false); } } @@ -1798,10 +1837,10 @@ public class PPTest extends JavaPlugin { put(o, i * 2, 2, 2, "stone"); put(o, i * 2, 1, 2, "bell[attachment=ceiling,facing=" + DIRS[i] + "]"); put(o, i * 2, 0, 4, "decorated_pot[facing=" + DIRS[i] + "]"); - if (o.clone().add(i * 2, 0, 4).getBlock().getState() instanceof org.bukkit.block.DecoratedPot pot) { - pot.setSherd(org.bukkit.block.DecoratedPot.Side.FRONT, Material.HEART_POTTERY_SHERD); - pot.setSherd(org.bukkit.block.DecoratedPot.Side.LEFT, Material.SKULL_POTTERY_SHERD); - pot.setSherd(org.bukkit.block.DecoratedPot.Side.RIGHT, Material.ANGLER_POTTERY_SHERD); + if (o.clone().add(i * 2, 0, 4).getBlock().getState() instanceof DecoratedPot pot) { + pot.setSherd(DecoratedPot.Side.FRONT, Material.HEART_POTTERY_SHERD); + pot.setSherd(DecoratedPot.Side.LEFT, Material.SKULL_POTTERY_SHERD); + pot.setSherd(DecoratedPot.Side.RIGHT, Material.ANGLER_POTTERY_SHERD); pot.update(true, false); } put(o, i * 2, 0, 6, "lectern[facing=" + DIRS[i] + ",has_book=true]"); @@ -1842,15 +1881,15 @@ public class PPTest extends JavaPlugin { private void frames(Location o) { for (int x = -2; x <= 11; x++) - for (int y = 0; y <= 5; y++) w.getBlockAt(o.getBlockX() + x, o.getBlockY() + y, o.getBlockZ() + 4).setType(Material.STONE, false); + for (int y = 0; y <= 5; y++) put(o, x, y, 4, "stone"); BlockFace f = BlockFace.NORTH; for (int r = 0; r < 8; r++) frame(o, r, 4, 3, f, false, it(Material.DIAMOND_SWORD), r, true); ItemStack banner = it(Material.RED_BANNER); - org.bukkit.inventory.meta.BannerMeta bm = (org.bukkit.inventory.meta.BannerMeta) banner.getItemMeta(); - bm.addPattern(new org.bukkit.block.banner.Pattern(DyeColor.YELLOW, org.bukkit.block.banner.PatternType.STRIPE_TOP)); + BannerMeta bm = (BannerMeta) banner.getItemMeta(); + bm.addPattern(new Pattern(DyeColor.YELLOW, PatternType.STRIPE_TOP)); banner.setItemMeta(bm); ItemStack[] row3 = {it(Material.STONE), it(Material.OAK_STAIRS), it(Material.GRASS_BLOCK), it(Material.APPLE), - it(Material.TORCH), it(Material.CHEST), banner, eu.mhsl.minecraft.pixelpics.survival.CameraItems.createCamera(3), + it(Material.TORCH), it(Material.CHEST), banner, CameraItems.createCamera(3), it(Material.CREEPER_HEAD), it(Material.SHIELD)}; for (int i = 0; i < row3.length; i++) frame(o, i - 1, 3, 3, f, false, row3[i], 0, true); ItemStack photo = photoMap(), synth = synthMap(), vmap = vanillaMap(); @@ -1875,7 +1914,7 @@ public class PPTest extends JavaPlugin { BlockFace[] faces = {BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST, BlockFace.UP, BlockFace.DOWN}; for (int i = 0; i < items.length; i++) { int bx = 3 + i * 5, by = 4, bz = 3; - w.getBlockAt(o.getBlockX() + bx, o.getBlockY() + by, o.getBlockZ() + bz).setType(Material.STONE, false); + put(o, bx, by, bz, "stone"); for (BlockFace face : faces) frame(o, bx + face.getModX(), by + face.getModY(), bz + face.getModZ(), face, i == 2, items[i], 1, true); } @@ -1884,22 +1923,22 @@ public class PPTest extends JavaPlugin { /** Black/white concrete checkerboard: wall z=6 (x -6..17, y 0..11), ceiling y=11 and floor y=-1. */ private void camChecker(Location o) { for (int a = -6; a < 18; a++) - for (int b = 0; b < 12; b++) { - Material m = ((a + b) & 1) == 0 ? Material.WHITE_CONCRETE : Material.BLACK_CONCRETE; - w.getBlockAt(o.getBlockX() + a, o.getBlockY() + b, o.getBlockZ() + 6).setType(m, false); - } + for (int b = 0; b < 12; b++) put(o, a, b, 6, checker(a + b)); for (int a = -6; a < 18; a++) for (int c = -12; c < 6; c++) { - Material m = ((a + c) & 1) == 0 ? Material.WHITE_CONCRETE : Material.BLACK_CONCRETE; - w.getBlockAt(o.getBlockX() + a, o.getBlockY() + 11, o.getBlockZ() + c).setType(m, false); - w.getBlockAt(o.getBlockX() + a, o.getBlockY() - 1, o.getBlockZ() + c).setType(m, false); + put(o, a, 11, c, checker(a + c)); + put(o, a, -1, c, checker(a + c)); } } + private static String checker(int parity) { + return (parity & 1) == 0 ? "white_concrete" : "black_concrete"; + } + /** Black wall z=6 with one item frame (synthetic map, 1x1 block) facing north at cell (5,5,5). */ private void camFrame(Location o) { for (int a = -6; a < 18; a++) - for (int b = 0; b < 12; b++) w.getBlockAt(o.getBlockX() + a, o.getBlockY() + b, o.getBlockZ() + 6).setType(Material.BLACK_CONCRETE, false); + for (int b = 0; b < 12; b++) put(o, a, b, 6, "black_concrete"); frame(o, 5, 5, 5, BlockFace.NORTH, false, synthMap(), 0, true); } @@ -2002,9 +2041,7 @@ public class PPTest extends JavaPlugin { /** Black wall z=6 with a white vertical stripe (x=5) and a white horizontal stripe (y=5). */ private void camStripes(Location o) { for (int a = -6; a < 18; a++) - for (int b = 0; b < 12; b++) - w.getBlockAt(o.getBlockX() + a, o.getBlockY() + b, o.getBlockZ() + 6) - .setType(a == 5 || b == 5 ? Material.WHITE_CONCRETE : Material.BLACK_CONCRETE, false); + for (int b = 0; b < 12; b++) put(o, a, b, 6, a == 5 || b == 5 ? "white_concrete" : "black_concrete"); } private ItemFrame frame(Location o, int x, int y, int z, BlockFace face, boolean glow, ItemStack item, int rot, boolean visible) { @@ -2021,9 +2058,9 @@ public class PPTest extends JavaPlugin { return fr; } - private static ItemStack mapItem(org.bukkit.map.MapView view) { + private static ItemStack mapItem(MapView view) { ItemStack s = it(Material.FILLED_MAP); - org.bukkit.inventory.meta.MapMeta mm = (org.bukkit.inventory.meta.MapMeta) s.getItemMeta(); + MapMeta mm = (MapMeta) s.getItemMeta(); mm.setMapView(view); s.setItemMeta(mm); return s; @@ -2036,27 +2073,29 @@ public class PPTest extends JavaPlugin { File[] files = dir.listFiles(); if (files != null) for (File f : files) { - java.util.regex.Matcher m = java.util.regex.Pattern.compile("mapId_(\\d+)\\.mcmap").matcher(f.getName()); + Matcher m = MAP_FILE.matcher(f.getName()); if (m.matches()) best = Math.min(best, Integer.parseInt(m.group(1))); } int id = best == Integer.MAX_VALUE ? -1 : best; - @SuppressWarnings("deprecation") org.bukkit.map.MapView v = id < 0 ? null : Bukkit.getMap(id); + @SuppressWarnings("deprecation") MapView v = id < 0 ? null : Bukkit.getMap(id); if (v == null) return synthMap(); - if (v.getRenderers().stream().noneMatch(r -> r instanceof eu.mhsl.minecraft.pixelpics.utils.ImageMapRenderer)) { - byte[] idx = eu.mhsl.minecraft.pixelpics.utils.MapManager.readIndices(id); - if (idx != null) eu.mhsl.minecraft.pixelpics.utils.MapManager.attachView(v, new eu.mhsl.minecraft.pixelpics.utils.ImageMapRenderer(idx)); + if (v.getRenderers().stream().noneMatch(r -> r instanceof ImageMapRenderer)) { + byte[] idx = MapManager.readIndices(id); + if (idx != null) MapManager.attachView(v, new ImageMapRenderer(idx)); } getLogger().info("frames: photo map id " + id); return mapItem(v); } + private static final java.util.regex.Pattern MAP_FILE = java.util.regex.Pattern.compile("mapId_(\\d+)\\.mcmap"); + private ItemStack synthMap; /** Asymmetric test picture as a PixelPics map: red top-left, blue top-right, green bottom-left, black "F". */ private ItemStack synthMap() { if (synthMap != null) return synthMap; BufferedImage img = new BufferedImage(128, 128, BufferedImage.TYPE_INT_RGB); - java.awt.Graphics2D g = img.createGraphics(); + Graphics2D g = img.createGraphics(); g.setColor(java.awt.Color.WHITE); g.fillRect(0, 0, 128, 128); g.setColor(java.awt.Color.RED); @@ -2070,18 +2109,16 @@ public class PPTest extends JavaPlugin { g.fillRect(48, 24, 44, 14); g.fillRect(48, 56, 32, 12); g.dispose(); - org.bukkit.map.MapView v = Bukkit.createMap(w); - eu.mhsl.minecraft.pixelpics.utils.MapManager.attachView(v, new eu.mhsl.minecraft.pixelpics.utils.ImageMapRenderer(eu.mhsl.minecraft.pixelpics.utils.MapImageDither.dither(img))); + MapView v = Bukkit.createMap(w); + MapManager.attachView(v, new ImageMapRenderer(MapImageDither.dither(img))); return synthMap = mapItem(v); } /** A plain vanilla map whose server-side colours are written directly (diagonal colour bands + transparent corner). */ private ItemStack vanillaMap() { - org.bukkit.map.MapView v = Bukkit.createMap(w); + MapView v = Bukkit.createMap(w); try { - java.lang.reflect.Field wm = v.getClass().getDeclaredField("worldMap"); - wm.setAccessible(true); - Object data = wm.get(v); + Object data = mapData(v); byte[] colors = (byte[]) data.getClass().getField("colors").get(data); byte[] palette = {(byte) (18 * 4 + 2), (byte) (7 * 4 + 2), (byte) (12 * 4 + 1), (byte) (29 * 4 + 2)}; for (int y = 0; y < 128; y++) diff --git a/tools/update-cem-models.sh b/tools/update-cem-models.sh index 2823d11..90e74a7 100755 --- a/tools/update-cem-models.sh +++ b/tools/update-cem-models.sh @@ -1,12 +1,7 @@ #!/usr/bin/env bash -# Updates the bundled CEM entity-model set (the only asset PixelPics vendors itself). -# -# Source: the CEM Template Loader data (Ewan Howell / wynem) — vanilla Java entity models as JSON, -# already correctly posed. These ultimately mirror Mojang's hardcoded Java EntityModel classes. -# -# NOTE: this set is now only the FALLBACK (rest-pose geometry) for entity types missing from the vanilla -# client dump (+ the elytra fallback). Mob AND block-entity models come from that dump — regenerate it -# with tools/update-vanilla-models.sh on every Minecraft update. +# Updates the vendored CEM template model set (Ewan Howell / wynem: vanilla Java entity models as JSON). It is +# only the rest-pose FALLBACK for entity types missing from the vanilla client dump (+ the elytra fallback); +# mob and block-entity models come from tools/update-vanilla-models.sh. # # Usage: tools/update-cem-models.sh # update if newer # tools/update-cem-models.sh --force # always overwrite @@ -20,21 +15,20 @@ trap 'rm -f "$TMP"' EXIT echo "Fetching $URL ..." curl -fsSL "$URL" -o "$TMP" -# Validate + read versions/counts. -read -r NEW_VER NEW_COUNT < <(python3 -c " -import json,sys -d=json.load(open('$TMP')) -assert 'models' in d and len(d['models'])>50, 'unexpected structure' -print(d.get('version','?'), len(d['models'])) -") - -CUR_VER="(none)" -if [ -f "$DEST" ]; then - CUR_VER="$(python3 -c "import json;print(json.load(open('$DEST')).get('version','?'))" 2>/dev/null || echo '?')" -fi +# Validates the download, prints " ". +read -r NEW_VER NEW_COUNT CUR_VER < <(python3 - "$TMP" "$DEST" <<'PY' +import json, os, sys +d = json.load(open(sys.argv[1])) +assert 'models' in d and len(d['models']) > 50, 'unexpected structure' +cur = '(none)' +if os.path.exists(sys.argv[2]): + try: cur = json.load(open(sys.argv[2])).get('version', '?') + except Exception: cur = '?' +print(d.get('version', '?'), len(d['models']), cur) +PY +) echo "Current: $CUR_VER Remote: $NEW_VER ($NEW_COUNT models)" - if [ "${1:-}" != "--force" ] && [ "$CUR_VER" = "$NEW_VER" ]; then echo "Already up to date. (use --force to overwrite)" exit 0 diff --git a/tools/update-vanilla-models.sh b/tools/update-vanilla-models.sh index 2c17758..439c977 100755 --- a/tools/update-vanilla-models.sh +++ b/tools/update-vanilla-models.sh @@ -1,14 +1,13 @@ #!/usr/bin/env bash -# Regenerates the vanilla entity-model dump straight from a Minecraft CLIENT jar (26.x+ ships -# unobfuscated, so no mappings are needed). This is the primary entity-model source of PixelPics: -# every mob is posed by its own renderer's setupAnim, and new entities/layers/baby/variant models of -# the new version are picked up automatically. Run it once per Minecraft update. +# Regenerates the vanilla entity-model dump from a Minecraft CLIENT jar (26.x+ ships unobfuscated, no mappings +# needed). This is PixelPics' primary entity-model source; run it once per Minecraft update. # -# Needs the version installed in the launcher (~/.minecraft/versions//.json + jar + libraries). +# Needs the version installed in the launcher (~/.minecraft/versions//.json + jar + libraries) and a +# JDK (the dump runs as a source-file program): $JAVA, $JAVA_HOME or the JDK of `javac` on the PATH. # -# Usage: tools/update-vanilla-models.sh 26.3 # -> src/main/resources/cem/ (bundled; rebuild jar) -# tools/update-vanilla-models.sh 26.4 # ->

/vanilla_entity_models.json -# # (overrides the bundled copy, no rebuild) +# Usage: tools/update-vanilla-models.sh 26.3 # -> src/main/resources/cem/ (bundled; rebuild jar) +# tools/update-vanilla-models.sh 26.4 # -> /vanilla_entity_models.json +# # (overrides the bundled copy, no rebuild) set -euo pipefail VER="${1:?usage: $0 [output-dir]}" @@ -17,9 +16,19 @@ OUT_DIR="${2:-$ROOT/src/main/resources/cem}" MC="${MINECRAFT_DIR:-$HOME/.minecraft}" JSON="$MC/versions/$VER/$VER.json" JAR="$MC/versions/$VER/$VER.jar" -JAVA="${JAVA:-$(command -v java)}" [[ -f "$JSON" && -f "$JAR" ]] || { echo "Version $VER not found under $MC/versions (start it once in the launcher)"; exit 1; } +NEED="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("javaVersion", {}).get("majorVersion", 0))' "$JSON")" +is_jdk() { # a JDK (jdk.compiler) at least as new as the game's Java version + [[ -n "$1" && -x "$1" ]] && "$1" --list-modules 2>/dev/null | grep -q '^jdk.compiler@' && + (( $("$1" --version 2>/dev/null | head -1 | grep -oE '[0-9]+' | head -1) >= NEED )) +} +JAVAC_JAVA="$(command -v javac >/dev/null && echo "$(dirname "$(readlink -f "$(command -v javac)")")/java" || true)" +for j in "${JAVA:-}" "${JAVA_HOME:+$JAVA_HOME/bin/java}" "$JAVAC_JAVA" "$(command -v java || true)"; do + if is_jdk "$j"; then JDK_JAVA="$j"; break; fi +done +[[ -n "${JDK_JAVA:-}" ]] || { echo "No JDK >= $NEED found (a JRE can't launch source files): set JAVA or JAVA_HOME"; exit 1; } + # Classpath: client jar + all libraries listed in the version json. CP="$(python3 - "$JSON" "$MC" "$JAR" <<'EOF' import json, os, sys @@ -34,19 +43,21 @@ print(":".join(cp)) EOF )" -TMP="$(mktemp --suffix=.json)" -trap 'rm -f "$TMP"; rm -rf "${WORK:-}"' EXIT -# Run from a scratch dir: the game's logger writes a logs/ folder into the working directory. +# Scratch dir as working directory: the game's logger writes a logs/ folder there. WORK="$(mktemp -d)" -(cd "$WORK" && "$JAVA" -cp "$CP" "$ROOT/tools/VanillaModelDump.java" "$TMP" 2>&1) | grep -E "STDOUT|Exception" | sed 's/.*STDOUT\]: //' -python3 -c " -import json; d = json.load(open('$TMP')) +trap 'rm -rf "$WORK"' EXIT +OUT="$WORK/vanilla_entity_models.json" +if ! (cd "$WORK" && "$JDK_JAVA" -cp "$CP" "$ROOT/tools/VanillaModelDump.java" "$OUT" > "$WORK/dump.log" 2>&1); then + cat "$WORK/dump.log"; echo "Dump failed"; exit 1 +fi +grep -E "STDOUT|Exception" "$WORK/dump.log" | sed 's/.*STDOUT\]: //' || true +python3 - "$OUT" <<'EOF' +import json, sys +d = json.load(open(sys.argv[1])) assert len(d['models']) > 100, 'dump looks incomplete' -print('version', d.get('version'), '-', len(d['models']), 'models,', len(d['entities']), 'entity types')" +print('version', d.get('version'), '-', len(d['models']), 'models,', len(d['entities']), 'entity types') +EOF mkdir -p "$OUT_DIR" -mv "$TMP" "$OUT_DIR/vanilla_entity_models.json" -chmod 644 "$OUT_DIR/vanilla_entity_models.json" -rm -rf "$WORK" -trap - EXIT +install -m 644 "$OUT" "$OUT_DIR/vanilla_entity_models.json" echo "Wrote $OUT_DIR/vanilla_entity_models.json" echo "Note: the runtime resource pack must come from the same version (incl. assets/minecraft/font for text)."