import eu.mhsl.minecraft.pixelpics.assets.*; import eu.mhsl.minecraft.pixelpics.assets.font.FontLoader; import 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.entity.cem.CemModelLoader; import eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer; import eu.mhsl.minecraft.pixelpics.render.render.RenderJob; import eu.mhsl.minecraft.pixelpics.render.sky.SkyContext; import eu.mhsl.minecraft.pixelpics.render.snapshot.WorldSnapshot; import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.block.data.BlockData; import org.bukkit.util.Vector; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Proxy; import java.util.List; import java.util.Map; import java.util.function.IntFunction; import java.util.logging.Logger; /** * Shared setup of the standalone render tools (no server: EntityTestRender, BlockEntityTestRender, * ObjectTestRender, EntityBenchmark). Renders with the test server's resource pack and the repo's model JSONs. * *

Build/run from the repo root with a JDK 25: {@code CP=build/classes/java/main:build/resources/main:$(./gradlew -q * printCompileClasspath)}, {@code javac -d -cp $CP $(ls tools/*.java | grep -v VanillaModelDump)}, * {@code java -cp :$CP [-Dout=

] [args]}. */ final class Harness { static final String PACK_ROOT = "/home/elias/Dokumente/mcTestServer/plugins/PixelPics"; static final int SSAA = 3; static final double V_FOV_HALF = Math.toRadians(DefaultScreenRenderer.V_FOV_DEG / 2); static final SkyContext NOON = new SkyContext(6000, 0, 6000); final Logger log; final ResourcePack pack; final AssetReader reader; final TextureCache textures; final BlockModelRegistry registry; final CemModelLoader geo = new CemModelLoader(); final CemBaker baker; final BlockEntityBaker beBaker; final DefaultScreenRenderer renderer; Harness(String logName) throws IOException { log = Logger.getLogger(logName); pack = ResourcePackLoader.load(new File(PACK_ROOT, "resourcepack"), log).orElseThrow(); reader = new AssetReader(pack); textures = new TextureCache(pack); registry = new BlockModelRegistry(reader, textures); // Template set = entity fallback only; the vanilla dump is the primary model source. for (String f : new String[] {"cem_template_models.json", "vanilla_entity_models.json"}) { try (InputStream in = new FileInputStream("src/main/resources/cem/" + f)) { geo.load(in, log); } } SkinCache skins = new SkinCache(); baker = new CemBaker(geo, textures, skins); beBaker = new BlockEntityBaker(geo, textures, skins, FontLoader.load(pack, textures, log)); renderer = new DefaultScreenRenderer(registry, new BiomeTintProvider(textures), textures, baker, beBaker, log); } /** * Server-free BlockData stand-in: material + blockstate string are all the model registry needs. Each proxy kind * has its own handler: a shared generic one made the hot getMaterial/hashCode calls megamorphic (slower benchmark). */ static BlockData block(Material m, String state) { String s = "minecraft:" + m.getKey().getKey() + (state == null ? "" : "[" + state + "]"); return (BlockData) Proxy.newProxyInstance(Harness.class.getClassLoader(), new Class[]{BlockData.class}, (p, meth, a) -> switch (meth.getName()) { case "getMaterial" -> m; case "getAsString", "toString" -> s; case "equals" -> p == a[0]; case "hashCode" -> System.identityHashCode(p); default -> defaultValue(meth.getReturnType()); }); } /** false / 0 / null for an unstubbed proxy method. */ static Object defaultValue(Class rt) { if (rt == boolean.class) return false; return rt.isPrimitive() ? 0 : null; } static WorldSnapshot emptyWorld(int minY, int maxY) { return new WorldSnapshot(Map.of(), minY, maxY, block(Material.AIR, null)); } /** Render job looking from {@code cam} at {@code look}, {@link #SSAA}x supersampled. */ static RenderJob job(WorldSnapshot world, Vector cam, Vector look, int w, int h, List mobs, List bes, List decos, List objects) { Location loc = new Location(null, cam.getX(), cam.getY(), cam.getZ()); loc.setDirection(look.clone().subtract(cam)); List rayMap = DefaultScreenRenderer.buildRayMap(loc, w * SSAA, h * SSAA); return new RenderJob(world, rayMap, cam, w, h, NOON, mobs, bes, decos, objects); } /** Distance at which a box of extent {@code ext} (at least 0.5) fills the view, scaled {@code mul}, plus {@code add}. */ static double frameDistance(double[] min, double[] max, double mul, double add) { double ext = 0.5; for (int a = 0; a < 3; a++) ext = Math.max(ext, max[a] - min[a]); return ext / (2 * Math.tan(V_FOV_HALF)) * mul + add; } static Vector center(double[] min, double[] max) { return new Vector((min[0] + max[0]) / 2, (min[1] + max[1]) / 2, (min[2] + max[2]) / 2); } /** Copy of an entity state with some record components replaced (by name). */ static EntityState copy(EntityState s, Map overrides) throws ReflectiveOperationException { var comps = EntityState.class.getRecordComponents(); Object[] vals = new Object[comps.length]; Class[] types = new Class[comps.length]; for (int i = 0; i < comps.length; i++) { types[i] = comps[i].getType(); String n = comps[i].getName(); vals[i] = overrides.containsKey(n) ? overrides.get(n) : comps[i].getAccessor().invoke(s); } return EntityState.class.getDeclaredConstructor(types).newInstance(vals); } static ItemSpec item(String id) { return new ItemSpec(id, null, null, false); } /** The views side by side under a 20 px label. */ static BufferedImage labelCell(String key, BufferedImage... views) { int w = 0, labelH = 20; for (BufferedImage v : views) w += v.getWidth(); BufferedImage cell = new BufferedImage(w, views[0].getHeight() + labelH, BufferedImage.TYPE_INT_RGB); Graphics2D g = cell.createGraphics(); g.setColor(new Color(25, 25, 30)); g.fillRect(0, 0, cell.getWidth(), cell.getHeight()); for (int i = 0, x = 0; i < views.length; x += views[i].getWidth(), i++) g.drawImage(views[i], x, labelH, null); g.setColor(Color.WHITE); g.setFont(new Font("SansSerif", Font.BOLD, 13)); g.drawString(key, 4, 15); g.dispose(); return cell; } /** Contact sheets of {@code cols} columns, {@code perPage} cells each, named {@code name(page)}. */ static void writeSheets(List cells, int cols, int perPage, File dir, IntFunction name) throws IOException { int cw = cells.get(0).getWidth(), ch = cells.get(0).getHeight(); for (int page = 0, idx = 0; idx < cells.size(); page++, idx += perPage) { int count = Math.min(perPage, cells.size() - idx); BufferedImage sheet = new BufferedImage(cols * cw, (count + cols - 1) / cols * ch, BufferedImage.TYPE_INT_RGB); Graphics2D g = sheet.createGraphics(); g.setColor(new Color(40, 40, 48)); g.fillRect(0, 0, sheet.getWidth(), sheet.getHeight()); for (int i = 0; i < count; i++) g.drawImage(cells.get(idx + i), (i % cols) * cw, (i / cols) * ch, null); g.dispose(); File f = new File(dir, name.apply(page)); ImageIO.write(sheet, "png", f); System.out.println("WROTE " + f); } } }