primitive chunk table in world snapshot instead of colliding long-keyed hashmap, bounded cache for tinted/composited mob and banner textures, vanilla dump with nested model json, village benchmark and perf test scenes
This commit is contained in:
1 parent
b5baffebbd
commit
88bae41a03
10 files changed
+306
-38
No files matched your search
@@ -116,10 +116,11 @@ public final class Main extends JavaPlugin {
|
||||
try (InputStream in =
|
||||
dump.isFile() ? new java.io.FileInputStream(dump) : getResource("cem/vanilla_entity_models.json")) {
|
||||
if (in != null) {
|
||||
long t0 = System.nanoTime();
|
||||
int n = cemLoader.load(in, getLogger());
|
||||
getLogger()
|
||||
.info("Loaded vanilla entity models" + (dump.isFile() ? " from " + dump.getName() : "") + " ("
|
||||
+ n + " models total).");
|
||||
+ n + " models total, " + (System.nanoTime() - t0) / 1_000_000 + " ms).");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
getLogger().severe("Failed to load vanilla entity models: " + e.getMessage());
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package eu.mhsl.minecraft.pixelpics.render.entity;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Bounded, thread-safe LRU cache of DERIVED textures — tinted / composited copies the bakers build per entity
|
||||
* (sheep wool colours, villager type + profession + level overlays, dyed leather armor, collars, banner
|
||||
* flags, …) — so identical mobs share one texture instead of re-tinting it on every bake.
|
||||
*
|
||||
* <p>Keys are built with {@link #key}: a value list of the inputs that determine the result. Source pixel
|
||||
* grids may be part of the key; arrays compare by identity, which is exact because no grid is mutated once
|
||||
* it is built (texture-cache grids live for the plugin's lifetime). Cached grids are shared and must not be
|
||||
* mutated by callers.
|
||||
* The computation runs outside the lock; concurrent misses on the same key may compute twice, the first
|
||||
* stored result wins.
|
||||
*/
|
||||
public final class DerivedTextureCache {
|
||||
|
||||
private static final int[][] NONE = new int[0][0]; // cached "no texture"
|
||||
|
||||
private final Map<List<Object>, int[][]> map;
|
||||
|
||||
public DerivedTextureCache(int capacity) {
|
||||
this.map = new LinkedHashMap<>(64, 0.75f, true) {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<List<Object>, int[][]> eldest) {
|
||||
return size() > capacity;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** A cache key from the inputs (nulls allowed; arrays compare by identity). */
|
||||
public static List<Object> key(Object... parts) {
|
||||
return Arrays.asList(parts);
|
||||
}
|
||||
|
||||
/** The cached texture for {@code key}, computing (and caching) it on a miss; null results are cached too. */
|
||||
public int[][] get(List<Object> key, Supplier<int[][]> compute) {
|
||||
int[][] v;
|
||||
synchronized (map) {
|
||||
v = map.get(key);
|
||||
}
|
||||
if (v == null) {
|
||||
int[][] computed = compute.get();
|
||||
synchronized (map) {
|
||||
v = map.putIfAbsent(key, computed == null ? NONE : computed);
|
||||
}
|
||||
if (v == null) return computed;
|
||||
}
|
||||
return v == NONE ? null : v;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import eu.mhsl.minecraft.pixelpics.assets.model.Face;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.Affine;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityModels;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.DerivedTextureCache;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.EntityBaker;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.EntityCube;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.RenderedEntity;
|
||||
@@ -37,6 +38,8 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
|
||||
private final TextureCache textures;
|
||||
private final SkinCache skins;
|
||||
private final BitmapFont font;
|
||||
// Composited banner flags (base + dyed pattern masks), shared by identical banners.
|
||||
private final DerivedTextureCache derived = new DerivedTextureCache(256);
|
||||
|
||||
/** The sign/text font (shared with text-display rendering). */
|
||||
public BitmapFont font() {
|
||||
@@ -104,9 +107,17 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
|
||||
* (one geometry, no coplanar layers).
|
||||
*/
|
||||
private int[][] bannerFlag(int[][] base, BlockEntityState s) {
|
||||
int baseColor = s.baseColorArgb();
|
||||
List<BlockEntityState.BannerPattern> patterns = List.copyOf(s.patterns());
|
||||
return derived.get(
|
||||
DerivedTextureCache.key("banner", base, baseColor, patterns),
|
||||
() -> buildBannerFlag(base, baseColor, patterns));
|
||||
}
|
||||
|
||||
private int[][] buildBannerFlag(int[][] base, int baseColor, List<BlockEntityState.BannerPattern> patterns) {
|
||||
int[][] out = TextureOps.deepCopy(base);
|
||||
if (s.baseColorArgb() != 0) overlayMask(out, "entity/banner/base", s.baseColorArgb());
|
||||
for (BlockEntityState.BannerPattern pat : s.patterns()) {
|
||||
if (baseColor != 0) overlayMask(out, "entity/banner/base", baseColor);
|
||||
for (BlockEntityState.BannerPattern pat : patterns) {
|
||||
overlayMask(out, "entity/banner/" + pat.patternKey(), pat.colorArgb());
|
||||
}
|
||||
return out;
|
||||
|
||||
@@ -7,6 +7,7 @@ import eu.mhsl.minecraft.pixelpics.assets.model.Face;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.Affine;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.BoxUv;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.DerivedTextureCache;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.EntityBaker;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.EntityCube;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.EntityModels;
|
||||
@@ -48,6 +49,8 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
private final TextureCache textures;
|
||||
private final SkinCache skins;
|
||||
private final Map<String, List<String>> subModelCache = new ConcurrentHashMap<>();
|
||||
// Tinted / composited per-entity textures (wool, villager overlays, armor, collars, …), shared by identical mobs.
|
||||
private final DerivedTextureCache derived = new DerivedTextureCache(512);
|
||||
|
||||
private volatile ItemGeometry items; // held/head items; null = not drawn
|
||||
|
||||
@@ -84,17 +87,21 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
if (base.equals("villager") || base.equals("zombie_villager")) {
|
||||
tex = compositeVillager(s, tex, base, ch != null && ch.babyModel);
|
||||
} else if (s.markings() != null && tex != null) {
|
||||
tex = TextureOps.deepCopy(tex); // horse coat markings
|
||||
String m = "entity/horse/horse_markings_" + s.markings();
|
||||
overlayIfPresent(tex, ch != null && ch.babyModel ? m + "_baby" : m);
|
||||
String m = "entity/horse/horse_markings_" + s.markings(); // horse coat markings
|
||||
String path = ch != null && ch.babyModel ? m + "_baby" : m;
|
||||
int[][] src = tex;
|
||||
tex = derived.get(DerivedTextureCache.key("markings", src, path), () -> {
|
||||
int[][] out = TextureOps.deepCopy(src);
|
||||
overlayIfPresent(out, path);
|
||||
return out;
|
||||
});
|
||||
}
|
||||
if (s.collar() != 0 && tex != null && layerModels(base).stream().noneMatch(n -> n.contains("CollarLayer."))) {
|
||||
// Collar drawn on the body model itself (WolfCollarLayer: parent model, same UV) -> composite it.
|
||||
tex = withCollar(tex, base, ch != null && ch.babyModel, s.collar());
|
||||
}
|
||||
if (s.typeKey().equals("tropical_fish") && tex != null && s.tint() != 0) {
|
||||
tex = TextureOps.deepCopy(tex);
|
||||
TextureOps.tint(tex, s.tint()); // white base body tinted with the body colour
|
||||
tex = tinted(tex, s.tint()); // white base body tinted with the body colour
|
||||
}
|
||||
|
||||
boolean invisible = s.invisible();
|
||||
@@ -401,9 +408,7 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
if ("sheared".equals(s.variant()) || lm.texture() == null) continue;
|
||||
int[][] t = texture(lm.texture());
|
||||
if (t == null) continue;
|
||||
t = TextureOps.deepCopy(t);
|
||||
if (s.tint() != 0) TextureOps.tint(t, s.tint());
|
||||
baked.addAll(pose.bake(lm, t));
|
||||
baked.addAll(pose.bake(lm, tinted(t, s.tint())));
|
||||
}
|
||||
case "TropicalFishPatternLayer" -> addTropicalPattern(s, n, lm, pose, baked);
|
||||
case "CatCollarLayer" -> {
|
||||
@@ -411,9 +416,7 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
if (s.collar() == 0 || lm.texture() == null) continue;
|
||||
int[][] t = texture(lm.texture());
|
||||
if (t == null) continue;
|
||||
t = TextureOps.deepCopy(t);
|
||||
TextureOps.tint(t, s.collar());
|
||||
baked.addAll(pose.bake(lm, t));
|
||||
baked.addAll(pose.bake(lm, tinted(t, s.collar())));
|
||||
}
|
||||
case "CreeperPowerLayer", "WitherArmorLayer" -> {
|
||||
// EnergySwirlLayer: charged creeper / wither below half health; drawn additively.
|
||||
@@ -477,13 +480,17 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
if (name.toLowerCase(Locale.ROOT).contains("small") != small) return;
|
||||
int[][] t = texture("entity/fish/tropical_" + (small ? "a" : "b") + "_pattern_" + (idx % 6 + 1));
|
||||
if (t == null) return;
|
||||
t = TextureOps.deepCopy(t);
|
||||
if (s.tint2() != 0) TextureOps.tint(t, s.tint2());
|
||||
baked.addAll(pose.bake(lm, t));
|
||||
baked.addAll(pose.bake(lm, tinted(t, s.tint2())));
|
||||
}
|
||||
|
||||
/** entity/equipment/<layerType>/<asset> (+ a "_carpet"-stripped name; leather dye + overlay). */
|
||||
/** entity/equipment/<layerType>/<asset> (+ a "_carpet"-stripped name; leather dye + overlay); cached. */
|
||||
private int[][] equipmentTexture(String layerType, String asset, boolean baby, int dye) {
|
||||
return derived.get(
|
||||
DerivedTextureCache.key("equip", layerType, asset, baby, dye),
|
||||
() -> buildEquipmentTexture(layerType, asset, baby, dye));
|
||||
}
|
||||
|
||||
private int[][] buildEquipmentTexture(String layerType, String asset, boolean baby, int dye) {
|
||||
String folder = "entity/equipment/" + layerType + "/";
|
||||
int[][] t = null;
|
||||
List<String> names = new ArrayList<>();
|
||||
@@ -520,6 +527,10 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
* xOffset: creeper {@code age·0.01}, wither {@code cos(age·0.02)·3}.
|
||||
*/
|
||||
private int[][] energySwirl(String path, boolean wither) {
|
||||
return derived.get(DerivedTextureCache.key("swirl", path, wither), () -> buildEnergySwirl(path, wither));
|
||||
}
|
||||
|
||||
private int[][] buildEnergySwirl(String path, boolean wither) {
|
||||
int[][] src = texture(path);
|
||||
if (src == null) return null;
|
||||
double u = wither ? Math.cos(SWIRL_AGE * 0.02) * 3 : SWIRL_AGE * 0.01;
|
||||
@@ -539,16 +550,29 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
|
||||
/** Body texture with the dye-tinted {@code entity/<base>/<base>_collar[_baby]} composited over it. */
|
||||
private int[][] withCollar(int[][] tex, String base, boolean baby, int dye) {
|
||||
return derived.get(
|
||||
DerivedTextureCache.key("collar", tex, base, baby, dye), () -> buildWithCollar(tex, base, baby, dye));
|
||||
}
|
||||
|
||||
private int[][] buildWithCollar(int[][] tex, String base, boolean baby, int dye) {
|
||||
String path = "entity/" + base + "/" + base + "_collar" + (baby ? "_baby" : "");
|
||||
int[][] collar = texture(path);
|
||||
if (collar == null || collar.length != tex.length || collar[0].length != tex[0].length) return tex;
|
||||
int[][] tinted = TextureOps.deepCopy(collar);
|
||||
TextureOps.tint(tinted, dye);
|
||||
int[][] out = TextureOps.deepCopy(tex);
|
||||
TextureOps.overlay(out, tinted);
|
||||
TextureOps.overlay(out, tinted(collar, dye));
|
||||
return out;
|
||||
}
|
||||
|
||||
/** {@code src} multiplied by an ARGB tint (cached; {@code src} itself for no tint). */
|
||||
private int[][] tinted(int[][] src, int argb) {
|
||||
if (argb == 0) return src;
|
||||
return derived.get(DerivedTextureCache.key("tint", src, argb), () -> {
|
||||
int[][] out = TextureOps.deepCopy(src);
|
||||
TextureOps.tint(out, argb);
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
// --- texture resolution --------------------------------------------------------------------------
|
||||
|
||||
private int[][] texture(String path) {
|
||||
@@ -602,6 +626,15 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
|
||||
private int[][] compositeVillager(EntityState s, int[][] base, String folder, boolean babyModel) {
|
||||
if (base == null) return null;
|
||||
// Only the inputs the composite reads: babies ignore profession and level.
|
||||
String prof = babyModel ? null : s.profession();
|
||||
int level = babyModel ? 0 : s.villagerLevel();
|
||||
return derived.get(
|
||||
DerivedTextureCache.key("villager", base, folder, babyModel, s.variant(), prof, level),
|
||||
() -> buildVillager(s, base, folder, babyModel));
|
||||
}
|
||||
|
||||
private int[][] buildVillager(EntityState s, int[][] base, String folder, boolean babyModel) {
|
||||
int[][] out = TextureOps.deepCopy(base);
|
||||
if (babyModel) {
|
||||
// Baby villagers: their own biome-type clothing set; no profession.
|
||||
@@ -658,7 +691,8 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
if (piece == null) return;
|
||||
CemModelLoader.CemModel model = models.vanilla(modelName);
|
||||
if (model == null) return;
|
||||
int[][] tex = buildArmorTexture(piece, layerFolder);
|
||||
int[][] tex = derived.get(
|
||||
DerivedTextureCache.key("armor", piece, layerFolder), () -> buildArmorTexture(piece, layerFolder));
|
||||
if (tex != null) baked.addAll(pose.bake(model, tex));
|
||||
}
|
||||
|
||||
@@ -713,9 +747,14 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
||||
if (model == null) return; // renderer without a WingsLayer
|
||||
int[][] tex = texture("entity/equipment/wings/elytra");
|
||||
if (tex == null) return;
|
||||
int[][] out = TextureOps.deepCopy(tex);
|
||||
if (piece.glint()) applyGlint(out);
|
||||
baked.addAll(pose.bake(model, out));
|
||||
if (piece.glint()) {
|
||||
tex = derived.get(DerivedTextureCache.key("elytra_glint", tex), () -> {
|
||||
int[][] out = TextureOps.deepCopy(texture("entity/equipment/wings/elytra"));
|
||||
applyGlint(out);
|
||||
return out;
|
||||
});
|
||||
}
|
||||
baked.addAll(pose.bake(model, tex));
|
||||
}
|
||||
|
||||
private RenderedEntity fallbackBox(EntityState s, int[][] tex) {
|
||||
|
||||
@@ -211,8 +211,12 @@ public final class CemModelLoader {
|
||||
try {
|
||||
JsonObject entry = e.getValue().getAsJsonObject();
|
||||
if (!entry.has("model")) continue;
|
||||
JsonObject model =
|
||||
JsonParser.parseString(entry.get("model").getAsString()).getAsJsonObject();
|
||||
// The vanilla dump nests the jem as an object; the CEM template set (and dumps written before
|
||||
// 2026-11) embed it as a JSON string that needs a second parse.
|
||||
JsonElement m = entry.get("model");
|
||||
JsonObject model = m.isJsonObject()
|
||||
? m.getAsJsonObject()
|
||||
: JsonParser.parseString(m.getAsString()).getAsJsonObject();
|
||||
int tw = model.getAsJsonArray("textureSize").get(0).getAsInt();
|
||||
int th = model.getAsJsonArray("textureSize").get(1).getAsInt();
|
||||
List<CemPart> parts = new ArrayList<>();
|
||||
|
||||
@@ -8,35 +8,75 @@ import org.bukkit.block.data.BlockData;
|
||||
/**
|
||||
* An immutable, thread-safe view of a bounded region of the world, backed by {@link ChunkSnapshot}s.
|
||||
* Block/biome lookups outside the captured region return air/null so rays simply terminate there.
|
||||
*
|
||||
* <p>The chunks live in a primitive open-addressing table (linear probing, Fibonacci hashing of the full
|
||||
* {@link #chunkKey}), not a {@code HashMap<Long, …>}: {@code Long.hashCode} of the key is {@code cx ^ cz}, so
|
||||
* every diagonal of the captured rectangle collided into treeified bins and each block lookup paid for
|
||||
* boxing, {@code TreeNode.find} and its reflective {@code comparableClassFor}. Keys are compared in full,
|
||||
* so lookups stay exact.
|
||||
*/
|
||||
public final class WorldSnapshot {
|
||||
|
||||
private final Map<Long, ChunkSnapshot> chunks;
|
||||
private static final long GOLDEN = 0x9E3779B97F4A7C15L;
|
||||
|
||||
private final long[] keys;
|
||||
private final ChunkSnapshot[] values; // null = empty slot
|
||||
private final int mask;
|
||||
private final int shift;
|
||||
private final int minY;
|
||||
private final int maxY; // exclusive
|
||||
private final BlockData air;
|
||||
|
||||
public WorldSnapshot(Map<Long, ChunkSnapshot> chunks, int minY, int maxY, BlockData air) {
|
||||
this.chunks = chunks;
|
||||
// Power-of-two capacity >= 2·size (load factor <= 0.5), at least 2 so the shift stays below 64.
|
||||
int cap = Math.max(2, Integer.highestOneBit(Math.max(1, chunks.size()) * 2 - 1) << 1);
|
||||
this.keys = new long[cap];
|
||||
this.values = new ChunkSnapshot[cap];
|
||||
this.mask = cap - 1;
|
||||
this.shift = 64 - Integer.numberOfTrailingZeros(cap);
|
||||
for (Map.Entry<Long, ChunkSnapshot> e : chunks.entrySet()) {
|
||||
if (e.getValue() == null) continue;
|
||||
long key = e.getKey();
|
||||
int i = slot(key);
|
||||
while (values[i] != null) i = (i + 1) & mask;
|
||||
keys[i] = key;
|
||||
values[i] = e.getValue();
|
||||
}
|
||||
this.minY = minY;
|
||||
this.maxY = maxY;
|
||||
this.air = air;
|
||||
}
|
||||
|
||||
/** Unique key of a chunk position (x in the high, z in the low 32 bits). */
|
||||
public static long chunkKey(int chunkX, int chunkZ) {
|
||||
return ((long) chunkX << 32) ^ (chunkZ & 0xFFFFFFFFL);
|
||||
}
|
||||
|
||||
private int slot(long key) {
|
||||
return (int) ((key * GOLDEN) >>> shift);
|
||||
}
|
||||
|
||||
private ChunkSnapshot chunk(int chunkX, int chunkZ) {
|
||||
long key = chunkKey(chunkX, chunkZ);
|
||||
int i = slot(key);
|
||||
ChunkSnapshot cs;
|
||||
while ((cs = values[i]) != null) {
|
||||
if (keys[i] == key) return cs;
|
||||
i = (i + 1) & mask;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public BlockData getBlockData(int x, int y, int z) {
|
||||
if (y < minY || y >= maxY) return air;
|
||||
ChunkSnapshot cs = chunks.get(chunkKey(x >> 4, z >> 4));
|
||||
ChunkSnapshot cs = chunk(x >> 4, z >> 4);
|
||||
if (cs == null) return air;
|
||||
return cs.getBlockData(x & 15, y, z & 15);
|
||||
}
|
||||
|
||||
public Biome getBiome(int x, int y, int z) {
|
||||
if (y < minY || y >= maxY) return null;
|
||||
ChunkSnapshot cs = chunks.get(chunkKey(x >> 4, z >> 4));
|
||||
ChunkSnapshot cs = chunk(x >> 4, z >> 4);
|
||||
if (cs == null) return null;
|
||||
return cs.getBiome(x & 15, y, z & 15);
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":"26.3","models":{"acacia_boat":{"model":"{\"textureSize\":[128,64],\"absolute\":true,\"layer\":\"boat/acacia#main\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,24.0,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"right_paddle\",\"pivot\":[-3.0,29.0,-9.0],\"rotate\":[37.5,-142.1347,11.25],\"boxes\":[{\"coordinates\":[-4.0,27.0,-14.0,2.0,2.0,18.0],\"textureOffset\":[62.0,20.0]},{\"coordinates\":[-4.001,26.0,-1.0,1.0,6.0,7.0],\"textureOffset\":[62.0,20.0]}]},{\"part\":\"left_paddle\",\"pivot\":[-3.0,29.0,9.0],\"rotate\":[37.5,-37.8653,11.25],\"boxes\":[{\"coordinates\":[-4.0,27.0,4.0,2.0,2.0,18.0],\"textureOffset\":[62.0,0.0]},{\"coordinates\":[-2.999,26.0,17.0,1.0,6.0,7.0],\"textureOffset\":[62.0,0.0]}]},{\"part\":\"left\",\"pivot\":[0.0,20.0,9.0],\"rotate\":[0.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,21.0,8.0,28.0,6.0,2.0],\"textureOffset\":[0.0,43.0]}]},{\"part\":\"bottom\",\"pivot\":[0.0,21.0,1.0],\"rotate\":[-90.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,14.0,-2.0,28.0,16.0,3.0],\"textureOffset\":[0.0,0.0]}]},{\"part\":\"back\",\"pivot\":[15.0,20.0,4.0],\"rotate\":[0.0,-270.0,0.0],\"boxes\":[{\"coordinates\":[10.0,21.0,3.0,18.0,6.0,2.0],\"textureOffset\":[0.0,19.0]}]},{\"part\":\"front\",\"pivot\":[-15.0,20.0,0.0],\"rotate\":[0.0,-90.0,0.0],\"boxes\":[{\"coordinates\":[-23.0,21.0,-1.0,16.0,6.0,2.0],\"textureOffset\":[0.0,27.0]}]},{\"part\":\"right\",\"pivot\":[0.0,20.0,-9.0],\"rotate\":[0.0,-180.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,21.0,-10.0,28.0,6.0,2.0],\"textureOffset\":[0.0,35.0]}]}]}]}"},"acacia_boat/waterPatchModel":{"model":"{\"textureSize\":[0,0],\"absolute\":true,\"layer\":\"boat#water_patch\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,24.0,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"water_patch\",\"pivot\":[0.0,27.0,1.0],\"rotate\":[-90.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,20.0,-2.0,28.0,16.0,3.0],\"textureOffset\":[0.0,0.0]}]}]}]}"},"acacia_chest_boat":{"model":"{\"textureSize\":[128,128],\"absolute\":true,\"layer\":\"chest_boat/acacia#main\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,24.0,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"chest_bottom\",\"pivot\":[2.0,29.0,-6.0],\"rotate\":[0.0,90.0,0.0],\"boxes\":[{\"coordinates\":[-10.0,21.0,-6.0,12.0,8.0,12.0],\"textureOffset\":[0.0,76.0]}]},{\"part\":\"right_paddle\",\"pivot\":[-3.0,29.0,-9.0],\"rotate\":[37.5,-142.1347,11.25],\"boxes\":[{\"coordinates\":[-4.0,27.0,-14.0,2.0,2.0,18.0],\"textureOffset\":[62.0,20.0]},{\"coordinates\":[-4.001,26.0,-1.0,1.0,6.0,7.0],\"textureOffset\":[62.0,20.0]}]},{\"part\":\"left_paddle\",\"pivot\":[-3.0,29.0,9.0],\"rotate\":[37.5,-37.8653,11.25],\"boxes\":[{\"coordinates\":[-4.0,27.0,4.0,2.0,2.0,18.0],\"textureOffset\":[62.0,0.0]},{\"coordinates\":[-2.999,26.0,17.0,1.0,6.0,7.0],\"textureOffset\":[62.0,0.0]}]},{\"part\":\"left\",\"pivot\":[0.0,20.0,9.0],\"rotate\":[0.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,21.0,8.0,28.0,6.0,2.0],\"textureOffset\":[0.0,43.0]}]},{\"part\":\"bottom\",\"pivot\":[0.0,21.0,1.0],\"rotate\":[-90.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,14.0,-2.0,28.0,16.0,3.0],\"textureOffset\":[0.0,0.0]}]},{\"part\":\"back\",\"pivot\":[15.0,20.0,4.0],\"rotate\":[0.0,-270.0,0.0],\"boxes\":[{\"coordinates\":[10.0,21.0,3.0,18.0,6.0,2.0],\"textureOffset\":[0.0,19.0]}]},{\"part\":\"chest_lid\",\"pivot\":[2.0,33.0,-6.0],\"rotate\":[0.0,90.0,0.0],\"boxes\":[{\"coordinates\":[-10.0,29.0,-6.0,12.0,4.0,12.0],\"textureOffset\":[0.0,59.0]}]},{\"part\":\"front\",\"pivot\":[-15.0,20.0,0.0],\"rotate\":[0.0,-90.0,0.0],\"boxes\":[{\"coordinates\":[-23.0,21.0,-1.0,16.0,6.0,2.0],\"textureOffset\":[0.0,27.0]}]},{\"part\":\"right\",\"pivot\":[0.0,20.0,-9.0],\"rotate\":[0.0,-180.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,21.0,-10.0,28.0,6.0,2.0],\"textureOffset\":[0.0,35.0]}]},{\"part\":\"chest_lock\",\"pivot\":[1.0,30.0,-1.0],\"rotate\":[0.0,90.0,0.0],\"boxes\":[{\"coordinates\":[-1.0,26.0,-1.0,2.0,4.0,1.0],\"textureOffset\":[0.0,59.0]}]}]}]}"},"acacia_chest_boat/waterPatchModel":{"model":"{\"textureSize\":[0,0],\"absolute\":true,\"layer\":\"boat#water_patch\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,24.0,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"water_patch\",\"pivot\":[0.0,27.0,1.0],\"rotate\":[-90.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,20.0,-2.0,28.0,16.0,3.0],\"textureOffset\":[0.0,0.0]}]}]}]}"},"allay":{"model":"{\"textureSize\":[32,32],\"absolute\":true,\"layer\":\"allay#main\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,0.25,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"head\",\"pivot\":[0.0,4.24,0.0],\"rotate\":[0.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-2.5,4.24,-2.5,5.0,5.0,5.0],\"textureOffset\":[0.0,0.0]}]},{\"part\":\"body\",\"pivot\":[0.0,4.25,0.0],\"rotate\":[0.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-1.5,0.25,-1.0,3.0,4.0,2.0],\"textureOffset\":[0.0,10.0]},{\"coordinates\":[-1.5,-0.75,-1.0,3.0,5.0,2.0],\"textureOffset\":[0.0,16.0],\"sizeAdd\":-0.2}],\"submodels\":[{\"part\":\"right_arLine truncated
|
||||
{"version":"26.3","models":{"acacia_boat":{"model":{"textureSize":[128,64],"absolute":true,"layer":"boat/acacia#main","models":[{"part":"root","pivot":[0.0,24.0,0.0],"rotate":[0.0,0.0,0.0],"submodels":[{"part":"right_paddle","pivot":[-3.0,29.0,-9.0],"rotate":[37.5,-142.1347,11.25],"boxes":[{"coordinates":[-4.0,27.0,-14.0,2.0,2.0,18.0],"textureOffset":[62.0,20.0]},{"coordinates":[-4.001,26.0,-1.0,1.0,6.0,7.0],"textureOffset":[62.0,20.0]}]},{"part":"left_paddle","pivot":[-3.0,29.0,9.0],"rotate":[37.5,-37.8653,11.25],"boxes":[{"coordinates":[-4.0,27.0,4.0,2.0,2.0,18.0],"textureOffset":[62.0,0.0]},{"coordinates":[-2.999,26.0,17.0,1.0,6.0,7.0],"textureOffset":[62.0,0.0]}]},{"part":"left","pivot":[0.0,20.0,9.0],"rotate":[0.0,0.0,0.0],"boxes":[{"coordinates":[-14.0,21.0,8.0,28.0,6.0,2.0],"textureOffset":[0.0,43.0]}]},{"part":"bottom","pivot":[0.0,21.0,1.0],"rotate":[-90.0,0.0,0.0],"boxes":[{"coordinates":[-14.0,14.0,-2.0,28.0,16.0,3.0],"textureOffset":[0.0,0.0]}]},{"part":"back","pivot":[15.0,20.0,4.0],"rotate":[0.0,-270.0,0.0],"boxes":[{"coordinates":[10.0,21.0,3.0,18.0,6.0,2.0],"textureOffset":[0.0,19.0]}]},{"part":"front","pivot":[-15.0,20.0,0.0],"rotate":[0.0,-90.0,0.0],"boxes":[{"coordinates":[-23.0,21.0,-1.0,16.0,6.0,2.0],"textureOffset":[0.0,27.0]}]},{"part":"right","pivot":[0.0,20.0,-9.0],"rotate":[0.0,-180.0,0.0],"boxes":[{"coordinates":[-14.0,21.0,-10.0,28.0,6.0,2.0],"textureOffset":[0.0,35.0]}]}]}]}},"acacia_boat/waterPatchModel":{"model":{"textureSize":[0,0],"absolute":true,"layer":"boat#water_patch","models":[{"part":"root","pivot":[0.0,24.0,0.0],"rotate":[0.0,0.0,0.0],"submodels":[{"part":"water_patch","pivot":[0.0,27.0,1.0],"rotate":[-90.0,0.0,0.0],"boxes":[{"coordinates":[-14.0,20.0,-2.0,28.0,16.0,3.0],"textureOffset":[0.0,0.0]}]}]}]}},"acacia_chest_boat":{"model":{"textureSize":[128,128],"absolute":true,"layer":"chest_boat/acacia#main","models":[{"part":"root","pivot":[0.0,24.0,0.0],"rotate":[0.0,0.0,0.0],"submodels":[{"part":"chest_bottom","pivot":[2.0,29.0,-6.0],"rotate":[0.0,90.0,0.0],"boxes":[{"coordinates":[-10.0,21.0,-6.0,12.0,8.0,12.0],"textureOffset":[0.0,76.0]}]},{"part":"right_paddle","pivot":[-3.0,29.0,-9.0],"rotate":[37.5,-142.1347,11.25],"boxes":[{"coordinates":[-4.0,27.0,-14.0,2.0,2.0,18.0],"textureOffset":[62.0,20.0]},{"coordinates":[-4.001,26.0,-1.0,1.0,6.0,7.0],"textureOffset":[62.0,20.0]}]},{"part":"left_paddle","pivot":[-3.0,29.0,9.0],"rotate":[37.5,-37.8653,11.25],"boxes":[{"coordinates":[-4.0,27.0,4.0,2.0,2.0,18.0],"textureOffset":[62.0,0.0]},{"coordinates":[-2.999,26.0,17.0,1.0,6.0,7.0],"textureOffset":[62.0,0.0]}]},{"part":"left","pivot":[0.0,20.0,9.0],"rotate":[0.0,0.0,0.0],"boxes":[{"coordinates":[-14.0,21.0,8.0,28.0,6.0,2.0],"textureOffset":[0.0,43.0]}]},{"part":"bottom","pivot":[0.0,21.0,1.0],"rotate":[-90.0,0.0,0.0],"boxes":[{"coordinates":[-14.0,14.0,-2.0,28.0,16.0,3.0],"textureOffset":[0.0,0.0]}]},{"part":"back","pivot":[15.0,20.0,4.0],"rotate":[0.0,-270.0,0.0],"boxes":[{"coordinates":[10.0,21.0,3.0,18.0,6.0,2.0],"textureOffset":[0.0,19.0]}]},{"part":"chest_lid","pivot":[2.0,33.0,-6.0],"rotate":[0.0,90.0,0.0],"boxes":[{"coordinates":[-10.0,29.0,-6.0,12.0,4.0,12.0],"textureOffset":[0.0,59.0]}]},{"part":"front","pivot":[-15.0,20.0,0.0],"rotate":[0.0,-90.0,0.0],"boxes":[{"coordinates":[-23.0,21.0,-1.0,16.0,6.0,2.0],"textureOffset":[0.0,27.0]}]},{"part":"right","pivot":[0.0,20.0,-9.0],"rotate":[0.0,-180.0,0.0],"boxes":[{"coordinates":[-14.0,21.0,-10.0,28.0,6.0,2.0],"textureOffset":[0.0,35.0]}]},{"part":"chest_lock","pivot":[1.0,30.0,-1.0],"rotate":[0.0,90.0,0.0],"boxes":[{"coordinates":[-1.0,26.0,-1.0,2.0,4.0,1.0],"textureOffset":[0.0,59.0]}]}]}]}},"acacia_chest_boat/waterPatchModel":{"model":{"textureSize":[0,0],"absolute":true,"layer":"boat#water_patch","models":[{"part":"root","pivot":[0.0,24.0,0.0],"rotate":[0.0,0.0,0.0],"submodels":[{"part":"water_patch","pivot":[0.0,27.0,1.0],"rotate":[-90.0,0.0,0.0],"boxes":[{"coordinates":[-14.0,20.0,-2.0,28.0,16.0,3.0],"textureOffset":[0.0,0.0]}]}]}]}},"allay":{"model":{"textureSize":[32,32],"absolute":true,"layer":"allay#main","models":[{"part":"root","pivot":[0.0,0.25,0.0],"rotate":[0.0,0.0,0.0],"submodels":[{"part":"head","pivot":[0.0,4.24,0.0],"rotate":[0.0,0.0,0.0],"boxes":[{"coordinates":[-2.5,4.24,-2.5,5.0,5.0,5.0],"textureOffset":[0.0,0.0]}]},{"part":"body","pivot":[0.0,4.25,0.0],"rotate":[0.0,0.0,0.0],"boxes":[{"coordinates":[-1.5,0.25,-1.0,3.0,4.0,2.0],"textureOffset":[0.0,10.0]},{"coordinates":[-1.5,-0.75,-1.0,3.0,5.0,2.0],"textureOffset":[0.0,16.0],"sizeAdd":-0.2}],"submodels":[{"part":"right_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
|
||||
@@ -24,7 +24,7 @@ import java.util.logging.Logger;
|
||||
* times (after {@code -Dwarmup} runs), prints the median wall time and writes one PNG per scene to
|
||||
* {@code -Dout} (default ~/Dokumente/PixelPics-Entity-Renders/bench) for before/after pixel diffs.
|
||||
* Args: scene names to run (default all): farm_mobs, farm_empty (same view, no entities), farm_mobs_close,
|
||||
* farm_items, normal. {@code -Djava.util.concurrent.ForkJoinPool.common.parallelism=N} pins the thread count.
|
||||
* farm_items, village, normal. {@code -Djava.util.concurrent.ForkJoinPool.common.parallelism=N} pins the thread count.
|
||||
*/
|
||||
public class EntityBenchmark {
|
||||
|
||||
@@ -114,6 +114,33 @@ public class EntityBenchmark {
|
||||
}
|
||||
out.add(new Scene("farm_items", new Vector(0, 7, -1), new Vector(7, 1, 7), List.of(), items));
|
||||
|
||||
// Village: 300 mobs with per-mob texture compositing (villager type/profession/level overlays, dyed
|
||||
// sheep wool, dyed leather armor, tinted wolf collars) — exercises the tinted-texture cache.
|
||||
List<EntityState> village = new ArrayList<>();
|
||||
String[] vTypes = {"plains", "desert", "jungle", "savanna", "snow", "swamp", "taiga"};
|
||||
String[] profs = {"armorer", "butcher", "cartographer", "cleric", "farmer", "fisherman", "fletcher",
|
||||
"leatherworker", "librarian", "mason", "shepherd", "toolsmith", "weaponsmith", "nitwit", "none"};
|
||||
Random rv = new Random(7);
|
||||
for (int i = 0; i < 15; i++) {
|
||||
for (int j = 0; j < 20; j++) {
|
||||
double x = i * 1.4 + rv.nextDouble() * 0.3, z = j * 1.4 + rv.nextDouble() * 0.3;
|
||||
float yaw = rv.nextFloat() * 360;
|
||||
int dye = DYES[rv.nextInt(DYES.length)];
|
||||
EntityState m = switch (rv.nextInt(4)) {
|
||||
case 0 -> villager(x, z, yaw, vTypes[rv.nextInt(vTypes.length)], profs[rv.nextInt(profs.length)],
|
||||
1 + rv.nextInt(5), rv.nextInt(6) == 0);
|
||||
case 1 -> with(mob("sheep", x, 1, z, yaw, rv.nextInt(6) == 0), dye, null, 0);
|
||||
case 2 -> {
|
||||
EntityState.EquipPiece le = new EntityState.EquipPiece("leather", dye, null, null, false);
|
||||
yield with(mob("zombie", x, 1, z, yaw, false), 0, new EntityState.Equipment(le, le, le, le), 0);
|
||||
}
|
||||
default -> with(mob("wolf", x, 1, z, yaw, rv.nextInt(6) == 0), 0, null, dye);
|
||||
};
|
||||
village.add(m);
|
||||
}
|
||||
}
|
||||
out.add(new Scene("village", new Vector(-5, 10, -5), new Vector(10, 1, 13), village, List.of()));
|
||||
|
||||
// Normal scene: a handful of mobs incl. translucent (slime) and additive (charged creeper) layers,
|
||||
// a text display with the default translucent background and two items.
|
||||
List<EntityState> few = new ArrayList<>();
|
||||
@@ -142,11 +169,32 @@ public class EntityBenchmark {
|
||||
false, 0, 0, 0, false, null, null, null, 0, false, null);
|
||||
}
|
||||
|
||||
/** Vanilla DyeColor texture colours (white … black). */
|
||||
static final int[] DYES = {0xFFF9FFFE, 0xFFF9801D, 0xFFC74EBD, 0xFF3AB3DA, 0xFFFED83D, 0xFF80C71F, 0xFFF38BAA,
|
||||
0xFF474F52, 0xFF9D9D97, 0xFF169C9C, 0xFF8932B8, 0xFF3C44AA, 0xFF835432, 0xFF5E7C16, 0xFFB02E26, 0xFF1D1D21};
|
||||
|
||||
static EntityState villager(double x, double z, float yaw, String type, String prof, int level, boolean baby) {
|
||||
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);
|
||||
}
|
||||
|
||||
/** {@code s} with wool tint, humanoid armor and collar dye replaced. */
|
||||
static EntityState with(EntityState s, int tint, EntityState.Equipment eq, int collar) {
|
||||
return new EntityState(s.typeKey(), s.x(), s.y(), s.z(), s.bodyYaw(), s.baby(), s.width(), s.height(),
|
||||
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());
|
||||
}
|
||||
|
||||
/** The variant the game always supplies (cow/pig/chicken have no texture without one). */
|
||||
static String variant(String type) {
|
||||
return switch (type) {
|
||||
case "villager" -> "plains";
|
||||
case "cow", "pig", "chicken" -> "temperate";
|
||||
case "wolf" -> "pale";
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
@@ -31,7 +30,8 @@ import net.minecraft.world.entity.EntityType;
|
||||
|
||||
/**
|
||||
* Dumps the vanilla Java entity models straight from an (unobfuscated, 26.x+) Minecraft CLIENT jar into
|
||||
* the CEM-template JSON layout PixelPics loads ({@code {"models": {name: {"model": "<jem json>"}}}}).
|
||||
* the CEM-template JSON layout PixelPics loads ({@code {"models": {name: {"model": {<jem>}}}}}; the jem is a
|
||||
* nested object, not the template set's embedded JSON string, so the plugin parses the file only once).
|
||||
*
|
||||
* <p>Unlike the third-party CEM template set (rest geometry only), every entity model is instantiated
|
||||
* through its real renderer and posed by its real {@code setupAnim} with a default (idle, adult, not
|
||||
@@ -152,7 +152,7 @@ public class VanillaModelDump {
|
||||
parts.add(part("root", rootDef, baked, 0, 0, 0));
|
||||
jem.add("models", parts);
|
||||
JsonObject entry = new JsonObject();
|
||||
entry.addProperty("model", new Gson().toJson(jem));
|
||||
entry.add("model", jem); // nested object, not a JSON string: parsed once at plugin start
|
||||
out.putIfAbsent(name, entry);
|
||||
} catch (Throwable t) {
|
||||
failed.add(name + " (layer: " + t + ")");
|
||||
@@ -315,7 +315,7 @@ public class VanillaModelDump {
|
||||
JsonObject jem = dump(factory.apply(set.bakeLayer(loc)), state, name, failed);
|
||||
if (jem == null) return;
|
||||
JsonObject entry = new JsonObject();
|
||||
entry.addProperty("model", new Gson().toJson(jem));
|
||||
entry.add("model", jem);
|
||||
out.put(name, entry);
|
||||
} catch (Throwable t) {
|
||||
failed.add(name + " (" + t + ")");
|
||||
@@ -368,7 +368,7 @@ public class VanillaModelDump {
|
||||
String tex = textures.get(f);
|
||||
if (tex != null) jem.addProperty("texture", tex);
|
||||
JsonObject entry = new JsonObject();
|
||||
entry.addProperty("model", new Gson().toJson(jem));
|
||||
entry.add("model", jem);
|
||||
out.put(name, entry);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +141,10 @@ public class PPTest extends JavaPlugin {
|
||||
scenes.put("perf_farm", new Scene("perf_farm", this::perfFarm, -6, 9, -13, 4, 0, 0));
|
||||
scenes.put("perf_farm_close", new Scene("perf_farm_close", this::perfFarm, 0.5, 3, -11.5, 5, 0.5, -4));
|
||||
scenes.put("perf_items", new Scene("perf_items", this::perfItems, 0.5, 5, -10, 5, 0, -2));
|
||||
// Per-mob texture compositing (tinted-texture cache): villager type/profession/level, sheep colours, dyed
|
||||
// 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));
|
||||
}
|
||||
|
||||
private void perfFarm(Location o) {
|
||||
@@ -156,6 +160,71 @@ public class PPTest extends JavaPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
private void perfVillage(Location o) {
|
||||
Random r = new Random(11);
|
||||
List<Villager.Type> vTypes = registry(io.papermc.paper.registry.RegistryKey.VILLAGER_TYPE);
|
||||
List<Villager.Profession> profs = registry(io.papermc.paper.registry.RegistryKey.VILLAGER_PROFESSION);
|
||||
DyeColor[] dyes = DyeColor.values();
|
||||
for (int i = 0; i < 14; i++)
|
||||
for (int j = 0; j < 16; j++) {
|
||||
double x = -5 + i * 1.2, z = -10 + j * 1.2;
|
||||
DyeColor dye = dyes[r.nextInt(dyes.length)];
|
||||
LivingEntity e;
|
||||
switch (r.nextInt(4)) {
|
||||
case 0 -> {
|
||||
Villager v = rfSpawn(o, x, 0, z, 0, Villager.class);
|
||||
v.setVillagerType(vTypes.get(r.nextInt(vTypes.size())));
|
||||
if (r.nextInt(6) == 0) v.setBaby();
|
||||
else {
|
||||
v.setProfession(profs.get(r.nextInt(profs.size())));
|
||||
v.setVillagerLevel(1 + r.nextInt(5));
|
||||
}
|
||||
e = v;
|
||||
}
|
||||
case 1 -> {
|
||||
Sheep sh = rfSpawn(o, x, 0, z, 0, Sheep.class);
|
||||
sh.setColor(dye);
|
||||
if (r.nextInt(6) == 0) sh.setBaby();
|
||||
e = sh;
|
||||
}
|
||||
case 2 -> {
|
||||
Zombie zo = rfSpawn(o, x, 0, z, 0, Zombie.class);
|
||||
Material[] pieces = {Material.LEATHER_HELMET, Material.LEATHER_CHESTPLATE,
|
||||
Material.LEATHER_LEGGINGS, Material.LEATHER_BOOTS};
|
||||
ItemStack[] armor = new ItemStack[4];
|
||||
for (int k = 0; k < 4; k++) {
|
||||
armor[k] = it(pieces[k]);
|
||||
org.bukkit.inventory.meta.LeatherArmorMeta m =
|
||||
(org.bukkit.inventory.meta.LeatherArmorMeta) armor[k].getItemMeta();
|
||||
m.setColor(dye.getColor());
|
||||
armor[k].setItemMeta(m);
|
||||
}
|
||||
zo.getEquipment().setHelmet(armor[0]);
|
||||
zo.getEquipment().setChestplate(armor[1]);
|
||||
zo.getEquipment().setLeggings(armor[2]);
|
||||
zo.getEquipment().setBoots(armor[3]);
|
||||
e = zo;
|
||||
}
|
||||
default -> {
|
||||
Wolf wo = rfSpawn(o, x, 0, z, 0, Wolf.class);
|
||||
tame(wo, dye);
|
||||
if (r.nextInt(6) == 0) wo.setBaby();
|
||||
e = wo;
|
||||
}
|
||||
}
|
||||
e.setBodyYaw(r.nextInt(8) * 45);
|
||||
}
|
||||
for (int b = 0; b < 10; b++) {
|
||||
int x = -4 + b * 2;
|
||||
put(o, x, 0, 10, dyes[r.nextInt(dyes.length)].name().toLowerCase() + "_banner[rotation=" + r.nextInt(16) + "]");
|
||||
if (o.clone().add(x, 0, 10).getBlock().getState() instanceof org.bukkit.block.Banner bn) {
|
||||
bn.addPattern(new org.bukkit.block.banner.Pattern(dyes[r.nextInt(dyes.length)], org.bukkit.block.banner.PatternType.STRIPE_TOP));
|
||||
bn.addPattern(new org.bukkit.block.banner.Pattern(dyes[r.nextInt(dyes.length)], org.bukkit.block.banner.PatternType.CREEPER));
|
||||
bn.update(true, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void perfItems(Location o) {
|
||||
Random r = new Random(7);
|
||||
Material[] mats = {Material.WHEAT, Material.CARROT, Material.EGG, Material.BONE, Material.ROTTEN_FLESH,
|
||||
|
||||
Reference in new issue
Block a user