Headless Generation

This commit is contained in:
Brian Neumann-Fopiano
2026-06-11 21:32:54 -04:00
parent 6b24105640
commit f32094a404
26 changed files with 816 additions and 80 deletions
+24
View File
@@ -9,8 +9,17 @@ application {
mainClass = 'art.arcane.iris.probe.ClassloadProbe'
}
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
.orElse('com.github.VolmitSoftware:VolmLib:master-SNAPSHOT')
.get()
dependencies {
implementation(project(':core'))
compileOnly(libs.spigot)
compileOnly(volmLibCoordinate) {
changing = true
transitive = false
}
runtimeOnly(libs.paralithic)
runtimeOnly(libs.gson)
runtimeOnly(libs.lru)
@@ -35,3 +44,18 @@ tasks.named('run', JavaExec).configure {
jvmArgs('--add-modules', 'jdk.incubator.vector')
dependsOn(':core:compileJava')
}
String probePack = providers.gradleProperty('probePack')
.orElse('/Users/brianfopiano/Developer/RemoteGit/[Minecraft Server]/consumers/plugin-consumers/instances/purpur-26.1.2/plugins/Iris/packs/overworld')
.get()
String probeRadius = providers.gradleProperty('probeRadius').orElse('2').get()
tasks.register('genProbe', JavaExec) {
group = 'verification'
description = 'Constructs a real Iris engine over a real pack and generates chunks into buffers on a Bukkit-free JVM.'
mainClass = 'art.arcane.iris.probe.GenerationProbe'
classpath = sourceSets.main.runtimeClasspath
jvmArgs('--add-modules', 'jdk.incubator.vector')
args(probePack, probeRadius)
dependsOn(':core:compileJava')
}
@@ -0,0 +1,339 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.probe;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.EngineWorldManager;
import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
import art.arcane.iris.engine.framework.MeteredCache;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.hunk.Hunk;
import org.bukkit.Chunk;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public final class GenerationProbe {
private static final String DIMENSION_KEY = "overworld";
private static final long SEED = 1337L;
private static final int BIOME_STEP = 4;
private static final List<Throwable> REPORTED = Collections.synchronizedList(new ArrayList<>());
private static final class InertPreservation implements PreservationRegistry {
@Override
public void register(Thread thread) {
}
@Override
public void registerCache(MeteredCache cache) {
}
@Override
public void dereference() {
}
}
private static final class InertWorldManager implements EngineWorldManager {
@Override
public void close() {
}
@Override
public int getEntityCount() {
return 0;
}
@Override
public int getChunkCount() {
return 0;
}
@Override
public double getEntitySaturation() {
return 0;
}
@Override
public void onTick() {
}
@Override
public void onSave() {
}
@Override
public void onBlockBreak(BlockBreakEvent e) {
}
@Override
public void onBlockPlace(BlockPlaceEvent e) {
}
@Override
public void onChunkLoad(Chunk e, boolean generated) {
}
@Override
public void onChunkUnload(Chunk e) {
}
@Override
public void teleportAsync(PlayerTeleportEvent e) {
}
}
public static void main(String[] args) throws Exception {
IrisPlatforms.bind(new StubPlatform());
StubPlatform.verbose(true);
StubPlatform.errorSink(REPORTED::add);
IrisServices.register(PreservationRegistry.class, new InertPreservation());
IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> new InertWorldManager());
File packSource = new File(args[0]);
int radius = args.length > 1 ? Integer.parseInt(args[1]) : 2;
if (!packSource.isDirectory()) {
System.out.println("[genprobe] pack folder not found: " + packSource.getAbsolutePath());
System.exit(1);
}
File workRoot = Files.createTempDirectory("iris-genprobe").toFile();
File pack = clonePack(packSource, workRoot);
System.out.println("[genprobe] pack: " + packSource.getAbsolutePath());
System.out.println("[genprobe] work copy: " + pack.getAbsolutePath());
System.out.println("[genprobe] radius: " + radius + " (" + ((2 * radius + 1) * (2 * radius + 1)) + " chunks)");
Engine engine;
try {
IrisData data = IrisData.get(pack);
IrisDimension dimension = data.getDimensionLoader().load(DIMENSION_KEY);
if (dimension == null) {
System.out.println("[genprobe] FAIL: dimension '" + DIMENSION_KEY + "' did not load from " + pack.getAbsolutePath());
System.exit(1);
return;
}
IrisWorld world = IrisWorld.builder()
.name("probe")
.seed(SEED)
.worldFolder(new File(workRoot, "world"))
.minHeight(dimension.getMinHeight())
.maxHeight(dimension.getMaxHeight())
.build();
EngineTarget target = new EngineTarget(world, dimension, data);
engine = new IrisEngine(target, false);
} catch (Throwable e) {
System.out.println("[genprobe] FAIL: engine construction threw before generation could start");
e.printStackTrace(System.out);
printDistinctCauses("construction-time reported errors", drainReported());
System.exit(1);
return;
}
List<Throwable> initNoise = settleAndDrain();
printDistinctCauses("engine-init reported errors (non-fatal, async)", initNoise);
int height = engine.getTarget().getHeight();
System.out.println("[genprobe] engine up: dim=" + engine.getDimension().getLoadKey()
+ " seed=" + engine.getSeedManager().getSeed()
+ " minY=" + engine.getMinHeight() + " maxY=" + engine.getMaxHeight());
Map<String, Integer> distinct = new LinkedHashMap<>();
Map<String, String> firstChunk = new LinkedHashMap<>();
int ok = 0;
int failed = 0;
for (int cz = -radius; cz <= radius; cz++) {
for (int cx = -radius; cx <= radius; cx++) {
String at = cx + "," + cz;
drainReported();
List<Throwable> failures = new ArrayList<>();
Hunk<PlatformBlockState> blocks = Hunk.newArrayHunk(16, height, 16);
Hunk<PlatformBiome> biomes = Hunk.newArrayHunk(16, height, 16);
try {
engine.generate(cx << 4, cz << 4, blocks, biomes, false);
} catch (Throwable e) {
failures.add(e);
}
failures.addAll(drainReported());
if (failures.isEmpty()) {
ok++;
System.out.println("[genprobe] chunk " + at + " OK " + hashChunk(blocks, biomes, height));
} else {
failed++;
System.out.println("[genprobe] chunk " + at + " FAILED (" + failures.size() + " error(s))");
for (Throwable failure : failures) {
failure.printStackTrace(System.out);
String key = causeKey(failure);
distinct.merge(key, 1, Integer::sum);
firstChunk.putIfAbsent(key, at);
}
}
}
}
System.out.println("[genprobe] generated OK: " + ok + ", failed: " + failed);
if (!distinct.isEmpty()) {
System.out.println("[genprobe] DISTINCT ROOT CAUSES (" + distinct.size() + "):");
for (Map.Entry<String, Integer> entry : distinct.entrySet()) {
System.out.println(" x" + entry.getValue() + " (first at chunk " + firstChunk.get(entry.getKey()) + ") " + entry.getKey());
}
}
System.out.println("[genprobe] RESULT: " + (failed == 0 ? "PASS" : "FAIL"));
System.exit(failed == 0 ? 0 : 1);
}
private static File clonePack(File source, File workRoot) throws Exception {
File destination = new File(workRoot, source.getName());
Process clone = new ProcessBuilder("cp", "-Rc", source.getAbsolutePath(), destination.getAbsolutePath())
.inheritIO()
.start();
if (clone.waitFor() != 0) {
Process copy = new ProcessBuilder("cp", "-R", source.getAbsolutePath(), destination.getAbsolutePath())
.inheritIO()
.start();
if (copy.waitFor() != 0) {
throw new IllegalStateException("Failed to copy pack to " + destination.getAbsolutePath());
}
}
return destination;
}
private static List<Throwable> settleAndDrain() throws InterruptedException {
List<Throwable> drained = new ArrayList<>();
long quietSince = System.currentTimeMillis();
long start = quietSince;
while (System.currentTimeMillis() - start < 15000L) {
List<Throwable> batch = drainReported();
if (batch.isEmpty()) {
if (System.currentTimeMillis() - quietSince >= 1500L) {
break;
}
} else {
drained.addAll(batch);
quietSince = System.currentTimeMillis();
}
Thread.sleep(50L);
}
return drained;
}
private static List<Throwable> drainReported() {
synchronized (REPORTED) {
List<Throwable> drained = new ArrayList<>(REPORTED);
REPORTED.clear();
return drained;
}
}
private static void printDistinctCauses(String label, List<Throwable> errors) {
if (errors.isEmpty()) {
return;
}
Map<String, Integer> distinct = new LinkedHashMap<>();
for (Throwable error : errors) {
distinct.merge(causeKey(error), 1, Integer::sum);
}
System.out.println("[genprobe] " + label + " (" + distinct.size() + " distinct):");
for (Map.Entry<String, Integer> entry : distinct.entrySet()) {
System.out.println(" x" + entry.getValue() + " " + entry.getKey());
}
for (Throwable error : errors) {
error.printStackTrace(System.out);
}
}
private static String causeKey(Throwable failure) {
List<Throwable> chain = new ArrayList<>();
Throwable cause = failure;
while (cause != null && !chain.contains(cause)) {
chain.add(cause);
cause = cause.getCause();
}
Throwable root = chain.get(chain.size() - 1);
return root.getClass().getName() + ": " + root.getMessage() + " @ " + siteOf(chain);
}
private static String siteOf(List<Throwable> chain) {
for (int i = chain.size() - 1; i >= 0; i--) {
for (StackTraceElement frame : chain.get(i).getStackTrace()) {
String className = frame.getClassName();
if (className.startsWith("art.arcane.") && !className.startsWith("art.arcane.iris.probe.")) {
return frame.toString();
}
}
}
StackTraceElement[] trace = chain.get(chain.size() - 1).getStackTrace();
return trace.length > 0 ? trace[0].toString() : "<no frames>";
}
private static String hashChunk(Hunk<PlatformBlockState> blocks, Hunk<PlatformBiome> biomes, int height) {
MessageDigest blockDigest = sha256();
MessageDigest biomeDigest = sha256();
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < height; y++) {
PlatformBlockState state = blocks.get(x, y, z);
String key = state == null ? "minecraft:air" : state.key();
blockDigest.update((key + "\n").getBytes(StandardCharsets.UTF_8));
}
}
}
for (int x = 0; x < 16; x += BIOME_STEP) {
for (int z = 0; z < 16; z += BIOME_STEP) {
for (int y = 0; y < height; y += BIOME_STEP) {
PlatformBiome biome = biomes.get(x, y, z);
String key = biome == null ? "null" : biome.key();
biomeDigest.update((key + "\n").getBytes(StandardCharsets.UTF_8));
}
}
}
return HexFormat.of().formatHex(blockDigest.digest()).substring(0, 16)
+ " " + HexFormat.of().formatHex(biomeDigest.digest()).substring(0, 16);
}
private static MessageDigest sha256() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
}
@@ -29,13 +29,31 @@ import art.arcane.iris.spi.PlatformItem;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.spi.PlatformScheduler;
import art.arcane.iris.spi.PlatformStructureHooks;
import art.arcane.iris.spi.PlatformWorld;
import java.io.File;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
public final class StubPlatform implements IrisPlatform {
private static volatile boolean VERBOSE = false;
private static volatile Consumer<Throwable> ERROR_SINK = null;
public static void verbose(boolean verbose) {
VERBOSE = verbose;
}
public static void errorSink(Consumer<Throwable> sink) {
ERROR_SINK = sink;
}
private final StubRegistries registries = new StubRegistries();
private final StubScheduler scheduler = new StubScheduler();
private final PlatformCapabilities capabilities = new PlatformCapabilities() {
};
private final StubStructureHooks structureHooks = new StubStructureHooks();
private final StubBiomeWriter biomeWriter = new StubBiomeWriter();
private static final class StubBlockState implements PlatformBlockState {
private static final ConcurrentHashMap<String, StubBlockState> CACHE = new ConcurrentHashMap<>();
@@ -161,6 +179,119 @@ public final class StubPlatform implements IrisPlatform {
}
}
private static final class StubBiome implements PlatformBiome {
private static final ConcurrentHashMap<String, StubBiome> CACHE = new ConcurrentHashMap<>();
private final String key;
private StubBiome(String key) {
this.key = key;
}
static StubBiome of(String key) {
return CACHE.computeIfAbsent(key, StubBiome::new);
}
@Override
public String key() {
return key;
}
@Override
public String namespace() {
int colon = key.indexOf(':');
return colon >= 0 ? key.substring(0, colon) : "minecraft";
}
@Override
public Object nativeHandle() {
return key;
}
}
private static final class StubScheduler implements PlatformScheduler {
@Override
public void global(Runnable task) {
task.run();
}
@Override
public void region(PlatformWorld world, int chunkX, int chunkZ, Runnable task) {
task.run();
}
@Override
public void async(Runnable task) {
task.run();
}
@Override
public void laterGlobal(Runnable task, int ticks) {
}
@Override
public void laterRegion(PlatformWorld world, int chunkX, int chunkZ, Runnable task, int ticks) {
}
}
private static final class StubStructureHooks implements PlatformStructureHooks {
@Override
public List<String> structureKeys() {
return List.of();
}
@Override
public List<String> structureSetKeys() {
return List.of();
}
@Override
public List<String> structureBiomeKeys(String structureKey) {
return List.of();
}
@Override
public List<String> objectFeatureKeys() {
return List.of();
}
@Override
public List<String> reachableStructureKeys(PlatformWorld world) {
return List.of();
}
@Override
public List<String> possibleBiomeKeys(PlatformWorld world) {
return List.of();
}
@Override
public boolean placeFeature(PlatformWorld world, int x, int y, int z, String featureKey, long seed) {
return false;
}
@Override
public int[] placeStructure(PlatformWorld world, int chunkX, int chunkZ, String structureKey, long seed, int maxSpan) {
return null;
}
@Override
public boolean supportsStructurePlacement() {
return false;
}
}
private static final class StubBiomeWriter implements PlatformBiomeWriter {
@Override
public int biomeIdFor(String key) {
return 0;
}
@Override
public List<PlatformBiome> allBiomes() {
return List.of();
}
}
private static final class StubRegistries implements PlatformRegistries {
@Override
public PlatformBlockState block(String key) {
@@ -189,7 +320,7 @@ public final class StubPlatform implements IrisPlatform {
@Override
public PlatformBiome biome(String key) {
return null;
return StubBiome.of(key);
}
@Override
@@ -235,22 +366,22 @@ public final class StubPlatform implements IrisPlatform {
@Override
public PlatformScheduler scheduler() {
return null;
return scheduler;
}
@Override
public PlatformCapabilities capabilities() {
return null;
return capabilities;
}
@Override
public PlatformStructureHooks structureHooks() {
return null;
return structureHooks;
}
@Override
public PlatformBiomeWriter biomeWriter() {
return null;
return biomeWriter;
}
@Override
@@ -288,13 +419,23 @@ public final class StubPlatform implements IrisPlatform {
@Override
public void log(LogLevel level, String message) {
if (VERBOSE) {
System.out.println("[stub/" + level + "] " + message);
}
}
@Override
public void msg(String message) {
if (VERBOSE) {
System.out.println("[stub/MSG] " + message);
}
}
@Override
public void reportError(Throwable error) {
Consumer<Throwable> sink = ERROR_SINK;
if (sink != null && error != null) {
sink.accept(error);
}
}
}