christmas chest textures (24-26 december, chests and trapped chests, local_time item models, pixelpics.testDate test property), chest items from the item definition texture, shield banner patterns, item displays turned 180° like vanilla, dropped item lift from the ground model bounds, baby held-item offset, map decorations on framed vanilla maps (banners, frames, markers; none on photo maps), inverted block elements in items and displays (spawner, vault inner faces), deterministic test scenes, items/xmas/marker test scenes
This commit is contained in:
1 parent
9e48e35680
commit
9093c1ccfb
17 files changed
+681
-74
No files matched your search
@@ -1,5 +1,6 @@
|
|||||||
package eu.mhsl.minecraft.pixelpics.render.entity;
|
package eu.mhsl.minecraft.pixelpics.render.entity;
|
||||||
|
|
||||||
|
import eu.mhsl.minecraft.pixelpics.render.util.SpecialDates;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -237,13 +238,18 @@ public final class BlockEntityModels {
|
|||||||
return s.chestKind() == null ? BlockEntityState.ChestKind.SINGLE : s.chestKind();
|
return s.chestKind() == null ? BlockEntityState.ChestKind.SINGLE : s.chestKind();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChestRenderer.getChestMaterial + Sheets.chooseSprite: copper by stage, ender, CHRISTMAS for regular AND
|
||||||
|
* trapped chests between 24 and 26 December ({@link SpecialDates#isExtendedChristmas}), else normal/trapped.
|
||||||
|
*/
|
||||||
private static List<String> chestTextures(BlockEntityState s) {
|
private static List<String> chestTextures(BlockEntityState s) {
|
||||||
|
boolean xmas = SpecialDates.isExtendedChristmas();
|
||||||
String base =
|
String base =
|
||||||
switch (s.kind()) {
|
switch (s.kind()) {
|
||||||
case TRAPPED_CHEST -> "trapped";
|
case TRAPPED_CHEST -> xmas ? "christmas" : "trapped";
|
||||||
case ENDER_CHEST -> "ender";
|
case ENDER_CHEST -> "ender";
|
||||||
case COPPER_CHEST -> s.colorName() == null ? "copper" : "copper_" + s.colorName();
|
case COPPER_CHEST -> s.colorName() == null ? "copper" : "copper_" + s.colorName();
|
||||||
default -> "normal";
|
default -> xmas ? "christmas" : "normal";
|
||||||
};
|
};
|
||||||
if (s.kind() == BlockEntityState.Kind.ENDER_CHEST) return List.of("entity/chest/ender");
|
if (s.kind() == BlockEntityState.Kind.ENDER_CHEST) return List.of("entity/chest/ender");
|
||||||
return switch (chestKind(s)) {
|
return switch (chestKind(s)) {
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ public final class DecorationBaker implements EntityBaker<DecorationState> {
|
|||||||
faces[Direction.NORTH.ordinal()] = new Face(s.mapPixels(), 1, 1, 0, 0, 0, -1);
|
faces[Direction.NORTH.ordinal()] = new Face(s.mapPixels(), 1, 1, 0, 0, 0, -1);
|
||||||
faces[Direction.SOUTH.ordinal()] = new Face(s.mapPixels(), 0, 1, 1, 0, 0, -1);
|
faces[Direction.SOUTH.ordinal()] = new Face(s.mapPixels(), 0, 1, 1, 0, 0, -1);
|
||||||
cubes.add(new EntityCube(new double[] {0, 0, -0.01}, new double[] {128, 128, -0.01}, faces, m));
|
cubes.add(new EntityCube(new double[] {0, 0, -0.01}, new double[] {128, 128, -0.01}, faces, m));
|
||||||
|
addMapMarkers(s, m, cubes);
|
||||||
} else if (s.item() != null && items != null) {
|
} else if (s.item() != null && items != null) {
|
||||||
Affine m =
|
Affine m =
|
||||||
content.mul(Affine.rotZ(Math.toRadians(s.rotation() * 45))).mul(Affine.scale(0.5));
|
content.mul(Affine.rotZ(Math.toRadians(s.rotation() * 45))).mul(Affine.scale(0.5));
|
||||||
@@ -108,6 +109,31 @@ public final class DecorationBaker implements EntityBaker<DecorationState> {
|
|||||||
return cubes.isEmpty() ? null : RenderedEntity.of(cubes);
|
return cubes.isEmpty() ? null : RenderedEntity.of(cubes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MapRenderer decorations (only those shown on frames, see {@code MapPixels.markers}): in map pixel space
|
||||||
|
* {@code T(x/2 + 64, y/2 + 64, -0.02)·rotZ(rot·22.5°)·S(4, 4, 3)·T(-0.125, 0.125, 0)}, the sprite quad
|
||||||
|
* (-1,1)..(1,-1) at z = n·-0.001 (n-th drawn decoration), {@code map/decorations/<asset>} with v0 at +y.
|
||||||
|
*/
|
||||||
|
private void addMapMarkers(DecorationState s, Affine map, List<EntityCube> cubes) {
|
||||||
|
int count = 0;
|
||||||
|
for (DecorationState.MapMarker mk : s.mapMarkers()) {
|
||||||
|
int[][] sprite = textures.get(ResourceLocation.parse("map/decorations/" + mk.asset()))
|
||||||
|
.orElse(null);
|
||||||
|
if (sprite != null) {
|
||||||
|
Affine m = map.mul(Affine.translation(mk.x() / 2.0 + 64, mk.y() / 2.0 + 64, -0.02))
|
||||||
|
.mul(Affine.rotZ(Math.toRadians(mk.rot() * 360 / 16.0)))
|
||||||
|
.mul(Affine.scale(4, 4, 3))
|
||||||
|
.mul(Affine.translation(-0.125, 0.125, 0));
|
||||||
|
double z = count * -0.001;
|
||||||
|
Face[] faces = new Face[6];
|
||||||
|
faces[Direction.SOUTH.ordinal()] = new Face(sprite, 0, 0, 1, 1, 0, -1);
|
||||||
|
faces[Direction.NORTH.ordinal()] = new Face(sprite, 1, 0, 0, 1, 0, -1);
|
||||||
|
cubes.add(new EntityCube(new double[] {-1, -1, z}, new double[] {1, 1, z}, faces, m));
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Vanilla Direction#toYRot for horizontal facings (south 0, west 90, north 180, east 270). */
|
/** Vanilla Direction#toYRot for horizontal facings (south 0, west 90, north 180, east 270). */
|
||||||
private static double yRot(DecorationState.Facing f) {
|
private static double yRot(DecorationState.Facing f) {
|
||||||
return switch (f) {
|
return switch (f) {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package eu.mhsl.minecraft.pixelpics.render.entity;
|
package eu.mhsl.minecraft.pixelpics.render.entity;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Immutable snapshot of a flat wall decoration (painting or item frame). These are Bukkit
|
* Immutable snapshot of a flat wall decoration (painting or item frame). These are Bukkit
|
||||||
* {@code Hanging} entities, not block-entities, but render as flat textured quads rather than CEM
|
* {@code Hanging} entities, not block-entities, but render as flat textured quads rather than CEM
|
||||||
@@ -21,8 +23,50 @@ public record DecorationState(
|
|||||||
int[][] mapPixels, // framed map's 128x128 ARGB colours (row-major [y][x]); null if not a map with data
|
int[][] mapPixels, // framed map's 128x128 ARGB colours (row-major [y][x]); null if not a map with data
|
||||||
int rotation, // item-frame content rotation in 45° steps (0..7, vanilla ItemFrame#getRotation)
|
int rotation, // item-frame content rotation in 45° steps (0..7, vanilla ItemFrame#getRotation)
|
||||||
boolean glow, // glow item frame
|
boolean glow, // glow item frame
|
||||||
boolean invisible // invisible frame (setVisible(false)): no frame model, item sits on the wall
|
boolean invisible, // invisible frame (setVisible(false)): no frame model, item sits on the wall
|
||||||
|
List<MapMarker> mapMarkers // framed map's decorations shown on item frames (banners, frames, …); may be empty
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
/** The layout without map markers (paintings, frames without a map or with an undecorated one). */
|
||||||
|
public DecorationState(
|
||||||
|
Kind kind,
|
||||||
|
double minX,
|
||||||
|
double minY,
|
||||||
|
double minZ,
|
||||||
|
double maxX,
|
||||||
|
double maxY,
|
||||||
|
double maxZ,
|
||||||
|
Facing facing,
|
||||||
|
String paintingArt,
|
||||||
|
ItemSpec item,
|
||||||
|
int[][] mapPixels,
|
||||||
|
int rotation,
|
||||||
|
boolean glow,
|
||||||
|
boolean invisible) {
|
||||||
|
this(
|
||||||
|
kind,
|
||||||
|
minX,
|
||||||
|
minY,
|
||||||
|
minZ,
|
||||||
|
maxX,
|
||||||
|
maxY,
|
||||||
|
maxZ,
|
||||||
|
facing,
|
||||||
|
paintingArt,
|
||||||
|
item,
|
||||||
|
mapPixels,
|
||||||
|
rotation,
|
||||||
|
glow,
|
||||||
|
invisible,
|
||||||
|
List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One map decoration (vanilla MapDecoration): sprite {@code map/decorations/<asset>}, position in half map
|
||||||
|
* pixels from the centre ({@code x}, {@code y} in -128..127), rotation in 16ths of a full turn.
|
||||||
|
*/
|
||||||
|
public record MapMarker(String asset, int x, int y, int rot) {}
|
||||||
|
|
||||||
public enum Kind {
|
public enum Kind {
|
||||||
PAINTING,
|
PAINTING,
|
||||||
ITEM_FRAME
|
ITEM_FRAME
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ public final class EntityCube {
|
|||||||
public final Affine toLocal; // inverse
|
public final Affine toLocal; // inverse
|
||||||
public final boolean additive; // emissive, added to what lies behind (energy swirl) instead of blended
|
public final boolean additive; // emissive, added to what lies behind (energy swirl) instead of blended
|
||||||
public final boolean unshaded; // no directional face shading (vanilla NO_CARDINAL_LIGHTING: eyes, breeze wind)
|
public final boolean unshaded; // no directional face shading (vanilla NO_CARDINAL_LIGHTING: eyes, breeze wind)
|
||||||
|
// Block-model element with from > to on some axis (spawner/vault inner faces): its faces point INTO the box
|
||||||
|
// and are back-face culled like vanilla quads (see EntityIntersector); mirrored = toWorld flips handedness.
|
||||||
|
public final boolean inverted, mirrored;
|
||||||
public final double[] aabbMin = {Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE};
|
public final double[] aabbMin = {Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE};
|
||||||
public final double[] aabbMax = {-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE};
|
public final double[] aabbMax = {-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE};
|
||||||
|
|
||||||
@@ -34,6 +37,12 @@ public final class EntityCube {
|
|||||||
this.faces = faces;
|
this.faces = faces;
|
||||||
this.toWorld = toWorld;
|
this.toWorld = toWorld;
|
||||||
this.toLocal = toWorld.inverse();
|
this.toLocal = toWorld.inverse();
|
||||||
|
this.inverted = from[0] > to[0] || from[1] > to[1] || from[2] > to[2];
|
||||||
|
double[] r = toWorld.r;
|
||||||
|
double det = r[0] * (r[4] * r[8] - r[5] * r[7])
|
||||||
|
- r[1] * (r[3] * r[8] - r[5] * r[6])
|
||||||
|
+ r[2] * (r[3] * r[7] - r[4] * r[6]);
|
||||||
|
this.mirrored = det < 0;
|
||||||
for (int i = 0; i < 8; i++) {
|
for (int i = 0; i < 8; i++) {
|
||||||
double x = (i & 1) == 0 ? from[0] : to[0];
|
double x = (i & 1) == 0 ? from[0] : to[0];
|
||||||
double y = (i & 2) == 0 ? from[1] : to[1];
|
double y = (i & 2) == 0 ? from[1] : to[1];
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import org.bukkit.util.Vector;
|
|||||||
public final class EntityIntersector {
|
public final class EntityIntersector {
|
||||||
|
|
||||||
private static final double EPS = 1e-7;
|
private static final double EPS = 1e-7;
|
||||||
|
private static final Direction[] DIRECTIONS = Direction.values();
|
||||||
|
|
||||||
private EntityIntersector() {}
|
private EntityIntersector() {}
|
||||||
|
|
||||||
@@ -55,6 +56,7 @@ public final class EntityIntersector {
|
|||||||
d[1] = r[3] * dx + r[4] * dy + r[5] * dz;
|
d[1] = r[3] * dx + r[4] * dy + r[5] * dz;
|
||||||
d[2] = r[6] * dx + r[7] * dy + r[8] * dz;
|
d[2] = r[6] * dx + r[7] * dy + r[8] * dz;
|
||||||
double[] from = cube.from, to = cube.to;
|
double[] from = cube.from, to = cube.to;
|
||||||
|
if (cube.inverted) return intersectInverted(cube, o, d, Math.max(EPS, minT), maxT, out);
|
||||||
|
|
||||||
double tmin = Double.NEGATIVE_INFINITY, tmax = Double.POSITIVE_INFINITY;
|
double tmin = Double.NEGATIVE_INFINITY, tmax = Double.POSITIVE_INFINITY;
|
||||||
int inAxis = -1, outAxis = -1;
|
int inAxis = -1, outAxis = -1;
|
||||||
@@ -110,6 +112,53 @@ public final class EntityIntersector {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A block-model element with {@code from > to} on some axis (spawner/vault/trial-spawner inner faces) baked as
|
||||||
|
* an item or display: like {@code ElementIntersector.invertedBox}, vanilla puts the WEST/DOWN/NORTH face at
|
||||||
|
* {@code from} and EAST/UP/SOUTH at {@code to} of its axis, and each inverted OTHER axis mirrors the quad and
|
||||||
|
* flips its winding — the faces point into the box and are back-face culled (seen through the cage: the far
|
||||||
|
* wall's inside). Nearest visible opaque face in {@code (lo, maxT]} wins; UVs follow the mirrored corners.
|
||||||
|
*/
|
||||||
|
private static boolean intersectInverted(EntityCube cube, double[] o, double[] d, double lo, double maxT, Hit out) {
|
||||||
|
double[] from = cube.from, to = cube.to;
|
||||||
|
double best = Double.POSITIVE_INFINITY;
|
||||||
|
for (Direction dir : DIRECTIONS) {
|
||||||
|
int a = dir.nx != 0 ? 0 : dir.ny != 0 ? 1 : 2;
|
||||||
|
if (Math.abs(d[a]) < EPS) continue;
|
||||||
|
boolean neg = dir.nx + dir.ny + dir.nz < 0;
|
||||||
|
int b = (a + 1) % 3, c = (a + 2) % 3;
|
||||||
|
int flips = (from[b] > to[b] ? 1 : 0) + (from[c] > to[c] ? 1 : 0);
|
||||||
|
double sign = (neg ? -1 : 1) * (flips % 2 == 0 ? 1 : -1) * (cube.mirrored ? -1 : 1);
|
||||||
|
if (sign * d[a] >= 0) continue; // back face
|
||||||
|
double t = ((neg ? from[a] : to[a]) - o[a]) / d[a];
|
||||||
|
if (!(t > lo) || t > maxT || t >= best) continue;
|
||||||
|
double pb = o[b] + d[b] * t, pc = o[c] + d[c] * t;
|
||||||
|
if (pb < Math.min(from[b], to[b]) - EPS || pb > Math.max(from[b], to[b]) + EPS) continue;
|
||||||
|
if (pc < Math.min(from[c], to[c]) - EPS || pc > Math.max(from[c], to[c]) + EPS) continue;
|
||||||
|
Face face = cube.faces[dir.ordinal()];
|
||||||
|
if (face == null) continue;
|
||||||
|
double fx = signedFrac(o[0] + d[0] * t, from[0], to[0]);
|
||||||
|
double fy = signedFrac(o[1] + d[1] * t, from[1], to[1]);
|
||||||
|
double fz = signedFrac(o[2] + d[2] * t, from[2], to[2]);
|
||||||
|
int color = face.sample(ElementIntersector.faceS(dir, fx, fy, fz), ElementIntersector.faceT(dir, fy, fz));
|
||||||
|
if (!ColorUtil.opaque(color)) continue;
|
||||||
|
best = t;
|
||||||
|
out.t = t;
|
||||||
|
out.color = color;
|
||||||
|
out.axis = a;
|
||||||
|
out.neg = neg;
|
||||||
|
}
|
||||||
|
return best < Double.POSITIVE_INFINITY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Position between {@code from} (0) and {@code to} (1), also when {@code from > to}; clamped to 0..1. */
|
||||||
|
private static double signedFrac(double v, double from, double to) {
|
||||||
|
double span = to - from;
|
||||||
|
if (Math.abs(span) < 1e-9) return 0;
|
||||||
|
double f = (v - from) / span;
|
||||||
|
return f < 0 ? 0 : Math.min(f, 1);
|
||||||
|
}
|
||||||
|
|
||||||
/** The result object for a hit of {@code cube} recorded by {@link #intersect} (world point + normal). */
|
/** The result object for a hit of {@code cube} recorded by {@link #intersect} (world point + normal). */
|
||||||
public static FaceHit toFaceHit(
|
public static FaceHit toFaceHit(
|
||||||
EntityCube cube, Hit h, double ox, double oy, double oz, double dx, double dy, double dz) {
|
EntityCube cube, Hit h, double ox, double oy, double oz, double dx, double dy, double dz) {
|
||||||
|
|||||||
@@ -43,7 +43,10 @@ public final class ObjectBaker implements EntityBaker<ObjectState> {
|
|||||||
case ITEM -> {
|
case ITEM -> {
|
||||||
Affine at = s.toWorld();
|
Affine at = s.toWorld();
|
||||||
if (s.dropped()) {
|
if (s.dropped()) {
|
||||||
double lift = 0.1 + 0.25 * items.displayScaleY(s.item(), s.context());
|
// ItemEntityRenderer: T(0, bob + 0.0625 - modelBox.minY), bob = sin(age/10 + offset)·0.1 + 0.1
|
||||||
|
// frozen at phase 0: the ground model's lowest point floats 0.1625 above the entity position.
|
||||||
|
double[] box = items.modelBounds(s.item(), s.context());
|
||||||
|
double lift = 0.1 + 0.0625 - (box == null ? 0 : box[1]);
|
||||||
at = Affine.translation(0, lift, 0).mul(at);
|
at = Affine.translation(0, lift, 0).mul(at);
|
||||||
}
|
}
|
||||||
items.bake(s.item(), s.context(), false, at, cubes);
|
items.bake(s.item(), s.context(), false, at, cubes);
|
||||||
|
|||||||
@@ -424,18 +424,28 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
|
|||||||
* (one geometry, no coplanar layers).
|
* (one geometry, no coplanar layers).
|
||||||
*/
|
*/
|
||||||
private int[][] bannerFlag(int[][] base, BlockEntityState s) {
|
private int[][] bannerFlag(int[][] base, BlockEntityState s) {
|
||||||
int baseColor = s.baseColorArgb();
|
return patterned(base, "entity/banner/", s.baseColorArgb(), s.patterns());
|
||||||
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) {
|
/**
|
||||||
|
* BannerRenderer.submitPatterns composited into one texture: {@code base} (banner_base / shield_base), the
|
||||||
|
* {@code <dir>base} mask dyed with {@code baseColor} (0 = none), then the first 16 pattern masks
|
||||||
|
* ({@code <dir><key>}) dyed with their colours. {@code dir} = {@code entity/banner/} or {@code entity/shield/}
|
||||||
|
* (shields, ShieldSpecialRenderer). Cached per input.
|
||||||
|
*/
|
||||||
|
public int[][] patterned(int[][] base, String dir, int baseColor, List<BlockEntityState.BannerPattern> patterns) {
|
||||||
|
List<BlockEntityState.BannerPattern> pats = List.copyOf(patterns.subList(0, Math.min(16, patterns.size())));
|
||||||
|
return derived.get(
|
||||||
|
DerivedTextureCache.key("banner", base, dir, baseColor, pats),
|
||||||
|
() -> buildPatterned(base, dir, baseColor, pats));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int[][] buildPatterned(
|
||||||
|
int[][] base, String dir, int baseColor, List<BlockEntityState.BannerPattern> patterns) {
|
||||||
int[][] out = TextureOps.deepCopy(base);
|
int[][] out = TextureOps.deepCopy(base);
|
||||||
if (baseColor != 0) overlayMask(out, "entity/banner/base", baseColor);
|
if (baseColor != 0) overlayMask(out, dir + "base", baseColor);
|
||||||
for (BlockEntityState.BannerPattern pat : patterns) {
|
for (BlockEntityState.BannerPattern pat : patterns) {
|
||||||
overlayMask(out, "entity/banner/" + pat.patternKey(), pat.colorArgb());
|
overlayMask(out, dir + pat.patternKey(), pat.colorArgb());
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -405,7 +405,8 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Held items (vanilla ItemInHandLayer: hand frame · rotX(-90) · rotY(180) · T(±1/16, 0.125, -0.625),
|
* Held items (vanilla ItemInHandLayer: hand frame · rotX(-90) · rotY(180) · T(±1/16, 0.125, -0.625), babies
|
||||||
|
* T(0, 1/16, -4.5/16),
|
||||||
* third-person display context), crossed-arms items (villagers/witches: root · T(0, 0.4, -0.4) ·
|
* third-person display context), crossed-arms items (villagers/witches: root · T(0, 0.4, -0.4) ·
|
||||||
* rotX(180), ground context) and head items (CustomHeadLayer: blocks/items at 0.625 with the "head"
|
* rotX(180), ground context) and head items (CustomHeadLayer: blocks/items at 0.625 with the "head"
|
||||||
* context, skulls at 1.1875 through the block-entity model). Frames are Java-model frames (blocks).
|
* context, skulls at 1.1875 through the block-entity model). Frames are Java-model frames (blocks).
|
||||||
@@ -421,10 +422,15 @@ public final class CemBaker implements EntityBaker<EntityState> {
|
|||||||
if (item == null) continue;
|
if (item == null) continue;
|
||||||
Affine arm = frames.get(left ? "left_arm" : "right_arm");
|
Affine arm = frames.get(left ? "left_arm" : "right_arm");
|
||||||
if (arm != null && !crossed && !(illager && !frames.containsKey(d + "right_arm"))) {
|
if (arm != null && !crossed && !(illager && !frames.containsKey(d + "right_arm"))) {
|
||||||
|
// ItemInHandLayer.useBabyOffset (babies except armor stands): (0, 1, -4.5) px instead of (±1, 2, -10).
|
||||||
|
boolean babyOffset = s.baby() && !s.typeKey().equals("armor_stand");
|
||||||
Affine x = arm.mul(CemGeometry.JAVA_LOCAL)
|
Affine x = arm.mul(CemGeometry.JAVA_LOCAL)
|
||||||
.mul(Affine.rotX(Math.toRadians(-90)))
|
.mul(Affine.rotX(Math.toRadians(-90)))
|
||||||
.mul(Affine.rotY(Math.PI))
|
.mul(Affine.rotY(Math.PI))
|
||||||
.mul(Affine.translation((left ? -1 : 1) / 16.0, 0.125, -0.625));
|
.mul(
|
||||||
|
babyOffset
|
||||||
|
? Affine.translation(0, 1 / 16.0, -4.5 / 16)
|
||||||
|
: Affine.translation((left ? -1 : 1) / 16.0, 0.125, -0.625));
|
||||||
items.bake(item, left ? "thirdperson_lefthand" : "thirdperson_righthand", left, x, out);
|
items.bake(item, left ? "thirdperson_lefthand" : "thirdperson_righthand", left, x, out);
|
||||||
} else if (crossed && !left && frames.containsKey("root")) {
|
} else if (crossed && !left && frames.containsKey("root")) {
|
||||||
Affine x = frames.get("root")
|
Affine x = frames.get("root")
|
||||||
|
|||||||
@@ -22,14 +22,21 @@ import eu.mhsl.minecraft.pixelpics.render.entity.TextureOps;
|
|||||||
import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTint;
|
import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTint;
|
||||||
import eu.mhsl.minecraft.pixelpics.render.tint.TintResolver;
|
import eu.mhsl.minecraft.pixelpics.render.tint.TintResolver;
|
||||||
import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil;
|
import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil;
|
||||||
|
import eu.mhsl.minecraft.pixelpics.render.util.SpecialDates;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.IdentityHashMap;
|
import java.util.IdentityHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.TimeZone;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import org.bukkit.DyeColor;
|
||||||
import org.bukkit.block.data.BlockData;
|
import org.bukkit.block.data.BlockData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,6 +84,7 @@ public final class ItemGeometry {
|
|||||||
List<String> layers, // generated-item layer textures (layer0, layer1, …); empty if not generated
|
List<String> layers, // generated-item layer textures (layer0, layer1, …); empty if not generated
|
||||||
List<Integer> tints, // ARGB per layer (0 = none)
|
List<Integer> tints, // ARGB per layer (0 = none)
|
||||||
String special, // special model type ("shield", "trident", "chest", "head", …) or null
|
String special, // special model type ("shield", "trident", "chest", "head", …) or null
|
||||||
|
JsonObject specialModel, // the special model's own fields (chest texture/chest_type, …) or null
|
||||||
Affine specialTransform) {} // item-model transformation for special models (T·Rl·S·Rr) or null
|
Affine specialTransform) {} // item-model transformation for special models (T·Rl·S·Rr) or null
|
||||||
|
|
||||||
// --- public API --------------------------------------------------------------------------------
|
// --- public API --------------------------------------------------------------------------------
|
||||||
@@ -90,7 +98,10 @@ public final class ItemGeometry {
|
|||||||
if (info.special != null) bakeSpecial(item, info, m, out);
|
if (info.special != null) bakeSpecial(item, info, m, out);
|
||||||
if (out.size() == before && !info.layers.isEmpty()) bakeGenerated(info, m, out);
|
if (out.size() == before && !info.layers.isEmpty()) bakeGenerated(info, m, out);
|
||||||
if (out.size() == before && item.block() != null) bakeBlock(item.block(), m, out);
|
if (out.size() == before && item.block() != null) bakeBlock(item.block(), m, out);
|
||||||
if (out.size() == before && item.blockEntity() != null) bakeBlockEntity(item.blockEntity(), m, out);
|
// Placed block-entity fallback for block items only (a shield carries its patterns as a banner state).
|
||||||
|
if (out.size() == before && item.block() != null && item.blockEntity() != null) {
|
||||||
|
bakeBlockEntity(item.blockEntity(), m, out);
|
||||||
|
}
|
||||||
if (out.size() == before) {
|
if (out.size() == before) {
|
||||||
int[][] sprite = sprite(item.id());
|
int[][] sprite = sprite(item.id());
|
||||||
if (sprite != null) addExtruded(sprite, m, out);
|
if (sprite != null) addExtruded(sprite, m, out);
|
||||||
@@ -130,13 +141,6 @@ public final class ItemGeometry {
|
|||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The display transform's Y scale for a context (vanilla lifts dropped items by 0.25·scaleY). */
|
|
||||||
public double displayScaleY(ItemSpec item, String context) {
|
|
||||||
if (item == null) return 1;
|
|
||||||
Info info = info(item.id(), context);
|
|
||||||
return info.display == null ? 1 : info.display[7];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A block's model elements in the block cube [0,1]³ (tinted with plains colours). */
|
/** A block's model elements in the block cube [0,1]³ (tinted with plains colours). */
|
||||||
public void bakeBlock(BlockData data, Affine toWorld, List<EntityCube> out) {
|
public void bakeBlock(BlockData data, Affine toWorld, List<EntityCube> out) {
|
||||||
if (data == null) return;
|
if (data == null) return;
|
||||||
@@ -174,30 +178,34 @@ public final class ItemGeometry {
|
|||||||
// --- item definition / model resolution --------------------------------------------------------
|
// --- item definition / model resolution --------------------------------------------------------
|
||||||
|
|
||||||
private Info info(String id, String context) {
|
private Info info(String id, String context) {
|
||||||
return infos.computeIfAbsent(id + "#" + context, k -> {
|
// Keyed by date too: minecraft:local_time selects (christmas chest items) change with the day.
|
||||||
|
ZonedDateTime now = SpecialDates.now();
|
||||||
|
return infos.computeIfAbsent(id + "#" + context + "#" + now.toLocalDate(), k -> {
|
||||||
try {
|
try {
|
||||||
return resolve(id, context);
|
return resolve(id, context, now);
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
// Unexpected item-definition format (newer version?): fall back to the plain sprite.
|
// Unexpected item-definition format (newer version?): fall back to the plain sprite.
|
||||||
return new Info(null, List.of(), List.of(), null, null);
|
return new Info(null, List.of(), List.of(), null, null, null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private Info resolve(String id, String context) {
|
private Info resolve(String id, String context, ZonedDateTime now) {
|
||||||
String modelRef = null, special = null;
|
String modelRef = null, special = null;
|
||||||
|
JsonObject specialModel = null;
|
||||||
Affine specialXf = null;
|
Affine specialXf = null;
|
||||||
List<Integer> tints = new ArrayList<>();
|
List<Integer> tints = new ArrayList<>();
|
||||||
Optional<JsonObject> def = reader.readJsonObject("assets/minecraft/items/" + id + ".json");
|
Optional<JsonObject> def = reader.readJsonObject("assets/minecraft/items/" + id + ".json");
|
||||||
if (def.isPresent() && def.get().has("model")) {
|
if (def.isPresent() && def.get().has("model")) {
|
||||||
Affine[] outer = {null};
|
Affine[] outer = {null};
|
||||||
JsonObject node = pick(def.get().getAsJsonObject("model"), context, outer);
|
JsonObject node = pick(def.get().getAsJsonObject("model"), context, now, outer);
|
||||||
if (node != null) {
|
if (node != null) {
|
||||||
String type = str(node, "type");
|
String type = str(node, "type");
|
||||||
if (type.endsWith("special")) {
|
if (type.endsWith("special")) {
|
||||||
modelRef = str(node, "base");
|
modelRef = str(node, "base");
|
||||||
JsonObject sm = node.has("model") ? node.getAsJsonObject("model") : null;
|
JsonObject sm = node.has("model") ? node.getAsJsonObject("model") : null;
|
||||||
special = sm == null ? null : str(sm, "type").replace("minecraft:", "");
|
special = sm == null ? null : str(sm, "type").replace("minecraft:", "");
|
||||||
|
specialModel = sm;
|
||||||
// Like ItemModel.Unbaked.bake: the transformations of the enclosing select/condition nodes
|
// Like ItemModel.Unbaked.bake: the transformations of the enclosing select/condition nodes
|
||||||
// compose with the special's own (shield/trident flips, copper golem statue placement).
|
// compose with the special's own (shield/trident flips, copper golem statue placement).
|
||||||
specialXf = outer[0];
|
specialXf = outer[0];
|
||||||
@@ -249,14 +257,14 @@ public final class ItemGeometry {
|
|||||||
if (generated) {
|
if (generated) {
|
||||||
for (int i = 0; tex.containsKey("layer" + i); i++) layers.add(resolveTex(tex, tex.get("layer" + i)));
|
for (int i = 0; tex.containsKey("layer" + i); i++) layers.add(resolveTex(tex, tex.get("layer" + i)));
|
||||||
}
|
}
|
||||||
return new Info(display, layers, tints, special, specialXf);
|
return new Info(display, layers, tints, special, specialModel, specialXf);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Walks an item-model node to the concrete model/special for a display context; {@code outer[0]}
|
* Walks an item-model node to the concrete model/special for a display context; {@code outer[0]}
|
||||||
* accumulates the {@code transformation}s of the nodes passed on the way (null = none).
|
* accumulates the {@code transformation}s of the nodes passed on the way (null = none).
|
||||||
*/
|
*/
|
||||||
private static JsonObject pick(JsonObject node, String context, Affine[] outer) {
|
private static JsonObject pick(JsonObject node, String context, ZonedDateTime now, Affine[] outer) {
|
||||||
for (int depth = 0; node != null && depth < 16; depth++) {
|
for (int depth = 0; node != null && depth < 16; depth++) {
|
||||||
String type = str(node, "type").replace("minecraft:", "");
|
String type = str(node, "type").replace("minecraft:", "");
|
||||||
if (!type.equals("model") && !type.equals("special") && node.has("transformation")) {
|
if (!type.equals("model") && !type.equals("special") && node.has("transformation")) {
|
||||||
@@ -270,10 +278,14 @@ public final class ItemGeometry {
|
|||||||
case "condition" -> node = obj(node, "on_false");
|
case "condition" -> node = obj(node, "on_false");
|
||||||
case "select" -> {
|
case "select" -> {
|
||||||
JsonObject chosen = null;
|
JsonObject chosen = null;
|
||||||
if (str(node, "property").endsWith("display_context") && node.has("cases")) {
|
String property = str(node, "property");
|
||||||
|
String value = property.endsWith("display_context")
|
||||||
|
? context
|
||||||
|
: property.endsWith("local_time") ? localTime(node, now) : null;
|
||||||
|
if (value != null && node.has("cases")) {
|
||||||
for (JsonElement c : node.getAsJsonArray("cases")) {
|
for (JsonElement c : node.getAsJsonArray("cases")) {
|
||||||
JsonObject co = c.getAsJsonObject();
|
JsonObject co = c.getAsJsonObject();
|
||||||
if (matches(co.get("when"), context)) {
|
if (matches(co.get("when"), value)) {
|
||||||
chosen = obj(co, "model");
|
chosen = obj(co, "model");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -304,6 +316,23 @@ public final class ItemGeometry {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@code minecraft:local_time} select value (LocalTime property: SimpleDateFormat {@code pattern} with
|
||||||
|
* optional {@code locale} and {@code time_zone}) for {@code now}; null when the format is invalid.
|
||||||
|
*/
|
||||||
|
private static String localTime(JsonObject node, ZonedDateTime now) {
|
||||||
|
try {
|
||||||
|
String locale = str(node, "locale");
|
||||||
|
SimpleDateFormat f = new SimpleDateFormat(
|
||||||
|
str(node, "pattern"), locale.isEmpty() ? Locale.ROOT : Locale.forLanguageTag(locale));
|
||||||
|
String tz = str(node, "time_zone");
|
||||||
|
f.setTimeZone(tz.isEmpty() ? TimeZone.getTimeZone(now.getZone()) : TimeZone.getTimeZone(tz));
|
||||||
|
return f.format(Date.from(now.toInstant()));
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean matches(JsonElement when, String context) {
|
private static boolean matches(JsonElement when, String context) {
|
||||||
if (when == null) return false;
|
if (when == null) return false;
|
||||||
if (when.isJsonArray()) {
|
if (when.isJsonArray()) {
|
||||||
@@ -384,33 +413,78 @@ public final class ItemGeometry {
|
|||||||
// --- geometry ------------------------------------------------------------------------------------
|
// --- geometry ------------------------------------------------------------------------------------
|
||||||
|
|
||||||
private void bakeSpecial(ItemSpec item, Info info, Affine m, List<EntityCube> out) {
|
private void bakeSpecial(ItemSpec item, Info info, Affine m, List<EntityCube> out) {
|
||||||
|
Affine xf = info.specialTransform != null ? m.mul(info.specialTransform) : m;
|
||||||
switch (info.special) {
|
switch (info.special) {
|
||||||
case "shield", "trident" -> {
|
case "shield" -> {
|
||||||
// Entity model in Java model space; the item definition's transformation flips it (1,-1,-1).
|
// ShieldSpecialRenderer: shield_base_nopattern, or (banner patterns / base colour present)
|
||||||
boolean shield = info.special.equals("shield");
|
// shield_base + the dyed shield pattern masks (base colour default white); the item definition's
|
||||||
CemModelLoader.CemModel model = models.vanilla(shield ? "layer/shield#main" : "trident");
|
// transformation flips the model (1,-1,-1).
|
||||||
if (model == null) model = models.vanilla("layer/" + info.special + "#main");
|
CemModelLoader.CemModel model = models.vanilla("layer/shield#main");
|
||||||
int[][] tex = textures.get(ResourceLocation.parse(
|
int[][] tex = shieldTexture(item.blockEntity());
|
||||||
shield ? "entity/shield/shield_base_nopattern" : "entity/trident/trident"))
|
if (model != null && tex != null) bakeEntityModel(model, tex, xf, out);
|
||||||
|
}
|
||||||
|
case "trident" -> {
|
||||||
|
CemModelLoader.CemModel model = models.vanilla("trident");
|
||||||
|
if (model == null) model = models.vanilla("layer/trident#main");
|
||||||
|
int[][] tex = textures.get(ResourceLocation.parse("entity/trident/trident"))
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
if (model == null || tex == null) return;
|
if (model != null && tex != null) bakeEntityModel(model, tex, xf, out);
|
||||||
Affine xf = info.specialTransform != null ? m.mul(info.specialTransform) : m;
|
}
|
||||||
Affine pre = xf.mul(Affine.scale(1 / 16.0)).mul(CemGeometry.CEM_TO_JAVA);
|
case "chest" -> {
|
||||||
for (CemGeometry.Baked b : CemGeometry.bakeModel(model, tex, pre, Set.of())) {
|
// ChestSpecialRenderer: the chest model of the item's chest_type with the item definition's
|
||||||
out.add(new EntityCube(b.from(), b.to(), b.faces(), b.world()));
|
// texture (Sheets.CHEST_MAPPER: entity/chest/<texture>; christmas via the local_time select).
|
||||||
|
if (!bakeChest(info.specialModel, xf, out) && item.blockEntity() != null && blockEntities != null) {
|
||||||
|
blockEntities.bakeModel(item.blockEntity(), xf, out);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default -> {
|
default -> {
|
||||||
// Block-entity specials (chest, head, banner, shulker box, conduit, pot, …): like the vanilla
|
// Block-entity specials (head, banner, shulker box, conduit, pot, …): like the vanilla special
|
||||||
// special renderers, the bare model (no block pose) placed by the item's transformation.
|
// renderers, the bare model (no block pose) placed by the item's transformation.
|
||||||
if (item.blockEntity() != null && blockEntities != null) {
|
if (item.blockEntity() != null && blockEntities != null) {
|
||||||
Affine xf = info.specialTransform != null ? m.mul(info.specialTransform) : m;
|
|
||||||
blockEntities.bakeModel(item.blockEntity(), xf, out);
|
blockEntities.bakeModel(item.blockEntity(), xf, out);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** An entity model (Java model space, px) through {@code xf}. */
|
||||||
|
private static void bakeEntityModel(CemModelLoader.CemModel model, int[][] tex, Affine xf, List<EntityCube> out) {
|
||||||
|
Affine pre = xf.mul(Affine.scale(1 / 16.0)).mul(CemGeometry.CEM_TO_JAVA);
|
||||||
|
for (CemGeometry.Baked b : CemGeometry.bakeModel(model, tex, pre, Set.of())) {
|
||||||
|
out.add(new EntityCube(b.from(), b.to(), b.faces(), b.world()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The chest special model from its JSON ({@code texture}, {@code chest_type}); false when unresolvable. */
|
||||||
|
private boolean bakeChest(JsonObject sm, Affine xf, List<EntityCube> out) {
|
||||||
|
if (sm == null || str(sm, "texture").isEmpty()) return false;
|
||||||
|
String layer =
|
||||||
|
switch (str(sm, "chest_type").replace("minecraft:", "")) {
|
||||||
|
case "left" -> "layer/double_chest_left#main";
|
||||||
|
case "right" -> "layer/double_chest_right#main";
|
||||||
|
default -> "layer/chest#main";
|
||||||
|
};
|
||||||
|
CemModelLoader.CemModel model = models.vanilla(layer);
|
||||||
|
ResourceLocation id = ResourceLocation.parse(str(sm, "texture"));
|
||||||
|
int[][] tex = textures.get(ResourceLocation.parse(id.namespace() + ":entity/chest/" + id.path()))
|
||||||
|
.orElse(null);
|
||||||
|
if (model == null || tex == null) return false;
|
||||||
|
bakeEntityModel(model, tex, xf, out);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The shield texture for the stack's patterns ({@code BANNER} state from the item meta; null = none). */
|
||||||
|
private int[][] shieldTexture(BlockEntityState patterns) {
|
||||||
|
boolean patterned = patterns != null
|
||||||
|
&& (patterns.baseColorArgb() != 0 || !patterns.patterns().isEmpty());
|
||||||
|
int[][] base = textures.get(ResourceLocation.parse(
|
||||||
|
patterned ? "entity/shield/shield_base" : "entity/shield/shield_base_nopattern"))
|
||||||
|
.orElse(null);
|
||||||
|
if (!patterned || base == null || blockEntities == null) return base;
|
||||||
|
int baseColor = patterns.baseColorArgb() != 0 ? patterns.baseColorArgb() : ColorUtil.dyeArgb(DyeColor.WHITE, 0);
|
||||||
|
return blockEntities.patterned(base, "entity/shield/", baseColor, patterns.patterns());
|
||||||
|
}
|
||||||
|
|
||||||
private void bakeGenerated(Info info, Affine m, List<EntityCube> out) {
|
private void bakeGenerated(Info info, Affine m, List<EntityCube> out) {
|
||||||
// Cached per Info (one per item+context): a stable texture identity keeps the extrusion cache bounded.
|
// Cached per Info (one per item+context): a stable texture identity keeps the extrusion cache bounded.
|
||||||
int[][] composed = composedSprites
|
int[][] composed = composedSprites
|
||||||
|
|||||||
+36
@@ -44,6 +44,7 @@ import org.bukkit.entity.EntitySnapshot;
|
|||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
import org.bukkit.inventory.meta.BannerMeta;
|
import org.bukkit.inventory.meta.BannerMeta;
|
||||||
import org.bukkit.inventory.meta.ItemMeta;
|
import org.bukkit.inventory.meta.ItemMeta;
|
||||||
|
import org.bukkit.inventory.meta.ShieldMeta;
|
||||||
import org.bukkit.inventory.meta.SkullMeta;
|
import org.bukkit.inventory.meta.SkullMeta;
|
||||||
import org.bukkit.profile.PlayerProfile;
|
import org.bukkit.profile.PlayerProfile;
|
||||||
import org.bukkit.util.Vector;
|
import org.bukkit.util.Vector;
|
||||||
@@ -130,6 +131,41 @@ public final class BlockEntitySnapshotBuilder {
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A shield's banner data (ShieldMeta: base colour + patterns) as a {@code BANNER} state at cell 0 for the
|
||||||
|
* shield special model; null without patterns and base colour (plain shield).
|
||||||
|
*/
|
||||||
|
public static BlockEntityState shieldPatterns(ItemMeta meta) {
|
||||||
|
try {
|
||||||
|
if (!(meta instanceof ShieldMeta shield)) return null;
|
||||||
|
DyeColor baseColor = shield.getBaseColor();
|
||||||
|
List<BannerPattern> pats = new ArrayList<>();
|
||||||
|
for (Pattern p : shield.getPatterns()) {
|
||||||
|
pats.add(new BannerPattern(patternKey(p), ColorUtil.dyeArgb(p.getColor(), 0xFFFFFFFF)));
|
||||||
|
}
|
||||||
|
if (baseColor == null && pats.isEmpty()) return null;
|
||||||
|
return new BlockEntityState(
|
||||||
|
Kind.BANNER,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
ColorUtil.dyeArgb(baseColor, 0),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
pats,
|
||||||
|
List.of(),
|
||||||
|
null,
|
||||||
|
null);
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
return null; // API drift: plain shield
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static BlockEntityState copy(BlockEntityState s, String skinUrl, List<BannerPattern> patterns) {
|
private static BlockEntityState copy(BlockEntityState s, String skinUrl, List<BannerPattern> patterns) {
|
||||||
return new BlockEntityState(
|
return new BlockEntityState(
|
||||||
s.kind(),
|
s.kind(),
|
||||||
|
|||||||
+2
-1
@@ -77,7 +77,8 @@ public final class DecorationSnapshotBuilder {
|
|||||||
map,
|
map,
|
||||||
frame.getRotation().ordinal(),
|
frame.getRotation().ordinal(),
|
||||||
glow,
|
glow,
|
||||||
!frame.isVisible());
|
!frame.isVisible(),
|
||||||
|
map != null ? MapPixels.markers(stack) : List.of());
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ public final class ItemSpecs {
|
|||||||
// Head skins / banner patterns live on the stack, not the material (only those kinds pay for the meta).
|
// Head skins / banner patterns live on the stack, not the material (only those kinds pay for the meta).
|
||||||
if (be != null && ("player".equals(be.headType()) || be.kind() == BlockEntityState.Kind.BANNER)) {
|
if (be != null && ("player".equals(be.headType()) || be.kind() == BlockEntityState.Kind.BANNER)) {
|
||||||
be = BlockEntitySnapshotBuilder.withItemMeta(be, stack.getItemMeta());
|
be = BlockEntitySnapshotBuilder.withItemMeta(be, stack.getItemMeta());
|
||||||
|
} else if (m == Material.SHIELD) {
|
||||||
|
// Shield banner patterns + base colour (ShieldSpecialRenderer), carried as a BANNER state.
|
||||||
|
be = BlockEntitySnapshotBuilder.shieldPatterns(stack.getItemMeta());
|
||||||
}
|
}
|
||||||
return new ItemSpec(m.getKey().getKey(), b.data, be, glint);
|
return new ItemSpec(m.getKey().getKey(), b.data, be, glint);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
package eu.mhsl.minecraft.pixelpics.render.snapshot;
|
package eu.mhsl.minecraft.pixelpics.render.snapshot;
|
||||||
|
|
||||||
|
import eu.mhsl.minecraft.pixelpics.render.entity.DecorationState;
|
||||||
import eu.mhsl.minecraft.pixelpics.utils.ImageMapRenderer;
|
import eu.mhsl.minecraft.pixelpics.utils.ImageMapRenderer;
|
||||||
import eu.mhsl.minecraft.pixelpics.utils.MapColorPalette;
|
import eu.mhsl.minecraft.pixelpics.utils.MapColorPalette;
|
||||||
import eu.mhsl.minecraft.pixelpics.utils.MapManager;
|
import eu.mhsl.minecraft.pixelpics.utils.MapManager;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
import org.bukkit.inventory.meta.MapMeta;
|
import org.bukkit.inventory.meta.MapMeta;
|
||||||
import org.bukkit.map.MapRenderer;
|
import org.bukkit.map.MapRenderer;
|
||||||
@@ -59,17 +63,65 @@ public final class MapPixels {
|
|||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static volatile Field worldMapField, colorsField;
|
/**
|
||||||
|
* The map's decorations an item frame shows (MapRenderer with showOnlyFrame: MapDecorationType.showOnItemFrame
|
||||||
|
* — banners, frame markers, structure/target markers; never players), in the map data's order. Only for maps
|
||||||
|
* drawn by the vanilla renderer (CraftMapRenderer sends {@code MapItemSavedData.decorations} as cursors):
|
||||||
|
* PixelPics photo maps and other plugin-rendered maps get none.
|
||||||
|
*/
|
||||||
|
public static List<DecorationState.MapMarker> markers(ItemStack stack) {
|
||||||
|
try {
|
||||||
|
if (!(stack.getItemMeta() instanceof MapMeta meta) || !meta.hasMapView()) return List.of();
|
||||||
|
MapView view = meta.getMapView();
|
||||||
|
if (view == null || MapManager.isManaged(view.getId())) return List.of();
|
||||||
|
boolean vanilla = false;
|
||||||
|
for (MapRenderer r : view.getRenderers()) {
|
||||||
|
if (r instanceof ImageMapRenderer) return List.of();
|
||||||
|
if (r.getClass().getSimpleName().equals("CraftMapRenderer")) vanilla = true;
|
||||||
|
}
|
||||||
|
if (!vanilla) return List.of();
|
||||||
|
Object data = worldMap(view);
|
||||||
|
if (data == null) return List.of();
|
||||||
|
Field f = decorationsField;
|
||||||
|
if (f == null || f.getDeclaringClass() != data.getClass()) {
|
||||||
|
f = data.getClass().getDeclaredField("decorations");
|
||||||
|
f.setAccessible(true);
|
||||||
|
decorationsField = f;
|
||||||
|
}
|
||||||
|
if (!(f.get(data) instanceof Map<?, ?> decorations) || decorations.isEmpty()) return List.of();
|
||||||
|
List<DecorationState.MapMarker> out = new ArrayList<>();
|
||||||
|
for (Object d : decorations.values()) {
|
||||||
|
Class<?> c = d.getClass();
|
||||||
|
if (!(Boolean) c.getMethod("renderOnFrame").invoke(d)) continue;
|
||||||
|
String sprite = String.valueOf(c.getMethod("getSpriteLocation").invoke(d));
|
||||||
|
String asset = sprite.substring(sprite.indexOf(':') + 1);
|
||||||
|
out.add(new DecorationState.MapMarker(
|
||||||
|
asset,
|
||||||
|
(Byte) c.getMethod("x").invoke(d),
|
||||||
|
(Byte) c.getMethod("y").invoke(d),
|
||||||
|
(Byte) c.getMethod("rot").invoke(d)));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
} catch (Throwable t) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** CraftMapView#worldMap → MapItemSavedData#colors (what the vanilla CraftMapRenderer sends). */
|
private static volatile Field worldMapField, colorsField, decorationsField;
|
||||||
private static byte[] worldMapColors(MapView view) throws ReflectiveOperationException {
|
|
||||||
|
private static Object worldMap(MapView view) throws ReflectiveOperationException {
|
||||||
Field wm = worldMapField;
|
Field wm = worldMapField;
|
||||||
if (wm == null || wm.getDeclaringClass() != view.getClass()) {
|
if (wm == null || wm.getDeclaringClass() != view.getClass()) {
|
||||||
wm = view.getClass().getDeclaredField("worldMap");
|
wm = view.getClass().getDeclaredField("worldMap");
|
||||||
wm.setAccessible(true);
|
wm.setAccessible(true);
|
||||||
worldMapField = wm;
|
worldMapField = wm;
|
||||||
}
|
}
|
||||||
Object data = wm.get(view);
|
return wm.get(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** CraftMapView#worldMap → MapItemSavedData#colors (what the vanilla CraftMapRenderer sends). */
|
||||||
|
private static byte[] worldMapColors(MapView view) throws ReflectiveOperationException {
|
||||||
|
Object data = worldMap(view);
|
||||||
if (data == null) return null;
|
if (data == null) return null;
|
||||||
Field c = colorsField;
|
Field c = colorsField;
|
||||||
if (c == null || c.getDeclaringClass() != data.getClass()) {
|
if (c == null || c.getDeclaringClass() != data.getClass()) {
|
||||||
|
|||||||
+6
-3
@@ -70,7 +70,8 @@ public final class ObjectSnapshotBuilder {
|
|||||||
}
|
}
|
||||||
if (d instanceof ItemDisplay id) {
|
if (d instanceof ItemDisplay id) {
|
||||||
String ctx = id.getItemDisplayTransform().name().toLowerCase(java.util.Locale.ROOT);
|
String ctx = id.getItemDisplayTransform().name().toLowerCase(java.util.Locale.ROOT);
|
||||||
return item(base, id.getItemStack(), ctx, false);
|
// ItemDisplayRenderer.submitInner turns the item by 180° about Y before submitting it.
|
||||||
|
return item(base.mul(Affine.rotY(Math.PI)), id.getItemStack(), ctx, false);
|
||||||
}
|
}
|
||||||
if (d instanceof TextDisplay td) {
|
if (d instanceof TextDisplay td) {
|
||||||
return text(base, td);
|
return text(base, td);
|
||||||
@@ -84,8 +85,10 @@ public final class ObjectSnapshotBuilder {
|
|||||||
return block(at.mul(Affine.translation(-0.5, 0, -0.5)), Material.TNT.createBlockData());
|
return block(at.mul(Affine.translation(-0.5, 0, -0.5)), Material.TNT.createBlockData());
|
||||||
}
|
}
|
||||||
if (e instanceof Item it) {
|
if (e instanceof Item it) {
|
||||||
// Dropped item: vanilla GROUND display transform, lifted by the baker; a fixed per-entity spin.
|
// Dropped item: vanilla GROUND display transform, lifted by the baker. The spin (vanilla: age/20 +
|
||||||
double spin = (e.getUniqueId().getLeastSignificantBits() & 0xFFFF) / 65536.0 * 2 * Math.PI;
|
// a random bobOffset) is frozen per entity from its yaw, which ItemEntity also randomises at creation
|
||||||
|
// (so items differ like in vanilla, and a test can fix it with setRotation).
|
||||||
|
double spin = Math.toRadians(l.getYaw());
|
||||||
return item(at.mul(Affine.rotY(spin)), it.getItemStack(), "ground", true);
|
return item(at.mul(Affine.rotY(spin)), it.getItemStack(), "ground", true);
|
||||||
}
|
}
|
||||||
if (e instanceof Minecart mc) {
|
if (e instanceof Minecart mc) {
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package eu.mhsl.minecraft.pixelpics.render.util;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
import java.time.MonthDay;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vanilla {@code net.minecraft.util.SpecialDates} (decompiled 26.3): the server's local date decides the
|
||||||
|
* christmas chest textures (ChestRenderer.xmasTextures = {@code isExtendedChristmas()}, 24–26 December) and the
|
||||||
|
* {@code minecraft:local_time} item-model property (chest / trapped chest items).
|
||||||
|
*
|
||||||
|
* <p>Testing: the JVM system property {@value #TEST_DATE_PROPERTY} ({@code yyyy-MM-dd}, e.g.
|
||||||
|
* {@code -Dpixelpics.testDate=2026-12-25}) replaces today's date (time of day stays the current one). Never set
|
||||||
|
* it in production.
|
||||||
|
*/
|
||||||
|
public final class SpecialDates {
|
||||||
|
|
||||||
|
public static final String TEST_DATE_PROPERTY = "pixelpics.testDate";
|
||||||
|
|
||||||
|
private static final List<MonthDay> CHRISTMAS_RANGE =
|
||||||
|
List.of(MonthDay.of(12, 24), MonthDay.of(12, 25), MonthDay.of(12, 26));
|
||||||
|
|
||||||
|
private SpecialDates() {}
|
||||||
|
|
||||||
|
/** The current local date-time ({@code ZonedDateTime.now()}), or the test date at the current time of day. */
|
||||||
|
public static ZonedDateTime now() {
|
||||||
|
ZonedDateTime now = ZonedDateTime.now();
|
||||||
|
String test = System.getProperty(TEST_DATE_PROPERTY);
|
||||||
|
if (test == null || test.isBlank()) return now;
|
||||||
|
try {
|
||||||
|
return ZonedDateTime.of(LocalDate.parse(test.trim()), LocalTime.from(now), ZoneId.systemDefault());
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Vanilla {@code SpecialDates.isExtendedChristmas()}: today is 24, 25 or 26 December. */
|
||||||
|
public static boolean isExtendedChristmas() {
|
||||||
|
return CHRISTMAS_RANGE.contains(MonthDay.from(now()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -244,6 +244,36 @@ public class BlockEntityTestRender {
|
|||||||
decoScenes.put("item_frame_E", List.of(new DecorationState(DecorationState.Kind.ITEM_FRAME,
|
decoScenes.put("item_frame_E", List.of(new DecorationState(DecorationState.Kind.ITEM_FRAME,
|
||||||
0.9375, 0.0625, 0.0625, 1.0, 0.9375, 0.9375, DecorationState.Facing.EAST, null, new eu.mhsl.minecraft.pixelpics.render.entity.ItemSpec("diamond", null, null, false), null, 0, false, false)));
|
0.9375, 0.0625, 0.0625, 1.0, 0.9375, 0.9375, DecorationState.Facing.EAST, null, new eu.mhsl.minecraft.pixelpics.render.entity.ItemSpec("diamond", null, null, false), null, 0, false, false)));
|
||||||
|
|
||||||
|
// Shields (plain / red base / blue + creeper + border patterns), chests (christmas with
|
||||||
|
// -Dpixelpics.testDate=2026-12-25) and a decorated map in frames (map/decorations sprites as MapRenderer).
|
||||||
|
BlockEntityState shieldPats = new BlockEntityState(Kind.BANNER, 0, 0, 0, 0, null, false, null, 0xFF3C44AA, null, null, null,
|
||||||
|
List.of(new BannerPattern("creeper", 0xFFFED83D), new BannerPattern("border", 0xFFF9FFFE)), List.of(), null, null);
|
||||||
|
BlockEntityState shieldRed = new BlockEntityState(Kind.BANNER, 0, 0, 0, 0, null, false, null, 0xFFB02E26, null, null, null,
|
||||||
|
List.of(), List.of(), null, null);
|
||||||
|
String[] frameItems = {"shield", "shield_red", "shield_patterns", "chest", "trapped_chest", "ender_chest"};
|
||||||
|
for (String id : frameItems) {
|
||||||
|
ItemSpec spec = switch (id) {
|
||||||
|
case "shield_red" -> new ItemSpec("shield", null, shieldRed, false);
|
||||||
|
case "shield_patterns" -> new ItemSpec("shield", null, shieldPats, false);
|
||||||
|
default -> new ItemSpec(id, null, null, false);
|
||||||
|
};
|
||||||
|
decoScenes.put("item_frame_" + id, List.of(new DecorationState(DecorationState.Kind.ITEM_FRAME,
|
||||||
|
0.0625, 0.0625, 0, 0.9375, 0.9375, 0.0625, DecorationState.Facing.SOUTH, null, spec, null, 0, false, false)));
|
||||||
|
}
|
||||||
|
int[][] mapPx = new int[128][128];
|
||||||
|
for (int y = 0; y < 128; y++) for (int x = 0; x < 128; x++) mapPx[y][x] = ((x / 16 + y / 16) & 1) == 0 ? 0xFF7FB238 : 0xFF3F76E4;
|
||||||
|
List<DecorationState.MapMarker> markers = List.of(
|
||||||
|
new DecorationState.MapMarker("frame", 0, 0, 0),
|
||||||
|
new DecorationState.MapMarker("red_banner", -60, -50, 8),
|
||||||
|
new DecorationState.MapMarker("target_x", 70, 60, 0),
|
||||||
|
new DecorationState.MapMarker("frame", 40, -90, 4),
|
||||||
|
new DecorationState.MapMarker("blue_banner", -100, 90, 0));
|
||||||
|
for (int rot : new int[] {0, 1}) {
|
||||||
|
decoScenes.put("item_frame_map_markers_r" + rot, List.of(new DecorationState(DecorationState.Kind.ITEM_FRAME,
|
||||||
|
0.0625, 0.0625, 0, 0.9375, 0.9375, 0.0625, DecorationState.Facing.SOUTH, null,
|
||||||
|
new ItemSpec("filled_map", null, null, false), mapPx, rot, false, false, markers)));
|
||||||
|
}
|
||||||
|
|
||||||
List<BufferedImage> cells = new ArrayList<>();
|
List<BufferedImage> cells = new ArrayList<>();
|
||||||
List<String> names = new ArrayList<>(scenes.keySet());
|
List<String> names = new ArrayList<>(scenes.keySet());
|
||||||
for (String name : names) {
|
for (String name : names) {
|
||||||
|
|||||||
@@ -198,6 +198,160 @@ public class PPTest extends JavaPlugin {
|
|||||||
scenes.put("be_contents_close", new Scene("be_contents_close", this::beContents, 1.2, 1.8, 2.2, 1.5, 0.6, 0));
|
scenes.put("be_contents_close", new Scene("be_contents_close", this::beContents, 1.2, 1.8, 2.2, 1.5, 0.6, 0));
|
||||||
scenes.put("be_contents_beam", new Scene("be_contents_beam", this::beContents, 4, 6, 26, 10, 9, 6));
|
scenes.put("be_contents_beam", new Scene("be_contents_beam", this::beContents, 4, 6, 26, 10, 9, 6));
|
||||||
scenes.put("be_contents_portal", new Scene("be_contents_portal", this::beContents, 11.5, 3.6, -1.8, 11.5, 0, 1.2));
|
scenes.put("be_contents_portal", new Scene("be_contents_portal", this::beContents, 11.5, 3.6, -1.8, 11.5, 0, 1.2));
|
||||||
|
// Items in detail (decompiled 26.3 special renderers): shields (plain, base colour, banner patterns), chests,
|
||||||
|
// spawner/vault held by armor stands (+ a baby zombie: baby hand offset), item displays (fixed), item frames
|
||||||
|
// (rotation 0/1) and dropped (ground: lift = 0.1625 - model min y). items_inverted: spawner, trial spawner and
|
||||||
|
// vault as block, block display, item display and dropped item (inverted inner faces). xmas_chests: chests,
|
||||||
|
// double/trapped/ender/copper chests + chest items; render it once normally and once with the server
|
||||||
|
// started with -Dpixelpics.testDate=2026-12-25 (christmas textures for chests and trapped chests).
|
||||||
|
// frames_markers: map decorations on framed maps (banner markers via toggleBanner, frame/target markers,
|
||||||
|
// an invisible-on-frames player marker; rotations 0/1/3), a PixelPics photo map and an undecorated map.
|
||||||
|
scenes.put("items_detail", new Scene("items_detail", this::itemsDetail, 4.5, 3.2, 13, 4.5, 0.9, 2));
|
||||||
|
scenes.put("items_detail_close", new Scene("items_detail_close", this::itemsDetail, 2.2, 1.7, 7.4, 2.2, 0.9, 2));
|
||||||
|
scenes.put("items_detail_side", new Scene("items_detail_side", this::itemsDetail, -4.5, 2.4, 4.5, 5, 0.9, 3));
|
||||||
|
scenes.put("items_inverted", new Scene("items_inverted", this::itemsInverted, 3, 2.4, 7, 3, 0.6, 1.5));
|
||||||
|
scenes.put("xmas_chests", new Scene("xmas_chests", this::xmasChests, 6, 3.6, 9.5, 6, 0.5, 1.5));
|
||||||
|
scenes.put("frames_markers", new Scene("frames_markers", this::framesMarkers, 2.5, 2.4, 4.2, 2.5, 2.4, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ItemStack shield(DyeColor base, Object... patterns) {
|
||||||
|
ItemStack s = it(Material.SHIELD);
|
||||||
|
org.bukkit.inventory.meta.ShieldMeta m = (org.bukkit.inventory.meta.ShieldMeta) s.getItemMeta();
|
||||||
|
m.setBaseColor(base);
|
||||||
|
for (int i = 0; i + 1 < patterns.length; i += 2)
|
||||||
|
m.addPattern(new org.bukkit.block.banner.Pattern((DyeColor) patterns[i], (org.bukkit.block.banner.PatternType) patterns[i + 1]));
|
||||||
|
s.setItemMeta(m);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void itemsDetail(Location o) {
|
||||||
|
for (int x = -1; x <= 11; x++) for (int y = 0; y <= 3; y++) put(o, x, y, -1, "stone");
|
||||||
|
ItemStack[] items = {it(Material.SHIELD), shield(DyeColor.RED),
|
||||||
|
shield(DyeColor.BLUE, DyeColor.YELLOW, org.bukkit.block.banner.PatternType.CREEPER,
|
||||||
|
DyeColor.WHITE, org.bukkit.block.banner.PatternType.BORDER),
|
||||||
|
it(Material.CHEST), it(Material.TRAPPED_CHEST), it(Material.ENDER_CHEST), it(Material.COPPER_CHEST),
|
||||||
|
it(Material.SPAWNER), it(Material.VAULT), it(Material.RED_BANNER)};
|
||||||
|
for (int i = 0; i < items.length; i++) {
|
||||||
|
frame(o, i, 1, 0, BlockFace.SOUTH, false, items[i], 0, true);
|
||||||
|
frame(o, i, 2, 0, BlockFace.SOUTH, false, items[i], 1, true);
|
||||||
|
ArmorStand as = spawn(o, i + 0.5, 0, 2.5, 0, ArmorStand.class);
|
||||||
|
as.setArms(true);
|
||||||
|
as.getEquipment().setItemInMainHand(items[i]);
|
||||||
|
if (i < 3) as.getEquipment().setItemInOffHand(items[i]);
|
||||||
|
ItemDisplay d = spawn(o, i + 0.5, 0.6, 4.5, 0, ItemDisplay.class);
|
||||||
|
d.setItemStack(items[i]);
|
||||||
|
d.setItemDisplayTransform(ItemDisplay.ItemDisplayTransform.FIXED);
|
||||||
|
d.setTransformation(new Transformation(new Vector3f(), new Quaternionf(), new Vector3f(0.6f), new Quaternionf()));
|
||||||
|
drop(o, i + 0.5, 0, 6.5, items[i], i * 20);
|
||||||
|
}
|
||||||
|
Zombie baby = spawn(o, 10.5, 0, 2.5, 0, Zombie.class);
|
||||||
|
baby.setBaby();
|
||||||
|
baby.getEquipment().setItemInMainHand(it(Material.CHEST));
|
||||||
|
baby.getEquipment().setItemInOffHand(items[2]);
|
||||||
|
Zombie z = spawn(o, 11.5, 0, 4, 0, Zombie.class);
|
||||||
|
z.setAdult();
|
||||||
|
z.getEquipment().setItemInMainHand(it(Material.TRAPPED_CHEST));
|
||||||
|
z.getEquipment().setItemInOffHand(items[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void itemsInverted(Location o) {
|
||||||
|
String[] blocks = {"spawner", "trial_spawner", "vault[facing=south]"};
|
||||||
|
Material[] mats = {Material.SPAWNER, Material.TRIAL_SPAWNER, Material.VAULT};
|
||||||
|
for (int i = 0; i < blocks.length; i++) {
|
||||||
|
put(o, i * 2, 0, 0, blocks[i]);
|
||||||
|
BlockDisplay b = spawn(o, i * 2, 0, 2, 0, BlockDisplay.class);
|
||||||
|
b.setBlock(Bukkit.createBlockData("minecraft:" + blocks[i]));
|
||||||
|
ItemDisplay d = spawn(o, i * 2 + 0.5, 0.5, 4.5, 0, ItemDisplay.class);
|
||||||
|
d.setItemStack(it(mats[i]));
|
||||||
|
drop(o, i * 2 + 0.5, 0, 6, it(mats[i]), 15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void xmasChests(Location o) {
|
||||||
|
put(o, 0, 0, 0, "chest[facing=south]");
|
||||||
|
put(o, 3, 0, 0, "chest[facing=south,type=left]");
|
||||||
|
put(o, 2, 0, 0, "chest[facing=south,type=right]");
|
||||||
|
put(o, 5, 0, 0, "trapped_chest[facing=south]");
|
||||||
|
put(o, 8, 0, 0, "trapped_chest[facing=south,type=left]");
|
||||||
|
put(o, 7, 0, 0, "trapped_chest[facing=south,type=right]");
|
||||||
|
put(o, 10, 0, 0, "ender_chest[facing=south]");
|
||||||
|
put(o, 12, 0, 0, "copper_chest[facing=south]");
|
||||||
|
Material[] mats = {Material.CHEST, Material.TRAPPED_CHEST};
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
ItemDisplay d = spawn(o, 0.5 + i * 1.5, 0.6, 3.5, 0, ItemDisplay.class);
|
||||||
|
d.setItemStack(it(mats[i]));
|
||||||
|
d.setItemDisplayTransform(ItemDisplay.ItemDisplayTransform.FIXED);
|
||||||
|
ArmorStand as = spawn(o, 4 + i * 1.5, 0, 3.5, 0, ArmorStand.class);
|
||||||
|
as.setArms(true);
|
||||||
|
as.getEquipment().setItemInMainHand(it(mats[i]));
|
||||||
|
drop(o, 7.5 + i, 0, 3.5, it(mats[i]), 30);
|
||||||
|
put(o, 10 + i, 0, 3, "stone");
|
||||||
|
frame(o, 10 + i, 1, 3, BlockFace.UP, false, it(mats[i]), 0, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void framesMarkers(Location o) {
|
||||||
|
for (int x = -2; x <= 7; x++) for (int y = 0; y <= 4; y++) put(o, x, y, -1, "stone");
|
||||||
|
ItemStack marked = vanillaMap();
|
||||||
|
org.bukkit.map.MapView v = ((org.bukkit.inventory.meta.MapMeta) marked.getItemMeta()).getMapView();
|
||||||
|
v.setScale(org.bukkit.map.MapView.Scale.CLOSEST);
|
||||||
|
v.setCenterX(o.getBlockX());
|
||||||
|
v.setCenterZ(o.getBlockZ());
|
||||||
|
put(o, -5, 0, -10, "red_banner[rotation=0]");
|
||||||
|
put(o, 16, 0, -11, "lime_banner[rotation=4]");
|
||||||
|
mapBanner(v, o.clone().add(-5, 0, -10));
|
||||||
|
mapBanner(v, o.clone().add(16, 0, -11));
|
||||||
|
ItemFrame f0 = frame(o, 0, 2, 0, BlockFace.SOUTH, false, marked, 0, true);
|
||||||
|
frame(o, 1, 2, 0, BlockFace.SOUTH, false, marked, 1, true);
|
||||||
|
frame(o, 2, 2, 0, BlockFace.SOUTH, false, synthMap(), 0, true);
|
||||||
|
frame(o, 3, 2, 0, BlockFace.SOUTH, false, vanillaMap(), 0, true);
|
||||||
|
frame(o, 4, 2, 0, BlockFace.SOUTH, true, marked, 3, true);
|
||||||
|
// Frame markers like MapItemSavedData.tickCarriedBy (key frame-<id>, frame block pos, rot = 2D facing · 90):
|
||||||
|
// the real frame, plus fictitious ones turned east/north; a red X and a player marker (not on frames).
|
||||||
|
mapDecoration(v, "FRAME", "frame-" + f0.getEntityId(), o.getBlockX(), o.getBlockZ(), 0);
|
||||||
|
mapDecoration(v, "FRAME", "frame-pptest-1", o.getBlockX() + 30, o.getBlockZ() - 25, 270);
|
||||||
|
mapDecoration(v, "FRAME", "frame-pptest-2", o.getBlockX() - 28, o.getBlockZ() + 30, 180);
|
||||||
|
mapDecoration(v, "RED_X", "pptest-x", o.getBlockX() + 40, o.getBlockZ() + 40, 180);
|
||||||
|
mapDecoration(v, "PLAYER", "pptest-player", o.getBlockX() + 10, o.getBlockZ() + 10, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** MapItemSavedData.toggleBanner(level, pos) on the map's server data (reflective: no API). */
|
||||||
|
private void mapBanner(org.bukkit.map.MapView v, Location banner) {
|
||||||
|
try {
|
||||||
|
Object data = mapData(v);
|
||||||
|
Object level = w.getClass().getMethod("getHandle").invoke(w);
|
||||||
|
Class<?> bp = Class.forName("net.minecraft.core.BlockPos", true, data.getClass().getClassLoader());
|
||||||
|
Object pos = bp.getConstructor(int.class, int.class, int.class)
|
||||||
|
.newInstance(banner.getBlockX(), banner.getBlockY(), banner.getBlockZ());
|
||||||
|
for (java.lang.reflect.Method m : data.getClass().getMethods())
|
||||||
|
if (m.getName().equals("toggleBanner") && m.getParameterCount() == 2)
|
||||||
|
getLogger().info("toggleBanner " + banner.toVector() + " -> " + m.invoke(data, level, pos));
|
||||||
|
} catch (ReflectiveOperationException e) {
|
||||||
|
getLogger().warning("mapBanner: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** MapItemSavedData.addDecoration(MapDecorationTypes.<type>, level, key, x, z, yRot, null) (private, reflective). */
|
||||||
|
private void mapDecoration(org.bukkit.map.MapView v, String type, String key, double x, double z, double rot) {
|
||||||
|
try {
|
||||||
|
Object data = mapData(v);
|
||||||
|
Object level = w.getClass().getMethod("getHandle").invoke(w);
|
||||||
|
Object holder = Class.forName("net.minecraft.world.level.saveddata.maps.MapDecorationTypes", true,
|
||||||
|
data.getClass().getClassLoader()).getField(type).get(null);
|
||||||
|
for (java.lang.reflect.Method m : data.getClass().getDeclaredMethods())
|
||||||
|
if (m.getName().equals("addDecoration") && m.getParameterCount() == 7) {
|
||||||
|
m.setAccessible(true);
|
||||||
|
m.invoke(data, holder, level, key, x, z, rot, null);
|
||||||
|
}
|
||||||
|
} catch (ReflectiveOperationException e) {
|
||||||
|
getLogger().warning("mapDecoration: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object mapData(org.bukkit.map.MapView v) throws ReflectiveOperationException {
|
||||||
|
java.lang.reflect.Field wm = v.getClass().getDeclaredField("worldMap");
|
||||||
|
wm.setAccessible(true);
|
||||||
|
return wm.get(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void beContents(Location o) {
|
private void beContents(Location o) {
|
||||||
@@ -212,6 +366,7 @@ public class PPTest extends JavaPlugin {
|
|||||||
c.setItem(i, it(food[i]));
|
c.setItem(i, it(food[i]));
|
||||||
c.setCookTimeTotal(i, 1_000_000); // else the lit campfire pops them out at once (total 0)
|
c.setCookTimeTotal(i, 1_000_000); // else the lit campfire pops them out at once (total 0)
|
||||||
}
|
}
|
||||||
|
c.stopCooking(); // keep the food raw and in place while the scene waits
|
||||||
c.update(true, false);
|
c.update(true, false);
|
||||||
}
|
}
|
||||||
if (o.clone().add(4, 0, 0).getBlock().getState() instanceof org.bukkit.block.Campfire c) {
|
if (o.clone().add(4, 0, 0).getBlock().getState() instanceof org.bukkit.block.Campfire c) {
|
||||||
@@ -561,7 +716,9 @@ public class PPTest extends JavaPlugin {
|
|||||||
put(o, 8, 0, 1, "water");
|
put(o, 8, 0, 1, "water");
|
||||||
Mannequin m3 = rfSpawn(o, 8.5, 0.1, 0.5, 180, Mannequin.class);
|
Mannequin m3 = rfSpawn(o, 8.5, 0.1, 0.5, 180, Mannequin.class);
|
||||||
m3.setPose(Pose.SWIMMING, true);
|
m3.setPose(Pose.SWIMMING, true);
|
||||||
|
m3.setGravity(false); // else it floats up in the water while the scene waits (non-deterministic)
|
||||||
Mannequin m4 = rfSpawn(o, 10, 1.0, 0, 180, Mannequin.class);
|
Mannequin m4 = rfSpawn(o, 10, 1.0, 0, 180, Mannequin.class);
|
||||||
|
m4.setGravity(false); // keeps gliding at y 1 instead of falling
|
||||||
m4.getEquipment().setChestplate(it(Material.ELYTRA));
|
m4.getEquipment().setChestplate(it(Material.ELYTRA));
|
||||||
m4.setPose(Pose.FALL_FLYING, true);
|
m4.setPose(Pose.FALL_FLYING, true);
|
||||||
Mannequin m5 = rfSpawn(o, 12, 0, 0, 180, Mannequin.class);
|
Mannequin m5 = rfSpawn(o, 12, 0, 0, 180, Mannequin.class);
|
||||||
@@ -649,7 +806,30 @@ public class PPTest extends JavaPlugin {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the scene's chunks and waits until they are entity-ticking before building: entities spawned into a
|
||||||
|
* chunk that only becomes entity-ticking a few (variable) ticks later start moving late (projectile scenes were
|
||||||
|
* not reproducible).
|
||||||
|
*/
|
||||||
private void run(Scene sc, Location o) {
|
private void run(Scene sc, Location o) {
|
||||||
|
loadArea(o);
|
||||||
|
whenEntityTicking(sc, o, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void whenEntityTicking(Scene sc, Location o, int waited) {
|
||||||
|
boolean ready = true;
|
||||||
|
for (int cx = -1; cx <= 2; cx++)
|
||||||
|
for (int cz = -2; cz <= 2; cz++)
|
||||||
|
ready &= w.getChunkAt((o.getBlockX() >> 4) + cx, (o.getBlockZ() >> 4) + cz).getLoadLevel() == Chunk.LoadLevel.ENTITY_TICKING;
|
||||||
|
if (!ready && waited < 200) {
|
||||||
|
Bukkit.getScheduler().runTaskLater(this, () -> whenEntityTicking(sc, o, waited + 1), 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ready) getLogger().warning("scene " + sc.name + ": chunks not entity-ticking after 200 ticks");
|
||||||
|
build(sc, o);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void build(Scene sc, Location o) {
|
||||||
try {
|
try {
|
||||||
prepareArea(o);
|
prepareArea(o);
|
||||||
sc.build.accept(o.clone());
|
sc.build.accept(o.clone());
|
||||||
@@ -666,7 +846,7 @@ public class PPTest extends JavaPlugin {
|
|||||||
Location eye = o.clone().add(sc.camX, sc.camY, sc.camZ);
|
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());
|
Vector look = o.clone().add(sc.lookX, sc.lookY, sc.lookZ).toVector().subtract(eye.toVector());
|
||||||
eye.setDirection(look);
|
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("pose_") || sc.name.startsWith("frames") || sc.name.startsWith("geo_") ? 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") || sc.name.startsWith("geo_") || sc.name.startsWith("items_") || sc.name.startsWith("xmas_") ? new Resolution(960, 640) : new Resolution(480, 320), null);
|
||||||
if (sc.name.equals("dbg")) {
|
if (sc.name.equals("dbg")) {
|
||||||
try {
|
try {
|
||||||
java.lang.reflect.Field f = DefaultScreenRenderer.class.getDeclaredField("objectBaker");
|
java.lang.reflect.Field f = DefaultScreenRenderer.class.getDeclaredField("objectBaker");
|
||||||
@@ -730,43 +910,74 @@ public class PPTest extends JavaPlugin {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Clears the area (entities + blocks) and lays a 14x14 stone floor at the origin's y-1. */
|
/** Loads the scene's chunks and keeps them loaded (plugin chunk tickets). */
|
||||||
private void prepareArea(Location o) {
|
private void loadArea(Location o) {
|
||||||
for (int cx = -1; cx <= 2; cx++)
|
for (int cx = -1; cx <= 2; cx++)
|
||||||
for (int cz = -2; cz <= 2; cz++) {
|
for (int cz = -2; cz <= 2; cz++) {
|
||||||
Chunk c = w.getChunkAt((o.getBlockX() >> 4) + cx, (o.getBlockZ() >> 4) + cz);
|
Chunk c = w.getChunkAt((o.getBlockX() >> 4) + cx, (o.getBlockZ() >> 4) + cz);
|
||||||
c.load(true);
|
c.load(true);
|
||||||
c.addPluginChunkTicket(this);
|
c.addPluginChunkTicket(this);
|
||||||
}
|
}
|
||||||
for (Entity e : w.getNearbyEntities(o.clone().add(6, 5, 2), 20, 20, 20)) if (!(e instanceof Player)) e.remove();
|
}
|
||||||
|
|
||||||
|
/** Clears the area (blocks + entities) and lays a 24x24 stone floor at the origin's y-1. */
|
||||||
|
private void prepareArea(Location o) {
|
||||||
|
// Blocks first: removing a campfire/shelf/container drops its items, which the entity sweep then removes
|
||||||
|
// (before, the previous run's cooked campfire food lay around at random).
|
||||||
for (int x = -6; x < 18; x++)
|
for (int x = -6; x < 18; x++)
|
||||||
for (int z = -12; z < 12; z++)
|
for (int z = -12; z < 12; z++)
|
||||||
for (int y = -1; y < 12; y++)
|
for (int y = -1; y < 12; y++)
|
||||||
w.getBlockAt(o.getBlockX() + x, o.getBlockY() + y, o.getBlockZ() + z)
|
w.getBlockAt(o.getBlockX() + x, o.getBlockY() + y, o.getBlockZ() + z)
|
||||||
.setType(y == -1 ? Material.SMOOTH_STONE : Material.AIR, false);
|
.setType(y == -1 ? Material.SMOOTH_STONE : Material.AIR, false);
|
||||||
|
for (Entity e : w.getNearbyEntities(o.clone().add(6, 5, 2), 20, 20, 20)) if (!(e instanceof Player)) e.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- helpers ---------------------------------------------------------------------------------
|
// --- helpers ---------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deterministic spawn (pixel-diffable between plugin builds): randomizeData=false (no finalizeSpawn — no random
|
||||||
|
* variants, sizes, babies/jockeys or equipment) and the body turned to {@code yaw} (spawned mobs otherwise keep a
|
||||||
|
* random body yaw). Scenes set every variant/equipment they show explicitly.
|
||||||
|
*/
|
||||||
private <T extends Entity> T spawn(Location o, double x, double y, double z, float yaw, Class<T> type) {
|
private <T extends Entity> T spawn(Location o, double x, double y, double z, float yaw, Class<T> type) {
|
||||||
Location l = o.clone().add(x, y, z);
|
T e = rfSpawn(o, x, y, z, yaw, type);
|
||||||
l.setYaw(yaw);
|
if (e instanceof LivingEntity le) le.setBodyYaw(yaw);
|
||||||
T e = w.spawn(l, type, en -> {
|
|
||||||
if (en instanceof Mob m) m.setAI(false);
|
|
||||||
en.setPersistent(false);
|
|
||||||
});
|
|
||||||
if (e instanceof LivingEntity le) le.setRemoveWhenFarAway(false);
|
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Entity spawn(Location o, double x, double y, double z, float yaw, EntityType type) {
|
private Entity spawn(Location o, double x, double y, double z, float yaw, EntityType type) {
|
||||||
Location l = o.clone().add(x, y, z);
|
Entity e = rfSpawn(o, x, y, z, yaw, type);
|
||||||
l.setYaw(yaw);
|
e.setPersistent(false);
|
||||||
Entity e = w.spawnEntity(l, type);
|
if (e instanceof LivingEntity le) {
|
||||||
if (e instanceof Mob m) m.setAI(false);
|
le.setRemoveWhenFarAway(false);
|
||||||
|
le.setBodyYaw(yaw);
|
||||||
|
}
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Respawns until the entity id is even: a full boat turns an animal passenger by 90° or 270° depending on the
|
||||||
|
* id's parity (AbstractBoat.positionRider), and ids depend on the server's history.
|
||||||
|
*/
|
||||||
|
private <T extends Entity> T evenId(java.util.function.Supplier<T> spawner) {
|
||||||
|
T e = spawner.get();
|
||||||
|
while (e.getEntityId() % 2 != 0) {
|
||||||
|
e.remove();
|
||||||
|
e = spawner.get();
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A dropped item at rest: no velocity/gravity, fixed yaw (PixelPics freezes the item spin from the yaw). */
|
||||||
|
private Item drop(Location o, double x, double y, double z, ItemStack stack, float yaw) {
|
||||||
|
return w.dropItem(o.clone().add(x, y, z), stack, i -> {
|
||||||
|
i.setVelocity(new Vector());
|
||||||
|
i.setGravity(false);
|
||||||
|
i.setRotation(yaw, 0);
|
||||||
|
i.setPersistent(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static ItemStack it(Material m) {
|
private static ItemStack it(Material m) {
|
||||||
return new ItemStack(m);
|
return new ItemStack(m);
|
||||||
}
|
}
|
||||||
@@ -839,7 +1050,7 @@ public class PPTest extends JavaPlugin {
|
|||||||
mc.addPassenger(spawn(o, 7, 1, 0, 180, Villager.class));
|
mc.addPassenger(spawn(o, 7, 1, 0, 180, Villager.class));
|
||||||
Boat b = (Boat) spawn(o, 9.5, 0, 0, 180, EntityType.OAK_BOAT);
|
Boat b = (Boat) spawn(o, 9.5, 0, 0, 180, EntityType.OAK_BOAT);
|
||||||
b.addPassenger(spawn(o, 9.5, 1, 0, 180, Zombie.class));
|
b.addPassenger(spawn(o, 9.5, 1, 0, 180, Zombie.class));
|
||||||
b.addPassenger(spawn(o, 9.5, 1, 0, 180, Pig.class));
|
b.addPassenger(evenId(() -> spawn(o, 9.5, 1, 0, 180, Pig.class)));
|
||||||
Camel c = spawn(o, 2, 0, 3.5, 90, Camel.class);
|
Camel c = spawn(o, 2, 0, 3.5, 90, Camel.class);
|
||||||
c.getInventory().setSaddle(it(Material.SADDLE));
|
c.getInventory().setSaddle(it(Material.SADDLE));
|
||||||
c.addPassenger(spawn(o, 2, 2, 3.5, 90, EntityType.MANNEQUIN));
|
c.addPassenger(spawn(o, 2, 2, 3.5, 90, EntityType.MANNEQUIN));
|
||||||
@@ -955,10 +1166,10 @@ public class PPTest extends JavaPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void misc(Location o) {
|
private void misc(Location o) {
|
||||||
w.dropItem(o.clone().add(0, 0.2, 0), it(Material.DIAMOND_SWORD), i -> i.setVelocity(new Vector()));
|
drop(o, 0, 0, 0, it(Material.DIAMOND_SWORD), 30);
|
||||||
w.dropItem(o.clone().add(1, 0.2, 0), it(Material.OAK_LOG), i -> i.setVelocity(new Vector()));
|
drop(o, 1, 0, 0, it(Material.OAK_LOG), 60);
|
||||||
w.dropItem(o.clone().add(2, 0.2, 0), new ItemStack(Material.COBBLESTONE, 32), i -> i.setVelocity(new Vector()));
|
drop(o, 2, 0, 0, new ItemStack(Material.COBBLESTONE, 32), 0);
|
||||||
w.dropItem(o.clone().add(3, 0.2, 0), it(Material.APPLE), i -> i.setVelocity(new Vector()));
|
drop(o, 3, 0, 0, it(Material.APPLE), 90);
|
||||||
FallingBlock fb = w.spawn(o.clone().add(4.5, 0.5, 0.5), FallingBlock.class, f -> f.setGravity(false));
|
FallingBlock fb = w.spawn(o.clone().add(4.5, 0.5, 0.5), FallingBlock.class, f -> f.setGravity(false));
|
||||||
fb.setBlockData(Bukkit.createBlockData("minecraft:sand"));
|
fb.setBlockData(Bukkit.createBlockData("minecraft:sand"));
|
||||||
TNTPrimed tnt = w.spawn(o.clone().add(6, 0, 0.5), TNTPrimed.class);
|
TNTPrimed tnt = w.spawn(o.clone().add(6, 0, 0.5), TNTPrimed.class);
|
||||||
|
|||||||
Reference in new issue
Block a user