diff --git a/build.gradle b/build.gradle index 04ed2b9..78eb51c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,6 @@ plugins { id 'java' + id 'com.diffplug.spotless' version '7.0.2' } group = 'eu.mhsl.minecraft.pixelpics' @@ -48,6 +49,14 @@ processResources { } } +spotless { + java { + target 'src/main/java/**/*.java' + palantirJavaFormat() + removeUnusedImports() + } +} + if (file("local.gradle").exists()) { apply from: "local.gradle" } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/Main.java b/src/main/java/eu/mhsl/minecraft/pixelpics/Main.java index cf1fe9a..42a8cb2 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/Main.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/Main.java @@ -21,14 +21,13 @@ 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; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.plugin.java.JavaPlugin; public final class Main extends JavaPlugin { @@ -75,8 +74,9 @@ public final class Main extends JavaPlugin { int timeoutSeconds = Math.max(1, getConfig().getInt("render.timeout-seconds", 30)); this.renderManager = new RenderManager(this, threads, maxConcurrent, queueSize, timeoutSeconds); - getLogger().info("Render pool: " + threads + " core(s), max " + maxConcurrent - + " concurrent, queue " + queueSize + ", timeout " + timeoutSeconds + "s."); + getLogger() + .info("Render pool: " + threads + " core(s), max " + maxConcurrent + " concurrent, queue " + queueSize + + ", timeout " + timeoutSeconds + "s."); } private void initRenderer() { @@ -87,9 +87,10 @@ public final class Main extends JavaPlugin { Optional pack = ResourcePackLoader.load(resourcePackDir, getLogger()); if (pack.isEmpty()) { - getLogger().severe("No resource pack found in " + resourcePackDir.getPath() - + " — place a vanilla resource pack (directory with assets/minecraft/... or a .zip) there. " - + "/pixelPic is disabled until a pack is available."); + getLogger() + .severe("No resource pack found in " + resourcePackDir.getPath() + + " — place a vanilla resource pack (directory with assets/minecraft/... or a .zip) there. " + + "/pixelPic is disabled until a pack is available."); return; } @@ -99,8 +100,7 @@ public final class Main extends JavaPlugin { BlockModelRegistry registry = new BlockModelRegistry(reader, textures); BiomeTintProvider tintProvider = new BiomeTintProvider(textures); - CemModelLoader cemLoader = - new CemModelLoader(); + 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."); @@ -108,16 +108,19 @@ public final class Main extends JavaPlugin { getLogger().severe("Failed to load CEM entity models: " + e.getMessage()); } SkinCache skinCache = new SkinCache(); - BitmapFont font = - FontLoader.load(resourcePack, textures, getLogger()); + BitmapFont font = FontLoader.load(resourcePack, textures, getLogger()); getLogger().info("Loaded sign font (" + (font.isEmpty() ? "no glyphs — text disabled" : "ok") + ")."); - CemBaker entityBaker = - new CemBaker(cemLoader, textures, skinCache); - BlockEntityBaker blockEntityBaker = - new 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()); + 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. MapColorPalette.size(); getLogger().info("PixelPics renderer initialized with resource pack assets."); diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/AssetPaths.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/AssetPaths.java index 7e9f8e7..d178a3e 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/AssetPaths.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/AssetPaths.java @@ -10,7 +10,8 @@ public final class AssetPaths { /** {@code assets//blockstates/.json} for a plain block name (no {@code block/} prefix). */ public static String blockState(String blockName) { - return String.format("assets/%s/blockstates/%s.json", ResourceLocation.DEFAULT_NAMESPACE, blockName.toLowerCase()); + return String.format( + "assets/%s/blockstates/%s.json", ResourceLocation.DEFAULT_NAMESPACE, blockName.toLowerCase()); } /** {@code assets//models/.json}. The id path already contains e.g. {@code block/stone}. */ diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/AssetReader.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/AssetReader.java index b8a8f1d..b93ded5 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/AssetReader.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/AssetReader.java @@ -3,7 +3,6 @@ package eu.mhsl.minecraft.pixelpics.assets; import com.google.gson.Gson; import com.google.gson.JsonObject; import com.google.gson.JsonParser; - import java.nio.charset.StandardCharsets; import java.util.Optional; @@ -32,7 +31,8 @@ public final class AssetReader { public Optional readJsonObject(String path) { return pack.read(path).flatMap(bytes -> { try { - return Optional.of(JsonParser.parseString(new String(bytes, StandardCharsets.UTF_8)).getAsJsonObject()); + return Optional.of(JsonParser.parseString(new String(bytes, StandardCharsets.UTF_8)) + .getAsJsonObject()); } catch (Exception e) { return Optional.empty(); } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/BlockModelRegistry.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/BlockModelRegistry.java index 1f0c3fb..0bfb872 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/BlockModelRegistry.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/BlockModelRegistry.java @@ -5,14 +5,13 @@ import eu.mhsl.minecraft.pixelpics.assets.model.Direction; import eu.mhsl.minecraft.pixelpics.assets.model.Element; import eu.mhsl.minecraft.pixelpics.assets.model.Face; 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; import java.util.concurrent.ConcurrentHashMap; +import org.bukkit.Material; +import org.bukkit.block.data.BlockData; /** * Top-level entry point that turns a {@link BlockData} into a baked {@link ResolvedModel}, combining @@ -88,8 +87,12 @@ public final class BlockModelRegistry { private static EnumSet buildInvisibleMaterials() { EnumSet set = EnumSet.noneOf(Material.class); - for (String n : new String[]{"BARRIER", "LIGHT", "STRUCTURE_VOID"}) { - try { set.add(Material.valueOf(n)); } catch (IllegalArgumentException ignored) { /* older/newer server */ } + for (String n : new String[] {"BARRIER", "LIGHT", "STRUCTURE_VOID"}) { + try { + set.add(Material.valueOf(n)); + } catch (IllegalArgumentException ignored) { + /* older/newer server */ + } } return set; } @@ -107,14 +110,17 @@ public final class BlockModelRegistry { EnumSet set = EnumSet.noneOf(Material.class); for (Material m : Material.values()) { String n = m.name(); - boolean match = - n.endsWith("_SIGN") // signs: standing/wall/(wall_)hanging - || n.endsWith("_BANNER") // banners: standing/wall - || n.endsWith("_BED") - || n.endsWith("SHULKER_BOX") // SHULKER_BOX + _SHULKER_BOX - || ((n.endsWith("_SKULL") || n.endsWith("_HEAD")) && m != Material.PISTON_HEAD) - || m == Material.CHEST || m == Material.TRAPPED_CHEST || m == Material.ENDER_CHEST - || m == Material.CONDUIT || m == Material.DECORATED_POT || m == Material.BELL; + boolean match = n.endsWith("_SIGN") // signs: standing/wall/(wall_)hanging + || n.endsWith("_BANNER") // banners: standing/wall + || n.endsWith("_BED") + || n.endsWith("SHULKER_BOX") // SHULKER_BOX + _SHULKER_BOX + || ((n.endsWith("_SKULL") || n.endsWith("_HEAD")) && m != Material.PISTON_HEAD) + || m == Material.CHEST + || m == Material.TRAPPED_CHEST + || m == Material.ENDER_CHEST + || m == Material.CONDUIT + || m == Material.DECORATED_POT + || m == Material.BELL; if (match) set.add(m); } return set; @@ -127,7 +133,7 @@ public final class BlockModelRegistry { for (Direction d : Direction.values()) { faces[d.ordinal()] = new Face(tex, 0, 0, 1, 1, 0, -1); } - return new Element(new double[]{0, 0, 0}, new double[]{1, 1, 1}, faces, null, -1, 0, false); + return new Element(new double[] {0, 0, 0}, new double[] {1, 1, 1}, faces, null, -1, 0, false); } private int fallbackColor(FlatModel flat) { @@ -169,7 +175,7 @@ public final class BlockModelRegistry { for (Direction d : Direction.values()) { faces[d.ordinal()] = new Face(tex, 0, 0, 1, 1, 0, tintIndex); } - Element cube = new Element(new double[]{0, 0, 0}, new double[]{1, 1, 1}, faces, null, -1, 0, false); + Element cube = new Element(new double[] {0, 0, 0}, new double[] {1, 1, 1}, faces, null, -1, 0, false); int avg = AverageColor.of(tex); return new ResolvedModel(List.of(cube), avg, transparency, reflection, true, true); } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/BlockStateProperties.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/BlockStateProperties.java index 8362f0d..adb829d 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/BlockStateProperties.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/BlockStateProperties.java @@ -1,9 +1,8 @@ package eu.mhsl.minecraft.pixelpics.assets; -import org.bukkit.block.data.BlockData; - import java.util.LinkedHashMap; import java.util.Map; +import org.bukkit.block.data.BlockData; /** * Parses the property map and plain block name out of a {@link BlockData} string such as diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/BlockStateResolver.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/BlockStateResolver.java index 0fe66fb..ae1e58d 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/BlockStateResolver.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/BlockStateResolver.java @@ -3,11 +3,10 @@ package eu.mhsl.minecraft.pixelpics.assets; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import org.bukkit.block.data.BlockData; - import java.util.ArrayList; import java.util.List; import java.util.Map; +import org.bukkit.block.data.BlockData; /** * Resolves a {@link BlockData} to the list of model variants vanilla would render, by reading the diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/FlatModel.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/FlatModel.java index 324b3ab..d86b302 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/FlatModel.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/FlatModel.java @@ -1,7 +1,6 @@ package eu.mhsl.minecraft.pixelpics.assets; import eu.mhsl.minecraft.pixelpics.assets.dto.ModelFileDto; - import java.util.List; import java.util.Map; diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/ModelBaker.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/ModelBaker.java index d5c07a9..2460789 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/ModelBaker.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/ModelBaker.java @@ -5,7 +5,6 @@ import eu.mhsl.minecraft.pixelpics.assets.model.AverageColor; import eu.mhsl.minecraft.pixelpics.assets.model.Direction; import eu.mhsl.minecraft.pixelpics.assets.model.Element; import eu.mhsl.minecraft.pixelpics.assets.model.Face; - import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -61,8 +60,9 @@ public final class ModelBaker { double rotAngle = 0; boolean rescale = false; if (dto.rotation != null && dto.rotation.angle != 0 && dto.rotation.origin != null) { - rotOrigin = new double[]{ - dto.rotation.origin[0] / 16.0, dto.rotation.origin[1] / 16.0, dto.rotation.origin[2] / 16.0}; + rotOrigin = new double[] { + dto.rotation.origin[0] / 16.0, dto.rotation.origin[1] / 16.0, dto.rotation.origin[2] / 16.0 + }; rotAxis = axisIndex(dto.rotation.axis); rotAngle = Math.toRadians(dto.rotation.angle); rescale = dto.rotation.rescale; @@ -106,8 +106,8 @@ public final class ModelBaker { return new BakedGeometry(baked, avgColor.average(0xFF7F7F7F), !baked.isEmpty()); } - private Face buildFace(Direction dir, ModelFileDto.FaceDto dto, double[] from, double[] to, - Map textureVars) { + private Face buildFace( + Direction dir, ModelFileDto.FaceDto dto, double[] from, double[] to, Map textureVars) { int[][] tex = resolveTexture(dto.texture, textureVars); if (tex == null) return null; @@ -134,9 +134,9 @@ public final class ModelBaker { */ private double[] defaultUv(Direction dir, double[] f, double[] t) { return switch (dir) { - case UP, DOWN -> new double[]{f[0], f[2], t[0], t[2]}; - case NORTH, SOUTH -> new double[]{f[0], 1 - t[1], t[0], 1 - f[1]}; - case WEST, EAST -> new double[]{f[2], 1 - t[1], t[2], 1 - f[1]}; + case UP, DOWN -> new double[] {f[0], f[2], t[0], t[2]}; + case NORTH, SOUTH -> new double[] {f[0], 1 - t[1], t[0], 1 - f[1]}; + case WEST, EAST -> new double[] {f[2], 1 - t[1], t[2], 1 - f[1]}; }; } @@ -179,18 +179,18 @@ public final class ModelBaker { private double[] rotatePointY(double[] p) { double x = p[0] - 0.5, z = p[2] - 0.5; - return new double[]{0.5 + z, p[1], 0.5 - x}; + return new double[] {0.5 + z, p[1], 0.5 - x}; } private double[] rotatePointX(double[] p) { double y = p[1] - 0.5, z = p[2] - 0.5; - return new double[]{p[0], 0.5 + z, 0.5 - y}; + return new double[] {p[0], 0.5 + z, 0.5 - y}; } private double[][] minMax(double[] a, double[] b) { double[] from = {Math.min(a[0], b[0]), Math.min(a[1], b[1]), Math.min(a[2], b[2])}; double[] to = {Math.max(a[0], b[0]), Math.max(a[1], b[1]), Math.max(a[2], b[2])}; - return new double[][]{from, to}; + return new double[][] {from, to}; } private Face[] rotateFacesY(Face[] faces) { @@ -216,7 +216,7 @@ public final class ModelBaker { private AxisRotation rotateAxisY(int axis) { // Y rotation maps x<->z; the y axis is unchanged. return switch (axis) { - case 0 -> new AxisRotation(2, true); // x -> z + case 0 -> new AxisRotation(2, true); // x -> z case 2 -> new AxisRotation(0, false); // z -> x default -> new AxisRotation(axis, false); }; @@ -225,7 +225,7 @@ public final class ModelBaker { private AxisRotation rotateAxisX(int axis) { // X rotation maps y<->z; the x axis is unchanged. return switch (axis) { - case 1 -> new AxisRotation(2, true); // y -> z + case 1 -> new AxisRotation(2, true); // y -> z case 2 -> new AxisRotation(1, false); // z -> y default -> new AxisRotation(axis, false); }; diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/ModelResolver.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/ModelResolver.java index b691d01..5f2525d 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/ModelResolver.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/ModelResolver.java @@ -1,7 +1,6 @@ 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; @@ -31,7 +30,8 @@ public final class ModelResolver { } private FlatModel resolve(ResourceLocation modelId, int depth) { - ModelFileDto dto = reader.readJson(AssetPaths.model(modelId), ModelFileDto.class).orElse(null); + ModelFileDto dto = + reader.readJson(AssetPaths.model(modelId), ModelFileDto.class).orElse(null); if (dto == null) { return new FlatModel(new HashMap<>(), null); } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/ResourcePackLoader.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/ResourcePackLoader.java index 2ec787a..1bb1da7 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/ResourcePackLoader.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/ResourcePackLoader.java @@ -45,15 +45,13 @@ public final class ResourcePackLoader { // Zip packs anywhere under the resourcepack folder. try (Stream walk = Files.walk(resourcePackDir.toPath())) { - List zips = walk - .filter(Files::isRegularFile) - .filter(p -> p.getFileName().toString().toLowerCase().endsWith(".zip")) - .toList(); + List zips = walk.filter(Files::isRegularFile) + .filter(p -> p.getFileName().toString().toLowerCase().endsWith(".zip")) + .toList(); for (Path zip : zips) { try { ZipResourcePack pack = new ZipResourcePack(zip); - if (pack.exists(MARKER + "/blockstates") || pack.exists("pack.mcmeta") - || hasAnyBlockstate(pack)) { + if (pack.exists(MARKER + "/blockstates") || pack.exists("pack.mcmeta") || hasAnyBlockstate(pack)) { packs.add(pack); logger.info("Loaded resource pack zip: " + zip); } else { diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/SkinCache.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/SkinCache.java index 63d33b7..e1aac5b 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/SkinCache.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/SkinCache.java @@ -1,12 +1,12 @@ package eu.mhsl.minecraft.pixelpics.assets; -import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.InputStream; import java.net.URI; import java.net.URL; import java.net.URLConnection; import java.util.Optional; +import javax.imageio.ImageIO; /** * Downloads and caches player skin textures (by URL) as ARGB pixel grids. Legacy 64x32 skins are diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/TextureCache.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/TextureCache.java index d0c3848..b8470aa 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/TextureCache.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/TextureCache.java @@ -1,8 +1,8 @@ package eu.mhsl.minecraft.pixelpics.assets; -import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; +import javax.imageio.ImageIO; /** * Loads and caches block textures as raw ARGB pixel grids. Textures are stored unflipped (vanilla diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/Variant.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/Variant.java index 0a0a084..09de069 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/Variant.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/Variant.java @@ -4,5 +4,4 @@ package eu.mhsl.minecraft.pixelpics.assets; * A resolved blockstate variant: which model to use plus its {@code x}/{@code y} rotation (in * degrees, multiples of 90) and {@code uvlock}. */ -public record Variant(ResourceLocation model, int x, int y, boolean uvlock) { -} +public record Variant(ResourceLocation model, int x, int y, boolean uvlock) {} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/ZipResourcePack.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/ZipResourcePack.java index 2787128..d6d8485 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/ZipResourcePack.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/ZipResourcePack.java @@ -22,9 +22,7 @@ public final class ZipResourcePack implements ResourcePack { public ZipResourcePack(Path zipPath) throws IOException { this.zipFile = new ZipFile(zipPath.toFile()); - zipFile.stream() - .filter(entry -> !entry.isDirectory()) - .forEach(entry -> entries.put(entry.getName(), entry)); + zipFile.stream().filter(entry -> !entry.isDirectory()).forEach(entry -> entries.put(entry.getName(), entry)); } @Override diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/dto/ModelFileDto.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/dto/ModelFileDto.java index bc78579..eb94633 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/dto/ModelFileDto.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/dto/ModelFileDto.java @@ -13,24 +13,24 @@ public class ModelFileDto { public List elements; public static class ElementDto { - public double[] from; // 0..16 - public double[] to; // 0..16 - public RotationDto rotation; // optional + public double[] from; // 0..16 + public double[] to; // 0..16 + public RotationDto rotation; // optional public Map faces; // keys: down/up/north/south/west/east } public static class FaceDto { - public double[] uv; // optional, 0..16 (x1,y1,x2,y2) - public String texture; // e.g. "#side" or "minecraft:block/oak_planks" + public double[] uv; // optional, 0..16 (x1,y1,x2,y2) + public String texture; // e.g. "#side" or "minecraft:block/oak_planks" public Integer tintindex; public String cullface; // ignored by the renderer - public int rotation; // 0/90/180/270 + public int rotation; // 0/90/180/270 } public static class RotationDto { public double[] origin; // 0..16 - public String axis; // "x" | "y" | "z" - public double angle; // -45..45 in 22.5 steps + public String axis; // "x" | "y" | "z" + public double angle; // -45..45 in 22.5 steps public boolean rescale; } } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/font/FontLoader.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/font/FontLoader.java index 08349d9..0235d12 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/font/FontLoader.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/font/FontLoader.java @@ -8,7 +8,6 @@ import eu.mhsl.minecraft.pixelpics.assets.AssetPaths; import eu.mhsl.minecraft.pixelpics.assets.ResourceLocation; import eu.mhsl.minecraft.pixelpics.assets.ResourcePack; import eu.mhsl.minecraft.pixelpics.assets.TextureCache; - import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.HashSet; @@ -63,7 +62,8 @@ public final class FontLoader { if (bytes.isEmpty()) return; JsonObject root; try { - root = JsonParser.parseString(new String(bytes.get(), StandardCharsets.UTF_8)).getAsJsonObject(); + root = JsonParser.parseString(new String(bytes.get(), StandardCharsets.UTF_8)) + .getAsJsonObject(); } catch (Exception e) { if (log != null) log.warning("PixelPics: failed to parse font " + path + ": " + e.getMessage()); return; @@ -84,7 +84,9 @@ public final class FontLoader { } case "space" -> space(p); case "bitmap" -> bitmap(p); - default -> { /* unihex, legacy_unicode, ttf, … — out of scope */ } + default -> { + /* unihex, legacy_unicode, ttf, … — out of scope */ + } } } @@ -117,7 +119,9 @@ public final class FontLoader { JsonArray rows = p.getAsJsonArray("chars"); int nRows = rows.size(); if (nRows == 0) return; - int nCols = rows.get(0).getAsString().codePointCount(0, rows.get(0).getAsString().length()); + int nCols = rows.get(0) + .getAsString() + .codePointCount(0, rows.get(0).getAsString().length()); if (nCols == 0) return; int cellW = imgW / nCols; int cellH = imgH / nRows; @@ -131,12 +135,12 @@ public final class FontLoader { int cp = row.codePointAt(ci); ci += Character.charCount(cp); int thisCol = col++; - if (cp == 0 || thisCol >= nCols) continue; // 0x0000 = empty slot - if (glyphs.containsKey(cp)) continue; // earlier provider wins + if (cp == 0 || thisCol >= nCols) continue; // 0x0000 = empty slot + if (glyphs.containsKey(cp)) continue; // earlier provider wins int srcX = thisCol * cellW; int srcY = r * cellH; int glyphPx = rightmostOpaqueColumn(tex, srcX, srcY, cellW, cellH) + 1; - if (glyphPx <= 0) continue; // blank glyph (handled by space provider) + if (glyphPx <= 0) continue; // blank glyph (handled by space provider) int advance = (int) (glyphPx * scale + 0.5) + 1; glyphs.put(cp, new Glyph(tex, srcX, srcY, cellW, cellH, glyphPx, height, ascent, advance)); maxAscent = Math.max(maxAscent, ascent); @@ -151,7 +155,10 @@ public final class FontLoader { for (int x = 0; x < cellW; x++) { for (int y = 0; y < cellH; y++) { int argb = tex[srcY + y][srcX + x]; - if (((argb >>> 24) & 0xFF) != 0) { last = x; break; } + if (((argb >>> 24) & 0xFF) != 0) { + last = x; + break; + } } } return last; diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/font/Glyph.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/font/Glyph.java index c8a308a..edacf62 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/font/Glyph.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/font/Glyph.java @@ -9,8 +9,8 @@ package eu.mhsl.minecraft.pixelpics.assets.font; *

Glyphs from different providers may declare different cell sizes and ascents (e.g. ascii 8px/ascent * 7 vs accented 12px/ascent 10); rendering aligns them on a common baseline so umlauts and Latin mix. */ -public record Glyph(int[][] tex, int srcX, int srcY, int cellW, int cellH, - int glyphPx, int height, int ascent, int advance) { +public record Glyph( + int[][] tex, int srcX, int srcY, int cellW, int cellH, int glyphPx, int height, int ascent, int advance) { /** Vertical pixels below the baseline (font px). */ public int descent() { diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/model/Direction.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/model/Direction.java index 9c45f7d..069f028 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/model/Direction.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/model/Direction.java @@ -11,7 +11,6 @@ public enum Direction { SOUTH(0, 0, 1), WEST(-1, 0, 0), EAST(1, 0, 0); - public final int nx, ny, nz; Direction(int nx, int ny, int nz) { @@ -42,8 +41,8 @@ public enum Direction { case EAST -> SOUTH; case SOUTH -> WEST; case WEST -> NORTH; - default -> d; // up/down unchanged - }; + // up/down unchanged + default -> d;}; } return d; } @@ -58,8 +57,8 @@ public enum Direction { case NORTH -> DOWN; case DOWN -> SOUTH; case SOUTH -> UP; - default -> d; // east/west unchanged - }; + // east/west unchanged + default -> d;}; } return d; } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/model/Element.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/model/Element.java index 4326566..77c619c 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/model/Element.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/model/Element.java @@ -10,17 +10,23 @@ package eu.mhsl.minecraft.pixelpics.assets.model; public final class Element { public final double[] from; // length 3, 0..1 - public final double[] to; // length 3, 0..1 - public final Face[] faces; // length 6, indexed by Direction.ordinal() + public final double[] to; // length 3, 0..1 + public final Face[] faces; // length 6, indexed by Direction.ordinal() // Element rotation (0..1 origin), null/zero when axis-aligned. public final double[] rotOrigin; // length 3, 0..1, may be null - public final int rotAxis; // 0=x,1=y,2=z, -1 when none + public final int rotAxis; // 0=x,1=y,2=z, -1 when none public final double rotAngleRad; // radians public final boolean rescale; - public Element(double[] from, double[] to, Face[] faces, - double[] rotOrigin, int rotAxis, double rotAngleRad, boolean rescale) { + public Element( + double[] from, + double[] to, + Face[] faces, + double[] rotOrigin, + int rotAxis, + double rotAngleRad, + boolean rescale) { this.from = from; this.to = to; this.faces = faces; diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/model/Face.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/model/Face.java index cb72cd1..be50fe2 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/model/Face.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/model/Face.java @@ -29,10 +29,21 @@ public final class Face { // Apply face rotation by rotating the (s,t) lookup. double rs = s, rt = t; switch (((rotation % 360) + 360) % 360) { - case 90 -> { rs = t; rt = 1.0 - s; } - case 180 -> { rs = 1.0 - s; rt = 1.0 - t; } - case 270 -> { rs = 1.0 - t; rt = s; } - default -> { /* 0 */ } + case 90 -> { + rs = t; + rt = 1.0 - s; + } + case 180 -> { + rs = 1.0 - s; + rt = 1.0 - t; + } + case 270 -> { + rs = 1.0 - t; + rt = s; + } + default -> { + /* 0 */ + } } double u = u1 + (u2 - u1) * rs; diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/model/ResolvedModel.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/model/ResolvedModel.java index 6eec1de..e75cefe 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/model/ResolvedModel.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/model/ResolvedModel.java @@ -15,12 +15,17 @@ public final class ResolvedModel { public final List elements; public final int averageColor; // ARGB public final double transparency; // 0..1 - public final double reflection; // 0..1 + public final double reflection; // 0..1 public final boolean occluding; public final boolean hasGeometry; - public ResolvedModel(List elements, int averageColor, - double transparency, double reflection, boolean occluding, boolean hasGeometry) { + public ResolvedModel( + List elements, + int averageColor, + double transparency, + double reflection, + boolean occluding, + boolean hasGeometry) { this.elements = elements; this.averageColor = averageColor; this.transparency = transparency; diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/commands/PixelPicsCommand.java b/src/main/java/eu/mhsl/minecraft/pixelpics/commands/PixelPicsCommand.java index 2adb3b1..5eb3051 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/commands/PixelPicsCommand.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/commands/PixelPicsCommand.java @@ -2,6 +2,8 @@ package eu.mhsl.minecraft.pixelpics.commands; import eu.mhsl.minecraft.pixelpics.survival.PhotoService; import eu.mhsl.minecraft.pixelpics.utils.MapManager; +import java.util.List; +import java.util.Set; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.command.Command; @@ -10,31 +12,31 @@ import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -import java.util.List; -import java.util.Set; - public class PixelPicsCommand implements CommandExecutor { private static final int DEFAULT_CLEANUP_DAYS = 30; @Override - public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, - @NotNull String @NotNull [] args) { + public boolean onCommand( + @NotNull CommandSender sender, + @NotNull Command command, + @NotNull String label, + @NotNull String @NotNull [] args) { if (args.length >= 1 && args[0].equalsIgnoreCase("cleanup")) { return cleanup(sender, args); } if (!(sender instanceof Player player)) { - sender.sendMessage(Component.text("Dieser Command kann nur von einem Spieler ausgeführt werden!", - NamedTextColor.RED)); + sender.sendMessage( + Component.text("Dieser Command kann nur von einem Spieler ausgeführt werden!", NamedTextColor.RED)); return true; } if (args.length > 0) return false; // Debug shortcut: render a photo from the player's view without needing a camera or film. if (!player.hasPermission("pixelpic.admin")) { - player.sendActionBar(Component.text("Dafür fehlt dir die Berechtigung — nutze eine Kamera.", - NamedTextColor.RED)); + player.sendActionBar( + Component.text("Dafür fehlt dir die Berechtigung — nutze eine Kamera.", NamedTextColor.RED)); return true; } @@ -45,7 +47,8 @@ public class PixelPicsCommand implements CommandExecutor { /** {@code /pixelPic cleanup [days]} dry-run; {@code /pixelPic cleanup confirm [days]} deletes orphans. */ private boolean cleanup(CommandSender sender, String[] args) { if (!sender.hasPermission("pixelpic.admin") && !sender.isOp()) { - sender.sendMessage(Component.text("Dafür fehlt dir die Berechtigung (pixelpic.admin).", NamedTextColor.RED)); + sender.sendMessage( + Component.text("Dafür fehlt dir die Berechtigung (pixelpic.admin).", NamedTextColor.RED)); return true; } @@ -64,21 +67,25 @@ public class PixelPicsCommand implements CommandExecutor { Set inUse = MapManager.collectInUseMapIds(); long cutoff = System.currentTimeMillis() - days * 86_400_000L; List candidates = MapManager.listStored().stream() - .filter(s -> !inUse.contains(s.id()) && s.lastModified() < cutoff) - .toList(); + .filter(s -> !inUse.contains(s.id()) && s.lastModified() < cutoff) + .toList(); if (candidates.isEmpty()) { - sender.sendMessage(Component.text("Keine aufräumbaren Aufnahmen gefunden (älter als " + days - + " Tage und nicht in Benutzung).", NamedTextColor.YELLOW)); + sender.sendMessage(Component.text( + "Keine aufräumbaren Aufnahmen gefunden (älter als " + days + " Tage und nicht in Benutzung).", + NamedTextColor.YELLOW)); return true; } if (!confirm) { - sender.sendMessage(Component.text(candidates.size() + " Aufnahme(n) könnten gelöscht werden " - + "(älter als " + days + " Tage, nicht in geladenen Itemframes/Online-Inventaren).", - NamedTextColor.YELLOW)); - sender.sendMessage(Component.text("Achtung: Maps in lange ungeladenen Bereichen werden hierbei nicht " - + "erkannt. Zum Löschen: /pixelPic cleanup confirm " + days, NamedTextColor.GRAY)); + sender.sendMessage(Component.text( + candidates.size() + " Aufnahme(n) könnten gelöscht werden " + "(älter als " + days + + " Tage, nicht in geladenen Itemframes/Online-Inventaren).", + NamedTextColor.YELLOW)); + sender.sendMessage(Component.text( + "Achtung: Maps in lange ungeladenen Bereichen werden hierbei nicht " + + "erkannt. Zum Löschen: /pixelPic cleanup confirm " + days, + NamedTextColor.GRAY)); return true; } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/listeners/OnMapInitialize.java b/src/main/java/eu/mhsl/minecraft/pixelpics/listeners/OnMapInitialize.java index e4ca940..dfcbf3d 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/listeners/OnMapInitialize.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/listeners/OnMapInitialize.java @@ -3,12 +3,11 @@ package eu.mhsl.minecraft.pixelpics.listeners; import eu.mhsl.minecraft.pixelpics.utils.ImageMapRenderer; import eu.mhsl.minecraft.pixelpics.utils.MapImageDither; import eu.mhsl.minecraft.pixelpics.utils.MapManager; +import java.awt.image.BufferedImage; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.server.MapInitializeEvent; -import java.awt.image.BufferedImage; - public class OnMapInitialize implements Listener { @EventHandler diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/RenderManager.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/RenderManager.java index 90ba7af..77590a4 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/RenderManager.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/RenderManager.java @@ -1,8 +1,5 @@ package eu.mhsl.minecraft.pixelpics.render; -import org.bukkit.Bukkit; -import org.bukkit.plugin.Plugin; - import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -19,6 +16,8 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Function; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; /** * Server-friendly scheduling for photo renders. Bounds CPU use and protects the server tick by: @@ -37,7 +36,11 @@ import java.util.function.Function; */ public final class RenderManager { - public enum Outcome { ACCEPTED, USER_BUSY, QUEUE_FULL } + public enum Outcome { + ACCEPTED, + USER_BUSY, + QUEUE_FULL + } private final Plugin plugin; private final ForkJoinPool tracePool; @@ -54,9 +57,12 @@ public final class RenderManager { this.timeoutMillis = Math.max(1L, timeoutSeconds) * 1000L; this.tracePool = new ForkJoinPool(threads, lowPriorityForkJoinFactory(), null, false); this.dispatcher = new ThreadPoolExecutor( - maxConcurrent, maxConcurrent, 30, TimeUnit.SECONDS, - new LinkedBlockingQueue<>(), // capacity is enforced by inFlight, not this queue - lowPriorityThreadFactory()); + maxConcurrent, + maxConcurrent, + 30, + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(), // capacity is enforced by inFlight, not this queue + lowPriorityThreadFactory()); this.dispatcher.allowCoreThreadTimeOut(true); this.watchdog = Executors.newSingleThreadScheduledExecutor(runnable -> { Thread thread = new Thread(runnable, "PixelPics-render-watchdog"); @@ -98,7 +104,8 @@ public final class RenderManager { public void dispatch(UUID user, Function work, Consumer onSuccess, Runnable onFailure) { dispatcher.execute(() -> { AtomicBoolean cancelled = new AtomicBoolean(false); - ScheduledFuture deadline = watchdog.schedule(() -> cancelled.set(true), timeoutMillis, TimeUnit.MILLISECONDS); + ScheduledFuture deadline = + watchdog.schedule(() -> cancelled.set(true), timeoutMillis, TimeUnit.MILLISECONDS); T result = null; boolean ok = false; try { @@ -109,8 +116,9 @@ public final class RenderManager { } finally { deadline.cancel(false); if (cancelled.get()) { - plugin.getLogger().warning("Render for " + user + " aborted after " - + (timeoutMillis / 1000) + "s (timeout)."); + plugin.getLogger() + .warning( + "Render for " + user + " aborted after " + (timeoutMillis / 1000) + "s (timeout)."); } release(user); } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/Affine.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/Affine.java index 1787c5f..4a91e38 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/Affine.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/Affine.java @@ -17,30 +17,30 @@ public final class Affine { } public static Affine identity() { - return new Affine(new double[]{1, 0, 0, 0, 1, 0, 0, 0, 1}, new double[]{0, 0, 0}); + return new Affine(new double[] {1, 0, 0, 0, 1, 0, 0, 0, 1}, new double[] {0, 0, 0}); } public static Affine translation(double x, double y, double z) { - return new Affine(new double[]{1, 0, 0, 0, 1, 0, 0, 0, 1}, new double[]{x, y, z}); + return new Affine(new double[] {1, 0, 0, 0, 1, 0, 0, 0, 1}, new double[] {x, y, z}); } public static Affine scale(double s) { - return new Affine(new double[]{s, 0, 0, 0, s, 0, 0, 0, s}, new double[]{0, 0, 0}); + return new Affine(new double[] {s, 0, 0, 0, s, 0, 0, 0, s}, new double[] {0, 0, 0}); } public static Affine rotX(double a) { double c = Math.cos(a), s = Math.sin(a); - return new Affine(new double[]{1, 0, 0, 0, c, -s, 0, s, c}, new double[]{0, 0, 0}); + return new Affine(new double[] {1, 0, 0, 0, c, -s, 0, s, c}, new double[] {0, 0, 0}); } public static Affine rotY(double a) { double c = Math.cos(a), s = Math.sin(a); - return new Affine(new double[]{c, 0, s, 0, 1, 0, -s, 0, c}, new double[]{0, 0, 0}); + return new Affine(new double[] {c, 0, s, 0, 1, 0, -s, 0, c}, new double[] {0, 0, 0}); } public static Affine rotZ(double a) { double c = Math.cos(a), s = Math.sin(a); - return new Affine(new double[]{c, -s, 0, s, c, 0, 0, 0, 1}, new double[]{0, 0, 0}); + return new Affine(new double[] {c, -s, 0, s, c, 0, 0, 0, 1}, new double[] {0, 0, 0}); } /** this ∘ o (apply o first, then this). */ @@ -53,7 +53,7 @@ public final class Affine { } } double[] ot = o.t; - double[] nt = new double[]{ + double[] nt = new double[] { a[0] * ot[0] + a[1] * ot[1] + a[2] * ot[2] + this.t[0], a[3] * ot[0] + a[4] * ot[1] + a[5] * ot[2] + this.t[1], a[6] * ot[0] + a[7] * ot[1] + a[8] * ot[2] + this.t[2] @@ -62,7 +62,7 @@ public final class Affine { } public double[] apply(double x, double y, double z) { - return new double[]{ + return new double[] { r[0] * x + r[1] * y + r[2] * z + t[0], r[3] * x + r[4] * y + r[5] * z + t[1], r[6] * x + r[7] * y + r[8] * z + t[2] @@ -71,10 +71,8 @@ public final class Affine { /** Linear part only (for directions). */ public double[] applyLinear(double x, double y, double z) { - return new double[]{ - r[0] * x + r[1] * y + r[2] * z, - r[3] * x + r[4] * y + r[5] * z, - r[6] * x + r[7] * y + r[8] * z + return new double[] { + r[0] * x + r[1] * y + r[2] * z, r[3] * x + r[4] * y + r[5] * z, r[6] * x + r[7] * y + r[8] * z }; } @@ -83,12 +81,12 @@ public final class Affine { double a = r[0], b = r[1], c = r[2], d = r[3], e = r[4], f = r[5], g = r[6], h = r[7], i = r[8]; double det = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g); double inv = Math.abs(det) < 1e-12 ? 0 : 1.0 / det; - double[] ir = new double[]{ + double[] ir = new double[] { (e * i - f * h) * inv, (c * h - b * i) * inv, (b * f - c * e) * inv, (f * g - d * i) * inv, (a * i - c * g) * inv, (c * d - a * f) * inv, (d * h - e * g) * inv, (b * g - a * h) * inv, (a * e - b * d) * inv }; - double[] it = new double[]{ + double[] it = new double[] { -(ir[0] * t[0] + ir[1] * t[1] + ir[2] * t[2]), -(ir[3] * t[0] + ir[4] * t[1] + ir[5] * t[2]), -(ir[6] * t[0] + ir[7] * t[1] + ir[8] * t[2]) diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/BlockEntityModels.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/BlockEntityModels.java index a5ab137..7dc724f 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/BlockEntityModels.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/BlockEntityModels.java @@ -1,7 +1,6 @@ package eu.mhsl.minecraft.pixelpics.render.entity; import eu.mhsl.minecraft.pixelpics.assets.ResourceLocation; - import java.util.ArrayList; import java.util.List; @@ -17,7 +16,9 @@ public final class BlockEntityModels { /** The CEM ({@code .jem}) model name for a block-entity. */ public static String cemModel(BlockEntityState s) { return switch (s.kind()) { - case CHEST, TRAPPED_CHEST, ENDER_CHEST -> switch (s.chestKind() == null ? BlockEntityState.ChestKind.SINGLE : s.chestKind()) { + case CHEST, TRAPPED_CHEST, ENDER_CHEST -> switch (s.chestKind() == null + ? BlockEntityState.ChestKind.SINGLE + : s.chestKind()) { case LEFT -> "chest_left"; case RIGHT -> "chest_right"; case SINGLE -> "chest"; @@ -56,7 +57,7 @@ public final class BlockEntityModels { case SIGN, WALL_SIGN -> { String wood = s.wood() == null ? "oak" : s.wood(); paths.add("entity/signs/" + wood); - paths.add("entity/sign"); // legacy single-texture fallback + paths.add("entity/sign"); // legacy single-texture fallback } case HANGING_SIGN -> { String wood = s.wood() == null ? "oak" : s.wood(); @@ -69,7 +70,7 @@ public final class BlockEntityModels { } case SHULKER_BOX -> { if (s.colorName() != null) paths.add("entity/shulker/shulker_" + s.colorName()); - paths.add("entity/shulker/shulker"); // uncoloured (purpur) default + paths.add("entity/shulker/shulker"); // uncoloured (purpur) default } case CONDUIT -> paths.add("entity/conduit/base"); case DECORATED_POT -> paths.add("entity/decorated_pot/decorated_pot_base"); @@ -92,7 +93,10 @@ public final class BlockEntityModels { } private static void headTextures(List paths, String headType) { - if (headType == null) { paths.add("entity/skeleton/skeleton"); return; } + if (headType == null) { + paths.add("entity/skeleton/skeleton"); + return; + } switch (headType) { case "wither_skeleton" -> paths.add("entity/skeleton/wither_skeleton"); case "zombie" -> paths.add("entity/zombie/zombie"); diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/BlockEntityState.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/BlockEntityState.java index e2ac907..615ef87 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/BlockEntityState.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/BlockEntityState.java @@ -12,36 +12,59 @@ import java.util.List; * facing/rotation, vanilla convention). Type-specific fields are null/0/empty when unused. */ public record BlockEntityState( - Kind kind, - int bx, int by, int bz, - float facingDeg, - ChestKind chestKind, // double-chest half (CHEST/TRAPPED_CHEST/ENDER_CHEST) - int baseColorArgb, // banner base tint (white texture); 0 = none - String colorName, // bed/shulker/banner colour variant ("red", "white", …); null = default - String wood, // sign/hanging-sign wood ("oak", "spruce", …); null = default - BedPart bedPart, // bed half - String headType, // "skeleton","wither_skeleton","zombie","creeper","piglin","dragon","player" - String skinUrl, // player-head owner skin URL; null otherwise - List patterns, // banner overlay patterns (may be empty) - List sherds, // decorated-pot sherds: front/back/left/right item keys (may be empty) - BellAttach bellAttach, // bell attachment; null when not a bell - SignText frontText, // sign front-side text; null when not a sign or blank - SignText backText // sign back-side text; null when not a sign or blank -) { + Kind kind, + int bx, + int by, + int bz, + float facingDeg, + ChestKind chestKind, // double-chest half (CHEST/TRAPPED_CHEST/ENDER_CHEST) + int baseColorArgb, // banner base tint (white texture); 0 = none + String colorName, // bed/shulker/banner colour variant ("red", "white", …); null = default + String wood, // sign/hanging-sign wood ("oak", "spruce", …); null = default + BedPart bedPart, // bed half + String headType, // "skeleton","wither_skeleton","zombie","creeper","piglin","dragon","player" + String skinUrl, // player-head owner skin URL; null otherwise + List patterns, // banner overlay patterns (may be empty) + List sherds, // decorated-pot sherds: front/back/left/right item keys (may be empty) + BellAttach bellAttach, // bell attachment; null when not a bell + SignText frontText, // sign front-side text; null when not a sign or blank + SignText backText // sign back-side text; null when not a sign or blank + ) { public enum Kind { - CHEST, TRAPPED_CHEST, ENDER_CHEST, - SIGN, WALL_SIGN, HANGING_SIGN, - BANNER, WALL_BANNER, - BED, SHULKER_BOX, - HEAD, WALL_HEAD, - CONDUIT, DECORATED_POT, BELL + CHEST, + TRAPPED_CHEST, + ENDER_CHEST, + SIGN, + WALL_SIGN, + HANGING_SIGN, + BANNER, + WALL_BANNER, + BED, + SHULKER_BOX, + HEAD, + WALL_HEAD, + CONDUIT, + DECORATED_POT, + BELL } - public enum ChestKind { SINGLE, LEFT, RIGHT } + public enum ChestKind { + SINGLE, + LEFT, + RIGHT + } - public enum BedPart { HEAD, FOOT } + public enum BedPart { + HEAD, + FOOT + } - public enum BellAttach { FLOOR, CEILING, SINGLE_WALL, DOUBLE_WALL } + public enum BellAttach { + FLOOR, + CEILING, + SINGLE_WALL, + DOUBLE_WALL + } /** One banner overlay layer: a pattern key (e.g. "stripe_top") and the ARGB colour it is dyed with. */ public record BannerPattern(String patternKey, int colorArgb) {} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/BoxUv.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/BoxUv.java index d2caa0f..b1386d0 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/BoxUv.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/BoxUv.java @@ -46,8 +46,13 @@ public final class BoxUv { double[] down = {u + dz + 2 * dx, v, -dx, dz}; if (cube.mirror) { - for (double[] f : new double[][]{east, west, up, down, south, north}) { f[0] += f[2]; f[2] = -f[2]; } - double[] tmp = east; east = west; west = tmp; // mirror swaps the left/right faces + for (double[] f : new double[][] {east, west, up, down, south, north}) { + f[0] += f[2]; + f[2] = -f[2]; + } + double[] tmp = east; + east = west; + west = tmp; // mirror swaps the left/right faces } Face[] faces = new Face[6]; diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/DecorationBaker.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/DecorationBaker.java index 8fd64d9..867d54e 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/DecorationBaker.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/DecorationBaker.java @@ -4,7 +4,6 @@ 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 java.util.ArrayList; import java.util.List; @@ -29,7 +28,8 @@ public final class DecorationBaker implements EntityBaker { } private RenderedEntity bakePainting(DecorationState s) { - int[][] art = textures.get(ResourceLocation.parse("painting/" + s.paintingArt())).orElse(null); + int[][] art = textures.get(ResourceLocation.parse("painting/" + s.paintingArt())) + .orElse(null); if (art == null) return null; int[][] back = textures.get(ResourceLocation.parse("painting/back")).orElse(art); @@ -53,10 +53,12 @@ public final class DecorationBaker implements EntityBaker { 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); + 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())); + Affine toWorld = Affine.translation(faceCenterX(s), faceCenterY(s), faceCenterZ(s)) + .mul(facingRotation(s.facing())); int si = Direction.SOUTH.ordinal(); List cubes = new ArrayList<>(3); @@ -83,7 +85,7 @@ public final class DecorationBaker implements EntityBaker { /** 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}; + 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/. */ diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/DecorationState.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/DecorationState.java index 4bd1186..e8abc95 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/DecorationState.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/DecorationState.java @@ -7,19 +7,31 @@ package eu.mhsl.minecraft.pixelpics.render.entity; * already encodes vanilla's awkward multi-block painting offset), avoiding placement math. */ public record DecorationState( - Kind kind, - double minX, double minY, double minZ, - double maxX, double maxY, double maxZ, - Facing facing, // direction the front faces (away from the wall) - String paintingArt, // painting art asset key (texture painting/); 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 -) { - public enum Kind { PAINTING, ITEM_FRAME } + Kind kind, + double minX, + double minY, + double minZ, + double maxX, + double maxY, + double maxZ, + Facing facing, // direction the front faces (away from the wall) + String paintingArt, // painting art asset key (texture painting/); 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 + ) { + public enum Kind { + PAINTING, + ITEM_FRAME + } public enum Facing { - NORTH, SOUTH, EAST, WEST, UP, DOWN; + NORTH, + SOUTH, + EAST, + WEST, + UP, + DOWN; /** The axis this face's normal lies on: 0=x, 1=y, 2=z. */ public int axis() { diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityCube.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityCube.java index 12ed643..c17bb84 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityCube.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityCube.java @@ -8,11 +8,11 @@ import eu.mhsl.minecraft.pixelpics.assets.model.Face; * broad-phase culling. */ public final class EntityCube { - public final double[] from; // local min (px, inflated) - public final double[] to; // local max - public final Face[] faces; // by Direction.ordinal() + public final double[] from; // local min (px, inflated) + public final double[] to; // local max + public final Face[] faces; // by Direction.ordinal() public final Affine toWorld; - public final Affine toLocal; // inverse + public final Affine toLocal; // inverse public final double[] aabbMin = {Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE}; public final double[] aabbMax = {-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE}; diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityIntersector.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityIntersector.java index 55813a5..c93f436 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityIntersector.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityIntersector.java @@ -18,8 +18,7 @@ public final class EntityIntersector { private EntityIntersector() {} - public static FaceHit intersect(EntityCube cube, double ox, double oy, double oz, - double dx, double dy, double dz) { + public static FaceHit intersect(EntityCube cube, double ox, double oy, double oz, double dx, double dy, double dz) { double[] o = cube.toLocal.apply(ox, oy, oz); double[] d = cube.toLocal.applyLinear(dx, dy, dz); @@ -36,23 +35,33 @@ public final class EntityIntersector { double t2 = (cube.to[a] - o[a]) * inv; boolean n = true; if (t1 > t2) { - double tmp = t1; t1 = t2; t2 = tmp; + double tmp = t1; + t1 = t2; + t2 = tmp; n = false; } - if (t1 > tmin) { tmin = t1; axis = a; neg = n; } + if (t1 > tmin) { + tmin = t1; + axis = a; + neg = n; + } if (t2 < tmax) tmax = t2; if (tmin > tmax) return null; } if (axis < 0) return null; double t = tmin; - if (t < EPS) { t = tmax; if (t < EPS) return null; } + if (t < EPS) { + t = tmax; + if (t < EPS) return null; + } double px = o[0] + d[0] * t, py = o[1] + d[1] * t, pz = o[2] + d[2] * t; - Direction dir = switch (axis) { - case 0 -> neg ? Direction.WEST : Direction.EAST; - case 1 -> neg ? Direction.DOWN : Direction.UP; - default -> neg ? Direction.NORTH : Direction.SOUTH; - }; + Direction dir = + switch (axis) { + case 0 -> neg ? Direction.WEST : Direction.EAST; + case 1 -> neg ? Direction.DOWN : Direction.UP; + default -> neg ? Direction.NORTH : Direction.SOUTH; + }; Face face = cube.faces[dir.ordinal()]; if (face == null) return null; @@ -63,12 +72,30 @@ public final class EntityIntersector { // run their horizontal axis opposite to back/left (they're viewed from the other side). double s, tt; switch (dir) { - case UP -> { s = fx; tt = fz; } - case DOWN -> { s = fx; tt = 1 - fz; } - case NORTH -> { s = 1 - fx; tt = 1 - fy; } - case SOUTH -> { s = fx; tt = 1 - fy; } - case EAST -> { s = 1 - fz; tt = 1 - fy; } - default -> { s = fz; tt = 1 - fy; } // WEST + case UP -> { + s = fx; + tt = fz; + } + case DOWN -> { + s = fx; + tt = 1 - fz; + } + case NORTH -> { + s = 1 - fx; + tt = 1 - fy; + } + case SOUTH -> { + s = fx; + tt = 1 - fy; + } + case EAST -> { + s = 1 - fz; + tt = 1 - fy; + } + default -> { + s = fz; + tt = 1 - fy; + } // WEST } int color = face.sample(s, tt); if (ColorUtil.alpha(color) <= ALPHA_THRESHOLD) return null; diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityModels.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityModels.java index 934963f..555fb25 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityModels.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityModels.java @@ -1,7 +1,6 @@ package eu.mhsl.minecraft.pixelpics.render.entity; import eu.mhsl.minecraft.pixelpics.assets.ResourceLocation; - import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -18,33 +17,32 @@ public final class EntityModels { // Type key -> CEM (.jem) model name. Default is the type key itself; these are the exceptions // (mob reuses another mob's model, or the CEM set only ships a version-suffixed/renamed name). private static final Map CEM_OVERRIDE = Map.ofEntries( - Map.entry("husk", "zombie"), - Map.entry("giant", "zombie"), - Map.entry("mooshroom", "cow"), - Map.entry("ocelot", "cat"), - Map.entry("cave_spider", "spider"), - Map.entry("elder_guardian", "guardian"), - Map.entry("glow_squid", "squid"), - Map.entry("mule", "donkey"), - Map.entry("skeleton_horse", "horse"), - Map.entry("zombie_horse", "horse"), - Map.entry("trader_llama", "llama"), - Map.entry("stray", "skeleton"), - Map.entry("wither_skeleton", "skeleton"), - Map.entry("zoglin", "hoglin"), - Map.entry("piglin_brute", "piglin"), - Map.entry("zombified_piglin", "piglin"), - Map.entry("evoker", "illager"), - Map.entry("vindicator", "illager"), - Map.entry("illusioner", "illager"), - Map.entry("wandering_trader", "villager"), - Map.entry("ender_dragon", "dragon"), - Map.entry("mannequin", "player"), - Map.entry("camel_husk", "camel"), - Map.entry("rabbit", "rabbit_21.11"), - Map.entry("pufferfish", "puffer_fish_big"), - Map.entry("tropical_fish", "tropical_fish_a") - ); + Map.entry("husk", "zombie"), + Map.entry("giant", "zombie"), + Map.entry("mooshroom", "cow"), + Map.entry("ocelot", "cat"), + Map.entry("cave_spider", "spider"), + Map.entry("elder_guardian", "guardian"), + Map.entry("glow_squid", "squid"), + Map.entry("mule", "donkey"), + Map.entry("skeleton_horse", "horse"), + Map.entry("zombie_horse", "horse"), + Map.entry("trader_llama", "llama"), + Map.entry("stray", "skeleton"), + Map.entry("wither_skeleton", "skeleton"), + Map.entry("zoglin", "hoglin"), + Map.entry("piglin_brute", "piglin"), + Map.entry("zombified_piglin", "piglin"), + Map.entry("evoker", "illager"), + Map.entry("vindicator", "illager"), + Map.entry("illusioner", "illager"), + Map.entry("wandering_trader", "villager"), + Map.entry("ender_dragon", "dragon"), + Map.entry("mannequin", "player"), + Map.entry("camel_husk", "camel"), + Map.entry("rabbit", "rabbit_21.11"), + Map.entry("pufferfish", "puffer_fish_big"), + Map.entry("tropical_fish", "tropical_fish_a")); /** The CEM model name for an entity type (boats/rafts share the boat hull). */ public static String cemModel(String typeKey) { @@ -53,64 +51,63 @@ public final class EntityModels { } // Type key -> texture path override (where the first derived candidate is wrong). - private static final Map TEX_OVERRIDE = Map.ofEntries( - Map.entry("cow", "entity/cow/cow_temperate"), - Map.entry("mooshroom", "entity/cow/mooshroom_red"), - Map.entry("zombie", "entity/zombie/zombie"), - Map.entry("husk", "entity/zombie/husk"), - Map.entry("drowned", "entity/zombie/drowned"), - Map.entry("zombified_piglin", "entity/piglin/zombified_piglin"), - Map.entry("skeleton", "entity/skeleton/skeleton"), - Map.entry("stray", "entity/skeleton/stray"), - Map.entry("wither_skeleton", "entity/skeleton/wither_skeleton"), - Map.entry("creeper", "entity/creeper/creeper"), - Map.entry("spider", "entity/spider/spider"), - Map.entry("enderman", "entity/enderman/enderman"), - Map.entry("player", "entity/player/wide/steve"), - // Textures whose folder/name doesn't follow the "entity//" pattern. - Map.entry("iron_golem", "entity/iron_golem/iron_golem"), - Map.entry("polar_bear", "entity/bear/polarbear"), - Map.entry("ender_dragon", "entity/enderdragon/dragon"), - Map.entry("magma_cube", "entity/slime/magmacube"), - Map.entry("tropical_fish", "entity/fish/tropical_a"), - Map.entry("bogged", "entity/skeleton/bogged"), - Map.entry("donkey", "entity/horse/donkey"), - Map.entry("mule", "entity/horse/mule"), - Map.entry("skeleton_horse", "entity/horse/horse_skeleton"), - Map.entry("zombie_horse", "entity/horse/horse_zombie"), - Map.entry("trader_llama", "entity/llama/llama_creamy"), - Map.entry("cave_spider", "entity/spider/cave_spider"), - Map.entry("guardian", "entity/guardian/guardian"), - Map.entry("elder_guardian", "entity/guardian/guardian_elder"), - Map.entry("piglin_brute", "entity/piglin/piglin_brute"), - Map.entry("zoglin", "entity/hoglin/zoglin"), - Map.entry("illusioner", "entity/illager/illusioner"), - Map.entry("giant", "entity/zombie/zombie"), - // Illagers share one texture folder; none follow the entity// pattern. - Map.entry("pillager", "entity/illager/pillager"), - Map.entry("vindicator", "entity/illager/vindicator"), - Map.entry("evoker", "entity/illager/evoker"), - Map.entry("ravager", "entity/illager/ravager"), - Map.entry("vex", "entity/illager/vex"), - // Fish share entity/fish/; squids share entity/squid/. - Map.entry("cod", "entity/fish/cod"), - Map.entry("salmon", "entity/fish/salmon"), - Map.entry("pufferfish", "entity/fish/pufferfish"), - Map.entry("glow_squid", "entity/squid/glow_squid"), - // Variant-only textures with no plain base file — pick a sensible default variant. - Map.entry("cat", "entity/cat/cat_tabby"), - Map.entry("ocelot", "entity/cat/ocelot"), // ocelot texture lives in the cat folder now - Map.entry("axolotl", "entity/axolotl/axolotl_wild"), - Map.entry("parrot", "entity/parrot/parrot_red_blue"), - Map.entry("turtle", "entity/turtle/turtle"), - Map.entry("wind_charge", "entity/projectiles/wind_charge"), - Map.entry("camel_husk", "entity/camel/camel_husk"), - Map.entry("armor_stand", "entity/armorstand/armorstand"), // texture folder is "armorstand" - Map.entry("happy_ghast", "entity/ghast/happy_ghast"), - Map.entry("parched", "entity/skeleton/parched"), // husk-style skeleton, texture in skeleton/ - Map.entry("zombie_nautilus_coral", "entity/nautilus/zombie_nautilus_coral"), - Map.entry("mannequin", "entity/player/wide/steve") - ); + private static final Map TEX_OVERRIDE = Map.ofEntries( + Map.entry("cow", "entity/cow/cow_temperate"), + Map.entry("mooshroom", "entity/cow/mooshroom_red"), + Map.entry("zombie", "entity/zombie/zombie"), + Map.entry("husk", "entity/zombie/husk"), + Map.entry("drowned", "entity/zombie/drowned"), + Map.entry("zombified_piglin", "entity/piglin/zombified_piglin"), + Map.entry("skeleton", "entity/skeleton/skeleton"), + Map.entry("stray", "entity/skeleton/stray"), + Map.entry("wither_skeleton", "entity/skeleton/wither_skeleton"), + Map.entry("creeper", "entity/creeper/creeper"), + Map.entry("spider", "entity/spider/spider"), + Map.entry("enderman", "entity/enderman/enderman"), + Map.entry("player", "entity/player/wide/steve"), + // Textures whose folder/name doesn't follow the "entity//" pattern. + Map.entry("iron_golem", "entity/iron_golem/iron_golem"), + Map.entry("polar_bear", "entity/bear/polarbear"), + Map.entry("ender_dragon", "entity/enderdragon/dragon"), + Map.entry("magma_cube", "entity/slime/magmacube"), + Map.entry("tropical_fish", "entity/fish/tropical_a"), + Map.entry("bogged", "entity/skeleton/bogged"), + Map.entry("donkey", "entity/horse/donkey"), + Map.entry("mule", "entity/horse/mule"), + Map.entry("skeleton_horse", "entity/horse/horse_skeleton"), + Map.entry("zombie_horse", "entity/horse/horse_zombie"), + Map.entry("trader_llama", "entity/llama/llama_creamy"), + Map.entry("cave_spider", "entity/spider/cave_spider"), + Map.entry("guardian", "entity/guardian/guardian"), + Map.entry("elder_guardian", "entity/guardian/guardian_elder"), + Map.entry("piglin_brute", "entity/piglin/piglin_brute"), + Map.entry("zoglin", "entity/hoglin/zoglin"), + Map.entry("illusioner", "entity/illager/illusioner"), + Map.entry("giant", "entity/zombie/zombie"), + // Illagers share one texture folder; none follow the entity// pattern. + Map.entry("pillager", "entity/illager/pillager"), + Map.entry("vindicator", "entity/illager/vindicator"), + Map.entry("evoker", "entity/illager/evoker"), + Map.entry("ravager", "entity/illager/ravager"), + Map.entry("vex", "entity/illager/vex"), + // Fish share entity/fish/; squids share entity/squid/. + Map.entry("cod", "entity/fish/cod"), + Map.entry("salmon", "entity/fish/salmon"), + Map.entry("pufferfish", "entity/fish/pufferfish"), + Map.entry("glow_squid", "entity/squid/glow_squid"), + // Variant-only textures with no plain base file — pick a sensible default variant. + Map.entry("cat", "entity/cat/cat_tabby"), + Map.entry("ocelot", "entity/cat/ocelot"), // ocelot texture lives in the cat folder now + Map.entry("axolotl", "entity/axolotl/axolotl_wild"), + Map.entry("parrot", "entity/parrot/parrot_red_blue"), + Map.entry("turtle", "entity/turtle/turtle"), + Map.entry("wind_charge", "entity/projectiles/wind_charge"), + Map.entry("camel_husk", "entity/camel/camel_husk"), + Map.entry("armor_stand", "entity/armorstand/armorstand"), // texture folder is "armorstand" + Map.entry("happy_ghast", "entity/ghast/happy_ghast"), + Map.entry("parched", "entity/skeleton/parched"), // husk-style skeleton, texture in skeleton/ + Map.entry("zombie_nautilus_coral", "entity/nautilus/zombie_nautilus_coral"), + Map.entry("mannequin", "entity/player/wide/steve")); /** Ordered texture-path candidates; the baker uses the first that loads. */ public static List textureCandidates(String typeKey, String variant) { @@ -139,30 +136,30 @@ public final class EntityModels { */ private static List variantPaths(String typeKey, String v) { return switch (typeKey) { - case "cat" -> List.of("entity/cat/cat_" + v); - case "axolotl" -> List.of("entity/axolotl/axolotl_" + v); - case "wolf" -> List.of("entity/wolf/wolf_" + v, "entity/wolf/wolf"); - case "horse" -> List.of("entity/horse/horse_" + HORSE_COLOR.getOrDefault(v, v)); - case "llama" -> List.of("entity/llama/llama_" + v); - case "cow" -> List.of("entity/cow/cow_" + v); - case "pig" -> List.of("entity/pig/pig_" + v); - case "chicken" -> List.of("entity/chicken/chicken_" + v); - case "frog" -> List.of("entity/frog/frog_" + v); - case "panda" -> List.of(v.equals("normal") ? "entity/panda/panda" : "entity/panda/panda_" + v); - case "fox" -> List.of(v.equals("snow") ? "entity/fox/fox_snow" : "entity/fox/fox"); - case "parrot" -> List.of("entity/parrot/parrot_" + PARROT_COLOR.getOrDefault(v, v)); - case "rabbit" -> List.of("entity/rabbit/rabbit_" + RABBIT_TYPE.getOrDefault(v, v)); - case "mooshroom" -> List.of("entity/cow/mooshroom_" + v); - case "shulker" -> List.of("entity/shulker/shulker_" + v); - // villager/zombie_villager: type/ and profession are transparent OVERLAYS (clothing - // only); the opaque base body is entity// — handled by the generic candidates. - default -> List.of(); + case "cat" -> List.of("entity/cat/cat_" + v); + case "axolotl" -> List.of("entity/axolotl/axolotl_" + v); + case "wolf" -> List.of("entity/wolf/wolf_" + v, "entity/wolf/wolf"); + case "horse" -> List.of("entity/horse/horse_" + HORSE_COLOR.getOrDefault(v, v)); + case "llama" -> List.of("entity/llama/llama_" + v); + case "cow" -> List.of("entity/cow/cow_" + v); + case "pig" -> List.of("entity/pig/pig_" + v); + case "chicken" -> List.of("entity/chicken/chicken_" + v); + case "frog" -> List.of("entity/frog/frog_" + v); + case "panda" -> List.of(v.equals("normal") ? "entity/panda/panda" : "entity/panda/panda_" + v); + case "fox" -> List.of(v.equals("snow") ? "entity/fox/fox_snow" : "entity/fox/fox"); + case "parrot" -> List.of("entity/parrot/parrot_" + PARROT_COLOR.getOrDefault(v, v)); + case "rabbit" -> List.of("entity/rabbit/rabbit_" + RABBIT_TYPE.getOrDefault(v, v)); + case "mooshroom" -> List.of("entity/cow/mooshroom_" + v); + case "shulker" -> List.of("entity/shulker/shulker_" + v); + // villager/zombie_villager: type/ and profession are transparent OVERLAYS (clothing + // only); the opaque base body is entity// — handled by the generic candidates. + default -> List.of(); }; } private static final Map HORSE_COLOR = Map.of("dark_brown", "darkbrown"); - private static final Map PARROT_COLOR = Map.of( - "red", "red_blue", "cyan", "yellow_blue", "gray", "grey"); - private static final Map RABBIT_TYPE = Map.of( - "black_and_white", "white_splotched", "salt_and_pepper", "salt", "the_killer_bunny", "caerbannog"); + private static final Map PARROT_COLOR = + Map.of("red", "red_blue", "cyan", "yellow_blue", "gray", "grey"); + private static final Map RABBIT_TYPE = + Map.of("black_and_white", "white_splotched", "salt_and_pepper", "salt", "the_killer_bunny", "caerbannog"); } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityScene.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityScene.java index d2dc56e..ff73377 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityScene.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityScene.java @@ -3,7 +3,6 @@ package eu.mhsl.minecraft.pixelpics.render.entity; import eu.mhsl.minecraft.pixelpics.render.entity.cem.BlockEntityBaker; import eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker; import eu.mhsl.minecraft.pixelpics.render.raytrace.FaceHit; - import java.util.ArrayList; import java.util.List; @@ -18,9 +17,13 @@ public final class EntityScene { private static final double EPS = 1e-7; private final List entities; - public EntityScene(List states, CemBaker baker, - List blockEntities, BlockEntityBaker beBaker, - List decorations, DecorationBaker decoBaker) { + public EntityScene( + List states, + CemBaker baker, + List blockEntities, + BlockEntityBaker beBaker, + List decorations, + DecorationBaker decoBaker) { this.entities = new ArrayList<>(states.size() + blockEntities.size() + decorations.size()); addAll(states, baker); addAll(blockEntities, beBaker); @@ -58,8 +61,8 @@ public final class EntityScene { return best; } - private static boolean rayAabb(double[] min, double[] max, double ox, double oy, double oz, - double dx, double dy, double dz, double maxT) { + private static boolean rayAabb( + double[] min, double[] max, double ox, double oy, double oz, double dx, double dy, double dz, double maxT) { double tmin = 0, tmax = maxT; double[] o = {ox, oy, oz}, d = {dx, dy, dz}; for (int a = 0; a < 3; a++) { @@ -69,7 +72,11 @@ public final class EntityScene { double inv = 1.0 / d[a]; double t1 = (min[a] - o[a]) * inv; double t2 = (max[a] - o[a]) * inv; - if (t1 > t2) { double tmp = t1; t1 = t2; t2 = tmp; } + if (t1 > t2) { + double tmp = t1; + t1 = t2; + t2 = tmp; + } if (t1 > tmin) tmin = t1; if (t2 < tmax) tmax = t2; if (tmin > tmax) return false; diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityState.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityState.java index fe9251a..546b86d 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityState.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/EntityState.java @@ -5,24 +5,31 @@ package eu.mhsl.minecraft.pixelpics.render.entity; * off-thread. Angles are in degrees (Minecraft convention). */ public record EntityState( - String typeKey, // e.g. "cow", "zombie", "player" - double x, double y, double z, - float bodyYaw, - boolean baby, - double width, double height, - boolean player, String skinUrl, boolean slim, - String variant, // texture-selecting variant key (e.g. "ashen", "warm", "tabby"); for villagers the biome type, or null - int tint, // ARGB multiplier for tintable layers (sheep wool); 0 = none - double sizeScale, // extra model scale (slime/magma-cube size); 1.0 = default - String profession, // villager profession key (e.g. "farmer", "librarian", "none"), or null - int villagerLevel, // villager profession level 1-5 (badge tier); 0 = none/unknown - String markings, // horse coat markings style (e.g. "white", "whitefield", "whitedots", "blackdots"), or null - boolean saddle, // horse/donkey/mule is saddled - boolean chest, // donkey/mule/llama is carrying a chest - String bodyEquip, // horse armor material (iron/gold/diamond/leather) OR llama carpet colour OR "trader_llama"; null = none - Equipment equipment, // worn humanoid armor (players, armor stands, zombies, …); null = none/not a wearer - boolean invisible // entity is invisible -> render only its equipment (like vanilla), not the body -) { + String typeKey, // e.g. "cow", "zombie", "player" + double x, + double y, + double z, + float bodyYaw, + boolean baby, + double width, + double height, + boolean player, + String skinUrl, + boolean slim, + String variant, // texture-selecting variant key (e.g. "ashen", "warm", "tabby"); for villagers the biome type, + // or null + int tint, // ARGB multiplier for tintable layers (sheep wool); 0 = none + double sizeScale, // extra model scale (slime/magma-cube size); 1.0 = default + String profession, // villager profession key (e.g. "farmer", "librarian", "none"), or null + int villagerLevel, // villager profession level 1-5 (badge tier); 0 = none/unknown + String markings, // horse coat markings style (e.g. "white", "whitefield", "whitedots", "blackdots"), or null + boolean saddle, // horse/donkey/mule is saddled + boolean chest, // donkey/mule/llama is carrying a chest + String bodyEquip, // horse armor material (iron/gold/diamond/leather) OR llama carpet colour OR "trader_llama"; + // null = none + Equipment equipment, // worn humanoid armor (players, armor stands, zombies, …); null = none/not a wearer + boolean invisible // entity is invisible -> render only its equipment (like vanilla), not the body + ) { /** Worn armor (4 slots) of a humanoid wearer; any field may be null. */ public record Equipment(EquipPiece head, EquipPiece chest, EquipPiece legs, EquipPiece feet) { @@ -33,10 +40,10 @@ public record EntityState( /** One worn armor piece. */ public record EquipPiece( - String asset, // equipment asset id, e.g. "diamond", "leather", "elytra", "turtle_scute" - int dyeColor, // ARGB tint for dyeable (leather) armor; 0 = undyed/not dyeable - String trimMaterial, // armor-trim material key (e.g. "diamond"); null = no trim - String trimPattern, // armor-trim pattern key (e.g. "coast"); null = no trim - boolean glint // item is enchanted -> render the enchantment glint - ) {} + String asset, // equipment asset id, e.g. "diamond", "leather", "elytra", "turtle_scute" + int dyeColor, // ARGB tint for dyeable (leather) armor; 0 = undyed/not dyeable + String trimMaterial, // armor-trim material key (e.g. "diamond"); null = no trim + String trimPattern, // armor-trim pattern key (e.g. "coast"); null = no trim + boolean glint // item is enchanted -> render the enchantment glint + ) {} } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/ModelCube.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/ModelCube.java index ecfc130..23e93bc 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/ModelCube.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/ModelCube.java @@ -5,10 +5,10 @@ package eu.mhsl.minecraft.pixelpics.render.entity; * {@code origin} is the minimum corner, {@code uv} is the box-UV texture offset. */ public final class ModelCube { - public final double[] origin; // 3, min corner (px) - public final double[] size; // 3 (px) - public final double inflate; // px, expands the box on all sides (overlay layers) - public final double[] uv; // 2, box-UV offset (texels) + public final double[] origin; // 3, min corner (px) + public final double[] size; // 3 (px) + public final double inflate; // px, expands the box on all sides (overlay layers) + public final double[] uv; // 2, box-UV offset (texels) public final boolean mirror; /** Optional modern per-face UV, indexed by {@code Direction.ordinal()}: {u, v, w, h} texels (h/w may be negative for flips). Null = use box-UV. */ public final double[][] faceUv; diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/TextureOps.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/TextureOps.java index 2d6a99d..40f751d 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/TextureOps.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/TextureOps.java @@ -39,7 +39,10 @@ public final class TextureOps { int sp = src[y][x]; int sa = (sp >>> 24) & 0xFF; if (sa == 0) continue; - if (sa == 255) { dst[y][x] = sp; continue; } + if (sa == 255) { + dst[y][x] = sp; + continue; + } int dp = dst[y][x]; int da = (dp >>> 24) & 0xFF; int outA = sa + da * (255 - sa) / 255; @@ -73,7 +76,10 @@ public final class TextureOps { int c = from[i]; int dr = pr - ((c >> 16) & 0xFF), dg = pg - ((c >> 8) & 0xFF), db = pb - (c & 0xFF); int d = dr * dr + dg * dg + db * db; - if (d < bestD) { bestD = d; best = i; } + if (d < bestD) { + bestD = d; + best = i; + } } row[x] = (a << 24) | (to[best] & 0xFFFFFF); } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/BlockEntityBaker.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/BlockEntityBaker.java index 548ee40..1c5db7c 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/BlockEntityBaker.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/BlockEntityBaker.java @@ -13,7 +13,6 @@ import eu.mhsl.minecraft.pixelpics.render.entity.EntityBaker; import eu.mhsl.minecraft.pixelpics.render.entity.EntityCube; 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; @@ -36,13 +35,13 @@ public final class BlockEntityBaker implements EntityBaker { // Sign-text placement (model px in the board's local frame; calibrated against the test harness). // The text block is scaled to fill the board's writable area (W×H) so few-line signs are rendered as // large as possible (legibility on low-res maps), capped so a single line isn't blown up absurdly. - private static final double SIGN_TEXT_W = 22.0, SIGN_TEXT_H = 11.0, SIGN_TEXT_CAP = 0.5; // board 24x12 - private static final double HANG_TEXT_W = 13.0, HANG_TEXT_H = 9.0, HANG_TEXT_CAP = 0.42; // board 14x10 - private static final double SIGN_TEXT_CY = 20.0; // board Y∈[14,26], centre 20 - private static final double HANGING_TEXT_CY = 5.0; // board Y∈[0,10], centre 5 - private static final double BOARD_FRONT_Z = 1.0; // both boards: front/back faces at Z=±1 - private static final double TEXT_Z_EPS = 0.05; // lift the text just off the board face - private static final double TEXT_THICK = 0.5; // slab thickness so the ray test never degenerates + private static final double SIGN_TEXT_W = 22.0, SIGN_TEXT_H = 11.0, SIGN_TEXT_CAP = 0.5; // board 24x12 + private static final double HANG_TEXT_W = 13.0, HANG_TEXT_H = 9.0, HANG_TEXT_CAP = 0.42; // board 14x10 + private static final double SIGN_TEXT_CY = 20.0; // board Y∈[14,26], centre 20 + private static final double HANGING_TEXT_CY = 5.0; // board Y∈[0,10], centre 5 + private static final double BOARD_FRONT_Z = 1.0; // both boards: front/back faces at Z=±1 + private static final double TEXT_Z_EPS = 0.05; // lift the text just off the board face + private static final double TEXT_THICK = 0.5; // slab thickness so the ray test never degenerates private final CemModelLoader models; private final TextureCache textures; @@ -69,12 +68,13 @@ public final class BlockEntityBaker implements EntityBaker { Place p = place(s); Affine pre = Affine.scale(p.scale / 16.0); Affine placement = Affine.translation(s.bx() + 0.5, s.by(), s.bz() + 0.5) - .mul(Affine.rotY(Math.toRadians(p.yaw))) - .mul(Affine.translation(p.lx, p.ly, p.lz)); + .mul(Affine.rotY(Math.toRadians(p.yaw))) + .mul(Affine.translation(p.lx, p.ly, p.lz)); List cubes = new ArrayList<>(); for (Layer layer : layers) { - for (CemGeometry.Baked b : CemGeometry.bakeModel(model, layer.tex, pre, layer.hidden, layer.texW, layer.texH, layer.boxUv)) { + for (CemGeometry.Baked b : + CemGeometry.bakeModel(model, layer.tex, pre, layer.hidden, layer.texW, layer.texH, layer.boxUv)) { cubes.add(new EntityCube(b.from(), b.to(), b.faces(), placement.mul(b.world()))); } } @@ -106,10 +106,18 @@ public final class BlockEntityBaker implements EntityBaker { addSide(s.backText(), Direction.SOUTH, tw, th, cap, cy, toWorld, cubes); } - private void addSide(BlockEntityState.SignText t, Direction faceDir, double tw, double th, double cap, - double cy, Affine toWorld, List cubes) { + private void addSide( + BlockEntityState.SignText t, + Direction faceDir, + double tw, + double th, + double cap, + double cy, + Affine toWorld, + List cubes) { if (t == null) return; - int[][] bmp = SignTextRasterizer.rasterize(trimBlankLines(t.lines()), font, t.fillArgb(), t.outlineArgb(), t.glowing()); + int[][] bmp = SignTextRasterizer.rasterize( + trimBlankLines(t.lines()), font, t.fillArgb(), t.outlineArgb(), t.glowing()); if (bmp == null) return; // Scale so the text block fills the board's writable area, whichever dimension binds, capped. double fpm = Math.min(cap, Math.min(tw / bmp[0].length, th / bmp.length)); @@ -117,10 +125,10 @@ public final class BlockEntityBaker implements EntityBaker { double blockH = bmp.length * fpm; double z0, z1; - if (faceDir == Direction.NORTH) { // front: −Z face, text sits just past −1 + if (faceDir == Direction.NORTH) { // front: −Z face, text sits just past −1 z1 = -BOARD_FRONT_Z - TEXT_Z_EPS; z0 = z1 - TEXT_THICK; - } else { // back: +Z face, text sits just past +1 + } else { // back: +Z face, text sits just past +1 z0 = BOARD_FRONT_Z + TEXT_Z_EPS; z1 = z0 + TEXT_THICK; } @@ -156,7 +164,9 @@ public final class BlockEntityBaker implements EntityBaker { /** A single bake pass: one texture applied to the parts not in {@code hidden}, with optional UV size * and a flag forcing box-UV (for standalone part textures not matching the model's per-face UV). */ private record Layer(int[][] tex, Set hidden, int texW, int texH, boolean boxUv) { - Layer(int[][] tex, Set hidden) { this(tex, hidden, 0, 0, false); } + Layer(int[][] tex, Set hidden) { + this(tex, hidden, 0, 0, false); + } } /** Some types paint different parts with different textures (pot sherds, conduit cage/heart). */ @@ -175,13 +185,17 @@ public final class BlockEntityBaker implements EntityBaker { /** The conduit's cage and inner heart use separate textures; the eye/wind (active state) are skipped. */ private List conduitLayers() { - int[][] cage = textures.get(ResourceLocation.parse("entity/conduit/cage")).orElse(null); - int[][] base = textures.get(ResourceLocation.parse("entity/conduit/base")).orElse(null); + int[][] cage = + textures.get(ResourceLocation.parse("entity/conduit/cage")).orElse(null); + int[][] base = + textures.get(ResourceLocation.parse("entity/conduit/base")).orElse(null); // Each part's texture is authored at its own native size (32x16) with a box-UV layout, so force // box-UV and normalise by the texture's own size (the model's per-face UV assumes a combined sheet). List layers = new ArrayList<>(2); - if (cage != null) layers.add(new Layer(cage, onlyPart("cage", CONDUIT_PARTS), cage[0].length, cage.length, true)); - if (base != null) layers.add(new Layer(base, onlyPart("base", CONDUIT_PARTS), base[0].length, base.length, true)); + if (cage != null) + layers.add(new Layer(cage, onlyPart("cage", CONDUIT_PARTS), cage[0].length, cage.length, true)); + if (base != null) + layers.add(new Layer(base, onlyPart("base", CONDUIT_PARTS), base[0].length, base.length, true)); return layers; } @@ -196,8 +210,10 @@ public final class BlockEntityBaker implements EntityBaker { private static final String[] POT_FACES = {"front", "left", "right", "back"}; // matches sherd capture order private List potLayers(BlockEntityState s) { - int[][] base = textures.get(ResourceLocation.parse("entity/decorated_pot/decorated_pot_base")).orElse(null); - int[][] side = textures.get(ResourceLocation.parse("entity/decorated_pot/decorated_pot_side")).orElse(null); + int[][] base = textures.get(ResourceLocation.parse("entity/decorated_pot/decorated_pot_base")) + .orElse(null); + int[][] side = textures.get(ResourceLocation.parse("entity/decorated_pot/decorated_pot_side")) + .orElse(null); if (base == null) return List.of(); List layers = new ArrayList<>(); // Structure (rim/neck/foot) comes from the combined base texture; the four sides are NOT in it. @@ -210,7 +226,8 @@ public final class BlockEntityBaker implements EntityBaker { String sherd = s.sherds().get(i); if (sherd != null && sherd.endsWith("_pottery_sherd")) { int[][] pat = textures.get(ResourceLocation.parse( - "entity/decorated_pot/" + sherd.replace("_pottery_sherd", "_pottery_pattern"))).orElse(null); + "entity/decorated_pot/" + sherd.replace("_pottery_sherd", "_pottery_pattern"))) + .orElse(null); if (pat != null) tex = pat; } } @@ -230,7 +247,7 @@ public final class BlockEntityBaker implements EntityBaker { return switch (s.kind()) { case SIGN -> new Place(yaw, SIGN_SCALE, 0, 0, 0); case WALL_SIGN -> new Place(yaw, SIGN_SCALE, 0, -0.3125, 0.4375); // drop to mid-block, push to wall - case WALL_HEAD -> new Place(yaw, 1.0, 0, 0.25, 0.25); // mid-height, against the wall + case WALL_HEAD -> new Place(yaw, 1.0, 0, 0.25, 0.25); // mid-height, against the wall case WALL_BANNER -> new Place(yaw, 1.0, 0, -0.16, 0.4375); default -> new Place(yaw, 1.0, 0, 0, 0); }; @@ -240,8 +257,8 @@ public final class BlockEntityBaker implements EntityBaker { private Set hiddenParts(BlockEntityState s) { return switch (s.kind()) { case BED -> s.bedPart() == BlockEntityState.BedPart.HEAD - ? Set.of("foot", "leg3", "leg4") - : Set.of("head", "leg1", "leg2"); + ? Set.of("foot", "leg3", "leg4") + : Set.of("head", "leg1", "leg2"); case CONDUIT -> Set.of("eye", "wind"); default -> Set.of(); }; @@ -251,7 +268,8 @@ public final class BlockEntityBaker implements EntityBaker { private int[][] resolveTexture(BlockEntityState s) { // Player heads use the owner's skin when available. - if (s.skinUrl() != null && (s.kind() == BlockEntityState.Kind.HEAD || s.kind() == BlockEntityState.Kind.WALL_HEAD)) { + if (s.skinUrl() != null + && (s.kind() == BlockEntityState.Kind.HEAD || s.kind() == BlockEntityState.Kind.WALL_HEAD)) { int[][] skin = skins.get(s.skinUrl()).orElse(null); if (skin != null) return skin; } @@ -273,7 +291,8 @@ public final class BlockEntityBaker implements EntityBaker { int[][] out = TextureOps.deepCopy(base); if (s.baseColorArgb() != 0) TextureOps.tint(out, s.baseColorArgb()); for (BlockEntityState.BannerPattern pat : s.patterns()) { - int[][] mask = textures.get(ResourceLocation.parse("entity/banner/" + pat.patternKey())).orElse(null); + int[][] mask = textures.get(ResourceLocation.parse("entity/banner/" + pat.patternKey())) + .orElse(null); if (mask == null) continue; int[][] dyed = TextureOps.deepCopy(mask); TextureOps.tint(dyed, pat.colorArgb()); diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/CemBaker.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/CemBaker.java index 875cbeb..c0dfbf8 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/CemBaker.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/CemBaker.java @@ -13,7 +13,6 @@ import eu.mhsl.minecraft.pixelpics.render.entity.EntityState; import eu.mhsl.minecraft.pixelpics.render.entity.ModelCube; 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; @@ -31,9 +30,8 @@ public final class CemBaker implements EntityBaker { // Parts representing an alternate state (rolled-up, sleeping, …) that must not render in the idle pose. private static final Map> HIDDEN_PARTS = Map.of( - "armadillo", Set.of("cube"), // the rolled-up ball - "illager", Set.of("left_arm", "right_arm") - ); + "armadillo", Set.of("cube"), // the rolled-up ball + "illager", Set.of("left_arm", "right_arm")); private final CemModelLoader models; private final TextureCache textures; @@ -53,11 +51,11 @@ public final class CemBaker implements EntityBaker { if (s.typeKey().equals("villager") || s.typeKey().equals("zombie_villager")) { tex = compositeVillager(s, tex); } else if (cem.equals("horse")) { - tex = compositeHorse(s, tex); // coat markings + horse armor + tex = compositeHorse(s, tex); // coat markings + horse armor } else if (cem.equals("llama")) { - tex = compositeLlama(s, tex); // dyed/trader carpet decor + tex = compositeLlama(s, tex); // dyed/trader carpet decor } else if (cem.equals("nautilus")) { - tex = compositeNautilus(s, tex); // body armor + saddle (same-UV overlays) + tex = compositeNautilus(s, tex); // body armor + saddle (same-UV overlays) } CemModelLoader.CemModel model = models.get(cem); // A visible entity needs its body model+texture; an invisible one renders only its equipment @@ -73,13 +71,18 @@ public final class CemBaker implements EntityBaker { Set 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"); } - else if (cem.equals("llama")) { hidden.add("chest_left"); hidden.add("chest_right"); } + if (cem.equals("donkey")) { + hidden.add("left_chest"); + hidden.add("right_chest"); + } else if (cem.equals("llama")) { + hidden.add("chest_left"); + hidden.add("chest_right"); + } } // 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 body = (model != null && tex != null) - ? CemGeometry.bakeModel(model, tex, pre, hidden) : List.of(); + List body = + (model != null && tex != null) ? CemGeometry.bakeModel(model, tex, pre, hidden) : List.of(); List baked = new ArrayList<>(); if (!invisible) { @@ -87,7 +90,8 @@ public final class CemBaker implements EntityBaker { // Sheep: render the inflated, dye-tinted wool fur layer over the body (transparent where the face shows). if (s.typeKey().equals("sheep")) { CemModelLoader.CemModel wool = models.get("sheep_wool"); - int[][] woolTex = textures.get(ResourceLocation.parse("entity/sheep/sheep_wool")).orElse(null); + int[][] woolTex = textures.get(ResourceLocation.parse("entity/sheep/sheep_wool")) + .orElse(null); if (wool != null && woolTex != null) { int[][] t = TextureOps.deepCopy(woolTex); if (s.tint() != 0) TextureOps.tint(t, s.tint()); @@ -98,9 +102,10 @@ public final class CemBaker implements EntityBaker { // left face is transparent in the texture → a see-through hole on the left. Add the mirrored left panel. if (cem.equals("guardian")) { double[] org = {-8, 2, -6}, size = {2, 12, 12}; - ModelCube mc = new ModelCube(org, size, 0, new double[]{0, 28}, true); + ModelCube mc = new ModelCube(org, size, 0, new double[] {0, 28}, true); Face[] faces = BoxUv.build(mc, tex, model.texW(), model.texH()); - baked.add(new CemGeometry.Baked(org, new double[]{org[0]+size[0], org[1]+size[1], org[2]+size[2]}, faces, pre)); + baked.add(new CemGeometry.Baked( + org, new double[] {org[0] + size[0], org[1] + size[1], org[2] + size[2]}, faces, pre)); } // Saddle: an extra inflated layer from the *_saddle CEM model, showing only its saddle-specific parts. if (s.saddle()) addSaddleLayer(s, cem, model, pre, baked); @@ -116,8 +121,8 @@ public final class CemBaker implements EntityBaker { for (CemGeometry.Baked b : ref) minY = Math.min(minY, b.minWorldY()); Affine place = Affine.translation(s.x(), s.y(), s.z()) - .mul(Affine.rotY(Math.PI - Math.toRadians(s.bodyYaw()))) - .mul(Affine.translation(0, -minY, 0)); + .mul(Affine.rotY(Math.PI - Math.toRadians(s.bodyYaw()))) + .mul(Affine.translation(0, -minY, 0)); List cubes = new ArrayList<>(baked.size()); for (CemGeometry.Baked b : baked) cubes.add(new EntityCube(b.from(), b.to(), b.faces(), place.mul(b.world()))); @@ -129,8 +134,9 @@ public final class CemBaker implements EntityBaker { if (s.player()) { int[][] skin = skins.get(s.skinUrl()).orElse(null); if (skin != null) return skin; - int[][] def = textures.get(ResourceLocation.parse( - s.slim() ? "entity/player/slim/steve" : "entity/player/wide/steve")).orElse(null); + int[][] def = textures.get( + ResourceLocation.parse(s.slim() ? "entity/player/slim/steve" : "entity/player/wide/steve")) + .orElse(null); if (def != null) return def; } for (ResourceLocation rl : EntityModels.textureCandidates(s.typeKey(), s.variant())) { @@ -195,12 +201,14 @@ public final class CemBaker implements EntityBaker { } /** Bake the saddle as a separate inflated layer; only the saddle-specific parts (those not in the base model). */ - private void addSaddleLayer(EntityState s, String cem, CemModelLoader.CemModel base, Affine pre, List baked) { + private void addSaddleLayer( + EntityState s, String cem, CemModelLoader.CemModel base, Affine pre, List baked) { String saddleModel = cem.equals("donkey") ? "donkey_saddle" : (cem.equals("horse") ? "horse_saddle" : null); if (saddleModel == null) return; // llamas and other mounts have no saddle model CemModelLoader.CemModel sm = models.get(saddleModel); if (sm == null) return; - int[][] saddleTex = textures.get(ResourceLocation.parse("entity/equipment/" + s.typeKey() + "_saddle/saddle")).orElse(null); + 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. Set hideBase = new HashSet<>(); @@ -214,10 +222,10 @@ public final class CemBaker implements EntityBaker { // shoes=boots; // armor_layer_2 (texture entity/equipment/humanoid_leggings/): waist+legs=leggings. // Each slot may use a different material, so each is baked separately, showing only its parts. - private static final Set ARMOR1_HEAD = Set.of("head"); + private static final Set ARMOR1_HEAD = Set.of("head"); private static final Set ARMOR1_CHEST = Set.of("body", "left_arm", "right_arm"); - private static final Set ARMOR1_FEET = Set.of("left_shoe", "right_shoe"); - private static final Set ARMOR2_LEGS = Set.of("waist", "left_leg", "right_leg"); + private static final Set ARMOR1_FEET = Set.of("left_shoe", "right_shoe"); + private static final Set 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 baked) { @@ -234,8 +242,13 @@ public final class CemBaker implements EntityBaker { } /** Bake one armor slot: its layer model with only {@code show} parts, textured for the material. */ - private void bakeArmorPiece(EntityState.EquipPiece piece, String modelName, Set show, - String layerFolder, Affine pre, List baked) { + private void bakeArmorPiece( + EntityState.EquipPiece piece, + String modelName, + Set show, + String layerFolder, + Affine pre, + List baked) { if (piece == null) return; CemModelLoader.CemModel model = models.get(modelName); if (model == null) return; @@ -249,7 +262,8 @@ public final class CemBaker implements EntityBaker { /** Resolve + composite a single armor slot's texture: material base (+leather dye/overlay), trim, glint. */ private int[][] buildArmorTexture(EntityState.EquipPiece piece, String layerFolder) { String asset = piece.asset(); - int[][] base = textures.get(ResourceLocation.parse("entity/equipment/" + layerFolder + "/" + asset)).orElse(null); + int[][] base = textures.get(ResourceLocation.parse("entity/equipment/" + layerFolder + "/" + asset)) + .orElse(null); if (base == null) return null; int[][] out = TextureOps.deepCopy(base); // Leather is dyeable: tint the base layer, then composite the (undyed) overlay layer on top. @@ -266,13 +280,15 @@ public final class CemBaker implements EntityBaker { /** Armor trim: recolour the grayscale pattern via the material palette (same UV) and overlay it. */ private void applyTrim(int[][] armorTex, String layerFolder, String pattern, String material, String armorAsset) { - int[][] mask = textures.get(ResourceLocation.parse("trims/entity/" + layerFolder + "/" + pattern)).orElse(null); + int[][] mask = textures.get(ResourceLocation.parse("trims/entity/" + layerFolder + "/" + pattern)) + .orElse(null); if (mask == null || mask.length != armorTex.length || mask[0].length != armorTex[0].length) return; int[] from = palette8("trims/color_palettes/trim_palette"); // Vanilla uses the darker palette variant when the trim material matches the armor material. String palette = material; if (material.equals(armorAsset) - && textures.get(ResourceLocation.parse("trims/color_palettes/" + material + "_darker")).isPresent()) { + && textures.get(ResourceLocation.parse("trims/color_palettes/" + material + "_darker")) + .isPresent()) { palette = material + "_darker"; } int[] to = palette8("trims/color_palettes/" + palette); @@ -291,14 +307,15 @@ public final class CemBaker implements EntityBaker { /** Static enchantment-glint approximation over an armor/elytra/item texture (in place). */ private void applyGlint(int[][] tex) { textures.get(ResourceLocation.parse("misc/enchanted_glint_armor")) - .ifPresent(glint -> TextureOps.addGlint(tex, glint, GLINT_COLOR, 0.6)); + .ifPresent(glint -> TextureOps.addGlint(tex, glint, GLINT_COLOR, 0.6)); } /** Elytra in the chest slot: bake the wing model with the default elytra texture (no chestplate). */ private void bakeElytra(EntityState.EquipPiece piece, Affine pre, List baked) { CemModelLoader.CemModel model = models.get("elytra"); if (model == null) return; - int[][] tex = textures.get(ResourceLocation.parse("entity/equipment/wings/elytra")).orElse(null); + int[][] tex = textures.get(ResourceLocation.parse("entity/equipment/wings/elytra")) + .orElse(null); if (tex == null) return; int[][] out = TextureOps.deepCopy(tex); if (piece.glint()) applyGlint(out); @@ -310,12 +327,12 @@ public final class CemBaker implements EntityBaker { double[] from = {-w / 2, 0, -w / 2}; double[] to = {w / 2, h, w / 2}; int[][] t = tex != null ? tex : flat(0xFF8C8C8C); - ModelCube box = new ModelCube(new double[]{-w/2, 0, -w/2}, new double[]{w, h, w}, 0, - new double[]{0, 0}, false); + ModelCube box = + new ModelCube(new double[] {-w / 2, 0, -w / 2}, new double[] {w, h, w}, 0, new double[] {0, 0}, false); Face[] faces = BoxUv.build(box, t, Math.max(64, (int) (2 * (w + w))), Math.max(64, (int) (2 * (w + h)))); Affine place = Affine.translation(s.x(), s.y(), s.z()) - .mul(Affine.rotY(Math.PI - Math.toRadians(s.bodyYaw()))) - .mul(Affine.scale(1.0 / 16.0)); + .mul(Affine.rotY(Math.PI - Math.toRadians(s.bodyYaw()))) + .mul(Affine.scale(1.0 / 16.0)); List cubes = new ArrayList<>(); cubes.add(new EntityCube(from, to, faces, place)); return RenderedEntity.of(cubes); diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/CemGeometry.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/CemGeometry.java index 8f2d163..7f178d9 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/CemGeometry.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/CemGeometry.java @@ -4,7 +4,6 @@ import eu.mhsl.minecraft.pixelpics.assets.model.Face; import eu.mhsl.minecraft.pixelpics.render.entity.Affine; import eu.mhsl.minecraft.pixelpics.render.entity.BoxUv; import eu.mhsl.minecraft.pixelpics.render.entity.ModelCube; - import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -45,8 +44,14 @@ final class CemGeometry { * {@code ignoreFaceUv} is set, box-UV is forced even if the model declares per-face UV — used for a * standalone texture (e.g. the conduit cage) whose layout is box-UV, not the combined-sheet layout. */ - static List bakeModel(CemModelLoader.CemModel model, int[][] tex, Affine pre, Set hidden, - int texW, int texH, boolean ignoreFaceUv) { + static List bakeModel( + CemModelLoader.CemModel model, + int[][] tex, + Affine pre, + Set hidden, + int texW, + int texH, + boolean ignoreFaceUv) { int nw = texW > 0 ? texW : model.texW(); int nh = texH > 0 ? texH : model.texH(); List out = new ArrayList<>(); @@ -63,15 +68,24 @@ final class CemGeometry { * parent origin from the 2nd nesting level on). Top-level boxes are absolute; nested boxes are offset * by their group origin. The group transform is {@code parent · T(O) · R · T(-O)}. */ - private static void bakePart(CemModelLoader.CemPart part, Affine parentWorld, double[] o, int depth, - Set hidden, int texW, int texH, int[][] tex, boolean ignoreFaceUv, List out) { + private static void bakePart( + CemModelLoader.CemPart part, + Affine parentWorld, + double[] o, + int depth, + Set hidden, + int texW, + int texH, + int[][] tex, + boolean ignoreFaceUv, + List out) { if (hidden.contains(part.name())) return; Affine world = parentWorld - .mul(Affine.translation(o[0], o[1], o[2])) - .mul(Affine.rotZ(Math.toRadians(part.rotate()[2]))) - .mul(Affine.rotY(Math.toRadians(part.rotate()[1]))) - .mul(Affine.rotX(Math.toRadians(part.rotate()[0]))) - .mul(Affine.translation(-o[0], -o[1], -o[2])); + .mul(Affine.translation(o[0], o[1], o[2])) + .mul(Affine.rotZ(Math.toRadians(part.rotate()[2]))) + .mul(Affine.rotY(Math.toRadians(part.rotate()[1]))) + .mul(Affine.rotX(Math.toRadians(part.rotate()[0]))) + .mul(Affine.translation(-o[0], -o[1], -o[2])); double ox = depth > 0 ? o[0] : 0, oy = depth > 0 ? o[1] : 0, oz = depth > 0 ? o[2] : 0; for (CemModelLoader.CemBox b : part.boxes()) { @@ -87,9 +101,9 @@ final class CemGeometry { for (CemModelLoader.CemPart child : part.children()) { double[] t = child.translate(); // submodel origin = its translate, accumulated with this group's origin from the 2nd level on. - double[] co = depth >= 1 ? new double[]{t[0] + o[0], t[1] + o[1], t[2] + o[2]} : new double[]{t[0], t[1], t[2]}; + double[] co = + depth >= 1 ? new double[] {t[0] + o[0], t[1] + o[1], t[2] + o[2]} : new double[] {t[0], t[1], t[2]}; bakePart(child, world, co, depth + 1, hidden, texW, texH, tex, ignoreFaceUv, out); } } - } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/CemModelLoader.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/CemModelLoader.java index 56301a4..68172b5 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/CemModelLoader.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/CemModelLoader.java @@ -4,7 +4,6 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; - import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; @@ -29,11 +28,13 @@ public final class CemModelLoader { * and optional per-face UV ({@code faceUv}, indexed by {@link eu.mhsl.minecraft.pixelpics.assets.model.Direction} * ordinal, each {@code {x, y, w, h}} texels; null = use box-UV). */ - public record CemBox(double[] origin, double[] size, double inflate, double[] uv, boolean mirror, double[][] faceUv) {} + public record CemBox( + double[] origin, double[] size, double inflate, double[] uv, boolean mirror, double[][] faceUv) {} /** A model part: its (raw) translate, rotation (deg), boxes and nested submodels. The rotation pivot * is {@code -(sum of translates from the root to this part)} — accumulated by the baker. */ - public record CemPart(String name, double[] translate, double[] rotate, List boxes, List children) {} + public record CemPart( + String name, double[] translate, double[] rotate, List boxes, List children) {} /** A whole model: declared texture size and its top-level parts. */ public record CemModel(int texW, int texH, List parts) {} @@ -50,13 +51,15 @@ public final class CemModelLoader { /** Parse the CEM template-models JSON stream. Returns the number of models loaded. */ public int load(InputStream in, Logger logger) { - JsonObject root = JsonParser.parseReader(new InputStreamReader(in, StandardCharsets.UTF_8)).getAsJsonObject(); + JsonObject root = JsonParser.parseReader(new InputStreamReader(in, StandardCharsets.UTF_8)) + .getAsJsonObject(); JsonObject modelsObj = root.getAsJsonObject("models"); for (Map.Entry e : modelsObj.entrySet()) { try { JsonObject entry = e.getValue().getAsJsonObject(); if (!entry.has("model")) continue; - JsonObject model = JsonParser.parseString(entry.get("model").getAsString()).getAsJsonObject(); + JsonObject model = + JsonParser.parseString(entry.get("model").getAsString()).getAsJsonObject(); int tw = model.getAsJsonArray("textureSize").get(0).getAsInt(); int th = model.getAsJsonArray("textureSize").get(1).getAsInt(); List parts = new ArrayList<>(); @@ -72,26 +75,36 @@ public final class CemModelLoader { private CemPart parsePart(JsonObject p) { double[] translate = arr3(p, "translate"); double[] rotate = arr3(p, "rotate"); - boolean partMirror = mirrorsU(p); // mirrorTexture "u" — applies to all of the part's boxes + boolean partMirror = mirrorsU(p); // mirrorTexture "u" — applies to all of the part's boxes List boxes = new ArrayList<>(); if (p.has("boxes")) { for (JsonElement be : p.getAsJsonArray("boxes")) { JsonObject b = be.getAsJsonObject(); if (!b.has("coordinates")) continue; JsonArray c = b.getAsJsonArray("coordinates"); - double[] origin = {c.get(0).getAsDouble(), c.get(1).getAsDouble(), c.get(2).getAsDouble()}; - double[] size = {c.get(3).getAsDouble(), c.get(4).getAsDouble(), c.get(5).getAsDouble()}; + double[] origin = { + c.get(0).getAsDouble(), c.get(1).getAsDouble(), c.get(2).getAsDouble() + }; + double[] size = { + c.get(3).getAsDouble(), c.get(4).getAsDouble(), c.get(5).getAsDouble() + }; double inflate = b.has("sizeAdd") ? b.get("sizeAdd").getAsDouble() : 0; double[] uv = b.has("textureOffset") - ? new double[]{b.getAsJsonArray("textureOffset").get(0).getAsDouble(), b.getAsJsonArray("textureOffset").get(1).getAsDouble()} - : new double[]{0, 0}; + ? new double[] { + b.getAsJsonArray("textureOffset").get(0).getAsDouble(), + b.getAsJsonArray("textureOffset").get(1).getAsDouble() + } + : new double[] {0, 0}; boxes.add(new CemBox(origin, size, inflate, uv, partMirror || mirrorsU(b), parseFaceUv(b))); } } List children = new ArrayList<>(); - if (p.has("submodels")) for (JsonElement se : p.getAsJsonArray("submodels")) children.add(parsePart(se.getAsJsonObject())); + if (p.has("submodels")) + for (JsonElement se : p.getAsJsonArray("submodels")) children.add(parsePart(se.getAsJsonObject())); if (p.has("submodel")) children.add(parsePart(p.getAsJsonObject("submodel"))); - String name = p.has("part") ? p.get("part").getAsString() : (p.has("id") ? p.get("id").getAsString() : ""); + String name = p.has("part") + ? p.get("part").getAsString() + : (p.has("id") ? p.get("id").getAsString() : ""); return new CemPart(name, translate, rotate, boxes, children); } @@ -101,7 +114,11 @@ public final class CemModelLoader { /** Parses per-face UV ({@code uvNorth} etc., each {@code [u1,v1,u2,v2]}) into {@code {x,y,w,h}}, or null. */ private static double[][] parseFaceUv(JsonObject b) { boolean any = false; - for (String k : FACE_UV_KEYS) if (b.has(k)) { any = true; break; } + for (String k : FACE_UV_KEYS) + if (b.has(k)) { + any = true; + break; + } if (!any) return null; double[][] faces = new double[6][]; for (int i = 0; i < FACE_UV_KEYS.length; i++) { @@ -109,7 +126,7 @@ public final class CemModelLoader { JsonArray a = b.getAsJsonArray(FACE_UV_KEYS[i]); double u1 = a.get(0).getAsDouble(), v1 = a.get(1).getAsDouble(); double u2 = a.get(2).getAsDouble(), v2 = a.get(3).getAsDouble(); - faces[i] = new double[]{u1, v1, u2 - u1, v2 - v1}; + faces[i] = new double[] {u1, v1, u2 - u1, v2 - v1}; } return faces; } @@ -119,8 +136,10 @@ public final class CemModelLoader { } private static double[] arr3(JsonObject o, String key) { - if (!o.has(key) || !o.get(key).isJsonArray()) return new double[]{0, 0, 0}; + if (!o.has(key) || !o.get(key).isJsonArray()) return new double[] {0, 0, 0}; JsonArray a = o.getAsJsonArray(key); - return new double[]{a.get(0).getAsDouble(), a.get(1).getAsDouble(), a.get(2).getAsDouble()}; + return new double[] { + a.get(0).getAsDouble(), a.get(1).getAsDouble(), a.get(2).getAsDouble() + }; } } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/SignTextRasterizer.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/SignTextRasterizer.java index 3533152..939da14 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/SignTextRasterizer.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/SignTextRasterizer.java @@ -2,7 +2,6 @@ package eu.mhsl.minecraft.pixelpics.render.entity.cem; import eu.mhsl.minecraft.pixelpics.assets.font.BitmapFont; import eu.mhsl.minecraft.pixelpics.assets.font.Glyph; - import java.util.List; /** @@ -22,8 +21,8 @@ final class SignTextRasterizer { if (font.isEmpty() || lines.isEmpty()) return null; int ascent = font.maxAscent(); - int pitch = font.lineHeight(); // ascent + descent - int margin = glow ? 1 : 0; // room for the outline + int pitch = font.lineHeight(); // ascent + descent + int margin = glow ? 1 : 0; // room for the outline int contentW = 1; for (String line : lines) contentW = Math.max(contentW, lineWidth(line, font)); diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/ElementIntersector.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/ElementIntersector.java index e8c6f57..9200585 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/ElementIntersector.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/ElementIntersector.java @@ -5,10 +5,9 @@ import eu.mhsl.minecraft.pixelpics.assets.model.Element; import eu.mhsl.minecraft.pixelpics.assets.model.Face; import eu.mhsl.minecraft.pixelpics.assets.model.ResolvedModel; import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil; -import org.bukkit.util.Vector; - import java.util.ArrayList; import java.util.List; +import org.bukkit.util.Vector; /** * Intersects a ray (in block coordinates) with a {@link ResolvedModel}'s element boxes and returns @@ -28,16 +27,23 @@ public final class ElementIntersector { * @param dx,dy,dz ray direction (need not be normalized) * @param bx,by,bz block min corner in world coordinates (for reconstructing the world hit point) */ - public static FaceHit intersect(ResolvedModel model, - double ox, double oy, double oz, - double dx, double dy, double dz, - int bx, int by, int bz) { + public static FaceHit intersect( + ResolvedModel model, + double ox, + double oy, + double oz, + double dx, + double dy, + double dz, + int bx, + int by, + int bz) { List candidates = new ArrayList<>(model.elements.size()); for (int i = 0; i < model.elements.size(); i++) { Element element = model.elements.get(i); Candidate c = element.isAxisAligned() - ? intersectAabb(element, ox, oy, oz, dx, dy, dz) - : intersectObb(element, ox, oy, oz, dx, dy, dz); + ? intersectAabb(element, ox, oy, oz, dx, dy, dz) + : intersectObb(element, ox, oy, oz, dx, dy, dz); if (c != null) candidates.add(new Candidate(c.element, c.t, c.dir, c.s, c.t2, c.normal, i)); } if (candidates.isEmpty()) return null; @@ -61,16 +67,16 @@ public final class ElementIntersector { return null; } - private record Candidate(Element element, double t, Direction dir, double s, double t2, double[] normal, int order) {} + private record Candidate( + Element element, double t, Direction dir, double s, double t2, double[] normal, int order) {} - private static Candidate intersectAabb(Element e, double ox, double oy, double oz, - double dx, double dy, double dz) { + private static Candidate intersectAabb( + Element e, double ox, double oy, double oz, double dx, double dy, double dz) { return slab(e, ox, oy, oz, dx, dy, dz, e.from, e.to, null); } /** Rotated element: transform the ray into the element's local (unrotated) frame, then slab-test. */ - private static Candidate intersectObb(Element e, double ox, double oy, double oz, - double dx, double dy, double dz) { + private static Candidate intersectObb(Element e, double ox, double oy, double oz, double dx, double dy, double dz) { double[] o = rotate(ox - e.rotOrigin[0], oy - e.rotOrigin[1], oz - e.rotOrigin[2], e.rotAxis, -e.rotAngleRad); o[0] += e.rotOrigin[0]; o[1] += e.rotOrigin[1]; @@ -79,9 +85,17 @@ public final class ElementIntersector { return slab(e, o[0], o[1], o[2], d[0], d[1], d[2], e.from, e.to, e); } - private static Candidate slab(Element e, double ox, double oy, double oz, - double dx, double dy, double dz, - double[] from, double[] to, Element obb) { + private static Candidate slab( + Element e, + double ox, + double oy, + double oz, + double dx, + double dy, + double dz, + double[] from, + double[] to, + Element obb) { double tmin = Double.NEGATIVE_INFINITY; double tmax = Double.POSITIVE_INFINITY; int axis = -1; @@ -99,7 +113,9 @@ public final class ElementIntersector { double t2 = (to[a] - o[a]) * inv; boolean neg = true; if (t1 > t2) { - double tmp = t1; t1 = t2; t2 = tmp; + double tmp = t1; + t1 = t2; + t2 = tmp; neg = false; } if (t1 > tmin) { @@ -136,11 +152,20 @@ public final class ElementIntersector { double s, t; switch (dir) { - // Texture V is top-down (0 = texture top). For side faces the texture top is the block - // top (high Y), so t = 1 - fracY. - case UP, DOWN -> { s = fracX; t = fracZ; } - case NORTH, SOUTH -> { s = fracX; t = 1 - fracY; } - default -> { s = fracZ; t = 1 - fracY; } // WEST, EAST + // Texture V is top-down (0 = texture top). For side faces the texture top is the block + // top (high Y), so t = 1 - fracY. + case UP, DOWN -> { + s = fracX; + t = fracZ; + } + case NORTH, SOUTH -> { + s = fracX; + t = 1 - fracY; + } + default -> { + s = fracZ; + t = 1 - fracY; + } // WEST, EAST } return new Candidate(e, tEntry, dir, s, t, normal, 0); } @@ -165,9 +190,9 @@ public final class ElementIntersector { double cos = Math.cos(angle); double sin = Math.sin(angle); return switch (axis) { - case 0 -> new double[]{x, y * cos - z * sin, y * sin + z * cos}; - case 1 -> new double[]{x * cos + z * sin, y, -x * sin + z * cos}; - default -> new double[]{x * cos - y * sin, x * sin + y * cos, z}; + case 0 -> new double[] {x, y * cos - z * sin, y * sin + z * cos}; + case 1 -> new double[] {x * cos + z * sin, y, -x * sin + z * cos}; + default -> new double[] {x * cos - y * sin, x * sin + y * cos, z}; }; } } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/FaceHit.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/FaceHit.java index 925f543..cb15b3f 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/FaceHit.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/FaceHit.java @@ -6,5 +6,4 @@ import org.bukkit.util.Vector; * The result of intersecting a ray with a block's geometry: the world-space hit point and normal, * the sampled ARGB color (before shading/tinting) and the face's tint index ({@code -1} = none). */ -public record FaceHit(double t, Vector point, Vector normal, int color, int tintIndex) { -} +public record FaceHit(double t, Vector point, Vector normal, int color, int tintIndex) {} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/SnapshotRaytracer.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/SnapshotRaytracer.java index 31976d8..5d6ad48 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/SnapshotRaytracer.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/raytrace/SnapshotRaytracer.java @@ -11,14 +11,13 @@ import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider; import eu.mhsl.minecraft.pixelpics.render.tint.TintResolver; import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil; import eu.mhsl.minecraft.pixelpics.render.util.MathUtil; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.bukkit.Material; 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 @@ -44,8 +43,12 @@ public final class SnapshotRaytracer { private final int maxSteps; private final Map tintCache = new ConcurrentHashMap<>(); - public SnapshotRaytracer(BlockModelRegistry registry, BiomeTintProvider tintProvider, - SkyRenderer skyRenderer, double maxDistance, int reflectionDepth) { + public SnapshotRaytracer( + BlockModelRegistry registry, + BiomeTintProvider tintProvider, + SkyRenderer skyRenderer, + double maxDistance, + int reflectionDepth) { this.registry = registry; this.tintProvider = tintProvider; this.skyRenderer = skyRenderer; @@ -58,7 +61,8 @@ public final class SnapshotRaytracer { return trace(snapshot, origin, direction, sky, scene, reflectionDepth); } - private int trace(WorldSnapshot snapshot, Vector origin, Vector direction, SkyContext sky, EntityScene scene, int depth) { + private int trace( + WorldSnapshot snapshot, Vector origin, Vector direction, SkyContext sky, EntityScene scene, int depth) { double ox = origin.getX(), oy = origin.getY(), oz = origin.getZ(); double dx = direction.getX(), dy = direction.getY(), dz = direction.getZ(); @@ -99,7 +103,8 @@ public final class SnapshotRaytracer { if (!reflected && model.reflection > 0 && depth > 0) { Vector reflectDir = MathUtil.reflectVector(direction, hit.normal()); - Vector reflectStart = hit.point().clone().add(hit.normal().clone().multiply(1e-3)); + Vector reflectStart = + hit.point().clone().add(hit.normal().clone().multiply(1e-3)); reflectionColor = trace(snapshot, reflectStart, reflectDir, sky, scene, depth - 1); reflectionFactor = model.reflection; reflected = true; @@ -147,11 +152,13 @@ public final class SnapshotRaytracer { if (transparencyStart != null) { baseColor = ColorUtil.mix( - baseColor, - transparencyColor, - transparencyFactor, - (1 - transparencyFactor) - * (1 + transparencyStart.distance(finalPoint == null ? transparencyStart : finalPoint) / 5.0)); + baseColor, + transparencyColor, + transparencyFactor, + (1 - transparencyFactor) + * (1 + + transparencyStart.distance(finalPoint == null ? transparencyStart : finalPoint) + / 5.0)); } if (reflected) { baseColor = ColorUtil.mix(baseColor, reflectionColor, 1 - reflectionFactor, reflectionFactor); @@ -191,7 +198,9 @@ public final class SnapshotRaytracer { * across the face. Only applied to axis-aligned faces. */ private double ambientOcclusion(FaceHit hit, WorldSnapshot snapshot, int bx, int by, int bz) { - double nx = hit.normal().getX(), ny = hit.normal().getY(), nz = hit.normal().getZ(); + double nx = hit.normal().getX(), + ny = hit.normal().getY(), + nz = hit.normal().getZ(); double ax = Math.abs(nx), ay = Math.abs(ny), az = Math.abs(nz); if (Math.max(ax, Math.max(ay, az)) < 0.99) return 1.0; // skip rotated/diagonal faces @@ -203,12 +212,33 @@ public final class SnapshotRaytracer { int ofx = (int) Math.round(nx), ofy = (int) Math.round(ny), ofz = (int) Math.round(nz); int ux, uy, uz, vx, vy, vz; double su, sv; - if (ay > 0.5) { // up/down - ux = 1; uy = 0; uz = 0; vx = 0; vy = 0; vz = 1; su = lx; sv = lz; - } else if (ax > 0.5) { // east/west - ux = 0; uy = 0; uz = 1; vx = 0; vy = 1; vz = 0; su = lz; sv = ly; - } else { // north/south - ux = 1; uy = 0; uz = 0; vx = 0; vy = 1; vz = 0; su = lx; sv = ly; + if (ay > 0.5) { // up/down + ux = 1; + uy = 0; + uz = 0; + vx = 0; + vy = 0; + vz = 1; + su = lx; + sv = lz; + } else if (ax > 0.5) { // east/west + ux = 0; + uy = 0; + uz = 1; + vx = 0; + vy = 1; + vz = 0; + su = lz; + sv = ly; + } else { // north/south + ux = 1; + uy = 0; + uz = 0; + vx = 0; + vy = 1; + vz = 0; + su = lx; + sv = ly; } double b00 = aoCorner(snapshot, bx, by, bz, ofx, ofy, ofz, ux, uy, uz, vx, vy, vz, -1, -1); @@ -221,13 +251,26 @@ public final class SnapshotRaytracer { return top + (bottom - top) * sv; } - private double aoCorner(WorldSnapshot snapshot, int bx, int by, int bz, - int ofx, int ofy, int ofz, int ux, int uy, int uz, int vx, int vy, int vz, - int du, int dv) { + private double aoCorner( + WorldSnapshot snapshot, + int bx, + int by, + int bz, + int ofx, + int ofy, + int ofz, + int ux, + int uy, + int uz, + int vx, + int vy, + int vz, + int du, + int dv) { boolean side1 = solid(snapshot, bx + ofx + du * ux, by + ofy + du * uy, bz + ofz + du * uz); boolean side2 = solid(snapshot, bx + ofx + dv * vx, by + ofy + dv * vy, bz + ofz + dv * vz); - boolean corner = solid(snapshot, - bx + ofx + du * ux + dv * vx, by + ofy + du * uy + dv * vy, bz + ofz + du * uz + dv * vz); + boolean corner = solid( + snapshot, bx + ofx + du * ux + dv * vx, by + ofy + du * uy + dv * vy, bz + ofz + du * uz + dv * vz); int level = (side1 && side2) ? 0 : 3 - (side1 ? 1 : 0) - (side2 ? 1 : 0) - (corner ? 1 : 0); return AO_BRIGHTNESS[Math.clamp(level, 0, 3)]; } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/DefaultScreenRenderer.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/DefaultScreenRenderer.java index 5e5d8d5..c1a9dbd 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/DefaultScreenRenderer.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/DefaultScreenRenderer.java @@ -2,13 +2,13 @@ package eu.mhsl.minecraft.pixelpics.render.render; import eu.mhsl.minecraft.pixelpics.assets.BlockModelRegistry; import eu.mhsl.minecraft.pixelpics.assets.TextureCache; -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.BlockEntityState; import eu.mhsl.minecraft.pixelpics.render.entity.DecorationBaker; import eu.mhsl.minecraft.pixelpics.render.entity.DecorationState; import eu.mhsl.minecraft.pixelpics.render.entity.EntityScene; import eu.mhsl.minecraft.pixelpics.render.entity.EntityState; +import eu.mhsl.minecraft.pixelpics.render.entity.cem.BlockEntityBaker; +import eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker; import eu.mhsl.minecraft.pixelpics.render.raytrace.SnapshotRaytracer; import eu.mhsl.minecraft.pixelpics.render.sky.SkyContext; import eu.mhsl.minecraft.pixelpics.render.sky.SkyRenderer; @@ -20,20 +20,18 @@ import eu.mhsl.minecraft.pixelpics.render.snapshot.WorldSnapshot; import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider; import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil; import eu.mhsl.minecraft.pixelpics.render.util.MathUtil; -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.util.Vector; - -import java.util.UUID; - import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; import java.util.ArrayList; import java.util.List; +import java.util.UUID; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Logger; import java.util.stream.IntStream; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.util.Vector; /** * Renders the scene by capturing a world snapshot on the main thread ({@link #prepare}) and then @@ -43,6 +41,7 @@ public class DefaultScreenRenderer implements Renderer { /** Horizontal half field-of-view; the vertical half is derived from the output aspect ratio. */ private static final double H_FOV_HALF_RAD = Math.toRadians(35); + private static final Vector BASE_VEC = new Vector(1, 0, 0); private static final double MAX_DISTANCE = 256; @@ -59,15 +58,24 @@ public class DefaultScreenRenderer implements Renderer { /** Bounds parallel ray tracing to a fixed, low-priority pool; {@code null} = use the common pool. */ private final ForkJoinPool tracePool; - public DefaultScreenRenderer(BlockModelRegistry registry, BiomeTintProvider tintProvider, - TextureCache textures, CemBaker entityBaker, - BlockEntityBaker blockEntityBaker, Logger logger) { + public DefaultScreenRenderer( + BlockModelRegistry registry, + BiomeTintProvider tintProvider, + TextureCache textures, + CemBaker entityBaker, + BlockEntityBaker blockEntityBaker, + Logger logger) { this(registry, tintProvider, textures, entityBaker, blockEntityBaker, logger, null); } - public DefaultScreenRenderer(BlockModelRegistry registry, BiomeTintProvider tintProvider, - TextureCache textures, CemBaker entityBaker, - BlockEntityBaker blockEntityBaker, Logger logger, ForkJoinPool tracePool) { + public DefaultScreenRenderer( + BlockModelRegistry registry, + BiomeTintProvider tintProvider, + TextureCache textures, + CemBaker entityBaker, + BlockEntityBaker blockEntityBaker, + Logger logger, + ForkJoinPool tracePool) { SkyRenderer skyRenderer = new SkyRenderer(textures); this.raytracer = new SnapshotRaytracer(registry, tintProvider, skyRenderer, MAX_DISTANCE, REFLECTION_DEPTH); this.entityBaker = entityBaker; @@ -99,8 +107,16 @@ public class DefaultScreenRenderer implements Renderer { int moonPhase = (int) (fullTime / 24000L % 8L); SkyContext sky = new SkyContext(dayTime, moonPhase, fullTime); - return new RenderJob(snapshot, rayMap, eyeLocation.toVector(), - resolution.getWidth(), resolution.getHeight(), sky, entities, blockEntities, decorations); + return new RenderJob( + snapshot, + rayMap, + eyeLocation.toVector(), + resolution.getWidth(), + resolution.getHeight(), + sky, + entities, + blockEntities, + decorations); } /** Traces every (super)ray in parallel, then downsamples gamma-correctly. Safe off the main thread. */ @@ -121,8 +137,8 @@ public class DefaultScreenRenderer implements Renderer { WorldSnapshot snapshot = job.snapshot(); Vector origin = job.origin(); SkyContext sky = job.sky(); - EntityScene scene = new EntityScene(job.entities(), entityBaker, job.blockEntities(), blockEntityBaker, - job.decorations(), decorationBaker); + EntityScene scene = new EntityScene( + job.entities(), entityBaker, job.blockEntities(), blockEntityBaker, job.decorations(), decorationBaker); int[] superBuf = new int[rayMap.size()]; runParallel(() -> IntStream.range(0, rayMap.size()).parallel().forEach(i -> { @@ -183,15 +199,19 @@ public class DefaultScreenRenderer implements Renderer { List rayMap = new ArrayList<>(width * height); Vector leftFraction = upperLeftCorner.clone().subtract(lowerLeftCorner).multiply(1.0 / (height - 1)); - Vector rightFraction = upperRightCorner.clone().subtract(lowerRightCorner).multiply(1.0 / (height - 1)); + Vector rightFraction = + upperRightCorner.clone().subtract(lowerRightCorner).multiply(1.0 / (height - 1)); for (int pitch = 0; pitch < height; pitch++) { - Vector leftPitch = upperLeftCorner.clone().subtract(leftFraction.clone().multiply(pitch)); - Vector rightPitch = upperRightCorner.clone().subtract(rightFraction.clone().multiply(pitch)); + Vector leftPitch = + upperLeftCorner.clone().subtract(leftFraction.clone().multiply(pitch)); + Vector rightPitch = + upperRightCorner.clone().subtract(rightFraction.clone().multiply(pitch)); Vector yawFraction = rightPitch.clone().subtract(leftPitch).multiply(1.0 / (width - 1)); for (int yaw = 0; yaw < width; yaw++) { - Vector ray = leftPitch.clone().add(yawFraction.clone().multiply(yaw)).normalize(); + Vector ray = + leftPitch.clone().add(yawFraction.clone().multiply(yaw)).normalize(); rayMap.add(ray); } } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/RenderJob.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/RenderJob.java index 3304a9f..ece71d5 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/RenderJob.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/RenderJob.java @@ -5,29 +5,47 @@ import eu.mhsl.minecraft.pixelpics.render.entity.DecorationState; import eu.mhsl.minecraft.pixelpics.render.entity.EntityState; import eu.mhsl.minecraft.pixelpics.render.sky.SkyContext; import eu.mhsl.minecraft.pixelpics.render.snapshot.WorldSnapshot; -import org.bukkit.util.Vector; - import java.util.List; +import org.bukkit.util.Vector; /** * A prepared render: the world snapshot (captured on the main thread) plus the ray map, camera origin, * the sky context (time of day / moon phase) and the captured entity, block-entity and decoration * (painting / item frame) states. {@link DefaultScreenRenderer#execute} can run this off the main thread. */ -public record RenderJob(WorldSnapshot snapshot, List rayMap, Vector origin, - int width, int height, SkyContext sky, List entities, - List blockEntities, List decorations) { +public record RenderJob( + WorldSnapshot snapshot, + List rayMap, + Vector origin, + int width, + int height, + SkyContext sky, + List entities, + List blockEntities, + List decorations) { /** Backwards-compatible constructor (no block-entities/decorations), used by the standalone harness. */ - public RenderJob(WorldSnapshot snapshot, List rayMap, Vector origin, - int width, int height, SkyContext sky, List entities) { + public RenderJob( + WorldSnapshot snapshot, + List rayMap, + Vector origin, + int width, + int height, + SkyContext sky, + List entities) { this(snapshot, rayMap, origin, width, height, sky, entities, List.of(), List.of()); } /** Convenience for callers that supply entities + block-entities but no decorations. */ - public RenderJob(WorldSnapshot snapshot, List rayMap, Vector origin, - int width, int height, SkyContext sky, List entities, - List blockEntities) { + public RenderJob( + WorldSnapshot snapshot, + List rayMap, + Vector origin, + int width, + int height, + SkyContext sky, + List entities, + List blockEntities) { this(snapshot, rayMap, origin, width, height, sky, entities, blockEntities, List.of()); } } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/Renderer.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/Renderer.java index f941376..018f3c5 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/Renderer.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/Renderer.java @@ -1,8 +1,7 @@ package eu.mhsl.minecraft.pixelpics.render.render; -import org.bukkit.Location; - import java.awt.image.BufferedImage; +import org.bukkit.Location; public interface Renderer { BufferedImage render(Location eyeLocation, Resolution resolution); diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/Resolution.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/Resolution.java index c8c7acf..9d2b7c9 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/Resolution.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/render/Resolution.java @@ -9,8 +9,9 @@ public final class Resolution { public Resolution(Pixels pixels, AspectRatio aspectRatio) { this( - (int) Math.round(Preconditions.checkNotNull(pixels).height * Preconditions.checkNotNull(aspectRatio).ratio), - pixels.height); + (int) Math.round( + Preconditions.checkNotNull(pixels).height * Preconditions.checkNotNull(aspectRatio).ratio), + pixels.height); } public Resolution(int width, int height) { diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/sky/SkyContext.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/sky/SkyContext.java index 7680428..a4d02db 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/sky/SkyContext.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/sky/SkyContext.java @@ -5,5 +5,4 @@ package eu.mhsl.minecraft.pixelpics.render.sky; * (0..7) and the absolute world time (for continuous cloud drift). Immutable so it can be read from * worker threads. */ -public record SkyContext(long dayTime, int moonPhase, long fullTime) { -} +public record SkyContext(long dayTime, int moonPhase, long fullTime) {} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/sky/SkyRenderer.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/sky/SkyRenderer.java index fd0ce29..fb7bdf1 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/sky/SkyRenderer.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/sky/SkyRenderer.java @@ -14,10 +14,10 @@ public final class SkyRenderer { private static final double TICKS_PER_DAY = 24000.0; private static final double CLOUD_HEIGHT = 192.0; - private static final double CLOUD_CELL = 12.0; // world blocks per cloud texel - private static final double CLOUD_SPEED = 0.03; // blocks per tick, drift along +X + private static final double CLOUD_CELL = 12.0; // world blocks per cloud texel + private static final double CLOUD_SPEED = 0.03; // blocks per tick, drift along +X - private static final double SUN_HALF = 0.085; // angular half-size (radians) + private static final double SUN_HALF = 0.085; // angular half-size (radians) private static final double MOON_HALF = 0.075; // Gradient endpoints (RGB). @@ -34,16 +34,21 @@ public final class SkyRenderer { private final int[][] cloudTexture; public SkyRenderer(TextureCache textures) { - this.sunTexture = textures.get(ResourceLocation.parse("environment/sun")).orElse(null); - this.moonTexture = textures.get(ResourceLocation.parse("environment/moon_phases")).orElse(null); - this.cloudTexture = textures.get(ResourceLocation.parse("environment/clouds")).orElse(null); + this.sunTexture = + textures.get(ResourceLocation.parse("environment/sun")).orElse(null); + this.moonTexture = + textures.get(ResourceLocation.parse("environment/moon_phases")).orElse(null); + this.cloudTexture = + textures.get(ResourceLocation.parse("environment/clouds")).orElse(null); } public int colorFor(Vector direction, Vector origin, SkyContext ctx) { double dx = direction.getX(), dy = direction.getY(), dz = direction.getZ(); double len = Math.sqrt(dx * dx + dy * dy + dz * dz); if (len < 1e-9) return DAY_ZENITH; - dx /= len; dy /= len; dz /= len; + dx /= len; + dy /= len; + dz /= len; // Sun/moon position, derived exactly from Minecraft's sky transforms: // celestialAngle ca = getTimeOfDay(dayTime); the sun is rotated by ca*360deg about the X axis @@ -65,8 +70,8 @@ public final class SkyRenderer { if (twilight > 0) { double az = Math.clamp(dx * Math.signum(sunX) * 0.5 + 0.5, 0, 1); // 1 toward sun .. 0 away int grad = up < 0.40 - ? lerp(SUNSET_ORANGE, SUNSET_RED, up / 0.40) - : lerp(SUNSET_RED, TWI_PURPLE, (up - 0.40) / 0.60); + ? lerp(SUNSET_ORANGE, SUNSET_RED, up / 0.40) + : lerp(SUNSET_RED, TWI_PURPLE, (up - 0.40) / 0.60); int twiColor = lerp(lerp(TWI_PURPLE, grad, 0.55), grad, az); // cooler away from the sun color = lerp(color, twiColor, twilight * 0.85); } @@ -98,7 +103,8 @@ public final class SkyRenderer { } // Moon disc (phase shape from the texture's alpha). if (-sunY > -0.15) { - color = overlayDisc(color, dx, dy, dz, -sunX, -sunY, 0, MOON_HALF, moonTexture, rgb(228, 228, 238), ctx.moonPhase()); + color = overlayDisc( + color, dx, dy, dz, -sunX, -sunY, 0, MOON_HALF, moonTexture, rgb(228, 228, 238), ctx.moonPhase()); } // Cloud layer: the ray crosses the cloud plane at y = CLOUD_HEIGHT; the world hit point is @@ -121,8 +127,18 @@ public final class SkyRenderer { } /** Draws a sun/moon disc, sampling a texture when available (moonPhase ≥ 0 picks the phase tile). */ - private int overlayDisc(int base, double dx, double dy, double dz, - double cx, double cy, double cz, double half, int[][] texture, int solid, int moonPhase) { + private int overlayDisc( + int base, + double dx, + double dy, + double dz, + double cx, + double cy, + double cz, + double half, + int[][] texture, + int solid, + int moonPhase) { double cos = dx * cx + dy * cy + dz * cz; if (cos <= 0) return base; double sinHalf = Math.sin(half); @@ -130,8 +146,15 @@ public final class SkyRenderer { // right = normalize(body x worldUp); discUp = right x body double crx = cz, cry = 0, crz = -cx; double crl = Math.sqrt(crx * crx + crz * crz); - if (crl < 1e-6) { crx = 1; cry = 0; crz = 0; crl = 1; } - crx /= crl; cry /= crl; crz /= crl; + if (crl < 1e-6) { + crx = 1; + cry = 0; + crz = 0; + crl = 1; + } + crx /= crl; + cry /= crl; + crz /= crl; // discUp = right cross body double ux = cry * cz - crz * cy; double uy = crz * cx - crx * cz; @@ -201,8 +224,7 @@ public final class SkyRenderer { return alpha > 16 ? 0.85 : 0.0; } double scale = 0.012; - double n = valueNoise(x * scale, z * scale) * 0.6 - + valueNoise(x * scale * 2.3, z * scale * 2.3) * 0.4; + double n = valueNoise(x * scale, z * scale) * 0.6 + valueNoise(x * scale * 2.3, z * scale * 2.3) * 0.4; return smoothstep(0.52, 0.72, n) * 0.8; } @@ -230,7 +252,9 @@ public final class SkyRenderer { // --- small color/math helpers --- - private static int rgb(int r, int g, int b) { return (r << 16) | (g << 8) | b; } + private static int rgb(int r, int g, int b) { + return (r << 16) | (g << 8) | b; + } private static int lerp(int a, int b, double t) { t = Math.clamp(t, 0, 1); @@ -260,5 +284,7 @@ public final class SkyRenderer { return t * t * (3 - 2 * t); } - private static int clamp(int v, int lo, int hi) { return v < lo ? lo : Math.min(v, hi); } + private static int clamp(int v, int lo, int hi) { + return v < lo ? lo : Math.min(v, hi); + } } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/BlockEntitySnapshotBuilder.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/BlockEntitySnapshotBuilder.java index 70f61bc..5fa8f72 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/BlockEntitySnapshotBuilder.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/BlockEntitySnapshotBuilder.java @@ -7,6 +7,11 @@ import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.BellAttach; import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.ChestKind; import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.Kind; import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; import org.bukkit.DyeColor; import org.bukkit.Location; import org.bukkit.Material; @@ -29,12 +34,6 @@ 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; - /** * Captures block-entities (chests, signs, banners, beds, heads, …) near the view frustum into immutable * {@link BlockEntityState}s. MUST run on the main thread (live {@link BlockState} access). Reads each @@ -82,15 +81,15 @@ public final class BlockEntitySnapshotBuilder { // --- chests --- if (mat == Material.CHEST || mat == Material.TRAPPED_CHEST || mat == Material.ENDER_CHEST) { - Kind kind = mat == Material.TRAPPED_CHEST ? Kind.TRAPPED_CHEST - : mat == Material.ENDER_CHEST ? Kind.ENDER_CHEST : Kind.CHEST; + 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 Chest cd) { ck = switch (cd.getType()) { case LEFT -> ChestKind.LEFT; case RIGHT -> ChestKind.RIGHT; - case SINGLE -> ChestKind.SINGLE; - }; + case SINGLE -> ChestKind.SINGLE;}; } return base(kind, bx, by, bz, facingYaw(data)).chestKind(ck).build(); } @@ -99,13 +98,17 @@ public final class BlockEntitySnapshotBuilder { 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(); + .bedPart(part) + .colorName(stripColor(n, "_BED")) + .build(); } // --- shulker boxes --- if (n.endsWith("SHULKER_BOX")) { String color = n.equals("SHULKER_BOX") ? null : stripColor(n, "_SHULKER_BOX"); - return base(Kind.SHULKER_BOX, bx, by, bz, facingYaw(data)).colorName(color).build(); + return base(Kind.SHULKER_BOX, bx, by, bz, facingYaw(data)) + .colorName(color) + .build(); } // --- banners --- @@ -131,12 +134,14 @@ public final class BlockEntitySnapshotBuilder { Kind kind; float yaw; if (n.endsWith("_WALL_SIGN")) { - kind = Kind.WALL_SIGN; yaw = facingYaw(data); + kind = Kind.WALL_SIGN; + yaw = facingYaw(data); } else if (n.endsWith("_HANGING_SIGN")) { kind = Kind.HANGING_SIGN; yaw = n.endsWith("_WALL_HANGING_SIGN") ? facingYaw(data) : rotationYaw(data); } else { - kind = Kind.SIGN; yaw = rotationYaw(data); + kind = Kind.SIGN; + yaw = rotationYaw(data); } Builder b = base(kind, bx, by, bz, yaw).wood(wood); if (ts instanceof Sign sign) { @@ -178,10 +183,11 @@ public final class BlockEntitySnapshotBuilder { case FLOOR -> BellAttach.FLOOR; case CEILING -> BellAttach.CEILING; case SINGLE_WALL -> BellAttach.SINGLE_WALL; - case DOUBLE_WALL -> BellAttach.DOUBLE_WALL; - }; + case DOUBLE_WALL -> BellAttach.DOUBLE_WALL;}; } - return base(Kind.BELL, bx, by, bz, facingYaw(data)).bellAttach(attach).build(); + return base(Kind.BELL, bx, by, bz, facingYaw(data)) + .bellAttach(attach) + .build(); } return null; @@ -250,14 +256,17 @@ public final class BlockEntitySnapshotBuilder { if (!any) return null; DyeColor dye = side.getColor(); boolean glow = side.isGlowingText(); - return new BlockEntityState.SignText(lines, ColorUtil.signFillArgb(dye, glow), - ColorUtil.signOutlineArgb(dye), glow); + return new BlockEntityState.SignText( + lines, ColorUtil.signFillArgb(dye, glow), ColorUtil.signOutlineArgb(dye), glow); } private static String signWood(String name) { String s = name; - 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; } + 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(Locale.ROOT); } @@ -278,8 +287,9 @@ public final class BlockEntitySnapshotBuilder { private static List sherds(DecoratedPot pot) { // Order: front, left, right, back — matches the CEM decorated_pot face parts. List out = new ArrayList<>(4); - for (DecoratedPot.Side side : new DecoratedPot.Side[]{ - DecoratedPot.Side.FRONT, DecoratedPot.Side.LEFT, DecoratedPot.Side.RIGHT, DecoratedPot.Side.BACK}) { + 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(Locale.ROOT)); } @@ -328,25 +338,92 @@ public final class BlockEntitySnapshotBuilder { private BlockEntityState.SignText backText; Builder(Kind kind, int bx, int by, int bz, float yaw) { - this.kind = kind; this.bx = bx; this.by = by; this.bz = bz; this.yaw = yaw; + this.kind = kind; + this.bx = bx; + this.by = by; + this.bz = bz; + this.yaw = yaw; } - Builder chestKind(ChestKind v) { this.chestKind = v; return this; } - Builder baseColorArgb(int v) { this.baseColorArgb = v; return this; } - Builder colorName(String v) { this.colorName = v; return this; } - Builder wood(String v) { this.wood = v; return this; } - Builder bedPart(BedPart v) { this.bedPart = v; return this; } - Builder headType(String v) { this.headType = v; return this; } - Builder skinUrl(String v) { this.skinUrl = v; return this; } - Builder patterns(List v) { this.patterns = v; return this; } - Builder sherds(List v) { this.sherds = v; return this; } - Builder bellAttach(BellAttach v) { this.bellAttach = v; return this; } - Builder frontText(BlockEntityState.SignText v) { this.frontText = v; return this; } - Builder backText(BlockEntityState.SignText v) { this.backText = v; return this; } + Builder chestKind(ChestKind v) { + this.chestKind = v; + return this; + } + + Builder baseColorArgb(int v) { + this.baseColorArgb = v; + return this; + } + + Builder colorName(String v) { + this.colorName = v; + return this; + } + + Builder wood(String v) { + this.wood = v; + return this; + } + + Builder bedPart(BedPart v) { + this.bedPart = v; + return this; + } + + Builder headType(String v) { + this.headType = v; + return this; + } + + Builder skinUrl(String v) { + this.skinUrl = v; + return this; + } + + Builder patterns(List v) { + this.patterns = v; + return this; + } + + Builder sherds(List v) { + this.sherds = v; + return this; + } + + Builder bellAttach(BellAttach v) { + this.bellAttach = v; + return this; + } + + Builder frontText(BlockEntityState.SignText v) { + this.frontText = v; + return this; + } + + Builder backText(BlockEntityState.SignText v) { + this.backText = v; + return this; + } BlockEntityState build() { - return new BlockEntityState(kind, bx, by, bz, yaw, chestKind, baseColorArgb, colorName, wood, - bedPart, headType, skinUrl, patterns, sherds, bellAttach, frontText, backText); + return new BlockEntityState( + kind, + bx, + by, + bz, + yaw, + chestKind, + baseColorArgb, + colorName, + wood, + bedPart, + headType, + skinUrl, + patterns, + sherds, + bellAttach, + frontText, + backText); } } } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/DecorationSnapshotBuilder.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/DecorationSnapshotBuilder.java index 749127b..2bb23e6 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/DecorationSnapshotBuilder.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/DecorationSnapshotBuilder.java @@ -1,6 +1,9 @@ package eu.mhsl.minecraft.pixelpics.render.snapshot; import eu.mhsl.minecraft.pixelpics.render.entity.DecorationState; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; import org.bukkit.Location; import org.bukkit.block.BlockFace; import org.bukkit.entity.Entity; @@ -11,10 +14,6 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.util.BoundingBox; import org.bukkit.util.Vector; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - /** * Captures flat wall decorations (paintings + item frames) near the view frustum into immutable * {@link DecorationState}s. MUST run on the main thread. Uses each entity's world bounding box directly, @@ -42,19 +41,39 @@ public final class DecorationSnapshotBuilder { if (e instanceof Painting painting) { BoundingBox bb = e.getBoundingBox(); String art = painting.getArt().assetId().value(); - return new DecorationState(DecorationState.Kind.PAINTING, - bb.getMinX(), bb.getMinY(), bb.getMinZ(), bb.getMaxX(), bb.getMaxY(), bb.getMaxZ(), - facing(e.getFacing()), art, null, 0, false); + return new DecorationState( + DecorationState.Kind.PAINTING, + bb.getMinX(), + bb.getMinY(), + bb.getMinZ(), + bb.getMaxX(), + bb.getMaxY(), + bb.getMaxZ(), + facing(e.getFacing()), + art, + null, + 0, + false); } if (e instanceof ItemFrame frame) { BoundingBox bb = e.getBoundingBox(); - boolean glow = e instanceof GlowItemFrame - || e.getType().getKey().getKey().equals("glow_item_frame"); + boolean glow = + e instanceof GlowItemFrame || e.getType().getKey().getKey().equals("glow_item_frame"); String itemId = itemId(frame.getItem()); int rot = frame.getRotation().ordinal() * 45; - return new DecorationState(DecorationState.Kind.ITEM_FRAME, - bb.getMinX(), bb.getMinY(), bb.getMinZ(), bb.getMaxX(), bb.getMaxY(), bb.getMaxZ(), - facing(e.getFacing()), null, itemId, rot, glow); + return new DecorationState( + DecorationState.Kind.ITEM_FRAME, + bb.getMinX(), + bb.getMinY(), + bb.getMinZ(), + bb.getMaxX(), + bb.getMaxY(), + bb.getMaxZ(), + facing(e.getFacing()), + null, + itemId, + rot, + glow); } return null; } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/EntitySnapshotBuilder.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/EntitySnapshotBuilder.java index 980dc6c..0546baf 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/EntitySnapshotBuilder.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/EntitySnapshotBuilder.java @@ -6,6 +6,15 @@ 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 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; import org.bukkit.Keyed; import org.bukkit.Location; import org.bukkit.Material; @@ -47,16 +56,6 @@ 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 * thread (live entity access). The camera entity is skipped. @@ -68,22 +67,46 @@ 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 Set NON_RENDERABLE = Set.of( - "area_effect_cloud", "marker", "interaction", - "item_frame", "glow_item_frame", "painting", - "block_display", "item_display", "text_display", - "fishing_bobber", "lightning_bolt", "eye_of_ender", - "experience_orb", "experience_bottle", "egg", "snowball", - "potion", "ender_pearl", "tnt", "falling_block", "item" - ); + "area_effect_cloud", + "marker", + "interaction", + "item_frame", + "glow_item_frame", + "painting", + "block_display", + "item_display", + "text_display", + "fishing_bobber", + "lightning_bolt", + "eye_of_ender", + "experience_orb", + "experience_bottle", + "egg", + "snowball", + "potion", + "ender_pearl", + "tnt", + "falling_block", + "item"); // 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 Set HUMANOID_ARMOR_WEARERS = Set.of( - "player", "mannequin", "armor_stand", "giant", - "zombie", "husk", "drowned", "zombie_villager", "zombified_piglin", - "skeleton", "stray", "wither_skeleton", "bogged", - "piglin", "piglin_brute" - ); + "player", + "mannequin", + "armor_stand", + "giant", + "zombie", + "husk", + "drowned", + "zombie_villager", + "zombified_piglin", + "skeleton", + "stray", + "wither_skeleton", + "bogged", + "piglin", + "piglin_brute"); public static List build(Location eye, List rayMap, double maxDistance, UUID shooter) { FrustumBounds bounds = FrustumBounds.of(eye.toVector(), rayMap, maxDistance); @@ -109,14 +132,13 @@ public final class EntitySnapshotBuilder { bodyYaw = le.getBodyYaw(); } - boolean baby = (e instanceof Ageable a && !a.isAdult()) - || (e instanceof Zombie z && z.isAdult()); + boolean baby = (e instanceof Ageable a && !a.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 ArmorStand as && !as.isVisible()) - || (e instanceof LivingEntity inv && inv.hasPotionEffect(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()); @@ -211,10 +233,29 @@ public final class EntitySnapshotBuilder { equipment = captureEquipment(wearer); } - return new EntityState(type, loc.getX(), loc.getY(), loc.getZ(), - bodyYaw, baby, width, height, - player, skinUrl, slim, variant, tint, sizeScale, profession, villagerLevel, - markings, saddle, chest, bodyEquip, equipment, invisible); + return new EntityState( + type, + loc.getX(), + loc.getY(), + loc.getZ(), + bodyYaw, + baby, + width, + height, + player, + skinUrl, + slim, + variant, + tint, + sizeScale, + profession, + villagerLevel, + markings, + saddle, + chest, + bodyEquip, + equipment, + invisible); } /** Worn armor (4 slots) from a humanoid wearer; null when nothing is equipped. */ @@ -223,8 +264,8 @@ public final class EntitySnapshotBuilder { EntityEquipment eq = le.getEquipment(); if (eq == null) return null; EntityState.Equipment equip = new EntityState.Equipment( - armorPiece(eq.getHelmet()), armorPiece(eq.getChestplate()), - armorPiece(eq.getLeggings()), armorPiece(eq.getBoots())); + armorPiece(eq.getHelmet()), armorPiece(eq.getChestplate()), + armorPiece(eq.getLeggings()), armorPiece(eq.getBoots())); return equip.isEmpty() ? null : equip; } catch (Throwable t) { return null; @@ -257,8 +298,11 @@ public final class EntitySnapshotBuilder { if (key.equals("elytra")) return "elytra"; if (key.equals("turtle_helmet")) return "turtle_scute"; String base = null; - for (String suf : new String[]{"_helmet", "_chestplate", "_leggings", "_boots"}) { - if (key.endsWith(suf)) { base = key.substring(0, key.length() - suf.length()); break; } + for (String suf : new String[] {"_helmet", "_chestplate", "_leggings", "_boots"}) { + if (key.endsWith(suf)) { + base = key.substring(0, key.length() - suf.length()); + break; + } } if (base == null) return null; return base.equals("golden") ? "gold" : base; @@ -305,7 +349,7 @@ public final class EntitySnapshotBuilder { /** Registry/Keyed values yield their key path; plain enums yield their lower-case name. */ private static String keyOf(Object o) { - return switch(o) { + return switch (o) { case null -> null; case Keyed k -> k.getKey().getKey(); case Enum en -> en.name().toLowerCase(Locale.ROOT); @@ -318,8 +362,7 @@ public final class EntitySnapshotBuilder { try { for (ProfileProperty prop : player.getPlayerProfile().getProperties()) { if (!prop.getName().equals("textures")) continue; - String json = new String(Base64.getDecoder().decode(prop.getValue()), - StandardCharsets.UTF_8); + 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(); @@ -327,11 +370,11 @@ public final class EntitySnapshotBuilder { if (skin.has("metadata") && skin.getAsJsonObject("metadata").has("model")) { model = skin.getAsJsonObject("metadata").get("model").getAsString(); } - return new String[]{url, model}; + return new String[] {url, model}; } } catch (Exception ignored) { } - return new String[]{null, null}; + return new String[] {null, null}; } /** Reads a dimension via {@code primary}, falling back to {@code fallback} on any version mismatch. */ diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/FrustumBounds.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/FrustumBounds.java index a04d2a7..3b3902e 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/FrustumBounds.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/FrustumBounds.java @@ -1,13 +1,12 @@ package eu.mhsl.minecraft.pixelpics.render.snapshot; +import java.util.Collection; +import java.util.List; import org.bukkit.Location; import org.bukkit.World; import org.bukkit.entity.Entity; import org.bukkit.util.Vector; -import java.util.Collection; -import java.util.List; - /** * Axis-aligned world-space bounds of the camera frustum: the min/max corner of the camera origin * together with every ray endpoint at {@code maxDistance}. Shared by all snapshot builders to size @@ -19,8 +18,12 @@ final class FrustumBounds { final double maxX, maxY, maxZ; private FrustumBounds(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { - this.minX = minX; this.minY = minY; this.minZ = minZ; - this.maxX = maxX; this.maxY = maxY; this.maxZ = maxZ; + this.minX = minX; + this.minY = minY; + this.minZ = minZ; + this.maxX = maxX; + this.maxY = maxY; + this.maxZ = maxZ; } static FrustumBounds of(Vector origin, List rayMap, double maxDistance) { @@ -30,9 +33,12 @@ final class FrustumBounds { double fx = origin.getX() + ray.getX() * maxDistance; double fy = origin.getY() + ray.getY() * maxDistance; double fz = origin.getZ() + ray.getZ() * maxDistance; - minX = Math.min(minX, fx); maxX = Math.max(maxX, fx); - minY = Math.min(minY, fy); maxY = Math.max(maxY, fy); - minZ = Math.min(minZ, fz); maxZ = Math.max(maxZ, fz); + minX = Math.min(minX, fx); + maxX = Math.max(maxX, fx); + minY = Math.min(minY, fy); + maxY = Math.max(maxY, fy); + minZ = Math.min(minZ, fz); + maxZ = Math.max(maxZ, fz); } return new FrustumBounds(minX, minY, minZ, maxX, maxY, maxZ); } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/SnapshotBuilder.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/SnapshotBuilder.java index 3f75106..c94fd30 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/SnapshotBuilder.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/SnapshotBuilder.java @@ -1,15 +1,14 @@ package eu.mhsl.minecraft.pixelpics.render.snapshot; -import org.bukkit.ChunkSnapshot; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.World; -import org.bukkit.util.Vector; - import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Logger; +import org.bukkit.ChunkSnapshot; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.util.Vector; /** * Captures the world region covered by the camera frustum into a {@link WorldSnapshot}. @@ -58,7 +57,8 @@ public final class SnapshotBuilder { } if (skipped > 0) { - logger.fine(String.format("Snapshot captured %d chunks, skipped %d (unloaded or over cap)", captured, skipped)); + logger.fine( + String.format("Snapshot captured %d chunks, skipped %d (unloaded or over cap)", captured, skipped)); } return new WorldSnapshot(chunks, clampedMinY, clampedMaxY, Material.AIR.createBlockData()); diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/WorldSnapshot.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/WorldSnapshot.java index 257b6f5..849c927 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/WorldSnapshot.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/WorldSnapshot.java @@ -1,11 +1,10 @@ package eu.mhsl.minecraft.pixelpics.render.snapshot; +import java.util.Map; import org.bukkit.ChunkSnapshot; import org.bukkit.block.Biome; import org.bukkit.block.data.BlockData; -import java.util.Map; - /** * An immutable, thread-safe view of a bounded region of the world, backed by {@link ChunkSnapshot}s. * Block/biome lookups outside the captured region return air/null so rays simply terminate there. @@ -42,6 +41,11 @@ public final class WorldSnapshot { return cs.getBiome(x & 15, y, z & 15); } - public int minY() { return minY; } - public int maxY() { return maxY; } + public int minY() { + return minY; + } + + public int maxY() { + return maxY; + } } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/BiomeClimate.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/BiomeClimate.java index 69f51a6..0905b5f 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/BiomeClimate.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/BiomeClimate.java @@ -17,8 +17,13 @@ public final class BiomeClimate { private static final Map TABLE = new HashMap<>(); - private static void put(String key, double t, double d) { TABLE.put(key, new Climate(t, d, DEFAULT_WATER)); } - private static void put(String key, double t, double d, int water) { TABLE.put(key, new Climate(t, d, water)); } + private static void put(String key, double t, double d) { + TABLE.put(key, new Climate(t, d, DEFAULT_WATER)); + } + + private static void put(String key, double t, double d, int water) { + TABLE.put(key, new Climate(t, d, water)); + } static { put("plains", 0.8, 0.4); diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/BiomeTint.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/BiomeTint.java index 5990545..f54b463 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/BiomeTint.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/BiomeTint.java @@ -3,5 +3,4 @@ package eu.mhsl.minecraft.pixelpics.render.tint; /** * The biome-dependent tint colors (RGB) for the colormap-driven channels. */ -public record BiomeTint(int grass, int foliage, int dryFoliage, int water) { -} +public record BiomeTint(int grass, int foliage, int dryFoliage, int water) {} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/BiomeTintProvider.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/BiomeTintProvider.java index 6c36f9f..ca8d206 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/BiomeTintProvider.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/BiomeTintProvider.java @@ -2,10 +2,9 @@ package eu.mhsl.minecraft.pixelpics.render.tint; import eu.mhsl.minecraft.pixelpics.assets.ResourceLocation; import eu.mhsl.minecraft.pixelpics.assets.TextureCache; -import org.bukkit.block.Biome; - import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.bukkit.block.Biome; /** * Computes per-biome grass/foliage tint colors by sampling the resource pack's colormaps using the @@ -20,8 +19,10 @@ public final class BiomeTintProvider { public BiomeTintProvider(TextureCache textures) { this.grassMap = textures.get(ResourceLocation.parse("colormap/grass")).orElse(null); - this.foliageMap = textures.get(ResourceLocation.parse("colormap/foliage")).orElse(null); - this.dryFoliageMap = textures.get(ResourceLocation.parse("colormap/dry_foliage")).orElse(null); + this.foliageMap = + textures.get(ResourceLocation.parse("colormap/foliage")).orElse(null); + this.dryFoliageMap = + textures.get(ResourceLocation.parse("colormap/dry_foliage")).orElse(null); } public BiomeTint forBiome(Biome biome) { @@ -57,7 +58,7 @@ public final class BiomeTintProvider { grass = 0xFF000000 | (((grass & 0xFEFEFE) + 0x28340A) >> 1); foliage = 0xFF000000 | (((foliage & 0xFEFEFE) + 0x28340A) >> 1); } - default -> { } + default -> {} } return new BiomeTint(grass, foliage, dry, 0xFF000000 | climate.water()); } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/TintResolver.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/TintResolver.java index bc2c2de..d94c61f 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/TintResolver.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/TintResolver.java @@ -32,8 +32,11 @@ public final class TintResolver { if (name.endsWith("stem")) return STEM; // grass_block (top/overlay), short_grass, tall_grass, fern, large_fern, sugar_cane, ... - if (name.contains("grass") || name.equals("fern") || name.equals("large_fern") - || name.equals("sugar_cane") || name.equals("potted_fern")) { + if (name.contains("grass") + || name.equals("fern") + || name.equals("large_fern") + || name.equals("sugar_cane") + || name.equals("potted_fern")) { return biomeTint.grass(); } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/util/ColorUtil.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/util/ColorUtil.java index a027c2d..4a4485a 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/util/ColorUtil.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/util/ColorUtil.java @@ -10,10 +10,21 @@ public final class ColorUtil { private ColorUtil() {} - public static int alpha(int argb) { return (argb >> 24) & 0xFF; } - public static int red(int argb) { return (argb >> 16) & 0xFF; } - public static int green(int argb) { return (argb >> 8) & 0xFF; } - public static int blue(int argb) { return argb & 0xFF; } + public static int alpha(int argb) { + return (argb >> 24) & 0xFF; + } + + public static int red(int argb) { + return (argb >> 16) & 0xFF; + } + + public static int green(int argb) { + return (argb >> 8) & 0xFF; + } + + public static int blue(int argb) { + return argb & 0xFF; + } public static int argb(int a, int r, int g, int b) { return (a << 24) | (r << 16) | (g << 8) | b; @@ -31,24 +42,25 @@ public final class ColorUtil { * table (the firework/text colours, NOT the cloth colours). Null = black (the default sign ink). */ public static int signTextColor(DyeColor dye) { - int rgb = switch (dye == null ? DyeColor.BLACK : dye) { - case WHITE -> 0xF9FFFE; - case ORANGE -> 0xF9801D; - case MAGENTA -> 0xC74EBD; - case LIGHT_BLUE -> 0x3AB3DA; - case YELLOW -> 0xFED83D; - case LIME -> 0x80C71F; - case PINK -> 0xF38BAA; - case GRAY -> 0x474F52; - case LIGHT_GRAY -> 0x9D9D97; - case CYAN -> 0x169C9C; - case PURPLE -> 0x8932B8; - case BLUE -> 0x3C44AA; - case BROWN -> 0x835432; - case GREEN -> 0x5E7C16; - case RED -> 0xB02E26; - case BLACK -> 0x1D1D21; - }; + int rgb = + switch (dye == null ? DyeColor.BLACK : dye) { + case WHITE -> 0xF9FFFE; + case ORANGE -> 0xF9801D; + case MAGENTA -> 0xC74EBD; + case LIGHT_BLUE -> 0x3AB3DA; + case YELLOW -> 0xFED83D; + case LIME -> 0x80C71F; + case PINK -> 0xF38BAA; + case GRAY -> 0x474F52; + case LIGHT_GRAY -> 0x9D9D97; + case CYAN -> 0x169C9C; + case PURPLE -> 0x8932B8; + case BLUE -> 0x3C44AA; + case BROWN -> 0x835432; + case GREEN -> 0x5E7C16; + case RED -> 0xB02E26; + case BLACK -> 0x1D1D21; + }; return 0xFF000000 | rgb; } @@ -108,6 +120,7 @@ public final class ColorUtil { // --- Gamma-correct (linear-light) averaging --- private static final float[] SRGB_TO_LINEAR = new float[256]; + static { for (int i = 0; i < 256; i++) { double c = i / 255.0; diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/util/MathUtil.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/util/MathUtil.java index 54a76f5..b19a6aa 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/util/MathUtil.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/util/MathUtil.java @@ -5,36 +5,36 @@ import org.bukkit.util.Vector; public class MathUtil { - private MathUtil() {} + private MathUtil() {} - private static Vector yawPitchRotation(Vector base, double angleYaw, double anglePitch) { - double oldX = base.getX(); - double oldY = base.getY(); - double oldZ = base.getZ(); + private static Vector yawPitchRotation(Vector base, double angleYaw, double anglePitch) { + double oldX = base.getX(); + double oldY = base.getY(); + double oldZ = base.getZ(); - double sinOne = Math.sin(angleYaw); - double sinTwo = Math.sin(anglePitch); - double cosOne = Math.cos(angleYaw); - double cosTwo = Math.cos(anglePitch); + double sinOne = Math.sin(angleYaw); + double sinTwo = Math.sin(anglePitch); + double cosOne = Math.cos(angleYaw); + double cosTwo = Math.cos(anglePitch); - double newX = oldX * cosOne * cosTwo - oldY * cosOne * sinTwo - oldZ * sinOne; - double newY = oldX * sinTwo + oldY * cosTwo; - double newZ = oldX * sinOne * cosTwo - oldY * sinOne * sinTwo + oldZ * cosOne; + double newX = oldX * cosOne * cosTwo - oldY * cosOne * sinTwo - oldZ * sinOne; + double newY = oldX * sinTwo + oldY * cosTwo; + double newZ = oldX * sinOne * cosTwo - oldY * sinOne * sinTwo + oldZ * cosOne; - return new Vector(newX, newY, newZ); - } + return new Vector(newX, newY, newZ); + } - public static Vector doubleYawPitchRotation(Vector base, double firstYaw, double firstPitch, double secondYaw, - double secondPitch) { - return yawPitchRotation(yawPitchRotation(base, firstYaw, firstPitch), secondYaw, secondPitch); - } + public static Vector doubleYawPitchRotation( + Vector base, double firstYaw, double firstPitch, double secondYaw, double secondPitch) { + return yawPitchRotation(yawPitchRotation(base, firstYaw, firstPitch), secondYaw, secondPitch); + } - /** Reflects {@code direction} across the plane with the given (unit) {@code normal}. */ - public static Vector reflectVector(Vector direction, Vector normal) { - return direction.clone().subtract(normal.clone().multiply(2 * direction.dot(normal))); - } + /** Reflects {@code direction} across the plane with the given (unit) {@code normal}. */ + public static Vector reflectVector(Vector direction, Vector normal) { + return direction.clone().subtract(normal.clone().multiply(2 * direction.dot(normal))); + } - public static Vector toVector(BlockFace face) { - return new Vector(face.getModX(), face.getModY(), face.getModZ()); - } + public static Vector toVector(BlockFace face) { + return new Vector(face.getModX(), face.getModY(), face.getModZ()); + } } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/CameraItems.java b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/CameraItems.java index f0fae7e..092b3b9 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/CameraItems.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/CameraItems.java @@ -3,6 +3,9 @@ package eu.mhsl.minecraft.pixelpics.survival; import com.destroystokyo.paper.profile.PlayerProfile; import com.destroystokyo.paper.profile.ProfileProperty; import eu.mhsl.minecraft.pixelpics.Main; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.UUID; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextDecoration; @@ -17,10 +20,6 @@ import org.bukkit.inventory.meta.components.EquippableComponent; import org.bukkit.persistence.PersistentDataContainer; import org.bukkit.persistence.PersistentDataType; -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.UUID; - /** * Factory and identity helpers for the survival items: the camera and the film roll are custom-skinned * {@code PLAYER_HEAD}s (the texture renders client-side without a resource pack), the photo is the @@ -35,8 +34,10 @@ public final class CameraItems { private static final int CAMERA_MODEL_DATA = 1001; private static final int FILM_MODEL_DATA = 1002; - static final String CAMERA_TEXTURE_B64 = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZDlkMmNiZjAyZDMwOGI2MDY1YTZmZThjNjU3MWI2MzU2NjMzZjQxOTJlOGVjNzEyMTNjNzcwNzgwZTNkZTRlMiJ9fX0="; - static final String FILM_TEXTURE_B64 = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMTVkMGY0OGJlNzNkYmIwZDJjYjE1NTRjMmUzODZiNWNjM2FiMjFhNGRjYWU4ZmYzOGI3NzRhZDNkMDFkMGE1OSJ9fX0="; + static final String CAMERA_TEXTURE_B64 = + "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZDlkMmNiZjAyZDMwOGI2MDY1YTZmZThjNjU3MWI2MzU2NjMzZjQxOTJlOGVjNzEyMTNjNzcwNzgwZTNkZTRlMiJ9fX0="; + static final String FILM_TEXTURE_B64 = + "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMTVkMGY0OGJlNzNkYmIwZDJjYjE1NTRjMmUzODZiNWNjM2FiMjFhNGRjYWU4ZmYzOGI3NzRhZDNkMDFkMGE1OSJ9fX0="; private CameraItems() {} @@ -48,8 +49,7 @@ public final class CameraItems { ItemStack item = new ItemStack(Material.PLAYER_HEAD); SkullMeta meta = (SkullMeta) item.getItemMeta(); applyHead(meta, "pixelpics:camera", CAMERA_TEXTURE_B64); - meta.displayName(Component.text("Kamera", NamedTextColor.AQUA) - .decoration(TextDecoration.ITALIC, false)); + meta.displayName(Component.text("Kamera", NamedTextColor.AQUA).decoration(TextDecoration.ITALIC, false)); meta.setCustomModelData(CAMERA_MODEL_DATA); meta.getPersistentDataContainer().set(Main.getInstance().cameraMarker, PersistentDataType.BYTE, (byte) 1); meta.getPersistentDataContainer().set(Main.getInstance().filmCountKey, PersistentDataType.INTEGER, count); @@ -64,11 +64,10 @@ public final class CameraItems { ItemStack item = new ItemStack(Material.PLAYER_HEAD); SkullMeta meta = (SkullMeta) item.getItemMeta(); applyHead(meta, "pixelpics:film", FILM_TEXTURE_B64); - meta.displayName(Component.text("Filmrolle", NamedTextColor.GREEN) - .decoration(TextDecoration.ITALIC, false)); + meta.displayName(Component.text("Filmrolle", NamedTextColor.GREEN).decoration(TextDecoration.ITALIC, false)); meta.setCustomModelData(FILM_MODEL_DATA); - meta.lore(List.of(Component.text("Lädt eine Kamera auf.", NamedTextColor.GRAY) - .decoration(TextDecoration.ITALIC, false))); + meta.lore(List.of( + Component.text("Lädt eine Kamera auf.", NamedTextColor.GRAY).decoration(TextDecoration.ITALIC, false))); meta.getPersistentDataContainer().set(Main.getInstance().filmMarker, PersistentDataType.BYTE, (byte) 1); makeUnwearable(meta); item.setItemMeta(meta); @@ -99,15 +98,17 @@ public final class CameraItems { /** A photo is a filled map carrying our picture id. */ public static boolean isPhoto(ItemStack item) { if (item == null || item.getType() != Material.FILLED_MAP || !item.hasItemMeta()) return false; - return item.getItemMeta().getPersistentDataContainer() - .has(Main.getInstance().pictureIdFlag, PersistentDataType.STRING); + return item.getItemMeta() + .getPersistentDataContainer() + .has(Main.getInstance().pictureIdFlag, PersistentDataType.STRING); } /** Loaded film on a camera, or 0 if the item is not a camera. */ public static int getFilmCount(ItemStack item) { if (!isCamera(item)) return 0; - Integer v = item.getItemMeta().getPersistentDataContainer() - .get(Main.getInstance().filmCountKey, PersistentDataType.INTEGER); + Integer v = item.getItemMeta() + .getPersistentDataContainer() + .get(Main.getInstance().filmCountKey, PersistentDataType.INTEGER); return v == null ? 0 : v; } @@ -121,11 +122,12 @@ public final class CameraItems { private static void applyCameraLore(ItemMeta meta, int count) { meta.lore(List.of( - Component.text("Film: " + count + " / " + MAX_FILM, - count > 0 ? NamedTextColor.YELLOW : NamedTextColor.RED).decoration(TextDecoration.ITALIC, false), - Component.text("Rechtsklick: Foto aufnehmen", NamedTextColor.DARK_GRAY) - .decoration(TextDecoration.ITALIC, false) - )); + Component.text( + "Film: " + count + " / " + MAX_FILM, + count > 0 ? NamedTextColor.YELLOW : NamedTextColor.RED) + .decoration(TextDecoration.ITALIC, false), + Component.text("Rechtsklick: Foto aufnehmen", NamedTextColor.DARK_GRAY) + .decoration(TextDecoration.ITALIC, false))); } /** diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/CameraListener.java b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/CameraListener.java index bfba90d..90a34fd 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/CameraListener.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/CameraListener.java @@ -1,5 +1,8 @@ package eu.mhsl.minecraft.pixelpics.survival; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Sound; @@ -11,10 +14,6 @@ import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.ItemStack; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - /** * Takes a photo when a player right-clicks while holding a camera. Consumes one loaded film per shot; * with no film loaded it gives a short fail feedback. Guards against the well-known double-fire of diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/CraftingListener.java b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/CraftingListener.java index b12ca7b..6954e64 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/CraftingListener.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/CraftingListener.java @@ -1,6 +1,7 @@ package eu.mhsl.minecraft.pixelpics.survival; import eu.mhsl.minecraft.pixelpics.Main; +import java.util.function.Predicate; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; @@ -19,8 +20,6 @@ import org.bukkit.inventory.Recipe; import org.bukkit.inventory.meta.MapMeta; import org.bukkit.persistence.PersistentDataType; -import java.util.function.Predicate; - /** * Drives the two dynamic recipes whose inputs carry variable NBT: load film * (camera + film → camera+1) and copy photo (photo + film → 2 photos sharing the same map). @@ -31,7 +30,11 @@ import java.util.function.Predicate; */ public class CraftingListener implements Listener { - private enum Kind { LOAD, COPY, NONE } + private enum Kind { + LOAD, + COPY, + NONE + } /** Scanned crafting grid: the relevant items and whether any of our marked items are present. */ private record Scan(Kind kind, ItemStack camera, ItemStack film, ItemStack photo, boolean hasOurItems) {} @@ -44,9 +47,8 @@ public class CraftingListener implements Listener { switch (scan.kind()) { case LOAD -> { int count = CameraItems.getFilmCount(scan.camera()); - inv.setResult(count >= CameraItems.MAX_FILM - ? null - : CameraItems.withFilmCount(scan.camera(), count + 1)); + inv.setResult( + count >= CameraItems.MAX_FILM ? null : CameraItems.withFilmCount(scan.camera(), count + 1)); } case COPY -> inv.setResult(buildPhotoCopy(scan.photo())); case NONE -> { @@ -109,10 +111,16 @@ public class CraftingListener implements Listener { ItemStack camera = null, film = null, photo = null; for (ItemStack it : matrix) { if (it == null || it.getType().isAir()) continue; - if (CameraItems.isCamera(it)) { cameras++; camera = it; } - else if (CameraItems.isFilm(it)) { films++; film = it; } - else if (CameraItems.isPhoto(it)) { photos++; photo = it; } - else others++; + if (CameraItems.isCamera(it)) { + cameras++; + camera = it; + } else if (CameraItems.isFilm(it)) { + films++; + film = it; + } else if (CameraItems.isPhoto(it)) { + photos++; + photo = it; + } else others++; } boolean ours = cameras + films + photos > 0; Kind kind = Kind.NONE; @@ -136,11 +144,9 @@ public class CraftingListener implements Listener { ItemStack copy = new ItemStack(Material.FILLED_MAP); MapMeta dst = (MapMeta) copy.getItemMeta(); dst.setMapView(src.getMapView()); - String pid = src.getPersistentDataContainer() - .get(Main.getInstance().pictureIdFlag, PersistentDataType.STRING); + String pid = src.getPersistentDataContainer().get(Main.getInstance().pictureIdFlag, PersistentDataType.STRING); if (pid != null) { - dst.getPersistentDataContainer() - .set(Main.getInstance().pictureIdFlag, PersistentDataType.STRING, pid); + dst.getPersistentDataContainer().set(Main.getInstance().pictureIdFlag, PersistentDataType.STRING, pid); } copy.setItemMeta(dst); return copy; @@ -152,7 +158,10 @@ public class CraftingListener implements Listener { if (it != null && !it.getType().isAir() && pred.test(it)) { int amt = it.getAmount(); if (amt <= 1) matrix[i] = null; - else { it.setAmount(amt - 1); matrix[i] = it; } + else { + it.setAmount(amt - 1); + matrix[i] = it; + } return; } } @@ -163,8 +172,8 @@ public class CraftingListener implements Listener { if (cursor.getType().isAir()) { player.setItemOnCursor(item); } else { - player.getInventory().addItem(item).values() - .forEach(left -> player.getWorld().dropItemNaturally(player.getLocation(), left)); + player.getInventory().addItem(item).values().forEach(left -> player.getWorld() + .dropItemNaturally(player.getLocation(), left)); } } } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/PhotoService.java b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/PhotoService.java index 00a9938..eae616d 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/PhotoService.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/PhotoService.java @@ -8,6 +8,8 @@ import eu.mhsl.minecraft.pixelpics.render.render.Resolution; import eu.mhsl.minecraft.pixelpics.utils.ImageMapRenderer; import eu.mhsl.minecraft.pixelpics.utils.MapImageDither; import eu.mhsl.minecraft.pixelpics.utils.MapManager; +import java.awt.image.BufferedImage; +import java.util.UUID; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; @@ -22,9 +24,6 @@ import org.bukkit.map.MapView; import org.bukkit.persistence.PersistentDataType; import org.bukkit.util.Vector; -import java.awt.image.BufferedImage; -import java.util.UUID; - /** * Renders a photo of a player's current view and delivers it as a developing {@code FILLED_MAP}, * with a shutter sound + flash particles on capture and a chime when the picture finishes developing. @@ -47,8 +46,8 @@ public final class PhotoService { Main plugin = Main.getInstance(); DefaultScreenRenderer renderer = plugin.getScreenRenderer(); if (renderer == null) { - player.sendActionBar(Component.text("PixelPics ist nicht einsatzbereit (kein Resource-Pack).", - NamedTextColor.RED)); + player.sendActionBar( + Component.text("PixelPics ist nicht einsatzbereit (kein Resource-Pack).", NamedTextColor.RED)); return false; } @@ -56,9 +55,11 @@ public final class PhotoService { UUID user = player.getUniqueId(); RenderManager.Outcome outcome = manager.tryReserve(user); if (outcome != RenderManager.Outcome.ACCEPTED) { - player.sendActionBar(Component.text(outcome == RenderManager.Outcome.USER_BUSY - ? "Deine letzte Aufnahme wird noch entwickelt …" - : "Zu viele Aufnahmen gerade — versuch es gleich erneut.", NamedTextColor.RED)); + player.sendActionBar(Component.text( + outcome == RenderManager.Outcome.USER_BUSY + ? "Deine letzte Aufnahme wird noch entwickelt …" + : "Zu viele Aufnahmen gerade — versuch es gleich erneut.", + NamedTextColor.RED)); return false; } @@ -77,8 +78,11 @@ public final class PhotoService { ItemStack map = new ItemStack(Material.FILLED_MAP, 1); MapMeta meta = (MapMeta) map.getItemMeta(); - meta.getPersistentDataContainer().set(plugin.pictureIdFlag, - PersistentDataType.STRING, UUID.randomUUID().toString()); + meta.getPersistentDataContainer() + .set( + plugin.pictureIdFlag, + PersistentDataType.STRING, + UUID.randomUUID().toString()); meta.setMapView(mapView); map.setItemMeta(meta); player.getInventory().addItem(map); @@ -88,19 +92,20 @@ public final class PhotoService { // Trace + dither off-thread (queued), then develop on the main thread. The cancel token is // tripped by the watchdog on timeout; we bail out before the (now pointless) dithering. - manager.dispatch(user, - cancelled -> { - BufferedImage image = renderer.execute(job, cancelled); - if (cancelled.get()) return null; - return new RenderOutput(image, MapImageDither.dither(image)); - }, - out -> { - MapManager.saveImage(out.image(), id); - MapManager.saveIndices(out.indices(), id); - mapRenderer.develop(out.indices()); - player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.8f, 1.2f); - }, - () -> player.sendActionBar(Component.text("Rendern fehlgeschlagen.", NamedTextColor.RED))); + manager.dispatch( + user, + cancelled -> { + BufferedImage image = renderer.execute(job, cancelled); + if (cancelled.get()) return null; + return new RenderOutput(image, MapImageDither.dither(image)); + }, + out -> { + MapManager.saveImage(out.image(), id); + MapManager.saveIndices(out.indices(), id); + mapRenderer.develop(out.indices()); + player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.8f, 1.2f); + }, + () -> player.sendActionBar(Component.text("Rendern fehlgeschlagen.", NamedTextColor.RED))); dispatched = true; } finally { // prepare()/map setup threw before handing off — free the reserved slot. diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/SurvivalRecipes.java b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/SurvivalRecipes.java index 54d0a0c..2c6856c 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/SurvivalRecipes.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/SurvivalRecipes.java @@ -1,6 +1,7 @@ package eu.mhsl.minecraft.pixelpics.survival; import eu.mhsl.minecraft.pixelpics.Main; +import java.util.List; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.NamespacedKey; @@ -9,8 +10,6 @@ import org.bukkit.inventory.RecipeChoice; import org.bukkit.inventory.ShapedRecipe; import org.bukkit.inventory.ShapelessRecipe; -import java.util.List; - /** * Registers the four survival recipes and makes them discoverable in the recipe book. Camera and * film have fixed results; load-film and copy-photo use {@code MaterialChoice} ingredients (their diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/utils/ImageMapRenderer.java b/src/main/java/eu/mhsl/minecraft/pixelpics/utils/ImageMapRenderer.java index 5b377d6..e6fa3ff 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/utils/ImageMapRenderer.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/utils/ImageMapRenderer.java @@ -1,13 +1,12 @@ package eu.mhsl.minecraft.pixelpics.utils; +import java.awt.image.BufferedImage; import org.bukkit.entity.Player; import org.bukkit.map.MapCanvas; import org.bukkit.map.MapRenderer; import org.bukkit.map.MapView; import org.jetbrains.annotations.NotNull; -import java.awt.image.BufferedImage; - /** * Draws precomputed map palette indices onto the canvas. Supports a Polaroid-style "developing" * animation: the picture dissolves in over {@link #DEVELOP_MILLIS} once the render is ready, like an @@ -24,9 +23,9 @@ public class ImageMapRenderer extends MapRenderer { private static byte filmIndex = 0; private static boolean filmResolved = false; - private volatile byte[] indices; // MAP_SIZE*MAP_SIZE, null while still rendering + private volatile byte[] indices; // MAP_SIZE*MAP_SIZE, null while still rendering private final boolean animate; - private volatile long developStart = 0; // 0 until indices are available + private volatile long developStart = 0; // 0 until indices are available private boolean finished = false; private boolean blankDrawn = false; @@ -88,8 +87,7 @@ public class ImageMapRenderer extends MapRenderer { byte film = film(); for (int y = 0; y < MAP_SIZE; y++) { for (int x = 0; x < MAP_SIZE; x++) { - byte value = (progress >= 1.0 || revealThreshold(x, y) <= progress) - ? data[y * MAP_SIZE + x] : film; + byte value = (progress >= 1.0 || revealThreshold(x, y) <= progress) ? data[y * MAP_SIZE + x] : film; canvas.setPixel(x, y, value); } } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/utils/MapColorPalette.java b/src/main/java/eu/mhsl/minecraft/pixelpics/utils/MapColorPalette.java index e340fe0..75fd606 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/utils/MapColorPalette.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/utils/MapColorPalette.java @@ -1,11 +1,10 @@ package eu.mhsl.minecraft.pixelpics.utils; import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil; -import org.bukkit.map.MapPalette; - import java.awt.Color; import java.util.ArrayList; import java.util.List; +import org.bukkit.map.MapPalette; /** * The set of usable Minecraft map colors, with nearest-color matching in CIELAB (perceptual) space. @@ -92,7 +91,7 @@ public final class MapColorPalette { double fx = pivotXyz(x / 0.95047); double fy = pivotXyz(y); double fz = pivotXyz(z / 1.08883); - return new double[]{116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)}; + return new double[] {116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)}; } private static double pivotXyz(double t) { diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/utils/MapManager.java b/src/main/java/eu/mhsl/minecraft/pixelpics/utils/MapManager.java index aea0de0..200110b 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/utils/MapManager.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/utils/MapManager.java @@ -1,16 +1,6 @@ package eu.mhsl.minecraft.pixelpics.utils; import eu.mhsl.minecraft.pixelpics.Main; -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.World; -import org.bukkit.entity.ItemFrame; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.MapMeta; -import org.bukkit.map.MapView; - -import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; @@ -19,6 +9,15 @@ import java.util.HashSet; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.imageio.ImageIO; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.entity.ItemFrame; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.MapMeta; +import org.bukkit.map.MapView; /** * Persists rendered images (PNG, source of truth) and their dithered map-color indices (cache) to