entity state poses from the vanilla dump: sleeping/sitting/crouching/swimming/gliding, fox/cat/panda/parrot/bat/camel/sniffer poses with pose transforms, per-sub-model poses (baby pivots), armor-stand part poses and body pitch via rotation bindings, fish in water, beds, shoulder parrots, pose test scenes
This commit is contained in:
1 parent
6660ebe557
commit
45d033bd89
10 files changed
+1108
-110
No files matched your search
@@ -39,9 +39,22 @@ public record EntityState(
|
||||
ItemSpec headItem, // non-armor item worn in the head slot (pumpkin, skull, block, …); null = none
|
||||
int collar, // ARGB dye of a tamed cat's/wolf's collar; 0 = no collar (untamed)
|
||||
boolean powered, // energy-swirl layer: charged creeper, wither below half health
|
||||
ItemSpec mouthItem // item carried in the mouth (fox/dolphin main hand); null = none
|
||||
ItemSpec mouthItem, // item carried in the mouth (fox/dolphin main hand); null = none
|
||||
String pose, // state pose flags, sorted and "+"-joined ("sitting", "lying+head_up", "sleeping", "in_water",
|
||||
// …; the dump's STATE_POSES names, unknown ones are ignored per type); null = none (idle)
|
||||
java.util.Map<String, float[]> partPoses, // continuous part poses by vanilla render-state field (armor stand
|
||||
// "headPose"/"bodyPose"/"leftArmPose"/…: x/y/z degrees); null = defaults
|
||||
String shoulderLeft, // parrot variant on a player's left shoulder (e.g. "red"); null = none
|
||||
String shoulderRight // parrot variant on the right shoulder; null = none
|
||||
) {
|
||||
|
||||
/** Whether the state pose flags contain {@code flag}. */
|
||||
public boolean hasPose(String flag) {
|
||||
if (pose == null) return false;
|
||||
for (String f : pose.split("\\+")) if (f.equals(flag)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Worn armor (4 slots) of a humanoid wearer; any field may be null. */
|
||||
public record Equipment(EquipPiece head, EquipPiece chest, EquipPiece legs, EquipPiece feet) {
|
||||
public boolean isEmpty() {
|
||||
|
||||
@@ -111,23 +111,30 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
if (info == null) info = models.entityInfo(base);
|
||||
boolean halfBaby = s.baby() && (ch == null || !ch.babyModel); // no dedicated baby model -> 0.5x adult
|
||||
double extra = (halfBaby ? 0.5 : 1.0) * s.sizeScale();
|
||||
SelectedPose sel = vanilla ? selectPose(s, info, ch) : SelectedPose.NONE;
|
||||
Affine t0 = null;
|
||||
Affine pre;
|
||||
Affine place;
|
||||
if (vanilla) {
|
||||
// Vanilla placement: the renderer's own captured pose (incl. its scale / offsets), yaw and pitch
|
||||
// applied the way that renderer applies them. CEM px -> Java model units: F·T(0,-24,0)/16.
|
||||
// applied the way that renderer applies them — the pose's own when it changes them (sleeping in a
|
||||
// bed, swimming, fish in water, …). CEM px -> Java model units: F·T(0,-24,0)/16.
|
||||
CemModelLoader.PoseTransform pt = sel.transform();
|
||||
boolean captured = info != null && info.transform() != null;
|
||||
Affine t0 = captured ? Affine.fromColumnMajor(info.transform()) : DEFAULT_LIVING_TRANSFORM;
|
||||
double[] tr = pt != null && pt.transform() != null ? pt.transform() : captured ? info.transform() : null;
|
||||
t0 = tr != null ? Affine.fromColumnMajor(tr) : DEFAULT_LIVING_TRANSFORM;
|
||||
pre = t0.mul(Affine.scale(1 / 16.0)).mul(CemGeometry.CEM_TO_JAVA);
|
||||
int yawSign = captured ? info.yawSign() : -1;
|
||||
int yawSign = pt != null ? pt.yawSign() : captured ? info.yawSign() : -1;
|
||||
double[] pitchAxis = pt != null ? pt.pitchAxis() : info != null ? info.pitchAxis() : null;
|
||||
place = Affine.translation(s.x(), s.y(), s.z())
|
||||
.mul(Affine.scale(extra))
|
||||
.mul(Affine.rotY(yawSign * Math.toRadians(s.bodyYaw())));
|
||||
// Projectiles (verified against ArrowRenderer/ThrownTridentRenderer 26.3): T0 = rotY(-90)[·rotZ(90)],
|
||||
// pitchAxis = -X, so rotY(yaw)·rotAxis(-X, pitch)·T0 == vanilla rotY(yRot-90)·rotZ(xRot[+90]) with the
|
||||
// raw projectile rotation (yaw atan2(dx,dz), pitch positive up); origin = entity position, no offset.
|
||||
if (info != null && info.pitchAxis() != null && s.pitch() != 0) {
|
||||
double[] a = info.pitchAxis();
|
||||
// Mob poses: the pitch rotation follows the body yaw directly (swimming/gliding players, pouncing fox).
|
||||
if (pitchAxis != null && s.pitch() != 0) {
|
||||
double[] a = pitchAxis;
|
||||
place = place.mul(Affine.rotAxis(a[0], a[1], a[2], Math.toRadians(s.pitch())));
|
||||
}
|
||||
} else {
|
||||
@@ -136,7 +143,7 @@ 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 ? selectPose(s, info) : Map.of());
|
||||
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();
|
||||
@@ -152,6 +159,7 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
if (vanilla && s.equipment() != null && info != null && info.humanoidArmor()) {
|
||||
addArmorLayers(s, ch, pose, baked);
|
||||
}
|
||||
if (vanilla && !invisible) addShoulderParrots(s, ch, t0, 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);
|
||||
@@ -184,16 +192,33 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
static final SelectedPose NONE = new SelectedPose(null, Map.of(), null);
|
||||
}
|
||||
|
||||
// Toggle flags that switch parts on/off or seat the entity; kept when held-item flags are dropped.
|
||||
private static final java.util.Set<String> PART_TOGGLES = java.util.Set.of("passenger", "arms", "chest");
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* {@code setupAnim} / renderer read, dumped per type and per body model (baby/small models have their own
|
||||
* pivots): sitting as a passenger ({@code passenger}), holding items ({@code item_r}/{@code item_l}),
|
||||
* illagers' crossbow hold ({@code arms_*}), armor-stand arms ({@code arms}), chests ({@code chest}) and the
|
||||
* state poses of {@link EntityState#pose()} (sleeping, sitting, crouching, lying, swimming, in_water, …;
|
||||
* flags the type's dump doesn't know are ignored). Falls back to fewer flags when the exact combination has
|
||||
* no own variant (identical to a simpler pose): state-pose subsets from all to none, each first with all
|
||||
* other flags, then 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();
|
||||
private SelectedPose selectPose(EntityState s, CemModelLoader.EntityInfo info, Choice ch) {
|
||||
if (info == null) return SelectedPose.NONE;
|
||||
String sub = ch == null || ch.name.equals(ch.base) || !ch.name.startsWith(ch.base + "/")
|
||||
? null
|
||||
: ch.name.substring(ch.base.length() + 1);
|
||||
CemModelLoader.PoseSet set = info.poseSet(sub);
|
||||
if (set.poses().isEmpty()) return SelectedPose.NONE;
|
||||
List<String> flags = new ArrayList<>();
|
||||
if (isIllager(info)) {
|
||||
if (isCrossbow(s.rightHand()) || isCrossbow(s.leftHand())) flags.add("arms_crossbow_hold");
|
||||
@@ -204,19 +229,99 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
if (s.passenger()) flags.add("passenger");
|
||||
if (s.arms()) flags.add("arms");
|
||||
if (s.chest()) flags.add("chest");
|
||||
List<String> states = new ArrayList<>();
|
||||
if (s.pose() != null) {
|
||||
java.util.Set<String> known = knownPoseFlags(set);
|
||||
for (String f : s.pose().split("\\+")) if (known.contains(f) && !states.contains(f)) states.add(f);
|
||||
}
|
||||
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.equals("arms") || f.equals("chest"))
|
||||
.toList());
|
||||
for (List<String> st : subsetsBySize(states)) {
|
||||
List<String> all = new ArrayList<>(st);
|
||||
all.addAll(flags);
|
||||
tries.add(all);
|
||||
List<String> noItems = new ArrayList<>(st);
|
||||
flags.stream().filter(f -> !f.startsWith("item_")).forEach(noItems::add);
|
||||
tries.add(noItems);
|
||||
List<String> toggles = new ArrayList<>(st);
|
||||
flags.stream().filter(PART_TOGGLES::contains).forEach(toggles::add);
|
||||
tries.add(toggles);
|
||||
}
|
||||
for (List<String> t : tries) {
|
||||
if (t.isEmpty()) continue;
|
||||
Map<String, CemModelLoader.PartPose> p =
|
||||
info.poses().get(String.join("+", t.stream().sorted().toList()));
|
||||
if (p != null) return p;
|
||||
String name = String.join("+", t.stream().sorted().toList());
|
||||
Map<String, CemModelLoader.PartPose> p = set.poses().get(name);
|
||||
if (p != null) return new SelectedPose(name, p, set.transforms().get(name));
|
||||
}
|
||||
return Map.of();
|
||||
return SelectedPose.NONE;
|
||||
}
|
||||
|
||||
/** Every flag that occurs in a pose name of the set (cached per loaded pose map). */
|
||||
private final Map<Map<String, Map<String, CemModelLoader.PartPose>>, java.util.Set<String>> knownFlags =
|
||||
java.util.Collections.synchronizedMap(new java.util.IdentityHashMap<>());
|
||||
|
||||
private java.util.Set<String> knownPoseFlags(CemModelLoader.PoseSet set) {
|
||||
return knownFlags.computeIfAbsent(set.poses(), poses -> {
|
||||
java.util.Set<String> out = new java.util.HashSet<>();
|
||||
for (String n : poses.keySet()) out.addAll(List.of(n.split("\\+")));
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
/** All subsets of {@code l}, largest first (input order within a size); the empty set last. */
|
||||
private static List<List<String>> subsetsBySize(List<String> l) {
|
||||
List<List<String>> out = new ArrayList<>();
|
||||
int n = Math.min(l.size(), 6);
|
||||
for (int size = n; size >= 0; size--) {
|
||||
for (int mask = 0; mask < (1 << n); mask++) {
|
||||
if (Integer.bitCount(mask) != size) continue;
|
||||
List<String> sub = new ArrayList<>();
|
||||
for (int i = 0; i < n; i++) if ((mask & (1 << i)) != 0) sub.add(l.get(i));
|
||||
out.add(sub);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* The pose overrides plus the dump's rotation bindings: armor-stand part poses ({@code headPose}, …, from
|
||||
* {@link EntityState#partPoses()}) and body pitch ({@code xRot} = {@link EntityState#pitch()}: dolphins,
|
||||
* striders, nautili), each added as {@code coeff · (value − default)} to the bound parts' rotation.
|
||||
*/
|
||||
private static Map<String, CemModelLoader.PartPose> withBindings(
|
||||
Map<String, CemModelLoader.PartPose> parts, EntityState s, CemModelLoader.EntityInfo info) {
|
||||
if (info == null || info.rotationBindings().isEmpty()) return parts;
|
||||
Map<String, CemModelLoader.PartPose> out = null;
|
||||
for (Map.Entry<String, CemModelLoader.RotationBinding> b :
|
||||
info.rotationBindings().entrySet()) {
|
||||
double[] value;
|
||||
if (b.getKey().equals("xRot")) {
|
||||
value = new double[] {s.pitch()};
|
||||
} else {
|
||||
float[] v = s.partPoses() == null ? null : s.partPoses().get(b.getKey());
|
||||
if (v == null) continue;
|
||||
value = new double[] {v[0], v[1], v[2]};
|
||||
}
|
||||
double[] def = b.getValue().def();
|
||||
double[] delta = new double[3];
|
||||
boolean any = false;
|
||||
for (int j = 0; j < value.length && j < 3; j++) {
|
||||
delta[j] = value[j] - (def != null && j < def.length ? def[j] : 0);
|
||||
any |= Math.abs(delta[j]) > 1e-6;
|
||||
}
|
||||
if (!any) continue;
|
||||
if (out == null) out = new java.util.HashMap<>(parts);
|
||||
for (Map.Entry<String, double[]> pc : b.getValue().parts().entrySet()) {
|
||||
double[] c = pc.getValue();
|
||||
double[] add = new double[3];
|
||||
for (int i = 0; i < 3; i++)
|
||||
add[i] = c[i * 3] * delta[0] + c[i * 3 + 1] * delta[1] + c[i * 3 + 2] * delta[2];
|
||||
CemModelLoader.PartPose cur = out.get(pc.getKey());
|
||||
out.put(
|
||||
pc.getKey(),
|
||||
cur != null ? cur.plusRotation(add) : new CemModelLoader.PartPose(null, null, null, null, add));
|
||||
}
|
||||
}
|
||||
return out == null ? parts : out;
|
||||
}
|
||||
|
||||
private static boolean isCrossbow(ItemSpec i) {
|
||||
@@ -274,25 +379,68 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
|
||||
/**
|
||||
* Item carried in the mouth, rendered in the "ground" context like vanilla (HoldingEntityRenderState).
|
||||
* Fox (FoxHeldItemLayer, awake): head pivot · [baby: S(0.75)] · head rotation · T(0.06, 0.27 [baby 0.26],
|
||||
* -0.5) · rotX(90) — the head frame carries exactly that pivot + rotation. Dolphin (DolphinCarryingItemLayer)
|
||||
* at the model root · T(0, 1, -1) (its xRot-dependent shift is 0: PixelPics dolphins aren't pitched).
|
||||
* Fox (FoxHeldItemLayer): head PIVOT (the layer turns by the head-look angles, not the posed head part:
|
||||
* PixelPics has no head look) · [baby: S(0.75)] · T(0.06, 0.27 [baby 0.26], -0.5) · rotX(90); asleep
|
||||
* T(0.46, 0.26, 0.22) [baby (0.4, 0.26, 0.15)] · rotX(90) · rotZ(90). Dolphin (DolphinCarryingItemLayer) at
|
||||
* the model root · T(0, 1 − a·0.5, −1 + a·0.5) looking up / T(0, 1 + a·0.8, −1 + a·0.2) looking down,
|
||||
* a = |pitch| / 60.
|
||||
*/
|
||||
private void addMouthItem(EntityState s, Map<String, Affine> frames, List<EntityCube> out) {
|
||||
ItemSpec item = s.mouthItem();
|
||||
if (item == null) return;
|
||||
boolean baby = s.baby();
|
||||
Affine x = null;
|
||||
if (s.typeKey().equals("fox") && frames.containsKey("head")) {
|
||||
x = frames.get("head").mul(CemGeometry.JAVA_LOCAL);
|
||||
String headPivot = CemGeometry.PIVOT + "head";
|
||||
if (s.typeKey().equals("fox") && frames.containsKey(headPivot)) {
|
||||
boolean asleep = s.hasPose("sleeping");
|
||||
x = frames.get(headPivot).mul(CemGeometry.JAVA_LOCAL);
|
||||
if (baby) x = x.mul(Affine.scale(0.75));
|
||||
x = x.mul(Affine.translation(0.06, baby ? 0.26 : 0.27, -0.5)).mul(Affine.rotX(Math.PI / 2));
|
||||
if (asleep) {
|
||||
x = x.mul(baby ? Affine.translation(0.4, 0.26, 0.15) : Affine.translation(0.46, 0.26, 0.22))
|
||||
.mul(Affine.rotX(Math.PI / 2))
|
||||
.mul(Affine.rotZ(Math.PI / 2));
|
||||
} else {
|
||||
x = x.mul(Affine.translation(0.06, baby ? 0.26 : 0.27, -0.5)).mul(Affine.rotX(Math.PI / 2));
|
||||
}
|
||||
} else if (s.typeKey().equals("dolphin") && frames.containsKey("root")) {
|
||||
x = frames.get("root").mul(CemGeometry.JAVA_LOCAL).mul(Affine.translation(0, 1, -1));
|
||||
double a = Math.abs(s.pitch()) / 60.0;
|
||||
Affine off = s.pitch() < 0
|
||||
? Affine.translation(0, 1 - a * 0.5, -1 + a * 0.5)
|
||||
: Affine.translation(0, 1 + a * 0.8, -1 + a * 0.2);
|
||||
x = frames.get("root").mul(CemGeometry.JAVA_LOCAL).mul(off);
|
||||
}
|
||||
if (x != null) items.bake(item, "ground", false, x, out);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (t0 == null || (s.shoulderLeft() == null && s.shoulderRight() == null)) return;
|
||||
if (ch == null || !ch.base.startsWith("player")) return;
|
||||
CemModelLoader.CemModel pm = models.vanilla("parrot");
|
||||
CemModelLoader.EntityInfo pi = models.entityInfo("parrot");
|
||||
if (pm == null || pi == null) return;
|
||||
Map<String, CemModelLoader.PartPose> parts = pi.poses().getOrDefault("on_shoulder", Map.of());
|
||||
double y = s.hasPose("crouching") ? -1.3 : -1.5;
|
||||
for (int side = 0; side < 2; side++) {
|
||||
String variant = side == 0 ? s.shoulderLeft() : s.shoulderRight();
|
||||
if (variant == null) continue;
|
||||
int[][] tex = null;
|
||||
for (ResourceLocation rl : EntityModels.textureCandidates("parrot", variant)) {
|
||||
tex = texture(rl.path());
|
||||
if (tex != null) break;
|
||||
}
|
||||
if (tex == null && pi.texture() != null) tex = texture(pi.texture());
|
||||
if (tex == null) continue;
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isIllager(CemModelLoader.EntityInfo info) {
|
||||
return info != null && info.poses().keySet().stream().anyMatch(k -> k.startsWith("arms_"));
|
||||
}
|
||||
|
||||
@@ -81,6 +81,9 @@ final class CemGeometry {
|
||||
/** Key prefix in the frames map marking parts that were actually drawn (visible). */
|
||||
static final String DRAWN = "drawn:";
|
||||
|
||||
/** Key prefix in the frames map for a part's pivot frame {@code parent·T(P)} (without its own rotation). */
|
||||
static final String PIVOT = "pivot:";
|
||||
|
||||
private record Ctx(
|
||||
Set<String> hidden,
|
||||
int texW,
|
||||
@@ -107,31 +110,35 @@ final class CemGeometry {
|
||||
private static void bakeAbsolute(CemModelLoader.CemPart part, Affine parentWorld, Ctx ctx, boolean draw) {
|
||||
if (ctx.hidden.contains(part.name())) return;
|
||||
CemModelLoader.PartPose ov = ctx.overrides.get(part.name());
|
||||
boolean invisible = ov != null ? !ov.visible() : part.invisible();
|
||||
boolean invisible = ov != null && ov.visible() != null ? !ov.visible() : part.invisible();
|
||||
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
|
||||
.mul(Affine.translation(o[0], o[1], o[2]))
|
||||
double[] scale = ov != null && ov.scale() != null ? ov.scale() : part.scale();
|
||||
double[] add = ov == null ? null : ov.rotateAdd();
|
||||
boolean moved = ov != null && ov.pivot() != null; // else visibility/scale/extra-rotation only
|
||||
double[] o = moved ? ov.pivot() : part.pivot();
|
||||
double[] rot = moved ? ov.rotate() : part.rotate();
|
||||
if (add != null) rot = new double[] {rot[0] + add[0], rot[1] + add[1], rot[2] + add[2]};
|
||||
Affine pivotFrame = parentWorld.mul(Affine.translation(o[0], o[1], o[2]));
|
||||
Affine world = pivotFrame
|
||||
.mul(Affine.rotZ(Math.toRadians(rot[2])))
|
||||
.mul(Affine.rotY(Math.toRadians(rot[1])))
|
||||
.mul(Affine.rotX(Math.toRadians(rot[0])));
|
||||
if (part.scale() != null) {
|
||||
if (scale != null) {
|
||||
// A collapsed (zero) scale is vanilla's way to hide a part; it would also be non-invertible.
|
||||
for (double v : part.scale()) if (Math.abs(v) < 1e-6) return;
|
||||
world = world.mul(Affine.scale(part.scale()[0], part.scale()[1], part.scale()[2]));
|
||||
for (double v : scale) if (Math.abs(v) < 1e-6) return;
|
||||
world = world.mul(Affine.scale(scale[0], scale[1], scale[2]));
|
||||
}
|
||||
if (ctx.frames != null) {
|
||||
ctx.frames.putIfAbsent(part.name(), world);
|
||||
ctx.frames.putIfAbsent(PIVOT + part.name(), pivotFrame);
|
||||
if (draw) ctx.frames.putIfAbsent(DRAWN + part.name(), world);
|
||||
}
|
||||
world = world.mul(Affine.translation(-o[0], -o[1], -o[2]));
|
||||
Affine boxWorld = world;
|
||||
if (ov != null) {
|
||||
if (moved) {
|
||||
// Boxes are stored relative to the idle pivot: move them along with the overridden pivot
|
||||
// (children carry their own absolute pivots, so they use the un-shifted transform).
|
||||
double[] p0 = part.pivot();
|
||||
|
||||
@@ -66,19 +66,54 @@ public final class CemModelLoader {
|
||||
double[] pitchAxis, // world-frame axis the pitch rotates about; null = pitch ignored
|
||||
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)
|
||||
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
|
||||
|
||||
/** Whether the vanilla renderer draws this layer for an entity in its default state. */
|
||||
public boolean layerDrawnByDefault(String layerClass) {
|
||||
return layers.getOrDefault(layerClass, false);
|
||||
}
|
||||
|
||||
/** The poses of a body model: its own set when the dump has one, else the main model's. */
|
||||
public PoseSet poseSet(String subModel) {
|
||||
PoseSet own = subModel == null ? null : modelPoses.get(subModel);
|
||||
return own != null ? own : new PoseSet(poses, poseTransforms);
|
||||
}
|
||||
}
|
||||
|
||||
/** Pose overrides + renderer transforms of one body model (see {@link EntityInfo#poseSet}). */
|
||||
public record PoseSet(Map<String, Map<String, PartPose>> poses, Map<String, PoseTransform> transforms) {}
|
||||
|
||||
/**
|
||||
* The renderer transform of a pose (like {@link EntityInfo#transform}/{@code yawSign}/{@code pitchAxis}):
|
||||
* e.g. sleeping turns the body by the bed direction (yawSign +1), swimming pitches it.
|
||||
*/
|
||||
public record PoseTransform(double[] transform, int yawSign, double[] pitchAxis) {}
|
||||
|
||||
/**
|
||||
* A continuous state input bound linearly to part rotations: {@code part rotate += coeff · (value − def)},
|
||||
* {@code coeff} row-major 3×3 (row = CEM rotate axis, column = input component). Armor-stand part poses,
|
||||
* body pitch.
|
||||
*/
|
||||
public record RotationBinding(double[] def, Map<String, double[]> parts) {}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* toggle (the part keeps the pivot of whichever model it is in) — visibility (null = the part's own), scale
|
||||
* (null = the part's own) and an extra rotation (deg, added to whichever rotation applies; null = none).
|
||||
*/
|
||||
public record PartPose(double[] pivot, double[] rotate, boolean visible) {}
|
||||
public record PartPose(double[] pivot, double[] rotate, Boolean visible, double[] scale, double[] rotateAdd) {
|
||||
|
||||
/** This override plus an extra rotation (rotation bindings). */
|
||||
public PartPose plusRotation(double[] add) {
|
||||
double[] r = rotateAdd == null
|
||||
? add
|
||||
: new double[] {rotateAdd[0] + add[0], rotateAdd[1] + add[1], rotateAdd[2] + add[2]};
|
||||
return new PartPose(pivot, rotate, visible, scale, r);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Client assets of a data-driven mob variant ({@code data/<ns>/<type>_variant/<key>.json}): the texture
|
||||
@@ -190,9 +225,12 @@ public final class CemModelLoader {
|
||||
doubles(o, "transform"),
|
||||
o.has("yawSign") ? o.get("yawSign").getAsInt() : -1,
|
||||
doubles(o, "pitchAxis"),
|
||||
poses(o),
|
||||
poses(o.getAsJsonObject("poses")),
|
||||
layers(o),
|
||||
o.has("humanoidArmor") && o.get("humanoidArmor").getAsBoolean()));
|
||||
o.has("humanoidArmor") && o.get("humanoidArmor").getAsBoolean(),
|
||||
poseTransforms(o.getAsJsonObject("poseTransforms")),
|
||||
modelPoses(o.getAsJsonObject("modelPoses")),
|
||||
rotationBindings(o.getAsJsonObject("rotationBindings"))));
|
||||
}
|
||||
if (root.has("variants")) {
|
||||
for (Map.Entry<String, JsonElement> t :
|
||||
@@ -315,10 +353,10 @@ public final class CemModelLoader {
|
||||
return o.has("mirrorTexture") && o.get("mirrorTexture").getAsString().contains("u");
|
||||
}
|
||||
|
||||
private static Map<String, Map<String, PartPose>> poses(JsonObject o) {
|
||||
private static Map<String, Map<String, PartPose>> poses(JsonObject poses) {
|
||||
Map<String, Map<String, PartPose>> out = new HashMap<>();
|
||||
if (!o.has("poses")) return out;
|
||||
for (Map.Entry<String, JsonElement> v : o.getAsJsonObject("poses").entrySet()) {
|
||||
if (poses == null) return out;
|
||||
for (Map.Entry<String, JsonElement> v : poses.entrySet()) {
|
||||
Map<String, PartPose> parts = new HashMap<>();
|
||||
for (Map.Entry<String, JsonElement> pe :
|
||||
v.getValue().getAsJsonObject().entrySet()) {
|
||||
@@ -328,13 +366,60 @@ public final class CemModelLoader {
|
||||
new PartPose(
|
||||
po.has("pivot") ? arr3(po, "pivot") : null,
|
||||
po.has("rotate") ? arr3(po, "rotate") : null,
|
||||
!po.has("visible") || po.get("visible").getAsBoolean()));
|
||||
!po.has("visible") || po.get("visible").getAsBoolean(),
|
||||
po.has("scale") ? arr3(po, "scale") : null,
|
||||
null));
|
||||
}
|
||||
out.put(v.getKey(), parts);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static Map<String, PoseTransform> poseTransforms(JsonObject o) {
|
||||
Map<String, PoseTransform> out = new HashMap<>();
|
||||
if (o == null) return out;
|
||||
for (Map.Entry<String, JsonElement> e : o.entrySet()) {
|
||||
JsonObject t = e.getValue().getAsJsonObject();
|
||||
out.put(
|
||||
e.getKey(),
|
||||
new PoseTransform(
|
||||
doubles(t, "transform"),
|
||||
t.has("yawSign") ? t.get("yawSign").getAsInt() : -1,
|
||||
doubles(t, "pitchAxis")));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static Map<String, PoseSet> modelPoses(JsonObject o) {
|
||||
Map<String, PoseSet> out = new HashMap<>();
|
||||
if (o == null) return out;
|
||||
for (Map.Entry<String, JsonElement> e : o.entrySet()) {
|
||||
JsonObject m = e.getValue().getAsJsonObject();
|
||||
out.put(
|
||||
e.getKey(),
|
||||
new PoseSet(
|
||||
poses(m.getAsJsonObject("poses")), poseTransforms(m.getAsJsonObject("poseTransforms"))));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static Map<String, RotationBinding> rotationBindings(JsonObject o) {
|
||||
Map<String, RotationBinding> out = new HashMap<>();
|
||||
if (o == null) return out;
|
||||
for (Map.Entry<String, JsonElement> e : o.entrySet()) {
|
||||
JsonObject b = e.getValue().getAsJsonObject();
|
||||
Map<String, double[]> parts = new HashMap<>();
|
||||
for (Map.Entry<String, JsonElement> p : b.getAsJsonObject("parts").entrySet()) {
|
||||
JsonArray a = p.getValue().getAsJsonArray();
|
||||
double[] c = new double[9];
|
||||
for (int i = 0; i < 9 && i < a.size(); i++) c[i] = a.get(i).getAsDouble();
|
||||
parts.put(p.getKey(), c);
|
||||
}
|
||||
out.put(e.getKey(), new RotationBinding(doubles(b, "default"), parts));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static Map<String, Boolean> layers(JsonObject o) {
|
||||
Map<String, Boolean> out = new HashMap<>();
|
||||
if (o.has("layers")) {
|
||||
|
||||
+156
-3
@@ -26,6 +26,7 @@ import org.bukkit.entity.AbstractHorse;
|
||||
import org.bukkit.entity.Ageable;
|
||||
import org.bukkit.entity.ArmorStand;
|
||||
import org.bukkit.entity.Axolotl;
|
||||
import org.bukkit.entity.Bat;
|
||||
import org.bukkit.entity.Cat;
|
||||
import org.bukkit.entity.ChestedHorse;
|
||||
import org.bukkit.entity.Creeper;
|
||||
@@ -44,7 +45,9 @@ import org.bukkit.entity.PufferFish;
|
||||
import org.bukkit.entity.Rabbit;
|
||||
import org.bukkit.entity.Sheep;
|
||||
import org.bukkit.entity.Shulker;
|
||||
import org.bukkit.entity.Sittable;
|
||||
import org.bukkit.entity.Slime;
|
||||
import org.bukkit.entity.Sniffer;
|
||||
import org.bukkit.entity.Snowman;
|
||||
import org.bukkit.entity.TraderLlama;
|
||||
import org.bukkit.entity.Trident;
|
||||
@@ -275,11 +278,35 @@ public final class EntitySnapshotBuilder {
|
||||
// (vanilla dump), so the wearer list follows the game version.
|
||||
EntityState.Equipment equipment = e instanceof LivingEntity wearer ? captureEquipment(wearer) : null;
|
||||
|
||||
String pose = poseFlags(e);
|
||||
double x = loc.getX(), z = loc.getZ();
|
||||
if (pose != null && pose.contains("sleeping") && e instanceof LivingEntity sleeper && !(e instanceof Fox)) {
|
||||
// LivingEntityRenderer: a sleeper in a bed lies along the bed (yaw from the bed direction instead of
|
||||
// the body yaw) and is shifted by its standing eye height - 0.1 towards the foot end.
|
||||
org.bukkit.block.BlockFace bed = bedFacing(e);
|
||||
if (bed != null) {
|
||||
bodyYaw = switch (bed) {
|
||||
case SOUTH -> 90;
|
||||
case NORTH -> 270;
|
||||
case EAST -> 180;
|
||||
default -> 0;};
|
||||
double off = standingEyeHeight(sleeper) - 0.1;
|
||||
x -= bed.getModX() * off;
|
||||
z -= bed.getModZ() * off;
|
||||
}
|
||||
}
|
||||
java.util.Map<String, float[]> partPoses = e instanceof ArmorStand as ? armorStandPoses(as) : null;
|
||||
String shoulderLeft = null, shoulderRight = null;
|
||||
if (e instanceof HumanEntity he) {
|
||||
shoulderLeft = shoulderParrot(he, true);
|
||||
shoulderRight = shoulderParrot(he, false);
|
||||
}
|
||||
|
||||
return new EntityState(
|
||||
type,
|
||||
loc.getX(),
|
||||
x,
|
||||
loc.getY(),
|
||||
loc.getZ(),
|
||||
z,
|
||||
bodyYaw,
|
||||
baby,
|
||||
width,
|
||||
@@ -308,7 +335,133 @@ public final class EntitySnapshotBuilder {
|
||||
headItem,
|
||||
collar,
|
||||
powered,
|
||||
mouthItem);
|
||||
mouthItem,
|
||||
pose,
|
||||
partPoses,
|
||||
shoulderLeft,
|
||||
shoulderRight);
|
||||
}
|
||||
|
||||
/**
|
||||
* The entity's state-pose flags (names of the vanilla dump's STATE_POSES, sorted, "+"-joined; null = idle):
|
||||
* the generic {@link Entity#getPose()} (sleeping, crouching, swimming/crawling, fall_flying, sitting,
|
||||
* digging) plus the per-mob flags vanilla's renderers read from the entity (sitting pets/pandas/camels,
|
||||
* fox sleeping/crouching/leaping/faceplanted, cat lying/head up/on a sleeping player, panda on its back,
|
||||
* resting bat, standing/dancing parrot) and {@code in_water}. The baker ignores flags a type doesn't know.
|
||||
*/
|
||||
private static String poseFlags(Entity e) {
|
||||
java.util.TreeSet<String> f = new java.util.TreeSet<>();
|
||||
try {
|
||||
switch (e.getPose()) {
|
||||
case SLEEPING -> f.add("sleeping");
|
||||
case SNEAKING -> f.add("crouching");
|
||||
case SWIMMING -> f.add(e.isInWater() ? "swimming" : "crawling");
|
||||
case FALL_FLYING -> f.add("fall_flying");
|
||||
case SITTING -> f.add("sitting");
|
||||
case DIGGING -> f.add("digging");
|
||||
default -> {}
|
||||
}
|
||||
if (e instanceof LivingEntity le && le.isGliding()) f.add("fall_flying");
|
||||
if (e.isInWater()) f.add("in_water");
|
||||
if (e instanceof Sittable st && st.isSitting()) f.add("sitting");
|
||||
if (e instanceof Fox fox) {
|
||||
if (fox.isSleeping()) f.add("sleeping");
|
||||
if (fox.isCrouching()) f.add("crouching");
|
||||
if (fox.isLeaping()) f.add("pouncing");
|
||||
if (fox.isFaceplanted()) f.add("faceplanted");
|
||||
} else if (e instanceof Cat cat) {
|
||||
if (cat.isLyingDown()) {
|
||||
f.add("lying");
|
||||
if (sleepingPlayerNear(cat)) f.add("on_player");
|
||||
}
|
||||
if (cat.isHeadUp()) f.add("head_up");
|
||||
} else if (e instanceof Panda pa) {
|
||||
if (pa.isOnBack()) f.add("on_back");
|
||||
} else if (e instanceof Bat bat) {
|
||||
if (!bat.isAwake()) f.add("resting");
|
||||
} else if (e instanceof Parrot pa) {
|
||||
// ParrotModel.getPose: party > sitting > flying (= not on the ground) > standing.
|
||||
if (pa.isDancing()) f.add("party");
|
||||
else if (!pa.isSitting() && pa.isOnGround()) f.add("standing");
|
||||
} else if (e instanceof Sniffer sn && sn.getState() == Sniffer.State.DIGGING) {
|
||||
f.add("digging");
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
// API missing on this server version: render the flags found so far
|
||||
}
|
||||
return f.isEmpty() ? null : String.join("+", f);
|
||||
}
|
||||
|
||||
/** Cat.isLyingOnTopOfSleepingPlayer: a sleeping player within the cat's block box inflated by 2. */
|
||||
private static boolean sleepingPlayerNear(Entity cat) {
|
||||
Location l = cat.getLocation();
|
||||
org.bukkit.util.BoundingBox box = new org.bukkit.util.BoundingBox(
|
||||
l.getBlockX() - 2,
|
||||
l.getBlockY() - 2,
|
||||
l.getBlockZ() - 2,
|
||||
l.getBlockX() + 3,
|
||||
l.getBlockY() + 3,
|
||||
l.getBlockZ() + 3);
|
||||
for (Entity p : cat.getWorld().getNearbyEntities(box, en -> en instanceof Player)) {
|
||||
if (((Player) p).isSleeping()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Eye height in the STANDING pose (vanilla state.eyeHeight of a sleeper): {@code getEyeHeight(true)} returns the
|
||||
* sleeping pose's 0.2 on Paper 26.x, so ask the server entity ({@code getEyeHeight(Pose.STANDING)}).
|
||||
*/
|
||||
private static double standingEyeHeight(LivingEntity le) {
|
||||
try {
|
||||
Object handle = le.getClass().getMethod("getHandle").invoke(le);
|
||||
for (java.lang.reflect.Method m : handle.getClass().getMethods()) {
|
||||
if (!m.getName().equals("getEyeHeight") || m.getParameterCount() != 1) continue;
|
||||
Class<?> pose = m.getParameterTypes()[0];
|
||||
if (!pose.isEnum()) continue;
|
||||
for (Object c : pose.getEnumConstants()) {
|
||||
if (((Enum<?>) c).name().equals("STANDING")) return ((Number) m.invoke(handle, c)).doubleValue();
|
||||
}
|
||||
}
|
||||
} catch (ReflectiveOperationException | RuntimeException ignored) {
|
||||
// fall through
|
||||
}
|
||||
return le.getEyeHeight(true);
|
||||
}
|
||||
|
||||
/** Facing of the bed the entity sleeps in (LivingEntity.getBedOrientation = the bed block's FACING), or null. */
|
||||
private static org.bukkit.block.BlockFace bedFacing(Entity e) {
|
||||
org.bukkit.block.data.BlockData bd = e.getLocation().getBlock().getBlockData();
|
||||
return bd instanceof org.bukkit.block.data.type.Bed bed ? bed.getFacing() : null;
|
||||
}
|
||||
|
||||
/** Armor-stand part poses in degrees, keyed by the vanilla render-state field (dump rotation bindings). */
|
||||
private static java.util.Map<String, float[]> armorStandPoses(ArmorStand as) {
|
||||
java.util.Map<String, float[]> out = new java.util.HashMap<>();
|
||||
out.put("headPose", deg(as.getHeadPose()));
|
||||
out.put("bodyPose", deg(as.getBodyPose()));
|
||||
out.put("leftArmPose", deg(as.getLeftArmPose()));
|
||||
out.put("rightArmPose", deg(as.getRightArmPose()));
|
||||
out.put("leftLegPose", deg(as.getLeftLegPose()));
|
||||
out.put("rightLegPose", deg(as.getRightLegPose()));
|
||||
return out;
|
||||
}
|
||||
|
||||
private static float[] deg(org.bukkit.util.EulerAngle a) {
|
||||
return new float[] {
|
||||
(float) Math.toDegrees(a.getX()), (float) Math.toDegrees(a.getY()), (float) Math.toDegrees(a.getZ())
|
||||
};
|
||||
}
|
||||
|
||||
/** Variant key of a parrot sitting on the player's left/right shoulder, or null. */
|
||||
@SuppressWarnings("deprecation")
|
||||
private static String shoulderParrot(HumanEntity he, boolean left) {
|
||||
try {
|
||||
Entity p = left ? he.getShoulderEntityLeft() : he.getShoulderEntityRight();
|
||||
return p instanceof Parrot parrot ? keyOf(parrot.getVariant()) : null;
|
||||
} catch (Throwable t) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Worn armor (4 slots) from a humanoid wearer; null when nothing is equipped. */
|
||||
|
||||
@@ -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_arm","pivot":[1.75,3.75,0.0],"rotate":[0.0,0.0,25.0],"boxes":[{"coordinates":[1.5,0.25,-1.0,1.0,4.0,2.0],"textureOffset":[23.0,0.0],"sizeAdd":-0.01}]},{"part":"left_arm","pivot":[-1.75,3.75,0.0],"rotate":[0.0,0.0,-25.0],"boxes":[{"coordinates":[-2.5,0.25,-1.0,1.0,4.0,2.0],"textureOffset":[23.0,6.0],"sizeAdd":-0.01}]},{"part":"right_wing","pivot":[0.5,4.25,0.6],"rotate":[-25.0,18.0,0.0],"boxes":[{"coordinates":[0.5,-1.75,0.6,0.0,Line 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_arm","pivot":[1.75,3.75,0.0],"rotate":[0.0,0.0,25.0],"boxes":[{"coordinates":[1.5,0.25,-1.0,1.0,4.0,2.0],"textureOffset":[23.0,0.0],"sizeAdd":-0.01}]},{"part":"left_arm","pivot":[-1.75,3.75,0.0],"rotate":[0.0,0.0,-25.0],"boxes":[{"coordinates":[-2.5,0.25,-1.0,1.0,4.0,2.0],"textureOffset":[23.0,6.0],"sizeAdd":-0.01}]},{"part":"right_wing","pivot":[0.5,4.25,0.6],"rotate":[-25.0,18.0,0.0],"boxes":[{"coordinates":[0.5,-1.75,0.6,0.0,Line truncated
|
||||
@@ -166,7 +166,7 @@ public class EntityBenchmark {
|
||||
false, null, false, variant(type), tint, 1.0,
|
||||
type.equals("villager") ? "farmer" : null, type.equals("villager") ? 2 : 0,
|
||||
null, false, false, false, null, null,
|
||||
false, 0, 0, 0, false, null, null, null, 0, false, null);
|
||||
false, 0, 0, 0, false, null, null, null, 0, false, null, null, null, null, null);
|
||||
}
|
||||
|
||||
/** Vanilla DyeColor texture colours (white … black). */
|
||||
@@ -177,7 +177,7 @@ public class EntityBenchmark {
|
||||
return new EntityState("villager", x, 1, z, yaw, baby, 0.6, 1.95,
|
||||
false, null, false, type, 0, 1.0, prof, level,
|
||||
null, false, false, false, null, null,
|
||||
false, 0, 0, 0, false, null, null, null, 0, false, null);
|
||||
false, 0, 0, 0, false, null, null, null, 0, false, null, null, null, null, null);
|
||||
}
|
||||
|
||||
/** {@code s} with wool tint, humanoid armor and collar dye replaced. */
|
||||
@@ -186,7 +186,7 @@ public class EntityBenchmark {
|
||||
s.player(), s.skinUrl(), s.slim(), s.variant(), tint, s.sizeScale(), s.profession(), s.villagerLevel(),
|
||||
s.markings(), s.saddle(), s.chest(), s.arms(), s.bodyEquip(), eq, s.invisible(), s.pitch(),
|
||||
s.bodyDye(), s.tint2(), s.passenger(), s.rightHand(), s.leftHand(), s.headItem(), collar, s.powered(),
|
||||
s.mouthItem());
|
||||
s.mouthItem(), s.pose(), s.partPoses(), s.shoulderLeft(), s.shoulderRight());
|
||||
}
|
||||
|
||||
/** The variant the game always supplies (cow/pig/chicken have no texture without one). */
|
||||
@@ -204,7 +204,7 @@ public class EntityBenchmark {
|
||||
s.player(), s.skinUrl(), s.slim(), s.variant(), s.tint(), s.sizeScale(), s.profession(), s.villagerLevel(),
|
||||
s.markings(), s.saddle(), s.chest(), s.arms(), s.bodyEquip(), s.equipment(), s.invisible(), s.pitch(),
|
||||
s.bodyDye(), s.tint2(), s.passenger(), s.rightHand(), s.leftHand(), s.headItem(), s.collar(), true,
|
||||
s.mouthItem());
|
||||
s.mouthItem(), s.pose(), s.partPoses(), s.shoulderLeft(), s.shoulderRight());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -269,10 +269,33 @@ public class EntityTestRender {
|
||||
String equip = plain ? EQUIP.get(key) : null;
|
||||
boolean arms = flags.contains("arms");
|
||||
int collar = flags.contains("tame") ? 0xFFB02E26 : 0;
|
||||
// Pose flags: "p:<state pose>" (sleeping, sitting, lying, …), "pitch:<deg>", "passenger", "shoulder"
|
||||
// (red + blue parrot on the shoulders), "stand" (custom armor-stand part poses), "mouth" (sweet berries).
|
||||
List<String> poses = new ArrayList<>();
|
||||
float pitch = 0;
|
||||
for (String f : flags) {
|
||||
if (f.startsWith("p:")) poses.add(f.substring(2));
|
||||
if (f.startsWith("pitch:")) pitch = Float.parseFloat(f.substring(6));
|
||||
}
|
||||
Collections.sort(poses);
|
||||
Map<String, float[]> partPoses = null;
|
||||
if (flags.contains("stand")) {
|
||||
partPoses = new HashMap<>();
|
||||
partPoses.put("headPose", new float[] {-20, 30, 0});
|
||||
partPoses.put("bodyPose", new float[] {10, 20, 0});
|
||||
partPoses.put("rightArmPose", new float[] {-90, 0, 0});
|
||||
partPoses.put("leftArmPose", new float[] {-30, 0, -40});
|
||||
partPoses.put("leftLegPose", new float[] {30, 0, -10});
|
||||
partPoses.put("rightLegPose", new float[] {-40, 0, 0});
|
||||
}
|
||||
boolean shoulder = flags.contains("shoulder");
|
||||
ItemSpec mouth = flags.contains("mouth") ? new ItemSpec("sweet_berries", null, null, false) : null;
|
||||
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);
|
||||
plain && !baby && INVISIBLE.contains(key), pitch, 0, tint2, flags.contains("passenger"), null, null, null,
|
||||
collar, false, mouth, poses.isEmpty() ? null : String.join("+", poses), partPoses,
|
||||
shoulder ? "red_blue" : null, shoulder ? "blue" : null);
|
||||
}
|
||||
|
||||
static BufferedImage labelCell(String key, BufferedImage v1, BufferedImage v2) {
|
||||
|
||||
+413
-51
@@ -105,8 +105,8 @@ public class VanillaModelDump {
|
||||
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);
|
||||
addPoseVariants(info, found, key, r);
|
||||
addRotationBindings(info, found, r);
|
||||
entities.put(key, info);
|
||||
emit(key, found, textures, state, out, failed);
|
||||
}
|
||||
@@ -127,8 +127,8 @@ public class VanillaModelDump {
|
||||
JsonObject info = new JsonObject();
|
||||
addLayerInfo(info, layers, r);
|
||||
addTransform(info, r);
|
||||
Model<?> main = found.get("model");
|
||||
if (main != null) addPoseVariants(info, main, r);
|
||||
addPoseVariants(info, found, key, r);
|
||||
addRotationBindings(info, found, r);
|
||||
entities.put(key, info);
|
||||
emit(key, found, textures, r.createRenderState(), out, failed);
|
||||
}
|
||||
@@ -446,15 +446,74 @@ public class VanillaModelDump {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Named STATE poses (the render-state fields vanilla's extractRenderState fills from the entity's pose/flags):
|
||||
* {@code {pose, field, value[, "unless:<field>"]}}. A pose applies to a renderer when at least one of its
|
||||
* assignments finds its field; the others are skipped (one name covers fox/cat/wolf/panda/parrot/camel
|
||||
* sitting, …). Values: boolean, float, enum constant name, or {@code anim:<ticks>} = start that
|
||||
* AnimationState that many ticks before the frame (static keyframe pose). The snapshot maps the entity's
|
||||
* Bukkit state onto these names; the dump only keeps poses that change parts or the renderer transform.
|
||||
*/
|
||||
static final String[][] STATE_POSES = {
|
||||
{"sleeping", "isSleeping", "true"},
|
||||
{"sleeping", "pose", "SLEEPING", "unless:isSleeping"}, // LivingEntityRenderer: lying in bed
|
||||
{"sitting", "isSitting", "true"},
|
||||
{"sitting", "sitAmount", "1"}, // panda
|
||||
{"sitting", "pose", "SITTING"}, // parrot (ParrotModel.Pose), camel
|
||||
{"sitting", "sitPoseAnimationState", "anim:0"}, // camel
|
||||
{"crouching", "isCrouching", "true"},
|
||||
{"crouching", "crouchAmount", "3"}, // fox (Fox.getCrouchAmount max)
|
||||
{"crouching", "pose", "CROUCHING"},
|
||||
{"crouching", "elytraRotX", "0.6981317"}, // ElytraAnimationState crouching targets
|
||||
{"crouching", "elytraRotY", "0.08726646"},
|
||||
{"crouching", "elytraRotZ", "-0.7853982"},
|
||||
{"pouncing", "isPouncing", "true"}, // fox leap
|
||||
{"faceplanted", "isFaceplanted", "true"}, // fox stuck in snow
|
||||
{"lying", "lieDownAmount", "1"}, // cat
|
||||
{"lying", "lieDownAmountTail", "1"},
|
||||
{"head_up", "relaxStateOneAmount", "1"}, // cat
|
||||
{"on_player", "isLyingOnTopOfSleepingPlayer", "true"}, // cat on its sleeping owner
|
||||
{"on_back", "lieOnBackAmount", "1"}, // panda
|
||||
{"resting", "isResting", "true"}, // bat hanging
|
||||
{"resting", "restAnimationState", "anim:0"},
|
||||
{"swimming", "isVisuallySwimming", "true"},
|
||||
{"swimming", "swimAmount", "1"},
|
||||
{"swimming", "isInWater", "true"},
|
||||
{"swimming", "pose", "SWIMMING"},
|
||||
{"crawling", "isVisuallySwimming", "true"}, // swimming pose out of water
|
||||
{"crawling", "swimAmount", "1"},
|
||||
{"crawling", "pose", "SWIMMING"},
|
||||
{"fall_flying", "isFallFlying", "true"},
|
||||
{"fall_flying", "fallFlyingTimeInTicks", "20"},
|
||||
{"fall_flying", "pose", "FALL_FLYING"},
|
||||
{"fall_flying", "elytraRotX", "0.34906584"}, // level glide
|
||||
{"fall_flying", "elytraRotZ", "-1.5707964"},
|
||||
{"in_water", "isInWater", "true"}, // fish lie on their side only out of water
|
||||
{"standing", "pose", "STANDING"}, // parrot on the ground (the render-state default is FLYING)
|
||||
{"on_shoulder", "pose", "ON_SHOULDER"}, // parrot on a player's shoulder (ParrotOnShoulderLayer)
|
||||
{"party", "pose", "PARTY"}, // dancing parrot
|
||||
{"digging", "pose", "DIGGING"},
|
||||
{"digging", "diggingAnimationState", "anim:72"}, // sniffer: head and body down in the ground
|
||||
};
|
||||
|
||||
/** State poses that combine (every subset of a group with 2+ applicable members is dumped too). */
|
||||
static final List<List<String>> POSE_GROUPS =
|
||||
List.of(List.of("crouching", "pouncing"), List.of("head_up", "lying", "on_player"));
|
||||
|
||||
/**
|
||||
* State-dependent poses as per-part overrides (only parts that differ from the idle pose): sitting as a
|
||||
* passenger ({@code isPassenger}/{@code isRiding}), holding an item in the right/left hand
|
||||
* ({@code right/leftArmPose = ITEM}) and their combinations, plus illager arm poses. Stored per entity
|
||||
* type as {@code poses: {variant: {part: {pivot, rotate, visible}}}}; the baker applies them by part
|
||||
* name to the body AND its layer models (armor follows the arms, like vanilla's copyPropertiesTo).
|
||||
* ({@code right/leftArmPose = ITEM}) and their combinations, illager arm poses, and the {@link #STATE_POSES}
|
||||
* (alone, in {@link #POSE_GROUPS} combinations and with held items). Stored per entity type as
|
||||
* {@code poses: {variant: {part: {pivot, rotate, visible[, scale]}}}} for the main body model and
|
||||
* {@code modelPoses: {<sub-model>: {poses, poseTransforms}}} for the other body models (baby, small, …: own
|
||||
* pivots); the baker applies them by part name to the body AND its layer models (armor follows the arms,
|
||||
* like vanilla's copyPropertiesTo). State poses also carry the parts of layer models the body lacks (elytra
|
||||
* wings) and, when the renderer transform changes (sleeping, swimming, fish in water, …),
|
||||
* {@code poseTransforms: {variant: {transform, yawSign[, pitchAxis]}}} measured like {@link #addTransform}.
|
||||
*/
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
static void addPoseVariants(JsonObject info, Model<?> model, EntityRenderer<?, ?> r) {
|
||||
static void addPoseVariants(
|
||||
JsonObject info, Map<String, Model<?>> found, String key, EntityRenderer<?, ?> r) {
|
||||
Object probe;
|
||||
try {
|
||||
probe = r.createRenderState();
|
||||
@@ -467,8 +526,12 @@ 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") && 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"}));
|
||||
List<List<String[]>> items = new ArrayList<>();
|
||||
if (hasField(probe, "rightArmPose") && probeEnum(probe, "rightArmPose"))
|
||||
items.add(java.util.Collections.singletonList(new String[] {"item_r", "rightArmPose", "ITEM"}));
|
||||
if (hasField(probe, "leftArmPose") && probeEnum(probe, "leftArmPose"))
|
||||
items.add(java.util.Collections.singletonList(new String[] {"item_l", "leftArmPose", "ITEM"}));
|
||||
toggles.addAll(items);
|
||||
// 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]))
|
||||
@@ -493,40 +556,294 @@ public class VanillaModelDump {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (combos.isEmpty()) return;
|
||||
Map<String, float[]> base = poseOf(model, freshState(r, List.of()));
|
||||
if (base == null) return;
|
||||
JsonObject poses = new JsonObject();
|
||||
for (List<String[]> c : combos) {
|
||||
Map<String, float[]> v = poseOf(model, freshState(r, c));
|
||||
if (v == null) continue;
|
||||
JsonObject diff = new JsonObject();
|
||||
for (Map.Entry<String, float[]> e : v.entrySet()) {
|
||||
float[] b = base.get(e.getKey());
|
||||
float[] x = e.getValue();
|
||||
boolean same = b != null;
|
||||
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();
|
||||
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);
|
||||
// State poses: alone, grouped, each with every held-item subset.
|
||||
List<List<String[]>> stateCombos = new ArrayList<>();
|
||||
java.util.Set<String> applicable = new java.util.LinkedHashSet<>();
|
||||
for (String[] a : STATE_POSES) if (assignmentApplies(probe, a)) applicable.add(a[0]);
|
||||
List<List<String>> stateNames = new ArrayList<>();
|
||||
for (String n : applicable) stateNames.add(List.of(n));
|
||||
for (List<String> g : POSE_GROUPS) {
|
||||
List<String> members = g.stream().filter(applicable::contains).toList();
|
||||
for (int mask = 1; mask < (1 << members.size()); mask++) {
|
||||
if (Integer.bitCount(mask) < 2) continue;
|
||||
List<String> sub = new ArrayList<>();
|
||||
for (int i = 0; i < members.size(); i++) if ((mask & (1 << i)) != 0) sub.add(members.get(i));
|
||||
stateNames.add(sub);
|
||||
}
|
||||
if (diff.isEmpty()) continue;
|
||||
StringBuilder name = new StringBuilder();
|
||||
c.stream().map(t -> t[0]).distinct().sorted().forEach(n -> name.append(name.isEmpty() ? "" : "+").append(n));
|
||||
poses.add(name.toString(), diff);
|
||||
}
|
||||
if (!poses.isEmpty()) info.add("poses", poses);
|
||||
// leave the model in its idle pose for the dump
|
||||
poseOf(model, freshState(r, List.of()));
|
||||
for (List<String> names : stateNames) {
|
||||
List<String[]> base = new ArrayList<>();
|
||||
for (String n : names) for (String[] a : STATE_POSES) if (a[0].equals(n)) base.add(a);
|
||||
for (int mask = 0; mask < (1 << items.size()); mask++) {
|
||||
List<String[]> c = new ArrayList<>(base);
|
||||
for (int i = 0; i < items.size(); i++) if ((mask & (1 << i)) != 0) c.addAll(items.get(i));
|
||||
stateCombos.add(c);
|
||||
}
|
||||
}
|
||||
if (combos.isEmpty() && stateCombos.isEmpty()) return;
|
||||
|
||||
// Every body model (main, baby, small, variant models) gets its own overrides: pivots differ per model.
|
||||
float[][] t0 = transformTriple(r, List.of(), false);
|
||||
for (Map.Entry<String, Model<?>> bm : found.entrySet()) {
|
||||
String f = bm.getKey();
|
||||
if (f.contains("Layer.") || f.startsWith("equip.")) continue;
|
||||
boolean main = f.equals("model") || f.equals("adultModel") || f.equals("normalModel");
|
||||
if (main && bm.getValue() != (found.containsKey("model") ? found.get("model") : found.get("adultModel")))
|
||||
continue;
|
||||
boolean baby = f.toLowerCase().contains("baby");
|
||||
List<Model<?>> layerModels = new ArrayList<>();
|
||||
for (Map.Entry<String, Model<?>> lm : found.entrySet()) {
|
||||
String ln = lm.getKey();
|
||||
if (!ln.contains("Layer.") || ln.toLowerCase().contains("baby") != baby) continue;
|
||||
layerModels.add(lm.getValue());
|
||||
}
|
||||
JsonObject poses = new JsonObject();
|
||||
JsonObject transforms = new JsonObject();
|
||||
Model<?> model = bm.getValue();
|
||||
// Main model: the dumped idle state (unchanged poses). Baby models: a baby's state (isBaby, ageScale
|
||||
// 0.5 — e.g. the wolf's sitting offsets scale with it).
|
||||
boolean babyState = baby && !main;
|
||||
Map<String, float[]> base = poseOf(model, freshState(r, List.of(), babyState));
|
||||
if (base == null) continue;
|
||||
for (List<String[]> c : combos) {
|
||||
if (!c.stream().allMatch(a -> assignmentApplies(probe, a))) continue; // e.g. piglin arm poses
|
||||
addPose(poses, c, model, base, poseOf(model, freshState(r, c, babyState)), false);
|
||||
}
|
||||
Map<String, float[]> baseAll = withLayers(base, layerModels, freshState(r, List.of(), babyState));
|
||||
java.util.Set<String> emitted = new java.util.HashSet<>();
|
||||
for (List<String[]> c : stateCombos) {
|
||||
// Held-item variants only for state poses that exist on their own (no item_r+standing duplicates).
|
||||
String states = c.stream().filter(a -> !a[0].startsWith("item_")).map(a -> a[0]).distinct().sorted()
|
||||
.reduce((x, y) -> x + "+" + y).orElse("");
|
||||
boolean withItems = c.stream().anyMatch(a -> a[0].startsWith("item_"));
|
||||
if (withItems && !emitted.contains(states)) continue;
|
||||
Map<String, float[]> v = withLayers(
|
||||
poseOf(model, freshState(r, c, babyState)), layerModels, freshState(r, c, babyState));
|
||||
float[][] tr = transformTriple(r, c, baby);
|
||||
boolean newTransform = tr != null && t0 != null && !sameTriple(tr, t0);
|
||||
String name = addPose(poses, c, model, baseAll, v, newTransform);
|
||||
if (name != null) emitted.add(name);
|
||||
if (name != null && newTransform) transforms.add(name, tripleJson(tr));
|
||||
}
|
||||
poseOf(model, freshState(r, List.of())); // leave the model in its idle pose for the dump
|
||||
if (main) {
|
||||
if (!poses.isEmpty()) info.add("poses", poses);
|
||||
if (!transforms.isEmpty()) info.add("poseTransforms", transforms);
|
||||
} else {
|
||||
// Always present (also when empty): the baker then never falls back to the main model's pivots.
|
||||
String sub = f.equals("babyModel") ? "baby" : f;
|
||||
if (!info.has("modelPoses")) info.add("modelPoses", new JsonObject());
|
||||
JsonObject mp = new JsonObject();
|
||||
mp.add("poses", poses);
|
||||
if (!transforms.isEmpty()) mp.add("poseTransforms", transforms);
|
||||
info.getAsJsonObject("modelPoses").add(sub, mp);
|
||||
}
|
||||
}
|
||||
for (Model<?> lm : found.values()) poseOf(lm, freshState(r, List.of()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the pose {@code v} of combo {@code c} as a diff against {@code base}; returns its name, or null when
|
||||
* nothing differs (and {@code keepEmpty} is false — state poses that only move the renderer transform).
|
||||
*/
|
||||
static String addPose(
|
||||
JsonObject poses,
|
||||
List<String[]> c,
|
||||
Model<?> model,
|
||||
Map<String, float[]> base,
|
||||
Map<String, float[]> v,
|
||||
boolean keepEmpty) {
|
||||
if (v == null) return null;
|
||||
JsonObject diff = new JsonObject();
|
||||
for (Map.Entry<String, float[]> e : v.entrySet()) {
|
||||
float[] b = base.get(e.getKey());
|
||||
float[] x = e.getValue();
|
||||
boolean same = b != null;
|
||||
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();
|
||||
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);
|
||||
boolean scaled = b == null ? (x[7] != 1 || x[8] != 1 || x[9] != 1) : false;
|
||||
for (int i = 7; b != null && i < 10; i++) scaled |= Math.abs(b[i] - x[i]) >= 1e-4f;
|
||||
if (scaled) po.add("scale", arr(x[7], x[8], x[9]));
|
||||
diff.add(e.getKey(), po);
|
||||
}
|
||||
if (diff.isEmpty() && !keepEmpty) return null;
|
||||
StringBuilder name = new StringBuilder();
|
||||
c.stream().map(t -> t[0]).distinct().sorted().forEach(n -> name.append(name.isEmpty() ? "" : "+").append(n));
|
||||
poses.add(name.toString(), diff);
|
||||
return name.toString();
|
||||
}
|
||||
|
||||
/** {@code parts} plus the posed parts of layer models that the body model lacks (elytra wings, …). */
|
||||
static Map<String, float[]> withLayers(Map<String, float[]> parts, List<Model<?>> layers, Object state) {
|
||||
if (parts == null) return null;
|
||||
Map<String, float[]> out = new LinkedHashMap<>(parts);
|
||||
for (Model<?> lm : layers) {
|
||||
Map<String, float[]> lp = poseOf(lm, state);
|
||||
if (lp == null) continue;
|
||||
lp.remove("root");
|
||||
lp.forEach(out::putIfAbsent);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Renderer transform, yaw sign and pitch axis for a posed state (see {@link #addTransform}); null = none. */
|
||||
static float[][] transformTriple(EntityRenderer<?, ?> r, List<String[]> assigns, boolean baby) {
|
||||
float[] t = captureTransform(r, 0, 0, assigns, baby);
|
||||
if (t == null) return null;
|
||||
org.joml.Matrix3f inv0 = new org.joml.Matrix4f().set(t).get3x3(new org.joml.Matrix3f()).invert();
|
||||
float[] yaw = {0};
|
||||
float[] ty = captureTransform(r, 90, 0, assigns, baby);
|
||||
if (ty != null) {
|
||||
org.joml.Matrix3f rel = new org.joml.Matrix4f().set(ty).get3x3(new org.joml.Matrix3f()).mul(inv0);
|
||||
float sinA = -rel.m02;
|
||||
yaw[0] = Math.abs(sinA) < 0.5f ? 0 : (sinA > 0 ? 1 : -1);
|
||||
}
|
||||
float[] axis = null;
|
||||
float[] tp = captureTransform(r, 0, 90, assigns, baby);
|
||||
if (tp != null) {
|
||||
org.joml.Matrix3f rel = new org.joml.Matrix4f().set(tp).get3x3(new org.joml.Matrix3f()).mul(inv0);
|
||||
float ax = (rel.m12 - rel.m21) / 2, ay = (rel.m20 - rel.m02) / 2, az = (rel.m01 - rel.m10) / 2;
|
||||
float len = (float) Math.sqrt(ax * ax + ay * ay + az * az);
|
||||
if (len > 0.5f) axis = new float[] {ax / len, ay / len, az / len};
|
||||
}
|
||||
return new float[][] {t, yaw, axis};
|
||||
}
|
||||
|
||||
static boolean sameTriple(float[][] a, float[][] b) {
|
||||
for (int i = 0; i < 16; i++) if (Math.abs(a[0][i] - b[0][i]) > 1e-4f) return false;
|
||||
if (a[1][0] != b[1][0]) return false;
|
||||
if ((a[2] == null) != (b[2] == null)) return false;
|
||||
if (a[2] != null) for (int i = 0; i < 3; i++) if (Math.abs(a[2][i] - b[2][i]) > 1e-3f) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static JsonObject tripleJson(float[][] tr) {
|
||||
JsonObject o = new JsonObject();
|
||||
JsonArray ta = new JsonArray();
|
||||
for (float v : tr[0]) ta.add(Math.round(v * 100000f) / 100000f);
|
||||
o.add("transform", ta);
|
||||
o.addProperty("yawSign", (int) tr[1][0]);
|
||||
if (tr[2] != null) o.add("pitchAxis", arr(tr[2][0], tr[2][1], tr[2][2]));
|
||||
return o;
|
||||
}
|
||||
|
||||
/**
|
||||
* Continuous state inputs bound linearly to part rotations: every {@code Rotations} field (armor-stand
|
||||
* head/body/arm/leg poses) and {@code xRot} (entity pitch: dolphin body, heads, …), measured by
|
||||
* re-running setupAnim with each component +10°. Stored as {@code rotationBindings: {field: {default:
|
||||
* [x,y,z], parts: {part: [9 coefficients, row = CEM rotate axis, column = field component]}}}}; the baker
|
||||
* adds {@code coeff · (value − default)} to each named part's rotation (body and layer models alike).
|
||||
*/
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
static void addRotationBindings(JsonObject info, Map<String, Model<?>> found, EntityRenderer<?, ?> r) {
|
||||
Object probe;
|
||||
try {
|
||||
probe = r.createRenderState();
|
||||
} catch (Throwable t) {
|
||||
return;
|
||||
}
|
||||
Model<?> main = found.containsKey("model") ? found.get("model") : found.get("adultModel");
|
||||
if (main == null) return;
|
||||
List<Model<?>> layers = new ArrayList<>();
|
||||
for (Map.Entry<String, Model<?>> lm : found.entrySet())
|
||||
if (lm.getKey().contains("Layer.") && !lm.getKey().toLowerCase().contains("baby")) layers.add(lm.getValue());
|
||||
JsonObject out = new JsonObject();
|
||||
for (Field f : probe.getClass().getFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
boolean rot = f.getType().getSimpleName().equals("Rotations");
|
||||
if (!rot && !(f.getName().equals("xRot") && f.getType() == float.class)) continue;
|
||||
try {
|
||||
Object st0 = freshState(r, List.of());
|
||||
Map<String, float[]> base = withLayers(poseOf(main, st0), layers, freshState(r, List.of()));
|
||||
if (base == null) continue;
|
||||
float[] def = new float[3];
|
||||
if (rot) {
|
||||
Object v = f.get(st0);
|
||||
for (int j = 0; j < 3; j++) def[j] = (float) v.getClass().getMethod(new String[] {"x", "y", "z"}[j]).invoke(v);
|
||||
} else {
|
||||
def[0] = f.getFloat(st0);
|
||||
}
|
||||
Map<String, float[]> coeff = new TreeMap<>();
|
||||
for (int j = 0; j < (rot ? 3 : 1); j++) {
|
||||
Object st = freshState(r, List.of());
|
||||
if (rot) {
|
||||
float[] d = def.clone();
|
||||
d[j] += 10;
|
||||
f.set(st, f.getType().getConstructor(float.class, float.class, float.class).newInstance(d[0], d[1], d[2]));
|
||||
} else {
|
||||
f.setFloat(st, def[0] + 10);
|
||||
}
|
||||
Map<String, float[]> v = withLayers(poseOf(main, st), layers, st);
|
||||
if (v == null) continue;
|
||||
for (Map.Entry<String, float[]> e : v.entrySet()) {
|
||||
float[] b = base.get(e.getKey());
|
||||
if (b == null) continue;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
float c = (e.getValue()[3 + i] - b[3 + i]) / 10f;
|
||||
if (Math.abs(c) < 1e-4f) continue;
|
||||
coeff.computeIfAbsent(e.getKey(), k -> new float[9])[i * 3 + j] = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Pitch tilts whole bodies only (dolphin, strider, nautilus): head look is left out, PixelPics has
|
||||
// no head yaw to go with it.
|
||||
if (!rot) {
|
||||
java.util.Set<String> heads = new java.util.HashSet<>();
|
||||
headParts(main.root(), "root", false, heads);
|
||||
for (Model<?> lm : layers) headParts(lm.root(), "root", false, heads);
|
||||
coeff.keySet().removeAll(heads);
|
||||
}
|
||||
if (coeff.isEmpty()) continue;
|
||||
JsonObject fb = new JsonObject();
|
||||
fb.add("default", rot ? arr(def) : arr(def[0]));
|
||||
JsonObject parts = new JsonObject();
|
||||
coeff.forEach((p, c) -> parts.add(p, arr(c)));
|
||||
fb.add("parts", parts);
|
||||
out.add(f.getName(), fb);
|
||||
} catch (Throwable t) {
|
||||
System.out.println("rotation binding " + f.getName() + " (" + r.getClass().getSimpleName() + "): " + t);
|
||||
}
|
||||
}
|
||||
poseOf(main, freshState(r, List.of()));
|
||||
for (Model<?> lm : layers) poseOf(lm, freshState(r, List.of()));
|
||||
if (!out.isEmpty()) info.add("rotationBindings", out);
|
||||
}
|
||||
|
||||
/** Names of every part named "*head*" and of everything below one. */
|
||||
@SuppressWarnings("unchecked")
|
||||
static void headParts(ModelPart p, String name, boolean inHead, java.util.Set<String> out) {
|
||||
inHead |= name.contains("head");
|
||||
if (inHead) out.add(name);
|
||||
Map<String, ModelPart> children = (Map<String, ModelPart>) fieldValue(p, "children");
|
||||
if (children == null) return;
|
||||
for (Map.Entry<String, ModelPart> c : children.entrySet()) headParts(c.getValue(), c.getKey(), inHead, out);
|
||||
}
|
||||
|
||||
/** Whether a {@link #STATE_POSES} assignment finds its field (and an enum value its constant). */
|
||||
static boolean assignmentApplies(Object st, String[] a) {
|
||||
if (a.length > 3 && a[3].startsWith("unless:") && hasField(st, a[3].substring(7))) return false;
|
||||
Field f;
|
||||
try {
|
||||
f = st.getClass().getField(a[1]);
|
||||
} catch (NoSuchFieldException e) {
|
||||
return false;
|
||||
}
|
||||
if (a[2].startsWith("anim:")) return f.getType().getSimpleName().equals("AnimationState");
|
||||
if (f.getType().isEnum()) {
|
||||
for (Object c : f.getType().getEnumConstants()) if (((Enum<?>) c).name().equals(a[2])) return true;
|
||||
return false;
|
||||
}
|
||||
return f.getType() == boolean.class || f.getType() == float.class || f.getType() == int.class;
|
||||
}
|
||||
|
||||
/** Whether the state field is an enum (HumanoidModel.ArmPose), not e.g. an armor stand's Rotations. */
|
||||
@@ -547,22 +864,41 @@ public class VanillaModelDump {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
static Object freshState(EntityRenderer<?, ?> r, List<String[]> sets) {
|
||||
return freshState(r, sets, false);
|
||||
}
|
||||
|
||||
/** {@code baby}: also isBaby + ageScale 0.5 like a baby's extracted state (baby models' pose offsets). */
|
||||
static Object freshState(EntityRenderer<?, ?> r, List<String[]> sets, boolean baby) {
|
||||
try {
|
||||
Object st = r.createRenderState();
|
||||
idleTweaks(st);
|
||||
for (String[] t : sets) {
|
||||
Field f = st.getClass().getField(t[1]);
|
||||
if (f.getType() == boolean.class) f.setBoolean(st, Boolean.parseBoolean(t[2]));
|
||||
else if (f.getType().isEnum()) f.set(st, Enum.valueOf((Class) f.getType(), t[2]));
|
||||
}
|
||||
for (String[] t : sets) assign(st, t);
|
||||
if (baby) babyState(st);
|
||||
return st;
|
||||
} catch (Throwable t) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static void babyState(Object st) throws ReflectiveOperationException {
|
||||
if (hasField(st, "isBaby")) st.getClass().getField("isBaby").setBoolean(st, true);
|
||||
if (hasField(st, "ageScale")) st.getClass().getField("ageScale").setFloat(st, 0.5f);
|
||||
}
|
||||
|
||||
/** Applies one {@code {name, field, value[, cond]}} assignment when it applies (see {@link #STATE_POSES}). */
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
static void assign(Object st, String[] t) throws ReflectiveOperationException {
|
||||
if (!assignmentApplies(st, t)) return;
|
||||
Field f = st.getClass().getField(t[1]);
|
||||
if (t[2].startsWith("anim:")) {
|
||||
f.getType().getMethod("start", int.class).invoke(f.get(st), -Integer.parseInt(t[2].substring(5)));
|
||||
} else if (f.getType() == boolean.class) f.setBoolean(st, Boolean.parseBoolean(t[2]));
|
||||
else if (f.getType() == float.class) f.setFloat(st, Float.parseFloat(t[2]));
|
||||
else if (f.getType() == int.class) f.setInt(st, Integer.parseInt(t[2]));
|
||||
else if (f.getType().isEnum()) f.set(st, Enum.valueOf((Class) f.getType(), t[2]));
|
||||
}
|
||||
|
||||
/** setupAnim with the state, then every part's absolute CEM pivot, CEM rotation and visibility, by name. */
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
static Map<String, float[]> poseOf(Model<?> model, Object state) {
|
||||
@@ -583,7 +919,7 @@ public class VanillaModelDump {
|
||||
out.put(name, new float[] {
|
||||
-ax, 24 - ay, az,
|
||||
(float) Math.toDegrees(-p.xRot), (float) Math.toDegrees(-p.yRot), (float) Math.toDegrees(p.zRot),
|
||||
p.visible ? 1 : 0
|
||||
p.visible ? 1 : 0, p.xScale, p.yScale, p.zScale
|
||||
});
|
||||
Map<String, ModelPart> children = (Map<String, ModelPart>) fieldValue(p, "children");
|
||||
if (children != null) children.forEach((n, c) -> collectPose(c, n, ax, ay, az, out));
|
||||
@@ -598,13 +934,27 @@ public class VanillaModelDump {
|
||||
}
|
||||
|
||||
static float[] captureTransform(EntityRenderer<?, ?> r, float yaw, float pitch) {
|
||||
return captureTransform(r, yaw, pitch, null, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code assigns != null}: a posed state ({@link #STATE_POSES}, {@code isBaby} when {@code baby}); the
|
||||
* renderer's {@code getRenderOffset} (crouching players sink 2 px) is then included like the dispatcher does.
|
||||
*/
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
static float[] captureTransform(
|
||||
EntityRenderer<?, ?> r, float yaw, float pitch, List<String[]> assigns, boolean baby) {
|
||||
Object state;
|
||||
try {
|
||||
state = r.createRenderState();
|
||||
idleTweaks(state);
|
||||
if (assigns != null) {
|
||||
for (String[] a : assigns) assign(state, a);
|
||||
if (baby) babyState(state);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
return null;
|
||||
}
|
||||
idleTweaks(state);
|
||||
setFloat(state, "bodyRot", yaw);
|
||||
setFloat(state, "yRot", yaw);
|
||||
setFloat(state, "xRot", pitch);
|
||||
@@ -641,6 +991,18 @@ public class VanillaModelDump {
|
||||
} catch (Throwable ignored) {
|
||||
// partial submissions still count (layers may fail on the null context)
|
||||
}
|
||||
if (assigns != null && first[0] != null) {
|
||||
try {
|
||||
net.minecraft.world.phys.Vec3 off = ((EntityRenderer) r).getRenderOffset((net.minecraft.client.renderer.entity.state.EntityRenderState) state);
|
||||
if (off != null && off.lengthSqr() > 0) {
|
||||
first[0] = new org.joml.Matrix4f().set(first[0])
|
||||
.translateLocal((float) off.x, (float) off.y, (float) off.z)
|
||||
.get(new float[16]);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
// no render offset for this state
|
||||
}
|
||||
}
|
||||
return first[0];
|
||||
}
|
||||
|
||||
|
||||
@@ -154,6 +154,20 @@ public class PPTest extends JavaPlugin {
|
||||
// leather armor, wolf collars and patterned banners, all from a seeded RNG (deterministic).
|
||||
scenes.put("perf_village", new Scene("perf_village", this::perfVillage, -6, 9, -13, 4, 0, 0));
|
||||
scenes.put("perf_village_close", new Scene("perf_village_close", this::perfVillage, 0.5, 3, -11.5, 5, 0.5, -4));
|
||||
// Entity poses (vanilla setupAnim/renderer state from the dump): sitting/sleeping/crouching pets with mouth
|
||||
// items, cats lying, parrots, pandas; camel/sniffer/bat/dolphin pitch/fish in water/sleeping villager/
|
||||
// mannequin poses/baby rider; armor stands with custom part poses. All rfSpawn (deterministic).
|
||||
scenes.put("pose_pets", new Scene("pose_pets", this::posePets, 4.3, 3.6, -6, 4.3, 0.2, 2));
|
||||
scenes.put("pose_pets_side", new Scene("pose_pets_side", this::posePets, -3, 3, -3, 4, 0, 2));
|
||||
scenes.put("pose_fox_close", new Scene("pose_fox_close", this::posePets, 2.2, 1.4, -2.2, 2.2, 0.2, 0));
|
||||
scenes.put("pose_misc", new Scene("pose_misc", this::poseMisc, 6.5, 5, -10, 6.5, 0.5, 0));
|
||||
scenes.put("pose_misc_side", new Scene("pose_misc_side", this::poseMisc, -4, 3, -7, 6, 0.5, -1));
|
||||
scenes.put("pose_humans", new Scene("pose_humans", this::poseHumans, 6, 3.5, -7, 6, 0.6, 1));
|
||||
scenes.put("pose_humans_side", new Scene("pose_humans_side", this::poseHumans, -3, 3, -4, 5, 0.5, 1));
|
||||
scenes.put("pose_humans_close", new Scene("pose_humans_close", this::poseHumans, 4.8, 1.6, -5.2, 4.8, 0.5, -2.5));
|
||||
scenes.put("pose_beds", new Scene("pose_beds", this::poseHumans, 1.3, 2.6, -3.2, 1.3, 0.4, 0.2));
|
||||
scenes.put("pose_stands", new Scene("pose_stands", this::poseStands, 4, 2.4, -5, 4, 0.9, 0));
|
||||
scenes.put("pose_stands_side", new Scene("pose_stands_side", this::poseStands, -3, 2.4, -3, 4, 0.9, 0));
|
||||
}
|
||||
|
||||
private void perfFarm(Location o) {
|
||||
@@ -234,6 +248,199 @@ public class PPTest extends JavaPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
// --- poses ------------------------------------------------------------------------------------
|
||||
|
||||
private void posePets(Location o) {
|
||||
// Row 1 (z=0): foxes — standing, sitting, sleeping, crouching, crouching+leaping, faceplanted.
|
||||
Fox f0 = rfSpawn(o, 0, 0, 0, 180, Fox.class);
|
||||
hands(f0, Material.SWEET_BERRIES, null);
|
||||
Fox f1 = rfSpawn(o, 1.2, 0, 0, 180, Fox.class);
|
||||
f1.setSitting(true);
|
||||
hands(f1, Material.SWEET_BERRIES, null);
|
||||
Fox f2 = rfSpawn(o, 2.4, 0, 0, 180, Fox.class);
|
||||
f2.setSleeping(true);
|
||||
hands(f2, Material.SWEET_BERRIES, null);
|
||||
Fox f3 = rfSpawn(o, 3.6, 0, 0, 180, Fox.class);
|
||||
f3.setCrouching(true);
|
||||
hands(f3, Material.FEATHER, null);
|
||||
Fox f4 = rfSpawn(o, 4.8, 0, 0, 180, Fox.class);
|
||||
f4.setCrouching(true);
|
||||
f4.setLeaping(true);
|
||||
Fox f5 = rfSpawn(o, 6.0, 0, 0, 180, Fox.class);
|
||||
f5.setFaceplanted(true);
|
||||
f5.setFoxType(Fox.Type.SNOW);
|
||||
Fox fb = rfSpawn(o, 7.2, 0, 0, 180, Fox.class);
|
||||
fb.setBaby();
|
||||
fb.setSleeping(true);
|
||||
hands(fb, Material.SWEET_BERRIES, null);
|
||||
Fox fb2 = rfSpawn(o, 8.2, 0, 0, 180, Fox.class);
|
||||
fb2.setBaby();
|
||||
fb2.setSitting(true);
|
||||
// Row 2 (z=1.8): cats & wolves — sitting, lying, lying + head up, baby sitting; wolf/pup sitting.
|
||||
Cat c0 = rfSpawn(o, 0, 0, 1.8, 180, Cat.class);
|
||||
c0.setSitting(true);
|
||||
Cat c1 = rfSpawn(o, 1.2, 0, 1.8, 180, Cat.class);
|
||||
c1.setLyingDown(true);
|
||||
c1.setCatType(Cat.Type.BLACK);
|
||||
Cat c2 = rfSpawn(o, 2.6, 0, 1.8, 180, Cat.class);
|
||||
c2.setLyingDown(true);
|
||||
c2.setHeadUp(true);
|
||||
c2.setCatType(Cat.Type.SIAMESE);
|
||||
Cat c3 = rfSpawn(o, 4.0, 0, 1.8, 180, Cat.class);
|
||||
c3.setBaby();
|
||||
c3.setSitting(true);
|
||||
tame(c3, DyeColor.RED);
|
||||
Wolf w0 = rfSpawn(o, 5.2, 0, 1.8, 180, Wolf.class);
|
||||
w0.setSitting(true);
|
||||
Wolf w1 = rfSpawn(o, 6.6, 0, 1.8, 180, Wolf.class);
|
||||
tame(w1, DyeColor.BLUE);
|
||||
w1.setSitting(true);
|
||||
Wolf w2 = rfSpawn(o, 8.0, 0, 1.8, 180, Wolf.class);
|
||||
w2.setBaby();
|
||||
w2.setSitting(true);
|
||||
// Row 3 (z=3.6): parrots (flying default, sitting, standing on the ground), pandas.
|
||||
Parrot p0 = rfSpawn(o, 0, 0, 3.6, 180, Parrot.class);
|
||||
p0.setVariant(Parrot.Variant.BLUE);
|
||||
Parrot p1 = rfSpawn(o, 0.8, 0, 3.6, 180, Parrot.class);
|
||||
p1.setSitting(true);
|
||||
p1.setVariant(Parrot.Variant.GREEN);
|
||||
Parrot p2 = rfSpawn(o, 1.6, 0, 3.6, 180, Parrot.class);
|
||||
p2.setVariant(Parrot.Variant.CYAN);
|
||||
if (p2 instanceof Mob m) {
|
||||
m.setAI(true);
|
||||
m.setAware(false); // no goals, but physics: lands -> on ground -> standing pose
|
||||
}
|
||||
Panda pa0 = rfSpawn(o, 3.2, 0, 3.6, 180, Panda.class);
|
||||
pa0.setSitting(true);
|
||||
Panda pa1 = rfSpawn(o, 5.2, 0, 3.6, 180, Panda.class);
|
||||
pa1.setOnBack(true);
|
||||
Panda pa2 = rfSpawn(o, 7.2, 0, 3.6, 180, Panda.class);
|
||||
pa2.setBaby();
|
||||
pa2.setSitting(true);
|
||||
Panda pa3 = rfSpawn(o, 8.6, 0, 3.6, 180, Panda.class);
|
||||
pa3.setBaby();
|
||||
pa3.setOnBack(true);
|
||||
faceNorth(o);
|
||||
}
|
||||
|
||||
private void poseMisc(Location o) {
|
||||
Camel ca = rfSpawn(o, 0, 0, 0, 180, Camel.class);
|
||||
ca.setSitting(true);
|
||||
rfSpawn(o, 2.6, 0, 0, 180, Camel.class);
|
||||
Sniffer sn = rfSpawn(o, 5.4, 0, 0, 180, Sniffer.class);
|
||||
sn.setState(Sniffer.State.DIGGING);
|
||||
rfSpawn(o, 8.6, 0, 0, 180, Sniffer.class);
|
||||
// Bats: hanging under a block (resting) and flying.
|
||||
put(o, 11, 2, 0, "stone");
|
||||
Bat b0 = rfSpawn(o, 11.5, 1.1, 0.5, 180, Bat.class);
|
||||
b0.setAwake(false);
|
||||
Bat b1 = rfSpawn(o, 12.8, 1.0, 0.5, 180, Bat.class);
|
||||
b1.setAwake(true);
|
||||
// Dolphins pitched down/up/level, a strider looking up, fish in water vs. flopping on land.
|
||||
Dolphin d0 = rfSpawn(o, 0, 0.6, -3.5, 180, Dolphin.class);
|
||||
d0.setRotation(180, 35);
|
||||
Dolphin d1 = rfSpawn(o, 2, 0.6, -3.5, 180, Dolphin.class);
|
||||
d1.setRotation(180, -35);
|
||||
hands(d1, Material.COD, null);
|
||||
Dolphin d2 = rfSpawn(o, 4, 0.6, -3.5, 180, Dolphin.class);
|
||||
hands(d2, Material.COD, null);
|
||||
Strider st = rfSpawn(o, 6, 0, -3.5, 180, Strider.class);
|
||||
st.setRotation(180, -30);
|
||||
for (int i = 0; i < 3; i++) put(o, 8 + i, 0, -4, "water");
|
||||
rfSpawn(o, 8.5, 0.3, -3.5, 180, Cod.class);
|
||||
rfSpawn(o, 9.5, 0.3, -3.5, 180, Salmon.class);
|
||||
TropicalFish tf = rfSpawn(o, 10.5, 0.3, -3.5, 180, TropicalFish.class);
|
||||
tf.setPattern(TropicalFish.Pattern.BETTY);
|
||||
rfSpawn(o, 8.5, 0, -2, 180, Cod.class);
|
||||
rfSpawn(o, 9.5, 0, -2, 180, Salmon.class);
|
||||
TropicalFish tf2 = rfSpawn(o, 10.5, 0, -2, 180, TropicalFish.class);
|
||||
tf2.setPattern(TropicalFish.Pattern.BETTY);
|
||||
faceNorth(o);
|
||||
d0.setRotation(180, 35);
|
||||
d1.setRotation(180, -35);
|
||||
st.setRotation(180, -30);
|
||||
}
|
||||
|
||||
private void poseHumans(Location o) {
|
||||
// Villager asleep in a bed (head at z=0 facing south -> lies along z), a baby villager in a bed facing east.
|
||||
put(o, 0, 0, 0, "red_bed[facing=south,part=head]");
|
||||
put(o, 0, 0, -1, "red_bed[facing=south,part=foot]");
|
||||
Villager v = rfSpawn(o, 0.5, 0, 1.5, 180, Villager.class);
|
||||
v.setProfession(Villager.Profession.LIBRARIAN);
|
||||
v.sleep(o.clone().add(0, 0, 0));
|
||||
put(o, 2, 0, 0, "blue_bed[facing=east,part=head]");
|
||||
put(o, 1, 0, 0, "blue_bed[facing=east,part=foot]");
|
||||
Villager vb = rfSpawn(o, 2.5, 0, 1.5, 180, Villager.class);
|
||||
vb.setBaby();
|
||||
vb.sleep(o.clone().add(2, 0, 0));
|
||||
// Mannequins: standing, crouching with a sword, crawling, swimming in water, gliding with elytra, sleeping.
|
||||
Mannequin m0 = rfSpawn(o, 4, 0, 0, 180, Mannequin.class);
|
||||
hands(m0, Material.IRON_SWORD, null);
|
||||
Mannequin m1 = rfSpawn(o, 5.2, 0, 0, 180, Mannequin.class);
|
||||
m1.setPose(Pose.SNEAKING, true);
|
||||
hands(m1, Material.IRON_SWORD, null);
|
||||
Mannequin m2 = rfSpawn(o, 6.6, 0, 0, 180, Mannequin.class);
|
||||
m2.setPose(Pose.SWIMMING, true);
|
||||
put(o, 8, 0, 0, "water");
|
||||
put(o, 8, 0, 1, "water");
|
||||
Mannequin m3 = rfSpawn(o, 8.5, 0.1, 0.5, 180, Mannequin.class);
|
||||
m3.setPose(Pose.SWIMMING, true);
|
||||
Mannequin m4 = rfSpawn(o, 10, 1.0, 0, 180, Mannequin.class);
|
||||
m4.getEquipment().setChestplate(it(Material.ELYTRA));
|
||||
m4.setPose(Pose.FALL_FLYING, true);
|
||||
Mannequin m5 = rfSpawn(o, 12, 0, 0, 180, Mannequin.class);
|
||||
m5.setPose(Pose.SLEEPING, true);
|
||||
// Baby zombie riding a chicken (jockey: baby model pivots in the passenger pose), crouching zombie.
|
||||
Chicken ch = rfSpawn(o, 4, 0, -2.5, 180, Chicken.class);
|
||||
Zombie bz = rfSpawn(o, 4, 0, -2.5, 180, Zombie.class);
|
||||
bz.setBaby();
|
||||
ch.addPassenger(bz);
|
||||
Zombie zc = rfSpawn(o, 5.5, 0, -2.5, 180, Zombie.class);
|
||||
zc.setPose(Pose.SNEAKING, true);
|
||||
faceNorth(o);
|
||||
}
|
||||
|
||||
private void poseStands(Location o) {
|
||||
// Custom armor-stand poses (head/body/arms/legs, radians), with arms, armor and held items.
|
||||
ArmorStand a = rfSpawn(o, 0, 0, 0, 180, ArmorStand.class);
|
||||
a.setArms(true);
|
||||
a.setHeadPose(new org.bukkit.util.EulerAngle(Math.toRadians(-20), Math.toRadians(30), 0));
|
||||
a.setRightArmPose(new org.bukkit.util.EulerAngle(Math.toRadians(-90), 0, 0));
|
||||
a.setLeftArmPose(new org.bukkit.util.EulerAngle(Math.toRadians(-30), 0, Math.toRadians(-40)));
|
||||
Ah(a);
|
||||
ArmorStand b = rfSpawn(o, 1.6, 0, 0, 180, ArmorStand.class);
|
||||
b.setArms(true);
|
||||
b.setBodyPose(new org.bukkit.util.EulerAngle(Math.toRadians(15), Math.toRadians(20), 0));
|
||||
b.setRightLegPose(new org.bukkit.util.EulerAngle(Math.toRadians(-40), 0, 0));
|
||||
b.setLeftLegPose(new org.bukkit.util.EulerAngle(Math.toRadians(30), 0, Math.toRadians(-10)));
|
||||
diamondArmor(b);
|
||||
hands(b, Material.IRON_SWORD, Material.SHIELD);
|
||||
ArmorStand c = rfSpawn(o, 3.2, 0, 0, 180, ArmorStand.class);
|
||||
c.setArms(true);
|
||||
c.setSmall(true);
|
||||
c.setRightArmPose(new org.bukkit.util.EulerAngle(Math.toRadians(-120), Math.toRadians(20), 0));
|
||||
c.setHeadPose(new org.bukkit.util.EulerAngle(0, 0, Math.toRadians(25)));
|
||||
diamondArmor(c);
|
||||
hands(c, Material.TORCH, null);
|
||||
ArmorStand d = rfSpawn(o, 4.8, 0, 0, 180, ArmorStand.class);
|
||||
d.setHeadPose(new org.bukkit.util.EulerAngle(Math.toRadians(30), 0, 0));
|
||||
d.getEquipment().setHelmet(it(Material.CARVED_PUMPKIN));
|
||||
ArmorStand e = rfSpawn(o, 6.4, 0, 0, 180, ArmorStand.class);
|
||||
e.setArms(true);
|
||||
e.setInvisible(true);
|
||||
e.setRightArmPose(new org.bukkit.util.EulerAngle(Math.toRadians(-60), Math.toRadians(-30), 0));
|
||||
e.setBodyPose(new org.bukkit.util.EulerAngle(0, Math.toRadians(30), 0));
|
||||
diamondArmor(e);
|
||||
hands(e, Material.DIAMOND_SWORD, null);
|
||||
ArmorStand f = rfSpawn(o, 8, 0, 0, 180, ArmorStand.class); // default pose (must match rf_stands)
|
||||
f.setArms(true);
|
||||
}
|
||||
|
||||
private static void Ah(ArmorStand a) {
|
||||
a.getEquipment().setHelmet(it(Material.GOLDEN_HELMET));
|
||||
hands(a, Material.BOW, Material.APPLE);
|
||||
}
|
||||
|
||||
private void perfItems(Location o) {
|
||||
Random r = new Random(7);
|
||||
Material[] mats = {Material.WHEAT, Material.CARROT, Material.EGG, Material.BONE, Material.ROTTEN_FLESH,
|
||||
@@ -283,7 +490,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.endsWith("_sq") ? new Resolution(384, 384) : sc.name.startsWith("proj_") || sc.name.startsWith("rf_") || sc.name.startsWith("frames") ? new Resolution(960, 640) : new Resolution(480, 320), null);
|
||||
RenderJob job = r.prepare(eye, sc.name.endsWith("_sq") ? new Resolution(384, 384) : sc.name.startsWith("proj_") || sc.name.startsWith("rf_") || sc.name.startsWith("pose_") || sc.name.startsWith("frames") ? new Resolution(960, 640) : new Resolution(480, 320), null);
|
||||
if (sc.name.equals("dbg")) {
|
||||
try {
|
||||
java.lang.reflect.Field f = DefaultScreenRenderer.class.getDeclaredField("objectBaker");
|
||||
|
||||
Reference in new issue
Block a user