derive default layers, armor wearers, arms/chest pose flags and mob variant textures from the vanilla dump, drop the hardcoded lists

This commit is contained in:
MineTec committed 2026-11-04 15:48:03 +01:00
1 parent 022b080fce
commit a743dc32c7
14 files changed
+720 -162

No files matched your search

+132 -4
View File
@@ -103,6 +103,7 @@ public class VanillaModelDump {
for (Object layer : ls) collectLayer(layer, found, textures);
}
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);
@@ -124,6 +125,7 @@ public class VanillaModelDump {
for (Object layer : ls) collectLayer(layer, found, textures);
}
JsonObject info = new JsonObject();
addLayerInfo(info, layers, r);
addTransform(info, r);
Model<?> main = found.get("model");
if (main != null) addPoseVariants(info, main, r);
@@ -187,6 +189,7 @@ public class VanillaModelDump {
JsonObject ents = new JsonObject();
entities.forEach(ents::add);
root.add("entities", ents);
root.add("variants", registryVariants());
try (FileWriter w = new FileWriter(args.length > 0 ? args[0] : "vanilla_entity_models.json")) {
new GsonBuilder().create().toJson(root, w);
}
@@ -194,6 +197,111 @@ public class VanillaModelDump {
for (String f : failed) System.out.println("FAILED " + f);
}
/**
* 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).
*/
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 net.minecraft.client.renderer.entity.layers.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);
}
/** True/false = the layer submits / doesn't submit geometry for the default state; null = submit threw. */
static Boolean layerDrawsByDefault(Object layer, EntityRenderer<?, ?> r) {
Object state;
try {
state = r.createRenderState();
} catch (Throwable t) {
return null;
}
idleTweaks(state);
boolean[] submitted = {false};
Object proxy = recordingCollector(m -> {
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;
}
return submitted[0];
}
/** A SubmitNodeCollector proxy that reports every call and otherwise does nothing. */
static Object recordingCollector(java.util.function.Consumer<Method> 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(
VanillaModelDump.class.getClassLoader(), new Class<?>[] {collector}, (px, m, a) -> {
if (m.getName().equals("order")) return px;
onCall.accept(m);
Class<?> rt = m.getReturnType();
if (rt == boolean.class) return false;
if (rt.isPrimitive() && rt != void.class) return 0;
return null;
});
}
/**
* The built-in data-driven mob variants ({@code data/minecraft/<type>_variant/<key>.json}, cat/wolf/frog/
* cow/pig/chicken/zombie_nautilus …) with their client assets: {@code variants: {<type>: {<key>: {asset_id,
* baby_asset_id, assets{wild,tame,angry}, baby_assets{…}, model}}}} (spawn conditions dropped).
*/
static JsonObject registryVariants() throws java.io.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<? extends java.util.zip.ZipEntry> 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());
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();
}
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;
}
/** One block-entity model posed with {@code state} (see main), stored as {@code posed/<model>#<layer>}. */
static void posedBlockEntity(
Map<String, JsonObject> out,
@@ -359,8 +467,13 @@ 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")) toggles.add(java.util.Collections.singletonList(new String[] {"item_r", "rightArmPose", "ITEM"}));
if (hasField(probe, "leftArmPose")) toggles.add(java.util.Collections.singletonList(new String[] {"item_l", "leftArmPose", "ITEM"}));
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"}));
// 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"}));
}
List<List<String[]>> combos = new ArrayList<>();
for (int mask = 1; mask < (1 << toggles.size()); mask++) {
List<String[]> c = new ArrayList<>();
@@ -395,8 +508,14 @@ public class VanillaModelDump {
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();
po.add("pivot", arr(x[0], x[1], x[2]));
po.add("rotate", arr(x[3], x[4], x[5]));
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);
}
@@ -410,6 +529,15 @@ public class VanillaModelDump {
poseOf(model, freshState(r, List.of()));
}
/** Whether the state field is an enum (HumanoidModel.ArmPose), not e.g. an armor stand's Rotations. */
static boolean probeEnum(Object o, String f) {
try {
return o.getClass().getField(f).getType().isEnum();
} catch (NoSuchFieldException e) {
return false;
}
}
static boolean hasField(Object o, String f) {
try {
o.getClass().getField(f);