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:
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;
|
||||
}
|
||||
}
|
||||
+17
-40
@@ -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);
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user