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:
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;
|
||||
|
||||
+49
@@ -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();
|
||||
|
||||
+12
-11
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user