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);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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). */
|
||||
|
||||
Reference in new issue
Block a user