render entities from vanilla client models, add items, displays and leashes
- dump entity models straight from the unobfuscated client jar (tools/VanillaModelDump.java, tools/update-vanilla-models.sh): models posed by vanilla setupAnim, baby/variant/layer models, renderer transforms, default textures, pose variants (sitting, holding items, illager arms) and all layer definitions; the old CEM template is only a fallback now - fix adult zombies captured as babies, zero-thickness planes (wings, fins) and back faces in the entity intersector - held items, head items (pumpkins, skulls), shields, elytra, saddles and body equipment for every mount, armor following the pose - block/item/text display entities, dropped items, falling blocks, primed TNT, minecart contents, block entities inside displays, leash ropes - item models resolved from the 26.x item definitions with display transforms; generated items extruded like vanilla - bundle the vanilla font definitions as fallback so sign/text rendering works with packs that lack font/*.json - one failing entity no longer aborts the whole photo - edge-case test plugin (tools/edgecase-plugin) and offline harnesses
This commit is contained in:
1 parent
5904814e69
commit
32afe08f03
41 files changed
+3761
-370
No files matched your search
@@ -57,7 +57,8 @@ public class BlockEntityTestRender {
|
||||
BlockModelRegistry registry = new BlockModelRegistry(reader, textures);
|
||||
BiomeTintProvider tint = new BiomeTintProvider(textures);
|
||||
CemModelLoader geo = new CemModelLoader();
|
||||
geo.load(new java.io.FileInputStream("/tmp/cem_models.json"), log);
|
||||
geo.load(new java.io.FileInputStream("src/main/resources/cem/cem_template_models.json"), log);
|
||||
geo.load(new java.io.FileInputStream("src/main/resources/cem/vanilla_entity_models.json"), log);
|
||||
SkinCache skins = new SkinCache();
|
||||
eu.mhsl.minecraft.pixelpics.assets.font.BitmapFont font =
|
||||
eu.mhsl.minecraft.pixelpics.assets.font.FontLoader.load(pack, textures, log);
|
||||
|
||||
+19
-10
@@ -39,7 +39,9 @@ public class EntityTestRender {
|
||||
"slime","sniffer","snow_golem","spider","squid","stray","strider","tadpole","trader_llama","tropical_fish",
|
||||
"turtle","vex","villager","vindicator","wandering_trader","warden","witch","wither","wither_skeleton","wolf",
|
||||
"zoglin","zombie","zombie_horse","zombie_villager","zombified_piglin","ender_dragon","player","mannequin",
|
||||
"nautilus","zombie_nautilus_coral","parched","camel_husk","oak_boat"
|
||||
"nautilus","zombie_nautilus","parched","camel_husk","oak_boat","sulfur_cube","oak_chest_boat","bamboo_raft",
|
||||
"baby:zombie","baby:cow","baby:villager","baby:wolf","baby:sheep","baby:chicken","baby:horse","baby:pig",
|
||||
"minecart","end_crystal","arrow","trident","leash_knot","evoker_fangs","wither_skull","shulker_bullet","cushion"
|
||||
};
|
||||
|
||||
// Representative variants (the game always supplies these; null base textures wouldn't exist otherwise).
|
||||
@@ -49,7 +51,8 @@ public class EntityTestRender {
|
||||
Map.entry("llama", "creamy"), Map.entry("trader_llama", "creamy"), Map.entry("fox", "red"),
|
||||
Map.entry("mooshroom", "red"), Map.entry("frog", "temperate"), Map.entry("panda", "normal"),
|
||||
Map.entry("cow", "temperate"), Map.entry("pig", "temperate"), Map.entry("chicken", "temperate"),
|
||||
Map.entry("villager", "taiga"), Map.entry("zombie_villager", "swamp")
|
||||
Map.entry("villager", "taiga"), Map.entry("zombie_villager", "swamp"),
|
||||
Map.entry("tropical_fish", "betty"), Map.entry("pufferfish", "big"), Map.entry("zombie_nautilus", "warm")
|
||||
);
|
||||
|
||||
// Villager profession / level for the standalone render (biome type comes from VAR above).
|
||||
@@ -62,10 +65,11 @@ public class EntityTestRender {
|
||||
|
||||
// Horse/llama/donkey equipment for the standalone render.
|
||||
static final Map<String, String> MARK = Map.of("horse", "blackdots"); // coat markings
|
||||
static final java.util.Set<String> SADDLE = java.util.Set.of("horse", "donkey", "mule", "nautilus");
|
||||
static final java.util.Set<String> SADDLE = java.util.Set.of("horse", "donkey", "mule", "nautilus", "pig", "strider", "camel");
|
||||
static final java.util.Set<String> CHEST = java.util.Set.of("llama", "donkey");
|
||||
static final Map<String, String> EQUIP = Map.ofEntries( // armor / carpet
|
||||
Map.entry("horse", "diamond"), Map.entry("llama", "red"), Map.entry("trader_llama", "trader_llama"),
|
||||
Map.entry("horse", "diamond"), Map.entry("llama", "red"), Map.entry("happy_ghast", "blue_harness"),
|
||||
Map.entry("wolf", "armadillo_scute"), Map.entry("trader_llama", "trader_llama"),
|
||||
Map.entry("nautilus", "diamond")
|
||||
);
|
||||
|
||||
@@ -105,7 +109,8 @@ public class EntityTestRender {
|
||||
BlockModelRegistry registry = new BlockModelRegistry(reader, textures);
|
||||
BiomeTintProvider tint = new BiomeTintProvider(textures);
|
||||
eu.mhsl.minecraft.pixelpics.render.entity.cem.CemModelLoader geo = new eu.mhsl.minecraft.pixelpics.render.entity.cem.CemModelLoader();
|
||||
int n = geo.load(new java.io.FileInputStream("/tmp/cem_models.json"), log);
|
||||
int n = geo.load(new java.io.FileInputStream("src/main/resources/cem/cem_template_models.json"), log);
|
||||
n = geo.load(new java.io.FileInputStream("src/main/resources/cem/vanilla_entity_models.json"), log);
|
||||
log.info("Loaded " + n + " geometries");
|
||||
SkinCache skins = new SkinCache();
|
||||
eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker baker = new eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker(geo, textures, skins);
|
||||
@@ -145,7 +150,7 @@ public class EntityTestRender {
|
||||
File single = new File(outDir, "einzeln");
|
||||
single.mkdirs();
|
||||
for (int i = 0; i < list.length; i++) {
|
||||
ImageIO.write(cells.get(i), "png", new File(single, list[i] + ".png"));
|
||||
ImageIO.write(cells.get(i), "png", new File(single, list[i].replace(':', '_') + ".png"));
|
||||
}
|
||||
|
||||
// Compose contact-sheet pages: 2 columns.
|
||||
@@ -172,11 +177,15 @@ public class EntityTestRender {
|
||||
|
||||
static BufferedImage renderEntity(DefaultScreenRenderer renderer, eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker baker, WorldSnapshot world,
|
||||
SkyContext sky, String key, float yaw) {
|
||||
boolean baby = key.startsWith("baby:");
|
||||
if (baby) key = key.substring(5);
|
||||
boolean isPlayer = key.equals("player");
|
||||
EntityState s = new EntityState(key, 0, 0, 0, yaw, false, 0.8, 1.0,
|
||||
isPlayer, null, false, VAR.get(key), 0, 1.0, PROF.get(key), LVL.getOrDefault(key, 0),
|
||||
MARK.get(key), SADDLE.contains(key), CHEST.contains(key), EQUIP.get(key), ARMOR.get(key),
|
||||
INVISIBLE.contains(key));
|
||||
int tint = key.equals("tropical_fish") ? 0xFFF9801D : (key.equals("sheep") ? 0xFFF9FFFE : 0);
|
||||
int tint2 = key.equals("tropical_fish") ? 0xFF3AB3DA : 0;
|
||||
EntityState s = new EntityState(key, 0, 0, 0, yaw, baby, 0.8, 1.0,
|
||||
isPlayer, null, false, VAR.get(key), tint, 1.0, PROF.get(key), LVL.getOrDefault(key, 0),
|
||||
MARK.get(key), SADDLE.contains(key), CHEST.contains(key), EQUIP.get(key), baby ? null : ARMOR.get(key),
|
||||
!baby && INVISIBLE.contains(key), 0, 0, tint2, false, null, null, null);
|
||||
RenderedEntity re = baker.bake(s);
|
||||
if (re == null || re.cubes.isEmpty()) { // invisible with no equipment -> nothing renders
|
||||
return new BufferedImage(TW, TH, BufferedImage.TYPE_INT_RGB);
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import eu.mhsl.minecraft.pixelpics.assets.*;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.*;
|
||||
import eu.mhsl.minecraft.pixelpics.render.render.*;
|
||||
import eu.mhsl.minecraft.pixelpics.render.sky.SkyContext;
|
||||
import eu.mhsl.minecraft.pixelpics.render.snapshot.WorldSnapshot;
|
||||
import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Standalone (no server) render of {@link ObjectState}s — block/item/text displays, dropped items,
|
||||
* falling blocks — in one scene, to check the ObjectBaker geometry, UV orientation and transforms.
|
||||
* Output: ~/Dokumente/PixelPics-Entity-Renders/objects.png
|
||||
*/
|
||||
public class ObjectTestRender {
|
||||
|
||||
static final String ROOT = "/home/elias/Dokumente/mcTestServer/plugins/PixelPics";
|
||||
|
||||
/** Server-free BlockData stand-in: material + blockstate string are all the model registry needs. */
|
||||
static BlockData block(Material m, String state) {
|
||||
String s = "minecraft:" + m.getKey().getKey() + (state == null ? "" : "[" + state + "]");
|
||||
return (BlockData) Proxy.newProxyInstance(ObjectTestRender.class.getClassLoader(),
|
||||
new Class<?>[]{BlockData.class}, (p, meth, a) -> switch (meth.getName()) {
|
||||
case "getMaterial" -> m;
|
||||
case "getAsString" -> s;
|
||||
case "equals" -> p == a[0];
|
||||
case "hashCode" -> System.identityHashCode(p);
|
||||
case "toString" -> s;
|
||||
default -> {
|
||||
Class<?> rt = meth.getReturnType();
|
||||
yield rt == boolean.class ? false : (rt.isPrimitive() ? 0 : null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static ObjectState obj(ObjectState.Kind k, Affine a, BlockData b, String item, List<String> lines, int bg) {
|
||||
return switch (k) {
|
||||
case BLOCK -> ObjectState.block(a, b, null);
|
||||
case ITEM -> ObjectState.item(a, new ItemSpec(item, b, null, false), "none", false);
|
||||
case TEXT -> ObjectState.text(a, lines, 200, 0xFFFFFFFF, bg, true);
|
||||
case LEASH -> ObjectState.leash(new double[6]);
|
||||
};
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
Logger log = Logger.getLogger("test");
|
||||
ResourcePack pack = ResourcePackLoader.load(new File(ROOT, "resourcepack"), log).orElseThrow();
|
||||
AssetReader reader = new AssetReader(pack);
|
||||
TextureCache textures = new TextureCache(pack);
|
||||
BlockModelRegistry registry = new BlockModelRegistry(reader, textures);
|
||||
BiomeTintProvider tint = new BiomeTintProvider(textures);
|
||||
var geo = new eu.mhsl.minecraft.pixelpics.render.entity.cem.CemModelLoader();
|
||||
var font = eu.mhsl.minecraft.pixelpics.assets.font.FontLoader.load(pack, textures, log);
|
||||
SkinCache skins = new SkinCache();
|
||||
var baker = new eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker(geo, textures, skins);
|
||||
var beBaker = new eu.mhsl.minecraft.pixelpics.render.entity.cem.BlockEntityBaker(geo, textures, skins, font);
|
||||
geo.load(new java.io.FileInputStream("src/main/resources/cem/cem_template_models.json"), log);
|
||||
geo.load(new java.io.FileInputStream("src/main/resources/cem/vanilla_entity_models.json"), log);
|
||||
DefaultScreenRenderer renderer = new DefaultScreenRenderer(registry, tint, textures, baker, beBaker, log);
|
||||
|
||||
List<ObjectState> objs = new ArrayList<>();
|
||||
// Block displays: plain grass block (tinted), rotated + scaled stairs.
|
||||
objs.add(obj(ObjectState.Kind.BLOCK, Affine.translation(0, 0, 0), block(Material.GRASS_BLOCK, "snowy=false"), null, List.of(), 0));
|
||||
objs.add(obj(ObjectState.Kind.BLOCK,
|
||||
Affine.translation(2, 0, 0).mul(Affine.rotY(Math.toRadians(30))).mul(Affine.scale(0.75)),
|
||||
block(Material.OAK_STAIRS, "facing=north,half=bottom,shape=straight,waterlogged=false"), null, List.of(), 0));
|
||||
// Item displays: a flat item and a block item (centred).
|
||||
objs.add(obj(ObjectState.Kind.ITEM, Affine.translation(0.5, 2.0, 0.5), null, "diamond_sword", List.of(), 0));
|
||||
objs.add(obj(ObjectState.Kind.ITEM, Affine.translation(2.5, 2.0, 0.5).mul(Affine.scale(0.6)),
|
||||
block(Material.OAK_LOG, "axis=y"), "oak_log", List.of(), 0));
|
||||
// Text display (facing the camera side), opaque background.
|
||||
objs.add(obj(ObjectState.Kind.TEXT,
|
||||
Affine.translation(1.5, 3.0, 0).mul(Affine.rotY(Math.PI)).mul(Affine.scale(0.025)), null, null,
|
||||
List.of("Hallo PixelPics!", "Zeile zwei äöü"), 0xFF203040));
|
||||
// Text seen from the BACK (front faces +Z, camera is on -Z) and a shield item.
|
||||
objs.add(obj(ObjectState.Kind.TEXT, Affine.translation(-0.5, 3.0, 0).mul(Affine.scale(0.025)), null, null,
|
||||
List.of("Rueckseite"), 0));
|
||||
objs.add(obj(ObjectState.Kind.ITEM, Affine.translation(4, 1.0, 0.5), null, "shield", List.of(), 0));
|
||||
// Dropped item (flat) on the ground.
|
||||
objs.add(obj(ObjectState.Kind.ITEM, Affine.translation(-1, 0.3, 0.5).mul(Affine.scale(0.5)), null, "apple", List.of(), 0));
|
||||
|
||||
BlockData air = block(Material.AIR, null);
|
||||
WorldSnapshot empty = new WorldSnapshot(Map.of(), 0, 1, air);
|
||||
SkyContext sky = new SkyContext(6000, 0, 6000);
|
||||
int W = 480, H = 360;
|
||||
Vector cam = new Vector(1.5, 3.2, -6.5);
|
||||
Location loc = new Location(null, cam.getX(), cam.getY(), cam.getZ());
|
||||
loc.setDirection(new Vector(1.2, 1.3, 0.5).subtract(cam));
|
||||
List<Vector> rayMap = EntityTestRender.buildRayMap(loc, W * 3, H * 3);
|
||||
RenderJob job = new RenderJob(empty, rayMap, cam, W, H, sky, List.of(), List.of(), List.of(), objs);
|
||||
BufferedImage img = renderer.execute(job);
|
||||
File out = new File("/home/elias/Dokumente/PixelPics-Entity-Renders/objects.png");
|
||||
ImageIO.write(img, "png", out);
|
||||
System.out.println("WROTE " + out);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,725 @@
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import java.io.FileWriter;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
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.ModelPart;
|
||||
import net.minecraft.client.model.geom.builders.CubeDefinition;
|
||||
import net.minecraft.client.model.geom.builders.LayerDefinition;
|
||||
import net.minecraft.client.model.geom.builders.PartDefinition;
|
||||
import net.minecraft.client.renderer.entity.EntityRenderer;
|
||||
import net.minecraft.client.renderer.entity.EntityRendererProvider;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
|
||||
/**
|
||||
* Dumps the vanilla Java entity models straight from an (unobfuscated, 26.x+) Minecraft CLIENT jar into
|
||||
* the CEM-template JSON layout PixelPics loads ({@code {"models": {name: {"model": "<jem json>"}}}}).
|
||||
*
|
||||
* <p>Unlike the third-party CEM template set (rest geometry only), every entity model is instantiated
|
||||
* through its real renderer and posed by its real {@code setupAnim} with a default (idle, adult, not
|
||||
* aggressive) render state — so zombie arms are raised, spider legs splayed, alternate-state parts
|
||||
* (rolled-up armadillo, crossed/straight illager arms, chests) carry their vanilla visibility, and new
|
||||
* mobs of the jar's version are included automatically.
|
||||
*
|
||||
* <p>Output per entity type key: {@code <type>} (main model) plus {@code <type>/<extra>} for the
|
||||
* renderer's additional models and render-layer models (outer layers, armor sets, saddles, …).
|
||||
* Part coordinates are ABSOLUTE in the CEM frame (x negated, y up with the ground at 24, z kept);
|
||||
* every part carries its absolute pivot ({@code "pivot"}), so the jem is flagged {@code "absolute": true}.
|
||||
*
|
||||
* <p>Run (classpath = client jar + its libraries from the version json):
|
||||
* {@code java -cp <mc-classpath> tools/VanillaModelDump.java <out.json>}
|
||||
*/
|
||||
public class VanillaModelDump {
|
||||
|
||||
static final Map<Object, ModelLayerLocation> BAKED = new IdentityHashMap<>();
|
||||
static Map<ModelLayerLocation, LayerDefinition> roots;
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
roots = LayerDefinitions.createRoots();
|
||||
EntityModelSet set = new EntityModelSet(roots) {
|
||||
@Override
|
||||
public ModelPart bakeLayer(ModelLayerLocation loc) {
|
||||
ModelPart p = super.bakeLayer(loc);
|
||||
BAKED.put(p, loc);
|
||||
return p;
|
||||
}
|
||||
};
|
||||
EntityRendererProvider.Context ctx =
|
||||
new EntityRendererProvider.Context(null, null, null, null, null, set, null, null, null, null, null);
|
||||
|
||||
Field provField = Class.forName("net.minecraft.client.renderer.entity.EntityRenderers")
|
||||
.getDeclaredField("PROVIDERS");
|
||||
provField.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<EntityType<?>, EntityRendererProvider<?>> providers =
|
||||
(Map<EntityType<?>, EntityRendererProvider<?>>) provField.get(null);
|
||||
|
||||
Map<String, JsonObject> out = new TreeMap<>();
|
||||
Map<String, JsonObject> entities = new TreeMap<>();
|
||||
List<String> failed = new ArrayList<>();
|
||||
for (Map.Entry<EntityType<?>, 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;
|
||||
}
|
||||
Object state;
|
||||
try {
|
||||
state = r.createRenderState();
|
||||
} catch (Throwable t) {
|
||||
state = null;
|
||||
}
|
||||
idleTweaks(state);
|
||||
Map<String, Model<?>> found = new LinkedHashMap<>();
|
||||
Map<String, String> textures = new LinkedHashMap<>();
|
||||
// The renderer's current model is the main one, whatever field else also holds it (bigModel, …).
|
||||
if (fieldValue(r, "model") instanceof Model<?> current) found.put("model", current);
|
||||
collectModels(r, "", found, 0);
|
||||
Object layers = fieldValue(r, "layers");
|
||||
if (layers instanceof List<?> ls) {
|
||||
for (Object layer : ls) collectLayer(layer, found, textures);
|
||||
}
|
||||
JsonObject info = entityInfo(r, state);
|
||||
addTransform(info, r);
|
||||
Model<?> main = found.containsKey("model") ? found.get("model") : found.get("adultModel");
|
||||
if (main != null) addPoseVariants(info, main, r);
|
||||
entities.put(key, info);
|
||||
emit(key, found, textures, state, out, failed);
|
||||
}
|
||||
// Players & mannequins use the avatar renderers (one per skin model type: wide/slim).
|
||||
Map<?, ?> avatars = (Map<?, ?>) Class.forName("net.minecraft.client.renderer.entity.EntityRenderers")
|
||||
.getMethod("createAvatarRenderers", EntityRendererProvider.Context.class)
|
||||
.invoke(null, ctx);
|
||||
for (Map.Entry<?, ?> a : avatars.entrySet()) {
|
||||
EntityRenderer<?, ?> r = (EntityRenderer<?, ?>) a.getValue();
|
||||
String key = String.valueOf(a.getKey()).toLowerCase().equals("slim") ? "player_slim" : "player";
|
||||
Map<String, Model<?>> found = new LinkedHashMap<>();
|
||||
Map<String, String> textures = new LinkedHashMap<>();
|
||||
collectModels(r, "", found, 0);
|
||||
Object layers = fieldValue(r, "layers");
|
||||
if (layers instanceof List<?> ls) {
|
||||
for (Object layer : ls) collectLayer(layer, found, textures);
|
||||
}
|
||||
JsonObject info = new JsonObject();
|
||||
addTransform(info, r);
|
||||
Model<?> main = found.get("model");
|
||||
if (main != null) addPoseVariants(info, main, r);
|
||||
entities.put(key, info);
|
||||
emit(key, found, textures, r.createRenderState(), out, failed);
|
||||
}
|
||||
// Every layer definition in its rest pose as "layer/<model>#<layer>" (special item models such as the
|
||||
// shield, block-entity models, …) — same absolute CEM frame as the entity models.
|
||||
for (Map.Entry<ModelLayerLocation, LayerDefinition> le : roots.entrySet()) {
|
||||
String name = "layer/" + le.getKey().model().getPath() + "#" + le.getKey().layer();
|
||||
try {
|
||||
ModelPart baked = le.getValue().bakeRoot();
|
||||
PartDefinition rootDef = ((net.minecraft.client.model.geom.builders.MeshDefinition)
|
||||
fieldValue(le.getValue(), "mesh"))
|
||||
.getRoot();
|
||||
Object mat = fieldValue(le.getValue(), "material");
|
||||
JsonObject jem = new JsonObject();
|
||||
JsonArray ts = new JsonArray();
|
||||
ts.add((int) fieldValue(mat, "xTexSize"));
|
||||
ts.add((int) fieldValue(mat, "yTexSize"));
|
||||
jem.add("textureSize", ts);
|
||||
jem.addProperty("absolute", true);
|
||||
JsonArray parts = new JsonArray();
|
||||
parts.add(part("root", rootDef, baked, 0, 0, 0));
|
||||
jem.add("models", parts);
|
||||
JsonObject entry = new JsonObject();
|
||||
entry.addProperty("model", new Gson().toJson(jem));
|
||||
out.putIfAbsent(name, entry);
|
||||
} catch (Throwable t) {
|
||||
failed.add(name + " (layer: " + t + ")");
|
||||
}
|
||||
}
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Small, name-based tweaks so the default render state shows the typical idle look: illagers stand
|
||||
* with crossed arms (their peaceful pose) instead of the empty-handed "neutral" pose.
|
||||
*/
|
||||
static void idleTweaks(Object state) {
|
||||
if (state == null) return;
|
||||
for (String sizeField : new String[] {"size"}) {
|
||||
try {
|
||||
Field f = state.getClass().getField(sizeField);
|
||||
if (f.getType() == int.class && f.getInt(state) == 0) f.setInt(state, 1);
|
||||
if (f.getType() == float.class && f.getFloat(state) == 0) f.setFloat(state, 1);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
try {
|
||||
Field f = state.getClass().getField("armPose");
|
||||
if (f.getType().isEnum()) {
|
||||
for (Object c : f.getType().getEnumConstants())
|
||||
if (((Enum<?>) c).name().equals("CROSSED")) f.set(state, c);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Writes every collected model of one entity type; adult/normal/plain "model" fields become the main model. */
|
||||
static void emit(
|
||||
String key,
|
||||
Map<String, Model<?>> found,
|
||||
Map<String, String> textures,
|
||||
Object state,
|
||||
Map<String, JsonObject> out,
|
||||
List<String> failed) {
|
||||
for (Map.Entry<String, Model<?>> m : found.entrySet()) {
|
||||
String f = m.getKey();
|
||||
String name =
|
||||
switch (f) {
|
||||
case "model", "adultModel", "normalModel" -> key;
|
||||
case "babyModel" -> key + "/baby";
|
||||
default -> key + "/" + f;
|
||||
};
|
||||
if (out.containsKey(name)) continue;
|
||||
JsonObject jem = dump(m.getValue(), state, name, failed);
|
||||
if (jem != null) {
|
||||
String tex = textures.get(f);
|
||||
if (tex != null) jem.addProperty("texture", tex);
|
||||
JsonObject entry = new JsonObject();
|
||||
entry.addProperty("model", new Gson().toJson(jem));
|
||||
out.put(name, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<String, Model<?>> found, Map<String, String> textures) {
|
||||
String prefix = layer.getClass().getSimpleName() + ".";
|
||||
Object type = fieldValue(layer, "layer");
|
||||
if (type instanceof Enum<?> en) prefix = "equip." + en.name().toLowerCase() + ".";
|
||||
Map<String, Model<?>> 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;
|
||||
String path = v.toString().replaceFirst("^minecraft:", "")
|
||||
.replaceFirst("^textures/", "")
|
||||
.replaceFirst("\\.png$", "");
|
||||
if (fl.getName().toLowerCase().contains("baby")) {
|
||||
if (baby == null) baby = path;
|
||||
} else if (adult == null) adult = path;
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Map.Entry<String, Model<?>> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the renderer's real {@code submit} (yaw 0, pitch 0) against a recording collector and returns the
|
||||
* pose matrix (column-major 4x4, Java model units -> entity-local blocks) of its first model submission:
|
||||
* the renderer's own translate/rotate/scale (end-crystal float, minecart offsets, wither x2, …), so no
|
||||
* per-entity placement table is needed.
|
||||
*/
|
||||
/**
|
||||
* Stores the renderer transform at yaw=pitch=0 ({@code transform}, column-major 4x4) plus how yaw and
|
||||
* pitch enter it, measured by re-rendering at yaw 90 / pitch 90: {@code yawSign} (+1/-1, 0 = ignores yaw)
|
||||
* and {@code pitchAxis} (world-frame axis for +pitch; absent = ignores pitch).
|
||||
*/
|
||||
static void addTransform(JsonObject info, EntityRenderer<?, ?> r) {
|
||||
float[] t0 = captureTransform(r, 0, 0);
|
||||
if (t0 == null) return;
|
||||
JsonArray ta = new JsonArray();
|
||||
for (float v : t0) ta.add(Math.round(v * 100000f) / 100000f);
|
||||
info.add("transform", ta);
|
||||
org.joml.Matrix3f inv0 = new org.joml.Matrix4f().set(t0).get3x3(new org.joml.Matrix3f()).invert();
|
||||
float[] ty = captureTransform(r, 90, 0);
|
||||
if (ty != null) {
|
||||
org.joml.Matrix3f rel = new org.joml.Matrix4f().set(ty).get3x3(new org.joml.Matrix3f()).mul(inv0);
|
||||
// rel ≈ RotY(±90): m20 (column 0, row 2) = -sin(angle)
|
||||
float sinA = -rel.m02;
|
||||
info.addProperty("yawSign", Math.abs(sinA) < 0.5f ? 0 : (sinA > 0 ? 1 : -1));
|
||||
}
|
||||
float[] tp = captureTransform(r, 0, 90);
|
||||
if (tp != null) {
|
||||
org.joml.Matrix3f rel = new org.joml.Matrix4f().set(tp).get3x3(new org.joml.Matrix3f()).mul(inv0);
|
||||
// axis of a 90° rotation: the antisymmetric part (R - R^T)/2
|
||||
float ax = (rel.m12 - rel.m21) / 2, ay = (rel.m20 - rel.m02) / 2, az = (rel.m01 - rel.m10) / 2;
|
||||
float len = (float) Math.sqrt(ax * ax + ay * ay + az * az);
|
||||
if (len > 0.5f) info.add("pitchAxis", arr(ax / len, ay / len, az / len));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
static void addPoseVariants(JsonObject info, Model<?> model, EntityRenderer<?, ?> r) {
|
||||
Object probe;
|
||||
try {
|
||||
probe = r.createRenderState();
|
||||
} catch (Throwable t) {
|
||||
return;
|
||||
}
|
||||
// Toggle groups: {name, field, value} entries sharing a name are applied together.
|
||||
List<List<String[]>> toggles = new ArrayList<>();
|
||||
List<String[]> 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);
|
||||
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"}));
|
||||
List<List<String[]>> combos = new ArrayList<>();
|
||||
for (int mask = 1; mask < (1 << toggles.size()); mask++) {
|
||||
List<String[]> c = new ArrayList<>();
|
||||
for (int i = 0; i < toggles.size(); i++) if ((mask & (1 << i)) != 0) c.addAll(toggles.get(i));
|
||||
combos.add(c);
|
||||
}
|
||||
// Illagers: the arm pose enum replaces the crossed arms (crossbow hold, attacking, …).
|
||||
if (hasField(probe, "armPose")) {
|
||||
for (String v : new String[] {"CROSSBOW_HOLD", "ATTACKING", "NEUTRAL", "BOW_AND_ARROW"}) {
|
||||
List<String[]> c = new ArrayList<>();
|
||||
c.add(new String[] {"arms_" + v.toLowerCase(), "armPose", v});
|
||||
combos.add(c);
|
||||
if (!passenger.isEmpty()) {
|
||||
List<String[]> cr = new ArrayList<>(c);
|
||||
cr.addAll(passenger);
|
||||
combos.add(cr);
|
||||
}
|
||||
}
|
||||
}
|
||||
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();
|
||||
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);
|
||||
}
|
||||
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()));
|
||||
}
|
||||
|
||||
static boolean hasField(Object o, String f) {
|
||||
try {
|
||||
o.getClass().getField(f);
|
||||
return true;
|
||||
} catch (NoSuchFieldException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
static Object freshState(EntityRenderer<?, ?> r, List<String[]> sets) {
|
||||
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]));
|
||||
}
|
||||
return st;
|
||||
} catch (Throwable t) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
if (state == null) return null;
|
||||
try {
|
||||
((Model) model).setupAnim(state);
|
||||
} catch (Throwable t) {
|
||||
return null;
|
||||
}
|
||||
Map<String, float[]> out = new LinkedHashMap<>();
|
||||
collectPose(model.root(), "root", 0, 0, 0, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static void collectPose(ModelPart p, String name, float px, float py, float pz, Map<String, float[]> 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
|
||||
});
|
||||
Map<String, ModelPart> children = (Map<String, ModelPart>) fieldValue(p, "children");
|
||||
if (children != null) children.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 float[] captureTransform(EntityRenderer<?, ?> r, float yaw, float pitch) {
|
||||
Object state;
|
||||
try {
|
||||
state = r.createRenderState();
|
||||
} catch (Throwable t) {
|
||||
return null;
|
||||
}
|
||||
idleTweaks(state);
|
||||
setFloat(state, "bodyRot", yaw);
|
||||
setFloat(state, "yRot", yaw);
|
||||
setFloat(state, "xRot", pitch);
|
||||
float[][] first = new float[1][];
|
||||
Class<?> collector;
|
||||
try {
|
||||
collector = Class.forName("net.minecraft.client.renderer.SubmitNodeCollector");
|
||||
} catch (ClassNotFoundException e) {
|
||||
return null;
|
||||
}
|
||||
Object proxy = java.lang.reflect.Proxy.newProxyInstance(
|
||||
VanillaModelDump.class.getClassLoader(), new Class<?>[] {collector}, (px, m, a) -> {
|
||||
if (m.getName().equals("order")) return px;
|
||||
if (a != null && first[0] == null && m.getName().startsWith("submitModel")) {
|
||||
for (Object arg : a) {
|
||||
if (arg instanceof com.mojang.blaze3d.vertex.PoseStack ps) {
|
||||
first[0] = ps.last().pose().get(new float[16]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Class<?> rt = m.getReturnType();
|
||||
if (rt == boolean.class) return false;
|
||||
if (rt.isPrimitive() && rt != void.class) return 0;
|
||||
return null;
|
||||
});
|
||||
try {
|
||||
Method submit = null;
|
||||
for (Method m : r.getClass().getMethods()) {
|
||||
if (m.getName().equals("submit") && m.getParameterCount() == 4 && !m.isBridge()) submit = m;
|
||||
}
|
||||
if (submit == null) return null;
|
||||
submit.invoke(r, state, new com.mojang.blaze3d.vertex.PoseStack(), proxy, null);
|
||||
} catch (Throwable ignored) {
|
||||
// partial submissions still count (layers may fail on the null context)
|
||||
}
|
||||
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 = null;
|
||||
for (Class<?> c = r.getClass(); c != null && m == null; c = c.getSuperclass()) {
|
||||
for (Method dm : c.getDeclaredMethods()) {
|
||||
if (dm.getName().equals("scale")
|
||||
&& dm.getParameterCount() == 2
|
||||
&& dm.getParameterTypes()[1].getSimpleName().equals("PoseStack")
|
||||
&& !dm.isBridge()) {
|
||||
m = dm;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m != null) {
|
||||
m.setAccessible(true);
|
||||
com.mojang.blaze3d.vertex.PoseStack ps = new com.mojang.blaze3d.vertex.PoseStack();
|
||||
m.invoke(r, state, ps);
|
||||
float sc = ps.last().pose().getScale(new org.joml.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}) {
|
||||
try {
|
||||
Object st = r.createRenderState();
|
||||
try {
|
||||
st.getClass().getField("isBaby").setBoolean(st, baby);
|
||||
} catch (NoSuchFieldException e) {
|
||||
if (baby) continue;
|
||||
}
|
||||
Method gt = null;
|
||||
for (Class<?> c = r.getClass(); c != null && gt == null; c = c.getSuperclass()) {
|
||||
for (Method dm : c.getDeclaredMethods())
|
||||
if (dm.getName().equals("getTextureLocation") && dm.getParameterCount() == 1 && !dm.isBridge())
|
||||
gt = dm;
|
||||
}
|
||||
if (gt == null) break;
|
||||
gt.setAccessible(true);
|
||||
Object id = gt.invoke(r, st);
|
||||
if (id != null) {
|
||||
String path = id.toString().replaceFirst("^minecraft:", "")
|
||||
.replaceFirst("^textures/", "")
|
||||
.replaceFirst("\\.png$", "");
|
||||
o.addProperty(baby ? "babyTexture" : "texture", path);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
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<String, Model<?>> 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. */
|
||||
@SuppressWarnings("unchecked")
|
||||
static List<String> findPath(ModelPart from, ModelPart target, List<String> path) {
|
||||
if (from == target) return path;
|
||||
Map<String, ModelPart> children = (Map<String, ModelPart>) fieldValue(from, "children");
|
||||
if (children == null) return null;
|
||||
for (Map.Entry<String, ModelPart> c : children.entrySet()) {
|
||||
List<String> p = new ArrayList<>(path);
|
||||
p.add(c.getKey());
|
||||
List<String> r = findPath(c.getValue(), target, p);
|
||||
if (r != null) return r;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
static JsonObject dump(Model<?> model, Object state, String name, List<String> failed) {
|
||||
ModelPart root = model.root();
|
||||
ModelLayerLocation loc = BAKED.get(root);
|
||||
List<String> 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<Object, ModelLayerLocation> b : BAKED.entrySet()) {
|
||||
List<String> 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;
|
||||
}
|
||||
// Pose with the renderer's default state; fall back to a fresh state of the model's own type.
|
||||
boolean posed = false;
|
||||
if (state != null) {
|
||||
try {
|
||||
((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 {
|
||||
Object st = m.getParameterTypes()[0].getDeclaredConstructor().newInstance();
|
||||
m.invoke(model, st);
|
||||
posed = true;
|
||||
break;
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!posed) failed.add(name + " (rest pose: setupAnim failed)");
|
||||
|
||||
LayerDefinition def = roots.get(loc);
|
||||
PartDefinition rootDef = ((net.minecraft.client.model.geom.builders.MeshDefinition)
|
||||
fieldValue(def, "mesh"))
|
||||
.getRoot();
|
||||
for (String seg : subPath) rootDef = rootDef.getChild(seg);
|
||||
Object mat = fieldValue(def, "material");
|
||||
int tw = (int) fieldValue(mat, "xTexSize"), th = (int) fieldValue(mat, "yTexSize");
|
||||
|
||||
JsonObject jem = new JsonObject();
|
||||
JsonArray ts = new JsonArray();
|
||||
ts.add(tw);
|
||||
ts.add(th);
|
||||
jem.add("textureSize", ts);
|
||||
jem.addProperty("absolute", true);
|
||||
jem.addProperty("layer", loc.model().getPath() + "#" + loc.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<CubeDefinition>) fieldValue(def, "cubes")) {
|
||||
org.joml.Vector3fc org = (org.joml.Vector3fc) fieldValue(c, "origin");
|
||||
org.joml.Vector3fc dim = (org.joml.Vector3fc) fieldValue(c, "dimensions");
|
||||
Object grow = fieldValue(c, "grow");
|
||||
float gx = (float) fieldValue(grow, "growX"), gy = (float) fieldValue(grow, "growY"),
|
||||
gz = (float) fieldValue(grow, "growZ");
|
||||
Object tex = fieldValue(c, "texCoord"), texScale = fieldValue(c, "texScale");
|
||||
boolean mirror = (boolean) fieldValue(c, "mirror");
|
||||
Set<Direction> faces = (Set<Direction>) 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 (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<String, PartDefinition> ce : def.getChildren()) {
|
||||
ModelPart child = p.getChild(ce.getKey());
|
||||
subs.add(part(ce.getKey(), ce.getValue(), child, 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;
|
||||
}
|
||||
}
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds the edge-case test plugin (PPTest.jar) against the compiled PixelPics classes.
|
||||
# Usage: ./gradlew compileJava && tools/edgecase-plugin/build.sh <compile-classpath> [out.jar]
|
||||
# (classpath = paper-api + its deps, e.g. the Gradle compileClasspath joined with ":")
|
||||
# Then drop the jar into the TEST server's plugins/ (never production), start the server and run
|
||||
# `pptest all` (or `pptest <scene>`) in its console; renders land in plugins/PPTest/<scene>.png.
|
||||
# Scenes are built at world (5000 + i*64, 200, 5000) on stone platforms; see PPTest#onEnable.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
API="${1:?compile classpath (paper-api + deps)}"
|
||||
OUT="${2:-$ROOT/build/PPTest.jar}"
|
||||
JAVAC="${JAVAC:-javac}"
|
||||
TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
"$JAVAC" -nowarn -d "$TMP" -cp "$ROOT/build/classes/java/main:$API" "$ROOT/tools/edgecase-plugin/pptest/PPTest.java"
|
||||
cp "$ROOT/tools/edgecase-plugin/plugin.yml" "$TMP/"
|
||||
(cd "$TMP" && zip -q -r "$OUT" .)
|
||||
echo "Wrote $OUT"
|
||||
@@ -0,0 +1,8 @@
|
||||
name: PPTest
|
||||
version: '1'
|
||||
main: pptest.PPTest
|
||||
api-version: '1.21'
|
||||
depend: [PixelPics]
|
||||
commands:
|
||||
pptest:
|
||||
usage: /pptest [scene...|all]
|
||||
@@ -0,0 +1,395 @@
|
||||
package pptest;
|
||||
|
||||
import eu.mhsl.minecraft.pixelpics.Main;
|
||||
import eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer;
|
||||
import eu.mhsl.minecraft.pixelpics.render.render.RenderJob;
|
||||
import eu.mhsl.minecraft.pixelpics.render.render.Resolution;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
import javax.imageio.ImageIO;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.block.data.type.Bed;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.*;
|
||||
import org.bukkit.inventory.EquipmentSlot;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.bukkit.util.Transformation;
|
||||
import org.bukkit.util.Vector;
|
||||
import org.joml.AxisAngle4f;
|
||||
import org.joml.Quaternionf;
|
||||
import org.joml.Vector3f;
|
||||
|
||||
/** Test-only plugin: builds edge-case scenes and renders them through PixelPics into PNGs. */
|
||||
public class PPTest extends JavaPlugin {
|
||||
|
||||
record Scene(String name, Consumer<Location> build, double camX, double camY, double camZ, double lookX, double lookY, double lookZ) {}
|
||||
|
||||
private final Map<String, Scene> scenes = new LinkedHashMap<>();
|
||||
private World w;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
// Each scene sits on its own stone platform; the camera is placed relative to the platform origin.
|
||||
scenes.put("held", new Scene("held", this::held, 4.4, 2.6, -8.5, 4.4, 1.0, 1));
|
||||
scenes.put("equip", new Scene("equip", this::equip, 4, 3, -8.5, 4, 1.0, 1.5));
|
||||
scenes.put("riding", new Scene("riding", this::riding, 5, 3, -7.5, 5, 1.2, 1));
|
||||
scenes.put("leash", new Scene("leash", this::leash, 5, 4, -9, 5, 2.5, 2));
|
||||
scenes.put("displays", new Scene("displays", this::displays, 5, 3, -9, 5, 1.0, 1.5));
|
||||
scenes.put("pixelblock", new Scene("pixelblock", this::pixelblock, 1.5, 1.9, -1.3, 0.5, 0.5, 0.5));
|
||||
scenes.put("misc", new Scene("misc", this::misc, 5, 3, -7, 5, 0.8, 1.5));
|
||||
scenes.put("dbg", new Scene("dbg", this::dbg, 1, 1.6, -3, 1, 0.8, 0.5));
|
||||
scenes.put("vehicles", new Scene("vehicles", this::vehicles, 5, 3, -7, 5, 0.6, 1.5));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
w = Bukkit.getWorlds().getFirst();
|
||||
w.setTime(6000);
|
||||
w.setStorm(false);
|
||||
List<String> todo = args.length == 0 || args[0].equals("all") ? new ArrayList<>(scenes.keySet()) : List.of(args);
|
||||
int i = 0;
|
||||
for (String n : todo) {
|
||||
Scene sc = scenes.get(n);
|
||||
if (sc == null) { getLogger().warning("unknown scene " + n); continue; }
|
||||
int idx = new ArrayList<>(scenes.keySet()).indexOf(n);
|
||||
Location origin = new Location(w, 5000 + idx * 64, 200, 5000);
|
||||
Bukkit.getScheduler().runTaskLater(this, () -> run(sc, origin), 1 + i * 60L);
|
||||
i++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void run(Scene sc, Location o) {
|
||||
try {
|
||||
prepareArea(o);
|
||||
sc.build.accept(o.clone());
|
||||
} catch (Throwable t) {
|
||||
getLogger().severe("build " + sc.name + " failed: " + t);
|
||||
t.printStackTrace();
|
||||
}
|
||||
// Let one tick pass (passengers/leashes settle), then render.
|
||||
Bukkit.getScheduler().runTaskLater(this, () -> render(sc, o), 10);
|
||||
}
|
||||
|
||||
private void render(Scene sc, Location o) {
|
||||
DefaultScreenRenderer r = Main.getInstance().getScreenRenderer();
|
||||
Location eye = o.clone().add(sc.camX, sc.camY, sc.camZ);
|
||||
Vector look = o.clone().add(sc.lookX, sc.lookY, sc.lookZ).toVector().subtract(eye.toVector());
|
||||
eye.setDirection(look);
|
||||
RenderJob job = r.prepare(eye, new Resolution(480, 320), null);
|
||||
if (sc.name.equals("dbg")) {
|
||||
try {
|
||||
java.lang.reflect.Field f = DefaultScreenRenderer.class.getDeclaredField("objectBaker");
|
||||
f.setAccessible(true);
|
||||
var ob = (eu.mhsl.minecraft.pixelpics.render.entity.ObjectBaker) f.get(r);
|
||||
for (var st : job.objects()) {
|
||||
double[] pos = st.toWorld().t;
|
||||
if (Math.abs(pos[0] - o.getX()) > 5 || Math.abs(pos[2] - o.getZ()) > 5) continue;
|
||||
try {
|
||||
var re = ob.bake(st);
|
||||
getLogger().info("dbg obj " + st.kind() + " item=" + st.item() + " ctx=" + st.context() + " -> "
|
||||
+ (re == null ? "null" : re.cubes.size() + " cubes " + java.util.Arrays.toString(re.aabbMin)));
|
||||
} catch (Throwable t) {
|
||||
getLogger().warning("dbg bake failed: " + t);
|
||||
t.printStackTrace();
|
||||
}
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
getLogger().warning("dbg: " + t);
|
||||
}
|
||||
}
|
||||
getLogger().info("scene " + sc.name + ": " + job.entities().size() + " entities, " + job.objects().size()
|
||||
+ " objects, " + job.blockEntities().size() + " block entities");
|
||||
Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
|
||||
try {
|
||||
BufferedImage img = r.execute(job);
|
||||
File f = new File(getDataFolder(), sc.name + ".png");
|
||||
f.getParentFile().mkdirs();
|
||||
ImageIO.write(img, "png", f);
|
||||
getLogger().info("WROTE " + f);
|
||||
} catch (Throwable t) {
|
||||
getLogger().severe("render " + sc.name + " failed: " + t);
|
||||
t.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Clears the area (entities + blocks) and lays a 14x14 stone floor at the origin's y-1. */
|
||||
private void prepareArea(Location o) {
|
||||
for (int cx = -1; cx <= 2; cx++)
|
||||
for (int cz = -2; cz <= 2; cz++) {
|
||||
Chunk c = w.getChunkAt((o.getBlockX() >> 4) + cx, (o.getBlockZ() >> 4) + cz);
|
||||
c.load(true);
|
||||
c.addPluginChunkTicket(this);
|
||||
}
|
||||
for (Entity e : w.getNearbyEntities(o.clone().add(6, 5, 2), 20, 20, 20)) if (!(e instanceof Player)) e.remove();
|
||||
for (int x = -6; x < 18; x++)
|
||||
for (int z = -12; z < 12; z++)
|
||||
for (int y = -1; y < 12; y++)
|
||||
w.getBlockAt(o.getBlockX() + x, o.getBlockY() + y, o.getBlockZ() + z)
|
||||
.setType(y == -1 ? Material.SMOOTH_STONE : Material.AIR, false);
|
||||
}
|
||||
|
||||
// --- helpers ---------------------------------------------------------------------------------
|
||||
|
||||
private <T extends Entity> T spawn(Location o, double x, double y, double z, float yaw, Class<T> type) {
|
||||
Location l = o.clone().add(x, y, z);
|
||||
l.setYaw(yaw);
|
||||
T e = w.spawn(l, type, en -> {
|
||||
if (en instanceof Mob m) m.setAI(false);
|
||||
en.setPersistent(false);
|
||||
});
|
||||
if (e instanceof LivingEntity le) le.setRemoveWhenFarAway(false);
|
||||
return e;
|
||||
}
|
||||
|
||||
private Entity spawn(Location o, double x, double y, double z, float yaw, EntityType type) {
|
||||
Location l = o.clone().add(x, y, z);
|
||||
l.setYaw(yaw);
|
||||
Entity e = w.spawnEntity(l, type);
|
||||
if (e instanceof Mob m) m.setAI(false);
|
||||
return e;
|
||||
}
|
||||
|
||||
private static ItemStack it(Material m) {
|
||||
return new ItemStack(m);
|
||||
}
|
||||
|
||||
private static void hands(Entity e, Material main, Material off) {
|
||||
if (!(e instanceof LivingEntity le) || le.getEquipment() == null) return;
|
||||
if (main != null) le.getEquipment().setItemInMainHand(it(main));
|
||||
if (off != null) le.getEquipment().setItemInOffHand(it(off));
|
||||
}
|
||||
|
||||
// --- scenes ----------------------------------------------------------------------------------
|
||||
|
||||
private void held(Location o) {
|
||||
hands(spawn(o, 0, 0, 0, 180, Zombie.class), Material.IRON_SWORD, Material.SHIELD);
|
||||
hands(spawn(o, 1.6, 0, 0, 180, Skeleton.class), Material.BOW, null);
|
||||
hands(spawn(o, 3.2, 0, 0, 180, Piglin.class), Material.GOLDEN_SWORD, null);
|
||||
hands(spawn(o, 4.8, 0, 0, 180, EntityType.MANNEQUIN), Material.DIAMOND_PICKAXE, Material.TORCH);
|
||||
ArmorStand as = spawn(o, 6.4, 0, 0, 180, ArmorStand.class);
|
||||
as.setArms(true);
|
||||
hands(as, Material.TRIDENT, Material.GRASS_BLOCK);
|
||||
hands(spawn(o, 8.0, 0, 0, 180, Drowned.class), Material.TRIDENT, null);
|
||||
hands(spawn(o, 0.8, 0, 2, 180, Pillager.class), Material.CROSSBOW, null);
|
||||
hands(spawn(o, 2.4, 0, 2, 180, Vindicator.class), Material.IRON_AXE, null);
|
||||
hands(spawn(o, 4.0, 0, 2, 180, Fox.class), Material.SWEET_BERRIES, null);
|
||||
hands(spawn(o, 5.6, 0.5, 2, 180, Allay.class), Material.DIAMOND, null);
|
||||
hands(spawn(o, 7.2, 0, 2, 180, Witch.class), Material.POTION, null);
|
||||
hands(spawn(o, 8.8, 0, 2, 180, Villager.class), Material.EMERALD, null);
|
||||
}
|
||||
|
||||
private void equip(Location o) {
|
||||
LivingEntity m = (LivingEntity) spawn(o, 0, 0, 0, 180, EntityType.MANNEQUIN);
|
||||
m.getEquipment().setChestplate(it(Material.ELYTRA));
|
||||
m.getEquipment().setHelmet(it(Material.IRON_HELMET));
|
||||
LivingEntity m2 = (LivingEntity) spawn(o, 1.6, 0, 0, 0, EntityType.MANNEQUIN); // back view: elytra
|
||||
m2.getEquipment().setChestplate(it(Material.ELYTRA));
|
||||
ArmorStand pumpkin = spawn(o, 3.2, 0, 0, 180, ArmorStand.class);
|
||||
pumpkin.getEquipment().setHelmet(it(Material.CARVED_PUMPKIN));
|
||||
pumpkin.getEquipment().setChestplate(it(Material.CHAINMAIL_CHESTPLATE));
|
||||
ArmorStand head = spawn(o, 4.8, 0, 0, 180, ArmorStand.class);
|
||||
head.getEquipment().setHelmet(it(Material.CREEPER_HEAD));
|
||||
head.setInvisible(true);
|
||||
Zombie z = spawn(o, 6.4, 0, 0, 180, Zombie.class);
|
||||
z.getEquipment().setHelmet(it(Material.GOLDEN_HELMET));
|
||||
z.getEquipment().setChestplate(it(Material.COPPER_CHESTPLATE));
|
||||
z.getEquipment().setLeggings(it(Material.NETHERITE_LEGGINGS));
|
||||
z.getEquipment().setBoots(it(Material.LEATHER_BOOTS));
|
||||
spawn(o, 8.0, 0, 0, 180, Snowman.class);
|
||||
Wolf wolf = spawn(o, 1, 0, 2.5, 180, Wolf.class);
|
||||
wolf.getEquipment().setItem(EquipmentSlot.BODY, it(Material.WOLF_ARMOR));
|
||||
Horse h = spawn(o, 4, 0, 3, 90, Horse.class);
|
||||
h.getInventory().setSaddle(it(Material.SADDLE));
|
||||
h.getInventory().setArmor(it(Material.GOLDEN_HORSE_ARMOR));
|
||||
Zombie baby = spawn(o, 7.5, 0, 2.5, 180, Zombie.class);
|
||||
baby.setBaby();
|
||||
baby.getEquipment().setHelmet(it(Material.DIAMOND_HELMET));
|
||||
hands(baby, Material.STONE_SWORD, null);
|
||||
}
|
||||
|
||||
private void riding(Location o) {
|
||||
Spider sp = spawn(o, 0, 0, 0, 180, Spider.class);
|
||||
sp.addPassenger(spawn(o, 0, 1, 0, 180, Skeleton.class));
|
||||
Chicken ch = spawn(o, 2, 0, 0, 180, Chicken.class);
|
||||
Zombie bz = spawn(o, 2, 1, 0, 180, Zombie.class);
|
||||
bz.setBaby();
|
||||
ch.addPassenger(bz);
|
||||
Horse h = spawn(o, 4.5, 0, 0.5, 180, Horse.class);
|
||||
h.getInventory().setSaddle(it(Material.SADDLE));
|
||||
h.addPassenger(spawn(o, 4.5, 2, 0.5, 180, EntityType.MANNEQUIN));
|
||||
Minecart mc = spawn(o, 7, 0, 0, 0, Minecart.class);
|
||||
mc.addPassenger(spawn(o, 7, 1, 0, 180, Villager.class));
|
||||
Boat b = (Boat) spawn(o, 9.5, 0, 0, 180, EntityType.OAK_BOAT);
|
||||
b.addPassenger(spawn(o, 9.5, 1, 0, 180, Zombie.class));
|
||||
b.addPassenger(spawn(o, 9.5, 1, 0, 180, Pig.class));
|
||||
Camel c = spawn(o, 2, 0, 3.5, 90, Camel.class);
|
||||
c.getInventory().setSaddle(it(Material.SADDLE));
|
||||
c.addPassenger(spawn(o, 2, 2, 3.5, 90, EntityType.MANNEQUIN));
|
||||
c.addPassenger(spawn(o, 2, 2, 3.5, 90, Villager.class));
|
||||
Strider st = spawn(o, 6, 0, 3.5, 180, Strider.class);
|
||||
st.setSaddle(true);
|
||||
st.addPassenger(spawn(o, 6, 2, 3.5, 180, Piglin.class));
|
||||
Boat cb = (Boat) spawn(o, 9, 0, 3.5, 180, EntityType.BIRCH_CHEST_BOAT);
|
||||
cb.addPassenger(spawn(o, 9, 1, 3.5, 180, EntityType.MANNEQUIN));
|
||||
}
|
||||
|
||||
private void leash(Location o) {
|
||||
Block fence = o.clone().add(0, 0, 0).getBlock();
|
||||
fence.setType(Material.OAK_FENCE);
|
||||
LeashHitch knot = w.spawn(fence.getLocation(), LeashHitch.class);
|
||||
Cow cow = spawn(o, 2.5, 0, 0.5, 180, Cow.class);
|
||||
cow.setLeashHolder(knot);
|
||||
LivingEntity holder = (LivingEntity) spawn(o, 5, 0, 0, 180, EntityType.MANNEQUIN);
|
||||
Wolf wolf = spawn(o, 7, 0, 0.5, 180, Wolf.class);
|
||||
wolf.setLeashHolder(holder);
|
||||
HappyGhast g = spawn(o, 4, 4, 5, 180, HappyGhast.class);
|
||||
g.getEquipment().setItem(EquipmentSlot.BODY, it(Material.BLUE_HARNESS));
|
||||
Boat boat = (Boat) spawn(o, 4, 0, 4, 180, EntityType.OAK_BOAT);
|
||||
try {
|
||||
boat.getClass().getMethod("setLeashHolder", Entity.class).invoke(boat, g);
|
||||
} catch (Throwable t) {
|
||||
getLogger().warning("boat not leashable: " + t);
|
||||
}
|
||||
Llama l1 = spawn(o, 9, 0, 2, 180, Llama.class);
|
||||
Llama l2 = spawn(o, 10.5, 0, 3.5, 180, Llama.class);
|
||||
l2.setLeashHolder(l1);
|
||||
l1.getInventory().setDecor(it(Material.RED_CARPET));
|
||||
l1.setCarryingChest(true);
|
||||
}
|
||||
|
||||
private void displays(Location o) {
|
||||
// Text displays: default background, coloured + shadowed + billboard center, multi-line wrapped.
|
||||
TextDisplay t1 = spawn(o, 0.5, 1.5, 0, 0, TextDisplay.class);
|
||||
t1.text(Component.text("Default Text"));
|
||||
TextDisplay t2 = spawn(o, 2.5, 1.5, 0, 0, TextDisplay.class);
|
||||
t2.text(Component.text("Billboard CENTER", NamedTextColor.GOLD));
|
||||
t2.setBillboard(Display.Billboard.CENTER);
|
||||
t2.setShadowed(true);
|
||||
t2.setBackgroundColor(Color.fromARGB(255, 30, 30, 90));
|
||||
TextDisplay t3 = spawn(o, 5, 1.5, 0, 180, TextDisplay.class);
|
||||
t3.text(Component.text("Dies ist ein langer Text, der umgebrochen werden sollte weil er breit ist"));
|
||||
t3.setLineWidth(80);
|
||||
// Item displays.
|
||||
ItemDisplay i1 = spawn(o, 7, 1, 0, 0, ItemDisplay.class);
|
||||
i1.setItemStack(it(Material.DIAMOND_SWORD));
|
||||
i1.setTransformation(new Transformation(new Vector3f(), new AxisAngle4f((float) Math.toRadians(45), 0, 0, 1),
|
||||
new Vector3f(1, 1, 1), new AxisAngle4f()));
|
||||
ItemDisplay i2 = spawn(o, 8.5, 0.5, 0, 30, ItemDisplay.class);
|
||||
i2.setItemStack(it(Material.CHEST));
|
||||
ItemDisplay i3 = spawn(o, 0.5, 0.5, 2.5, 0, ItemDisplay.class);
|
||||
i3.setItemStack(it(Material.PLAYER_HEAD));
|
||||
ItemDisplay i4 = spawn(o, 2, 0.5, 2.5, 0, ItemDisplay.class);
|
||||
i4.setItemStack(it(Material.SHIELD));
|
||||
// Block displays: rotated stairs, scaled chest (block entity!), banner, bed, glass.
|
||||
BlockDisplay b1 = spawn(o, 3.5, 0, 2.5, 0, BlockDisplay.class);
|
||||
b1.setBlock(Bukkit.createBlockData("minecraft:oak_stairs[facing=east]"));
|
||||
b1.setTransformation(new Transformation(new Vector3f(), new Quaternionf().rotateY(0.6f), new Vector3f(0.8f), new Quaternionf()));
|
||||
BlockDisplay b2 = spawn(o, 5, 0, 2.5, 0, BlockDisplay.class);
|
||||
b2.setBlock(Bukkit.createBlockData("minecraft:chest[facing=north]"));
|
||||
BlockDisplay b3 = spawn(o, 6.5, 0, 2.5, 0, BlockDisplay.class);
|
||||
b3.setBlock(Bukkit.createBlockData("minecraft:red_banner"));
|
||||
BlockDisplay b4 = spawn(o, 8, 0, 2.5, 0, BlockDisplay.class);
|
||||
b4.setBlock(Bukkit.createBlockData("minecraft:light_blue_stained_glass"));
|
||||
BlockDisplay b5 = spawn(o, 9.5, 0, 2.5, 0, BlockDisplay.class);
|
||||
b5.setBlock(Bukkit.createBlockData("minecraft:oak_sign"));
|
||||
}
|
||||
|
||||
/** Mimics PixelBlocks: 1/16-scaled block displays forming a mini build + the placeholder displays. */
|
||||
private void pixelblock(Location o) {
|
||||
float s = 1 / 16f;
|
||||
String[] palette = {"minecraft:oak_planks", "minecraft:stone_bricks", "minecraft:glass", "minecraft:red_wool",
|
||||
"minecraft:chest[facing=north]", "minecraft:red_bed[part=head,facing=north]", "minecraft:oak_stairs[facing=south]",
|
||||
"minecraft:torch", "minecraft:grass_block"};
|
||||
Random rnd = new Random(1);
|
||||
for (int x = 0; x < 16; x++)
|
||||
for (int z = 0; z < 16; z++) {
|
||||
int h = 1 + (int) (3 * Math.abs(Math.sin(x * 0.4) * Math.cos(z * 0.3)));
|
||||
for (int y = 0; y < h; y++) {
|
||||
String b = y == h - 1 ? palette[rnd.nextInt(palette.length)] : "minecraft:stone_bricks";
|
||||
pixel(o, x, y, z, s, Bukkit.createBlockData(b));
|
||||
}
|
||||
}
|
||||
// Placeholder look (small pixel blocks): end-crystal item cores + glass container, next to it.
|
||||
Location p = o.clone().add(-1.5, 0.5, 0.5);
|
||||
for (int i = 0; i <= 90; i += 90) {
|
||||
ItemDisplay core = w.spawn(p, ItemDisplay.class);
|
||||
core.setRotation(i, 0);
|
||||
core.setItemStack(it(Material.END_CRYSTAL));
|
||||
Transformation t = core.getTransformation();
|
||||
t.getScale().set(0.5);
|
||||
core.setTransformation(t);
|
||||
}
|
||||
ItemDisplay glass = w.spawn(p, ItemDisplay.class);
|
||||
glass.setItemStack(it(Material.WHITE_STAINED_GLASS));
|
||||
Interaction hit = w.spawn(o.clone().add(0.5, 0, 0.5), Interaction.class);
|
||||
hit.setInteractionWidth(1);
|
||||
hit.setInteractionHeight(1);
|
||||
}
|
||||
|
||||
private void pixel(Location o, int x, int y, int z, float s, BlockData data) {
|
||||
Location l = o.clone().add(x * s + s / 2, y * s + s / 2, z * s + s / 2);
|
||||
BlockDisplay d = w.spawn(l, BlockDisplay.class);
|
||||
d.setBlock(data);
|
||||
Transformation t = d.getTransformation();
|
||||
t.getScale().set(s);
|
||||
t.getTranslation().set(-s / 2, -s / 2, -s / 2);
|
||||
d.setTransformation(t);
|
||||
}
|
||||
|
||||
private void misc(Location o) {
|
||||
w.dropItem(o.clone().add(0, 0.2, 0), it(Material.DIAMOND_SWORD), i -> i.setVelocity(new Vector()));
|
||||
w.dropItem(o.clone().add(1, 0.2, 0), it(Material.OAK_LOG), i -> i.setVelocity(new Vector()));
|
||||
w.dropItem(o.clone().add(2, 0.2, 0), new ItemStack(Material.COBBLESTONE, 32), i -> i.setVelocity(new Vector()));
|
||||
w.dropItem(o.clone().add(3, 0.2, 0), it(Material.APPLE), i -> i.setVelocity(new Vector()));
|
||||
FallingBlock fb = w.spawn(o.clone().add(4.5, 0.5, 0.5), FallingBlock.class, f -> f.setGravity(false));
|
||||
fb.setBlockData(Bukkit.createBlockData("minecraft:sand"));
|
||||
TNTPrimed tnt = w.spawn(o.clone().add(6, 0, 0.5), TNTPrimed.class);
|
||||
tnt.setFuseTicks(10000);
|
||||
tnt.setGravity(false);
|
||||
w.spawn(o.clone().add(8, 0, 0.5), EnderCrystal.class);
|
||||
Sheep sh = spawn(o, 0.5, 0, 3, 180, Sheep.class);
|
||||
sh.setSheared(true);
|
||||
Sheep sh2 = spawn(o, 2.5, 0, 3, 180, Sheep.class);
|
||||
sh2.setColor(DyeColor.LIME);
|
||||
ArmorStand small = spawn(o, 4.5, 0, 3, 180, ArmorStand.class);
|
||||
small.setSmall(true);
|
||||
small.getEquipment().setHelmet(it(Material.LEATHER_HELMET));
|
||||
spawn(o, 6, 0, 3, 180, EntityType.EXPERIENCE_ORB);
|
||||
Arrow a = w.spawn(o.clone().add(7, 0.5, 3), Arrow.class, ar -> ar.setGravity(false));
|
||||
a.setVelocity(new Vector(1, -0.4, 0));
|
||||
Trident tr = w.spawn(o.clone().add(9, 0.5, 3), Trident.class, ar -> ar.setGravity(false));
|
||||
tr.setVelocity(new Vector(0, -1, 0.2));
|
||||
spawn(o, 10, 0, 0, 180, TropicalFish.class);
|
||||
}
|
||||
|
||||
private void dbg(Location o) {
|
||||
TextDisplay t1 = spawn(o, 0, 1.2, 0, 0, TextDisplay.class);
|
||||
t1.text(Component.text("Default Text"));
|
||||
ItemDisplay i4 = spawn(o, 2, 0.5, 0.5, 0, ItemDisplay.class);
|
||||
i4.setItemStack(it(Material.SHIELD));
|
||||
getLogger().info("dbg text=" + t1.isValid() + " opacity=" + t1.getTextOpacity() + " shield=" + i4.getItemStack());
|
||||
}
|
||||
|
||||
private void vehicles(Location o) {
|
||||
spawn(o, 0, 0, 0, 0, EntityType.CHEST_MINECART);
|
||||
spawn(o, 1.8, 0, 0, 90, EntityType.FURNACE_MINECART);
|
||||
spawn(o, 3.6, 0, 0, 0, EntityType.HOPPER_MINECART);
|
||||
spawn(o, 5.4, 0, 0, 0, EntityType.TNT_MINECART);
|
||||
spawn(o, 7.2, 0, 0, 0, EntityType.SPAWNER_MINECART);
|
||||
spawn(o, 9, 0, 0, 0, EntityType.COMMAND_BLOCK_MINECART);
|
||||
spawn(o, 0.5, 0, 3, 30, EntityType.PALE_OAK_BOAT);
|
||||
spawn(o, 3, 0, 3, 60, EntityType.BAMBOO_CHEST_RAFT);
|
||||
spawn(o, 6, 0, 3, 90, EntityType.CHERRY_CHEST_BOAT);
|
||||
spawn(o, 9, 0, 3, 120, EntityType.MANGROVE_BOAT);
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,9 @@
|
||||
# Source: the CEM Template Loader data (Ewan Howell / wynem) — vanilla Java entity models as JSON,
|
||||
# already correctly posed. These ultimately mirror Mojang's hardcoded Java EntityModel classes.
|
||||
#
|
||||
# Most future-proof alternative for a NEW Minecraft version (sourced straight from the game):
|
||||
# 1. Install the JsonEM mod (Fabric), set dump_models=true in .minecraft/config/jsonem.properties
|
||||
# 2. Launch the game once -> models dumped to .minecraft/jsonem_dump
|
||||
# (a small format-adapter would be needed; the CEM source below covers vanilla until then.)
|
||||
# NOTE: this set is now only the FALLBACK (rest-pose geometry) + the block-entity models (chests, signs,
|
||||
# heads, banners, armor_layer_*). Mob models come from the vanilla client dump — regenerate that with
|
||||
# tools/update-vanilla-models.sh <version> on every Minecraft update.
|
||||
#
|
||||
# Usage: tools/update-cem-models.sh # update if newer
|
||||
# tools/update-cem-models.sh --force # always overwrite
|
||||
|
||||
Executable
+52
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regenerates the vanilla entity-model dump straight from a Minecraft CLIENT jar (26.x+ ships
|
||||
# unobfuscated, so no mappings are needed). This is the primary entity-model source of PixelPics:
|
||||
# every mob is posed by its own renderer's setupAnim, and new entities/layers/baby/variant models of
|
||||
# the new version are picked up automatically. Run it once per Minecraft update.
|
||||
#
|
||||
# Needs the version installed in the launcher (~/.minecraft/versions/<ver>/<ver>.json + jar + libraries).
|
||||
#
|
||||
# Usage: tools/update-vanilla-models.sh 26.3 # -> src/main/resources/cem/ (bundled; rebuild jar)
|
||||
# tools/update-vanilla-models.sh 26.4 <plugin-data-dir> # -> <dir>/vanilla_entity_models.json
|
||||
# # (overrides the bundled copy, no rebuild)
|
||||
set -euo pipefail
|
||||
|
||||
VER="${1:?usage: $0 <minecraft-version> [output-dir]}"
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
OUT_DIR="${2:-$ROOT/src/main/resources/cem}"
|
||||
MC="${MINECRAFT_DIR:-$HOME/.minecraft}"
|
||||
JSON="$MC/versions/$VER/$VER.json"
|
||||
JAR="$MC/versions/$VER/$VER.jar"
|
||||
JAVA="${JAVA:-$(command -v java)}"
|
||||
[[ -f "$JSON" && -f "$JAR" ]] || { echo "Version $VER not found under $MC/versions (start it once in the launcher)"; exit 1; }
|
||||
|
||||
# Classpath: client jar + all libraries listed in the version json.
|
||||
CP="$(python3 - "$JSON" "$MC" "$JAR" <<'EOF'
|
||||
import json, os, sys
|
||||
j = json.load(open(sys.argv[1])); mc = sys.argv[2]; cp = []
|
||||
for lib in j.get("libraries", []):
|
||||
a = lib.get("downloads", {}).get("artifact")
|
||||
if a:
|
||||
p = os.path.join(mc, "libraries", a["path"])
|
||||
if os.path.exists(p): cp.append(p)
|
||||
cp.append(sys.argv[3])
|
||||
print(":".join(cp))
|
||||
EOF
|
||||
)"
|
||||
|
||||
TMP="$(mktemp --suffix=.json)"
|
||||
trap 'rm -f "$TMP"; rm -rf "${WORK:-}"' EXIT
|
||||
# Run from a scratch dir: the game's logger writes a logs/ folder into the working directory.
|
||||
WORK="$(mktemp -d)"
|
||||
(cd "$WORK" && "$JAVA" -cp "$CP" "$ROOT/tools/VanillaModelDump.java" "$TMP" 2>&1) | grep -E "STDOUT|Exception" | sed 's/.*STDOUT\]: //'
|
||||
python3 -c "
|
||||
import json; d = json.load(open('$TMP'))
|
||||
assert len(d['models']) > 100, 'dump looks incomplete'
|
||||
print('version', d.get('version'), '-', len(d['models']), 'models,', len(d['entities']), 'entity types')"
|
||||
mkdir -p "$OUT_DIR"
|
||||
mv "$TMP" "$OUT_DIR/vanilla_entity_models.json"
|
||||
chmod 644 "$OUT_DIR/vanilla_entity_models.json"
|
||||
rm -rf "$WORK"
|
||||
trap - EXIT
|
||||
echo "Wrote $OUT_DIR/vanilla_entity_models.json"
|
||||
echo "Note: the runtime resource pack must come from the same version (incl. assets/minecraft/font for text)."
|
||||
Reference in new issue
Block a user