refactor: replace fully qualified class names with imports and simplify code readability across rendering modules

This commit is contained in:
2026-06-21 17:56:32 +02:00
parent f83ccdc7ff
commit 1d3040610e
11 changed files with 189 additions and 112 deletions
@@ -4,17 +4,29 @@ import eu.mhsl.minecraft.pixelpics.assets.AssetReader;
import eu.mhsl.minecraft.pixelpics.assets.BlockModelRegistry;
import eu.mhsl.minecraft.pixelpics.assets.ResourcePack;
import eu.mhsl.minecraft.pixelpics.assets.ResourcePackLoader;
import eu.mhsl.minecraft.pixelpics.assets.SkinCache;
import eu.mhsl.minecraft.pixelpics.assets.TextureCache;
import eu.mhsl.minecraft.pixelpics.assets.font.BitmapFont;
import eu.mhsl.minecraft.pixelpics.assets.font.FontLoader;
import eu.mhsl.minecraft.pixelpics.commands.PixelPicsCommand;
import eu.mhsl.minecraft.pixelpics.listeners.OnMapInitialize;
import eu.mhsl.minecraft.pixelpics.render.RenderManager;
import eu.mhsl.minecraft.pixelpics.render.entity.cem.BlockEntityBaker;
import eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker;
import eu.mhsl.minecraft.pixelpics.render.entity.cem.CemModelLoader;
import eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer;
import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider;
import eu.mhsl.minecraft.pixelpics.survival.CameraListener;
import eu.mhsl.minecraft.pixelpics.survival.CraftingListener;
import eu.mhsl.minecraft.pixelpics.survival.JoinListener;
import eu.mhsl.minecraft.pixelpics.survival.SurvivalRecipes;
import eu.mhsl.minecraft.pixelpics.utils.MapColorPalette;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.io.InputStream;
import java.util.Objects;
import java.util.Optional;
@@ -45,10 +57,10 @@ public final class Main extends JavaPlugin {
Bukkit.getPluginManager().registerEvents(new OnMapInitialize(), this);
Objects.requireNonNull(Bukkit.getPluginCommand("pixelPic")).setExecutor(new PixelPicsCommand());
Bukkit.getPluginManager().registerEvents(new eu.mhsl.minecraft.pixelpics.survival.CameraListener(), this);
Bukkit.getPluginManager().registerEvents(new eu.mhsl.minecraft.pixelpics.survival.CraftingListener(), this);
Bukkit.getPluginManager().registerEvents(new eu.mhsl.minecraft.pixelpics.survival.JoinListener(), this);
eu.mhsl.minecraft.pixelpics.survival.SurvivalRecipes.register();
Bukkit.getPluginManager().registerEvents(new CameraListener(), this);
Bukkit.getPluginManager().registerEvents(new CraftingListener(), this);
Bukkit.getPluginManager().registerEvents(new JoinListener(), this);
SurvivalRecipes.register();
initRenderer();
}
@@ -87,33 +99,33 @@ public final class Main extends JavaPlugin {
BlockModelRegistry registry = new BlockModelRegistry(reader, textures);
BiomeTintProvider tintProvider = new BiomeTintProvider(textures);
eu.mhsl.minecraft.pixelpics.render.entity.cem.CemModelLoader cemLoader =
new eu.mhsl.minecraft.pixelpics.render.entity.cem.CemModelLoader();
try (java.io.InputStream in = getResource("cem/cem_template_models.json")) {
CemModelLoader cemLoader =
new CemModelLoader();
try (InputStream in = getResource("cem/cem_template_models.json")) {
int n = in == null ? 0 : cemLoader.load(in, getLogger());
getLogger().info("Loaded " + n + " CEM entity models.");
} catch (Exception e) {
getLogger().severe("Failed to load CEM entity models: " + e.getMessage());
}
eu.mhsl.minecraft.pixelpics.assets.SkinCache skinCache = new eu.mhsl.minecraft.pixelpics.assets.SkinCache();
eu.mhsl.minecraft.pixelpics.assets.font.BitmapFont font =
eu.mhsl.minecraft.pixelpics.assets.font.FontLoader.load(resourcePack, textures, getLogger());
SkinCache skinCache = new SkinCache();
BitmapFont font =
FontLoader.load(resourcePack, textures, getLogger());
getLogger().info("Loaded sign font (" + (font.isEmpty() ? "no glyphs — text disabled" : "ok") + ").");
eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker entityBaker =
new eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker(cemLoader, textures, skinCache);
eu.mhsl.minecraft.pixelpics.render.entity.cem.BlockEntityBaker blockEntityBaker =
new eu.mhsl.minecraft.pixelpics.render.entity.cem.BlockEntityBaker(cemLoader, textures, skinCache, font);
CemBaker entityBaker =
new CemBaker(cemLoader, textures, skinCache);
BlockEntityBaker blockEntityBaker =
new BlockEntityBaker(cemLoader, textures, skinCache, font);
this.screenRenderer = new DefaultScreenRenderer(registry, tintProvider, textures, entityBaker,
blockEntityBaker, getLogger(), renderManager.tracePool());
// Warm the map palette on the main thread so off-thread dithering never triggers its first init.
eu.mhsl.minecraft.pixelpics.utils.MapColorPalette.size();
MapColorPalette.size();
getLogger().info("PixelPics renderer initialized with resource pack assets.");
}
@Override
public void onDisable() {
eu.mhsl.minecraft.pixelpics.survival.SurvivalRecipes.unregister();
SurvivalRecipes.unregister();
if (renderManager != null) {
renderManager.shutdown();
renderManager = null;
@@ -8,6 +8,7 @@ import eu.mhsl.minecraft.pixelpics.assets.model.ResolvedModel;
import org.bukkit.Material;
import org.bukkit.block.data.BlockData;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
@@ -51,7 +52,7 @@ public final class BlockModelRegistry {
List<Variant> variants = blockStateResolver.resolve(data);
List<Element> elements = new java.util.ArrayList<>();
List<Element> elements = new ArrayList<>();
AverageColor.Accumulator avgColor = new AverageColor.Accumulator();
FlatModel lastFlat = null;
@@ -3,6 +3,7 @@ package eu.mhsl.minecraft.pixelpics.assets;
import eu.mhsl.minecraft.pixelpics.assets.dto.ModelFileDto;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -36,7 +37,7 @@ public final class ModelResolver {
}
Map<String, String> textures = new HashMap<>();
java.util.List<ModelFileDto.ElementDto> elements = dto.elements;
List<ModelFileDto.ElementDto> elements = dto.elements;
if (dto.parent != null && depth < MAX_DEPTH && !dto.parent.startsWith("builtin/")) {
FlatModel parent = resolve(ResourceLocation.parse(dto.parent), depth + 1);
@@ -15,6 +15,7 @@ import eu.mhsl.minecraft.pixelpics.render.entity.RenderedEntity;
import eu.mhsl.minecraft.pixelpics.render.entity.TextureOps;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -186,7 +187,7 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
/** Hidden set that leaves only {@code keep} visible out of {@code all}. */
private static Set<String> onlyPart(String keep, Set<String> all) {
Set<String> hidden = new java.util.HashSet<>(all);
Set<String> hidden = new HashSet<>(all);
hidden.remove(keep);
return hidden;
}
@@ -200,7 +201,7 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
if (base == null) return List.of();
List<Layer> layers = new ArrayList<>();
// Structure (rim/neck/foot) comes from the combined base texture; the four sides are NOT in it.
layers.add(new Layer(base, new java.util.HashSet<>(java.util.List.of("front", "back", "left", "right"))));
layers.add(new Layer(base, new HashSet<>(List.of("front", "back", "left", "right"))));
// Each side: its sherd pattern if set, else the plain brick side. The model's per-face UV maps the
// centre of the 16x16 texture onto the face (centred, edges intact).
for (int i = 0; i < POT_FACES.length; i++) {
@@ -15,7 +15,10 @@ import eu.mhsl.minecraft.pixelpics.render.entity.RenderedEntity;
import eu.mhsl.minecraft.pixelpics.render.entity.TextureOps;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Bakes an {@link EntityState} into world-space cubes using a vanilla Java {@link CemModelLoader.CemModel}
@@ -27,9 +30,9 @@ import java.util.List;
public final class CemBaker implements EntityBaker<EntityState> {
// Parts representing an alternate state (rolled-up, sleeping, …) that must not render in the idle pose.
private static final java.util.Map<String, java.util.Set<String>> HIDDEN_PARTS = java.util.Map.of(
"armadillo", java.util.Set.of("cube"), // the rolled-up ball
"illager", java.util.Set.of("left_arm", "right_arm")
private static final Map<String, Set<String>> HIDDEN_PARTS = Map.of(
"armadillo", Set.of("cube"), // the rolled-up ball
"illager", Set.of("left_arm", "right_arm")
);
private final CemModelLoader models;
@@ -67,7 +70,7 @@ public final class CemBaker implements EntityBaker<EntityState> {
// rotations and handedness; only px->block scaling is applied.
Affine pre = Affine.scale(sc / 16.0);
java.util.Set<String> hidden = new java.util.HashSet<>(HIDDEN_PARTS.getOrDefault(cem, java.util.Set.of()));
Set<String> hidden = new HashSet<>(HIDDEN_PARTS.getOrDefault(cem, Set.of()));
// Donkeys/llamas carry the chest boxes inside the base model; hide them unless a chest is equipped.
if (!s.chest()) {
if (cem.equals("donkey")) { hidden.add("left_chest"); hidden.add("right_chest"); }
@@ -76,7 +79,7 @@ public final class CemBaker implements EntityBaker<EntityState> {
// The body model is baked even when invisible — not drawn, but used as the ground-snap reference
// so equipment stays at body height (e.g. a lone helmet sits at the head, not on the floor).
List<CemGeometry.Baked> body = (model != null && tex != null)
? CemGeometry.bakeModel(model, tex, pre, hidden) : java.util.List.of();
? CemGeometry.bakeModel(model, tex, pre, hidden) : List.of();
List<CemGeometry.Baked> baked = new ArrayList<>();
if (!invisible) {
@@ -200,7 +203,7 @@ public final class CemBaker implements EntityBaker<EntityState> {
int[][] saddleTex = textures.get(ResourceLocation.parse("entity/equipment/" + s.typeKey() + "_saddle/saddle")).orElse(null);
if (saddleTex == null) return;
// Show only the saddle-specific parts: hide every part the base body model also defines.
java.util.Set<String> hideBase = new java.util.HashSet<>();
Set<String> hideBase = new HashSet<>();
for (CemModelLoader.CemPart p : base.parts()) hideBase.add(p.name());
baked.addAll(CemGeometry.bakeModel(sm, saddleTex, pre, hideBase));
}
@@ -211,10 +214,10 @@ public final class CemBaker implements EntityBaker<EntityState> {
// shoes=boots;
// armor_layer_2 (texture entity/equipment/humanoid_leggings/<mat>): waist+legs=leggings.
// Each slot may use a different material, so each is baked separately, showing only its parts.
private static final java.util.Set<String> ARMOR1_HEAD = java.util.Set.of("head");
private static final java.util.Set<String> ARMOR1_CHEST = java.util.Set.of("body", "left_arm", "right_arm");
private static final java.util.Set<String> ARMOR1_FEET = java.util.Set.of("left_shoe", "right_shoe");
private static final java.util.Set<String> ARMOR2_LEGS = java.util.Set.of("waist", "left_leg", "right_leg");
private static final Set<String> ARMOR1_HEAD = Set.of("head");
private static final Set<String> ARMOR1_CHEST = Set.of("body", "left_arm", "right_arm");
private static final Set<String> ARMOR1_FEET = Set.of("left_shoe", "right_shoe");
private static final Set<String> ARMOR2_LEGS = Set.of("waist", "left_leg", "right_leg");
private static final int GLINT_COLOR = 0xFF8040CC; // approximated enchantment-glint purple
private void addArmorLayers(EntityState s, Affine pre, List<CemGeometry.Baked> baked) {
@@ -231,14 +234,14 @@ public final class CemBaker implements EntityBaker<EntityState> {
}
/** Bake one armor slot: its layer model with only {@code show} parts, textured for the material. */
private void bakeArmorPiece(EntityState.EquipPiece piece, String modelName, java.util.Set<String> show,
private void bakeArmorPiece(EntityState.EquipPiece piece, String modelName, Set<String> show,
String layerFolder, Affine pre, List<CemGeometry.Baked> baked) {
if (piece == null) return;
CemModelLoader.CemModel model = models.get(modelName);
if (model == null) return;
int[][] tex = buildArmorTexture(piece, layerFolder);
if (tex == null) return;
java.util.Set<String> hidden = new java.util.HashSet<>();
Set<String> hidden = new HashSet<>();
for (CemModelLoader.CemPart p : model.parts()) if (!show.contains(p.name())) hidden.add(p.name());
baked.addAll(CemGeometry.bakeModel(model, tex, pre, hidden));
}
@@ -299,7 +302,7 @@ public final class CemBaker implements EntityBaker<EntityState> {
if (tex == null) return;
int[][] out = TextureOps.deepCopy(tex);
if (piece.glint()) applyGlint(out);
baked.addAll(CemGeometry.bakeModel(model, out, pre, java.util.Set.of()));
baked.addAll(CemGeometry.bakeModel(model, out, pre, Set.of()));
}
private RenderedEntity fallbackBox(EntityState s, int[][] tex) {
@@ -16,6 +16,9 @@ import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import org.bukkit.util.Vector;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Traces a single ray against a {@link WorldSnapshot}, sampling block models via the
* {@link ElementIntersector} and applying biome tint, directional face shading, transparency and
@@ -39,7 +42,7 @@ public final class SnapshotRaytracer {
private final double maxDistance;
private final int reflectionDepth;
private final int maxSteps;
private final java.util.Map<Long, BiomeTint> tintCache = new java.util.concurrent.ConcurrentHashMap<>();
private final Map<Long, BiomeTint> tintCache = new ConcurrentHashMap<>();
public SnapshotRaytracer(BlockModelRegistry registry, BiomeTintProvider tintProvider,
SkyRenderer skyRenderer, double maxDistance, int reflectionDepth) {
@@ -15,16 +15,24 @@ import org.bukkit.block.Banner;
import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockState;
import org.bukkit.block.DecoratedPot;
import org.bukkit.block.Sign;
import org.bukkit.block.Skull;
import org.bukkit.block.banner.Pattern;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.Directional;
import org.bukkit.block.data.Rotatable;
import org.bukkit.block.data.type.Bed;
import org.bukkit.block.data.type.Bell;
import org.bukkit.block.data.type.Chest;
import org.bukkit.block.sign.Side;
import org.bukkit.block.sign.SignSide;
import org.bukkit.profile.PlayerProfile;
import org.bukkit.util.Vector;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
@@ -77,7 +85,7 @@ public final class BlockEntitySnapshotBuilder {
Kind kind = mat == Material.TRAPPED_CHEST ? Kind.TRAPPED_CHEST
: mat == Material.ENDER_CHEST ? Kind.ENDER_CHEST : Kind.CHEST;
ChestKind ck = ChestKind.SINGLE;
if (data instanceof org.bukkit.block.data.type.Chest cd) {
if (data instanceof Chest cd) {
ck = switch (cd.getType()) {
case LEFT -> ChestKind.LEFT;
case RIGHT -> ChestKind.RIGHT;
@@ -88,8 +96,8 @@ public final class BlockEntitySnapshotBuilder {
}
// --- beds ---
if (data instanceof org.bukkit.block.data.type.Bed bed) {
BedPart part = bed.getPart() == org.bukkit.block.data.type.Bed.Part.HEAD ? BedPart.HEAD : BedPart.FOOT;
if (data instanceof Bed bed) {
BedPart part = bed.getPart() == Bed.Part.HEAD ? BedPart.HEAD : BedPart.FOOT;
return base(Kind.BED, bx, by, bz, faceToYaw(bed.getFacing()))
.bedPart(part).colorName(stripColor(n, "_BED")).build();
}
@@ -131,9 +139,9 @@ public final class BlockEntitySnapshotBuilder {
kind = Kind.SIGN; yaw = rotationYaw(data);
}
Builder b = base(kind, bx, by, bz, yaw).wood(wood);
if (ts instanceof org.bukkit.block.Sign sign) {
b.frontText(signText(sign.getSide(org.bukkit.block.sign.Side.FRONT)));
b.backText(signText(sign.getSide(org.bukkit.block.sign.Side.BACK)));
if (ts instanceof Sign sign) {
b.frontText(signText(sign.getSide(Side.FRONT)));
b.backText(signText(sign.getSide(Side.BACK)));
}
return b.build();
}
@@ -165,7 +173,7 @@ public final class BlockEntitySnapshotBuilder {
// --- bell ---
if (mat == Material.BELL) {
BellAttach attach = BellAttach.FLOOR;
if (data instanceof org.bukkit.block.data.type.Bell bd) {
if (data instanceof Bell bd) {
attach = switch (bd.getAttachment()) {
case FLOOR -> BellAttach.FLOOR;
case CEILING -> BellAttach.CEILING;
@@ -226,11 +234,11 @@ public final class BlockEntitySnapshotBuilder {
// --- data extraction helpers ---
private static String stripColor(String name, String suffix) {
return name.substring(0, name.length() - suffix.length()).toLowerCase(java.util.Locale.ROOT);
return name.substring(0, name.length() - suffix.length()).toLowerCase(Locale.ROOT);
}
/** One sign side → {@link BlockEntityState.SignText}, or null when all four lines are blank. */
private static BlockEntityState.SignText signText(org.bukkit.block.sign.SignSide side) {
private static BlockEntityState.SignText signText(SignSide side) {
String[] raw = side.getLines();
List<String> lines = new ArrayList<>(raw.length);
boolean any = false;
@@ -251,7 +259,7 @@ public final class BlockEntitySnapshotBuilder {
for (String suf : new String[]{"_WALL_HANGING_SIGN", "_HANGING_SIGN", "_WALL_SIGN", "_SIGN"}) {
if (s.endsWith(suf)) { s = s.substring(0, s.length() - suf.length()); break; }
}
return s.toLowerCase(java.util.Locale.ROOT);
return s.toLowerCase(Locale.ROOT);
}
private static String headType(String name) {
@@ -274,14 +282,14 @@ public final class BlockEntitySnapshotBuilder {
for (DecoratedPot.Side side : new DecoratedPot.Side[]{
DecoratedPot.Side.FRONT, DecoratedPot.Side.LEFT, DecoratedPot.Side.RIGHT, DecoratedPot.Side.BACK}) {
Material m = pot.getSherd(side);
out.add(m.name().toLowerCase(java.util.Locale.ROOT));
out.add(m.name().toLowerCase(Locale.ROOT));
}
return out;
}
private static String skinUrl(Skull skull) {
try {
org.bukkit.profile.PlayerProfile profile = skull.getOwnerProfile();
PlayerProfile profile = skull.getOwnerProfile();
if (profile != null && profile.getTextures().getSkin() != null) {
return profile.getTextures().getSkin().toString();
}
@@ -4,6 +4,7 @@ import eu.mhsl.minecraft.pixelpics.render.entity.DecorationState;
import org.bukkit.Location;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.Entity;
import org.bukkit.entity.GlowItemFrame;
import org.bukkit.entity.ItemFrame;
import org.bukkit.entity.Painting;
import org.bukkit.inventory.ItemStack;
@@ -47,7 +48,7 @@ public final class DecorationSnapshotBuilder {
}
if (e instanceof ItemFrame frame) {
BoundingBox bb = e.getBoundingBox();
boolean glow = e instanceof org.bukkit.entity.GlowItemFrame
boolean glow = e instanceof GlowItemFrame
|| e.getType().getKey().getKey().equals("glow_item_frame");
String itemId = itemId(frame.getItem());
int rot = frame.getRotation().ordinal() * 45;
@@ -1,18 +1,61 @@
package eu.mhsl.minecraft.pixelpics.render.snapshot;
import com.destroystokyo.paper.profile.ProfileProperty;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import eu.mhsl.minecraft.pixelpics.render.entity.EntityState;
import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil;
import io.papermc.paper.datacomponent.DataComponentTypes;
import org.bukkit.Keyed;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.AbstractHorse;
import org.bukkit.entity.AbstractNautilus;
import org.bukkit.entity.Ageable;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Axolotl;
import org.bukkit.entity.Cat;
import org.bukkit.entity.ChestedHorse;
import org.bukkit.entity.Chicken;
import org.bukkit.entity.Cow;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Fox;
import org.bukkit.entity.Frog;
import org.bukkit.entity.Horse;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Llama;
import org.bukkit.entity.MushroomCow;
import org.bukkit.entity.Panda;
import org.bukkit.entity.Parrot;
import org.bukkit.entity.Pig;
import org.bukkit.entity.Player;
import org.bukkit.entity.Rabbit;
import org.bukkit.entity.Sheep;
import org.bukkit.entity.Shulker;
import org.bukkit.entity.Slime;
import org.bukkit.entity.TraderLlama;
import org.bukkit.entity.Villager;
import org.bukkit.entity.Wolf;
import org.bukkit.entity.Zombie;
import org.bukkit.entity.ZombieVillager;
import org.bukkit.inventory.EntityEquipment;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ArmorMeta;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.LeatherArmorMeta;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.util.Vector;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
import java.util.function.DoubleSupplier;
/**
* Captures entities near the view frustum into immutable {@link EntityState}s. MUST run on the main
@@ -24,7 +67,7 @@ public final class EntitySnapshotBuilder {
// Technical / non-mob entity types that have no meaningful geometry; rendering them would only
// produce stray fallback boxes. Markers, displays, item frames, paintings, projectiles, drops, etc.
private static final java.util.Set<String> NON_RENDERABLE = java.util.Set.of(
private static final Set<String> NON_RENDERABLE = Set.of(
"area_effect_cloud", "marker", "interaction",
"item_frame", "glow_item_frame", "painting",
"block_display", "item_display", "text_display",
@@ -35,7 +78,7 @@ public final class EntitySnapshotBuilder {
// Entities whose vanilla renderer draws the humanoid armor layers (HumanoidArmorLayer) and held
// items. Their CEM bodies share standard humanoid proportions, so the armor_layer_1/2 models align.
private static final java.util.Set<String> HUMANOID_ARMOR_WEARERS = java.util.Set.of(
private static final Set<String> HUMANOID_ARMOR_WEARERS = Set.of(
"player", "mannequin", "armor_stand", "giant",
"zombie", "husk", "drowned", "zombie_villager", "zombified_piglin",
"skeleton", "stray", "wither_skeleton", "bogged",
@@ -67,13 +110,13 @@ public final class EntitySnapshotBuilder {
}
boolean baby = (e instanceof Ageable a && !a.isAdult())
|| (e instanceof org.bukkit.entity.Zombie z && z.isAdult());
|| (e instanceof Zombie z && z.isAdult());
// Invisible entities render only their equipment (like vanilla): the generic invisible flag, an
// invisibility potion effect, or an explicitly-hidden armor stand.
boolean invisible = e.isInvisible()
|| (e instanceof org.bukkit.entity.ArmorStand as && !as.isVisible())
|| (e instanceof LivingEntity inv && inv.hasPotionEffect(org.bukkit.potion.PotionEffectType.INVISIBILITY));
|| (e instanceof ArmorStand as && !as.isVisible())
|| (e instanceof LivingEntity inv && inv.hasPotionEffect(PotionEffectType.INVISIBILITY));
double width = safeDim(e::getWidth, () -> e.getBoundingBox().getWidthX());
double height = safeDim(e::getHeight, () -> e.getBoundingBox().getHeight());
@@ -98,65 +141,65 @@ public final class EntitySnapshotBuilder {
String bodyEquip = null;
try {
// Slime & magma cube (MagmaCube extends Slime) scale their model by size (1/2/4).
if (e instanceof org.bukkit.entity.Slime sl) sizeScale = sl.getSize();
if (e instanceof Slime sl) sizeScale = sl.getSize();
// MushroomCow extends Cow, ZombieVillager does not extend Villager — order matters.
if (e instanceof org.bukkit.entity.Sheep sh) {
if (e instanceof Sheep sh) {
tint = ColorUtil.dyeArgb(sh.getColor(), 0);
} else if (e instanceof org.bukkit.entity.Cat c) {
} else if (e instanceof Cat c) {
variant = keyOf(c.getCatType());
} else if (e instanceof org.bukkit.entity.Wolf w) {
} else if (e instanceof Wolf w) {
variant = keyOf(w.getVariant());
} else if (e instanceof org.bukkit.entity.Axolotl a) {
} else if (e instanceof Axolotl a) {
variant = keyOf(a.getVariant());
} else if (e instanceof org.bukkit.entity.Parrot p) {
} else if (e instanceof Parrot p) {
variant = keyOf(p.getVariant());
} else if (e instanceof org.bukkit.entity.Rabbit r) {
} else if (e instanceof Rabbit r) {
variant = keyOf(r.getRabbitType());
} else if (e instanceof org.bukkit.entity.Horse h) {
} else if (e instanceof Horse h) {
variant = keyOf(h.getColor());
markings = markingsKey(h.getStyle());
saddle = isSaddled(h);
bodyEquip = horseArmorKey(h);
} else if (e instanceof org.bukkit.entity.Llama l) {
} else if (e instanceof Llama l) {
variant = keyOf(l.getColor());
chest = l.isCarryingChest();
// Trader llamas wear a fixed decor; normal llamas carry a dyed carpet in the decor slot.
bodyEquip = (e instanceof org.bukkit.entity.TraderLlama) ? "trader_llama" : carpetKey(l);
} else if (e instanceof org.bukkit.entity.ChestedHorse ch) {
bodyEquip = (e instanceof TraderLlama) ? "trader_llama" : carpetKey(l);
} else if (e instanceof ChestedHorse ch) {
// Donkey & mule (llama already handled above).
chest = ch.isCarryingChest();
saddle = isSaddled(ch);
} else if (e instanceof org.bukkit.entity.AbstractHorse ah) {
} else if (e instanceof AbstractHorse ah) {
// Skeleton/zombie horse: only saddle (no colour/markings/armor variants).
saddle = isSaddled(ah);
} else if (e instanceof org.bukkit.entity.AbstractNautilus nl) {
} else if (e instanceof AbstractNautilus nl) {
// Nautilus body armor + saddle are same-UV overlays (like horse armor).
org.bukkit.inventory.EntityEquipment eq = nl.getEquipment();
bodyEquip = equipAsset(eq.getItem(org.bukkit.inventory.EquipmentSlot.BODY));
org.bukkit.inventory.ItemStack sd = eq.getItem(org.bukkit.inventory.EquipmentSlot.SADDLE);
EntityEquipment eq = nl.getEquipment();
bodyEquip = equipAsset(eq.getItem(EquipmentSlot.BODY));
ItemStack sd = eq.getItem(EquipmentSlot.SADDLE);
saddle = !sd.getType().isAir();
} else if (e instanceof org.bukkit.entity.Fox f) {
} else if (e instanceof Fox f) {
variant = keyOf(f.getFoxType());
} else if (e instanceof org.bukkit.entity.MushroomCow mc) {
} else if (e instanceof MushroomCow mc) {
variant = keyOf(mc.getVariant());
} else if (e instanceof org.bukkit.entity.Panda pa) {
} else if (e instanceof Panda pa) {
variant = keyOf(pa.getMainGene());
} else if (e instanceof org.bukkit.entity.Frog fr) {
} else if (e instanceof Frog fr) {
variant = keyOf(fr.getVariant());
} else if (e instanceof org.bukkit.entity.Shulker s) {
} else if (e instanceof Shulker s) {
variant = s.getColor() == null ? null : keyOf(s.getColor());
} else if (e instanceof org.bukkit.entity.ZombieVillager zv) {
} else if (e instanceof ZombieVillager zv) {
variant = keyOf(zv.getVillagerType());
// ZombieVillager exposes no level via Bukkit -> no profession-level badge (matches vanilla).
} else if (e instanceof org.bukkit.entity.Villager vi) {
} else if (e instanceof Villager vi) {
variant = keyOf(vi.getVillagerType());
profession = keyOf(vi.getProfession());
villagerLevel = vi.getVillagerLevel();
} else if (e instanceof org.bukkit.entity.Cow co) {
} else if (e instanceof Cow co) {
variant = keyOf(co.getVariant());
} else if (e instanceof org.bukkit.entity.Pig pg) {
} else if (e instanceof Pig pg) {
variant = keyOf(pg.getVariant());
} else if (e instanceof org.bukkit.entity.Chicken ch) {
} else if (e instanceof Chicken ch) {
variant = keyOf(ch.getVariant());
}
} catch (Throwable ignored) {
@@ -177,7 +220,7 @@ public final class EntitySnapshotBuilder {
/** Worn armor (4 slots) from a humanoid wearer; null when nothing is equipped. */
private static EntityState.Equipment captureEquipment(LivingEntity le) {
try {
org.bukkit.inventory.EntityEquipment eq = le.getEquipment();
EntityEquipment eq = le.getEquipment();
if (eq == null) return null;
EntityState.Equipment equip = new EntityState.Equipment(
armorPiece(eq.getHelmet()), armorPiece(eq.getChestplate()),
@@ -189,7 +232,7 @@ public final class EntitySnapshotBuilder {
}
/** One armor slot -> EquipPiece (asset, leather dye, trim, glint); null for empty / non-armor items. */
private static EntityState.EquipPiece armorPiece(org.bukkit.inventory.ItemStack it) {
private static EntityState.EquipPiece armorPiece(ItemStack it) {
if (it == null || it.getType().isAir()) return null;
String asset = armorAsset(it.getType());
if (asset == null) return null; // not a humanoid-armor item (e.g. a mob head / pumpkin)
@@ -197,10 +240,10 @@ public final class EntitySnapshotBuilder {
String trimMat = null, trimPat = null;
boolean glint = false;
if (it.hasItemMeta()) {
org.bukkit.inventory.meta.ItemMeta meta = it.getItemMeta();
ItemMeta meta = it.getItemMeta();
glint = meta.hasEnchants();
if (meta instanceof org.bukkit.inventory.meta.LeatherArmorMeta lam) dye = lam.getColor().asARGB();
if (meta instanceof org.bukkit.inventory.meta.ArmorMeta am && am.getTrim() != null) {
if (meta instanceof LeatherArmorMeta lam) dye = lam.getColor().asARGB();
if (meta instanceof ArmorMeta am && am.getTrim() != null) {
trimMat = keyOf(am.getTrim().getMaterial());
trimPat = keyOf(am.getTrim().getPattern());
}
@@ -209,7 +252,7 @@ public final class EntitySnapshotBuilder {
}
/** Item Material -> equipment asset id (= texture name): strips the slot suffix; null if not armor. */
private static String armorAsset(org.bukkit.Material m) {
private static String armorAsset(Material m) {
String key = m.getKey().getKey();
if (key.equals("elytra")) return "elytra";
if (key.equals("turtle_helmet")) return "turtle_scute";
@@ -222,32 +265,32 @@ public final class EntitySnapshotBuilder {
}
/** Horse coat markings overlay key (vanilla texture suffix); null for the plain NONE style. */
private static String markingsKey(org.bukkit.entity.Horse.Style style) {
if (style == null || style == org.bukkit.entity.Horse.Style.NONE) return null;
return style.name().toLowerCase(java.util.Locale.ROOT).replace("_", ""); // WHITE_DOTS -> whitedots
private static String markingsKey(Horse.Style style) {
if (style == null || style == Horse.Style.NONE) return null;
return style.name().toLowerCase(Locale.ROOT).replace("_", ""); // WHITE_DOTS -> whitedots
}
/** Horse armor material -> equipment/horse_body texture key (golden uses the "gold" file); null if none. */
private static String horseArmorKey(org.bukkit.entity.Horse h) {
org.bukkit.inventory.ItemStack a = h.getInventory().getArmor();
private static String horseArmorKey(Horse h) {
ItemStack a = h.getInventory().getArmor();
if (a == null || a.getType().isAir()) return null;
String k = a.getType().getKey().getKey().replace("_horse_armor", "");
return k.equals("golden") ? "gold" : k;
}
/** Llama carpet decor -> equipment/llama_body colour key; null if none. */
private static String carpetKey(org.bukkit.entity.Llama l) {
org.bukkit.inventory.ItemStack d = l.getInventory().getDecor();
private static String carpetKey(Llama l) {
ItemStack d = l.getInventory().getDecor();
if (d == null || d.getType().isAir()) return null;
String k = d.getType().getKey().getKey();
return k.endsWith("_carpet") ? k.substring(0, k.length() - "_carpet".length()) : null;
}
/** Equipment asset id (= equipment/<asset> texture name) from an item's EQUIPPABLE component; null if none. */
private static String equipAsset(org.bukkit.inventory.ItemStack it) {
private static String equipAsset(ItemStack it) {
if (it == null || it.getType().isAir()) return null;
try {
var comp = it.getData(io.papermc.paper.datacomponent.DataComponentTypes.EQUIPPABLE);
var comp = it.getData(DataComponentTypes.EQUIPPABLE);
if (comp != null && comp.assetId() != null) return comp.assetId().value();
} catch (Throwable ignored) {
}
@@ -255,8 +298,8 @@ public final class EntitySnapshotBuilder {
}
/** Whether a horse-like mount carries a saddle in its dedicated saddle slot. */
private static boolean isSaddled(org.bukkit.entity.AbstractHorse h) {
org.bukkit.inventory.ItemStack st = h.getInventory().getSaddle();
private static boolean isSaddled(AbstractHorse h) {
ItemStack st = h.getInventory().getSaddle();
return st != null && !st.getType().isAir();
}
@@ -264,21 +307,21 @@ public final class EntitySnapshotBuilder {
private static String keyOf(Object o) {
return switch(o) {
case null -> null;
case org.bukkit.Keyed k -> k.getKey().getKey();
case Enum<?> en -> en.name().toLowerCase(java.util.Locale.ROOT);
default -> o.toString().toLowerCase(java.util.Locale.ROOT);
case Keyed k -> k.getKey().getKey();
case Enum<?> en -> en.name().toLowerCase(Locale.ROOT);
default -> o.toString().toLowerCase(Locale.ROOT);
};
}
/** Returns {skinUrl, model} from the player's profile texture property, or {null, null}. */
private static String[] resolveSkin(Player player) {
try {
for (com.destroystokyo.paper.profile.ProfileProperty prop : player.getPlayerProfile().getProperties()) {
for (ProfileProperty prop : player.getPlayerProfile().getProperties()) {
if (!prop.getName().equals("textures")) continue;
String json = new String(java.util.Base64.getDecoder().decode(prop.getValue()),
java.nio.charset.StandardCharsets.UTF_8);
com.google.gson.JsonObject root = com.google.gson.JsonParser.parseString(json).getAsJsonObject();
com.google.gson.JsonObject skin = root.getAsJsonObject("textures").getAsJsonObject("SKIN");
String json = new String(Base64.getDecoder().decode(prop.getValue()),
StandardCharsets.UTF_8);
JsonObject root = JsonParser.parseString(json).getAsJsonObject();
JsonObject skin = root.getAsJsonObject("textures").getAsJsonObject("SKIN");
String url = skin.get("url").getAsString();
String model = null;
if (skin.has("metadata") && skin.getAsJsonObject("metadata").has("model")) {
@@ -292,7 +335,7 @@ public final class EntitySnapshotBuilder {
}
/** Reads a dimension via {@code primary}, falling back to {@code fallback} on any version mismatch. */
private static double safeDim(java.util.function.DoubleSupplier primary, java.util.function.DoubleSupplier fallback) {
private static double safeDim(DoubleSupplier primary, DoubleSupplier fallback) {
try {
return primary.getAsDouble();
} catch (Throwable t) {
@@ -1,5 +1,8 @@
package eu.mhsl.minecraft.pixelpics.render.util;
import org.bukkit.Color;
import org.bukkit.DyeColor;
/**
* Helpers for packed ARGB integer colors.
*/
@@ -17,9 +20,9 @@ public final class ColorUtil {
}
/** Opaque ARGB for a Bukkit dye colour, or {@code fallback} when {@code dye} is null. */
public static int dyeArgb(org.bukkit.DyeColor dye, int fallback) {
public static int dyeArgb(DyeColor dye, int fallback) {
if (dye == null) return fallback;
org.bukkit.Color c = dye.getColor();
Color c = dye.getColor();
return argb(0xFF, c.getRed(), c.getGreen(), c.getBlue());
}
@@ -27,8 +30,8 @@ public final class ColorUtil {
* The vanilla sign-text colour for a dye (opaque ARGB), from Mojang's {@code DyeColor.getTextColor()}
* table (the firework/text colours, NOT the cloth colours). Null = black (the default sign ink).
*/
public static int signTextColor(org.bukkit.DyeColor dye) {
int rgb = switch (dye == null ? org.bukkit.DyeColor.BLACK : dye) {
public static int signTextColor(DyeColor dye) {
int rgb = switch (dye == null ? DyeColor.BLACK : dye) {
case WHITE -> 0xF9FFFE;
case ORANGE -> 0xF9801D;
case MAGENTA -> 0xC74EBD;
@@ -53,7 +56,7 @@ public final class ColorUtil {
* The fill colour for sign text: glowing text uses the full dye colour; non-glowing text is the dye
* colour darkened to 40% (matching vanilla {@code SignRenderer}, which is why normal ink looks dim).
*/
public static int signFillArgb(org.bukkit.DyeColor dye, boolean glowing) {
public static int signFillArgb(DyeColor dye, boolean glowing) {
int base = signTextColor(dye);
return glowing ? base : (0xFF000000 | (shade(base, 0.4) & 0xFFFFFF));
}
@@ -63,8 +66,8 @@ public final class ColorUtil {
* the dye colour darkened to 40%, except glowing BLACK ink which gets a light cream outline so it
* stays readable.
*/
public static int signOutlineArgb(org.bukkit.DyeColor dye) {
if ((dye == null ? org.bukkit.DyeColor.BLACK : dye) == org.bukkit.DyeColor.BLACK) return 0xFFF0EBCC;
public static int signOutlineArgb(DyeColor dye) {
if ((dye == null ? DyeColor.BLACK : dye) == DyeColor.BLACK) return 0xFFF0EBCC;
return 0xFF000000 | (shade(signTextColor(dye), 0.4) & 0xFFFFFF);
}
@@ -8,6 +8,7 @@ import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
@@ -112,7 +113,7 @@ public final class CameraItems {
// --- internals ---
private static boolean hasMarker(ItemStack item, org.bukkit.NamespacedKey key) {
private static boolean hasMarker(ItemStack item, NamespacedKey key) {
if (item == null || item.getType() != Material.PLAYER_HEAD || !item.hasItemMeta()) return false;
PersistentDataContainer pdc = item.getItemMeta().getPersistentDataContainer();
return pdc.has(key, PersistentDataType.BYTE);