diff --git a/probe/build.gradle b/probe/build.gradle
index 926526037..66fa6b246 100644
--- a/probe/build.gradle
+++ b/probe/build.gradle
@@ -180,6 +180,7 @@ tasks.register('genProbe', JavaExec) {
systemProperty('iris.probe.pregenProfile', providers.gradleProperty('probePregenProfile').orElse('false').get())
systemProperty('iris.probe.hydrologyPreplan', providers.gradleProperty('probeHydrologyPreplan').orElse('false').get())
systemProperty('iris.probe.historyRouter', providers.gradleProperty('probeHistoryRouter').orElse('false').get())
+ systemProperty('iris.probe.historyDigest', providers.gradleProperty('probeHistoryDigest').orElse('false').get())
systemProperty('iris.probe.productionBuffers', providers.gradleProperty('probeProductionBuffers').orElse('false').get())
String probeCounters = providers.gradleProperty('probeCounters').orElse('false').get()
systemProperty('iris.generation.counters', probeCounters)
diff --git a/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java b/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java
index 81b78ae28..c1014314a 100644
--- a/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java
+++ b/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java
@@ -177,7 +177,7 @@ public final class GenerationProbe {
}
record ProbeOptions(int threads, Path dumpFile, boolean pregenProfile, boolean hydrologyPreplan,
- boolean historyRouter, boolean productionBuffers, boolean counters) {
+ boolean historyRouter, boolean productionBuffers, boolean counters, boolean historyDigest) {
ProbeOptions {
if (threads < 1) {
throw new IllegalArgumentException("Thread count must be at least 1.");
@@ -196,7 +196,8 @@ public final class GenerationProbe {
booleanProperty("iris.probe.hydrologyPreplan"),
booleanProperty("iris.probe.historyRouter"),
booleanProperty("iris.probe.productionBuffers"),
- booleanProperty("iris.generation.counters"));
+ booleanProperty("iris.generation.counters"),
+ booleanProperty("iris.probe.historyDigest"));
}
private static boolean booleanProperty(String key) {
@@ -437,7 +438,8 @@ public final class GenerationProbe {
+ " timeMs=" + String.format(Locale.ROOT, "%.3f", nanosToMillis(engineReadyNanos)));
System.out.println("IRIS_GENPROBE_MANTLE version=1 radius=" + engine.getMantle().getRadius()
+ " real_radius=" + engine.getMantle().getRealRadius());
- ProbeChunkGenerator generator = new ProbeChunkGenerator(engine, router, options.productionBuffers());
+ ProbeChunkGenerator generator = new ProbeChunkGenerator(engine, router, options.productionBuffers(),
+ options.historyDigest());
long nativeSamplesBefore = FractalBillowSimplexNoise.nativeSampleCount();
GenerationResult generation = generateWithProfile(generator, configuration, options);
verifyBackend(nativeSamplesBefore);
@@ -445,6 +447,9 @@ public final class GenerationProbe {
System.out.println("IRIS_GENPROBE_BUFFERS version=1 mode=production biome_expanded_chunks="
+ generator.expandedBiomeHunks());
}
+ if (generator.historySummary() != null) {
+ System.out.println(generator.historySummary());
+ }
String status = generation.failedChunks() == 0 ? "PASS" : "FAIL";
printGenerationFailures(generation);
return new ProbeResult(
diff --git a/probe/src/main/java/art/arcane/iris/probe/HistoryDigest.java b/probe/src/main/java/art/arcane/iris/probe/HistoryDigest.java
new file mode 100644
index 000000000..ca0724703
--- /dev/null
+++ b/probe/src/main/java/art/arcane/iris/probe/HistoryDigest.java
@@ -0,0 +1,86 @@
+/*
+ * 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 .
+ */
+
+package art.arcane.iris.probe;
+
+import art.arcane.iris.world.history.ChunkGenerationSemantics;
+import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
+import art.arcane.iris.world.history.SavedBiomeChunk;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HexFormat;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentSkipListMap;
+
+final class HistoryDigest {
+ private final GenerationHistoryRuntimeRouter router;
+ private final Map chunks = new ConcurrentSkipListMap<>();
+
+ HistoryDigest(GenerationHistoryRuntimeRouter router) {
+ this.router = router;
+ }
+
+ void record(int chunkX, int chunkZ) throws IOException {
+ StringBuilder canonical = new StringBuilder(4096);
+ Optional semantics = router.history().semantics(chunkX, chunkZ);
+ canonical.append(semantics.map(value -> value + "|" + value.pointsOfInterest()).orElse("none")).append('\n');
+ Optional biomes = router.biomes().snapshot(chunkX, chunkZ);
+ if (biomes.isEmpty()) {
+ canonical.append("none");
+ } else {
+ SavedBiomeChunk chunk = biomes.get();
+ canonical.append(chunk.header());
+ for (int localZ = 0; localZ < 16; localZ++) {
+ for (int localX = 0; localX < 16; localX++) {
+ canonical.append('\n').append(chunk.column(localX, localZ));
+ }
+ }
+ }
+ chunks.put(((long) chunkX << 32) | (chunkZ & 0xFFFFFFFFL), sha256(canonical.toString().getBytes(StandardCharsets.UTF_8)));
+ }
+
+ String summary() {
+ MessageDigest digest = newDigest();
+ ByteBuffer key = ByteBuffer.allocate(Long.BYTES);
+ for (Map.Entry entry : chunks.entrySet()) {
+ key.clear();
+ key.putLong(entry.getKey());
+ digest.update(key.array());
+ digest.update(entry.getValue());
+ }
+ return "IRIS_GENPROBE_HISTORY version=1 chunks=" + chunks.size()
+ + " digest=" + HexFormat.of().formatHex(digest.digest(), 0, 8);
+ }
+
+ private static byte[] sha256(byte[] bytes) {
+ return newDigest().digest(bytes);
+ }
+
+ private static MessageDigest newDigest() {
+ try {
+ return MessageDigest.getInstance("SHA-256");
+ } catch (NoSuchAlgorithmException missing) {
+ throw new IllegalStateException("SHA-256 is unavailable", missing);
+ }
+ }
+}
diff --git a/probe/src/main/java/art/arcane/iris/probe/ProbeChunkGenerator.java b/probe/src/main/java/art/arcane/iris/probe/ProbeChunkGenerator.java
index 64db59352..5e3b7d6d4 100644
--- a/probe/src/main/java/art/arcane/iris/probe/ProbeChunkGenerator.java
+++ b/probe/src/main/java/art/arcane/iris/probe/ProbeChunkGenerator.java
@@ -36,12 +36,19 @@ final class ProbeChunkGenerator {
private final Engine engine;
private final GenerationHistoryRuntimeRouter router;
private final boolean productionBuffers;
+ private final HistoryDigest historyDigest;
private final AtomicInteger expandedBiomeHunks = new AtomicInteger();
- ProbeChunkGenerator(Engine engine, GenerationHistoryRuntimeRouter router, boolean productionBuffers) {
+ ProbeChunkGenerator(Engine engine, GenerationHistoryRuntimeRouter router, boolean productionBuffers,
+ boolean historyDigest) {
this.engine = engine;
this.router = router;
this.productionBuffers = productionBuffers;
+ this.historyDigest = historyDigest && router != null ? new HistoryDigest(router) : null;
+ }
+
+ String historySummary() {
+ return historyDigest == null ? null : historyDigest.summary();
}
Engine engine() {
@@ -64,6 +71,13 @@ final class ProbeChunkGenerator {
failures.add(e);
}
long elapsed = System.nanoTime() - started;
+ if (historyDigest != null && failures.isEmpty()) {
+ try {
+ historyDigest.record(coordinate.x(), coordinate.z());
+ } catch (Throwable e) {
+ failures.add(e);
+ }
+ }
if (biomes instanceof ProductionBiomeHunk production && production.expanded()) {
expandedBiomeHunks.incrementAndGet();
}
diff --git a/probe/src/test/java/art/arcane/iris/probe/GenerationProbeTest.java b/probe/src/test/java/art/arcane/iris/probe/GenerationProbeTest.java
index 54d240378..ee72fe4f9 100644
--- a/probe/src/test/java/art/arcane/iris/probe/GenerationProbeTest.java
+++ b/probe/src/test/java/art/arcane/iris/probe/GenerationProbeTest.java
@@ -134,7 +134,7 @@ public final class GenerationProbeTest {
assertEquals(1, defaults.threads());
assertNull(defaults.dumpFile());
assertThrows(IllegalArgumentException.class,
- () -> new GenerationProbe.ProbeOptions(0, null, false, false, false, false, false));
+ () -> new GenerationProbe.ProbeOptions(0, null, false, false, false, false, false, false));
assertFalse(defaults.hydrologyPreplan());
assertFalse(defaults.historyRouter());
assertFalse(defaults.productionBuffers());