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 gets its own handler class: a shared generic one made the hot getMaterial/hashCode calls megamorphic and
* EntityBenchmark measurably slower.)
*/
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);
}
}
}