render emissive eye/glow layers, per-model vanilla render types (cutout vs translucent, unshaded), sheep undercoat and vanilla wool colour, angry wolves, tipped arrow and invulnerable wither textures, small armor stand armor textures, elytra rest pose, vanilla mob arm poses (piglins, illagers), layer test scenes
This commit is contained in:
1 parent
45d033bd89
commit
2c2e9e2e32
12 files changed
+896
-99
No files matched your search
@@ -14,6 +14,7 @@ public final class EntityCube {
|
||||
public final Affine toWorld;
|
||||
public final Affine toLocal; // inverse
|
||||
public final boolean additive; // emissive, added to what lies behind (energy swirl) instead of blended
|
||||
public final boolean unshaded; // no directional face shading (vanilla NO_CARDINAL_LIGHTING: eyes, breeze wind)
|
||||
public final double[] aabbMin = {Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE};
|
||||
public final double[] aabbMax = {-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE};
|
||||
|
||||
@@ -22,7 +23,12 @@ public final class EntityCube {
|
||||
}
|
||||
|
||||
public EntityCube(double[] from, double[] to, Face[] faces, Affine toWorld, boolean additive) {
|
||||
this(from, to, faces, toWorld, additive, false);
|
||||
}
|
||||
|
||||
public EntityCube(double[] from, double[] to, Face[] faces, Affine toWorld, boolean additive, boolean unshaded) {
|
||||
this.additive = additive;
|
||||
this.unshaded = unshaded;
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
this.faces = faces;
|
||||
|
||||
@@ -120,7 +120,7 @@ public final class EntityIntersector {
|
||||
Vector normal = new Vector(n[0], n[1], n[2]).normalize();
|
||||
// A far/back face is seen from inside: shade it as facing the viewer.
|
||||
if (normal.getX() * dx + normal.getY() * dy + normal.getZ() * dz > 0) normal.multiply(-1);
|
||||
return new FaceHit(t, world, normal, h.color, -1, cube.additive);
|
||||
return new FaceHit(t, world, normal, h.color, -1, cube.additive, cube.unshaded);
|
||||
}
|
||||
|
||||
private static Direction direction(int axis, boolean neg) {
|
||||
|
||||
@@ -145,24 +145,29 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
// Body: baked even when invisible — not drawn, but it yields the part frames (and legacy ground snap).
|
||||
Pose pose = new Pose(pre, vanilla ? withBindings(sel.parts(), 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, pose.parts, frames) : List.of();
|
||||
List<CemGeometry.Baked> body = (model != null && tex != null)
|
||||
? CemGeometry.bakePosed(model, renderTexture(model, tex), pre, pose.parts, frames)
|
||||
: List.of();
|
||||
|
||||
List<CemGeometry.Baked> baked = new ArrayList<>();
|
||||
List<CemGeometry.Baked> glow = new ArrayList<>(); // additive energy-swirl layers
|
||||
Out out = new Out();
|
||||
if (!invisible) {
|
||||
baked.addAll(body);
|
||||
if (vanilla) addVanillaLayers(s, ch, info, pose, tex, baked, glow);
|
||||
(vanilla && unshaded(model) ? out.unshaded : out.lit).addAll(body);
|
||||
if (vanilla) addVanillaLayers(s, ch, info, pose, tex, out);
|
||||
}
|
||||
// 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);
|
||||
addArmorLayers(s, ch, pose, out);
|
||||
}
|
||||
if (vanilla && !invisible) addShoulderParrots(s, ch, t0, baked);
|
||||
if (vanilla && !invisible) addShoulderParrots(s, ch, t0, out);
|
||||
if (vanilla) addEmissiveLayers(s, ch, info, pose, out);
|
||||
List<CemGeometry.Baked> baked = out.lit;
|
||||
List<CemGeometry.Baked> glow = out.glow;
|
||||
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);
|
||||
if (baked.isEmpty() && out.unshaded.isEmpty() && glow.isEmpty() && itemCubes.isEmpty()) {
|
||||
return invisible ? null : fallbackBox(s, tex);
|
||||
}
|
||||
|
||||
// Legacy CEM models: snap the lowest point to the ground (their rest geometry has no reliable origin).
|
||||
if (place == null) {
|
||||
@@ -176,6 +181,9 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
|
||||
List<EntityCube> cubes = new ArrayList<>(baked.size());
|
||||
for (CemGeometry.Baked b : baked) cubes.add(new EntityCube(b.from(), b.to(), b.faces(), place.mul(b.world())));
|
||||
for (CemGeometry.Baked b : out.unshaded) {
|
||||
cubes.add(new EntityCube(b.from(), b.to(), b.faces(), place.mul(b.world()), false, true));
|
||||
}
|
||||
for (CemGeometry.Baked b : glow) {
|
||||
cubes.add(new EntityCube(b.from(), b.to(), b.faces(), place.mul(b.world()), true));
|
||||
}
|
||||
@@ -192,6 +200,80 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Baked boxes of one entity by how they are drawn: shaded (normal), unshaded (render types without cardinal
|
||||
* lighting: eyes, breeze wind) and additive (energy swirl).
|
||||
*/
|
||||
private static final class Out {
|
||||
final List<CemGeometry.Baked> lit = new ArrayList<>();
|
||||
final List<CemGeometry.Baked> unshaded = new ArrayList<>();
|
||||
final List<CemGeometry.Baked> glow = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bakes a vanilla model the way its render type draws it: texels processed by {@link #renderTexture}, routed to
|
||||
* the unshaded boxes when the pipeline has no cardinal lighting.
|
||||
*/
|
||||
private void add(Out out, Pose pose, CemModelLoader.CemModel m, int[][] tex) {
|
||||
(unshaded(m) ? out.unshaded : out.lit).addAll(pose.bake(m, renderTexture(m, tex)));
|
||||
}
|
||||
|
||||
// Overlay layers that cover surfaces of the body (EyesLayer draws the parent model itself; the warden / copper
|
||||
// golem / creaking emissive layers and the sheep undercoat draw the body mesh or subsets of it, no inflation).
|
||||
// Vanilla draws them afterwards and the depth test passes on equal depth, i.e. on top of the coplanar body; here
|
||||
// their boxes grow by this much (model px) so they lie just in front of it — well above the tracer's layer
|
||||
// epsilon, far below a visible size (on coplanar ties the earlier-baked body would win).
|
||||
private static final double OVERLAY_INFLATE = 1 / 256.0;
|
||||
|
||||
/** {@code baked} boxes grown by {@link #OVERLAY_INFLATE}, added to {@code target}. */
|
||||
private static void addOverlay(List<CemGeometry.Baked> target, List<CemGeometry.Baked> baked) {
|
||||
for (CemGeometry.Baked b : baked) {
|
||||
double[] from = b.from().clone(), to = b.to().clone();
|
||||
for (int i = 0; i < 3; i++) {
|
||||
from[i] -= OVERLAY_INFLATE;
|
||||
to[i] += OVERLAY_INFLATE;
|
||||
}
|
||||
target.add(new CemGeometry.Baked(from, to, b.faces(), b.world()));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean unshaded(CemModelLoader.CemModel m) {
|
||||
CemModelLoader.RenderType rt = m == null ? null : models.renderType(m.renderType());
|
||||
return rt != null && !rt.shaded();
|
||||
}
|
||||
|
||||
/**
|
||||
* A texture as the model's vanilla render type sees it: texels below the shader's ALPHA_CUTOUT (0.1 → alpha
|
||||
* < 26) are discarded; cutout and solid types write the rest opaque (partial alpha in e.g. llama or
|
||||
* tropical-fish pattern textures is NOT blended in vanilla), solid types draw even fully transparent texels;
|
||||
* only blending types (entity_translucent: slime skin, vex, allay, player skins, …) keep the alpha for the
|
||||
* tracer to blend. Unknown render type (legacy models, old dumps) or additive: unchanged. Cached; returns
|
||||
* {@code tex} itself when nothing changes.
|
||||
*/
|
||||
private int[][] renderTexture(CemModelLoader.CemModel m, int[][] tex) {
|
||||
return renderTexture(m == null ? null : models.renderType(m.renderType()), tex);
|
||||
}
|
||||
|
||||
private int[][] renderTexture(CemModelLoader.RenderType rt, int[][] tex) {
|
||||
if (rt == null || tex == null || "additive".equals(rt.blend())) return tex;
|
||||
boolean blend = rt.blend() != null;
|
||||
int min = rt.cutout() < 0 ? 0 : (int) Math.ceil(rt.cutout() * 255 - 1e-6);
|
||||
return derived.get(DerivedTextureCache.key("rendertype", tex, min, blend), () -> {
|
||||
int[][] out = null;
|
||||
for (int y = 0; y < tex.length; y++) {
|
||||
for (int x = 0; x < tex[y].length; x++) {
|
||||
int p = tex[y][x];
|
||||
int a = p >>> 24;
|
||||
int na = a < min ? 0 : blend ? a : 255;
|
||||
if (na == a) continue;
|
||||
if (out == null) out = TextureOps.deepCopy(tex);
|
||||
out[y][x] = (na << 24) | (p & 0xFFFFFF);
|
||||
}
|
||||
}
|
||||
return out == null ? tex : out;
|
||||
});
|
||||
}
|
||||
|
||||
/** A chosen pose variant: its name, part overrides and (when it changes it) the renderer transform. */
|
||||
private record SelectedPose(
|
||||
String name, Map<String, CemModelLoader.PartPose> parts, CemModelLoader.PoseTransform transform) {
|
||||
@@ -219,13 +301,11 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
: ch.name.substring(ch.base.length() + 1);
|
||||
CemModelLoader.PoseSet set = info.poseSet(sub);
|
||||
if (set.poses().isEmpty()) return SelectedPose.NONE;
|
||||
// Held-item arm poses (players' ITEM pose) only exist where vanilla has them; mob arm poses (illagers'
|
||||
// crossbow hold, piglins' admiring/attacking, …) come as "arms_<pose>" state flags from the snapshot.
|
||||
List<String> flags = new ArrayList<>();
|
||||
if (isIllager(info)) {
|
||||
if (isCrossbow(s.rightHand()) || isCrossbow(s.leftHand())) flags.add("arms_crossbow_hold");
|
||||
} else {
|
||||
if (s.rightHand() != null) flags.add("item_r");
|
||||
if (s.leftHand() != null) flags.add("item_l");
|
||||
}
|
||||
if (s.rightHand() != null) flags.add("item_r");
|
||||
if (s.leftHand() != null) flags.add("item_l");
|
||||
if (s.passenger()) flags.add("passenger");
|
||||
if (s.arms()) flags.add("arms");
|
||||
if (s.chest()) flags.add("chest");
|
||||
@@ -324,10 +404,6 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
return out == null ? parts : out;
|
||||
}
|
||||
|
||||
private static boolean isCrossbow(ItemSpec i) {
|
||||
return i != null && i.id().equals("crossbow");
|
||||
}
|
||||
|
||||
/**
|
||||
* Held items (vanilla ItemInHandLayer: hand frame · rotX(-90) · rotY(180) · T(±1/16, 0.125, -0.625),
|
||||
* third-person display context), crossed-arms items (villagers/witches: root · T(0, 0.4, -0.4) ·
|
||||
@@ -416,7 +492,7 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
* Parrots on a player's shoulders (vanilla ParrotOnShoulderLayer): the parrot model in its ON_SHOULDER pose,
|
||||
* in the player's model space at T(±0.4, -1.5 [crouching -1.3], 0), textured by its variant.
|
||||
*/
|
||||
private void addShoulderParrots(EntityState s, Choice ch, Affine t0, List<CemGeometry.Baked> baked) {
|
||||
private void addShoulderParrots(EntityState s, Choice ch, Affine t0, Out out) {
|
||||
if (t0 == null || (s.shoulderLeft() == null && s.shoulderRight() == null)) return;
|
||||
if (ch == null || !ch.base.startsWith("player")) return;
|
||||
CemModelLoader.CemModel pm = models.vanilla("parrot");
|
||||
@@ -437,7 +513,7 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
Affine pre = t0.mul(Affine.translation(side == 0 ? 0.4 : -0.4, y, 0))
|
||||
.mul(Affine.scale(1 / 16.0))
|
||||
.mul(CemGeometry.CEM_TO_JAVA);
|
||||
baked.addAll(CemGeometry.bakePosed(pm, tex, pre, parts, null));
|
||||
out.lit.addAll(CemGeometry.bakePosed(pm, renderTexture(pm, tex), pre, parts, null));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -536,13 +612,7 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
// --- vanilla render layers -----------------------------------------------------------------------
|
||||
|
||||
private void addVanillaLayers(
|
||||
EntityState s,
|
||||
Choice ch,
|
||||
CemModelLoader.EntityInfo info,
|
||||
Pose pose,
|
||||
int[][] bodyTex,
|
||||
List<CemGeometry.Baked> baked,
|
||||
List<CemGeometry.Baked> glow) {
|
||||
EntityState s, Choice ch, CemModelLoader.EntityInfo info, Pose pose, int[][] bodyTex, Out out) {
|
||||
String base = ch.base;
|
||||
boolean baby = ch.babyModel;
|
||||
for (String n : layerModels(base)) {
|
||||
@@ -553,29 +623,39 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
if (lm == null) continue;
|
||||
switch (layerClass) {
|
||||
case "SheepWoolLayer", "SheepWoolUndercoatLayer" -> {
|
||||
if ("sheared".equals(s.variant()) || lm.texture() == null) continue;
|
||||
// Wool: unless sheared. Undercoat (SheepWoolUndercoatLayer): only adults (no baby model) with
|
||||
// non-white or jeb_ wool ("undercoat" flag from the snapshot) — sheared or not.
|
||||
boolean undercoat = layerClass.equals("SheepWoolUndercoatLayer");
|
||||
if (undercoat ? !s.hasPose("undercoat") : "sheared".equals(s.variant())) continue;
|
||||
if (lm.texture() == null) continue;
|
||||
int[][] t = texture(lm.texture());
|
||||
if (t == null) continue;
|
||||
baked.addAll(pose.bake(lm, tinted(t, s.tint())));
|
||||
if (undercoat || baby) {
|
||||
// Undercoat and baby wool have the body's mesh (no inflation; coloredCutoutModelCopyLayerRender
|
||||
// draws them on top): an overlay on its surface.
|
||||
addOverlay(out.lit, pose.bake(lm, renderTexture(lm, tinted(t, s.tint()))));
|
||||
} else {
|
||||
add(out, pose, lm, tinted(t, s.tint()));
|
||||
}
|
||||
}
|
||||
case "TropicalFishPatternLayer" -> addTropicalPattern(s, n, lm, pose, baked);
|
||||
case "TropicalFishPatternLayer" -> addTropicalPattern(s, n, lm, pose, out);
|
||||
case "CatCollarLayer" -> {
|
||||
// Own collar model (inflated by 0.01), collar texture tinted with the collar's dye colour.
|
||||
if (s.collar() == 0 || lm.texture() == null) continue;
|
||||
int[][] t = texture(lm.texture());
|
||||
if (t == null) continue;
|
||||
baked.addAll(pose.bake(lm, tinted(t, s.collar())));
|
||||
add(out, pose, lm, tinted(t, s.collar()));
|
||||
}
|
||||
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));
|
||||
if (t != null) out.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));
|
||||
if (t != null) add(out, pose, lm, t);
|
||||
}
|
||||
case "LlamaDecorLayer", "WolfArmorLayer" -> {
|
||||
// Body equipment drawn by a dedicated layer: texture folder <base>_body (trader llama: llama_body).
|
||||
@@ -585,19 +665,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));
|
||||
if (t != null) add(out, pose, 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));
|
||||
if (t != null) add(out, pose, lm, t);
|
||||
}
|
||||
default -> {
|
||||
// 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));
|
||||
if (t != null) add(out, pose, lm, t);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -615,20 +695,19 @@ 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));
|
||||
if (t != null) add(out, pose, em, t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Tropical fish pattern overlay: pattern texture (tinted with the pattern colour) on the pattern model. */
|
||||
private void addTropicalPattern(
|
||||
EntityState s, String name, CemModelLoader.CemModel lm, Pose pose, List<CemGeometry.Baked> baked) {
|
||||
private void addTropicalPattern(EntityState s, String name, CemModelLoader.CemModel lm, Pose pose, Out out) {
|
||||
int idx = EntityModels.tropicalPattern(s.variant());
|
||||
if (idx < 0) return;
|
||||
boolean small = idx < 6; // patterns 0-5 use shape A (small model), 6-11 shape B (large model)
|
||||
if (name.toLowerCase(Locale.ROOT).contains("small") != small) return;
|
||||
int[][] t = texture("entity/fish/tropical_" + (small ? "a" : "b") + "_pattern_" + (idx % 6 + 1));
|
||||
if (t == null) return;
|
||||
baked.addAll(pose.bake(lm, tinted(t, s.tint2())));
|
||||
add(out, pose, lm, tinted(t, s.tint2()));
|
||||
}
|
||||
|
||||
/** entity/equipment/<layerType>/<asset> (+ a "_carpet"-stripped name; leather dye + overlay); cached. */
|
||||
@@ -721,6 +800,51 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
});
|
||||
}
|
||||
|
||||
// --- emissive layers -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Emissive layers of the dump ({@link CemModelLoader.EmissiveLayer}: spider / enderman / phantom / breeze /
|
||||
* copper-golem / creaking eyes, warden bioluminescence): the layer's texture (per variant, e.g. copper-golem
|
||||
* weathering) on its model, processed by its render type (eyes: blended and unshaded; translucent emissive:
|
||||
* blended, shaded), times the static layer alpha. Skipped: animated layers (warden pulsating spots), layers
|
||||
* whose state flag is off (creaking eyes glow only while active), and on invisible entities those vanilla
|
||||
* hides (EyesLayer draws even then).
|
||||
*/
|
||||
private void addEmissiveLayers(EntityState s, Choice ch, CemModelLoader.EntityInfo info, Pose pose, Out out) {
|
||||
if (info == null || ch == null) return;
|
||||
for (CemModelLoader.EmissiveLayer e : info.emissive()) {
|
||||
if (e.animated() || e.alpha() <= 1e-5) continue;
|
||||
if (e.when() != null && !s.hasPose(e.when())) continue;
|
||||
if (s.invisible() && !e.invisible()) continue;
|
||||
// A layer drawing the parent model follows the chosen body model.
|
||||
CemModelLoader.CemModel m = e.model().equals(ch.base) ? ch.model : models.vanilla(e.model());
|
||||
if (m == null) continue;
|
||||
String variantTex =
|
||||
s.variant() == null ? null : e.textures().get(s.variant().toLowerCase(Locale.ROOT));
|
||||
int[][] t = texture(variantTex != null ? variantTex : e.texture());
|
||||
if (t == null) continue;
|
||||
CemModelLoader.RenderType rt = models.renderType(e.renderType());
|
||||
t = renderTexture(rt, t);
|
||||
if (e.alpha() < 1) t = withAlpha(t, e.alpha());
|
||||
// Overlay on the coplanar body surface (see OVERLAY_INFLATE).
|
||||
addOverlay(rt != null && !rt.shaded() ? out.unshaded : out.lit, pose.bake(m, t));
|
||||
}
|
||||
}
|
||||
|
||||
/** {@code src} with every texel's alpha multiplied by {@code alpha} (vertex alpha of a layer); cached. */
|
||||
private int[][] withAlpha(int[][] src, double alpha) {
|
||||
return derived.get(DerivedTextureCache.key("alpha", src, alpha), () -> {
|
||||
int[][] out = TextureOps.deepCopy(src);
|
||||
for (int[] row : out) {
|
||||
for (int x = 0; x < row.length; x++) {
|
||||
int a = (int) Math.round((row[x] >>> 24) * alpha);
|
||||
row[x] = (a << 24) | (row[x] & 0xFFFFFF);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
// --- texture resolution --------------------------------------------------------------------------
|
||||
|
||||
private int[][] texture(String path) {
|
||||
@@ -728,7 +852,9 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Player skin, else the data-driven variant's asset (baby / tame aware), else the variant-aware candidates
|
||||
* Player skin, else a render-state texture of the dump ({@code stateTextures}: tipped arrow, invulnerable
|
||||
* wither) whose flag the entity has, else the data-driven variant's asset (baby / tame / angry 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.
|
||||
*/
|
||||
@@ -740,15 +866,22 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
if (def != null) return def;
|
||||
}
|
||||
boolean babyModel = ch != null && ch.babyModel;
|
||||
CemModelLoader.EntityInfo info = models.entityInfo(s.typeKey());
|
||||
if (info == null && ch != null) info = models.entityInfo(ch.base);
|
||||
if (info != null && s.pose() != null) {
|
||||
for (Map.Entry<String, String> st : info.stateTextures().entrySet()) {
|
||||
if (!s.hasPose(st.getKey())) continue;
|
||||
int[][] t = texture(st.getValue());
|
||||
if (t != null) return t;
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
String asset = va == null ? null : va.texture(babyModel, s.collar() != 0, s.hasPose("angry"));
|
||||
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<>();
|
||||
for (ResourceLocation rl : EntityModels.textureCandidates(s.typeKey(), s.variant())) adult.add(rl.path());
|
||||
if (info != null && info.texture() != null && !info.texture().equals("missingno")) {
|
||||
@@ -813,35 +946,35 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
// like the body — raised zombie arms included.
|
||||
private static final int GLINT_COLOR = 0xFF8040CC; // approximated enchantment-glint purple
|
||||
|
||||
private void addArmorLayers(EntityState s, Choice ch, Pose pose, List<CemGeometry.Baked> baked) {
|
||||
private void addArmorLayers(EntityState s, Choice ch, Pose pose, Out out) {
|
||||
EntityState.Equipment eq = s.equipment();
|
||||
String base = ch == null ? s.typeKey() : ch.base;
|
||||
boolean baby = ch != null && ch.babyModel;
|
||||
String set = base + "/HumanoidArmorLayer." + (baby ? "babyModelSet" : "modelSet") + ".";
|
||||
String folder = baby ? "humanoid_baby" : "humanoid";
|
||||
bakeArmorPiece(eq.head(), set + "head", folder, pose, baked);
|
||||
// HumanoidArmorLayer: babies use the humanoid_baby equipment textures — except small armor stands, whose
|
||||
// armor models are the adult ones scaled (ARMOR_STAND_SMALL_ARMOR = BABY_TRANSFORMER over the adult
|
||||
// layer, adult UVs) and keep humanoid / humanoid_leggings.
|
||||
boolean babyTextures = baby && !s.typeKey().equals("armor_stand");
|
||||
String folder = babyTextures ? "humanoid_baby" : "humanoid";
|
||||
bakeArmorPiece(eq.head(), set + "head", folder, pose, out);
|
||||
if (eq.chest() != null && eq.chest().asset().equals("elytra")) {
|
||||
bakeElytra(eq.chest(), base, baby, pose, baked);
|
||||
bakeElytra(eq.chest(), base, baby, pose, out);
|
||||
} else {
|
||||
bakeArmorPiece(eq.chest(), set + "chest", folder, pose, baked);
|
||||
bakeArmorPiece(eq.chest(), set + "chest", folder, pose, out);
|
||||
}
|
||||
bakeArmorPiece(eq.legs(), set + "legs", baby ? "humanoid_baby" : "humanoid_leggings", pose, baked);
|
||||
bakeArmorPiece(eq.feet(), set + "feet", folder, pose, baked);
|
||||
bakeArmorPiece(eq.legs(), set + "legs", babyTextures ? "humanoid_baby" : "humanoid_leggings", pose, out);
|
||||
bakeArmorPiece(eq.feet(), set + "feet", folder, pose, out);
|
||||
}
|
||||
|
||||
/** Bake one armor slot: its vanilla per-slot model, textured for the material. */
|
||||
private void bakeArmorPiece(
|
||||
EntityState.EquipPiece piece,
|
||||
String modelName,
|
||||
String layerFolder,
|
||||
Pose pose,
|
||||
List<CemGeometry.Baked> baked) {
|
||||
EntityState.EquipPiece piece, String modelName, String layerFolder, Pose pose, Out out) {
|
||||
if (piece == null) return;
|
||||
CemModelLoader.CemModel model = models.vanilla(modelName);
|
||||
if (model == null) return;
|
||||
int[][] tex = derived.get(
|
||||
DerivedTextureCache.key("armor", piece, layerFolder), () -> buildArmorTexture(piece, layerFolder));
|
||||
if (tex != null) baked.addAll(pose.bake(model, tex));
|
||||
if (tex != null) add(out, pose, model, tex);
|
||||
}
|
||||
|
||||
/** Resolve + composite a single armor slot's texture: material base (+leather dye/overlay), trim, glint. */
|
||||
@@ -888,8 +1021,7 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
private void bakeElytra(EntityState.EquipPiece piece, String base, boolean baby, Pose pose, Out out) {
|
||||
CemModelLoader.CemModel model =
|
||||
models.vanilla(base + "/WingsLayer." + (baby ? "elytraBabyModel" : "elytraModel"));
|
||||
if (model == null) return; // renderer without a WingsLayer
|
||||
@@ -897,12 +1029,12 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
if (tex == null) return;
|
||||
if (piece.glint()) {
|
||||
tex = derived.get(DerivedTextureCache.key("elytra_glint", tex), () -> {
|
||||
int[][] out = TextureOps.deepCopy(texture("entity/equipment/wings/elytra"));
|
||||
applyGlint(out);
|
||||
return out;
|
||||
int[][] glinted = TextureOps.deepCopy(texture("entity/equipment/wings/elytra"));
|
||||
applyGlint(glinted);
|
||||
return glinted;
|
||||
});
|
||||
}
|
||||
baked.addAll(pose.bake(model, tex));
|
||||
add(out, pose, model, tex);
|
||||
}
|
||||
|
||||
private RenderedEntity fallbackBox(EntityState s, int[][] tex) {
|
||||
|
||||
@@ -53,9 +53,36 @@ public final class CemModelLoader {
|
||||
|
||||
/**
|
||||
* A whole model: declared texture size and its top-level parts. {@code absolute} models (dumped from
|
||||
* the vanilla client by {@code tools/VanillaModelDump.java}) carry absolute part pivots + boxes.
|
||||
* the vanilla client by {@code tools/VanillaModelDump.java}) carry absolute part pivots + boxes and the name
|
||||
* of the render type vanilla draws them with ({@code renderType}, see {@link #renderType}; null = unknown).
|
||||
*/
|
||||
public record CemModel(int texW, int texH, List<CemPart> parts, boolean absolute, String texture) {}
|
||||
public record CemModel(
|
||||
int texW, int texH, List<CemPart> parts, boolean absolute, String texture, String renderType) {}
|
||||
|
||||
/**
|
||||
* How a vanilla render type's pipeline draws texels: {@code cutout} = shader ALPHA_CUTOUT threshold (0..1,
|
||||
* texels below it are discarded; -1 = none), {@code blend} = alpha-blended ("translucent") / added
|
||||
* ("additive") / null = written opaque (cutout and solid types: alpha ignored), {@code shaded} = directional
|
||||
* face shading (false with NO_CARDINAL_LIGHTING: eyes, breeze wind).
|
||||
*/
|
||||
public record RenderType(double cutout, String blend, boolean shaded) {}
|
||||
|
||||
/**
|
||||
* An emissive render layer (EyesLayer, LivingEntityEmissiveLayer, BreezeEyesLayer): the dumped model it
|
||||
* draws (the body model's name for layers that draw the parent model), its texture (per render-state enum
|
||||
* constant in {@code textures}, e.g. copper-golem weathering), render type, the constant vertex alpha of
|
||||
* its alpha function, the state flag it needs ({@code when}, null = always), whether it is still drawn on an
|
||||
* invisible entity, and whether its alpha is animated (not drawn: a photo has no age).
|
||||
*/
|
||||
public record EmissiveLayer(
|
||||
String model,
|
||||
String texture,
|
||||
Map<String, String> textures,
|
||||
String renderType,
|
||||
double alpha,
|
||||
String when,
|
||||
boolean invisible,
|
||||
boolean animated) {}
|
||||
|
||||
/** Per-entity-type renderer info from the vanilla dump: extra render scale and default textures. */
|
||||
public record EntityInfo(
|
||||
@@ -69,7 +96,9 @@ public final class CemModelLoader {
|
||||
boolean humanoidArmor, // the renderer has a HumanoidArmorLayer (wears humanoid armor)
|
||||
Map<String, PoseTransform> poseTransforms, // pose -> renderer transform when it differs (sleeping, …)
|
||||
Map<String, PoseSet> modelPoses, // body sub-model ("baby", "smallModel", …) -> its own poses
|
||||
Map<String, RotationBinding> rotationBindings) { // state field ("headPose", "xRot", …) -> part rotations
|
||||
Map<String, RotationBinding> rotationBindings, // state field ("headPose", "xRot", …) -> part rotations
|
||||
List<EmissiveLayer> emissive, // emissive layers (eyes, glow), in vanilla layer order
|
||||
Map<String, String> stateTextures) { // state flag ("tipped", "invulnerable") -> body texture
|
||||
|
||||
/** Whether the vanilla renderer draws this layer for an entity in its default state. */
|
||||
public boolean layerDrawnByDefault(String layerClass) {
|
||||
@@ -132,9 +161,12 @@ public final class CemModelLoader {
|
||||
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";
|
||||
/**
|
||||
* Texture id for the entity's state: wolves by tame / angry / wild (vanilla Wolf.getTexture: a tame wolf
|
||||
* keeps its tame texture when angry), others by asset id; baby first if asked.
|
||||
*/
|
||||
public String texture(boolean baby, boolean tame, boolean angry) {
|
||||
String state = tame ? "tame" : angry ? "angry" : "wild";
|
||||
if (baby && babyAssets.containsKey(state)) return babyAssets.get(state);
|
||||
if (baby && babyAsset != null) return babyAsset;
|
||||
if (assets.containsKey(state)) return assets.get(state);
|
||||
@@ -162,6 +194,7 @@ public final class CemModelLoader {
|
||||
// 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<>();
|
||||
private final Map<String, RenderType> renderTypes = 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<>();
|
||||
|
||||
@@ -191,6 +224,11 @@ public final class CemModelLoader {
|
||||
return vanilla.keySet();
|
||||
}
|
||||
|
||||
/** Pipeline properties of a vanilla render type by name (see {@link CemModel#renderType()}), or null. */
|
||||
public RenderType renderType(String name) {
|
||||
return name == null ? null : renderTypes.get(name);
|
||||
}
|
||||
|
||||
/** Vanilla renderer info for an entity type key, or null (not in the dump). */
|
||||
public EntityInfo entityInfo(String typeKey) {
|
||||
return entities.get(typeKey);
|
||||
@@ -230,7 +268,21 @@ public final class CemModelLoader {
|
||||
o.has("humanoidArmor") && o.get("humanoidArmor").getAsBoolean(),
|
||||
poseTransforms(o.getAsJsonObject("poseTransforms")),
|
||||
modelPoses(o.getAsJsonObject("modelPoses")),
|
||||
rotationBindings(o.getAsJsonObject("rotationBindings"))));
|
||||
rotationBindings(o.getAsJsonObject("rotationBindings")),
|
||||
emissive(o.getAsJsonArray("emissive")),
|
||||
strings(o.getAsJsonObject("stateTextures"))));
|
||||
}
|
||||
if (root.has("renderTypes")) {
|
||||
for (Map.Entry<String, JsonElement> t :
|
||||
root.getAsJsonObject("renderTypes").entrySet()) {
|
||||
JsonObject o = t.getValue().getAsJsonObject();
|
||||
renderTypes.put(
|
||||
t.getKey(),
|
||||
new RenderType(
|
||||
o.has("cutout") ? o.get("cutout").getAsDouble() : -1,
|
||||
o.has("blend") ? o.get("blend").getAsString() : null,
|
||||
!o.has("shaded") || o.get("shaded").getAsBoolean()));
|
||||
}
|
||||
}
|
||||
if (root.has("variants")) {
|
||||
for (Map.Entry<String, JsonElement> t :
|
||||
@@ -262,7 +314,9 @@ public final class CemModelLoader {
|
||||
boolean absolute =
|
||||
model.has("absolute") && model.get("absolute").getAsBoolean();
|
||||
String texture = model.has("texture") ? model.get("texture").getAsString() : null;
|
||||
target.put(e.getKey(), new CemModel(tw, th, parts, absolute, texture));
|
||||
String renderType =
|
||||
model.has("renderType") ? model.get("renderType").getAsString() : null;
|
||||
target.put(e.getKey(), new CemModel(tw, th, parts, absolute, texture, renderType));
|
||||
} catch (Exception ex) {
|
||||
if (logger != null) logger.warning("Failed to parse CEM model " + e.getKey() + ": " + ex.getMessage());
|
||||
}
|
||||
@@ -420,6 +474,32 @@ public final class CemModelLoader {
|
||||
return out;
|
||||
}
|
||||
|
||||
private static List<EmissiveLayer> emissive(JsonArray a) {
|
||||
List<EmissiveLayer> out = new ArrayList<>();
|
||||
if (a == null) return out;
|
||||
for (JsonElement el : a) {
|
||||
JsonObject o = el.getAsJsonObject();
|
||||
out.add(new EmissiveLayer(
|
||||
o.get("model").getAsString(),
|
||||
o.get("texture").getAsString(),
|
||||
strings(o.getAsJsonObject("textures")),
|
||||
o.has("renderType") ? o.get("renderType").getAsString() : null,
|
||||
o.has("alpha") ? o.get("alpha").getAsDouble() : 1,
|
||||
o.has("when") ? o.get("when").getAsString() : null,
|
||||
o.has("invisible") && o.get("invisible").getAsBoolean(),
|
||||
o.has("animated") && o.get("animated").getAsBoolean()));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static Map<String, String> strings(JsonObject o) {
|
||||
Map<String, String> out = new HashMap<>();
|
||||
if (o == null) return out;
|
||||
for (Map.Entry<String, JsonElement> e : o.entrySet())
|
||||
out.put(e.getKey(), e.getValue().getAsString());
|
||||
return out;
|
||||
}
|
||||
|
||||
private static Map<String, Boolean> layers(JsonObject o) {
|
||||
Map<String, Boolean> out = new HashMap<>();
|
||||
if (o.has("layers")) {
|
||||
|
||||
@@ -149,7 +149,7 @@ public final class ElementIntersector {
|
||||
double t = sc.t[k];
|
||||
Vector world = new Vector(bx + ox + dx * t, by + oy + dy * t, bz + oz + dz * t);
|
||||
Vector normal = new Vector(sc.nx[k], sc.ny[k], sc.nz[k]);
|
||||
return new FaceHit(t, world, normal, color, face.tintIndex, false);
|
||||
return new FaceHit(t, world, normal, color, face.tintIndex, false, false);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ import org.bukkit.util.Vector;
|
||||
|
||||
/**
|
||||
* The result of intersecting a ray with a block's geometry: the world-space hit point and normal,
|
||||
* the sampled ARGB color (before shading/tinting), the face's tint index ({@code -1} = none) and whether the
|
||||
* surface is drawn additively (emissive energy-swirl layers: its colour is added to what lies behind).
|
||||
* the sampled ARGB color (before shading/tinting), the face's tint index ({@code -1} = none), whether the
|
||||
* surface is drawn additively (emissive energy-swirl layers: its colour is added to what lies behind) and whether
|
||||
* it skips directional face shading (entity layers without cardinal lighting: eyes, breeze wind).
|
||||
*/
|
||||
public record FaceHit(double t, Vector point, Vector normal, int color, int tintIndex, boolean additive) {}
|
||||
public record FaceHit(
|
||||
double t, Vector point, Vector normal, int color, int tintIndex, boolean additive, boolean unshaded) {}
|
||||
@@ -102,9 +102,11 @@ public final class SnapshotRaytracer {
|
||||
double t;
|
||||
boolean additive = false;
|
||||
if (eh != null) {
|
||||
// Emissive additive surfaces (energy swirl) are unshaded: vanilla NO_CARDINAL_LIGHTING + EMISSIVE.
|
||||
// Emissive additive surfaces (energy swirl) and layers without cardinal lighting (eyes, breeze
|
||||
// wind) are unshaded: vanilla NO_CARDINAL_LIGHTING (+ EMISSIVE).
|
||||
additive = eh.additive();
|
||||
color = additive ? eh.color() : ColorUtil.shade(eh.color(), shadeFactor(eh.normal()));
|
||||
boolean flat = additive || eh.unshaded();
|
||||
color = flat ? eh.color() : ColorUtil.shade(eh.color(), shadeFactor(eh.normal()));
|
||||
t = eh.t();
|
||||
} else if (pending != null) {
|
||||
color = pending.color;
|
||||
|
||||
+121
-1
@@ -16,6 +16,7 @@ import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.DoubleSupplier;
|
||||
import org.bukkit.DyeColor;
|
||||
import org.bukkit.Keyed;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
@@ -164,7 +165,7 @@ public final class EntitySnapshotBuilder {
|
||||
if (e instanceof Slime sl) sizeScale = sl.getSize();
|
||||
// MushroomCow extends Cow, ZombieVillager does not extend Villager — order matters.
|
||||
if (e instanceof Sheep sh) {
|
||||
tint = ColorUtil.dyeArgb(sh.getColor(), 0);
|
||||
tint = isJebSheep(sh) ? jebWoolColor(sh.getTicksLived()) : sheepWoolColor(sh.getColor());
|
||||
if (sh.isSheared()) variant = "sheared";
|
||||
} else if (e instanceof TropicalFish tf) {
|
||||
variant = keyOf(tf.getPattern());
|
||||
@@ -187,6 +188,9 @@ public final class EntitySnapshotBuilder {
|
||||
// WitherBoss.isPowered(): health <= max health / 2 (wither armor layer).
|
||||
AttributeInstance max = wi.getAttribute(Attribute.MAX_HEALTH);
|
||||
powered = max != null && wi.getHealth() <= max.getValue() / 2;
|
||||
// WitherBossRenderer.scale: 2 - invulnerableTicks / 220 · 0.5 (the dumped transform holds the 2).
|
||||
int inv = wi.getInvulnerableTicks();
|
||||
if (inv > 0) sizeScale = (2 - inv / 220f * 0.5f) / 2;
|
||||
} else if (e instanceof Axolotl a) {
|
||||
variant = keyOf(a.getVariant());
|
||||
} else if (e instanceof Parrot p) {
|
||||
@@ -386,12 +390,128 @@ public final class EntitySnapshotBuilder {
|
||||
} else if (e instanceof Sniffer sn && sn.getState() == Sniffer.State.DIGGING) {
|
||||
f.add("digging");
|
||||
}
|
||||
renderFlags(e, f);
|
||||
} catch (Throwable ignored) {
|
||||
// API missing on this server version: render the flags found so far
|
||||
}
|
||||
return f.isEmpty() ? null : String.join("+", f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render-state flags beyond poses (the baker ignores those a type doesn't use): mob arm poses
|
||||
* ({@code arms_<pose>}, the dump's IllagerArmPose / PiglinArmPose constants), angry wolves ({@code angry}:
|
||||
* angry texture + raised tail), tipped arrows ({@code tipped}), the invulnerable (blinking) wither
|
||||
* ({@code invulnerable}), active creakings ({@code eyes_glowing}) and sheep with an undercoat
|
||||
* ({@code undercoat}).
|
||||
*/
|
||||
private static void renderFlags(Entity e, java.util.Set<String> f) {
|
||||
String arms = armPose(e);
|
||||
if (arms != null) f.add("arms_" + arms);
|
||||
if (e instanceof Wolf w && w.isAngry()) f.add("angry");
|
||||
// TippableArrowRenderer: isTipped = Arrow.getColor() > 0 — the raw ARGB int, so only colours without the
|
||||
// alpha bit (custom RGB colours) count; potion colours are opaque ARGB (negative) and keep the plain arrow.
|
||||
if (e instanceof org.bukkit.entity.Arrow ar
|
||||
&& ar.getColor() != null
|
||||
&& ar.getColor().asARGB() > 0) {
|
||||
f.add("tipped");
|
||||
}
|
||||
if (e instanceof Wither wi) {
|
||||
// WitherBossRenderer.getTextureLocation: invulnerable texture above 80 ticks, then blinking every 5.
|
||||
int t = wi.getInvulnerableTicks();
|
||||
if (t > 0 && (t > 80 || t / 5 % 2 != 1)) f.add("invulnerable");
|
||||
}
|
||||
if (e instanceof org.bukkit.entity.Creaking cr && cr.isActive()) f.add("eyes_glowing");
|
||||
// SheepWoolUndercoatLayer: jeb_ or non-white wool (adults only, decided by the baker), sheared or not.
|
||||
if (e instanceof Sheep sh && (isJebSheep(sh) || sh.getColor() != DyeColor.WHITE)) f.add("undercoat");
|
||||
}
|
||||
|
||||
/**
|
||||
* Arm pose of a mob whose renderer reads one from the entity (lower-case enum constant; null = the idle
|
||||
* pose): Piglin/PiglinBrute.getArmPose (dancing, admiring a loved off-hand item, aggressive with a melee
|
||||
* weapon, charging / holding a charged crossbow) and the illagers' getArmPose (pillager: crossbow charge /
|
||||
* hold, else attacking or neutral; vindicator: attacking / celebrating; spellcasters: spellcasting,
|
||||
* illusioner bow, celebrating; otherwise crossed).
|
||||
*/
|
||||
private static String armPose(Entity e) {
|
||||
boolean aggressive = e instanceof Mob m && m.isAggressive();
|
||||
if (e instanceof org.bukkit.entity.PiglinAbstract pa) {
|
||||
EntityEquipment eq = pa.getEquipment();
|
||||
ItemStack main = eq == null ? null : eq.getItemInMainHand();
|
||||
ItemStack off = eq == null ? null : eq.getItemInOffHand();
|
||||
boolean melee = main != null && !main.getType().isAir() && main.hasData(DataComponentTypes.TOOL);
|
||||
if (e instanceof org.bukkit.entity.Piglin p) {
|
||||
if (p.isDancing()) return "dancing";
|
||||
if (off != null && org.bukkit.Tag.ITEMS_PIGLIN_LOVED.isTagged(off.getType())) return "admiring_item";
|
||||
if (aggressive && melee) return "attacking_with_melee_weapon";
|
||||
if (p.isChargingCrossbow()) return "crossbow_charge";
|
||||
boolean holding = (main != null && main.getType() == Material.CROSSBOW)
|
||||
|| (off != null && off.getType() == Material.CROSSBOW);
|
||||
boolean charged = main != null
|
||||
&& main.getItemMeta() instanceof org.bukkit.inventory.meta.CrossbowMeta cm
|
||||
&& cm.hasChargedProjectiles();
|
||||
return holding && charged ? "crossbow_hold" : null;
|
||||
}
|
||||
return aggressive && melee ? "attacking_with_melee_weapon" : null;
|
||||
}
|
||||
if (e instanceof org.bukkit.entity.Pillager pi) {
|
||||
if (pi.isChargingAttack()) return "crossbow_charge";
|
||||
EntityEquipment eq = pi.getEquipment();
|
||||
boolean crossbow = eq != null
|
||||
&& (eq.getItemInMainHand().getType() == Material.CROSSBOW
|
||||
|| eq.getItemInOffHand().getType() == Material.CROSSBOW);
|
||||
return crossbow ? "crossbow_hold" : aggressive ? "attacking" : "neutral";
|
||||
}
|
||||
if (e instanceof org.bukkit.entity.Vindicator v) {
|
||||
return aggressive ? "attacking" : v.isCelebrating() ? "celebrating" : null;
|
||||
}
|
||||
if (e instanceof org.bukkit.entity.Spellcaster sc) {
|
||||
if (sc.getSpell() != org.bukkit.entity.Spellcaster.Spell.NONE) return "spellcasting";
|
||||
if (e instanceof org.bukkit.entity.Illusioner) return aggressive ? "bow_and_arrow" : null;
|
||||
return sc.isCelebrating() ? "celebrating" : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** SheepRenderer: a sheep named "jeb_" (checkMagicName) cycles through all wool colours. */
|
||||
private static boolean isJebSheep(Sheep sh) {
|
||||
net.kyori.adventure.text.Component name = sh.customName();
|
||||
return name != null
|
||||
&& "jeb_"
|
||||
.equals(net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer.plainText()
|
||||
.serialize(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Vanilla wool colour (ColorLerper.Type.SHEEP): the dye's texture-diffuse colour × 0.75, white 0xE6E6E6.
|
||||
*/
|
||||
private static int sheepWoolColor(DyeColor dye) {
|
||||
if (dye == null || dye == DyeColor.WHITE) return 0xFFE6E6E6;
|
||||
org.bukkit.Color c = dye.getColor();
|
||||
return ColorUtil.argb(
|
||||
0xFF,
|
||||
Math.clamp((int) Math.floor(c.getRed() * 0.75f), 0, 255),
|
||||
Math.clamp((int) Math.floor(c.getGreen() * 0.75f), 0, 255),
|
||||
Math.clamp((int) Math.floor(c.getBlue() * 0.75f), 0, 255));
|
||||
}
|
||||
|
||||
/**
|
||||
* jeb_ wool (ColorLerper.getLerpedColor(SHEEP, ageInTicks)) at the entity's age: 25 ticks per dye colour, linearly
|
||||
* blended to the next one.
|
||||
*/
|
||||
private static int jebWoolColor(int ticks) {
|
||||
DyeColor[] dyes = DyeColor.values();
|
||||
int value = ticks / 25;
|
||||
int c1 = sheepWoolColor(dyes[value % dyes.length]);
|
||||
int c2 = sheepWoolColor(dyes[(value + 1) % dyes.length]);
|
||||
float step = (ticks % 25) / 25f;
|
||||
int out = 0xFF000000;
|
||||
for (int sh = 0; sh <= 16; sh += 8) {
|
||||
int a = (c1 >> sh) & 0xFF, b = (c2 >> sh) & 0xFF;
|
||||
out |= (a + (int) Math.floor(step * (b - a))) << sh;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Cat.isLyingOnTopOfSleepingPlayer: a sleeping player within the cat's block box inflated by 2. */
|
||||
private static boolean sleepingPlayerNear(Entity cat) {
|
||||
Location l = cat.getLocation();
|
||||
|
||||
Reference in new issue
Block a user