render item frame contents with the item geometry like vanilla ItemFrameRenderer (fixed context, 8 rotations, all facings), frame block models, map contents incl. pixelpics photos, invisible frames, head skins/banner patterns from item meta

This commit is contained in:
MineTec committed 2026-11-04 16:03:34 +01:00
1 parent a743dc32c7
commit 2d56a1910c
13 files changed
+408 -96

No files matched your search

@@ -26,6 +26,7 @@ public final class BlockModelRegistry {
private final ModelBaker baker;
private final Map<BlockData, ResolvedModel> cache = new ConcurrentHashMap<>();
private final Map<ResourceLocation, ResolvedModel> modelCache = new ConcurrentHashMap<>();
private volatile ResolvedModel waterModel;
private volatile ResolvedModel lavaModel;
@@ -48,6 +49,17 @@ public final class BlockModelRegistry {
return cache.computeIfAbsent(data, this::resolve);
}
/**
* A block model by id (e.g. {@code block/item_frame_map}) without a blockstate, unrotated: for models
* vanilla draws from a fake block state (item frames). Empty geometry when the model is missing.
*/
public ResolvedModel model(ResourceLocation modelId) {
return modelCache.computeIfAbsent(modelId, id -> {
ModelBaker.BakedGeometry baked = baker.bake(modelResolver.resolve(id), new Variant(id, 0, 0, false));
return new ResolvedModel(baked.elements(), baked.averageColor(), 0, 0, false, baked.hasGeometry());
});
}
private ResolvedModel resolve(BlockData data) {
Material material = data.getMaterial();
if (material == Material.WATER) return water();
@@ -1,25 +1,31 @@
package eu.mhsl.minecraft.pixelpics.render.entity;
import eu.mhsl.minecraft.pixelpics.assets.BlockModelRegistry;
import eu.mhsl.minecraft.pixelpics.assets.ResourceLocation;
import eu.mhsl.minecraft.pixelpics.assets.TextureCache;
import eu.mhsl.minecraft.pixelpics.assets.model.Direction;
import eu.mhsl.minecraft.pixelpics.assets.model.Face;
import eu.mhsl.minecraft.pixelpics.render.entity.cem.ItemGeometry;
import java.util.ArrayList;
import java.util.List;
/**
* Bakes flat wall decorations (paintings, item frames) into world-space {@link EntityCube}s. These are
* not CEM models but simple textured quads, so the geometry is built directly from the captured world
* bounding box and the front-facing direction. Added to the {@link EntityScene} like any other entity.
* Bakes wall decorations into world-space {@link EntityCube}s: paintings as flat textured quads built from the
* captured world bounding box, item frames as their vanilla frame block model plus the framed item (item
* model geometry) or map. Added to the {@link EntityScene} like any other entity.
*/
public final class DecorationBaker implements EntityBaker<DecorationState> {
private static final double MIN_THICKNESS = 0.0625; // 1px, so the slab test never degenerates
private final TextureCache textures;
private final BlockModelRegistry registry;
private final ItemGeometry items;
public DecorationBaker(TextureCache textures) {
public DecorationBaker(TextureCache textures, BlockModelRegistry registry, ItemGeometry items) {
this.textures = textures;
this.registry = registry;
this.items = items;
}
@Override
@@ -46,77 +52,69 @@ public final class DecorationBaker implements EntityBaker<DecorationState> {
}
/**
* Item frames render as the vanilla geometry: a 12x12 birch border, a recessed 10x10 leather back
* panel ({@code block/item_frame}), and the held item as a centred 8x8 sprite. Built in a canonical
* local frame (front toward +Z, centred on the block face) and oriented to the wall by an affine.
* Item frames as vanilla's ItemFrameRenderer (decompiled 26.3) draws them. Pose from the entity position
* (the bbox centre; the renderer's own offset cancels the dispatcher's): {@code T(dir·0.46875)} to the
* block centre, then {@code rotX(xRot)·rotY(yRot)} (horizontal: 0 / 180−yRot(dir); up/down: ∓90 / 180),
* which turns local +Z toward the wall. Frame = the fake-blockstate block model
* ({@code block/[glow_]item_frame[_map]}) at {@code T(-0.5)}, skipped when invisible. Content at
* {@code T(0,0,0.4375)} ({@code 0.5} when invisible): a map as a 128x128 quad
* ({@code rotZ(rot%4·90)·rotZ(180)·S(1/128)·T(-64,-64,-1)}, z −0.01 map px, MapRenderer), any other item
* through the item model in the {@code fixed} display context at {@code rotZ(rot·45)·S(0.5)}.
*/
private RenderedEntity bakeItemFrame(DecorationState s) {
String frameTex = s.glow() ? "block/glow_item_frame" : "block/item_frame";
int[][] leather = textures.get(ResourceLocation.parse(frameTex)).orElse(null);
int[][] wood =
textures.get(ResourceLocation.parse("block/birch_planks")).orElse(leather);
if (leather == null) return null;
Affine toWorld = Affine.translation(faceCenterX(s), faceCenterY(s), faceCenterZ(s))
.mul(facingRotation(s.facing()));
int si = Direction.SOUTH.ordinal();
List<EntityCube> cubes = new ArrayList<>(3);
// Wood border 12x12 (behind), leather back 10x10 (1px proud), item 8x8 (in front). Front = local +Z.
// `wood` is never null here (orElse(leather) and leather is non-null past the guard above).
Face[] woodFace = new Face[6];
woodFace[si] = new Face(wood, 1, 0, 0, 1, 0, -1); // tileable; flip matches the others
cubes.add(new EntityCube(px(-6, -6, 0), px(6, 6, 1), woodFace, toWorld));
Face[] back = new Face[6];
back[si] = new Face(leather, 13.0 / 16, 3.0 / 16, 3.0 / 16, 13.0 / 16, 0, -1); // centre 10x10, flipped
cubes.add(new EntityCube(px(-5, -5, 1), px(5, 5, 2), back, toWorld));
if (s.itemId() != null) {
int[][] item = resolveItem(s.itemId());
if (item != null) {
int rot = ((Math.round(s.itemRotationDeg() / 90f) * 90) % 360 + 360) % 360;
Face[] f = new Face[6];
f[si] = new Face(item, 1, 0, 0, 1, rot, -1); // full sprite, flipped to read upright
cubes.add(new EntityCube(px(-4, -4, 2), px(4, 4, 3), f, toWorld));
}
DecorationState.Facing f = s.facing();
double off = 0.46875;
double cx = (s.minX() + s.maxX()) / 2 + (f.axis() == 0 ? f.sign() * off : 0);
double cy = (s.minY() + s.maxY()) / 2 + (f.axis() == 1 ? f.sign() * off : 0);
double cz = (s.minZ() + s.maxZ()) / 2 + (f.axis() == 2 ? f.sign() * off : 0);
double xRot, yRot;
if (f.axis() != 1) {
xRot = 0;
yRot = 180 - yRot(f);
} else {
xRot = -90 * f.sign();
yRot = 180;
}
return RenderedEntity.of(cubes);
Affine pose = Affine.translation(cx, cy, cz)
.mul(Affine.rotX(Math.toRadians(xRot)))
.mul(Affine.rotY(Math.toRadians(yRot)));
List<EntityCube> cubes = new ArrayList<>();
boolean map = s.mapPixels() != null;
if (!s.invisible() && registry != null && items != null) {
String model = "block/" + (s.glow() ? "glow_item_frame" : "item_frame") + (map ? "_map" : "");
items.bakeModel(
registry.model(ResourceLocation.parse(model)),
null,
pose.mul(Affine.translation(-0.5, -0.5, -0.5)),
cubes);
}
Affine content = pose.mul(Affine.translation(0, 0, s.invisible() ? 0.5 : 0.4375));
if (map) {
Affine m = content.mul(Affine.rotZ(Math.toRadians(s.rotation() % 4 * 90)))
.mul(Affine.rotZ(Math.PI))
.mul(Affine.scale(1 / 128.0))
.mul(Affine.translation(-64, -64, -1));
// Quad (0,0)..(128,128) at z −0.01 facing local −Z (away from the wall); u along +x, v along +y.
Face[] faces = new Face[6];
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);
cubes.add(new EntityCube(new double[] {0, 0, -0.01}, new double[] {128, 128, -0.01}, faces, m));
} else if (s.item() != null && items != null) {
Affine m =
content.mul(Affine.rotZ(Math.toRadians(s.rotation() * 45))).mul(Affine.scale(0.5));
items.bake(s.item(), "fixed", false, m, cubes);
}
return cubes.isEmpty() ? null : RenderedEntity.of(cubes);
}
/** A local-space corner in model pixels (1/16 block); z is the outward (front) offset from the wall. */
private static double[] px(double x, double y, double z) {
return new double[] {x / 16.0, y / 16.0, z / 16.0};
}
/** Item sprite: generated items live under item/, block items fall back to block/. */
private int[][] resolveItem(String id) {
int[][] t = textures.get(ResourceLocation.parse("item/" + id)).orElse(null);
if (t != null) return t;
return textures.get(ResourceLocation.parse("block/" + id)).orElse(null);
}
// Frame placement: in-plane centre from the bbox, depth at the outward wall surface.
private static double faceCenterX(DecorationState s) {
return s.facing().axis() == 0 ? (s.facing().sign() > 0 ? s.maxX() : s.minX()) : (s.minX() + s.maxX()) / 2;
}
private static double faceCenterY(DecorationState s) {
return s.facing().axis() == 1 ? (s.facing().sign() > 0 ? s.maxY() : s.minY()) : (s.minY() + s.maxY()) / 2;
}
private static double faceCenterZ(DecorationState s) {
return s.facing().axis() == 2 ? (s.facing().sign() > 0 ? s.maxZ() : s.minZ()) : (s.minZ() + s.maxZ()) / 2;
}
/** Maps the canonical local frame (front +Z, up +Y) onto the wall facing the given direction. */
private static Affine facingRotation(DecorationState.Facing f) {
/** Vanilla Direction#toYRot for horizontal facings (south 0, west 90, north 180, east 270). */
private static double yRot(DecorationState.Facing f) {
return switch (f) {
case SOUTH -> Affine.identity();
case NORTH -> Affine.rotY(Math.PI);
case EAST -> Affine.rotY(Math.PI / 2);
case WEST -> Affine.rotY(-Math.PI / 2);
case UP -> Affine.rotX(-Math.PI / 2);
case DOWN -> Affine.rotX(Math.PI / 2);
case WEST -> 90;
case NORTH -> 180;
case EAST -> 270;
default -> 0;
};
}
@@ -4,7 +4,8 @@ package eu.mhsl.minecraft.pixelpics.render.entity;
* 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
* models, so they have their own state/baker. The world-space bounding box is captured directly (it
* already encodes vanilla's awkward multi-block painting offset), avoiding placement math.
* already encodes vanilla's awkward multi-block painting offset), avoiding placement math. Item frames use
* its centre (the vanilla entity position).
*/
public record DecorationState(
Kind kind,
@@ -16,9 +17,11 @@ public record DecorationState(
double maxZ,
Facing facing, // direction the front faces (away from the wall)
String paintingArt, // painting art asset key (texture painting/<art>); null for frames
String itemId, // item-frame contents material key (e.g. "diamond"); null if empty
int itemRotationDeg, // item-frame content rotation (0/45/…/315)
boolean glow // glow item frame
ItemSpec item, // item-frame contents; null if empty
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)
boolean glow, // glow item frame
boolean invisible // invisible frame (setVisible(false)): no frame model, item sits on the wall
) {
public enum Kind {
PAINTING,
@@ -107,7 +107,11 @@ public final class ItemGeometry {
/** 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) {
if (data == null) return;
ResolvedModel model = registry.get(data);
bakeModel(registry.get(data), data, toWorld, out);
}
/** A resolved block model's elements in the block cube [0,1]³; {@code data} = tint context or null. */
public void bakeModel(ResolvedModel model, BlockData data, Affine toWorld, List<EntityCube> out) {
if (model == null || !model.hasGeometry) return;
for (Element world : model.elements) {
// Rotated: bake the model-space element turned by the blockstate rotation, so textures stay bound
@@ -467,7 +471,7 @@ public final class ItemGeometry {
/** Bakes a biome tint into a copy of the face texture (the entity path has no per-hit tinting). */
private Face tint(Face f, BlockData data) {
if (f == null || f.tintIndex < 0) return f;
if (f == null || f.tintIndex < 0 || data == null) return f;
int color = TintResolver.resolve(data, f.tintIndex, DEFAULT_TINT);
if (color == -1) return f;
int[][] t;
@@ -88,7 +88,6 @@ public class DefaultScreenRenderer implements Renderer {
this.raytracer = new SnapshotRaytracer(registry, tintProvider, skyRenderer, MAX_DISTANCE, REFLECTION_DEPTH);
this.entityBaker = entityBaker;
this.blockEntityBaker = blockEntityBaker;
this.decorationBaker = new DecorationBaker(textures);
ItemGeometry items = new ItemGeometry(
registry.reader(),
registry,
@@ -96,6 +95,7 @@ public class DefaultScreenRenderer implements Renderer {
entityBaker == null ? null : entityBaker.models(),
blockEntityBaker);
if (entityBaker != null) entityBaker.setItemGeometry(items);
this.decorationBaker = new DecorationBaker(textures, registry, items);
this.objectBaker = new ObjectBaker(items, blockEntityBaker == null ? null : blockEntityBaker.font());
this.logger = logger;
this.tracePool = tracePool;
@@ -29,6 +29,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.inventory.meta.BannerMeta;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.SkullMeta;
import org.bukkit.profile.PlayerProfile;
import org.bukkit.util.Vector;
@@ -89,6 +92,52 @@ public final class BlockEntitySnapshotBuilder {
}
}
/**
* Adds the item-stack details a placed tile would carry to a {@link #fromBlockData} state: the owner skin
* of a player head ({@code SkullMeta} profile) and banner patterns ({@code BannerMeta}). Other kinds are
* returned unchanged. MUST run on the main thread.
*/
public static BlockEntityState withItemMeta(BlockEntityState s, ItemMeta meta) {
if (s == null || meta == null) return s;
try {
if (meta instanceof SkullMeta skull && "player".equals(s.headType())) {
PlayerProfile profile = skull.getOwnerProfile();
if (profile == null || profile.getTextures().getSkin() == null) return s;
return copy(s, profile.getTextures().getSkin().toString(), s.patterns());
}
if (meta instanceof BannerMeta banner && !banner.getPatterns().isEmpty()) {
List<BannerPattern> pats = new ArrayList<>();
for (Pattern p : banner.getPatterns()) {
pats.add(new BannerPattern(patternKey(p), ColorUtil.dyeArgb(p.getColor(), 0xFFFFFFFF)));
}
return copy(s, s.skinUrl(), pats);
}
} catch (Throwable ignored) {
// API drift: keep the plain block state
}
return s;
}
private static BlockEntityState copy(BlockEntityState s, String skinUrl, List<BannerPattern> patterns) {
return new BlockEntityState(
s.kind(),
s.bx(),
s.by(),
s.bz(),
s.facingDeg(),
s.direction(),
s.active(),
s.chestKind(),
s.baseColorArgb(),
s.colorName(),
s.headType(),
skinUrl,
patterns,
s.sherds(),
s.frontText(),
s.backText());
}
private static BlockEntityState classify(BlockData data, int bx, int by, int bz, BlockState ts) {
Material mat = data.getMaterial();
String n = mat.name();
@@ -1,9 +1,11 @@
package eu.mhsl.minecraft.pixelpics.render.snapshot;
import eu.mhsl.minecraft.pixelpics.render.entity.DecorationState;
import eu.mhsl.minecraft.pixelpics.render.entity.ItemSpec;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.bukkit.Material;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.Entity;
import org.bukkit.entity.GlowItemFrame;
@@ -49,15 +51,18 @@ public final class DecorationSnapshotBuilder {
facing(e.getFacing()),
art,
null,
null,
0,
false,
false);
}
if (e instanceof ItemFrame frame) {
BoundingBox bb = e.getBoundingBox();
boolean glow =
e instanceof GlowItemFrame || e.getType().getKey().getKey().equals("glow_item_frame");
String itemId = itemId(frame.getItem());
int rot = frame.getRotation().ordinal() * 45;
ItemStack stack = frame.getItem();
ItemSpec item = ItemSpecs.of(stack);
int[][] map = item != null && stack.getType() == Material.FILLED_MAP ? MapPixels.of(stack) : null;
return new DecorationState(
DecorationState.Kind.ITEM_FRAME,
bb.getMinX(),
@@ -68,19 +73,15 @@ public final class DecorationSnapshotBuilder {
bb.getMaxZ(),
facing(e.getFacing()),
null,
itemId,
rot,
glow);
item,
map,
frame.getRotation().ordinal(),
glow,
!frame.isVisible());
}
return null;
}
/** The framed item's material key (e.g. "diamond"); the baker resolves item/ or block/ textures from it. */
private static String itemId(ItemStack item) {
if (item == null || item.getType().isAir()) return null;
return item.getType().getKey().getKey();
}
private static DecorationState.Facing facing(BlockFace face) {
return switch (face) {
case NORTH -> DecorationState.Facing.NORTH;
@@ -29,7 +29,12 @@ public final class ItemSpecs {
} catch (Throwable t) {
glint = false;
}
return new ItemSpec(m.getKey().getKey(), b.data, b.blockEntity, glint);
BlockEntityState be = b.blockEntity;
// 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)) {
be = BlockEntitySnapshotBuilder.withItemMeta(be, stack.getItemMeta());
}
return new ItemSpec(m.getKey().getKey(), b.data, be, glint);
}
private static BlockInfo blockInfo(Material m) {
@@ -0,0 +1,81 @@
package eu.mhsl.minecraft.pixelpics.render.snapshot;
import eu.mhsl.minecraft.pixelpics.utils.ImageMapRenderer;
import eu.mhsl.minecraft.pixelpics.utils.MapColorPalette;
import eu.mhsl.minecraft.pixelpics.utils.MapManager;
import java.lang.reflect.Field;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.MapMeta;
import org.bukkit.map.MapRenderer;
import org.bukkit.map.MapView;
/**
* Captures a filled map's 128x128 colours for rendering it in an item frame. PixelPics photo maps come from
* their {@link ImageMapRenderer} (or the on-disk index cache), other maps from the server's map data
* ({@code MapItemSavedData.colors}, reflectively — Bukkit has no API to read it). MUST run on the main thread.
*/
public final class MapPixels {
private static final int SIZE = ImageMapRenderer.MAP_SIZE;
private MapPixels() {}
/** ARGB {@code [y][x]}, or null when the stack is no map or has no map data (vanilla then draws the item). */
public static int[][] of(ItemStack stack) {
try {
if (!(stack.getItemMeta() instanceof MapMeta meta) || !meta.hasMapView()) return null;
MapView view = meta.getMapView();
if (view == null) return null;
byte[] colors = colors(view);
if (colors == null || colors.length != SIZE * SIZE) return null;
int[][] out = new int[SIZE][SIZE];
for (int y = 0; y < SIZE; y++) {
for (int x = 0; x < SIZE; x++) out[y][x] = MapColorPalette.argb(colors[y * SIZE + x]);
}
return out;
} catch (Throwable t) {
return null;
}
}
private static byte[] colors(MapView view) throws ReflectiveOperationException {
for (MapRenderer r : view.getRenderers()) {
if (r instanceof ImageMapRenderer image) {
byte[] indices = image.getIndices();
return indices != null ? indices : film();
}
}
if (MapManager.isManaged(view.getId())) {
byte[] cached = MapManager.readIndices(view.getId());
if (cached != null) return cached;
}
return worldMapColors(view);
}
/** A photo still being rendered shows blank film, as its map does. */
private static byte[] film() {
byte[] b = new byte[SIZE * SIZE];
java.util.Arrays.fill(b, ImageMapRenderer.filmColor());
return b;
}
private static volatile Field worldMapField, colorsField;
/** CraftMapView#worldMap → MapItemSavedData#colors (what the vanilla CraftMapRenderer sends). */
private static byte[] worldMapColors(MapView view) throws ReflectiveOperationException {
Field wm = worldMapField;
if (wm == null || wm.getDeclaringClass() != view.getClass()) {
wm = view.getClass().getDeclaredField("worldMap");
wm.setAccessible(true);
worldMapField = wm;
}
Object data = wm.get(view);
if (data == null) return null;
Field c = colorsField;
if (c == null || c.getDeclaringClass() != data.getClass()) {
c = data.getClass().getField("colors");
colorsField = c;
}
return (byte[]) c.get(data);
}
}
@@ -109,6 +109,11 @@ public class ImageMapRenderer extends MapRenderer {
return Math.min(0.999, noise * 0.7 + sweep * 0.3);
}
/** The blank-film map colour a photo shows while it is still being rendered. */
public static byte filmColor() {
return film();
}
private static byte film() {
if (!filmResolved) {
filmIndex = MapColorPalette.mapIndex(MapColorPalette.nearestPos(206, 202, 190));
@@ -17,6 +17,7 @@ public final class MapColorPalette {
private static byte[] indices;
private static int[] rgb;
private static double[][] lab;
private static int[] argbByIndex; // all 256 map colour bytes → ARGB (transparent slots alpha 0)
private MapColorPalette() {}
@@ -48,9 +49,24 @@ public final class MapColorPalette {
rgb[i] = rgbs.get(i);
lab[i] = labs.get(i);
}
argbByIndex = new int[256];
for (int i = 0; i < 256; i++) {
try {
Color c = MapPalette.getColor((byte) i);
argbByIndex[i] = c.getAlpha() < 255 ? 0 : (0xFF << 24) | (c.getRGB() & 0xFFFFFF);
} catch (Throwable t) {
argbByIndex[i] = 0;
}
}
initialized = true;
}
/** ARGB of a raw map colour byte as the client draws it; transparent slots are 0. */
public static int argb(byte mapColor) {
ensure();
return argbByIndex[mapColor & 0xFF];
}
public static int size() {
ensure();
return indices.length;
+8 -7
View File
@@ -71,7 +71,8 @@ public class BlockEntityTestRender {
log.info("font glyphs loaded: " + (font.isEmpty() ? "NONE" : "ok"));
CemBaker baker = new CemBaker(geo, textures, skins);
beBaker = new BlockEntityBaker(geo, textures, skins, font);
decoBaker = new DecorationBaker(textures);
decoBaker = new DecorationBaker(textures, registry,
new eu.mhsl.minecraft.pixelpics.render.entity.cem.ItemGeometry(reader, registry, textures, geo, beBaker));
DefaultScreenRenderer renderer = new DefaultScreenRenderer(registry, tint, textures, baker, beBaker, log);
BlockData air = (BlockData) Proxy.newProxyInstance(BlockEntityTestRender.class.getClassLoader(),
@@ -204,17 +205,17 @@ public class BlockEntityTestRender {
LinkedHashMap<String, List<DecorationState>> decoScenes = new LinkedHashMap<>();
// kebab is 1x1; place a thin quad facing south spanning the block at z=0..0.0625.
decoScenes.put("painting_kebab_S", List.of(new DecorationState(DecorationState.Kind.PAINTING,
0, 0, 0, 1, 1, 0.0625, DecorationState.Facing.SOUTH, "kebab", null, 0, false)));
0, 0, 0, 1, 1, 0.0625, DecorationState.Facing.SOUTH, "kebab", null, null, 0, false, false)));
decoScenes.put("painting_kebab_N", List.of(new DecorationState(DecorationState.Kind.PAINTING,
0, 0, 0.9375, 1, 1, 1, DecorationState.Facing.NORTH, "kebab", null, 0, false)));
0, 0, 0.9375, 1, 1, 1, DecorationState.Facing.NORTH, "kebab", null, null, 0, false, false)));
decoScenes.put("painting_pool_S", List.of(new DecorationState(DecorationState.Kind.PAINTING,
0, 0, 0, 2, 1, 0.0625, DecorationState.Facing.SOUTH, "pool", null, 0, false)));
0, 0, 0, 2, 1, 0.0625, DecorationState.Facing.SOUTH, "pool", null, null, 0, false, false)));
decoScenes.put("item_frame", List.of(new DecorationState(DecorationState.Kind.ITEM_FRAME,
0.0625, 0.0625, 0, 0.9375, 0.9375, 0.0625, DecorationState.Facing.SOUTH, null, "diamond", 0, false)));
0.0625, 0.0625, 0, 0.9375, 0.9375, 0.0625, DecorationState.Facing.SOUTH, null, new eu.mhsl.minecraft.pixelpics.render.entity.ItemSpec("diamond", null, null, false), null, 0, false, false)));
decoScenes.put("item_frame_empty", List.of(new DecorationState(DecorationState.Kind.ITEM_FRAME,
0.0625, 0.0625, 0, 0.9375, 0.9375, 0.0625, DecorationState.Facing.SOUTH, null, null, 0, false)));
0.0625, 0.0625, 0, 0.9375, 0.9375, 0.0625, DecorationState.Facing.SOUTH, null, null, null, 0, false, false)));
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, "diamond", 0, 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)));
List<BufferedImage> cells = new ArrayList<>();
List<String> names = new ArrayList<>(scenes.keySet());
+138 -1
View File
@@ -13,6 +13,7 @@ import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.*;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.type.Bed;
import org.bukkit.command.Command;
@@ -128,6 +129,13 @@ public class PPTest extends JavaPlugin {
// Needs a datapack cat variant "pptest:ocelotcat" (asset_id entity/cat/ocelot) in the world's datapacks.
scenes.put("rf_dpcat", new Scene("rf_dpcat", this::rfDatapackCat, 1.2, 1.4, -2.2, 1.2, 0.2, 0.5));
scenes.put("rf_armor", new Scene("rf_armor", this::rfArmor, 7, 3.2, -6.5, 7, 1.0, 1.5));
// Item frames (vanilla ItemFrameRenderer): north wall with rotations 0-7, block/flat/tool/block-entity
// items, photo + synthetic + vanilla-data maps, invisible and glow frames; frames on all 6 block sides.
scenes.put("frames", new Scene("frames", this::frames, 4.5, 2.8, -3.5, 4.5, 2.6, 4));
scenes.put("frames_close", new Scene("frames_close", this::frames, 2.6, 2.4, 0.8, 2.6, 2.3, 4));
scenes.put("frames_side", new Scene("frames_side", this::frames, -1.5, 3.2, -1.0, 4.5, 2.6, 3.6));
scenes.put("frames_dirs_ne", new Scene("frames_dirs_ne", this::framesDirs, 16, 7.5, -4, 8, 4, 3.5));
scenes.put("frames_dirs_sw", new Scene("frames_dirs_sw", this::framesDirs, 0, 1.2, 10, 8, 4, 3.5));
}
@Override
@@ -165,7 +173,7 @@ public class PPTest extends JavaPlugin {
Location eye = o.clone().add(sc.camX, sc.camY, sc.camZ);
Vector look = o.clone().add(sc.lookX, sc.lookY, sc.lookZ).toVector().subtract(eye.toVector());
eye.setDirection(look);
RenderJob job = r.prepare(eye, sc.name.startsWith("proj_") || sc.name.startsWith("rf_") ? new Resolution(960, 640) : new Resolution(480, 320), null);
RenderJob job = r.prepare(eye, sc.name.startsWith("proj_") || sc.name.startsWith("rf_") || sc.name.startsWith("frames") ? new Resolution(960, 640) : new Resolution(480, 320), null);
if (sc.name.equals("dbg")) {
try {
java.lang.reflect.Field f = DefaultScreenRenderer.class.getDeclaredField("objectBaker");
@@ -1098,4 +1106,133 @@ public class PPTest extends JavaPlugin {
b.setBlock(Bukkit.createBlockData("minecraft:" + blocks[i]));
}
}
// --- item frames -------------------------------------------------------------------------------
private void frames(Location o) {
for (int x = -2; x <= 11; x++)
for (int y = 0; y <= 5; y++) w.getBlockAt(o.getBlockX() + x, o.getBlockY() + y, o.getBlockZ() + 4).setType(Material.STONE, false);
BlockFace f = BlockFace.NORTH;
for (int r = 0; r < 8; r++) frame(o, r, 4, 3, f, false, it(Material.DIAMOND_SWORD), r, true);
ItemStack banner = it(Material.RED_BANNER);
org.bukkit.inventory.meta.BannerMeta bm = (org.bukkit.inventory.meta.BannerMeta) banner.getItemMeta();
bm.addPattern(new org.bukkit.block.banner.Pattern(DyeColor.YELLOW, org.bukkit.block.banner.PatternType.STRIPE_TOP));
banner.setItemMeta(bm);
ItemStack[] row3 = {it(Material.STONE), it(Material.OAK_STAIRS), it(Material.GRASS_BLOCK), it(Material.APPLE),
it(Material.TORCH), it(Material.CHEST), banner, eu.mhsl.minecraft.pixelpics.survival.CameraItems.createCamera(3),
it(Material.CREEPER_HEAD), it(Material.SHIELD)};
for (int i = 0; i < row3.length; i++) frame(o, i - 1, 3, 3, f, false, row3[i], 0, true);
ItemStack photo = photoMap(), synth = synthMap(), vmap = vanillaMap();
int[] photoRot = {0, 1, 2, 3, 5};
for (int i = 0; i < photoRot.length; i++) frame(o, i - 1, 2, 3, f, false, photo, photoRot[i], true);
frame(o, 4, 2, 3, f, false, synth, 0, true);
frame(o, 5, 2, 3, f, false, synth, 1, true);
frame(o, 6, 2, 3, f, false, vmap, 0, true);
frame(o, 7, 2, 3, f, false, it(Material.MAP), 0, true);
frame(o, 8, 2, 3, f, false, it(Material.FILLED_MAP), 0, true);
ItemStack[] inv = {it(Material.DIAMOND_SWORD), it(Material.STONE), synth, it(Material.CHEST)};
for (int i = 0; i < inv.length; i++) frame(o, i - 1, 1, 3, f, false, inv[i], i == 0 ? 1 : 0, false);
ItemStack[] glow = {it(Material.APPLE), it(Material.STONE), synth, null};
for (int i = 0; i < glow.length; i++) frame(o, i + 3, 1, 3, f, true, glow[i], 0, true);
frame(o, 7, 1, 3, f, false, null, 0, true);
frame(o, 8, 1, 3, f, true, null, 0, true);
}
/** Three stone blocks with a frame on every side: block item, tool, synthetic map (all at rotation 1). */
private void framesDirs(Location o) {
ItemStack[] items = {it(Material.OAK_STAIRS), it(Material.DIAMOND_SWORD), synthMap()};
BlockFace[] faces = {BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST, BlockFace.UP, BlockFace.DOWN};
for (int i = 0; i < items.length; i++) {
int bx = 3 + i * 5, by = 4, bz = 3;
w.getBlockAt(o.getBlockX() + bx, o.getBlockY() + by, o.getBlockZ() + bz).setType(Material.STONE, false);
for (BlockFace face : faces)
frame(o, bx + face.getModX(), by + face.getModY(), bz + face.getModZ(), face, i == 2, items[i], 1, true);
}
}
private ItemFrame frame(Location o, int x, int y, int z, BlockFace face, boolean glow, ItemStack item, int rot, boolean visible) {
Location l = o.clone().add(x + 0.5, y + 0.5, z + 0.5);
Class<? extends ItemFrame> c = glow ? GlowItemFrame.class : ItemFrame.class;
ItemFrame fr = w.spawn(l, c, e -> {
e.setFacingDirection(face, true);
e.setPersistent(false);
});
if (item != null) fr.setItem(item, false);
fr.setRotation(Rotation.values()[rot]);
fr.setVisible(visible);
fr.setFixed(true);
return fr;
}
private static ItemStack mapItem(org.bukkit.map.MapView view) {
ItemStack s = it(Material.FILLED_MAP);
org.bukkit.inventory.meta.MapMeta mm = (org.bukkit.inventory.meta.MapMeta) s.getItemMeta();
mm.setMapView(view);
s.setItemMeta(mm);
return s;
}
/** A real stored PixelPics photo (lowest managed map id on disk), or the synthetic map if none exists. */
private ItemStack photoMap() {
File dir = new File(Main.getInstance().getDataFolder(), "images");
int best = Integer.MAX_VALUE;
File[] files = dir.listFiles();
if (files != null)
for (File f : files) {
java.util.regex.Matcher m = java.util.regex.Pattern.compile("mapId_(\\d+)\\.mcmap").matcher(f.getName());
if (m.matches()) best = Math.min(best, Integer.parseInt(m.group(1)));
}
int id = best == Integer.MAX_VALUE ? -1 : best;
@SuppressWarnings("deprecation") org.bukkit.map.MapView v = id < 0 ? null : Bukkit.getMap(id);
if (v == null) return synthMap();
if (v.getRenderers().stream().noneMatch(r -> r instanceof eu.mhsl.minecraft.pixelpics.utils.ImageMapRenderer)) {
byte[] idx = eu.mhsl.minecraft.pixelpics.utils.MapManager.readIndices(id);
if (idx != null) eu.mhsl.minecraft.pixelpics.utils.MapManager.attachView(v, new eu.mhsl.minecraft.pixelpics.utils.ImageMapRenderer(idx));
}
getLogger().info("frames: photo map id " + id);
return mapItem(v);
}
private ItemStack synthMap;
/** Asymmetric test picture as a PixelPics map: red top-left, blue top-right, green bottom-left, black "F". */
private ItemStack synthMap() {
if (synthMap != null) return synthMap;
BufferedImage img = new BufferedImage(128, 128, BufferedImage.TYPE_INT_RGB);
java.awt.Graphics2D g = img.createGraphics();
g.setColor(java.awt.Color.WHITE);
g.fillRect(0, 0, 128, 128);
g.setColor(java.awt.Color.RED);
g.fillRect(0, 0, 32, 32);
g.setColor(java.awt.Color.BLUE);
g.fillRect(96, 0, 32, 32);
g.setColor(new java.awt.Color(0, 160, 0));
g.fillRect(0, 96, 32, 32);
g.setColor(java.awt.Color.BLACK);
g.fillRect(48, 24, 14, 80);
g.fillRect(48, 24, 44, 14);
g.fillRect(48, 56, 32, 12);
g.dispose();
org.bukkit.map.MapView v = Bukkit.createMap(w);
eu.mhsl.minecraft.pixelpics.utils.MapManager.attachView(v, new eu.mhsl.minecraft.pixelpics.utils.ImageMapRenderer(img));
return synthMap = mapItem(v);
}
/** A plain vanilla map whose server-side colours are written directly (diagonal colour bands + transparent corner). */
private ItemStack vanillaMap() {
org.bukkit.map.MapView v = Bukkit.createMap(w);
try {
java.lang.reflect.Field wm = v.getClass().getDeclaredField("worldMap");
wm.setAccessible(true);
Object data = wm.get(v);
byte[] colors = (byte[]) data.getClass().getField("colors").get(data);
byte[] palette = {(byte) (18 * 4 + 2), (byte) (7 * 4 + 2), (byte) (12 * 4 + 1), (byte) (29 * 4 + 2)};
for (int y = 0; y < 128; y++)
for (int x = 0; x < 128; x++)
colors[y * 128 + x] = x < 24 && y < 24 ? 0 : palette[((x + y) / 32) % palette.length];
} catch (ReflectiveOperationException e) {
getLogger().warning("vanillaMap: " + e);
}
return mapItem(v);
}
}