From 02e052787ceda43c5d5b9ca7b195297eacbbb7b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Thu, 5 Nov 2026 03:24:30 +0100 Subject: [PATCH] simplify tools: shared harness setup (pipeline, block proxy, render job, framing, record copy, label cells, contact sheets) for the standalone renderers and the benchmark, block-entity scenes as name/yaw records, one dump pipeline for mob and avatar renderers with a shared submit helper, transform record and jem/entry/subset helpers, posed block entities from direct layer constants, pptest scenes registered once with settle-tick and resolution tables and put() for all blocks, jdk detection and full error log in the dump script, pptest jar rebuilt instead of updated, gradle task printing the tool classpath, comments trimmed --- build.gradle | 6 + tools/BlockEntityTestRender.java | 515 ++++------ tools/EntityBenchmark.java | 115 +-- tools/EntityTestRender.java | 188 +--- tools/Harness.java | 170 ++++ tools/ObjectTestRender.java | 103 +- tools/VanillaModelDump.java | 1103 ++++++++++------------ tools/edgecase-plugin/build.sh | 13 +- tools/edgecase-plugin/pptest/PPTest.java | 835 ++++++++-------- tools/update-cem-models.sh | 36 +- tools/update-vanilla-models.sh | 51 +- 11 files changed, 1449 insertions(+), 1686 deletions(-) create mode 100644 tools/Harness.java 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)."