block-entity contents from the decompiled renderers: enchanting table book (closed, open towards a nearby photographer), campfire and shelf items, vault display item, copper golem statues (poses, weathering), spawner and trial spawner mobs, beacon beam with glass colours, end portal and gateway star field, block-entity parts in block displays (lectern book, bell), outer item-model transformations for special models, contents test scenes
This commit is contained in:
1 parent
318a309580
commit
9e48e35680
13 files changed
+925
-28
No files matched your search
@@ -140,7 +140,10 @@ public final class BlockModelRegistry {
|
||||
|| n.endsWith("COPPER_CHEST") // copper chests incl. weathered/waxed stages
|
||||
|| m == Material.CONDUIT
|
||||
|| m == Material.DECORATED_POT
|
||||
|| m == Material.BELL;
|
||||
|| m == Material.BELL
|
||||
|| n.endsWith("COPPER_GOLEM_STATUE") // statue incl. weathered/waxed stages
|
||||
|| m == Material.END_PORTAL
|
||||
|| m == Material.END_GATEWAY;
|
||||
if (match) set.add(m);
|
||||
}
|
||||
return set;
|
||||
|
||||
@@ -9,7 +9,8 @@ import java.util.Set;
|
||||
* dump, see {@code tools/VanillaModelDump.java}) and to the renderer's PoseStack. Every pose below is
|
||||
* transcribed from the decompiled 26.3 client renderers (ChestRenderer, SkullBlockRenderer,
|
||||
* BannerRenderer, ShulkerBoxRenderer, DecoratedPotRenderer, BellRenderer, ConduitRenderer,
|
||||
* LecternRenderer, Standing/HangingSignRenderer) — no calibration offsets. Mirrors the role
|
||||
* LecternRenderer, EnchantTableRenderer, CopperGolemStatueBlockRenderer, Standing/HangingSignRenderer) — no
|
||||
* calibration offsets. Mirrors the role
|
||||
* {@link EntityModels} plays for mobs.
|
||||
*
|
||||
* <p>Beds and sign boards are plain block models since 26.x (no renderer model): the block-model
|
||||
@@ -136,7 +137,40 @@ public final class BlockEntityModels {
|
||||
.mul(Affine.translation(0, -0.125, 0)),
|
||||
List.of(Part.of("posed/book#main", "entity/enchantment/enchanting_table_book")))
|
||||
: null;
|
||||
case SIGN, WALL_SIGN, HANGING_SIGN -> null;
|
||||
// EnchantTableRenderer: T(.5,.75,.5)·T(0, 0.1 + sin(time·0.1)·0.01, 0)·rotY(-rot)·rotZ(80°), book state
|
||||
// forAnimation(time, flip pages, open) at time 0 / flip 0, closed or open (dumped posed).
|
||||
case ENCHANTING_TABLE -> new VanillaModel(
|
||||
Affine.translation(0.5, 0.75 + 0.1, 0.5)
|
||||
.mul(Affine.rotY(Math.toRadians(-deg)))
|
||||
.mul(Affine.rotZ(Math.toRadians(80))),
|
||||
List.of(Part.of(
|
||||
s.active() ? "posed/enchanting_book_open#main" : "posed/enchanting_book_closed#main",
|
||||
"entity/enchantment/enchanting_table_book")));
|
||||
// CopperGolemStatueBlockRenderer: T(.5,0,.5)·rotY(-opposite(facing).toYRot()), one model per pose
|
||||
// (CopperGolemStatueModel.setupAnim: root.y 0, root.zRot π), texture per weathering stage.
|
||||
case COPPER_GOLEM_STATUE -> new VanillaModel(
|
||||
Affine.translation(0.5, 0, 0.5).mul(Affine.rotY(Math.toRadians(-(deg + 180)))),
|
||||
List.of(new Part(
|
||||
"posed/copper_golem_statue_" + (s.pose() == null ? "standing" : s.pose()) + "#main",
|
||||
s.colorName() == null
|
||||
? List.of("entity/copper_golem/copper_golem")
|
||||
: List.of(
|
||||
"entity/copper_golem/copper_golem_" + s.colorName(),
|
||||
"entity/copper_golem/copper_golem"),
|
||||
Set.of(),
|
||||
Paint.PLAIN)));
|
||||
// Signs: text only (BlockEntityBaker). Items, spawner mobs, beams and portals are not entity models
|
||||
// (BlockEntityBaker.addContents).
|
||||
case SIGN,
|
||||
WALL_SIGN,
|
||||
HANGING_SIGN,
|
||||
CAMPFIRE,
|
||||
SHELF,
|
||||
SPAWNER,
|
||||
VAULT,
|
||||
BEACON,
|
||||
END_PORTAL,
|
||||
END_GATEWAY -> null;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ public record BlockEntityState(
|
||||
int bz,
|
||||
float facingDeg,
|
||||
String direction, // 6-way facing ("up", "north", …) of shulker boxes; null otherwise
|
||||
boolean active, // lectern: has a book; conduit: active
|
||||
boolean active, // lectern: has a book; conduit: active; enchanting table: book open
|
||||
ChestKind chestKind, // double-chest half (CHEST/TRAPPED_CHEST/COPPER_CHEST)
|
||||
int baseColorArgb, // banner base colour; 0 = none
|
||||
String colorName, // shulker colour ("red", …) or copper-chest stage ("exposed", …); null = default
|
||||
@@ -28,8 +28,54 @@ public record BlockEntityState(
|
||||
List<BannerPattern> patterns, // banner overlay patterns (may be empty)
|
||||
List<String> sherds, // decorated-pot sherds: front/left/right/back item keys (may be empty)
|
||||
SignText frontText, // sign front-side text; null when not a sign or blank
|
||||
SignText backText // sign back-side text; null when not a sign or blank
|
||||
SignText backText, // sign back-side text; null when not a sign or blank
|
||||
List<ItemSpec> items, // campfire (4) / shelf (3) / vault (1) slots, null = empty slot; may be empty
|
||||
EntityState displayEntity, // spawner / trial spawner mob (captured at its own position); null = none
|
||||
Beam beam, // beacon beam (sections bottom-up); null = no beam
|
||||
String pose // copper golem statue pose ("standing", "running", "sitting", "star"); null otherwise
|
||||
) {
|
||||
|
||||
/** The pre-contents layout (no items/entity/beam/pose): chests, signs, banners, heads, … */
|
||||
public BlockEntityState(
|
||||
Kind kind,
|
||||
int bx,
|
||||
int by,
|
||||
int bz,
|
||||
float facingDeg,
|
||||
String direction,
|
||||
boolean active,
|
||||
ChestKind chestKind,
|
||||
int baseColorArgb,
|
||||
String colorName,
|
||||
String headType,
|
||||
String skinUrl,
|
||||
List<BannerPattern> patterns,
|
||||
List<String> sherds,
|
||||
SignText frontText,
|
||||
SignText backText) {
|
||||
this(
|
||||
kind,
|
||||
bx,
|
||||
by,
|
||||
bz,
|
||||
facingDeg,
|
||||
direction,
|
||||
active,
|
||||
chestKind,
|
||||
baseColorArgb,
|
||||
colorName,
|
||||
headType,
|
||||
skinUrl,
|
||||
patterns,
|
||||
sherds,
|
||||
frontText,
|
||||
backText,
|
||||
List.of(),
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
}
|
||||
|
||||
public enum Kind {
|
||||
CHEST,
|
||||
TRAPPED_CHEST,
|
||||
@@ -46,9 +92,28 @@ public record BlockEntityState(
|
||||
CONDUIT,
|
||||
DECORATED_POT,
|
||||
BELL,
|
||||
LECTERN
|
||||
LECTERN,
|
||||
ENCHANTING_TABLE, // facingDeg = book yaw (vanilla rot, degrees), active = open (player within 3 blocks)
|
||||
CAMPFIRE,
|
||||
SHELF,
|
||||
COPPER_GOLEM_STATUE, // colorName = weathering stage, pose = statue pose
|
||||
SPAWNER, // spawner and trial spawner: displayEntity
|
||||
VAULT, // items[0] = displayed key/reward item
|
||||
BEACON,
|
||||
END_PORTAL,
|
||||
END_GATEWAY // direction = "+"-joined faces to draw (neighbour not occluding)
|
||||
}
|
||||
|
||||
/**
|
||||
* A beacon beam: coloured sections bottom-up (BeaconBlockEntity: white from the beacon, a stained-glass
|
||||
* colour or the average with the previous one per change; the last runs to the top) and the radius scale
|
||||
* (BeaconRenderer: {@code max(1, horizontal camera distance / 96)}).
|
||||
*/
|
||||
public record Beam(List<BeamSection> sections, float radiusScale) {}
|
||||
|
||||
/** One beam section: ARGB colour and height in blocks. */
|
||||
public record BeamSection(int argb, int height) {}
|
||||
|
||||
public enum ChestKind {
|
||||
SINGLE,
|
||||
LEFT,
|
||||
|
||||
@@ -35,8 +35,10 @@ public final class ObjectBaker implements EntityBaker<ObjectState> {
|
||||
List<EntityCube> cubes = new ArrayList<>();
|
||||
switch (s.kind()) {
|
||||
case BLOCK -> {
|
||||
// The block model plus what its block-entity renderer adds (chest/banner/… bodies, the lectern's
|
||||
// book, the bell, the enchanting-table book): block-entity blocks draw no block-model geometry.
|
||||
items.bakeBlock(s.block(), s.toWorld(), cubes);
|
||||
if (cubes.isEmpty()) items.bakeBlockEntity(s.blockEntity(), s.toWorld(), cubes);
|
||||
items.bakeBlockEntity(s.blockEntity(), s.toWorld(), cubes);
|
||||
}
|
||||
case ITEM -> {
|
||||
Affine at = s.toWorld();
|
||||
|
||||
@@ -12,8 +12,11 @@ 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.EntityState;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.ItemSpec;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.RenderedEntity;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.TextureOps;
|
||||
import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -40,6 +43,22 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
|
||||
private final BitmapFont font;
|
||||
// Composited banner flags (base + dyed pattern masks), shared by identical banners.
|
||||
private final DerivedTextureCache derived = new DerivedTextureCache(256);
|
||||
// End portal/gateway face textures (per block face, the star field is not periodic) and beam textures.
|
||||
private final DerivedTextureCache portals = new DerivedTextureCache(512);
|
||||
// Item models (campfire/shelf/vault contents) and the mob baker (spawners); wired after construction
|
||||
// because both depend on this baker themselves. Null = contents not drawn.
|
||||
private volatile ItemGeometry items;
|
||||
private volatile CemBaker entities;
|
||||
|
||||
/** Item geometry for campfire/shelf/vault contents. */
|
||||
public void setItemGeometry(ItemGeometry items) {
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
/** Mob baker for the spawner's display entity. */
|
||||
public void setEntityBaker(CemBaker entities) {
|
||||
this.entities = entities;
|
||||
}
|
||||
|
||||
/** The sign/text font (shared with text-display rendering). */
|
||||
public BitmapFont font() {
|
||||
@@ -61,6 +80,7 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
|
||||
BlockEntityModels.VanillaModel vm = BlockEntityModels.vanillaModel(s);
|
||||
if (vm != null) bakeParts(s, vm, cell.mul(vm.pose()), cubes);
|
||||
addSignText(s, cell, cubes);
|
||||
addContents(s, cell, cubes);
|
||||
return cubes.isEmpty() ? null : RenderedEntity.of(cubes);
|
||||
}
|
||||
|
||||
@@ -86,6 +106,303 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
|
||||
}
|
||||
}
|
||||
|
||||
// --- contents: items, spawner mob, beacon beam, end portal ---
|
||||
|
||||
private void addContents(BlockEntityState s, Affine cell, List<EntityCube> cubes) {
|
||||
switch (s.kind()) {
|
||||
case CAMPFIRE -> addCampfireItems(s, cell, cubes);
|
||||
case SHELF -> addShelfItems(s, cell, cubes);
|
||||
case VAULT -> addVaultItem(s, cell, cubes);
|
||||
case SPAWNER -> addSpawnerMob(s, cell, cubes);
|
||||
case BEACON -> addBeam(s, cell, cubes);
|
||||
case END_PORTAL, END_GATEWAY -> addPortal(s, cell, cubes);
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private static ItemSpec slot(BlockEntityState s, int i) {
|
||||
return i < s.items().size() ? s.items().get(i) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* CampfireRenderer: slot i lies flat at {@code T(.5, 0.44921875, .5)·rotY(-dir.toYRot())·rotX(90°)·
|
||||
* T(-0.3125, -0.3125, 0)·S(0.375)}, {@code dir = from2DDataValue((i + facing.get2DDataValue()) % 4)}, in
|
||||
* the {@code fixed} display context.
|
||||
*/
|
||||
private void addCampfireItems(BlockEntityState s, Affine cell, List<EntityCube> cubes) {
|
||||
ItemGeometry ig = items;
|
||||
if (ig == null) return;
|
||||
int facing2d = Math.floorMod(Math.round(s.facingDeg() / 90), 4); // S0 W1 N2 E3 = yaw / 90
|
||||
for (int i = 0; i < 4; i++) {
|
||||
ItemSpec item = slot(s, i);
|
||||
if (item == null) continue;
|
||||
double yRot = ((i + facing2d) % 4) * 90;
|
||||
Affine m = cell.mul(Affine.translation(0.5, 0.44921875, 0.5))
|
||||
.mul(Affine.rotY(Math.toRadians(-yRot)))
|
||||
.mul(Affine.rotX(Math.toRadians(90)))
|
||||
.mul(Affine.translation(-0.3125, -0.3125, 0))
|
||||
.mul(Affine.scale(0.375));
|
||||
ig.bake(item, "fixed", false, m, cubes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ShelfRenderer (align_items_to_bottom off): slot i at {@code T(.5,.5,.5)·rotY(-facing.toYRot())·
|
||||
* T((i-1)·0.3125, 0, -0.25)·S(0.25)·T(0, -minY - (maxY-minY)/2, 0)} — the item's {@code on_shelf} model
|
||||
* bounding box vertically centred.
|
||||
*/
|
||||
private void addShelfItems(BlockEntityState s, Affine cell, List<EntityCube> cubes) {
|
||||
ItemGeometry ig = items;
|
||||
if (ig == null) return;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
ItemSpec item = slot(s, i);
|
||||
if (item == null) continue;
|
||||
double[] box = ig.modelBounds(item, "on_shelf");
|
||||
if (box == null) continue;
|
||||
double offsetY = -box[1] - (box[4] - box[1]) / 2;
|
||||
Affine m = cell.mul(Affine.translation(0.5, 0.5, 0.5))
|
||||
.mul(Affine.rotY(Math.toRadians(-s.facingDeg())))
|
||||
.mul(Affine.translation((i - 1) * 0.3125, 0, -0.25))
|
||||
.mul(Affine.scale(0.25))
|
||||
.mul(Affine.translation(0, offsetY, 0));
|
||||
ig.bake(item, "on_shelf", false, m, cubes);
|
||||
}
|
||||
}
|
||||
|
||||
/** VaultRenderer: the display item at {@code T(.5, .4, .5)·rotY(spin)} (spin 0), {@code ground} context. */
|
||||
private void addVaultItem(BlockEntityState s, Affine cell, List<EntityCube> cubes) {
|
||||
ItemGeometry ig = items;
|
||||
ItemSpec item = slot(s, 0);
|
||||
if (ig == null || item == null) return;
|
||||
ig.bake(item, "ground", false, cell.mul(Affine.translation(0.5, 0.4, 0.5)), cubes);
|
||||
}
|
||||
|
||||
/**
|
||||
* SpawnerRenderer.submitEntityInSpawner: {@code T(.5,.4,.5)·rotY(spin)·T(0,-.2,0)·rotX(-30°)·S(scale)}
|
||||
* (spin 0, scale 0.53125 / max(width, height) when that exceeds 1), then the mob as the entity
|
||||
* dispatcher places it at the origin (baked at its own position, moved back by -pos).
|
||||
*/
|
||||
private void addSpawnerMob(BlockEntityState s, Affine cell, List<EntityCube> cubes) {
|
||||
CemBaker baker = entities;
|
||||
EntityState e = s.displayEntity();
|
||||
if (baker == null || e == null) return;
|
||||
RenderedEntity mob = baker.bake(e);
|
||||
if (mob == null) return;
|
||||
double scale = 0.53125;
|
||||
double max = Math.max(e.width(), e.height());
|
||||
if (max > 1) scale /= max;
|
||||
Affine m = cell.mul(Affine.translation(0.5, 0.4, 0.5))
|
||||
.mul(Affine.translation(0, -0.2, 0))
|
||||
.mul(Affine.rotX(Math.toRadians(-30)))
|
||||
.mul(Affine.scale(scale))
|
||||
.mul(Affine.translation(-e.x(), -e.y(), -e.z()));
|
||||
for (EntityCube c : mob.cubes) {
|
||||
cubes.add(new EntityCube(c.from, c.to, c.faces, m.mul(c.toWorld), c.additive, c.unshaded));
|
||||
}
|
||||
}
|
||||
|
||||
// BeaconRenderer: sections up to MAX_RENDER_Y (the last one), solid core radius 0.2 rotated by
|
||||
// animationTime·2.25 − 45° (time 0: an axis-aligned square of half-width 0.2/√2), texture v 2.5× per block;
|
||||
// glow radius 0.25 axis-aligned, alpha 32, v 1× per block. Both unlit (full-bright), no caps.
|
||||
private static final int BEAM_MAX = 2048;
|
||||
private static final int BEAM_PIECE = 32; // blocks per cube; textures tiled to cover it
|
||||
|
||||
private void addBeam(BlockEntityState s, Affine cell, List<EntityCube> cubes) {
|
||||
BlockEntityState.Beam beam = s.beam();
|
||||
if (beam == null || beam.sections().isEmpty()) return;
|
||||
int[][] tex = textures.get(ResourceLocation.parse("entity/beacon/beacon_beam"))
|
||||
.orElse(null);
|
||||
if (tex == null) return;
|
||||
double core = 0.2 * beam.radiusScale() / Math.sqrt(2), glow = 0.25 * beam.radiusScale();
|
||||
int start = 0;
|
||||
List<BlockEntityState.BeamSection> secs = beam.sections();
|
||||
for (int i = 0; i < secs.size(); i++) {
|
||||
int height = i == secs.size() - 1 ? BEAM_MAX : secs.get(i).height();
|
||||
int argb = secs.get(i).argb();
|
||||
int[][] coreTex = beamTexture(tex, argb, false);
|
||||
int[][] glowTex = beamTexture(tex, argb, true);
|
||||
for (int y = 0; y < height; y += BEAM_PIECE) {
|
||||
int len = Math.min(BEAM_PIECE, height - y);
|
||||
double v = (double) len / BEAM_PIECE; // textures span BEAM_PIECE blocks; v grows upwards
|
||||
Affine at = cell.mul(Affine.translation(0.5, start + y, 0.5));
|
||||
cubes.add(beamCube(coreTex, core, len, v, at));
|
||||
cubes.add(beamCube(glowTex, glow, len, v, at));
|
||||
}
|
||||
start += height;
|
||||
}
|
||||
}
|
||||
|
||||
private static EntityCube beamCube(int[][] tex, double r, int len, double v, Affine at) {
|
||||
Face side = new Face(tex, 0, v, 1, 0, 0, -1);
|
||||
Face[] faces = new Face[6];
|
||||
for (Direction d : new Direction[] {Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST}) {
|
||||
faces[d.ordinal()] = side;
|
||||
}
|
||||
return new EntityCube(new double[] {-r, 0, -r}, new double[] {r, len, r}, faces, at, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* The beam texture tiled over {@link #BEAM_PIECE} blocks (core 2.5 repeats per block, glow 1) and tinted
|
||||
* with the section colour: the core opaque (BEACON_BEAM_OPAQUE), the glow at alpha 32/255.
|
||||
*/
|
||||
private int[][] beamTexture(int[][] tex, int argb, boolean glowLayer) {
|
||||
return portals.get(DerivedTextureCache.key("beam", tex, argb, glowLayer), () -> {
|
||||
int reps = glowLayer ? BEAM_PIECE : BEAM_PIECE * 5 / 2;
|
||||
int h = tex.length, w = tex[0].length;
|
||||
int[][] out = new int[h * reps][w];
|
||||
for (int y = 0; y < out.length; y++) {
|
||||
for (int x = 0; x < w; x++) {
|
||||
int c = ColorUtil.multiply(tex[y % h][x], argb);
|
||||
int a = glowLayer ? ColorUtil.alpha(tex[y % h][x]) * 32 / 255 : 255;
|
||||
out[y][x] = (a << 24) | (c & 0xFFFFFF);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
// rendertype_end_portal.fsh: end_sky · COLORS[0] + Σ end_portal sampled through 15 (gateway 16) scaled,
|
||||
// rotated and shifted layers. Vanilla projects the textures in screen space; here they are mapped onto
|
||||
// the face plane in world space (one screen ≈ PORTAL_SPAN blocks) at GameTime 0.
|
||||
private static final double[][] PORTAL_COLORS = {
|
||||
{0.022087, 0.098399, 0.110818}, {0.011892, 0.095924, 0.089485}, {0.027636, 0.101689, 0.100326},
|
||||
{0.046564, 0.109883, 0.114838}, {0.064901, 0.117696, 0.097189}, {0.063761, 0.086895, 0.123646},
|
||||
{0.084817, 0.111994, 0.166380}, {0.097489, 0.154120, 0.091064}, {0.106152, 0.131144, 0.195191},
|
||||
{0.097721, 0.110188, 0.187229}, {0.133516, 0.138278, 0.148582}, {0.070006, 0.243332, 0.235792},
|
||||
{0.196766, 0.142899, 0.214696}, {0.047281, 0.315338, 0.321970}, {0.204675, 0.390010, 0.302066},
|
||||
{0.080955, 0.314821, 0.661491}
|
||||
};
|
||||
private static final double PORTAL_SPAN = 16;
|
||||
private static final int PORTAL_RES = 64; // texels per block face edge
|
||||
|
||||
/**
|
||||
* TheEndPortalRenderer: the top and bottom face of {@code T(0,.375,0)·S(1,.375,1)} (y 0.375–0.75);
|
||||
* TheEndGatewayRenderer: the full cube's unoccluded faces. Unlit like the end_portal pipeline.
|
||||
*/
|
||||
private void addPortal(BlockEntityState s, Affine cell, List<EntityCube> cubes) {
|
||||
int[][] sky =
|
||||
textures.get(ResourceLocation.parse("environment/end_sky")).orElse(null);
|
||||
int[][] stars = textures.get(ResourceLocation.parse("entity/end_portal/end_portal"))
|
||||
.orElse(null);
|
||||
if (sky == null || stars == null) return;
|
||||
boolean gateway = s.kind() == BlockEntityState.Kind.END_GATEWAY;
|
||||
double y0 = gateway ? 0 : 0.375, y1 = gateway ? 1 : 0.75;
|
||||
Face[] faces = new Face[6];
|
||||
for (Direction d : Direction.values()) {
|
||||
boolean show = gateway
|
||||
? s.direction() == null
|
||||
|| ("+" + s.direction() + "+")
|
||||
.contains("+" + d.name().toLowerCase(java.util.Locale.ROOT) + "+")
|
||||
: d == Direction.UP || d == Direction.DOWN;
|
||||
if (!show) continue;
|
||||
int layers = gateway ? 16 : 15;
|
||||
faces[d.ordinal()] = new Face(
|
||||
portals.get(
|
||||
DerivedTextureCache.key("portal", sky, stars, layers, d, s.bx(), s.by(), s.bz(), y0, y1),
|
||||
() -> portalFace(sky, stars, layers, d, s.bx(), s.by(), s.bz(), y0, y1)),
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
-1);
|
||||
}
|
||||
cubes.add(new EntityCube(new double[] {0, y0, 0}, new double[] {1, y1, 1}, faces, cell, false, true));
|
||||
}
|
||||
|
||||
/** One face texture: each texel's world point on the face, projected onto the face plane, shaded. */
|
||||
private static int[][] portalFace(
|
||||
int[][] sky, int[][] stars, int layers, Direction d, int bx, int by, int bz, double y0, double y1) {
|
||||
int n = PORTAL_RES;
|
||||
int[][] out = new int[n][n];
|
||||
for (int j = 0; j < n; j++) {
|
||||
for (int i = 0; i < n; i++) {
|
||||
double fs = (i + 0.5) / n, ft = (j + 0.5) / n;
|
||||
// Invert ElementIntersector.faceS/faceT to the face point (local 0..1 per axis).
|
||||
double fx = 0, fy = 0, fz = 0;
|
||||
switch (d) {
|
||||
case UP -> {
|
||||
fx = fs;
|
||||
fz = ft;
|
||||
fy = 1;
|
||||
}
|
||||
case DOWN -> {
|
||||
fx = fs;
|
||||
fz = 1 - ft;
|
||||
}
|
||||
case SOUTH -> {
|
||||
fx = fs;
|
||||
fy = 1 - ft;
|
||||
fz = 1;
|
||||
}
|
||||
case NORTH -> {
|
||||
fx = 1 - fs;
|
||||
fy = 1 - ft;
|
||||
}
|
||||
case WEST -> {
|
||||
fz = fs;
|
||||
fy = 1 - ft;
|
||||
}
|
||||
case EAST -> {
|
||||
fz = 1 - fs;
|
||||
fy = 1 - ft;
|
||||
fx = 1;
|
||||
}
|
||||
}
|
||||
double wx = bx + fx, wy = by + y0 + fy * (y1 - y0), wz = bz + fz;
|
||||
double a, b;
|
||||
switch (d) {
|
||||
case UP, DOWN -> {
|
||||
a = wx;
|
||||
b = wz;
|
||||
}
|
||||
case NORTH, SOUTH -> {
|
||||
a = wx;
|
||||
b = -wy;
|
||||
}
|
||||
default -> {
|
||||
a = wz;
|
||||
b = -wy;
|
||||
}
|
||||
}
|
||||
out[j][i] = portalColor(sky, stars, layers, a / PORTAL_SPAN, b / PORTAL_SPAN);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static int portalColor(int[][] sky, int[][] stars, int layers, double u, double v) {
|
||||
double[] c0 = PORTAL_COLORS[0];
|
||||
int skyTexel = sampleRepeat(sky, u, v);
|
||||
double r = ColorUtil.red(skyTexel) / 255.0 * c0[0];
|
||||
double g = ColorUtil.green(skyTexel) / 255.0 * c0[1];
|
||||
double b = ColorUtil.blue(skyTexel) / 255.0 * c0[2];
|
||||
for (int i = 0; i < layers; i++) {
|
||||
double layer = i + 1;
|
||||
// end_portal_layer: (scale·rotate) then translate (17/layer, GameTime term) then ×0.5 + 0.25.
|
||||
double sc = (4.5 - layer / 4.0) * 2.0;
|
||||
double ang = Math.toRadians((layer * layer * 4321.0 + layer * 9.0) * 2.0);
|
||||
double cos = Math.cos(ang), sin = Math.sin(ang);
|
||||
double x = sc * (u * cos - v * sin), y = sc * (u * sin + v * cos);
|
||||
x = (x + 17.0 / layer) * 0.5 + 0.25;
|
||||
y = y * 0.5 + 0.25;
|
||||
int t = sampleRepeat(stars, x, y);
|
||||
double[] c = PORTAL_COLORS[i];
|
||||
r += ColorUtil.red(t) / 255.0 * c[0];
|
||||
g += ColorUtil.green(t) / 255.0 * c[1];
|
||||
b += ColorUtil.blue(t) / 255.0 * c[2];
|
||||
}
|
||||
return ColorUtil.argb(
|
||||
255, (int) Math.min(255, Math.round(r * 255)), (int) Math.min(255, Math.round(g * 255)), (int)
|
||||
Math.min(255, Math.round(b * 255)));
|
||||
}
|
||||
|
||||
private static int sampleRepeat(int[][] tex, double u, double v) {
|
||||
int h = tex.length, w = tex[0].length;
|
||||
int x = Math.floorMod((int) Math.floor(u * w), w), y = Math.floorMod((int) Math.floor(v * h), h);
|
||||
return tex[y][x];
|
||||
}
|
||||
|
||||
// --- textures ---
|
||||
|
||||
private int[][] texture(BlockEntityState s, BlockEntityModels.Part part) {
|
||||
|
||||
@@ -97,6 +97,39 @@ public final class ItemGeometry {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ItemStackRenderState.getModelBoundingBox: the model's quad extents through the display transform
|
||||
* ({@code display·T(-0.5)}), as {minX, minY, minZ, maxX, maxY, maxZ}; null when nothing renders. Generated
|
||||
* items span their whole sprite square (the 16×16 front/back quads), not just the opaque texels.
|
||||
*/
|
||||
public double[] modelBounds(ItemSpec item, String context) {
|
||||
if (item == null) return null;
|
||||
Info info = info(item.id(), context);
|
||||
List<EntityCube> cubes = new ArrayList<>();
|
||||
if (info.special == null && !info.layers.isEmpty()) {
|
||||
Affine m = displayTransform(info.display, false).mul(Affine.translation(-0.5, -0.5, -0.5));
|
||||
cubes.add(new EntityCube(new double[] {0, 0, 7.5 / 16}, new double[] {1, 1, 8.5 / 16}, new Face[6], m));
|
||||
} else {
|
||||
bake(item, context, false, Affine.identity(), cubes);
|
||||
}
|
||||
if (cubes.isEmpty()) return null;
|
||||
double[] box = {
|
||||
Double.MAX_VALUE,
|
||||
Double.MAX_VALUE,
|
||||
Double.MAX_VALUE,
|
||||
-Double.MAX_VALUE,
|
||||
-Double.MAX_VALUE,
|
||||
-Double.MAX_VALUE
|
||||
};
|
||||
for (EntityCube c : cubes) {
|
||||
for (int a = 0; a < 3; a++) {
|
||||
box[a] = Math.min(box[a], c.aabbMin[a]);
|
||||
box[a + 3] = Math.max(box[a + 3], c.aabbMax[a]);
|
||||
}
|
||||
}
|
||||
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;
|
||||
@@ -157,14 +190,21 @@ public final class ItemGeometry {
|
||||
List<Integer> tints = new ArrayList<>();
|
||||
Optional<JsonObject> def = reader.readJsonObject("assets/minecraft/items/" + id + ".json");
|
||||
if (def.isPresent() && def.get().has("model")) {
|
||||
JsonObject node = pick(def.get().getAsJsonObject("model"), context);
|
||||
Affine[] outer = {null};
|
||||
JsonObject node = pick(def.get().getAsJsonObject("model"), context, outer);
|
||||
if (node != null) {
|
||||
String type = str(node, "type");
|
||||
if (type.endsWith("special")) {
|
||||
modelRef = str(node, "base");
|
||||
JsonObject sm = node.has("model") ? node.getAsJsonObject("model") : null;
|
||||
special = sm == null ? null : str(sm, "type").replace("minecraft:", "");
|
||||
if (node.has("transformation")) specialXf = transformation(node.getAsJsonObject("transformation"));
|
||||
// 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).
|
||||
specialXf = outer[0];
|
||||
if (node.has("transformation")) {
|
||||
Affine own = transformation(node.getAsJsonObject("transformation"));
|
||||
specialXf = specialXf == null ? own : specialXf.mul(own);
|
||||
}
|
||||
} else {
|
||||
modelRef = str(node, "model");
|
||||
}
|
||||
@@ -212,10 +252,17 @@ public final class ItemGeometry {
|
||||
return new Info(display, layers, tints, special, specialXf);
|
||||
}
|
||||
|
||||
/** Walks an item-model node to the concrete model/special for a display context. */
|
||||
private static JsonObject pick(JsonObject node, String context) {
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
private static JsonObject pick(JsonObject node, String context, Affine[] outer) {
|
||||
for (int depth = 0; node != null && depth < 16; depth++) {
|
||||
String type = str(node, "type").replace("minecraft:", "");
|
||||
if (!type.equals("model") && !type.equals("special") && node.has("transformation")) {
|
||||
Affine t = transformation(node.getAsJsonObject("transformation"));
|
||||
outer[0] = outer[0] == null ? t : outer[0].mul(t);
|
||||
}
|
||||
switch (type) {
|
||||
case "model", "special" -> {
|
||||
return node;
|
||||
@@ -339,7 +386,7 @@ public final class ItemGeometry {
|
||||
private void bakeSpecial(ItemSpec item, Info info, Affine m, List<EntityCube> out) {
|
||||
switch (info.special) {
|
||||
case "shield", "trident" -> {
|
||||
// Entity model in Java model space; the special renderer flips it with scale(1,-1,-1).
|
||||
// Entity model in Java model space; the item definition's transformation flips it (1,-1,-1).
|
||||
boolean shield = info.special.equals("shield");
|
||||
CemModelLoader.CemModel model = models.vanilla(shield ? "layer/shield#main" : "trident");
|
||||
if (model == null) model = models.vanilla("layer/" + info.special + "#main");
|
||||
@@ -348,9 +395,7 @@ public final class ItemGeometry {
|
||||
.orElse(null);
|
||||
if (model == null || tex == null) return;
|
||||
Affine xf = info.specialTransform != null ? m.mul(info.specialTransform) : m;
|
||||
Affine pre = xf.mul(Affine.scale(1, -1, -1))
|
||||
.mul(Affine.scale(1 / 16.0))
|
||||
.mul(CemGeometry.CEM_TO_JAVA);
|
||||
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()));
|
||||
}
|
||||
|
||||
@@ -95,6 +95,10 @@ public class DefaultScreenRenderer implements Renderer {
|
||||
entityBaker == null ? null : entityBaker.models(),
|
||||
blockEntityBaker);
|
||||
if (entityBaker != null) entityBaker.setItemGeometry(items);
|
||||
if (blockEntityBaker != null) {
|
||||
blockEntityBaker.setItemGeometry(items); // campfire/shelf/vault contents
|
||||
blockEntityBaker.setEntityBaker(entityBaker); // spawner mobs
|
||||
}
|
||||
this.decorationBaker = new DecorationBaker(textures, registry, items);
|
||||
this.objectBaker = new ObjectBaker(items, blockEntityBaker == null ? null : blockEntityBaker.font());
|
||||
this.logger = logger;
|
||||
|
||||
+263
-10
@@ -4,23 +4,33 @@ import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.BannerPattern;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.ChestKind;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.Kind;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.EntityState;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.ItemSpec;
|
||||
import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.DyeColor;
|
||||
import org.bukkit.HeightMap;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Banner;
|
||||
import org.bukkit.block.Beacon;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.BlockState;
|
||||
import org.bukkit.block.Campfire;
|
||||
import org.bukkit.block.Conduit;
|
||||
import org.bukkit.block.CreatureSpawner;
|
||||
import org.bukkit.block.DecoratedPot;
|
||||
import org.bukkit.block.Shelf;
|
||||
import org.bukkit.block.Sign;
|
||||
import org.bukkit.block.Skull;
|
||||
import org.bukkit.block.TrialSpawner;
|
||||
import org.bukkit.block.Vault;
|
||||
import org.bukkit.block.banner.Pattern;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.block.data.Directional;
|
||||
@@ -29,6 +39,9 @@ import org.bukkit.block.data.type.Chest;
|
||||
import org.bukkit.block.data.type.Lectern;
|
||||
import org.bukkit.block.sign.Side;
|
||||
import org.bukkit.block.sign.SignSide;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.EntitySnapshot;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.BannerMeta;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.inventory.meta.SkullMeta;
|
||||
@@ -61,9 +74,12 @@ public final class BlockEntitySnapshotBuilder {
|
||||
if (!world.isChunkLoaded(cx, cz)) continue;
|
||||
for (BlockState ts : world.getChunkAt(cx, cz).getTileEntities(false)) {
|
||||
int bx = ts.getX(), by = ts.getY(), bz = ts.getZ();
|
||||
if (bx < bMinX || bx > bMaxX || by < bMinY || by > bMaxY || bz < bMinZ || bz > bMaxZ) continue;
|
||||
// A beacon's beam rises from below: keep beacons under the view box too.
|
||||
boolean below = by < bMinY && ts instanceof Beacon;
|
||||
if (bx < bMinX || bx > bMaxX || (by < bMinY && !below) || by > bMaxY || bz < bMinZ || bz > bMaxZ)
|
||||
continue;
|
||||
try {
|
||||
BlockEntityState s = classify(ts);
|
||||
BlockEntityState s = classify(ts.getBlockData(), bx, by, bz, ts, eye);
|
||||
if (s != null) out.add(s);
|
||||
} catch (Throwable ignored) {
|
||||
// Unsupported on this server version / odd state — skip this one.
|
||||
@@ -74,10 +90,6 @@ public final class BlockEntitySnapshotBuilder {
|
||||
return out;
|
||||
}
|
||||
|
||||
private static BlockEntityState classify(BlockState ts) {
|
||||
return classify(ts.getBlockData(), ts.getX(), ts.getY(), ts.getZ(), ts);
|
||||
}
|
||||
|
||||
/**
|
||||
* A block-entity state from bare block data (no world tile entity) at cell (0,0,0): for block-entity
|
||||
* blocks shown by BLOCK DISPLAYS, minecarts and special ITEMS (chest/head/banner/shulker/… items).
|
||||
@@ -86,7 +98,7 @@ public final class BlockEntitySnapshotBuilder {
|
||||
*/
|
||||
public static BlockEntityState fromBlockData(BlockData data) {
|
||||
try {
|
||||
return classify(data, 0, 0, 0, null);
|
||||
return classify(data, 0, 0, 0, null, null);
|
||||
} catch (Throwable t) {
|
||||
return null;
|
||||
}
|
||||
@@ -135,10 +147,18 @@ public final class BlockEntitySnapshotBuilder {
|
||||
patterns,
|
||||
s.sherds(),
|
||||
s.frontText(),
|
||||
s.backText());
|
||||
s.backText(),
|
||||
s.items(),
|
||||
s.displayEntity(),
|
||||
s.beam(),
|
||||
s.pose());
|
||||
}
|
||||
|
||||
private static BlockEntityState classify(BlockData data, int bx, int by, int bz, BlockState ts) {
|
||||
/**
|
||||
* {@code ts} = the live tile (null for bare block data), {@code eye} = camera position (null: none) for
|
||||
* the camera-dependent parts (enchanting-table book, beacon beam width).
|
||||
*/
|
||||
private static BlockEntityState classify(BlockData data, int bx, int by, int bz, BlockState ts, Location eye) {
|
||||
Material mat = data.getMaterial();
|
||||
String n = mat.name();
|
||||
|
||||
@@ -263,9 +283,214 @@ public final class BlockEntitySnapshotBuilder {
|
||||
return base(Kind.BELL, bx, by, bz, 0).build();
|
||||
}
|
||||
|
||||
// --- enchanting table: the floating book (EnchantingTableBlockEntity.bookAnimationTick) ---
|
||||
if (mat == Material.ENCHANTING_TABLE) {
|
||||
return enchantingTable(bx, by, bz, eye);
|
||||
}
|
||||
|
||||
// --- campfire items (CampfireRenderer) ---
|
||||
if (data instanceof org.bukkit.block.data.type.Campfire) {
|
||||
Builder b = base(Kind.CAMPFIRE, bx, by, bz, facingYaw(data));
|
||||
if (ts instanceof Campfire c) {
|
||||
List<ItemSpec> items = new ArrayList<>(c.getSize());
|
||||
for (int i = 0; i < c.getSize(); i++) items.add(ItemSpecs.of(c.getItem(i)));
|
||||
b.items(items);
|
||||
}
|
||||
return b.build();
|
||||
}
|
||||
|
||||
// --- shelf items (ShelfRenderer; align_items_to_bottom has no API → false) ---
|
||||
if (data instanceof org.bukkit.block.data.type.Shelf) {
|
||||
Builder b = base(Kind.SHELF, bx, by, bz, facingYaw(data));
|
||||
if (ts instanceof Shelf shelf) {
|
||||
ItemStack[] content = shelf.getSnapshotInventory().getContents();
|
||||
List<ItemSpec> items = new ArrayList<>(content.length);
|
||||
for (ItemStack it : content) items.add(ItemSpecs.of(it));
|
||||
b.items(items);
|
||||
}
|
||||
return b.build();
|
||||
}
|
||||
|
||||
// --- copper golem statue: [WAXED_][EXPOSED_|WEATHERED_|OXIDIZED_]COPPER_GOLEM_STATUE ---
|
||||
if (data instanceof org.bukkit.block.data.type.CopperGolemStatue statue) {
|
||||
String stage =
|
||||
n.replace("WAXED_", "").replace("COPPER_GOLEM_STATUE", "").replace("_", "");
|
||||
return base(Kind.COPPER_GOLEM_STATUE, bx, by, bz, facingYaw(data))
|
||||
.colorName(stage.isEmpty() ? null : stage.toLowerCase(Locale.ROOT))
|
||||
.pose(statue.getCopperGolemPose().name().toLowerCase(Locale.ROOT))
|
||||
.build();
|
||||
}
|
||||
|
||||
// --- spawners: the spinning mini mob (SpawnerRenderer / TrialSpawnerRenderer) ---
|
||||
if (mat == Material.SPAWNER || n.equals("TRIAL_SPAWNER")) {
|
||||
return base(Kind.SPAWNER, bx, by, bz, 0)
|
||||
.displayEntity(ts == null ? null : spawnerEntity(ts, data))
|
||||
.build();
|
||||
}
|
||||
|
||||
// --- vault: the displayed key/reward item while a player is connected (VaultRenderer) ---
|
||||
if (n.equals("VAULT")) {
|
||||
Builder b = base(Kind.VAULT, bx, by, bz, facingYaw(data));
|
||||
if (ts instanceof Vault vault && !vault.getConnectedPlayers().isEmpty()) {
|
||||
ItemSpec item = ItemSpecs.of(vault.getDisplayedItem());
|
||||
if (item != null) b.items(List.of(item));
|
||||
}
|
||||
return b.build();
|
||||
}
|
||||
|
||||
// --- beacon beam (BeaconRenderer; only with a pyramid: getBeamSections is empty at level 0) ---
|
||||
if (mat == Material.BEACON) {
|
||||
Builder b = base(Kind.BEACON, bx, by, bz, 0);
|
||||
if (ts instanceof Beacon beacon && beacon.getTier() > 0) b.beam(beam(ts.getWorld(), bx, by, bz, eye));
|
||||
return b.build();
|
||||
}
|
||||
|
||||
// --- end portal / gateway (TheEndPortalRenderer: top + bottom; gateway: unoccluded faces) ---
|
||||
if (mat == Material.END_PORTAL) {
|
||||
return base(Kind.END_PORTAL, bx, by, bz, 0).build();
|
||||
}
|
||||
if (mat == Material.END_GATEWAY) {
|
||||
return base(Kind.END_GATEWAY, bx, by, bz, 0)
|
||||
.direction(ts == null ? null : gatewayFaces(ts))
|
||||
.build();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- contents helpers ---
|
||||
|
||||
/**
|
||||
* EnchantingTableBlockEntity.bookAnimationTick: with a player within 3 blocks of the table centre the book
|
||||
* opens and turns to {@code atan2(dz, dx)}; otherwise it is closed (its idle yaw drifts — static 0 here).
|
||||
* The photographer's feet ({@code eye − 1.62}) stand in for the nearest player.
|
||||
*/
|
||||
private static BlockEntityState enchantingTable(int bx, int by, int bz, Location eye) {
|
||||
boolean open = false;
|
||||
float yaw = 0;
|
||||
if (eye != null) {
|
||||
double dx = eye.getX() - (bx + 0.5), dy = eye.getY() - 1.62 - (by + 0.5), dz = eye.getZ() - (bz + 0.5);
|
||||
if (dx * dx + dy * dy + dz * dz <= 9) {
|
||||
open = true;
|
||||
yaw = (float) Math.toDegrees(Math.atan2(dz, dx));
|
||||
}
|
||||
}
|
||||
return base(Kind.ENCHANTING_TABLE, bx, by, bz, yaw).active(open).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* The spawner's display mob as an entity state at its own (unspawned) position: the spawn data's entity
|
||||
* snapshot (vanilla BaseSpawner/TrialSpawnerStateData.getOrCreateDisplayEntity loads the same NBT;
|
||||
* {@code createEntity(World)} loads it without spawning or finalizeSpawn randomisation), else a bare
|
||||
* entity of the spawned type. Trial spawners show it only while waiting/active, from the
|
||||
* ominous configuration when ominous. Null when the spawner is empty.
|
||||
*/
|
||||
private static EntityState spawnerEntity(BlockState ts, BlockData data) {
|
||||
try {
|
||||
org.bukkit.spawner.BaseSpawner spawner;
|
||||
if (ts instanceof CreatureSpawner cs) {
|
||||
spawner = cs;
|
||||
} else if (ts instanceof TrialSpawner trial && data instanceof org.bukkit.block.data.type.TrialSpawner td) {
|
||||
String st = td.getTrialSpawnerState().name();
|
||||
if (!st.equals("WAITING_FOR_PLAYERS") && !st.equals("ACTIVE")) return null;
|
||||
spawner = trial.isOminous() ? trial.getOminousConfiguration() : trial.getNormalConfiguration();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
EntitySnapshot snap = spawner.getSpawnedEntity();
|
||||
if (snap == null && !spawner.getPotentialSpawns().isEmpty()) {
|
||||
snap = spawner.getPotentialSpawns().getFirst().getSnapshot();
|
||||
}
|
||||
if (snap == null && spawner.getSpawnedType() != null) {
|
||||
snap = Bukkit.getEntityFactory()
|
||||
.createEntitySnapshot(
|
||||
"{id:\"" + spawner.getSpawnedType().getKey() + "\"}");
|
||||
}
|
||||
if (snap == null) return null;
|
||||
// createEntity(World), NOT createEntity(Location): the Location variant ADDS the entity to the world.
|
||||
// The display state sits wherever the bare entity is; the baker moves it to the cage by -position.
|
||||
Entity e = snap.createEntity(ts.getWorld());
|
||||
return e == null ? null : EntitySnapshotBuilder.displayState(e);
|
||||
} catch (Throwable t) {
|
||||
return null; // API drift / unknown entity
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BeaconBlockEntity.tick's beam scan from the beacon up to the WORLD_SURFACE height: the beacon starts a
|
||||
* white section, stained glass (panes) starts a section of its dye colour (the first one over the beacon
|
||||
* replaces white, later ones average with the previous colour, equal colours just extend it), other
|
||||
* blocks extend the current section, and a light-blocking block (except bedrock) stops the beam.
|
||||
*/
|
||||
private static BlockEntityState.Beam beam(World world, int bx, int by, int bz, Location eye) {
|
||||
int top = world.getHighestBlockYAt(bx, bz, HeightMap.WORLD_SURFACE);
|
||||
List<int[]> sections = new ArrayList<>(); // {argb, height}
|
||||
for (int y = by; y <= top; y++) {
|
||||
Material m = y == by ? Material.BEACON : world.getBlockAt(bx, y, bz).getType();
|
||||
int color = beamColor(m);
|
||||
if (color != 0) {
|
||||
if (sections.size() <= 1) {
|
||||
sections.add(new int[] {color, 1});
|
||||
} else {
|
||||
int[] last = sections.getLast();
|
||||
if (color == last[0]) last[1]++;
|
||||
else sections.add(new int[] {average(last[0], color), 1});
|
||||
}
|
||||
} else {
|
||||
if (sections.isEmpty() || (blocksBeam(m) && m != Material.BEDROCK)) return null;
|
||||
sections.getLast()[1]++;
|
||||
}
|
||||
}
|
||||
List<BlockEntityState.BeamSection> out = new ArrayList<>(sections.size());
|
||||
for (int[] sec : sections) out.add(new BlockEntityState.BeamSection(sec[0], sec[1]));
|
||||
float scale = 1;
|
||||
if (eye != null) {
|
||||
double dx = eye.getX() - (bx + 0.5), dz = eye.getZ() - (bz + 0.5);
|
||||
scale = (float) Math.max(1, Math.sqrt(dx * dx + dz * dz) / 96);
|
||||
}
|
||||
return out.isEmpty() ? null : new BlockEntityState.Beam(out, scale);
|
||||
}
|
||||
|
||||
/** BeaconBeamBlock colour (DyeColor texture-diffuse colour) of a beacon/stained glass (pane); 0 = none. */
|
||||
private static int beamColor(Material m) {
|
||||
if (m == Material.BEACON) return ColorUtil.dyeArgb(DyeColor.WHITE, 0);
|
||||
String n = m.name();
|
||||
int i = n.indexOf("_STAINED_GLASS");
|
||||
if (i < 0) return 0;
|
||||
try {
|
||||
return ColorUtil.dyeArgb(DyeColor.valueOf(n.substring(0, i)), 0);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** Light dampening 15 (full opaque cubes, tinted glass) — the beam stops there. */
|
||||
private static boolean blocksBeam(Material m) {
|
||||
return m.isOccluding() || m == Material.TINTED_GLASS;
|
||||
}
|
||||
|
||||
/** ARGB.average: per-channel integer mean. */
|
||||
private static int average(int a, int b) {
|
||||
return ColorUtil.argb(
|
||||
(ColorUtil.alpha(a) + ColorUtil.alpha(b)) / 2,
|
||||
(ColorUtil.red(a) + ColorUtil.red(b)) / 2,
|
||||
(ColorUtil.green(a) + ColorUtil.green(b)) / 2,
|
||||
(ColorUtil.blue(a) + ColorUtil.blue(b)) / 2);
|
||||
}
|
||||
|
||||
/** TheEndGatewayBlockEntity.shouldRenderFace ≈ the neighbour is not a full opaque cube; "+"-joined. */
|
||||
private static String gatewayFaces(BlockState ts) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (BlockFace f : new BlockFace[] {
|
||||
BlockFace.DOWN, BlockFace.UP, BlockFace.NORTH, BlockFace.SOUTH, BlockFace.WEST, BlockFace.EAST
|
||||
}) {
|
||||
if (ts.getBlock().getRelative(f).getType().isOccluding()) continue;
|
||||
if (!sb.isEmpty()) sb.append('+');
|
||||
sb.append(f.name().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// --- facing / rotation helpers ---
|
||||
|
||||
/** Yaw preferring the block's {@link Directional} facing (wall-mounted block-entities). */
|
||||
@@ -397,6 +622,10 @@ public final class BlockEntitySnapshotBuilder {
|
||||
private List<String> sherds = List.of();
|
||||
private BlockEntityState.SignText frontText;
|
||||
private BlockEntityState.SignText backText;
|
||||
private List<ItemSpec> items = List.of();
|
||||
private EntityState displayEntity;
|
||||
private BlockEntityState.Beam beam;
|
||||
private String pose;
|
||||
|
||||
Builder(Kind kind, int bx, int by, int bz, float yaw) {
|
||||
this.kind = kind;
|
||||
@@ -461,6 +690,26 @@ public final class BlockEntitySnapshotBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder items(List<ItemSpec> v) {
|
||||
this.items = v;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder displayEntity(EntityState v) {
|
||||
this.displayEntity = v;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder beam(BlockEntityState.Beam v) {
|
||||
this.beam = v;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder pose(String v) {
|
||||
this.pose = v;
|
||||
return this;
|
||||
}
|
||||
|
||||
BlockEntityState build() {
|
||||
return new BlockEntityState(
|
||||
kind,
|
||||
@@ -478,7 +727,11 @@ public final class BlockEntitySnapshotBuilder {
|
||||
patterns,
|
||||
sherds,
|
||||
frontText,
|
||||
backText);
|
||||
backText,
|
||||
items,
|
||||
displayEntity,
|
||||
beam,
|
||||
pose);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,6 +113,14 @@ public final class EntitySnapshotBuilder {
|
||||
return states;
|
||||
}
|
||||
|
||||
/**
|
||||
* A not-spawned display entity (spawner mob) as a state at its own position; null for technical types.
|
||||
* MUST run on the main thread.
|
||||
*/
|
||||
public static EntityState displayState(Entity e) {
|
||||
return toState(e);
|
||||
}
|
||||
|
||||
private static EntityState toState(Entity e) {
|
||||
Location loc = e.getLocation();
|
||||
// Skip non-renderable technical entities.
|
||||
|
||||
@@ -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]}]}]}],"renderType":"entity_cutout"}},"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]}]}]}],"renderType":"entity_cutout"}},"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":[-Line truncated
|
||||
{"version":"26.3","models":{"acacia_boat":{"model":{"textureSize":[128,64],"absolute":true,"layer":"boat/acacia#main","models":[{"part":"root","pivot":[0.0,24.0,0.0],"rotate":[0.0,0.0,0.0],"submodels":[{"part":"right_paddle","pivot":[-3.0,29.0,-9.0],"rotate":[37.5,-142.1347,11.25],"boxes":[{"coordinates":[-4.0,27.0,-14.0,2.0,2.0,18.0],"textureOffset":[62.0,20.0]},{"coordinates":[-4.001,26.0,-1.0,1.0,6.0,7.0],"textureOffset":[62.0,20.0]}]},{"part":"left_paddle","pivot":[-3.0,29.0,9.0],"rotate":[37.5,-37.8653,11.25],"boxes":[{"coordinates":[-4.0,27.0,4.0,2.0,2.0,18.0],"textureOffset":[62.0,0.0]},{"coordinates":[-2.999,26.0,17.0,1.0,6.0,7.0],"textureOffset":[62.0,0.0]}]},{"part":"left","pivot":[0.0,20.0,9.0],"rotate":[0.0,0.0,0.0],"boxes":[{"coordinates":[-14.0,21.0,8.0,28.0,6.0,2.0],"textureOffset":[0.0,43.0]}]},{"part":"bottom","pivot":[0.0,21.0,1.0],"rotate":[-90.0,0.0,0.0],"boxes":[{"coordinates":[-14.0,14.0,-2.0,28.0,16.0,3.0],"textureOffset":[0.0,0.0]}]},{"part":"back","pivot":[15.0,20.0,4.0],"rotate":[0.0,-270.0,0.0],"boxes":[{"coordinates":[10.0,21.0,3.0,18.0,6.0,2.0],"textureOffset":[0.0,19.0]}]},{"part":"front","pivot":[-15.0,20.0,0.0],"rotate":[0.0,-90.0,0.0],"boxes":[{"coordinates":[-23.0,21.0,-1.0,16.0,6.0,2.0],"textureOffset":[0.0,27.0]}]},{"part":"right","pivot":[0.0,20.0,-9.0],"rotate":[0.0,-180.0,0.0],"boxes":[{"coordinates":[-14.0,21.0,-10.0,28.0,6.0,2.0],"textureOffset":[0.0,35.0]}]}]}],"renderType":"entity_cutout"}},"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]}]}]}],"renderType":"entity_cutout"}},"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":[-Line truncated
|
||||
@@ -199,6 +199,35 @@ public class BlockEntityTestRender {
|
||||
scenes.put("wall_piglin_E", List.of(head(Kind.WALL_HEAD, "piglin", 270)));
|
||||
frontCam.put("wall_piglin_E", 270f);
|
||||
|
||||
// --- block-entity contents (no block models in this harness: tables/campfires/shelves/cages absent) ---
|
||||
scenes.put("enchant_book_closed", List.of(be(Kind.ENCHANTING_TABLE, 0)));
|
||||
scenes.put("enchant_book_open", List.of(st(Kind.ENCHANTING_TABLE, 0, 0, 90, null, true, null, 0, null, List.of(), List.of())));
|
||||
frontCam.put("enchant_book_open", 0f);
|
||||
scenes.put("campfire_items", List.of(contents(Kind.CAMPFIRE, 0, items("beef", "cod", "porkchop", null), null, null, null, null)));
|
||||
scenes.put("campfire_items_E", List.of(contents(Kind.CAMPFIRE, 270, items("beef", "cod", "porkchop", "salmon"), null, null, null, null)));
|
||||
scenes.put("shelf_items_S", List.of(contents(Kind.SHELF, 0, items("diamond", "book", "golden_apple"), null, null, null, null)));
|
||||
frontCam.put("shelf_items_S", 0f);
|
||||
scenes.put("shelf_items_E", List.of(contents(Kind.SHELF, 270, items("stick", null, "emerald"), null, null, null, null)));
|
||||
frontCam.put("shelf_items_E", 270f);
|
||||
scenes.put("vault_item", List.of(contents(Kind.VAULT, 0, items("trial_key"), null, null, null, null)));
|
||||
frontCam.put("vault_item", 0f);
|
||||
String[] poses = {"standing", "running", "sitting", "star"};
|
||||
String[] stages = {null, "exposed", "weathered", "oxidized"};
|
||||
for (int i = 0; i < 4; i++) {
|
||||
scenes.put("statue_" + poses[i], List.of(contents(Kind.COPPER_GOLEM_STATUE, i * 90, List.of(), null, null, poses[i], stages[i])));
|
||||
frontCam.put("statue_" + poses[i], i * 90f);
|
||||
}
|
||||
for (String mob : new String[] {"zombie", "pig", "spider", "blaze"}) {
|
||||
scenes.put("spawner_" + mob, List.of(contents(Kind.SPAWNER, 0, List.of(), spawnerMob(mob), null, null, null)));
|
||||
frontCam.put("spawner_" + mob, 0f);
|
||||
}
|
||||
scenes.put("beacon_beam", List.of(contents(Kind.BEACON, 0, List.of(), null,
|
||||
new Beam(List.of(new BeamSection(0xFFF9FFFE, 1), new BeamSection(0xFFB02E26, 2), new BeamSection(0xFF3C44AA, 1)), 1f), null, null)));
|
||||
List<BlockEntityState> portal = new ArrayList<>();
|
||||
for (int x = 0; x < 3; x++) for (int z = 0; z < 3; z++) portal.add(st(Kind.END_PORTAL, x, z, 0, null, false, null, 0, null, List.of(), List.of()));
|
||||
scenes.put("end_portal", portal);
|
||||
scenes.put("end_gateway", List.of(st(Kind.END_GATEWAY, 0, 0, 0, "down+up+north+south+west+east", false, null, 0, null, List.of(), List.of())));
|
||||
|
||||
// Decoration scenes (paintings + item frames), rendered via the same scene path.
|
||||
LinkedHashMap<String, List<DecorationState>> decoScenes = new LinkedHashMap<>();
|
||||
// kebab is 1x1; place a thin quad facing south spanning the block at z=0..0.0625.
|
||||
@@ -252,6 +281,42 @@ public class BlockEntityTestRender {
|
||||
List.of(), List.of(), null, null);
|
||||
}
|
||||
|
||||
/** A contents-bearing block-entity at the origin (items / spawner mob / beacon beam / statue pose). */
|
||||
static BlockEntityState contents(Kind kind, float yaw, List<eu.mhsl.minecraft.pixelpics.render.entity.ItemSpec> items,
|
||||
EntityState mob, Beam beam, String pose, String color) {
|
||||
return new BlockEntityState(kind, 0, 0, 0, yaw, null, false, null, 0, color, null, null,
|
||||
List.of(), List.of(), null, null, items, mob, beam, pose);
|
||||
}
|
||||
|
||||
static List<eu.mhsl.minecraft.pixelpics.render.entity.ItemSpec> items(String... ids) {
|
||||
List<eu.mhsl.minecraft.pixelpics.render.entity.ItemSpec> out = new ArrayList<>();
|
||||
for (String id : ids) out.add(id == null ? null : new eu.mhsl.minecraft.pixelpics.render.entity.ItemSpec(id, null, null, false));
|
||||
return out;
|
||||
}
|
||||
|
||||
/** EntityTestRender's default state of a mob with its real bounding box (spawner scale depends on it). */
|
||||
static EntityState spawnerMob(String type) throws Exception {
|
||||
EntityState e = EntityTestRender.state(type, 0);
|
||||
double[] dims = switch (type) {
|
||||
case "zombie" -> new double[] {0.6, 1.95};
|
||||
case "pig" -> new double[] {0.9, 0.9};
|
||||
case "spider" -> new double[] {1.4, 0.9};
|
||||
default -> new double[] {0.6, 1.8};
|
||||
};
|
||||
var comps = EntityState.class.getRecordComponents();
|
||||
Object[] vals = new Object[comps.length];
|
||||
Class<?>[] types = new Class<?>[comps.length];
|
||||
for (int i = 0; i < comps.length; i++) {
|
||||
types[i] = comps[i].getType();
|
||||
vals[i] = switch (comps[i].getName()) {
|
||||
case "width" -> dims[0];
|
||||
case "height" -> dims[1];
|
||||
default -> comps[i].getAccessor().invoke(e);
|
||||
};
|
||||
}
|
||||
return EntityState.class.getDeclaredConstructor(types).newInstance(vals);
|
||||
}
|
||||
|
||||
static BlockEntityState head(Kind kind, String type, float yaw) {
|
||||
return st(kind, 0, 0, yaw, null, false, null, 0, type, List.of(), List.of());
|
||||
}
|
||||
@@ -266,6 +331,7 @@ public class BlockEntityTestRender {
|
||||
for (int a = 0; a < 3; a++) { min[a] = Math.min(min[a], re.aabbMin[a]); max[a] = Math.max(max[a], re.aabbMax[a]); }
|
||||
}
|
||||
if (min[0] > max[0]) { min = new double[]{0, 0, 0}; max = new double[]{1, 1, 1}; }
|
||||
if (states.get(0).kind() == Kind.BEACON) max[1] = min[1] + 4; // frame the beam's foot, not 2048 blocks
|
||||
double cx = (min[0] + max[0]) / 2, cy = (min[1] + max[1]) / 2, cz = (min[2] + max[2]) / 2;
|
||||
double ext = 0.5;
|
||||
for (int a = 0; a < 3; a++) ext = Math.max(ext, max[a] - min[a]);
|
||||
|
||||
@@ -185,6 +185,29 @@ public class VanillaModelDump {
|
||||
} catch (ReflectiveOperationException e) {
|
||||
failed.add("posed/book (" + e + ")");
|
||||
}
|
||||
// EnchantTableRenderer: BookModel.State.forAnimation(time 0, flip 0 → pages 0.1/0.9, open) — closed
|
||||
// (idle, no player within 3 blocks) and fully open (a player nearby).
|
||||
for (float open : new float[] {0F, 1F}) {
|
||||
posedBlockEntity(out, failed, set, net.minecraft.client.model.geom.ModelLayers.BOOK,
|
||||
"posed/enchanting_book_" + (open == 0 ? "closed" : "open") + "#main",
|
||||
m -> new net.minecraft.client.model.object.book.BookModel(m),
|
||||
net.minecraft.client.model.object.book.BookModel.State.forAnimation(0F, 0.1F, 0.9F, open));
|
||||
}
|
||||
// CopperGolemStatueBlockRenderer: one baked layer per statue pose, CopperGolemStatueModel.setupAnim
|
||||
// (root.y = 0, root.zRot = π).
|
||||
String[][] statues = {{"standing", "COPPER_GOLEM"}, {"running", "COPPER_GOLEM_RUNNING"},
|
||||
{"sitting", "COPPER_GOLEM_SITTING"}, {"star", "COPPER_GOLEM_STAR"}};
|
||||
for (String[] st : statues) {
|
||||
try {
|
||||
ModelLayerLocation loc = (ModelLayerLocation) net.minecraft.client.model.geom.ModelLayers.class
|
||||
.getField(st[1]).get(null);
|
||||
posedBlockEntity(out, failed, set, loc, "posed/copper_golem_statue_" + st[0] + "#main",
|
||||
m -> new net.minecraft.client.model.object.statue.CopperGolemStatueModel(m),
|
||||
net.minecraft.util.Unit.INSTANCE);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
failed.add("posed/copper_golem_statue_" + st[0] + " (" + e + ")");
|
||||
}
|
||||
}
|
||||
JsonObject root = new JsonObject();
|
||||
root.addProperty("version", SharedConstants.getCurrentVersion().name());
|
||||
JsonObject models = new JsonObject();
|
||||
@@ -607,7 +630,18 @@ public class VanillaModelDump {
|
||||
ModelLayerLocation loc,
|
||||
java.util.function.Function<ModelPart, Model<?>> factory,
|
||||
Object state) {
|
||||
String name = "posed/" + loc.model().getPath() + "#" + loc.layer();
|
||||
posedBlockEntity(out, failed, set, loc, "posed/" + loc.model().getPath() + "#" + loc.layer(), factory, state);
|
||||
}
|
||||
|
||||
/** {@link #posedBlockEntity} under an explicit name (several static states of one layer). */
|
||||
static void posedBlockEntity(
|
||||
Map<String, JsonObject> out,
|
||||
List<String> failed,
|
||||
EntityModelSet set,
|
||||
ModelLayerLocation loc,
|
||||
String name,
|
||||
java.util.function.Function<ModelPart, Model<?>> factory,
|
||||
Object state) {
|
||||
try {
|
||||
JsonObject jem = dump(factory.apply(set.bakeLayer(loc)), state, name, failed);
|
||||
if (jem == null) return;
|
||||
|
||||
@@ -189,6 +189,72 @@ public class PPTest extends JavaPlugin {
|
||||
scenes.put("geo_water", new Scene("geo_water", this::geoWater, 6, 3.5, -5, 6, -3, 2.5));
|
||||
scenes.put("geo_water_top", new Scene("geo_water_top", this::geoWater, 6, 9, 2.4, 6, -4, 2.5));
|
||||
scenes.put("geo_water_under", new Scene("geo_water_under", this::geoWater, 11.5, -3.3, 0.3, 4, -5, 3));
|
||||
// Block-entity contents: enchanting-table book (closed; open when the camera is within 3 blocks), campfire
|
||||
// and shelf items, copper golem statues (4 poses x stages), spawner/trial-spawner mobs, vault, beacon beam
|
||||
// through red+blue glass, 3x3 end portal and an end gateway. Rendered 100 ticks after building (beacon
|
||||
// levels update every 80 ticks).
|
||||
scenes.put("be_contents", new Scene("be_contents", this::beContents, 7, 4.5, 13, 7, 0.8, 3));
|
||||
scenes.put("be_contents_n", new Scene("be_contents_n", this::beContents, 7, 4.5, -7, 7, 0.8, 3));
|
||||
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_portal", new Scene("be_contents_portal", this::beContents, 11.5, 3.6, -1.8, 11.5, 0, 1.2));
|
||||
}
|
||||
|
||||
private void beContents(Location o) {
|
||||
put(o, 0, 0, 0, "enchanting_table");
|
||||
put(o, 2, 0, 0, "campfire[facing=south,lit=true]");
|
||||
put(o, 4, 0, 0, "soul_campfire[facing=east,lit=false]");
|
||||
put(o, 6, 0, 0, "oak_shelf[facing=south]");
|
||||
put(o, 8, 0, 0, "cherry_shelf[facing=south,powered=false]");
|
||||
if (o.clone().add(2, 0, 0).getBlock().getState() instanceof org.bukkit.block.Campfire c) {
|
||||
Material[] food = {Material.BEEF, Material.COD, Material.PORKCHOP, Material.POTATO};
|
||||
for (int i = 0; i < 4; 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.update(true, false);
|
||||
}
|
||||
if (o.clone().add(4, 0, 0).getBlock().getState() instanceof org.bukkit.block.Campfire c) {
|
||||
c.setItem(0, it(Material.SALMON));
|
||||
c.setItem(2, it(Material.CHICKEN));
|
||||
c.update(true, false);
|
||||
}
|
||||
Material[][] shelves = {{Material.DIAMOND, Material.BOOK, Material.OAK_LOG}, {Material.GOLDEN_APPLE, null, Material.LANTERN}};
|
||||
for (int k = 0; k < 2; k++) {
|
||||
if (o.clone().add(6 + 2 * k, 0, 0).getBlock().getState() instanceof org.bukkit.block.Shelf sh) {
|
||||
for (int i = 0; i < 3; i++) if (shelves[k][i] != null) sh.getSnapshotInventory().setItem(i, it(shelves[k][i]));
|
||||
sh.update(true, false);
|
||||
}
|
||||
}
|
||||
String[] poses = {"standing", "running", "sitting", "star"};
|
||||
String[] stages = {"", "exposed_", "weathered_", "waxed_oxidized_"};
|
||||
String[] facing = {"south", "south", "east", "west"};
|
||||
for (int i = 0; i < 4; i++)
|
||||
put(o, i * 2, 0, 3, stages[i] + "copper_golem_statue[copper_golem_pose=" + poses[i] + ",facing=" + facing[i] + "]");
|
||||
put(o, 0, 0, 6, "spawner");
|
||||
put(o, 2, 0, 6, "spawner");
|
||||
put(o, 4, 0, 6, "trial_spawner[trial_spawner_state=waiting_for_players]");
|
||||
put(o, 6, 0, 6, "vault[facing=south]");
|
||||
EntityType[] mobs = {EntityType.ZOMBIE, EntityType.PIG};
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if (o.clone().add(i * 2, 0, 6).getBlock().getState() instanceof org.bukkit.block.CreatureSpawner cs) {
|
||||
cs.setSpawnedType(mobs[i]);
|
||||
cs.update(true, false);
|
||||
}
|
||||
}
|
||||
if (o.clone().add(4, 0, 6).getBlock().getState() instanceof org.bukkit.block.TrialSpawner ts) {
|
||||
ts.getNormalConfiguration().setSpawnedType(EntityType.SKELETON);
|
||||
ts.update(true, false);
|
||||
put(o, 4, 0, 6, "trial_spawner[trial_spawner_state=waiting_for_players]");
|
||||
}
|
||||
// Beacon on a 3x3 iron pyramid, red glass 2 above, blue glass 4 above (red+blue = averaged section).
|
||||
for (int x = -1; x <= 1; x++) for (int z = -1; z <= 1; z++) put(o, 10 + x, -1, 6 + z, "iron_block");
|
||||
put(o, 10, 0, 6, "beacon");
|
||||
put(o, 10, 2, 6, "red_stained_glass");
|
||||
put(o, 10, 4, 6, "blue_stained_glass_pane");
|
||||
for (int x = 0; x < 3; x++) for (int z = 0; z < 3; z++) put(o, 10 + x, 0, z, "end_portal");
|
||||
put(o, 14, 1, 1, "end_gateway");
|
||||
put(o, 14, 0, 1, "obsidian");
|
||||
}
|
||||
|
||||
private void rfVisualLayers(Location o) {
|
||||
@@ -592,7 +658,7 @@ public class PPTest extends JavaPlugin {
|
||||
t.printStackTrace();
|
||||
}
|
||||
// Let one tick pass (passengers/leashes settle), then render.
|
||||
Bukkit.getScheduler().runTaskLater(this, () -> render(sc, o), sc.name.equals("proj_shake") ? 4 : sc.name.startsWith("proj_") ? 30 : 10);
|
||||
Bukkit.getScheduler().runTaskLater(this, () -> render(sc, o), sc.name.equals("proj_shake") ? 4 : sc.name.startsWith("proj_") ? 30 : sc.name.startsWith("be_contents") ? 100 : 10);
|
||||
}
|
||||
|
||||
private void render(Scene sc, Location o) {
|
||||
|
||||
Reference in new issue
Block a user