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