entity state poses from the vanilla dump: sleeping/sitting/crouching/swimming/gliding, fox/cat/panda/parrot/bat/camel/sniffer poses with pose transforms, per-sub-model poses (baby pivots), armor-stand part poses and body pitch via rotation bindings, fish in water, beds, shoulder parrots, pose test scenes

This commit is contained in:
MineTec committed 2026-11-04 17:51:01 +01:00
1 parent 6660ebe557
commit 45d033bd89
10 files changed
+1108 -110

No files matched your search

+413 -51
View File
@@ -105,8 +105,8 @@ public class VanillaModelDump {
JsonObject info = entityInfo(r, state);
addLayerInfo(info, layers, r);
addTransform(info, r);
Model<?> main = found.containsKey("model") ? found.get("model") : found.get("adultModel");
if (main != null) addPoseVariants(info, main, r);
addPoseVariants(info, found, key, r);
addRotationBindings(info, found, r);
entities.put(key, info);
emit(key, found, textures, state, out, failed);
}
@@ -127,8 +127,8 @@ public class VanillaModelDump {
JsonObject info = new JsonObject();
addLayerInfo(info, layers, r);
addTransform(info, r);
Model<?> main = found.get("model");
if (main != null) addPoseVariants(info, main, r);
addPoseVariants(info, found, key, r);
addRotationBindings(info, found, r);
entities.put(key, info);
emit(key, found, textures, r.createRenderState(), out, failed);
}
@@ -446,15 +446,74 @@ public class VanillaModelDump {
}
}
/**
* Named STATE poses (the render-state fields vanilla's extractRenderState fills from the entity's pose/flags):
* {@code {pose, field, value[, "unless:<field>"]}}. 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:<ticks>} = 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
};
/** State poses that combine (every subset of a group with 2+ applicable members is dumped too). */
static final List<List<String>> POSE_GROUPS =
List.of(List.of("crouching", "pouncing"), List.of("head_up", "lying", "on_player"));
/**
* 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, plus illager arm poses. Stored per entity
* type as {@code poses: {variant: {part: {pivot, rotate, visible}}}}; the baker applies them by part
* name to the body AND its layer models (armor follows the arms, like vanilla's copyPropertiesTo).
* ({@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 poses: {variant: {part: {pivot, rotate, visible[, scale]}}}} for the main body model and
* {@code modelPoses: {<sub-model>: {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}.
*/
@SuppressWarnings({"unchecked", "rawtypes"})
static void addPoseVariants(JsonObject info, Model<?> model, EntityRenderer<?, ?> r) {
static void addPoseVariants(
JsonObject info, Map<String, Model<?>> found, String key, EntityRenderer<?, ?> r) {
Object probe;
try {
probe = r.createRenderState();
@@ -467,8 +526,12 @@ public class VanillaModelDump {
for (String f : new String[] {"isPassenger", "isRiding"})
if (hasField(probe, f)) passenger.add(new String[] {"passenger", f, "true"});
if (!passenger.isEmpty()) toggles.add(passenger);
if (hasField(probe, "rightArmPose") && probeEnum(probe, "rightArmPose")) toggles.add(java.util.Collections.singletonList(new String[] {"item_r", "rightArmPose", "ITEM"}));
if (hasField(probe, "leftArmPose") && probeEnum(probe, "leftArmPose")) toggles.add(java.util.Collections.singletonList(new String[] {"item_l", "leftArmPose", "ITEM"}));
List<List<String[]>> items = new ArrayList<>();
if (hasField(probe, "rightArmPose") && probeEnum(probe, "rightArmPose"))
items.add(java.util.Collections.singletonList(new String[] {"item_r", "rightArmPose", "ITEM"}));
if (hasField(probe, "leftArmPose") && probeEnum(probe, "leftArmPose"))
items.add(java.util.Collections.singletonList(new String[] {"item_l", "leftArmPose", "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]))
@@ -493,40 +556,294 @@ public class VanillaModelDump {
}
}
}
if (combos.isEmpty()) return;
Map<String, float[]> base = poseOf(model, freshState(r, List.of()));
if (base == null) return;
JsonObject poses = new JsonObject();
for (List<String[]> c : combos) {
Map<String, float[]> v = poseOf(model, freshState(r, c));
if (v == null) continue;
JsonObject diff = new JsonObject();
for (Map.Entry<String, float[]> 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) {
// 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).
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);
diff.add(e.getKey(), po);
// State poses: alone, grouped, each with every held-item subset.
List<List<String[]>> stateCombos = new ArrayList<>();
java.util.Set<String> applicable = new java.util.LinkedHashSet<>();
for (String[] a : STATE_POSES) if (assignmentApplies(probe, a)) applicable.add(a[0]);
List<List<String>> stateNames = new ArrayList<>();
for (String n : applicable) stateNames.add(List.of(n));
for (List<String> g : POSE_GROUPS) {
List<String> members = g.stream().filter(applicable::contains).toList();
for (int mask = 1; mask < (1 << members.size()); mask++) {
if (Integer.bitCount(mask) < 2) continue;
List<String> sub = new ArrayList<>();
for (int i = 0; i < members.size(); i++) if ((mask & (1 << i)) != 0) sub.add(members.get(i));
stateNames.add(sub);
}
if (diff.isEmpty()) continue;
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);
}
if (!poses.isEmpty()) info.add("poses", poses);
// leave the model in its idle pose for the dump
poseOf(model, freshState(r, List.of()));
for (List<String> names : stateNames) {
List<String[]> 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++) {
List<String[]> c = new ArrayList<>(base);
for (int i = 0; i < items.size(); i++) if ((mask & (1 << i)) != 0) c.addAll(items.get(i));
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);
for (Map.Entry<String, Model<?>> 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 baby = f.toLowerCase().contains("baby");
List<Model<?>> layerModels = new ArrayList<>();
for (Map.Entry<String, Model<?>> lm : found.entrySet()) {
String ln = lm.getKey();
if (!ln.contains("Layer.") || ln.toLowerCase().contains("baby") != baby) continue;
layerModels.add(lm.getValue());
}
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).
boolean babyState = baby && !main;
Map<String, float[]> base = poseOf(model, freshState(r, List.of(), babyState));
if (base == null) continue;
for (List<String[]> 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);
}
Map<String, float[]> baseAll = withLayers(base, layerModels, freshState(r, List.of(), babyState));
java.util.Set<String> emitted = new java.util.HashSet<>();
for (List<String[]> 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<String, float[]> 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));
}
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.
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);
}
}
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<String[]> c,
Model<?> model,
Map<String, float[]> base,
Map<String, float[]> v,
boolean keepEmpty) {
if (v == null) return null;
JsonObject diff = new JsonObject();
for (Map.Entry<String, float[]> 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) {
// 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).
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;
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();
}
/** {@code parts} plus the posed parts of layer models that the body model lacks (elytra wings, …). */
static Map<String, float[]> withLayers(Map<String, float[]> parts, List<Model<?>> layers, Object state) {
if (parts == null) return null;
Map<String, float[]> out = new LinkedHashMap<>(parts);
for (Model<?> lm : layers) {
Map<String, float[]> lp = poseOf(lm, state);
if (lp == null) continue;
lp.remove("root");
lp.forEach(out::putIfAbsent);
}
return out;
}
/** Renderer transform, yaw sign and pitch axis for a posed state (see {@link #addTransform}); null = none. */
static float[][] transformTriple(EntityRenderer<?, ?> r, List<String[]> 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).
*/
@SuppressWarnings({"unchecked", "rawtypes"})
static void addRotationBindings(JsonObject info, Map<String, Model<?>> found, EntityRenderer<?, ?> r) {
Object probe;
try {
probe = r.createRenderState();
} catch (Throwable t) {
return;
}
Model<?> main = found.containsKey("model") ? found.get("model") : found.get("adultModel");
if (main == null) return;
List<Model<?>> layers = new ArrayList<>();
for (Map.Entry<String, Model<?>> lm : found.entrySet())
if (lm.getKey().contains("Layer.") && !lm.getKey().toLowerCase().contains("baby")) layers.add(lm.getValue());
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<String, float[]> 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<String, float[]> 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<String, float[]> v = withLayers(poseOf(main, st), layers, st);
if (v == null) continue;
for (Map.Entry<String, float[]> 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) {
java.util.Set<String> heads = new java.util.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. */
@SuppressWarnings("unchecked")
static void headParts(ModelPart p, String name, boolean inHead, java.util.Set<String> out) {
inHead |= name.contains("head");
if (inHead) out.add(name);
Map<String, ModelPart> children = (Map<String, ModelPart>) fieldValue(p, "children");
if (children == null) return;
for (Map.Entry<String, ModelPart> c : children.entrySet()) headParts(c.getValue(), c.getKey(), inHead, out);
}
/** 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) {
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;
}
/** Whether the state field is an enum (HumanoidModel.ArmPose), not e.g. an armor stand's Rotations. */
@@ -547,22 +864,41 @@ public class VanillaModelDump {
}
}
@SuppressWarnings({"unchecked", "rawtypes"})
static Object freshState(EntityRenderer<?, ?> r, List<String[]> sets) {
return freshState(r, sets, false);
}
/** {@code baby}: also isBaby + ageScale 0.5 like a baby's extracted state (baby models' pose offsets). */
static Object freshState(EntityRenderer<?, ?> r, List<String[]> sets, boolean baby) {
try {
Object st = r.createRenderState();
idleTweaks(st);
for (String[] t : sets) {
Field f = st.getClass().getField(t[1]);
if (f.getType() == boolean.class) f.setBoolean(st, Boolean.parseBoolean(t[2]));
else if (f.getType().isEnum()) f.set(st, Enum.valueOf((Class) f.getType(), t[2]));
}
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, CEM rotation and visibility, by name. */
@SuppressWarnings({"unchecked", "rawtypes"})
static Map<String, float[]> poseOf(Model<?> model, Object state) {
@@ -583,7 +919,7 @@ public class VanillaModelDump {
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.visible ? 1 : 0, p.xScale, p.yScale, p.zScale
});
Map<String, ModelPart> children = (Map<String, ModelPart>) fieldValue(p, "children");
if (children != null) children.forEach((n, c) -> collectPose(c, n, ax, ay, az, out));
@@ -598,13 +934,27 @@ public class VanillaModelDump {
}
static float[] captureTransform(EntityRenderer<?, ?> r, float yaw, float pitch) {
return captureTransform(r, yaw, pitch, null, false);
}
/**
* {@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.
*/
@SuppressWarnings({"unchecked", "rawtypes"})
static float[] captureTransform(
EntityRenderer<?, ?> r, float yaw, float pitch, List<String[]> 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;
}
idleTweaks(state);
setFloat(state, "bodyRot", yaw);
setFloat(state, "yRot", yaw);
setFloat(state, "xRot", pitch);
@@ -641,6 +991,18 @@ public class VanillaModelDump {
} 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);
if (off != null && off.lengthSqr() > 0) {
first[0] = new org.joml.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];
}