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 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). * *

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}). * *

Run via {@code tools/update-vanilla-models.sh }, or directly: * {@code java -cp tools/VanillaModelDump.java } */ public class VanillaModelDump { static final Map BAKED = new IdentityHashMap<>(); static Map roots; public static void main(String[] args) throws Exception { 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) { ModelPart p = super.bakeLayer(loc); BAKED.put(p, loc); 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); Class renderers = Class.forName("net.minecraft.client.renderer.entity.EntityRenderers"); Field provField = renderers.getDeclaredField("PROVIDERS"); provField.setAccessible(true); @SuppressWarnings("unchecked") Map, EntityRendererProvider> providers = (Map, EntityRendererProvider>) provField.get(null); Map out = new TreeMap<>(); Map entities = new TreeMap<>(); List failed = new ArrayList<>(); for (Map.Entry, EntityRendererProvider> e : providers.entrySet()) { String key = BuiltInRegistries.ENTITY_TYPE.getKey(e.getKey()).getPath(); EntityRenderer r; try { r = e.getValue().create(ctx); } catch (Throwable t) { failed.add(key + " (renderer: " + t + ")"); continue; } dumpRenderer(key, r, true, out, entities, failed); } // Players & mannequins use the avatar renderers (one per skin model type: wide/slim). Map avatars = (Map) renderers .getMethod("createAvatarRenderers", EntityRendererProvider.Context.class) .invoke(null, ctx); for (Map.Entry a : avatars.entrySet()) { String key = String.valueOf(a.getKey()).toLowerCase().equals("slim") ? "player_slim" : "player"; dumpRenderer(key, (EntityRenderer) a.getValue(), false, out, entities, failed); } // 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 { out.putIfAbsent(name, entry(jem(le.getValue(), List.of(), le.getValue().bakeRoot(), null))); } catch (Throwable t) { failed.add(name + " (layer: " + t + ")"); } } dumpPosedBlockEntities(set, out, failed); JsonObject root = new JsonObject(); root.addProperty("version", SharedConstants.getCurrentVersion().name()); JsonObject models = new JsonObject(); out.forEach(models::add); root.add("models", models); JsonObject ents = new JsonObject(); entities.forEach(ents::add); root.add("entities", ents); root.add("variants", registryVariants()); JsonObject rts = new JsonObject(); RENDER_TYPES.forEach(rts::add); root.add("renderTypes", rts); try (FileWriter w = new FileWriter(args.length > 0 ? args[0] : "vanilla_entity_models.json")) { new GsonBuilder().create().toJson(root, w); } System.out.println("models: " + out.size()); for (String f : failed) System.out.println("FAILED " + f); } /** * 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; JsonObject out = new JsonObject(); for (Object layer : ls) { Class lc = layer.getClass(); while (lc.getSimpleName().isEmpty()) lc = lc.getSuperclass(); // anonymous layer -> its base class String name = lc.getSimpleName(); 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"); drawn = false; } if (!out.has(name) || drawn) out.addProperty(name, drawn); } info.add("layers", out); } /** 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 = freshState(r, List.of()); if (state == null) return null; boolean[] submitted = {false}; Boolean ok = submit(layer, state, recordingCollector((m, a) -> { if (m.getName().startsWith("submit")) submitted[0] = true; })); 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(BiConsumer onCall) { Class collector; try { collector = Class.forName("net.minecraft.client.renderer.SubmitNodeCollector"); } catch (ClassNotFoundException e) { throw new IllegalStateException(e); } return Proxy.newProxyInstance( VanillaModelDump.class.getClassLoader(), new Class[] {collector}, (px, m, a) -> { if (m.getName().equals("order")) return px; onCall.accept(m, a); Class rt = m.getReturnType(); if (rt == boolean.class) return false; if (rt.isPrimitive() && rt != void.class) return 0; return null; }); } /** * 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 type name -> pipeline properties ({@link #renderTypeJson}); written as root "renderTypes". */ static final Map RENDER_TYPES = new TreeMap<>(); /** * {@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 on an emissive layer the default state leaves off (creaking eyes while active). */ static final String[][] LAYER_STATES = { {"eyes_glowing", "eyesGlowing", "true"}, }; /** One recorded {@code submitModel} call: model, render type, vertex colour (ARGB, -1 = white). */ record Submission(Model model, Object renderType, int color) {} /** 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<>(); Object proxy = recordingCollector((m, a) -> { if (!m.getName().equals("submitModel") || a == null) return; Model model = null; Object rt = null; for (Object arg : a) { if (arg instanceof Model mm && model == null) model = mm; 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)); }); submit(layer == null ? r : layer, state, proxy); return out; } /** Name of a RenderType (registered in {@link #RENDER_TYPES} with its pipeline properties). */ static String renderTypeName(Object renderType) { String name = (String) fieldValue(renderType, "name"); if (name == null) return null; RENDER_TYPES.computeIfAbsent(name, n -> renderTypeJson(renderType)); return name; } /** The render type a model draws with by default ({@code Model.renderType(texture)}). */ @SuppressWarnings({"unchecked", "rawtypes"}) static String renderTypeName(Model model) { try { return renderTypeName(((Model) model).renderType(Identifier.withDefaultNamespace("textures/entity/dummy.png"))); } catch (Throwable t) { return null; } } /** * 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(); RenderPipeline p = (RenderPipeline) fieldValue(fieldValue(renderType, "state"), "pipeline"); if (p == null) return o; o.addProperty("pipeline", p.getLocation().getPath()); 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 (ColorTargetState c : p.getColorTargetStates()) { if (c == null || c.blendFunction().isEmpty()) continue; 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 a render type samples as Sampler0, or null. */ @SuppressWarnings("unchecked") static String renderTypeTexture(Object renderType) { Object setup = fieldValue(renderType, "state"); Map tex = setup == null ? null : (Map) fieldValue(setup, "textures"); Object binding = tex == null ? null : tex.get("Sampler0"); Object id = binding == null ? null : fieldValue(binding, "location"); 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$", ""); } static boolean emissive(Object renderType) { JsonObject o = RENDER_TYPES.get(renderTypeName(renderType)); return o != null && o.has("emissive") && o.get("emissive").getAsBoolean(); } /** * Render info of one renderer: the render type of every model the default state submits (returned, written * 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 (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 (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) 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()) for (Field f : c.getDeclaredFields()) equipment |= f.getType().getSimpleName().equals("EquipmentLayerRenderer"); if (!equipment) continue; Map> mine = new LinkedHashMap<>(); collectModels(layer, "", mine, 0); for (Model m : mine.values()) types.put(m, armor); } } Map, String> names = new IdentityHashMap<>(); for (Map.Entry> m : found.entrySet()) names.putIfAbsent(m.getValue(), modelName(key, m.getKey())); JsonArray emissive = new JsonArray(); Set seen = new HashSet<>(); if (layers instanceof List ls) { for (Object layer : ls) { 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; 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()); if (name == null || tex == null || !seen.add(name + "|" + tex)) continue; JsonObject e = new JsonObject(); e.addProperty("model", name); 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[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}) { Object st = freshState(r, base); setFloat(st, "ageInTicks", age); int a = -2; for (Submission s2 : emissiveSubmissions(r, layer, st)) if (s2.model() == sub.model()) a = s2.color() >>> 24; if (a != (sub.color() >>> 24)) animated = true; } if (animated) e.addProperty("animated", true); Object inv = freshState(r, base); setBoolean(inv, "isInvisible", true); if (emissiveSubmissions(r, layer, inv).stream().anyMatch(s2 -> s2.model() == sub.model())) e.addProperty("invisible", true); JsonObject variants = textureVariants(r, layer, sub.model(), base, tex); if (!variants.isEmpty()) e.add("textures", variants); emissive.add(e); } } } if (!emissive.isEmpty()) info.add("emissive", emissive); JsonObject stateTex = new JsonObject(); String def = textureOf(r, st0); for (String[] a : TEXTURE_STATES) { if (!assignmentApplies(st0, a)) continue; String t = textureOf(r, freshState(r, List.of(a))); if (t != null && !t.equals(def)) stateTex.addProperty(a[0], t); } if (!stateTex.isEmpty()) info.add("stateTextures", stateTex); return types; } static List emissiveSubmissions(EntityRenderer r, Object layer, Object state) { if (state == null) return List.of(); return submissions(r, layer, state).stream().filter(s -> emissive(s.renderType())).toList(); } /** Textures an emissive layer uses per constant of the render state's enum fields (when they differ). */ static JsonObject textureVariants(EntityRenderer r, Object layer, Model model, List base, String def) { JsonObject out = new JsonObject(); Object probe = freshState(r, base); for (Field f : probe.getClass().getFields()) { if (Modifier.isStatic(f.getModifiers()) || !f.getType().isEnum()) continue; for (Object c : f.getType().getEnumConstants()) { Object st = freshState(r, base); try { f.set(st, c); } catch (Throwable t) { continue; } for (Submission s : emissiveSubmissions(r, layer, st)) { if (s.model() != model) continue; String t = renderTypeTexture(s.renderType()); if (t != null && !t.equals(def)) out.addProperty(((Enum) c).name().toLowerCase(), t); } } } return out; } /** The renderer's body texture for a state ({@code getTextureLocation}), or null. */ static String textureOf(EntityRenderer r, Object state) { if (state == null) return null; try { Method gt = declaredMethod(r, m -> m.getName().equals("getTextureLocation") && m.getParameterCount() == 1); if (gt == null) return null; Object id = gt.invoke(r, state); return id == null ? null : texturePath(id); } catch (Throwable t) { return null; } } /** * 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}) with their client * assets: {@code variants: {: {: {asset_id, baby_asset_id, assets, baby_assets, model}}}}. */ static JsonObject registryVariants() throws IOException { JsonObject out = new JsonObject(); 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 (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"}) { if (v.has(k)) keep.add(k, v.get(k)); } if (keep.isEmpty()) continue; if (!out.has(type)) out.add(type, new JsonObject()); out.getAsJsonObject(type).add(m.group(2), keep); } } return out; } /** * 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; 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"); if (f.getType().isEnum()) { for (Object c : f.getType().getEnumConstants()) if (((Enum) c).name().equals("CROSSED")) f.set(state, c); } } catch (Throwable ignored) { } if (hasField(state, "elytraRotX") && hasField(state, "elytraRotZ")) { setFloat(state, "elytraRotX", (float) (Math.PI / 12)); setFloat(state, "elytraRotZ", (float) (-Math.PI / 12)); } } /** Writes every collected model of one entity type. */ static void emit( String key, Map> found, Map textures, Object state, Map, String> renderTypes, Map out, List failed) { for (Map.Entry> m : found.entrySet()) { String name = modelName(key, m.getKey()); if (out.containsKey(name)) continue; JsonObject jem = dump(m.getValue(), state, name, failed); 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)); } } 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) { if (isMainField(field)) return key; return field.equals("babyModel") ? key + "/baby" : key + "/" + field; } /** * A render layer's models (prefixed by the layer class; equipment layers by their equipment layer type, * e.g. {@code equip.horse_saddle}) plus the texture the layer draws them with, when it holds one. */ static void collectLayer(Object layer, Map> found, Map textures) { String prefix = layer.getClass().getSimpleName() + "."; 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 + "."; } Map> mine = new LinkedHashMap<>(); collectModels(layer, prefix, mine, 0); // Texture identifiers held by the layer (static constants or instance fields): baby vs adult. String adult = null, baby = null; for (Class c = layer.getClass(); c != null && c != Object.class; c = c.getSuperclass()) { for (Field fl : c.getDeclaredFields()) { if (!fl.getType().getSimpleName().equals("Identifier")) continue; try { fl.setAccessible(true); Object v = fl.get(Modifier.isStatic(fl.getModifiers()) ? null : layer); if (v == null) continue; if (fl.getName().toLowerCase().contains("baby")) { if (baby == null) baby = texturePath(v); } else if (adult == null) adult = texturePath(v); } catch (Throwable ignored) { } } } for (Map.Entry> m : mine.entrySet()) { if (found.containsValue(m.getValue())) continue; found.put(m.getKey(), m.getValue()); String tex = m.getKey().toLowerCase().contains("baby") ? baby : adult; if (tex != null) textures.put(m.getKey(), tex); } } static boolean hasPrefix(Map> found, String prefix) { for (String k : found.keySet()) if (k.startsWith(prefix)) return true; return false; } /** * 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) { 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; } 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 (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 * AnimationState that many ticks before the frame (static keyframe pose). The snapshot maps the entity's * Bukkit state onto these names; the dump only keeps poses that change parts or the renderer transform. */ static final String[][] STATE_POSES = { {"sleeping", "isSleeping", "true"}, {"sleeping", "pose", "SLEEPING", "unless:isSleeping"}, // LivingEntityRenderer: lying in bed {"sitting", "isSitting", "true"}, {"sitting", "sitAmount", "1"}, // panda {"sitting", "pose", "SITTING"}, // parrot (ParrotModel.Pose), camel {"sitting", "sitPoseAnimationState", "anim:0"}, // camel {"crouching", "isCrouching", "true"}, {"crouching", "crouchAmount", "3"}, // fox (Fox.getCrouchAmount max) {"crouching", "pose", "CROUCHING"}, {"crouching", "elytraRotX", "0.6981317"}, // ElytraAnimationState crouching targets {"crouching", "elytraRotY", "0.08726646"}, {"crouching", "elytraRotZ", "-0.7853982"}, {"pouncing", "isPouncing", "true"}, // fox leap {"faceplanted", "isFaceplanted", "true"}, // fox stuck in snow {"lying", "lieDownAmount", "1"}, // cat {"lying", "lieDownAmountTail", "1"}, {"head_up", "relaxStateOneAmount", "1"}, // cat {"on_player", "isLyingOnTopOfSleepingPlayer", "true"}, // cat on its sleeping owner {"on_back", "lieOnBackAmount", "1"}, // panda {"resting", "isResting", "true"}, // bat hanging {"resting", "restAnimationState", "anim:0"}, {"swimming", "isVisuallySwimming", "true"}, {"swimming", "swimAmount", "1"}, {"swimming", "isInWater", "true"}, {"swimming", "pose", "SWIMMING"}, {"crawling", "isVisuallySwimming", "true"}, // swimming pose out of water {"crawling", "swimAmount", "1"}, {"crawling", "pose", "SWIMMING"}, {"fall_flying", "isFallFlying", "true"}, {"fall_flying", "fallFlyingTimeInTicks", "20"}, {"fall_flying", "pose", "FALL_FLYING"}, {"fall_flying", "elytraRotX", "0.34906584"}, // level glide {"fall_flying", "elytraRotZ", "-1.5707964"}, {"in_water", "isInWater", "true"}, // fish lie on their side only out of water {"standing", "pose", "STANDING"}, // parrot on the ground (the render-state default is FLYING) {"on_shoulder", "pose", "ON_SHOULDER"}, // parrot on a player's shoulder (ParrotOnShoulderLayer) {"party", "pose", "PARTY"}, // dancing parrot {"digging", "pose", "DIGGING"}, {"digging", "diggingAnimationState", "anim:72"}, // sniffer: head and body down in the ground {"angry", "isAngry", "true"}, // wolf: tail straight (WolfModel), raised (Wolf.getTailAngle) {"angry", "tailAngle", "1.5393804"}, }; /** State poses that combine (every subset of a group with 2+ applicable members is dumped too). */ 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}), 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 (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) { Object probe; try { probe = r.createRenderState(); } catch (Throwable t) { return; } // 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 extracts EMPTY (spear, aggressive bow/trident aside). List> items = new ArrayList<>(); 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(List.of(new String[] {f[0], f[1], "true"})); } List> combos = new ArrayList<>(); 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 (isEnumField(probe, "armPose")) { Object idle = fieldValue(freshState(r, List.of()), "armPose"); 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 riding = new ArrayList<>(combo); riding.addAll(passenger); combos.add(riding); } } } // State poses: alone, grouped, each with every held-item subset. 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) 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 (List> held : subsets(items, 0)) { List c = new ArrayList<>(base); 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. 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 = isMainField(f); if (main && bm.getValue() != mainModel(found)) continue; boolean baby = f.toLowerCase().contains("baby"); List> layerModels = layerModels(found, baby); JsonObject poses = new JsonObject(); JsonObject transforms = new JsonObject(); Model model = bm.getValue(); // 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, base, poseOf(model, freshState(r, c, babyState)), false); } Map baseAll = withLayers(base, layerModels, freshState(r, List.of(), babyState)); 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() .reduce((x, y) -> x + "+" + y).orElse(""); boolean withItems = c.stream().anyMatch(a -> a[0].startsWith("item_")); if (withItems && !emitted.contains(states)) continue; Map v = withLayers( poseOf(model, freshState(r, c, babyState)), layerModels, freshState(r, c, babyState)); 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) { if (!poses.isEmpty()) info.add("poses", poses); 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. 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(f.equals("babyModel") ? "baby" : f, mp); } } for (Model lm : found.values()) poseOf(lm, freshState(r, List.of())); } /** * Adds the pose {@code v} of combo {@code c} as a diff against {@code base}; returns its name, or null when * nothing differs (and {@code keepEmpty} is false — state poses that only move the renderer transform). */ static String addPose( 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()) { float[] b = base.get(e.getKey()); float[] x = e.getValue(); boolean same = b != null; for (int i = 0; same && i < x.length; i++) same = Math.abs(b[i] - x[i]) < 1e-4f; if (same) continue; JsonObject po = new JsonObject(); boolean moved = b == null; for (int i = 0; !moved && i < 6; i++) moved = Math.abs(b[i] - x[i]) >= 1e-4f; if (moved) { // 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); 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; 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, …). */ static Map withLayers(Map parts, List> layers, Object state) { if (parts == null) return null; Map out = new LinkedHashMap<>(parts); for (Model lm : layers) { Map lp = poseOf(lm, state); if (lp == null) continue; lp.remove("root"); lp.forEach(out::putIfAbsent); } return out; } /** * 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, …), 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). */ static void addRotationBindings(JsonObject info, Map> found, EntityRenderer r) { Object probe; try { probe = r.createRenderState(); } catch (Throwable t) { return; } Model main = mainModel(found); if (main == null) return; List> layers = layerModels(found, false); JsonObject out = new JsonObject(); for (Field f : probe.getClass().getFields()) { if (Modifier.isStatic(f.getModifiers())) continue; boolean rot = f.getType().getSimpleName().equals("Rotations"); if (!rot && !(f.getName().equals("xRot") && f.getType() == float.class)) continue; try { Object st0 = freshState(r, List.of()); Map base = withLayers(poseOf(main, st0), layers, freshState(r, List.of())); if (base == null) continue; float[] def = new float[3]; if (rot) { Object v = f.get(st0); for (int j = 0; j < 3; j++) def[j] = (float) v.getClass().getMethod(new String[] {"x", "y", "z"}[j]).invoke(v); } else { def[0] = f.getFloat(st0); } Map coeff = new TreeMap<>(); for (int j = 0; j < (rot ? 3 : 1); j++) { Object st = freshState(r, List.of()); if (rot) { float[] d = def.clone(); d[j] += 10; f.set(st, f.getType().getConstructor(float.class, float.class, float.class).newInstance(d[0], d[1], d[2])); } else { f.setFloat(st, def[0] + 10); } Map v = withLayers(poseOf(main, st), layers, st); if (v == null) continue; for (Map.Entry e : v.entrySet()) { float[] b = base.get(e.getKey()); if (b == null) continue; for (int i = 0; i < 3; i++) { float c = (e.getValue()[3 + i] - b[3 + i]) / 10f; if (Math.abs(c) < 1e-4f) continue; coeff.computeIfAbsent(e.getKey(), k -> new float[9])[i * 3 + j] = c; } } } // Pitch tilts whole bodies only (dolphin, strider, nautilus): head look is left out, PixelPics has // no head yaw to go with it. if (!rot) { Set heads = new HashSet<>(); headParts(main.root(), "root", false, heads); for (Model lm : layers) headParts(lm.root(), "root", false, heads); coeff.keySet().removeAll(heads); } if (coeff.isEmpty()) continue; JsonObject fb = new JsonObject(); fb.add("default", rot ? arr(def) : arr(def[0])); JsonObject parts = new JsonObject(); coeff.forEach((p, c) -> parts.add(p, arr(c))); fb.add("parts", parts); out.add(f.getName(), fb); } catch (Throwable t) { System.out.println("rotation binding " + f.getName() + " (" + r.getClass().getSimpleName() + "): " + t); } } poseOf(main, freshState(r, List.of())); for (Model lm : layers) poseOf(lm, freshState(r, List.of())); if (!out.isEmpty()) info.add("rotationBindings", out); } /** Names of every part named "*head*" and of everything below one. */ static void headParts(ModelPart p, String name, boolean inHead, Set out) { inHead |= name.contains("head"); if (inHead) out.add(name); 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; 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; } return type == boolean.class || type == float.class || type == int.class; } /** Type of a public field, or null. */ static Class fieldType(Object o, String f) { try { return o.getClass().getField(f).getType(); } catch (NoSuchFieldException e) { return null; } } static boolean hasField(Object o, String f) { 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); } /** 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(); idleTweaks(st); for (String[] t : sets) assign(st, t); if (baby) babyState(st); return st; } catch (Throwable t) { return null; } } static void babyState(Object st) throws ReflectiveOperationException { if (hasField(st, "isBaby")) st.getClass().getField("isBaby").setBoolean(st, true); if (hasField(st, "ageScale")) st.getClass().getField("ageScale").setFloat(st, 0.5f); } /** Applies one {@code {name, field, value[, cond]}} assignment when it applies (see {@link #STATE_POSES}). */ @SuppressWarnings({"unchecked", "rawtypes"}) static void assign(Object st, String[] t) throws ReflectiveOperationException { if (!assignmentApplies(st, t)) return; Field f = st.getClass().getField(t[1]); if (t[2].startsWith("anim:")) { f.getType().getMethod("start", int.class).invoke(f.get(st), -Integer.parseInt(t[2].substring(5))); } else if (f.getType() == boolean.class) f.setBoolean(st, Boolean.parseBoolean(t[2])); else if (f.getType() == float.class) f.setFloat(st, Float.parseFloat(t[2])); else if (f.getType() == int.class) f.setInt(st, Integer.parseInt(t[2])); 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, rotation, visibility and scale, by name. */ @SuppressWarnings({"unchecked", "rawtypes"}) static Map poseOf(Model model, Object state) { if (state == null) return null; try { ((Model) model).setupAnim(state); } catch (Throwable t) { return null; } Map out = new LinkedHashMap<>(); collectPose(model.root(), "root", 0, 0, 0, out); return out; } 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[] { -ax, 24 - ay, az, (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 }); children(p).forEach((n, c) -> collectPose(c, n, ax, ay, az, out)); } static void setFloat(Object o, String field, float v) { try { Field f = o.getClass().getField(field); if (f.getType() == float.class) f.setFloat(o, v); } catch (Throwable ignored) { } } 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) { } } /** * 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) { Object state; try { state = r.createRenderState(); idleTweaks(state); if (assigns != null) { for (String[] a : assigns) assign(state, a); if (baby) babyState(state); } } catch (Throwable t) { return null; } setFloat(state, "bodyRot", yaw); setFloat(state, "yRot", yaw); setFloat(state, "xRot", pitch); float[][] first = new float[1][]; 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 (assigns != null && first[0] != null) { try { Vec3 off = ((EntityRenderer) r).getRenderOffset((EntityRenderState) state); if (off != null && off.lengthSqr() > 0) { first[0] = new Matrix4f().set(first[0]) .translateLocal((float) off.x, (float) off.y, (float) off.z) .get(new float[16]); } } catch (Throwable ignored) { // no render offset for this state } } return first[0]; } /** Render scale (the renderer's own {@code scale(state, poseStack)}) and default adult/baby textures. */ static JsonObject entityInfo(EntityRenderer r, Object state) { JsonObject o = new JsonObject(); if (state == null) return o; try { Method m = declaredMethod(r, dm -> dm.getName().equals("scale") && dm.getParameterCount() == 2 && dm.getParameterTypes()[1].getSimpleName().equals("PoseStack")); if (m != null) { PoseStack ps = new PoseStack(); m.invoke(r, state, ps); 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 { 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; } /** Finds Model instances in the object's fields (also inside records / maps / ArmorModelSet-like holders). */ static void collectModels(Object o, String prefix, Map> found, int depth) { if (o == null || depth > 2) return; for (Class c = o.getClass(); c != null && c != Object.class; c = c.getSuperclass()) { for (Field f : c.getDeclaredFields()) { if (Modifier.isStatic(f.getModifiers())) continue; Object v; try { f.setAccessible(true); v = f.get(o); } catch (Throwable t) { continue; } if (v == null) continue; String n = prefix + f.getName(); if (v instanceof Model m) { if (!found.containsValue(m)) found.put(n, m); } else if (v instanceof Map map) { for (Map.Entry me : map.entrySet()) { String mk = n + "." + String.valueOf(me.getKey()).toLowerCase(); if (me.getValue() instanceof Model m) { if (!found.containsValue(m)) found.put(mk, m); } else if (me.getValue() != null) { collectModels(me.getValue(), mk + ".", found, depth + 1); // e.g. adult/baby pairs } } } else if (v.getClass().isRecord() || v.getClass().getSimpleName().endsWith("ModelSet")) { collectModels(v, n + ".", found, depth + 1); } } } } /** Path of child names from {@code from} down to the identical part {@code target}, or null. */ static List findPath(ModelPart from, ModelPart target, List path) { if (from == target) return path; for (Map.Entry c : children(from).entrySet()) { List p = new ArrayList<>(path); p.add(c.getKey()); List r = findPath(c.getValue(), target, p); if (r != null) return r; } 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 { Field f = c.getDeclaredField(name); f.setAccessible(true); return f.get(o); } catch (NoSuchFieldException ignored) { } catch (Throwable t) { return null; } } 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(); ModelLayerLocation loc = BAKED.get(root); List subPath = List.of(); if (loc == null) { // Some models keep a CHILD of the baked layer root as their root (e.g. allay, vex, frog). for (Map.Entry b : BAKED.entrySet()) { List path = findPath((ModelPart) b.getKey(), root, new ArrayList<>()); if (path != null) { loc = b.getValue(); subPath = path; break; } } } if (loc == null) { failed.add(name + " (no layer)"); return null; } boolean posed = false; if (state != null) { try { ((Model) model).setupAnim(state); posed = true; } catch (Throwable t) { // layer model expecting another state type } } if (!posed) { for (Method m : model.getClass().getMethods()) { if (!m.getName().equals("setupAnim") || m.getParameterCount() != 1) continue; try { m.invoke(model, m.getParameterTypes()[0].getDeclaredConstructor().newInstance()); posed = true; break; } catch (Throwable ignored) { } } } if (!posed) failed.add(name + " (rest pose: setupAnim failed)"); return jem(roots.get(loc), subPath, root, loc.model().getPath() + "#" + loc.layer()); } /** 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"); 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); 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); return jem; } /** One part: absolute pivot = parent pivot + this part's (posed) offset; boxes absolute. */ @SuppressWarnings("unchecked") static JsonObject part(String name, PartDefinition def, ModelPart p, float px, float py, float pz) { float ax = px + p.x, ay = py + p.y, az = pz + p.z; JsonObject o = new JsonObject(); o.addProperty("part", name); o.add("pivot", arr(-ax, 24 - ay, az)); // CEM frame = Java rotated 180° about Z: x/y rotations flip sign, z keeps it. o.add("rotate", arr( (float) Math.toDegrees(-p.xRot), (float) Math.toDegrees(-p.yRot), (float) Math.toDegrees(p.zRot))); if (p.xScale != 1 || p.yScale != 1 || p.zScale != 1) o.add("scale", arr(p.xScale, p.yScale, p.zScale)); if (!p.visible) o.addProperty("invisible", true); if (p.skipDraw) o.addProperty("skipDraw", true); JsonArray boxes = new JsonArray(); for (CubeDefinition c : (List) fieldValue(def, "cubes")) { 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"); Set faces = (Set) fieldValue(c, "visibleFaces"); float x0 = ax + org.x(), y0 = ay + org.y(), z0 = az + org.z(); JsonObject b = new JsonObject(); b.add("coordinates", arr(-(x0 + dim.x()), 24 - (y0 + dim.y()), z0, dim.x(), dim.y(), dim.z())); b.add("textureOffset", arr((float) fieldValue(tex, "u"), (float) fieldValue(tex, "v"))); float su = (float) fieldValue(texScale, "u"), sv = (float) fieldValue(texScale, "v"); if (su != 1 || sv != 1) b.add("textureScale", arr(su, sv)); if (gx == gy && gy == gz) { if (gx != 0) b.addProperty("sizeAdd", gx); } else { b.add("sizeAddXYZ", arr(gx, gy, gz)); } 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. for (Direction d : Direction.values()) if (!faces.contains(d)) hide.add( switch (d) { case EAST -> "west"; case WEST -> "east"; case UP -> "down"; case DOWN -> "up"; default -> d.getName(); }); b.add("hiddenFaces", hide); } boxes.add(b); } if (!boxes.isEmpty()) o.add("boxes", boxes); JsonArray subs = new JsonArray(); for (Map.Entry ce : def.getChildren()) { subs.add(part(ce.getKey(), ce.getValue(), p.getChild(ce.getKey()), ax, ay, az)); } if (!subs.isEmpty()) o.add("submodels", subs); return o; } static JsonArray arr(float... v) { JsonArray a = new JsonArray(); for (float f : v) a.add(Math.round(f * 10000f) / 10000f); return a; } }