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

@@ -14,6 +14,7 @@ import eu.mhsl.minecraft.pixelpics.render.RenderManager;
import eu.mhsl.minecraft.pixelpics.render.entity.cem.BlockEntityBaker;
import eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker;
import eu.mhsl.minecraft.pixelpics.render.entity.cem.CemModelLoader;
import eu.mhsl.minecraft.pixelpics.render.entity.cem.DatapackVariants;
import eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer;
import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider;
import eu.mhsl.minecraft.pixelpics.survival.CameraListener;
@@ -23,6 +24,8 @@ import eu.mhsl.minecraft.pixelpics.survival.SurvivalRecipes;
import eu.mhsl.minecraft.pixelpics.utils.MapColorPalette;
import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.bukkit.Bukkit;
@@ -121,6 +124,10 @@ public final class Main extends JavaPlugin {
} catch (Exception e) {
getLogger().severe("Failed to load vanilla entity models: " + e.getMessage());
}
List<File> worldFolders = new ArrayList<>();
for (org.bukkit.World w : getServer().getWorlds()) worldFolders.add(w.getWorldFolder());
int datapackVariants = DatapackVariants.load(cemLoader, worldFolders, getLogger());
if (datapackVariants > 0) getLogger().info("Loaded " + datapackVariants + " datapack mob variants.");
SkinCache skinCache = new SkinCache();
BitmapFont font = FontLoader.load(resourcePack, textures, getLogger());
getLogger().info("Loaded sign font (" + (font.isEmpty() ? "no glyphs — text disabled" : "ok") + ").");
@@ -106,21 +106,17 @@ public final class EntityModels {
}
/**
* Variant-specific texture paths (modern pack naming is "entity/<folder>/<entity>_<variant>", with a
* handful of mismatches the small maps below normalise). Returned paths are tried before the generic
* Variant-specific texture paths of the ENUM variants vanilla hardcodes in its renderers (modern pack
* naming is "entity/<folder>/<entity>_<variant>", with a handful of mismatches the small maps below
* normalise). Data-driven variants (cat, wolf, frog, cow, pig, chicken, zombie nautilus) are resolved from
* their {@code asset_id} in the vanilla dump instead. Returned paths are tried before the generic
* fallbacks, so an unknown variant still degrades to the base texture.
*/
private static List<String> variantPaths(String typeKey, String v) {
return switch (typeKey) {
case "cat" -> List.of("entity/cat/cat_" + v);
case "axolotl" -> List.of("entity/axolotl/axolotl_" + v);
case "wolf" -> List.of("entity/wolf/wolf_" + v, "entity/wolf/wolf");
case "horse" -> List.of("entity/horse/horse_" + HORSE_COLOR.getOrDefault(v, v));
case "llama" -> List.of("entity/llama/llama_" + v);
case "cow" -> List.of("entity/cow/cow_" + v);
case "pig" -> List.of("entity/pig/pig_" + v);
case "chicken" -> List.of("entity/chicken/chicken_" + v);
case "frog" -> List.of("entity/frog/frog_" + v);
case "panda" -> List.of(v.equals("normal") ? "entity/panda/panda" : "entity/panda/panda_" + v);
case "fox" -> List.of(v.equals("snow") ? "entity/fox/fox_snow" : "entity/fox/fox");
case "parrot" -> List.of("entity/parrot/parrot_" + PARROT_COLOR.getOrDefault(v, v));
@@ -129,8 +125,6 @@ public final class EntityModels {
case "shulker" -> List.of("entity/shulker/shulker_" + v);
case "cushion" -> List.of("entity/cushion/" + v + "_cushion");
case "tropical_fish" -> List.of("entity/fish/tropical_" + (tropicalPattern(v) >= 6 ? "b" : "a"));
case "zombie_nautilus" -> List.of(
v.equals("warm") ? "entity/nautilus/zombie_nautilus_coral" : "entity/nautilus/zombie_nautilus");
case "copper_golem" -> List.of(
v.equals("unaffected")
? "entity/copper_golem/copper_golem"
@@ -16,15 +16,16 @@ public record EntityState(
boolean player,
String skinUrl,
boolean slim,
String variant, // texture-selecting variant key (e.g. "ashen", "warm", "tabby"); for villagers the biome type,
// or null
String variant, // variant key (e.g. "ashen", "warm", "tabby"; "<ns>:<key>" outside minecraft); for villagers
// the biome type, or null
int tint, // ARGB multiplier for tintable layers (sheep wool); 0 = none
double sizeScale, // extra model scale (slime/magma-cube size); 1.0 = default
String profession, // villager profession key (e.g. "farmer", "librarian", "none"), or null
int villagerLevel, // villager profession level 1-5 (badge tier); 0 = none/unknown
String markings, // horse coat markings style (e.g. "white", "whitefield", "whitedots", "blackdots"), or null
boolean saddle, // horse/donkey/mule is saddled
boolean chest, // donkey/mule/llama is carrying a chest
boolean chest, // donkey/mule/llama is carrying a chest (pose flag "chest")
boolean arms, // armor stand shows its arms (pose flag "arms")
String bodyEquip, // BODY-slot equipment asset (horse armor material, llama carpet, harness, wolf armor,
// nautilus armor) or "trader_llama"; null = none
Equipment equipment, // worn humanoid armor (players, armor stands, zombies, …); null = none/not a wearer
@@ -16,11 +16,9 @@ import eu.mhsl.minecraft.pixelpics.render.entity.ModelCube;
import eu.mhsl.minecraft.pixelpics.render.entity.RenderedEntity;
import eu.mhsl.minecraft.pixelpics.render.entity.TextureOps;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
@@ -30,8 +28,10 @@ import java.util.concurrent.ConcurrentHashMap;
* "absolute" models). They are posed by vanilla's own {@code setupAnim} (idle, adult, peaceful), carry
* vanilla part visibility, real baby models, per-variant models, and the renderer's layer models
* (outer layers, saddles, body equipment, humanoid armor, elytra). Model choice is generic and
* name-based ({@code <type>}, {@code <type>/baby}, {@code <type>/<…variant…>}, {@code <type>/<Layer>.…}),
* so mobs of a newer dump work without code changes. Vanilla models are placed with their feet at the
* name-based ({@code <type>}, {@code <type>/baby}, {@code <type>/<…variant…>}, {@code <type>/<Layer>.…});
* which layers vanilla draws by default, who wears humanoid armor, state pose flags (sitting, held items,
* armor-stand arms, chests) and data-driven variant assets ({@code asset_id}, model kind) come from the
* dump too, so mobs of a newer dump work without code changes. Vanilla models are placed with their feet at the
* entity position (model y=0), exactly like vanilla — floating mobs keep their height.
*
* <p>Fallback: the CEM template set (rest geometry only, ground-snapped, no layers) for a type the dump
@@ -39,28 +39,6 @@ import java.util.concurrent.ConcurrentHashMap;
*/
public final class CemBaker implements EntityBaker<EntityState> {
// Textured render layers that are conditional in vanilla (charged creeper, tamed collar, riptide, …) or
// handled explicitly below; every OTHER textured layer of the dump is drawn automatically.
private static final Set<String> CONDITIONAL_LAYERS = Set.of(
"CreeperPowerLayer",
"CatCollarLayer",
"WolfCollarLayer",
"RopesLayer",
"ArrowLayer",
"BeeStingerLayer",
"SpinAttackEffectLayer",
"Deadmau5EarsLayer",
"CapeLayer",
"ParrotOnShoulderLayer",
"WitherArmorLayer",
"SheepWoolLayer",
"SheepWoolUndercoatLayer",
"TropicalFishPatternLayer",
"LlamaDecorLayer",
"HumanoidArmorLayer",
"WingsLayer",
"VillagerProfessionLayer");
// LivingEntityRenderer at yaw 0: rotY(180)·scale(-1,-1,1)·translate(0,-1.501,0) — used when the dump has no
// captured transform for a type.
private static final Affine DEFAULT_LIVING_TRANSFORM =
@@ -150,27 +128,23 @@ public final class CemBaker implements EntityBaker<EntityState> {
place = null; // legacy: computed after baking (ground snap)
}
// Parts that vanilla hides by default but this entity's state shows.
Set<String> show = new HashSet<>();
if (s.typeKey().equals("armor_stand") && "arms".equals(s.variant()))
show.addAll(Set.of("right_arm", "left_arm"));
if (s.chest()) show.addAll(Set.of("left_chest", "right_chest"));
// Body: baked even when invisible — not drawn, but it yields the part frames (and legacy ground snap).
Pose pose = new Pose(pre, vanilla ? selectPose(s, info) : Map.of());
Map<String, Affine> frames = new java.util.HashMap<>();
List<CemGeometry.Baked> body = (model != null && tex != null)
? CemGeometry.bakePosed(model, tex, pre, show, pose.parts, frames)
: List.of();
List<CemGeometry.Baked> body =
(model != null && tex != null) ? CemGeometry.bakePosed(model, tex, pre, pose.parts, frames) : List.of();
List<CemGeometry.Baked> baked = new ArrayList<>();
List<CemGeometry.Baked> glow = new ArrayList<>(); // additive energy-swirl layers
if (!invisible) {
baked.addAll(body);
if (vanilla) addVanillaLayers(s, ch, pose, show, tex, baked, glow);
if (vanilla) addVanillaLayers(s, ch, info, pose, tex, baked, glow);
}
// Humanoid armor / elytra (renderers with a HumanoidArmorLayer): drawn even when invisible (vanilla
// "floating armor").
if (vanilla && s.equipment() != null && info != null && info.humanoidArmor()) {
addArmorLayers(s, ch, pose, baked);
}
// Humanoid armor / elytra: drawn even when invisible (vanilla "floating armor").
if (vanilla && s.equipment() != null) addArmorLayers(s, ch, pose, baked);
List<EntityCube> itemCubes = new ArrayList<>();
if (vanilla && items != null) addItems(s, frames, isIllager(info), itemCubes);
if (baked.isEmpty() && glow.isEmpty() && itemCubes.isEmpty()) return invisible ? null : fallbackBox(s, tex);
@@ -198,15 +172,18 @@ public final class CemBaker implements EntityBaker<EntityState> {
/** The model→pre-space transform plus the pose overrides shared by every vanilla model of one entity. */
private record Pose(Affine pre, Map<String, CemModelLoader.PartPose> parts) {
List<CemGeometry.Baked> bake(CemModelLoader.CemModel m, int[][] tex, Set<String> show) {
return CemGeometry.bakePosed(m, tex, pre, show, parts, null);
List<CemGeometry.Baked> bake(CemModelLoader.CemModel m, int[][] tex) {
return CemGeometry.bakePosed(m, tex, pre, parts, null);
}
}
/**
* Picks the vanilla pose variant for the entity's state: sitting ({@code passenger}), holding items
* ({@code item_r}/{@code item_l}), illagers' crossbow hold. Falls back to fewer flags when the exact
* combination has no own variant (identical to a simpler pose).
* Picks the vanilla pose variant for the entity's state flags — the render-state fields vanilla's
* {@code setupAnim} reads, dumped per type: sitting ({@code passenger}), holding items ({@code item_r}/
* {@code item_l}), illagers' crossbow hold ({@code arms_*}), armor-stand arms ({@code arms}), chests
* ({@code chest}). Falls back to fewer flags when the exact combination has no own variant (identical to
* a simpler pose): first without the held-item flags (e.g. zombies' arms ignore held items), then with
* the part-toggling flags only.
*/
private static Map<String, CemModelLoader.PartPose> selectPose(EntityState s, CemModelLoader.EntityInfo info) {
if (info == null || info.poses().isEmpty()) return Map.of();
@@ -218,13 +195,14 @@ public final class CemBaker implements EntityBaker<EntityState> {
if (s.leftHand() != null) flags.add("item_l");
}
if (s.passenger()) flags.add("passenger");
// Exact combination first, then without the item flags (e.g. zombies' arms ignore held items).
if (s.arms()) flags.add("arms");
if (s.chest()) flags.add("chest");
List<List<String>> tries = new ArrayList<>();
tries.add(flags);
tries.add(flags.stream().filter(f -> !f.startsWith("item_")).toList());
tries.add(flags.stream()
.filter(f -> f.equals("passenger") || f.startsWith("arms_"))
.filter(f -> f.equals("passenger") || f.equals("arms") || f.equals("chest"))
.toList());
tries.add(flags.stream().filter(f -> f.equals("passenger")).toList());
for (List<String> t : tries) {
if (t.isEmpty()) continue;
Map<String, CemModelLoader.PartPose> p =
@@ -328,6 +306,8 @@ public final class CemBaker implements EntityBaker<EntityState> {
if (base.equals("player") && s.slim() && models.hasVanilla("player_slim")) base = "player_slim";
String variant = s.variant() == null ? null : s.variant().toLowerCase(Locale.ROOT);
CemModelLoader.VariantAssets va = models.variant(type, s.variant());
if (va != null) variant = va.model(); // data-driven variant: its model kind ("cold", …; null = normal)
if (type.equals("tropical_fish") && variant != null) {
variant = EntityModels.tropicalPattern(variant) >= 6 ? "large" : "small"; // pattern -> body shape
}
@@ -403,8 +383,8 @@ public final class CemBaker implements EntityBaker<EntityState> {
private void addVanillaLayers(
EntityState s,
Choice ch,
CemModelLoader.EntityInfo info,
Pose pose,
Set<String> show,
int[][] bodyTex,
List<CemGeometry.Baked> baked,
List<CemGeometry.Baked> glow) {
@@ -423,7 +403,7 @@ public final class CemBaker implements EntityBaker<EntityState> {
if (t == null) continue;
t = TextureOps.deepCopy(t);
if (s.tint() != 0) TextureOps.tint(t, s.tint());
baked.addAll(pose.bake(lm, t, show));
baked.addAll(pose.bake(lm, t));
}
case "TropicalFishPatternLayer" -> addTropicalPattern(s, n, lm, pose, baked);
case "CatCollarLayer" -> {
@@ -433,18 +413,18 @@ public final class CemBaker implements EntityBaker<EntityState> {
if (t == null) continue;
t = TextureOps.deepCopy(t);
TextureOps.tint(t, s.collar());
baked.addAll(pose.bake(lm, t, show));
baked.addAll(pose.bake(lm, t));
}
case "CreeperPowerLayer", "WitherArmorLayer" -> {
// EnergySwirlLayer: charged creeper / wither below half health; drawn additively.
if (!s.powered() || lm.texture() == null) continue;
int[][] t = energySwirl(lm.texture(), layerClass.equals("WitherArmorLayer"));
if (t != null) glow.addAll(pose.bake(lm, t, show));
if (t != null) glow.addAll(pose.bake(lm, t));
}
case "SlimeOuterLayer" -> {
// Translucent outer cube on the body texture (alpha < 255 there; blended by the tracer).
int[][] t = lm.texture() != null ? texture(lm.texture()) : bodyTex;
if (t != null) baked.addAll(pose.bake(lm, t, show));
if (t != null) baked.addAll(pose.bake(lm, t));
}
case "LlamaDecorLayer", "WolfArmorLayer" -> {
// Body equipment drawn by a dedicated layer: texture folder <base>_body (trader llama: llama_body).
@@ -454,17 +434,19 @@ public final class CemBaker implements EntityBaker<EntityState> {
t = equipmentTexture(
base.substring(base.indexOf('_') + 1) + "_body", s.bodyEquip(), baby, s.bodyDye());
}
if (t != null) baked.addAll(pose.bake(lm, t, show));
if (t != null) baked.addAll(pose.bake(lm, t));
}
case "RopesLayer" -> {
if (s.bodyEquip() == null || lm.texture() == null) continue; // ropes only with a harness
int[][] t = texture(lm.texture());
if (t != null) baked.addAll(pose.bake(lm, t, show));
if (t != null) baked.addAll(pose.bake(lm, t));
}
default -> {
if (CONDITIONAL_LAYERS.contains(layerClass) || lm.texture() == null) continue;
int[][] t = texture(lm.texture()); // always-on overlay (drowned/stray/bogged outer, wind, …)
if (t != null) baked.addAll(pose.bake(lm, t, show));
// Layers vanilla draws for the default state (drowned/stray/bogged outer, breeze wind, …);
// state-dependent ones (charged, tamed, equipped, …) are handled above or not drawn.
if (info == null || !info.layerDrawnByDefault(layerClass) || lm.texture() == null) continue;
int[][] t = texture(lm.texture());
if (t != null) baked.addAll(pose.bake(lm, t));
}
}
}
@@ -482,7 +464,7 @@ public final class CemBaker implements EntityBaker<EntityState> {
&& !s.bodyEquip().equals("trader_llama")) {
t = equipmentTexture(layerType, s.bodyEquip(), baby, s.bodyDye());
}
if (t != null) baked.addAll(pose.bake(em, t, Set.of()));
if (t != null) baked.addAll(pose.bake(em, t));
}
}
@@ -497,7 +479,7 @@ public final class CemBaker implements EntityBaker<EntityState> {
if (t == null) return;
t = TextureOps.deepCopy(t);
if (s.tint2() != 0) TextureOps.tint(t, s.tint2());
baked.addAll(pose.bake(lm, t, Set.of()));
baked.addAll(pose.bake(lm, t));
}
/** entity/equipment/<layerType>/<asset> (+ a "_carpet"-stripped name; leather dye + overlay). */
@@ -574,8 +556,9 @@ public final class CemBaker implements EntityBaker<EntityState> {
}
/**
* Player skin, else the variant-aware candidates ({@link EntityModels}), the vanilla renderer's default
* texture as a generic fallback, and — for a dedicated baby model — the {@code _baby} counterparts first.
* Player skin, else the data-driven variant's asset (baby / tame aware), else the variant-aware candidates
* ({@link EntityModels}), the vanilla renderer's default texture as a generic fallback, and — for a
* dedicated baby model — the {@code _baby} counterparts first.
*/
private int[][] resolveTexture(EntityState s, Choice ch) {
if (s.player()) {
@@ -584,6 +567,14 @@ public final class CemBaker implements EntityBaker<EntityState> {
int[][] def = texture(s.slim() ? "entity/player/slim/steve" : "entity/player/wide/steve");
if (def != null) return def;
}
boolean babyModel = ch != null && ch.babyModel;
// Data-driven variant (cat/wolf/frog/cow/pig/chicken/…): the texture its definition names.
CemModelLoader.VariantAssets va = models.variant(s.typeKey(), s.variant());
String asset = va == null ? null : va.texture(babyModel, s.collar() != 0);
if (asset != null) {
int[][] t = texture(asset);
if (t != null) return t;
}
CemModelLoader.EntityInfo info = models.entityInfo(s.typeKey());
if (info == null && ch != null) info = models.entityInfo(ch.base);
List<String> adult = new ArrayList<>();
@@ -593,14 +584,8 @@ public final class CemBaker implements EntityBaker<EntityState> {
Math.min(adult.size(), EntityModels.specificCandidateCount(s.typeKey(), s.variant())),
info.texture());
}
if (s.collar() != 0) {
// Tamed: vanilla switches to the "_tame" texture where the variant has one (wolves).
List<String> tame = new ArrayList<>();
for (String a : adult) tame.add(a + "_tame");
adult.addAll(0, tame);
}
List<String> order = new ArrayList<>();
if (ch != null && ch.babyModel) {
if (babyModel) {
for (String a : adult) order.add(a + "_baby");
if (info != null && info.babyTexture() != null) order.add(info.babyTexture());
}
@@ -674,7 +659,7 @@ public final class CemBaker implements EntityBaker<EntityState> {
CemModelLoader.CemModel model = models.vanilla(modelName);
if (model == null) return;
int[][] tex = buildArmorTexture(piece, layerFolder);
if (tex != null) baked.addAll(pose.bake(model, tex, Set.of()));
if (tex != null) baked.addAll(pose.bake(model, tex));
}
/** Resolve + composite a single armor slot's texture: material base (+leather dye/overlay), trim, glint. */
@@ -720,18 +705,17 @@ public final class CemBaker implements EntityBaker<EntityState> {
.ifPresent(glint -> TextureOps.addGlint(tex, glint, GLINT_COLOR, 0.6));
}
/** Elytra in the chest slot: the wearer's own wings model (vanilla), else the CEM elytra model. */
/** Elytra in the chest slot: the wearer's own wings model (vanilla WingsLayer). */
private void bakeElytra(
EntityState.EquipPiece piece, String base, boolean baby, Pose pose, List<CemGeometry.Baked> baked) {
CemModelLoader.CemModel model =
models.vanilla(base + "/WingsLayer." + (baby ? "elytraBabyModel" : "elytraModel"));
if (model == null) model = models.get("elytra");
if (model == null) return;
if (model == null) return; // renderer without a WingsLayer
int[][] tex = texture("entity/equipment/wings/elytra");
if (tex == null) return;
int[][] out = TextureOps.deepCopy(tex);
if (piece.glint()) applyGlint(out);
baked.addAll(pose.bake(model, out, Set.of()));
baked.addAll(pose.bake(model, out));
}
private RenderedEntity fallbackBox(EntityState s, int[][] tex) {
@@ -40,13 +40,13 @@ final class CemGeometry {
/** Bake all parts of a model with the given pre-transform; parts in {@code hidden} are skipped. */
static List<Baked> bakeModel(CemModelLoader.CemModel model, int[][] tex, Affine pre, Set<String> hidden) {
return bake(model, tex, pre, hidden, Set.of(), Map.of(), null);
return bake(model, tex, pre, hidden, Map.of(), null);
}
/**
* Posed variant for vanilla ("absolute") models: {@code show} names parts vanilla hides by default but
* this entity's state draws (chests, armor-stand arms); {@code overrides} replace parts' pivot/rotation/
* visibility by name (pose variants: sitting, holding items, …; boxes follow their pivot); {@code frames},
* Posed variant for vanilla ("absolute") models: {@code overrides} replace parts' pivot/rotation/visibility
* by name (pose variants: sitting, holding items, armor-stand arms, chests, …; boxes follow their pivot;
* a visibility-only override keeps the part's own pivot); {@code frames},
* when non-null, receives each part's frame {@code parent·T(P)·R·S} (pre-space, maps a part-local CEM px
* offset) — used to attach held/head items to hands and heads.
*/
@@ -54,10 +54,9 @@ final class CemGeometry {
CemModelLoader.CemModel model,
int[][] tex,
Affine pre,
Set<String> show,
Map<String, CemModelLoader.PartPose> overrides,
Map<String, Affine> frames) {
return bake(model, tex, pre, Set.of(), show, overrides, frames);
return bake(model, tex, pre, Set.of(), overrides, frames);
}
private static List<Baked> bake(
@@ -65,10 +64,9 @@ final class CemGeometry {
int[][] tex,
Affine pre,
Set<String> hidden,
Set<String> show,
Map<String, CemModelLoader.PartPose> overrides,
Map<String, Affine> frames) {
Ctx ctx = new Ctx(hidden, show, model.texW(), model.texH(), tex, new ArrayList<>(), overrides, frames);
Ctx ctx = new Ctx(hidden, model.texW(), model.texH(), tex, new ArrayList<>(), overrides, frames);
for (CemModelLoader.CemPart p : model.parts()) {
if (model.absolute()) {
bakeAbsolute(p, pre, ctx);
@@ -85,7 +83,6 @@ final class CemGeometry {
private record Ctx(
Set<String> hidden,
Set<String> show,
int texW,
int texH,
int[][] tex,
@@ -96,8 +93,8 @@ final class CemGeometry {
/**
* Vanilla-dumped ("absolute") part: pivot and boxes are absolute model px, so the group transform is
* simply {@code parent · T(P) · Rz·Ry·Rx · S · T(-P)} — the same order as vanilla's
* {@code ModelPart.translateAndRotate}. Invisible parts (vanilla {@code visible=false}) skip their
* whole subtree unless named in {@code show}; {@code skipDraw} parts only skip their own boxes.
* {@code ModelPart.translateAndRotate}. Invisible parts (vanilla {@code visible=false}, or hidden by a pose
* override) skip their whole subtree; {@code skipDraw} parts only skip their own boxes.
*/
private static void bakeAbsolute(CemModelLoader.CemPart part, Affine parentWorld, Ctx ctx) {
bakeAbsolute(part, parentWorld, ctx, true);
@@ -111,10 +108,11 @@ final class CemGeometry {
if (ctx.hidden.contains(part.name())) return;
CemModelLoader.PartPose ov = ctx.overrides.get(part.name());
boolean invisible = ov != null ? !ov.visible() : part.invisible();
if (invisible && !ctx.show.contains(part.name())) {
if (invisible) {
if (ctx.frames == null) return;
draw = false;
}
if (ov != null && ov.pivot() == null) ov = null; // visibility-only override: applied above
double[] o = ov != null ? ov.pivot() : part.pivot();
double[] rot = ov != null ? ov.rotate() : part.rotate();
Affine world = parentWorld
@@ -64,16 +64,83 @@ public final class CemModelLoader {
double[] transform, // vanilla renderer pose at yaw=pitch=0 (column-major 4x4, Java model -> blocks)
int yawSign, // how the entity yaw enters the renderer: -1 (mobs), +1 (projectiles), 0 (ignored)
double[] pitchAxis, // world-frame axis the pitch rotates about; null = pitch ignored
Map<String, Map<String, PartPose>> poses) {} // variant ("passenger+item_r", …) -> part overrides
Map<String, Map<String, PartPose>> poses, // pose ("passenger+item_r", "arms", "chest", …) -> overrides
Map<String, Boolean> layers, // render layer class -> drawn for the default (idle, unequipped) state
boolean humanoidArmor) { // the renderer has a HumanoidArmorLayer (wears humanoid armor)
/** A posed part override: absolute CEM pivot, CEM rotation (deg) and visibility. */
/** Whether the vanilla renderer draws this layer for an entity in its default state. */
public boolean layerDrawnByDefault(String layerClass) {
return layers.getOrDefault(layerClass, false);
}
}
/**
* A posed part override: absolute CEM pivot and CEM rotation (deg) — both null for a pure visibility
* toggle (the part keeps the pivot of whichever model it is in) — and visibility.
*/
public record PartPose(double[] pivot, double[] rotate, boolean visible) {}
/**
* Client assets of a data-driven mob variant ({@code data/<ns>/<type>_variant/<key>.json}): the texture
* ({@code asset_id}), its baby texture, the per-state textures of wolves ({@code assets.wild/tame/angry},
* {@code baby_assets}) and the model kind ({@code model}: "cold", "warm"; null = normal).
*/
public record VariantAssets(
String asset, String babyAsset, Map<String, String> assets, Map<String, String> babyAssets, String model) {
static VariantAssets parse(JsonObject o) {
return new VariantAssets(
str(o, "asset_id"),
str(o, "baby_asset_id"),
strMap(o, "assets"),
strMap(o, "baby_assets"),
str(o, "model"));
}
/** Texture id for the entity's state: wolves by wild/tame, others by asset id; baby first if asked. */
public String texture(boolean baby, boolean tame) {
String state = tame ? "tame" : "wild";
if (baby && babyAssets.containsKey(state)) return babyAssets.get(state);
if (baby && babyAsset != null) return babyAsset;
if (assets.containsKey(state)) return assets.get(state);
return asset;
}
private static String str(JsonObject o, String k) {
return o.has(k) && o.get(k).isJsonPrimitive() ? o.get(k).getAsString() : null;
}
private static Map<String, String> strMap(JsonObject o, String k) {
Map<String, String> out = new HashMap<>();
if (o.has(k) && o.get(k).isJsonObject()) {
for (Map.Entry<String, JsonElement> e : o.getAsJsonObject(k).entrySet()) {
if (e.getValue().isJsonPrimitive())
out.put(e.getKey(), e.getValue().getAsString());
}
}
return out;
}
}
private final Map<String, CemModel> models = new HashMap<>();
// Vanilla dump models live in their own namespace so they never shadow CEM-template names the
// legacy entity fallback relies on (the two sets use different names for different models).
private final Map<String, CemModel> vanilla = new HashMap<>();
private final Map<String, EntityInfo> entities = new HashMap<>();
// entity type -> variant key ("tabby", or "<ns>:<key>" outside minecraft) -> assets
private final Map<String, Map<String, VariantAssets>> variants = new java.util.concurrent.ConcurrentHashMap<>();
/** Assets of a data-driven variant of {@code typeKey} (registry {@code <typeKey>_variant}), or null. */
public VariantAssets variant(String typeKey, String key) {
Map<String, VariantAssets> m = key == null ? null : variants.get(typeKey);
return m == null ? null : m.get(key);
}
/** Registers a (datapack) variant definition — the raw {@code <type>_variant} JSON; replaces the vanilla one. */
public void addVariant(String typeKey, String key, JsonObject definition) {
variants.computeIfAbsent(typeKey, k -> new java.util.concurrent.ConcurrentHashMap<>())
.put(key, VariantAssets.parse(definition));
}
/** A model from the vanilla client dump, or null. */
public CemModel vanilla(String name) {
@@ -123,7 +190,20 @@ public final class CemModelLoader {
doubles(o, "transform"),
o.has("yawSign") ? o.get("yawSign").getAsInt() : -1,
doubles(o, "pitchAxis"),
poses(o)));
poses(o),
layers(o),
o.has("humanoidArmor") && o.get("humanoidArmor").getAsBoolean()));
}
if (root.has("variants")) {
for (Map.Entry<String, JsonElement> t :
root.getAsJsonObject("variants").entrySet()) {
Map<String, VariantAssets> m = new java.util.concurrent.ConcurrentHashMap<>();
for (Map.Entry<String, JsonElement> v :
t.getValue().getAsJsonObject().entrySet()) {
m.put(v.getKey(), VariantAssets.parse(v.getValue().getAsJsonObject()));
}
variants.put(t.getKey(), m);
}
}
}
JsonObject modelsObj = root.getAsJsonObject("models");
@@ -242,8 +322,8 @@ public final class CemModelLoader {
parts.put(
pe.getKey(),
new PartPose(
arr3(po, "pivot"),
arr3(po, "rotate"),
po.has("pivot") ? arr3(po, "pivot") : null,
po.has("rotate") ? arr3(po, "rotate") : null,
!po.has("visible") || po.get("visible").getAsBoolean()));
}
out.put(v.getKey(), parts);
@@ -251,6 +331,16 @@ public final class CemModelLoader {
return out;
}
private static Map<String, Boolean> layers(JsonObject o) {
Map<String, Boolean> out = new HashMap<>();
if (o.has("layers")) {
for (Map.Entry<String, JsonElement> e : o.getAsJsonObject("layers").entrySet()) {
out.put(e.getKey(), e.getValue().getAsBoolean());
}
}
return out;
}
private static double[] doubles(JsonObject o, String key) {
if (!o.has(key) || !o.get(key).isJsonArray()) return null;
JsonArray a = o.getAsJsonArray(key);
@@ -0,0 +1,96 @@
package eu.mhsl.minecraft.pixelpics.render.entity.cem;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
* Registers the data-driven mob variants of the worlds' datapacks ({@code data/<ns>/<type>_variant/<key>.json},
* folder or zip packs) with the model loader, next to the vanilla ones from the dump — so a datapack cat/wolf/
* cow/… variant renders with the texture its {@code asset_id} names (the resource pack must ship it).
*/
public final class DatapackVariants {
private static final Pattern VARIANT =
Pattern.compile("data/([a-z0-9_.-]+)/([a-z0-9_]+)_variant/([a-z0-9_/.-]+)\\.json");
private DatapackVariants() {}
/**
* Scans {@code <level>/datapacks/*} for every world folder — the folder itself or the level directory
* above it (26.x keeps dimensions under {@code <level>/dimensions/<ns>/<name>}); returns the number of
* variants registered.
*/
public static int load(CemModelLoader models, Iterable<File> worldFolders, Logger log) {
int n = 0;
java.util.Set<File> seen = new java.util.HashSet<>();
for (File world : worldFolders) {
File dir = world.getAbsoluteFile();
for (int up = 0; dir != null && up < 4 && !new File(dir, "datapacks").isDirectory(); up++) {
dir = dir.getParentFile();
}
if (dir == null || !seen.add(dir)) continue;
File[] packs = new File(dir, "datapacks").listFiles();
if (packs == null) continue;
for (File pack : packs) {
try {
n += pack.isDirectory() ? scanDir(models, pack.toPath()) : scanZip(models, pack);
} catch (IOException | RuntimeException e) {
log.warning("Skipping datapack " + pack + ": " + e.getMessage());
}
}
}
return n;
}
private static int scanDir(CemModelLoader models, Path root) throws IOException {
int n = 0;
try (Stream<Path> files = Files.walk(root)) {
for (Path f : (Iterable<Path>) files.filter(Files::isRegularFile)::iterator) {
String rel = root.relativize(f).toString().replace(File.separatorChar, '/');
try (InputStream in = Files.newInputStream(f)) {
if (register(models, rel, in)) n++;
}
}
}
return n;
}
private static int scanZip(CemModelLoader models, File zip) throws IOException {
if (!zip.getName().endsWith(".zip")) return 0;
int n = 0;
try (ZipFile zf = new ZipFile(zip)) {
for (ZipEntry ze : java.util.Collections.list(zf.entries())) {
if (ze.isDirectory()) continue;
try (InputStream in = zf.getInputStream(ze)) {
if (register(models, ze.getName(), in)) n++;
}
}
}
return n;
}
private static boolean register(CemModelLoader models, String path, InputStream in) {
Matcher m = VARIANT.matcher(path);
if (!m.matches()) return false;
String type = m.group(2);
if (type.endsWith("_sound") || type.equals("painting")) return false; // no mob texture
JsonObject def = JsonParser.parseReader(new InputStreamReader(in, StandardCharsets.UTF_8))
.getAsJsonObject();
String key = m.group(1).equals("minecraft") ? m.group(3) : m.group(1) + ":" + m.group(3);
models.addVariant(type, key, def);
return true;
}
}
@@ -28,12 +28,9 @@ import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Axolotl;
import org.bukkit.entity.Cat;
import org.bukkit.entity.ChestedHorse;
import org.bukkit.entity.Chicken;
import org.bukkit.entity.Cow;
import org.bukkit.entity.Creeper;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Fox;
import org.bukkit.entity.Frog;
import org.bukkit.entity.Horse;
import org.bukkit.entity.HumanEntity;
import org.bukkit.entity.LivingEntity;
@@ -42,7 +39,6 @@ import org.bukkit.entity.Mob;
import org.bukkit.entity.MushroomCow;
import org.bukkit.entity.Panda;
import org.bukkit.entity.Parrot;
import org.bukkit.entity.Pig;
import org.bukkit.entity.Player;
import org.bukkit.entity.PufferFish;
import org.bukkit.entity.Rabbit;
@@ -99,25 +95,6 @@ public final class EntitySnapshotBuilder {
"falling_block",
"item");
// Entities whose vanilla renderer draws the humanoid armor layers (HumanoidArmorLayer) and held
// items. Their CEM bodies share standard humanoid proportions, so the armor_layer_1/2 models align.
private static final Set<String> HUMANOID_ARMOR_WEARERS = Set.of(
"player",
"mannequin",
"armor_stand",
"giant",
"zombie",
"husk",
"drowned",
"zombie_villager",
"zombified_piglin",
"skeleton",
"stray",
"wither_skeleton",
"bogged",
"piglin",
"piglin_brute");
// Mobs whose main-hand item is drawn in the mouth (FoxHeldItemLayer, DolphinCarryingItemLayer).
private static final Set<String> MOUTH_CARRIERS = Set.of("fox", "dolphin");
@@ -173,6 +150,7 @@ public final class EntitySnapshotBuilder {
String markings = null;
boolean saddle = false;
boolean chest = false;
boolean arms = false;
String bodyEquip = null;
int bodyDye = 0;
int tint2 = 0;
@@ -195,7 +173,7 @@ public final class EntitySnapshotBuilder {
case 1 -> "mid";
default -> "big";};
} else if (e instanceof Cat c) {
variant = keyOf(c.getCatType());
variant = keyOf(c.getCatType()); // data-driven variants: key -> asset_id via the vanilla dump
if (c.isTamed()) collar = ColorUtil.dyeArgb(c.getCollarColor(), 0);
} else if (e instanceof Wolf w) {
variant = keyOf(w.getVariant());
@@ -235,8 +213,6 @@ public final class EntitySnapshotBuilder {
variant = keyOf(mc.getVariant());
} else if (e instanceof Panda pa) {
variant = keyOf(pa.getMainGene());
} else if (e instanceof Frog fr) {
variant = keyOf(fr.getVariant());
} else if (e instanceof Shulker s) {
variant = s.getColor() == null ? null : keyOf(s.getColor());
} else if (e instanceof ZombieVillager zv) {
@@ -246,15 +222,11 @@ public final class EntitySnapshotBuilder {
variant = keyOf(vi.getVillagerType());
profession = keyOf(vi.getProfession());
villagerLevel = vi.getVillagerLevel();
} else if (e instanceof Cow co) {
variant = keyOf(co.getVariant());
} else if (e instanceof Pig pg) {
variant = keyOf(pg.getVariant());
} else if (e instanceof Chicken ch) {
variant = keyOf(ch.getVariant());
}
if (e instanceof ArmorStand st && st.hasArms()) variant = "arms";
// Any other mob with a variant/weathering getter (new mobs, copper golem, zombie nautilus, …).
if (e instanceof ArmorStand st) arms = st.hasArms();
// Any other mob with a variant/weathering getter (frog, cow, pig, chicken, zombie nautilus, copper
// golem, new mobs, …). Reflective on purpose: the API's compile-time declaring interface of
// getVariant() (AbstractCow) doesn't exist at runtime on every server build (NoSuchMethodError).
if (variant == null) variant = genericVariant(e);
} catch (Throwable ignored) {
// Unsupported on this server version — fall back to the base texture.
@@ -299,10 +271,9 @@ public final class EntitySnapshotBuilder {
}
}
EntityState.Equipment equipment = null;
if (HUMANOID_ARMOR_WEARERS.contains(type) && e instanceof LivingEntity wearer) {
equipment = captureEquipment(wearer);
}
// Worn armor of every living entity; the baker draws it only for renderers with a HumanoidArmorLayer
// (vanilla dump), so the wearer list follows the game version.
EntityState.Equipment equipment = e instanceof LivingEntity wearer ? captureEquipment(wearer) : null;
return new EntityState(
type,
@@ -324,6 +295,7 @@ public final class EntitySnapshotBuilder {
markings,
saddle,
chest,
arms,
bodyEquip,
equipment,
invisible,
@@ -494,11 +466,16 @@ public final class EntitySnapshotBuilder {
return st != null && !st.getType().isAir();
}
/** Registry/Keyed values yield their key path; plain enums yield their lower-case name. */
/**
* Registry/Keyed values yield their key path ({@code "<ns>:<path>"} outside the minecraft namespace, e.g.
* datapack variants); plain enums yield their lower-case name.
*/
private static String keyOf(Object o) {
return switch (o) {
case null -> null;
case Keyed k -> k.getKey().getKey();
case Keyed k -> k.getKey().getNamespace().equals("minecraft")
? k.getKey().getKey()
: k.getKey().toString();
case Enum<?> en -> en.name().toLowerCase(Locale.ROOT);
default -> o.toString().toLowerCase(Locale.ROOT);
};
@@ -1 +1 @@
{"version":"26.3","models":{"acacia_boat":{"model":"{\"textureSize\":[128,64],\"absolute\":true,\"layer\":\"boat/acacia#main\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,24.0,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"right_paddle\",\"pivot\":[-3.0,29.0,-9.0],\"rotate\":[37.5,-142.1347,11.25],\"boxes\":[{\"coordinates\":[-4.0,27.0,-14.0,2.0,2.0,18.0],\"textureOffset\":[62.0,20.0]},{\"coordinates\":[-4.001,26.0,-1.0,1.0,6.0,7.0],\"textureOffset\":[62.0,20.0]}]},{\"part\":\"left_paddle\",\"pivot\":[-3.0,29.0,9.0],\"rotate\":[37.5,-37.8653,11.25],\"boxes\":[{\"coordinates\":[-4.0,27.0,4.0,2.0,2.0,18.0],\"textureOffset\":[62.0,0.0]},{\"coordinates\":[-2.999,26.0,17.0,1.0,6.0,7.0],\"textureOffset\":[62.0,0.0]}]},{\"part\":\"left\",\"pivot\":[0.0,20.0,9.0],\"rotate\":[0.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,21.0,8.0,28.0,6.0,2.0],\"textureOffset\":[0.0,43.0]}]},{\"part\":\"bottom\",\"pivot\":[0.0,21.0,1.0],\"rotate\":[-90.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,14.0,-2.0,28.0,16.0,3.0],\"textureOffset\":[0.0,0.0]}]},{\"part\":\"back\",\"pivot\":[15.0,20.0,4.0],\"rotate\":[0.0,-270.0,0.0],\"boxes\":[{\"coordinates\":[10.0,21.0,3.0,18.0,6.0,2.0],\"textureOffset\":[0.0,19.0]}]},{\"part\":\"front\",\"pivot\":[-15.0,20.0,0.0],\"rotate\":[0.0,-90.0,0.0],\"boxes\":[{\"coordinates\":[-23.0,21.0,-1.0,16.0,6.0,2.0],\"textureOffset\":[0.0,27.0]}]},{\"part\":\"right\",\"pivot\":[0.0,20.0,-9.0],\"rotate\":[0.0,-180.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,21.0,-10.0,28.0,6.0,2.0],\"textureOffset\":[0.0,35.0]}]}]}]}"},"acacia_boat/waterPatchModel":{"model":"{\"textureSize\":[0,0],\"absolute\":true,\"layer\":\"boat#water_patch\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,24.0,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"water_patch\",\"pivot\":[0.0,27.0,1.0],\"rotate\":[-90.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,20.0,-2.0,28.0,16.0,3.0],\"textureOffset\":[0.0,0.0]}]}]}]}"},"acacia_chest_boat":{"model":"{\"textureSize\":[128,128],\"absolute\":true,\"layer\":\"chest_boat/acacia#main\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,24.0,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"chest_bottom\",\"pivot\":[2.0,29.0,-6.0],\"rotate\":[0.0,90.0,0.0],\"boxes\":[{\"coordinates\":[-10.0,21.0,-6.0,12.0,8.0,12.0],\"textureOffset\":[0.0,76.0]}]},{\"part\":\"right_paddle\",\"pivot\":[-3.0,29.0,-9.0],\"rotate\":[37.5,-142.1347,11.25],\"boxes\":[{\"coordinates\":[-4.0,27.0,-14.0,2.0,2.0,18.0],\"textureOffset\":[62.0,20.0]},{\"coordinates\":[-4.001,26.0,-1.0,1.0,6.0,7.0],\"textureOffset\":[62.0,20.0]}]},{\"part\":\"left_paddle\",\"pivot\":[-3.0,29.0,9.0],\"rotate\":[37.5,-37.8653,11.25],\"boxes\":[{\"coordinates\":[-4.0,27.0,4.0,2.0,2.0,18.0],\"textureOffset\":[62.0,0.0]},{\"coordinates\":[-2.999,26.0,17.0,1.0,6.0,7.0],\"textureOffset\":[62.0,0.0]}]},{\"part\":\"left\",\"pivot\":[0.0,20.0,9.0],\"rotate\":[0.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,21.0,8.0,28.0,6.0,2.0],\"textureOffset\":[0.0,43.0]}]},{\"part\":\"bottom\",\"pivot\":[0.0,21.0,1.0],\"rotate\":[-90.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,14.0,-2.0,28.0,16.0,3.0],\"textureOffset\":[0.0,0.0]}]},{\"part\":\"back\",\"pivot\":[15.0,20.0,4.0],\"rotate\":[0.0,-270.0,0.0],\"boxes\":[{\"coordinates\":[10.0,21.0,3.0,18.0,6.0,2.0],\"textureOffset\":[0.0,19.0]}]},{\"part\":\"chest_lid\",\"pivot\":[2.0,33.0,-6.0],\"rotate\":[0.0,90.0,0.0],\"boxes\":[{\"coordinates\":[-10.0,29.0,-6.0,12.0,4.0,12.0],\"textureOffset\":[0.0,59.0]}]},{\"part\":\"front\",\"pivot\":[-15.0,20.0,0.0],\"rotate\":[0.0,-90.0,0.0],\"boxes\":[{\"coordinates\":[-23.0,21.0,-1.0,16.0,6.0,2.0],\"textureOffset\":[0.0,27.0]}]},{\"part\":\"right\",\"pivot\":[0.0,20.0,-9.0],\"rotate\":[0.0,-180.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,21.0,-10.0,28.0,6.0,2.0],\"textureOffset\":[0.0,35.0]}]},{\"part\":\"chest_lock\",\"pivot\":[1.0,30.0,-1.0],\"rotate\":[0.0,90.0,0.0],\"boxes\":[{\"coordinates\":[-1.0,26.0,-1.0,2.0,4.0,1.0],\"textureOffset\":[0.0,59.0]}]}]}]}"},"acacia_chest_boat/waterPatchModel":{"model":"{\"textureSize\":[0,0],\"absolute\":true,\"layer\":\"boat#water_patch\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,24.0,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"water_patch\",\"pivot\":[0.0,27.0,1.0],\"rotate\":[-90.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,20.0,-2.0,28.0,16.0,3.0],\"textureOffset\":[0.0,0.0]}]}]}]}"},"allay":{"model":"{\"textureSize\":[32,32],\"absolute\":true,\"layer\":\"allay#main\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,0.25,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"head\",\"pivot\":[0.0,4.24,0.0],\"rotate\":[0.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-2.5,4.24,-2.5,5.0,5.0,5.0],\"textureOffset\":[0.0,0.0]}]},{\"part\":\"body\",\"pivot\":[0.0,4.25,0.0],\"rotate\":[0.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-1.5,0.25,-1.0,3.0,4.0,2.0],\"textureOffset\":[0.0,10.0]},{\"coordinates\":[-1.5,-0.75,-1.0,3.0,5.0,2.0],\"textureOffset\":[0.0,16.0],\"sizeAdd\":-0.2}],\"submodels\":[{\"part\":\"right_arLine truncated
{"version":"26.3","models":{"acacia_boat":{"model":"{\"textureSize\":[128,64],\"absolute\":true,\"layer\":\"boat/acacia#main\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,24.0,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"right_paddle\",\"pivot\":[-3.0,29.0,-9.0],\"rotate\":[37.5,-142.1347,11.25],\"boxes\":[{\"coordinates\":[-4.0,27.0,-14.0,2.0,2.0,18.0],\"textureOffset\":[62.0,20.0]},{\"coordinates\":[-4.001,26.0,-1.0,1.0,6.0,7.0],\"textureOffset\":[62.0,20.0]}]},{\"part\":\"left_paddle\",\"pivot\":[-3.0,29.0,9.0],\"rotate\":[37.5,-37.8653,11.25],\"boxes\":[{\"coordinates\":[-4.0,27.0,4.0,2.0,2.0,18.0],\"textureOffset\":[62.0,0.0]},{\"coordinates\":[-2.999,26.0,17.0,1.0,6.0,7.0],\"textureOffset\":[62.0,0.0]}]},{\"part\":\"left\",\"pivot\":[0.0,20.0,9.0],\"rotate\":[0.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,21.0,8.0,28.0,6.0,2.0],\"textureOffset\":[0.0,43.0]}]},{\"part\":\"bottom\",\"pivot\":[0.0,21.0,1.0],\"rotate\":[-90.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,14.0,-2.0,28.0,16.0,3.0],\"textureOffset\":[0.0,0.0]}]},{\"part\":\"back\",\"pivot\":[15.0,20.0,4.0],\"rotate\":[0.0,-270.0,0.0],\"boxes\":[{\"coordinates\":[10.0,21.0,3.0,18.0,6.0,2.0],\"textureOffset\":[0.0,19.0]}]},{\"part\":\"front\",\"pivot\":[-15.0,20.0,0.0],\"rotate\":[0.0,-90.0,0.0],\"boxes\":[{\"coordinates\":[-23.0,21.0,-1.0,16.0,6.0,2.0],\"textureOffset\":[0.0,27.0]}]},{\"part\":\"right\",\"pivot\":[0.0,20.0,-9.0],\"rotate\":[0.0,-180.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,21.0,-10.0,28.0,6.0,2.0],\"textureOffset\":[0.0,35.0]}]}]}]}"},"acacia_boat/waterPatchModel":{"model":"{\"textureSize\":[0,0],\"absolute\":true,\"layer\":\"boat#water_patch\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,24.0,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"water_patch\",\"pivot\":[0.0,27.0,1.0],\"rotate\":[-90.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,20.0,-2.0,28.0,16.0,3.0],\"textureOffset\":[0.0,0.0]}]}]}]}"},"acacia_chest_boat":{"model":"{\"textureSize\":[128,128],\"absolute\":true,\"layer\":\"chest_boat/acacia#main\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,24.0,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"chest_bottom\",\"pivot\":[2.0,29.0,-6.0],\"rotate\":[0.0,90.0,0.0],\"boxes\":[{\"coordinates\":[-10.0,21.0,-6.0,12.0,8.0,12.0],\"textureOffset\":[0.0,76.0]}]},{\"part\":\"right_paddle\",\"pivot\":[-3.0,29.0,-9.0],\"rotate\":[37.5,-142.1347,11.25],\"boxes\":[{\"coordinates\":[-4.0,27.0,-14.0,2.0,2.0,18.0],\"textureOffset\":[62.0,20.0]},{\"coordinates\":[-4.001,26.0,-1.0,1.0,6.0,7.0],\"textureOffset\":[62.0,20.0]}]},{\"part\":\"left_paddle\",\"pivot\":[-3.0,29.0,9.0],\"rotate\":[37.5,-37.8653,11.25],\"boxes\":[{\"coordinates\":[-4.0,27.0,4.0,2.0,2.0,18.0],\"textureOffset\":[62.0,0.0]},{\"coordinates\":[-2.999,26.0,17.0,1.0,6.0,7.0],\"textureOffset\":[62.0,0.0]}]},{\"part\":\"left\",\"pivot\":[0.0,20.0,9.0],\"rotate\":[0.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,21.0,8.0,28.0,6.0,2.0],\"textureOffset\":[0.0,43.0]}]},{\"part\":\"bottom\",\"pivot\":[0.0,21.0,1.0],\"rotate\":[-90.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,14.0,-2.0,28.0,16.0,3.0],\"textureOffset\":[0.0,0.0]}]},{\"part\":\"back\",\"pivot\":[15.0,20.0,4.0],\"rotate\":[0.0,-270.0,0.0],\"boxes\":[{\"coordinates\":[10.0,21.0,3.0,18.0,6.0,2.0],\"textureOffset\":[0.0,19.0]}]},{\"part\":\"chest_lid\",\"pivot\":[2.0,33.0,-6.0],\"rotate\":[0.0,90.0,0.0],\"boxes\":[{\"coordinates\":[-10.0,29.0,-6.0,12.0,4.0,12.0],\"textureOffset\":[0.0,59.0]}]},{\"part\":\"front\",\"pivot\":[-15.0,20.0,0.0],\"rotate\":[0.0,-90.0,0.0],\"boxes\":[{\"coordinates\":[-23.0,21.0,-1.0,16.0,6.0,2.0],\"textureOffset\":[0.0,27.0]}]},{\"part\":\"right\",\"pivot\":[0.0,20.0,-9.0],\"rotate\":[0.0,-180.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,21.0,-10.0,28.0,6.0,2.0],\"textureOffset\":[0.0,35.0]}]},{\"part\":\"chest_lock\",\"pivot\":[1.0,30.0,-1.0],\"rotate\":[0.0,90.0,0.0],\"boxes\":[{\"coordinates\":[-1.0,26.0,-1.0,2.0,4.0,1.0],\"textureOffset\":[0.0,59.0]}]}]}]}"},"acacia_chest_boat/waterPatchModel":{"model":"{\"textureSize\":[0,0],\"absolute\":true,\"layer\":\"boat#water_patch\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,24.0,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"water_patch\",\"pivot\":[0.0,27.0,1.0],\"rotate\":[-90.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,20.0,-2.0,28.0,16.0,3.0],\"textureOffset\":[0.0,0.0]}]}]}]}"},"allay":{"model":"{\"textureSize\":[32,32],\"absolute\":true,\"layer\":\"allay#main\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,0.25,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"head\",\"pivot\":[0.0,4.24,0.0],\"rotate\":[0.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-2.5,4.24,-2.5,5.0,5.0,5.0],\"textureOffset\":[0.0,0.0]}]},{\"part\":\"body\",\"pivot\":[0.0,4.25,0.0],\"rotate\":[0.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-1.5,0.25,-1.0,3.0,4.0,2.0],\"textureOffset\":[0.0,10.0]},{\"coordinates\":[-1.5,-0.75,-1.0,3.0,5.0,2.0],\"textureOffset\":[0.0,16.0],\"sizeAdd\":-0.2}],\"submodels\":[{\"part\":\"right_arLine truncated
+1 -1
View File
@@ -228,7 +228,7 @@ public class BlockEntityTestRender {
log.info("rendered " + name);
}
File outDir = new File("/home/elias/Dokumente/PixelPics-BlockEntity-Renders");
File outDir = new File(System.getProperty("out", "/home/elias/Dokumente/PixelPics-BlockEntity-Renders"));
outDir.mkdirs();
for (int i = 0; i < names.size(); i++) ImageIO.write(cells.get(i), "png", new File(outDir, names.get(i) + ".png"));
+81 -12
View File
@@ -44,6 +44,49 @@ public class EntityTestRender {
"minecart","end_crystal","arrow","trident","leash_knot","evoker_fangs","wither_skull","shulker_bullet","cushion"
};
// "@variants" case set (before/after pixel diffs of variant / pose-flag / armor handling). Spec syntax:
// [baby:]type[@variant][+flag…], flags: chest, arms, tame (red collar), saddle, armor (full diamond), noarmor.
static final List<String> VARIANT_CASES = new ArrayList<>();
static {
for (String v : new String[] {"tabby", "black", "red", "siamese", "british_shorthair", "calico", "persian",
"ragdoll", "white", "jellie", "all_black"}) {
VARIANT_CASES.add("cat@" + v);
VARIANT_CASES.add("baby:cat@" + v + "+tame");
}
for (String v : new String[] {"pale", "spotted", "snowy", "black", "ashen", "rusty", "woods", "chestnut",
"striped"}) {
VARIANT_CASES.add("wolf@" + v);
VARIANT_CASES.add("wolf@" + v + "+tame");
VARIANT_CASES.add("baby:wolf@" + v);
VARIANT_CASES.add("baby:wolf@" + v + "+tame");
}
for (String t : new String[] {"cow", "pig", "chicken"})
for (String v : new String[] {"temperate", "warm", "cold"}) {
VARIANT_CASES.add(t + "@" + v);
VARIANT_CASES.add("baby:" + t + "@" + v);
}
for (String v : new String[] {"temperate", "warm", "cold"}) VARIANT_CASES.add("frog@" + v);
for (String v : new String[] {"temperate", "warm"}) VARIANT_CASES.add("zombie_nautilus@" + v);
VARIANT_CASES.add("mooshroom@red");
VARIANT_CASES.add("mooshroom@brown");
VARIANT_CASES.add("baby:mooshroom@brown");
// Armor stand with/without arms, small, with armor.
for (String a : new String[] {"armor_stand", "armor_stand+arms", "baby:armor_stand", "baby:armor_stand+arms",
"armor_stand+arms+armor", "baby:armor_stand+arms+armor"}) VARIANT_CASES.add(a);
// Chests.
for (String c : new String[] {"llama@creamy", "llama@creamy+chest", "llama@brown+chest+saddle", "llama@gray+chest",
"trader_llama@white+chest", "baby:llama@creamy+chest", "donkey", "donkey+chest", "donkey+chest+saddle",
"mule", "mule+chest", "mule+chest+saddle", "baby:donkey+chest"}) VARIANT_CASES.add(c);
// Armor wearers (and non-wearers given armor: nothing must appear).
for (String t : new String[] {"zombie", "husk", "drowned", "zombie_villager", "zombified_piglin", "skeleton",
"stray", "wither_skeleton", "bogged", "parched", "piglin", "piglin_brute", "giant", "player",
"mannequin", "armor_stand", "baby:zombie", "baby:piglin", "baby:zombified_piglin", "baby:husk",
"baby:drowned", "baby:zombie_villager", "villager", "pillager", "vindicator", "witch", "cow", "pig",
"creeper", "enderman", "evoker", "illusioner", "wandering_trader", "copper_golem", "fox", "panda"})
VARIANT_CASES.add(t + "+armor");
VARIANT_CASES.addAll(List.of(ENTITIES));
}
// Representative variants (the game always supplies these; null base textures wouldn't exist otherwise).
static final Map<String, String> VAR = Map.ofEntries(
Map.entry("cat", "tabby"), Map.entry("wolf", "pale"), Map.entry("axolotl", "lucy"),
@@ -135,7 +178,8 @@ public class EntityTestRender {
WorldSnapshot empty = new WorldSnapshot(Map.of(), 0, 1, air);
SkyContext sky = new SkyContext(6000, 0, 6000);
String[] list = args.length > 0 ? args : ENTITIES;
String[] list = args.length == 0 ? ENTITIES
: args[0].equals("@variants") ? VARIANT_CASES.toArray(new String[0]) : args;
List<BufferedImage> cells = new ArrayList<>();
for (String key : list) {
BufferedImage v1 = renderEntity(renderer, baker, empty, sky, key, 45);
@@ -144,13 +188,13 @@ public class EntityTestRender {
log.info("rendered " + key);
}
File outDir = new File("/home/elias/Dokumente/PixelPics-Entity-Renders");
File outDir = new File(System.getProperty("out", "/home/elias/Dokumente/PixelPics-Entity-Renders"));
outDir.mkdirs();
// One image per entity (both 45°/225° views side by side), named by entity key.
File single = new File(outDir, "einzeln");
single.mkdirs();
for (int i = 0; i < list.length; i++) {
ImageIO.write(cells.get(i), "png", new File(single, list[i].replace(':', '_') + ".png"));
ImageIO.write(cells.get(i), "png", new File(single, list[i].replaceAll("[:@+]", "_") + ".png"));
}
// Compose contact-sheet pages: 2 columns.
@@ -177,15 +221,7 @@ 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");
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, 0, false, null);
EntityState s = state(key, yaw);
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);
@@ -208,6 +244,39 @@ public class EntityTestRender {
return renderer.execute(job);
}
/** Entity state for a case spec ({@code [baby:]type[@variant][+flag…]}, see VARIANT_CASES). */
static EntityState state(String spec, float yaw) {
boolean baby = spec.startsWith("baby:");
if (baby) spec = spec.substring(5);
String[] flagSplit = spec.split("\\+");
Set<String> flags = new HashSet<>(Arrays.asList(flagSplit).subList(1, flagSplit.length));
String key = flagSplit[0];
String variant = null;
if (key.contains("@")) {
variant = key.substring(key.indexOf('@') + 1);
key = key.substring(0, key.indexOf('@'));
}
boolean plain = flagSplit.length == 1 && variant == null; // default ENTITIES entry: legacy test setup
if (variant == null) variant = VAR.get(key);
boolean isPlayer = key.equals("player");
int tint = key.equals("tropical_fish") ? 0xFFF9801D : (key.equals("sheep") ? 0xFFF9FFFE : 0);
int tint2 = key.equals("tropical_fish") ? 0xFF3AB3DA : 0;
EntityState.Equipment armor = plain ? (baby ? null : ARMOR.get(key)) : null;
if (flags.contains("armor")) {
EntityState.EquipPiece d = P("diamond");
armor = new EntityState.Equipment(d, d, d, d);
}
boolean chest = plain ? CHEST.contains(key) : flags.contains("chest");
boolean saddle = plain ? SADDLE.contains(key) : flags.contains("saddle");
String equip = plain ? EQUIP.get(key) : null;
boolean arms = flags.contains("arms");
int collar = flags.contains("tame") ? 0xFFB02E26 : 0;
return new EntityState(key, 0, 0, 0, yaw, baby, 0.8, 1.0,
isPlayer, null, false, variant, tint, 1.0, PROF.get(key), LVL.getOrDefault(key, 0),
plain ? MARK.get(key) : null, saddle, chest, arms, equip, armor,
plain && !baby && INVISIBLE.contains(key), 0, 0, tint2, false, null, null, null, collar, false, null);
}
static BufferedImage labelCell(String key, BufferedImage v1, BufferedImage v2) {
int w = v1.getWidth() + v2.getWidth();
int labelH = 20;
+1 -1
View File
@@ -100,7 +100,7 @@ public class ObjectTestRender {
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");
File out = new File(System.getProperty("out", "/home/elias/Dokumente/PixelPics-Entity-Renders/objects.png"));
ImageIO.write(img, "png", out);
System.out.println("WROTE " + out);
}
+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);
+215 -1
View File
@@ -117,6 +117,17 @@ public class PPTest extends JavaPlugin {
scenes.put("be_items", new Scene("be_items", this::beItems, 4.5, 2.6, 11, 4.5, 0.8, 3));
scenes.put("be_items_back", new Scene("be_items_back", this::beItems, 4.5, 2.6, -5, 4.5, 0.8, 3));
scenes.put("proj_stuck_sw", new Scene("proj_stuck_sw", this::projStuck, -3, 6, 10, 8, 1.5, 2));
// Data-driven variants / pose flags / armor wearers (vanilla dump: asset_id, setupAnim flags, armor layers).
scenes.put("rf_cats", new Scene("rf_cats", this::rfCats, 6.5, 2.6, -4.5, 6.5, 0.2, 1.2));
scenes.put("rf_wolves", new Scene("rf_wolves", this::rfWolves, 5.5, 3.2, -5.5, 5.5, 0.2, 1.8));
scenes.put("rf_farm", new Scene("rf_farm", this::rfFarm, 6, 3.4, -6, 6, 0.3, 1.8));
scenes.put("rf_stands", new Scene("rf_stands", this::rfStands, 5, 2.4, -4.5, 5, 0.9, 0));
scenes.put("rf_stands_back", new Scene("rf_stands_back", this::rfStands, 5, 2.4, 4.5, 5, 0.9, 0));
scenes.put("rf_chests", new Scene("rf_chests", this::rfChests, 7, 4, -7, 7, 0.8, 1));
scenes.put("rf_chests_side", new Scene("rf_chests_side", this::rfChests, 7, 2.5, 7, 7, 0.8, 1));
// Needs a datapack cat variant "pptest:ocelotcat" (asset_id entity/cat/ocelot) in the world's datapacks.
scenes.put("rf_dpcat", new Scene("rf_dpcat", this::rfDatapackCat, 1.2, 1.4, -2.2, 1.2, 0.2, 0.5));
scenes.put("rf_armor", new Scene("rf_armor", this::rfArmor, 7, 3.2, -6.5, 7, 1.0, 1.5));
}
@Override
@@ -154,7 +165,7 @@ public class PPTest extends JavaPlugin {
Location eye = o.clone().add(sc.camX, sc.camY, sc.camZ);
Vector look = o.clone().add(sc.lookX, sc.lookY, sc.lookZ).toVector().subtract(eye.toVector());
eye.setDirection(look);
RenderJob job = r.prepare(eye, sc.name.startsWith("proj_") ? new Resolution(960, 640) : new Resolution(480, 320), null);
RenderJob job = r.prepare(eye, sc.name.startsWith("proj_") || sc.name.startsWith("rf_") ? new Resolution(960, 640) : new Resolution(480, 320), null);
if (sc.name.equals("dbg")) {
try {
java.lang.reflect.Field f = DefaultScreenRenderer.class.getDeclaredField("objectBaker");
@@ -690,6 +701,209 @@ public class PPTest extends JavaPlugin {
for (Entity e : w.getNearbyEntities(o, 12, 8, 8)) if (e instanceof Wither wi) wi.setBodyYaw(180);
}
// --- data-driven variants / pose flags / armor wearers ------------------------------------------
private static <T extends Keyed> List<T> registry(io.papermc.paper.registry.RegistryKey<T> key) {
List<T> out = new ArrayList<>();
io.papermc.paper.registry.RegistryAccess.registryAccess().getRegistry(key).forEach(out::add);
out.sort(Comparator.comparing(k -> k.getKey().toString()));
return out;
}
/** Spawn without vanilla's random spawn data (baby chance, jockeys, gear, biome variants): reproducible. */
private <T extends Entity> T rfSpawn(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, false, en -> {
if (en instanceof Mob m) m.setAI(false);
en.setPersistent(false);
});
if (e instanceof LivingEntity le) le.setRemoveWhenFarAway(false);
return e;
}
private Entity rfSpawn(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, false);
if (e instanceof Mob m) m.setAI(false);
return e;
}
private void faceNorth(Location o) {
for (Entity e : w.getNearbyEntities(o.clone().add(6, 0, 0), 14, 4, 12))
if (e instanceof LivingEntity le && !(e instanceof Player)) le.setBodyYaw(180);
}
private void rfCats(Location o) {
List<Cat.Type> types = registry(io.papermc.paper.registry.RegistryKey.CAT_VARIANT);
for (int i = 0; i < types.size(); i++) {
rfSpawn(o, i * 1.2, 0, 0, 180, Cat.class).setCatType(types.get(i));
Cat kit = rfSpawn(o, i * 1.2, 0, 1.6, 180, Cat.class);
kit.setCatType(types.get(i));
kit.setBaby();
if (i % 2 == 0) tame(kit, DyeColor.values()[i % 16]);
}
faceNorth(o);
}
private void rfDatapackCat(Location o) {
Cat.Type custom = io.papermc.paper.registry.RegistryAccess.registryAccess()
.getRegistry(io.papermc.paper.registry.RegistryKey.CAT_VARIANT)
.get(new NamespacedKey("pptest", "ocelotcat"));
if (custom == null) throw new IllegalStateException("datapack variant pptest:ocelotcat missing");
rfSpawn(o, 0, 0, 0, 180, Cat.class).setCatType(custom);
Cat kit = rfSpawn(o, 1.2, 0, 0, 180, Cat.class);
kit.setCatType(custom);
kit.setBaby();
rfSpawn(o, 2.4, 0, 0, 180, Cat.class).setCatType(Cat.Type.TABBY);
faceNorth(o);
}
private void rfWolves(Location o) {
List<Wolf.Variant> vs = registry(io.papermc.paper.registry.RegistryKey.WOLF_VARIANT);
for (int i = 0; i < vs.size(); i++) {
rfSpawn(o, i * 1.3, 0, 0, 180, Wolf.class).setVariant(vs.get(i));
Wolf t = rfSpawn(o, i * 1.3, 0, 1.6, 180, Wolf.class);
t.setVariant(vs.get(i));
tame(t, DyeColor.values()[i % 16]);
Wolf pup = rfSpawn(o, i * 1.3, 0, 3.0, 180, Wolf.class);
pup.setVariant(vs.get(i));
pup.setBaby();
if (i % 2 == 1) tame(pup, DyeColor.BLUE);
}
faceNorth(o);
}
private void rfFarm(Location o) {
List<Cow.Variant> cows = registry(io.papermc.paper.registry.RegistryKey.COW_VARIANT);
List<Pig.Variant> pigs = registry(io.papermc.paper.registry.RegistryKey.PIG_VARIANT);
List<Chicken.Variant> chickens = registry(io.papermc.paper.registry.RegistryKey.CHICKEN_VARIANT);
List<Frog.Variant> frogs = registry(io.papermc.paper.registry.RegistryKey.FROG_VARIANT);
List<ZombieNautilus.Variant> nautili = registry(io.papermc.paper.registry.RegistryKey.ZOMBIE_NAUTILUS_VARIANT);
for (int i = 0; i < 3; i++) {
double x = i * 4;
setVar(rfSpawn(o, x, 0, 0, 180, Cow.class), cows.get(i));
Cow cb = rfSpawn(o, x + 1.6, 0, 0, 180, Cow.class);
setVar(cb, cows.get(i));
cb.setBaby();
setVar(rfSpawn(o, x, 0, 1.8, 180, Pig.class), pigs.get(i));
Pig pb = rfSpawn(o, x + 1.4, 0, 1.8, 180, Pig.class);
setVar(pb, pigs.get(i));
pb.setBaby();
setVar(rfSpawn(o, x + 2.6, 0, 1.8, 180, Chicken.class), chickens.get(i));
Chicken chb = rfSpawn(o, x + 3.3, 0, 1.8, 180, Chicken.class);
setVar(chb, chickens.get(i));
chb.setBaby();
setVar(rfSpawn(o, x + 0.5, 0, 3.4, 180, Frog.class), frogs.get(i));
}
for (int i = 0; i < nautili.size(); i++)
setVar(rfSpawn(o, 2.5 + i * 3, 0, 4.8, 180, ZombieNautilus.class), nautili.get(i));
faceNorth(o);
}
/** setVariant via reflection (the variant setters' declaring interface differs between API and server). */
private static void setVar(Entity e, Object variant) {
for (java.lang.reflect.Method m : e.getClass().getMethods())
if (m.getName().equals("setVariant") && m.getParameterCount() == 1
&& m.getParameterTypes()[0].isInstance(variant)) {
try {
m.invoke(e, variant);
return;
} catch (ReflectiveOperationException ex) {
throw new RuntimeException(ex);
}
}
throw new IllegalStateException("no setVariant for " + variant);
}
private void rfStands(Location o) {
rfSpawn(o, 0, 0, 0, 180, ArmorStand.class);
rfSpawn(o, 1.5, 0, 0, 180, ArmorStand.class).setArms(true);
rfSpawn(o, 3, 0, 0, 180, ArmorStand.class).setSmall(true);
ArmorStand sa = rfSpawn(o, 4.2, 0, 0, 180, ArmorStand.class);
sa.setSmall(true);
sa.setArms(true);
ArmorStand full = rfSpawn(o, 5.7, 0, 0, 180, ArmorStand.class);
full.setArms(true);
diamondArmor(full);
hands(full, Material.IRON_SWORD, Material.SHIELD);
ArmorStand sfull = rfSpawn(o, 7.2, 0, 0, 180, ArmorStand.class);
sfull.setSmall(true);
sfull.setArms(true);
diamondArmor(sfull);
hands(sfull, Material.TORCH, null);
ArmorStand noArmsHeld = rfSpawn(o, 8.6, 0, 0, 180, ArmorStand.class);
hands(noArmsHeld, Material.DIAMOND, null);
ArmorStand inv = rfSpawn(o, 10, 0, 0, 180, ArmorStand.class);
inv.setArms(true);
inv.setInvisible(true);
diamondArmor(inv);
}
private static void diamondArmor(LivingEntity e) {
e.getEquipment().setHelmet(it(Material.DIAMOND_HELMET));
e.getEquipment().setChestplate(it(Material.DIAMOND_CHESTPLATE));
e.getEquipment().setLeggings(it(Material.DIAMOND_LEGGINGS));
e.getEquipment().setBoots(it(Material.DIAMOND_BOOTS));
}
private void rfChests(Location o) {
rfSpawn(o, 0, 0, 0, 180, Llama.class).setColor(Llama.Color.CREAMY);
Llama lc = rfSpawn(o, 1.6, 0, 0, 180, Llama.class);
lc.setColor(Llama.Color.BROWN);
lc.setCarryingChest(true);
Llama lcc = rfSpawn(o, 3.2, 0, 0, 180, Llama.class);
lcc.setColor(Llama.Color.GRAY);
lcc.setCarryingChest(true);
lcc.getInventory().setDecor(it(Material.RED_CARPET));
TraderLlama tl = rfSpawn(o, 4.8, 0, 0, 180, TraderLlama.class);
tl.setColor(Llama.Color.WHITE);
tl.setCarryingChest(true);
Llama bl = rfSpawn(o, 6.2, 0, 0, 180, Llama.class);
bl.setColor(Llama.Color.CREAMY);
bl.setBaby();
rfSpawn(o, 8, 0, 0, 180, Donkey.class);
Donkey dc = rfSpawn(o, 9.6, 0, 0, 180, Donkey.class);
dc.setCarryingChest(true);
Donkey dcs = rfSpawn(o, 11.2, 0, 0, 180, Donkey.class);
dcs.setCarryingChest(true);
dcs.getInventory().setSaddle(it(Material.SADDLE));
Mule mc = rfSpawn(o, 12.8, 0, 0, 180, Mule.class);
mc.setCarryingChest(true);
Mule mcs = rfSpawn(o, 14.4, 0, 0, 180, Mule.class);
mcs.setCarryingChest(true);
mcs.getInventory().setSaddle(it(Material.SADDLE));
faceNorth(o);
}
private void rfArmor(Location o) {
Class<?>[] wearers = {Zombie.class, Husk.class, Drowned.class, ZombieVillager.class, PigZombie.class,
Skeleton.class, Stray.class, WitherSkeleton.class, Bogged.class, Parched.class, Piglin.class, PiglinBrute.class};
for (int i = 0; i < wearers.length; i++) {
@SuppressWarnings("unchecked")
LivingEntity e = rfSpawn(o, i * 1.25, 0, 0, 180, (Class<LivingEntity>) wearers[i]);
diamondArmor(e);
}
LivingEntity m = (LivingEntity) rfSpawn(o, 0, 0, 2.2, 180, EntityType.MANNEQUIN);
diamondArmor(m);
Zombie bz = rfSpawn(o, 1.3, 0, 2.2, 180, Zombie.class);
bz.setBaby();
diamondArmor(bz);
Piglin bp = rfSpawn(o, 2.5, 0, 2.2, 180, Piglin.class);
bp.setBaby();
diamondArmor(bp);
// Non-wearers given armor: vanilla draws nothing.
Class<?>[] others = {Villager.class, Pillager.class, Vindicator.class, Witch.class, Evoker.class,
Creeper.class, Enderman.class, Cow.class, Fox.class};
for (int i = 0; i < others.length; i++) {
@SuppressWarnings("unchecked")
LivingEntity e = rfSpawn(o, 4 + i * 1.25, 0, 2.2, 180, (Class<LivingEntity>) others[i]);
diamondArmor(e);
}
faceNorth(o);
}
// --- projectiles -----------------------------------------------------------------------------
/** Spawns a projectile at p moving along dir, rotated like vanilla Projectile#shoot (yaw atan2(x,z), pitch up). */