mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-11 19:14:11 +00:00
Reworked terrain transitions
- New terrain heights meet saved boundaries while retaining local relief - Surface water levels approach saved fluid heights across the band - Cave and physical biome matching use a four-block seam patch - Terrain generates once without speculative neighbor containment - Bounded caches serve transition membership and boundary lookups - Saved biome writes honor the selected durability policy - Release labels and presentation code leave generation identity unchanged - Unfinished objects retain blocks and metadata across pack changes - Whole structure assemblies preserve pending excavation and foundations - New placements keep pending historical pieces clear - Transition probes cover cutovers and retained object fragments
This commit is contained in:
+26
-1
@@ -37,6 +37,7 @@ dependencies {
|
||||
runtimeOnly(libs.fastutil)
|
||||
runtimeOnly(libs.caffeine)
|
||||
runtimeOnly(libs.lz4)
|
||||
runtimeOnly('com.fulcrumgenomics:jlibdeflate:0.2.0')
|
||||
runtimeOnly(libs.zip)
|
||||
runtimeOnly(libs.guava)
|
||||
runtimeOnly(libs.oshi)
|
||||
@@ -291,7 +292,31 @@ tasks.register('riverTransectProbe', JavaExec) {
|
||||
}
|
||||
}
|
||||
|
||||
['hydrologyProbe', 'genProbe', 'hydrologyPackProbe', 'generationOrderProbe', 'riverTransectProbe', 'terrain3DProbe', 'hydrologyBlockProbe'].each { String taskName ->
|
||||
tasks.register('transitionProbe', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Exercises a bounded two-pack Studio cutover, transition routing, receipts and semantics with a vanilla-biome pack.'
|
||||
mainClass = 'art.arcane.iris.probe.TransitionProbe'
|
||||
workingDir = rootProject.projectDir
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
jvmArgs('--add-modules', 'jdk.incubator.vector')
|
||||
timeout = java.time.Duration.ofMinutes(12)
|
||||
doFirst {
|
||||
Provider<String> replacement = providers.gradleProperty('probeReplacementPack')
|
||||
if (!probePack.isPresent() || !replacement.isPresent() || !probeDimension.isPresent() || !probeOutput.isPresent()) {
|
||||
throw new GradleException('transitionProbe requires probePack, probeReplacementPack, probeDimension and probeOutput')
|
||||
}
|
||||
java.nio.file.Path output = new File(probeOutput.get()).canonicalFile.toPath()
|
||||
if (output.startsWith(rootProject.projectDir.canonicalFile.toPath())) {
|
||||
throw new GradleException('probeOutput must point outside the repository')
|
||||
}
|
||||
args(probePack.get(), replacement.get(), probeDimension.get(), probeSeed.getOrElse('1337'),
|
||||
providers.gradleProperty('probeTransitionWidth').getOrElse('32'),
|
||||
providers.gradleProperty('probeTransitionChunks').getOrElse('2'), output.toString(),
|
||||
providers.gradleProperty('probeTransitionReverse').getOrElse('false'))
|
||||
}
|
||||
}
|
||||
|
||||
['transitionProbe', 'hydrologyProbe', 'genProbe', 'hydrologyPackProbe', 'generationOrderProbe', 'riverTransectProbe', 'terrain3DProbe', 'hydrologyBlockProbe'].each { String taskName ->
|
||||
tasks.named(taskName, JavaExec).configure { JavaExec task ->
|
||||
classpath += configurations.studioProbeApi
|
||||
doFirst {
|
||||
|
||||
@@ -206,7 +206,10 @@ public final class StubPlatform implements IrisPlatform {
|
||||
|
||||
@Override
|
||||
public boolean isStorageChest() {
|
||||
return false;
|
||||
return switch (blockKey()) {
|
||||
case "minecraft:chest", "minecraft:trapped_chest", "minecraft:barrel" -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,405 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.world.history.ChunkGenerationOwnership;
|
||||
import art.arcane.iris.world.history.ChunkGenerationSemantics;
|
||||
import art.arcane.iris.world.history.GenerationActivation;
|
||||
import art.arcane.iris.world.history.GenerationEpochContractFactory;
|
||||
import art.arcane.iris.world.history.GenerationHistory;
|
||||
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.iris.world.history.GenerationPackFingerprint;
|
||||
import art.arcane.iris.world.history.GenerationRegistryContract;
|
||||
import art.arcane.iris.world.history.NativeTerrainReceipt;
|
||||
import art.arcane.iris.world.history.SavedTerrainChunk;
|
||||
import art.arcane.iris.world.history.SavedBiomeChunk;
|
||||
import art.arcane.iris.world.history.TerrainBoundarySignature;
|
||||
import art.arcane.iris.world.history.TransitionGenerationPlan;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeTerrainReceiptStorage;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.HexFormat;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.FileVisitOption;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public final class TransitionProbe {
|
||||
private TransitionProbe() {
|
||||
}
|
||||
|
||||
public static void main(String[] arguments) throws Exception {
|
||||
Configuration configuration = Configuration.parse(arguments);
|
||||
Files.createDirectory(configuration.output());
|
||||
long deadline = System.nanoTime() + Duration.ofMinutes(10).toNanos();
|
||||
Bindings bindings = new Bindings();
|
||||
Evidence evidence = new Evidence(
|
||||
new ArrayList<>(List.of("phase,chunk_x,chunk_z,activation,band,elapsed_ns,receipt_bytes,blocks_sha256,biomes_sha256,receipt_sha256,metadata_sha256")),
|
||||
new ArrayList<>(List.of("phase,x,z,ground_y,fluid_y,solid_spans,fluid_gaps")),
|
||||
new ArrayList<>(List.of("phase,x,z,minimum_y,maximum_y_exclusive,block")),
|
||||
new ArrayList<>(List.of("phase,chunk_x,chunk_z,block,count")));
|
||||
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(
|
||||
new RealPackProbeSupport.WorkspaceOptions(configuration.packA().toFile(), configuration.dimension(),
|
||||
"[transition-probe]", bindings, Path.of(System.getProperty("java.io.tmpdir"))));
|
||||
RealPackProbeSupport.EngineSession session = workspace.openEngine(configuration.seed(), true, "transition")) {
|
||||
IrisEngine engine = (IrisEngine) session.engine();
|
||||
GenerationHistoryRuntimeRouter router = engine.getGenerationHistoryRuntimeRouter().orElseThrow();
|
||||
GenerationHistory history = router.history();
|
||||
long originalActivation = history.activeActivation().activationId();
|
||||
for (int x = 0; x < 2; x++) {
|
||||
for (int z = 0; z < 2; z++) {
|
||||
generate(new GenerationRequest(engine, bindings, x, z, "original", false, deadline), evidence);
|
||||
}
|
||||
}
|
||||
Path world = engine.getWorld().worldFolder().toPath();
|
||||
Map<Long, byte[]> historicalReceipts = new HashMap<>();
|
||||
for (int x = 0; x < 2; x++) {
|
||||
for (int z = 0; z < 2; z++) {
|
||||
historicalReceipts.put(ChunkGenerationOwnership.packChunk(x, z), SavedTerrainChunk.readReceipt(world, x, z));
|
||||
}
|
||||
}
|
||||
Path replacementPack = workspace.pack().toPath().getParent().resolve("replacement-pack");
|
||||
copyPack(configuration.packB(), replacementPack);
|
||||
SavedBiomeChunk historicalBiomes = router.biomes().snapshot(0, 0).orElseThrow();
|
||||
IrisData replacement = IrisData.openRuntime(replacementPack.toFile());
|
||||
try {
|
||||
IrisDimension dimension = replacement.getDimensionLoader().load(configuration.dimension());
|
||||
if (dimension == null) {
|
||||
throw new IOException("Replacement dimension did not load");
|
||||
}
|
||||
requireVanillaBiomes(replacement, dimension);
|
||||
history.stageUpdate(replacementPack, GenerationPackFingerprint.compute(replacementPack,
|
||||
GenerationPackFingerprint.CURRENT_VERSION),
|
||||
GenerationEpochContractFactory.create(dimension, configuration.dimension(), "iris:transition_probe"),
|
||||
GenerationRegistryContract.empty(), configuration.width());
|
||||
} finally {
|
||||
replacement.close();
|
||||
}
|
||||
try (GenerationHistoryRuntimeRouter.StudioCutover cutover = router.beginStudioCutover(30_000)) {
|
||||
GenerationActivation activated = cutover.promotePending();
|
||||
require(activated.activationId() != originalActivation, "Pack update did not create another activation");
|
||||
}
|
||||
require(historicalBiomes.equals(router.biomes().snapshot(0, 0).orElseThrow()),
|
||||
"Historical biome data changed during promotion");
|
||||
require(history.resolveActivation(0, 0).activationId() == originalActivation,
|
||||
"Historical chunk ownership changed");
|
||||
require(history.semantics(0, 0).orElseThrow().activationId() == originalActivation,
|
||||
"Historical semantic ownership changed");
|
||||
for (int sequence = 0; sequence < configuration.chunks(); sequence++) {
|
||||
int index = configuration.reverse() ? configuration.chunks() - 1 - sequence : sequence;
|
||||
generate(new GenerationRequest(engine, bindings, 2 + index, 0, "transition", true, deadline), evidence);
|
||||
generate(new GenerationRequest(engine, bindings, 2 + configuration.width() / 16 + index, 0,
|
||||
"distant", false, deadline), evidence);
|
||||
}
|
||||
for (int x = 0; x < 2; x++) {
|
||||
for (int z = 0; z < 2; z++) {
|
||||
require(Arrays.equals(historicalReceipts.get(ChunkGenerationOwnership.packChunk(x, z)),
|
||||
SavedTerrainChunk.readReceipt(world, x, z)), "Historical disk receipt changed after generation");
|
||||
}
|
||||
}
|
||||
require(historicalBiomes.equals(router.biomes().snapshot(0, 0).orElseThrow()),
|
||||
"Historical biome data changed after generation");
|
||||
for (Throwable failure : RealPackProbeSupport.settleAndDrain()) {
|
||||
throw new IllegalStateException("Generation reported a failure", failure);
|
||||
}
|
||||
require(bindings.captures > 0, "Promotion did not capture historical terrain");
|
||||
Files.write(configuration.output().resolve("chunks.csv"), evidence.chunks());
|
||||
Files.write(configuration.output().resolve("transect.csv"), evidence.transect());
|
||||
Files.write(configuration.output().resolve("sections.csv"), evidence.sections());
|
||||
Files.write(configuration.output().resolve("materials.csv"), evidence.materials());
|
||||
System.out.println("[transition-probe] PASS generated=" + bindings.terrain.size()
|
||||
+ " boundaryChunkCaptures=" + bindings.captures
|
||||
+ " timing=diagnostic-only platform=stub cutover=studio output=" + configuration.output());
|
||||
}
|
||||
}
|
||||
|
||||
private static void generate(GenerationRequest request, Evidence evidence) throws Exception {
|
||||
require(System.nanoTime() < request.deadline(), "Probe exceeded ten-minute workload deadline");
|
||||
long key = ChunkGenerationOwnership.packChunk(request.x(), request.z());
|
||||
require(!request.bindings().terrain.containsKey(key), "Probe scheduled a duplicate chunk");
|
||||
GenerationHistoryRuntimeRouter router = request.engine().getGenerationHistoryRuntimeRouter().orElseThrow();
|
||||
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = router.openRoute(request.x(), request.z());
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope scope = route.openRuntimeScope()) {
|
||||
TransitionGenerationPlan plan = route.transitionPlan();
|
||||
boolean band = plan != null && plan.hasTransitionAtChunk(request.x(), request.z());
|
||||
require(band == request.band(), "Unexpected transition membership at " + request.x() + "," + request.z());
|
||||
long started = System.nanoTime();
|
||||
RealPackProbeSupport.GeneratedChunk generated = RealPackProbeSupport.generateChunk(
|
||||
request.engine(), request.x(), request.z());
|
||||
require(route.claimGeneratedSemantics((x, y, z) -> {
|
||||
NativeBlockState state = generated.blocks().getRaw(x & 15, y, z & 15);
|
||||
return state == null || state.isAir();
|
||||
}), "Generated semantic claim was rejected");
|
||||
SavedTerrainChunk terrain = route.naturalTerrain().orElseThrow();
|
||||
byte[] receipt = NativeTerrainReceipt.encode(terrain, route.activation().activationId(), route.epoch().epochId());
|
||||
NativeTerrainReceipt.Decoded decoded = NativeTerrainReceipt.decode(receipt, "minecraft:full");
|
||||
require(decoded.activationId() == route.activation().activationId()
|
||||
&& decoded.epochId().equals(route.epoch().epochId()), "Receipt changed generation ownership");
|
||||
for (int localX = 0; localX < 16; localX++) {
|
||||
for (int localZ = 0; localZ < 16; localZ++) {
|
||||
if (!terrain.hasColumn(localX, localZ)) {
|
||||
continue;
|
||||
}
|
||||
TerrainBoundarySignature original = terrain.column(request.x() * 16 + localX, request.z() * 16 + localZ);
|
||||
TerrainBoundarySignature restored = decoded.terrain().column(request.x() * 16 + localX, request.z() * 16 + localZ);
|
||||
require(original.column().equals(restored.column()) && original.geometry().equals(restored.geometry()),
|
||||
"Receipt changed captured terrain");
|
||||
}
|
||||
}
|
||||
request.bindings().terrain.put(key, terrain);
|
||||
writeReceipt(request.engine().getWorld().worldFolder().toPath(), request.x(), request.z(), receipt);
|
||||
require(router.history().semantics(request.x(), request.z()).orElseThrow().activationId()
|
||||
== route.activation().activationId(), "Generated semantics lost activation ownership");
|
||||
require(router.biomes().snapshot(request.x(), request.z()).orElseThrow().activationId()
|
||||
== route.activation().activationId(), "Generated saved biomes lost activation ownership");
|
||||
long elapsed = System.nanoTime() - started;
|
||||
GenerationOrderProbe.ChunkHash hash = GenerationOrderProbe.hashChunk(
|
||||
new GenerationOrderProbe.ChunkCoordinate(request.x(), request.z()),
|
||||
generated.blocks(), generated.biomes(), generated.height());
|
||||
String metadataHash = metadataHash(router.biomes().snapshot(request.x(), request.z()).orElseThrow(),
|
||||
router.history().semantics(request.x(), request.z()).orElseThrow());
|
||||
for (Map.Entry<String, Integer> material : new TreeMap<>(hash.blockCounts()).entrySet()) {
|
||||
evidence.materials().add(request.phase() + "," + request.x() + "," + request.z() + ","
|
||||
+ material.getKey() + "," + material.getValue());
|
||||
}
|
||||
evidence.chunks().add(request.phase() + "," + request.x() + "," + request.z() + ","
|
||||
+ route.activation().activationId() + "," + band + "," + elapsed + "," + receipt.length
|
||||
+ "," + hash.blocks() + "," + hash.biomes() + ","
|
||||
+ HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(receipt)) + "," + metadataHash);
|
||||
if (request.z() == 0) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
int ground = -1;
|
||||
int fluid = -1;
|
||||
int solidSpans = 0;
|
||||
boolean solidBelow = false;
|
||||
String stateBelow = null;
|
||||
int spanStart = 0;
|
||||
for (int y = 0; y < generated.height(); y++) {
|
||||
NativeBlockState state = generated.blocks().getRaw(x, y, 8);
|
||||
String stateKey = state == null ? "minecraft:air" : state.key();
|
||||
if (!stateKey.equals(stateBelow)) {
|
||||
if (stateBelow != null) {
|
||||
evidence.sections().add(request.phase() + "," + (request.x() * 16 + x) + ",8,"
|
||||
+ (spanStart + request.engine().getMinHeight()) + ","
|
||||
+ (y + request.engine().getMinHeight()) + "," + stateBelow);
|
||||
}
|
||||
stateBelow = stateKey;
|
||||
spanStart = y;
|
||||
}
|
||||
boolean solid = state != null && !state.isAir() && !isFluid(state);
|
||||
if (solid && !solidBelow) {
|
||||
solidSpans++;
|
||||
}
|
||||
solidBelow = solid;
|
||||
if (state != null && !state.isAir()) {
|
||||
if (isFluid(state)) {
|
||||
fluid = y + request.engine().getMinHeight();
|
||||
} else {
|
||||
ground = y + request.engine().getMinHeight();
|
||||
}
|
||||
}
|
||||
}
|
||||
evidence.sections().add(request.phase() + "," + (request.x() * 16 + x) + ",8,"
|
||||
+ (spanStart + request.engine().getMinHeight()) + ","
|
||||
+ (generated.height() + request.engine().getMinHeight()) + "," + stateBelow);
|
||||
int fluidGaps = 0;
|
||||
for (int y = ground + 1; y < fluid; y++) {
|
||||
NativeBlockState state = generated.blocks().getRaw(x, y - request.engine().getMinHeight(), 8);
|
||||
if (state == null || state.isAir()) {
|
||||
fluidGaps++;
|
||||
}
|
||||
}
|
||||
evidence.transect().add(request.phase() + "," + (request.x() * 16 + x) + ",8," + ground + "," + fluid
|
||||
+ "," + solidSpans + "," + fluidGaps);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isFluid(NativeBlockState state) {
|
||||
return state.isWater() || state.key().startsWith("minecraft:lava");
|
||||
}
|
||||
|
||||
private static String metadataHash(SavedBiomeChunk biomes, ChunkGenerationSemantics semantics) throws Exception {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
digest.update(biomes.column(x, z).toString().getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
for (Collection<?> values : List.of(semantics.surfaceBiomeKeys(), semantics.caveBiomeKeys(),
|
||||
semantics.regionKeys(), semantics.riverProfileKeys(), semantics.objectKeys(), semantics.riverFeatures(),
|
||||
semantics.pointsOfInterest(), semantics.structures())) {
|
||||
digest.update(values.stream().map(Object::toString).sorted().toList().toString().getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
return HexFormat.of().formatHex(digest.digest());
|
||||
}
|
||||
|
||||
static void writeReceipt(Path world, int chunkX, int chunkZ, byte[] receipt) throws IOException {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
try (DataOutputStream nbt = new DataOutputStream(bytes)) {
|
||||
nbt.writeByte(10);
|
||||
nbt.writeUTF("");
|
||||
nbt.writeByte(3);
|
||||
nbt.writeUTF("xPos");
|
||||
nbt.writeInt(chunkX);
|
||||
nbt.writeByte(3);
|
||||
nbt.writeUTF("zPos");
|
||||
nbt.writeInt(chunkZ);
|
||||
nbt.writeByte(8);
|
||||
nbt.writeUTF("Status");
|
||||
nbt.writeUTF("minecraft:full");
|
||||
nbt.writeByte(7);
|
||||
nbt.writeUTF(NativeTerrainReceiptStorage.NBT_KEY);
|
||||
nbt.writeInt(receipt.length);
|
||||
nbt.write(receipt);
|
||||
nbt.writeByte(0);
|
||||
}
|
||||
Path region = Files.createDirectories(world.resolve("region")).resolve(
|
||||
"r." + (chunkX >> 5) + "." + (chunkZ >> 5) + ".mca");
|
||||
byte[] payload = bytes.toByteArray();
|
||||
int sectors = (payload.length + 5 + 4095) / 4096;
|
||||
require(sectors <= 255, "Receipt exceeds MCA sector limit");
|
||||
try (RandomAccessFile output = new RandomAccessFile(region.toFile(), "rw")) {
|
||||
if (output.length() == 0) {
|
||||
output.setLength(8192);
|
||||
}
|
||||
int offset = Math.toIntExact((output.length() + 4095) / 4096);
|
||||
output.setLength((long) (offset + sectors) * 4096);
|
||||
output.seek((long) offset * 4096);
|
||||
output.writeInt(payload.length + 1);
|
||||
output.writeByte(3);
|
||||
output.write(payload);
|
||||
output.seek((long) ((chunkX & 31) + (chunkZ & 31) * 32) * 4);
|
||||
output.writeInt(offset << 8 | sectors);
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyPack(Path source, Path destination) throws IOException {
|
||||
Files.walkFileTree(source, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE,
|
||||
new SimpleFileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path directory, BasicFileAttributes attributes) throws IOException {
|
||||
Files.createDirectory(destination.resolve(source.relativize(directory)));
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException {
|
||||
Files.copy(file, destination.resolve(source.relativize(file)));
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void requireVanillaBiomes(IrisData data, IrisDimension dimension) {
|
||||
require(!dimension.getAllBiomes(() -> data).stream().anyMatch(biome -> biome.isCustom()),
|
||||
"Transition probe requires vanilla physical biomes");
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) {
|
||||
throw new IllegalStateException(message);
|
||||
}
|
||||
}
|
||||
|
||||
record Configuration(Path packA, Path packB, String dimension, long seed, int width, int chunks, Path output, boolean reverse) {
|
||||
static Configuration parse(String[] arguments) {
|
||||
if (arguments.length != 7 && arguments.length != 8) {
|
||||
throw new IllegalArgumentException("Expected packA packB dimension seed width chunks output [reverse]");
|
||||
}
|
||||
int width = Integer.parseInt(arguments[4]);
|
||||
int chunks = Integer.parseInt(arguments[5]);
|
||||
if (width < 16 || width > 128 || width % 16 != 0 || chunks < 1 || chunks > 8 || chunks * 16 > width) {
|
||||
throw new IllegalArgumentException("Width must be 16..128 in multiples of 16; chunks must be 1..8 within width");
|
||||
}
|
||||
Path packA = Path.of(arguments[0]).toAbsolutePath().normalize();
|
||||
Path packB = Path.of(arguments[1]).toAbsolutePath().normalize();
|
||||
if (!Files.isDirectory(packA) || !Files.isDirectory(packB)) {
|
||||
throw new IllegalArgumentException("Both source packs must be existing directories");
|
||||
}
|
||||
Path output = Path.of(arguments[6]).toAbsolutePath().normalize();
|
||||
if (Files.exists(output) || !Files.isDirectory(output.getParent())) {
|
||||
throw new IllegalArgumentException("Output must be a new directory beneath an existing parent");
|
||||
}
|
||||
if (arguments[2].isBlank()) {
|
||||
throw new IllegalArgumentException("Dimension cannot be blank");
|
||||
}
|
||||
return new Configuration(packA, packB, arguments[2], Long.parseLong(arguments[3]),
|
||||
width, chunks, output, arguments.length == 8
|
||||
&& RealPackProbeSupport.parseBoolean(arguments[7], "reverse"));
|
||||
}
|
||||
}
|
||||
|
||||
private record Evidence(List<String> chunks, List<String> transect, List<String> sections, List<String> materials) {
|
||||
}
|
||||
|
||||
private record GenerationRequest(IrisEngine engine, Bindings bindings, int x, int z, String phase,
|
||||
boolean band, long deadline) {
|
||||
}
|
||||
|
||||
private static final class Bindings implements RealPackProbeSupport.RuntimeBindings {
|
||||
private final Map<Long, SavedTerrainChunk> terrain = new HashMap<>();
|
||||
private int captures;
|
||||
|
||||
@Override
|
||||
public IrisPlatform create(File platformRoot) {
|
||||
return new StubPlatform(platformRoot);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare(IrisData data, IrisDimension dimension) {
|
||||
requireVanillaBiomes(data, dimension);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GenerationHistory createHistory(RealPackProbeSupport.HistoryRequest request) throws IOException {
|
||||
Files.createDirectories(request.worldRoot());
|
||||
Path pack = request.data().getDataFolder().toPath();
|
||||
return GenerationHistory.create(request.worldRoot(), pack,
|
||||
GenerationPackFingerprint.compute(pack, GenerationPackFingerprint.CURRENT_VERSION), request.seed(),
|
||||
GenerationEpochContractFactory.create(request.dimension(), request.dimension().getLoadKey(),
|
||||
"iris:transition_probe"), GenerationRegistryContract.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<SavedTerrainChunk> captureSavedTerrainChunk(Engine engine, int chunkX, int chunkZ) {
|
||||
captures++;
|
||||
SavedTerrainChunk chunk = terrain.get(ChunkGenerationOwnership.packChunk(chunkX, chunkZ));
|
||||
return chunk == null ? CompletableFuture.failedFuture(new IOException("Missing generated historical chunk"))
|
||||
: CompletableFuture.completedFuture(chunk);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> flushSavedTerrainCapture(Engine engine) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
terrain.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.EngineTarget;
|
||||
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.structure.placement.StructurePlacementMarker;
|
||||
import art.arcane.iris.testsupport.IrisRuntimeState;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.iris.world.history.GenerationEpochContractFactory;
|
||||
import art.arcane.iris.world.history.GenerationHistory;
|
||||
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.iris.world.history.GenerationPackFingerprint;
|
||||
import art.arcane.iris.world.history.GenerationRegistryContract;
|
||||
import art.arcane.iris.world.history.IrisBoundarySignatureSampler;
|
||||
import art.arcane.iris.world.history.NativeTerrainReceipt;
|
||||
import art.arcane.iris.world.history.SavedTerrainChunk;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class ObjectContinuationProbeTest {
|
||||
private static final long SEED = 1338;
|
||||
private static final Fixture TREE = new Fixture("boundary-tree", "trees/boundary", Set.of("minecraft:oak_log", "minecraft:oak_leaves"), true);
|
||||
private static final Fixture STRUCTURE = new Fixture("boundary-structure", "buildings/boundary", Set.of("minecraft:bricks", "minecraft:oak_planks", "minecraft:chest"), false);
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporary = new TemporaryFolder();
|
||||
|
||||
@AfterClass
|
||||
public static void resetRuntime() {
|
||||
IrisRuntimeState.reset();
|
||||
}
|
||||
|
||||
@Test(timeout = 120_000)
|
||||
public void removedTreeCompletesOnlyItsPublishedPlacementsAfterRestart() throws Exception {
|
||||
verifyCompletion(false, TREE);
|
||||
}
|
||||
|
||||
@Test(timeout = 120_000)
|
||||
public void originalTreeSurvivesTwoUpdatesBeforeItsTailGenerates() throws Exception {
|
||||
verifyCompletion(true, TREE);
|
||||
}
|
||||
|
||||
@Test(timeout = 120_000)
|
||||
public void removedStructureCompletesItsCarveAndFoundationAfterRestart() throws Exception {
|
||||
verifyCompletion(false, STRUCTURE);
|
||||
}
|
||||
|
||||
private void verifyCompletion(boolean secondUpdate, Fixture fixture) throws Exception {
|
||||
Map<Position, ObjectVoxel> controlOld;
|
||||
Map<Position, ObjectVoxel> controlNew;
|
||||
GeneratedSnapshot nativeSource;
|
||||
GeneratedSnapshot nativeDestination;
|
||||
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(
|
||||
fixture(fixture.pack()).toFile(), "transition", "[object-control]");
|
||||
RealPackProbeSupport.EngineSession session = workspace.openEngine(SEED, true, "control")) {
|
||||
nativeSource = generatedObjects(session.engine(), 0, fixture);
|
||||
controlOld = nativeSource.objects();
|
||||
nativeDestination = generatedObjects(session.engine(), 1, fixture);
|
||||
controlNew = nativeDestination.objects();
|
||||
}
|
||||
Set<String> historicalPlacements = new HashSet<>();
|
||||
for (ObjectVoxel voxel : controlOld.values()) {
|
||||
historicalPlacements.add(voxel.marker());
|
||||
}
|
||||
Map<Position, ObjectVoxel> expectedTail = new HashMap<>();
|
||||
Map<Position, ObjectVoxel> unrelated = new HashMap<>();
|
||||
for (Map.Entry<Position, ObjectVoxel> entry : controlNew.entrySet()) {
|
||||
(historicalPlacements.contains(entry.getValue().marker()) ? expectedTail : unrelated)
|
||||
.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
assertFalse("Fixture must have old object blocks in the saved chunk", controlOld.isEmpty());
|
||||
assertFalse("Fixture must have an old placement crossing into the next chunk", expectedTail.isEmpty());
|
||||
if (fixture.tree()) {
|
||||
assertFalse("Fixture must include old placements wholly inside the new area", unrelated.isEmpty());
|
||||
Map<String, Position> centers = new HashMap<>();
|
||||
Map<Position, ObjectVoxel> control = new HashMap<>(controlOld);
|
||||
control.putAll(controlNew);
|
||||
int trunkY = control.keySet().stream().mapToInt(Position::y).min().orElseThrow();
|
||||
for (Map.Entry<Position, ObjectVoxel> entry : control.entrySet()) {
|
||||
if (entry.getKey().y() == trunkY) {
|
||||
centers.put(entry.getValue().marker(), entry.getKey());
|
||||
}
|
||||
}
|
||||
boolean crossingFromNewSource = false;
|
||||
boolean unrelatedFromNewSource = false;
|
||||
for (Map.Entry<String, Position> entry : centers.entrySet()) {
|
||||
if (entry.getValue().x() >= 16 && entry.getValue().x() < 32) {
|
||||
crossingFromNewSource |= historicalPlacements.contains(entry.getKey());
|
||||
unrelatedFromNewSource |= !historicalPlacements.contains(entry.getKey());
|
||||
}
|
||||
}
|
||||
assertTrue("Fixture must distinguish two placements from the same new source chunk: " + centers,
|
||||
crossingFromNewSource && unrelatedFromNewSource);
|
||||
} else {
|
||||
assertTrue("Structure fixture must place foundations", nativeDestination.foundations() > 0);
|
||||
assertTrue("Structure fixture must carve solid ground", nativeDestination.carvedGround() > 0);
|
||||
}
|
||||
|
||||
Bindings bindings = new Bindings();
|
||||
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(
|
||||
new RealPackProbeSupport.WorkspaceOptions(fixture(fixture.pack()).toFile(), "transition",
|
||||
"[object-update]", bindings, temporary.getRoot().toPath()))) {
|
||||
IrisWorld world;
|
||||
byte[] oldReceipt;
|
||||
long originalActivation;
|
||||
try (RealPackProbeSupport.EngineSession session = workspace.openEngine(SEED, true, "original")) {
|
||||
IrisEngine engine = (IrisEngine) session.engine();
|
||||
GenerationHistoryRuntimeRouter router = engine.getGenerationHistoryRuntimeRouter().orElseThrow();
|
||||
originalActivation = router.history().activeActivation().activationId();
|
||||
assertEquals(controlOld, generateClaimed(engine, bindings, 0, fixture).objects());
|
||||
world = engine.getWorld();
|
||||
oldReceipt = SavedTerrainChunk.readReceipt(world.worldFolder().toPath(), 0, 0);
|
||||
Path replacement = workspace.pack().toPath().getParent().resolve("replacement");
|
||||
copyPack(fixture("lowland"), replacement);
|
||||
IrisData replacementData = IrisData.openRuntime(replacement.toFile());
|
||||
try {
|
||||
IrisDimension dimension = replacementData.getDimensionLoader().load("transition");
|
||||
assertNotNull(dimension);
|
||||
router.history().stageUpdate(replacement,
|
||||
GenerationPackFingerprint.compute(replacement, GenerationPackFingerprint.CURRENT_VERSION),
|
||||
GenerationEpochContractFactory.create(dimension, "transition", "iris:object_probe"),
|
||||
GenerationRegistryContract.empty(), 32);
|
||||
} finally {
|
||||
replacementData.close();
|
||||
}
|
||||
try (GenerationHistoryRuntimeRouter.StudioCutover cutover = router.beginStudioCutover(30_000)) {
|
||||
assertTrue(cutover.promotePending().activationId() > originalActivation);
|
||||
}
|
||||
}
|
||||
GenerationHistory reopened = GenerationHistory.open(world.worldFolder().toPath(), SEED);
|
||||
if (secondUpdate) {
|
||||
Path third = workspace.pack().toPath().getParent().resolve("third-pack");
|
||||
copyPack(fixture("mountain"), third);
|
||||
IrisData thirdData = IrisData.openRuntime(third.toFile());
|
||||
try {
|
||||
IrisDimension dimension = thirdData.getDimensionLoader().load("transition");
|
||||
assertNotNull(dimension);
|
||||
reopened.stageUpdate(third,
|
||||
GenerationPackFingerprint.compute(third, GenerationPackFingerprint.CURRENT_VERSION),
|
||||
GenerationEpochContractFactory.create(dimension, "transition", "iris:object_probe"),
|
||||
GenerationRegistryContract.empty(), 32);
|
||||
} finally {
|
||||
thirdData.close();
|
||||
}
|
||||
reopened.prepareCurrentGenerator(32);
|
||||
reopened = GenerationHistory.open(world.worldFolder().toPath(), SEED);
|
||||
assertTrue(reopened.activeActivation().activationId() > originalActivation + 1);
|
||||
}
|
||||
IrisData data = IrisData.openRuntime(reopened.activePackRoot().toFile());
|
||||
try {
|
||||
data.bindGenerationRegistryContract(reopened.activeEpoch().registryContract());
|
||||
IrisDimension dimension = data.getDimensionLoader().load("transition");
|
||||
assertNotNull(dimension);
|
||||
IrisEngine engine = new IrisEngine(new EngineTarget(world, dimension, data),
|
||||
IrisEngine.InitializationMode.RUNTIME,
|
||||
reopened.paths().activationMantleRoot(reopened.activeActivation().activationId()),
|
||||
reopened.activeEpoch().kernelVersion(), reopened.transitionPlan(reopened.activeActivation().activationId()));
|
||||
try {
|
||||
GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach(
|
||||
engine, reopened, IrisBoundarySignatureSampler.INSTANCE);
|
||||
router.preloadActiveRuntimes();
|
||||
assertEquals("Old object blocks and metadata must reload before neighboring generation",
|
||||
controlOld, historicalObjects(engine, fixture));
|
||||
assertHistoricalEdits(engine, nativeSource.edits());
|
||||
GeneratedSnapshot result = generateClaimed(engine, bindings, 1, fixture);
|
||||
Map<Position, ObjectVoxel> completed = result.objects();
|
||||
if (!fixture.tree()) {
|
||||
for (Map.Entry<Position, String> edit : nativeDestination.edits().entrySet()) {
|
||||
assertEquals("Structure carve/foundation at " + edit.getKey(), edit.getValue(),
|
||||
result.blocks().get(edit.getKey()));
|
||||
}
|
||||
assertTrue("Completed storage blocks must retain structure-aware placement markers",
|
||||
completed.values().stream().anyMatch(voxel -> voxel.marker() != null));
|
||||
}
|
||||
assertEquals("Complete only placements with a published old portion", expectedTail, completed);
|
||||
for (Map.Entry<Position, ObjectVoxel> entry : unrelated.entrySet()) {
|
||||
assertFalse("Unrelated old placement was imported at " + entry.getKey(),
|
||||
completed.containsKey(entry.getKey()));
|
||||
}
|
||||
assertEquals("Old geometry and metadata must survive neighboring generation",
|
||||
controlOld, historicalObjects(engine, fixture));
|
||||
assertHistoricalEdits(engine, nativeSource.edits());
|
||||
assertArrayEquals(oldReceipt, SavedTerrainChunk.readReceipt(world.worldFolder().toPath(), 0, 0));
|
||||
assertEquals(originalActivation, reopened.resolveActivation(0, 0).activationId());
|
||||
assertTrue(reopened.semantics(1, 0).orElseThrow().objectKeys().contains(fixture.objectKey()));
|
||||
assertTrue("Generation must not report swallowed errors", RealPackProbeSupport.settleAndDrain().isEmpty());
|
||||
} finally {
|
||||
engine.close();
|
||||
}
|
||||
} finally {
|
||||
data.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static GeneratedSnapshot generateClaimed(IrisEngine engine, Bindings bindings, int chunkX, Fixture fixture)
|
||||
throws Exception {
|
||||
GenerationHistoryRuntimeRouter router = engine.getGenerationHistoryRuntimeRouter().orElseThrow();
|
||||
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = router.openRoute(chunkX, 0);
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope scope = route.openRuntimeScope()) {
|
||||
GeneratedSnapshot result = generatedObjects(engine, chunkX, fixture);
|
||||
assertTrue(route.claimGeneratedSemantics());
|
||||
SavedTerrainChunk terrain = route.naturalTerrain().orElseThrow();
|
||||
bindings.terrain.put(chunkX, terrain);
|
||||
TransitionProbe.writeReceipt(engine.getWorld().worldFolder().toPath(), chunkX, 0,
|
||||
NativeTerrainReceipt.encode(terrain, route.activation().activationId(), route.epoch().epochId()));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private static GeneratedSnapshot generatedObjects(Engine engine, int chunkX, Fixture fixture) {
|
||||
RealPackProbeSupport.GeneratedChunk chunk = RealPackProbeSupport.generateChunk(engine, chunkX, 0);
|
||||
Map<Position, ObjectVoxel> result = objectMetadata(engine, chunkX, fixture);
|
||||
for (Map.Entry<Position, ObjectVoxel> entry : result.entrySet()) {
|
||||
Position position = entry.getKey();
|
||||
NativeBlockState state = chunk.blocks().getRaw(position.x() & 15, position.y(), position.z());
|
||||
assertNotNull("Object metadata must correspond to a generated block", state);
|
||||
assertEquals(entry.getValue().block(), state.key());
|
||||
}
|
||||
Map<Position, String> blocks = new HashMap<>();
|
||||
Map<Position, String> edits = new HashMap<>();
|
||||
int objectBlocks = 0;
|
||||
int foundations = 0;
|
||||
int carvedGround = 0;
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int y = 0; y < chunk.height(); y++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
NativeBlockState state = chunk.blocks().getRaw(x, y, z);
|
||||
if (state != null && fixture.materials().contains(state.key().split("\\[", 2)[0])) {
|
||||
objectBlocks++;
|
||||
}
|
||||
if (!fixture.tree()) {
|
||||
Position position = new Position(chunkX * 16 + x, y, z);
|
||||
String block = state == null || state.isAir() ? "minecraft:air" : state.key();
|
||||
blocks.put(position, block);
|
||||
if (state != null && state.key().contains("cobblestone")) {
|
||||
foundations++;
|
||||
edits.put(position, block);
|
||||
}
|
||||
if (y > 1 && y <= 72 && (state == null || state.isAir())) {
|
||||
carvedGround++;
|
||||
edits.put(position, block);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assertEquals("Every generated object block must keep placement metadata", objectBlocks, result.size());
|
||||
return new GeneratedSnapshot(result, blocks, edits, foundations, carvedGround);
|
||||
}
|
||||
|
||||
private static Map<Position, ObjectVoxel> historicalObjects(IrisEngine engine, Fixture fixture) throws Exception {
|
||||
GenerationHistoryRuntimeRouter router = engine.getGenerationHistoryRuntimeRouter().orElseThrow();
|
||||
try (GenerationHistoryRuntimeRouter.SavedChunkMantle saved = router.openSavedChunkMantle(0, 0)) {
|
||||
return objectMetadata(saved.mantle().getChunk(0, 0), 0, engine.getHeight(), fixture);
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertHistoricalEdits(IrisEngine engine, Map<Position, String> edits) throws Exception {
|
||||
if (edits.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
GenerationHistoryRuntimeRouter router = engine.getGenerationHistoryRuntimeRouter().orElseThrow();
|
||||
try (GenerationHistoryRuntimeRouter.SavedChunkMantle saved = router.openSavedChunkMantle(0, 0)) {
|
||||
MantleChunk<Matter> chunk = saved.mantle().getChunk(0, 0).use();
|
||||
try {
|
||||
for (Map.Entry<Position, String> edit : edits.entrySet()) {
|
||||
Position position = edit.getKey();
|
||||
NativeBlockState block = chunk.get(position.x(), position.y(), position.z(), NativeBlockState.class);
|
||||
assertNotNull("Historical carve/foundation must reload at " + position, block);
|
||||
assertEquals("Historical carve/foundation changed at " + position, edit.getValue(),
|
||||
block.isAir() ? "minecraft:air" : block.key());
|
||||
}
|
||||
} finally {
|
||||
chunk.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<Position, ObjectVoxel> objectMetadata(Engine engine, int chunkX, Fixture fixture) {
|
||||
return objectMetadata(engine.getMantle().getMantle().getChunk(chunkX, 0), chunkX, engine.getHeight(), fixture);
|
||||
}
|
||||
|
||||
private static Map<Position, ObjectVoxel> objectMetadata(MantleChunk<Matter> chunk, int chunkX, int height, Fixture fixture) {
|
||||
Map<Position, ObjectVoxel> result = new HashMap<>();
|
||||
chunk.use();
|
||||
try {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
String marker = chunk.get(x, y, z, String.class);
|
||||
StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(marker);
|
||||
NativeBlockState block = chunk.get(x, y, z, NativeBlockState.class);
|
||||
boolean objectBlock = block != null && fixture.materials().contains(block.key().split("\\[", 2)[0]);
|
||||
if (decoded != null && decoded.objectKey().equals(fixture.objectKey()) || !fixture.tree() && objectBlock) {
|
||||
TreeBlockMaterial material = chunk.get(x, y, z, TreeBlockMaterial.class);
|
||||
assertNotNull("Placement marker must retain its object block", block);
|
||||
if (fixture.tree()) {
|
||||
assertNotNull("Tree material metadata must survive", material);
|
||||
assertTrue(material.matches(block.key()));
|
||||
} else if (block.isStorageChest()) {
|
||||
assertNotNull("Structure storage blocks must have placement metadata", decoded);
|
||||
assertEquals("boundary", decoded.structureKey());
|
||||
}
|
||||
result.put(new Position(chunkX * 16 + x, y, z), new ObjectVoxel(block.key(), marker, material));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
chunk.release();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Path fixture(String name) throws Exception {
|
||||
URL resource = Objects.requireNonNull(ObjectContinuationProbeTest.class.getResource("/transition-packs/" + name));
|
||||
return Path.of(resource.toURI());
|
||||
}
|
||||
|
||||
private static void copyPack(Path source, Path destination) throws IOException {
|
||||
try (Stream<Path> paths = Files.walk(source)) {
|
||||
for (Path path : paths.toList()) {
|
||||
Path target = destination.resolve(source.relativize(path));
|
||||
if (Files.isDirectory(path)) {
|
||||
Files.createDirectories(target);
|
||||
} else {
|
||||
Files.copy(path, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record Fixture(String pack, String objectKey, Set<String> materials, boolean tree) {
|
||||
}
|
||||
|
||||
private record GeneratedSnapshot(Map<Position, ObjectVoxel> objects, Map<Position, String> blocks,
|
||||
Map<Position, String> edits, int foundations, int carvedGround) {
|
||||
}
|
||||
|
||||
private record Position(int x, int y, int z) {
|
||||
}
|
||||
|
||||
private record ObjectVoxel(String block, String marker, TreeBlockMaterial material) {
|
||||
}
|
||||
|
||||
private static final class Bindings implements RealPackProbeSupport.RuntimeBindings {
|
||||
private final Map<Integer, SavedTerrainChunk> terrain = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public IrisPlatform create(File platformRoot) {
|
||||
return new StubPlatform(platformRoot);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare(IrisData data, IrisDimension dimension) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public GenerationHistory createHistory(RealPackProbeSupport.HistoryRequest request) throws IOException {
|
||||
Files.createDirectories(request.worldRoot());
|
||||
Path pack = request.data().getDataFolder().toPath();
|
||||
return GenerationHistory.create(request.worldRoot(), pack,
|
||||
GenerationPackFingerprint.compute(pack, GenerationPackFingerprint.CURRENT_VERSION), request.seed(),
|
||||
GenerationEpochContractFactory.create(request.dimension(), "transition", "iris:object_probe"),
|
||||
GenerationRegistryContract.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<SavedTerrainChunk> captureSavedTerrainChunk(Engine engine, int chunkX, int chunkZ) {
|
||||
SavedTerrainChunk chunk = chunkZ == 0 ? terrain.get(chunkX) : null;
|
||||
return chunk == null ? CompletableFuture.failedFuture(new IOException("Missing historical chunk"))
|
||||
: CompletableFuture.completedFuture(chunk);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> flushSavedTerrainCapture(Engine engine) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
terrain.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.testsupport.IrisRuntimeState;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.util.Arrays;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class TransitionFixtureProbeTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporary = new TemporaryFolder();
|
||||
|
||||
@AfterClass
|
||||
public static void resetRuntime() {
|
||||
IrisRuntimeState.reset();
|
||||
}
|
||||
|
||||
@Test(timeout = 120_000)
|
||||
public void lowlandToMountainPreservesHistoricalTrackingAndActivatesTheNewPack() throws Exception {
|
||||
run(new Scenario("lowland", "mountain", 72, -1, 104, -1));
|
||||
}
|
||||
|
||||
@Test(timeout = 120_000)
|
||||
public void oceanToMountainRetainsOldWaterAndProducesDryDistantTerrain() throws Exception {
|
||||
run(new Scenario("ocean", "mountain", 24, 40, 104, -1));
|
||||
}
|
||||
|
||||
@Test(timeout = 120_000)
|
||||
public void mountainToOceanProducesTheReplacementWaterTable() throws Exception {
|
||||
run(new Scenario("mountain", "ocean", 104, -1, 24, 40));
|
||||
}
|
||||
|
||||
@Test(timeout = 120_000)
|
||||
public void waterLevelChangeKeepsTheAuthoredOceanFloor() throws Exception {
|
||||
run(new Scenario("ocean", "ocean-highwater", 24, 40, 24, 48));
|
||||
}
|
||||
|
||||
private void run(Scenario scenario) throws Exception {
|
||||
Path output = temporary.newFolder().toPath().resolve("output");
|
||||
Path originalPack = fixture(scenario.original());
|
||||
Path replacementPack = fixture(scenario.replacement());
|
||||
Map<Path, byte[]> originalSource = snapshot(originalPack);
|
||||
Map<Path, byte[]> replacementSource = snapshot(replacementPack);
|
||||
TransitionProbe.main(new String[]{originalPack.toString(), replacementPack.toString(),
|
||||
"transition", "1337", "32", "2", output.toString()});
|
||||
assertSnapshotEquals(originalSource, snapshot(originalPack));
|
||||
assertSnapshotEquals(replacementSource, snapshot(replacementPack));
|
||||
List<String> chunks = Files.readAllLines(output.resolve("chunks.csv"));
|
||||
assertEquals(9, chunks.size());
|
||||
long originalActivation = Long.parseLong(chunks.get(1).split(",")[3]);
|
||||
int original = 0;
|
||||
int transition = 0;
|
||||
int distant = 0;
|
||||
for (String row : chunks.subList(1, chunks.size())) {
|
||||
String[] cells = row.split(",");
|
||||
long activation = Long.parseLong(cells[3]);
|
||||
boolean band = Boolean.parseBoolean(cells[4]);
|
||||
assertTrue(Long.parseLong(cells[6]) > 0);
|
||||
switch (cells[0]) {
|
||||
case "original" -> {
|
||||
original++;
|
||||
assertEquals(originalActivation, activation);
|
||||
assertTrue(!band);
|
||||
}
|
||||
case "transition" -> {
|
||||
transition++;
|
||||
assertTrue(activation > originalActivation);
|
||||
assertTrue(band);
|
||||
}
|
||||
case "distant" -> {
|
||||
distant++;
|
||||
assertTrue(activation > originalActivation);
|
||||
assertTrue(!band);
|
||||
}
|
||||
default -> throw new AssertionError("Unexpected phase " + cells[0]);
|
||||
}
|
||||
}
|
||||
assertEquals(4, original);
|
||||
assertEquals(2, transition);
|
||||
assertEquals(2, distant);
|
||||
List<String> transect = Files.readAllLines(output.resolve("transect.csv"));
|
||||
assertEquals(97, transect.size());
|
||||
assertFlatSeam(scenario, transect);
|
||||
for (String row : transect.subList(1, transect.size())) {
|
||||
String[] cells = row.split(",");
|
||||
if (cells[0].equals("original")) {
|
||||
assertEquals(row, scenario.originalGround(), Integer.parseInt(cells[3]));
|
||||
assertEquals(row, scenario.originalFluid(), Integer.parseInt(cells[4]));
|
||||
} else if (cells[0].equals("distant")) {
|
||||
assertEquals(row, scenario.replacementGround(), Integer.parseInt(cells[3]));
|
||||
assertEquals(row, scenario.replacementFluid(), Integer.parseInt(cells[4]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 120_000)
|
||||
public void slopedTransitionsKeepChunkOutputAndTrackingIndependentOfGenerationOrder() throws Exception {
|
||||
compareOrders("slope-lowland", "slope-mountain", false);
|
||||
}
|
||||
|
||||
@Test(timeout = 120_000)
|
||||
public void volumetricTransitionsKeepChunkOutputIndependentOfGenerationOrder() throws Exception {
|
||||
compareOrders("volumetric-lowland", "volumetric-mountain", true);
|
||||
}
|
||||
|
||||
@Test(timeout = 120_000)
|
||||
public void nearSeamDisplacementRetainsNewTerrainRippleCurvature() throws Exception {
|
||||
Path original = fixture("lowland");
|
||||
Path replacement = fixture("ripple-mountain");
|
||||
Path output = temporary.newFolder().toPath().resolve("ripple");
|
||||
TransitionProbe.main(new String[]{original.toString(), replacement.toString(), "transition", "1337",
|
||||
"64", "4", output.toString()});
|
||||
Map<Integer, int[]> blended = columns(Files.readAllLines(output.resolve("transect.csv")));
|
||||
Map<Integer, Integer> nativeHeights = new HashMap<>();
|
||||
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(
|
||||
replacement.toFile(), "transition", "[transition-ripple-control]");
|
||||
RealPackProbeSupport.EngineSession session = workspace.openEngine(1337, true, "native-control")) {
|
||||
Map<String, List<String>> displacedChunks = chunkContents(output);
|
||||
for (int chunkX : new int[]{2, 3, 6, 7, 8, 9}) {
|
||||
RealPackProbeSupport.GeneratedChunk chunk = RealPackProbeSupport.generateChunk(session.engine(), chunkX, 0);
|
||||
if (chunkX >= 6) {
|
||||
GenerationOrderProbe.ChunkHash nativeHash = GenerationOrderProbe.hashChunk(
|
||||
new GenerationOrderProbe.ChunkCoordinate(chunkX, 0), chunk.blocks(), chunk.biomes(), chunk.height());
|
||||
assertEquals("Transition must not alter distant voxels", displacedChunks.get(chunkX + ",0").get(0), nativeHash.blocks());
|
||||
assertEquals("Transition must not alter distant physical biomes", displacedChunks.get(chunkX + ",0").get(1), nativeHash.biomes());
|
||||
continue;
|
||||
}
|
||||
for (int x = 0; x < 16; x++) {
|
||||
int ground = -1;
|
||||
for (int y = 0; y < chunk.height(); y++) {
|
||||
NativeBlockState block = chunk.blocks().getRaw(x, y, 8);
|
||||
if (block != null && !block.isAir() && !block.isWater()) {
|
||||
ground = y;
|
||||
}
|
||||
}
|
||||
nativeHeights.put(chunkX * 16 + x, ground);
|
||||
}
|
||||
}
|
||||
}
|
||||
int strongRipples = 0;
|
||||
for (int x = 40; x <= 60; x++) {
|
||||
int nativeCurvature = nativeHeights.get(x - 3) - 2 * nativeHeights.get(x) + nativeHeights.get(x + 3);
|
||||
if (Math.abs(nativeCurvature) >= 6) {
|
||||
strongRipples++;
|
||||
int blendedCurvature = blended.get(x - 3)[0] - 2 * blended.get(x)[0] + blended.get(x + 3)[0];
|
||||
assertTrue("New terrain relief was attenuated near the seam at x=" + x
|
||||
+ " native=" + nativeCurvature + " blended=" + blendedCurvature,
|
||||
Math.abs(nativeCurvature - blendedCurvature) <= 3);
|
||||
}
|
||||
}
|
||||
assertTrue("Control fixture must contain enough measurable ripples", strongRipples >= 5);
|
||||
}
|
||||
|
||||
@Test(timeout = 120_000)
|
||||
public void transitionSolidsUseTheNewPaletteWithoutHistoricalMaterialDither() throws Exception {
|
||||
Path original = fixture("granite-lowland");
|
||||
Path replacement = fixture("calcite-mountain");
|
||||
Path output = temporary.newFolder().toPath().resolve("materials");
|
||||
TransitionProbe.main(new String[]{original.toString(), replacement.toString(), "transition", "1337",
|
||||
"32", "2", output.toString()});
|
||||
int oldSolid = 0;
|
||||
int newSolid = 0;
|
||||
List<String> rows = Files.readAllLines(output.resolve("materials.csv"));
|
||||
for (String row : rows.subList(1, rows.size())) {
|
||||
String[] cells = row.split(",");
|
||||
if (cells[3].equals("minecraft:air") || cells[3].equals("minecraft:cave_air")) {
|
||||
continue;
|
||||
}
|
||||
if (cells[0].equals("original")) {
|
||||
assertEquals(row, "minecraft:granite", cells[3]);
|
||||
oldSolid += Integer.parseInt(cells[4]);
|
||||
} else {
|
||||
assertEquals(row, "minecraft:calcite", cells[3]);
|
||||
newSolid += Integer.parseInt(cells[4]);
|
||||
}
|
||||
}
|
||||
assertTrue(oldSolid > 0);
|
||||
assertTrue(newSolid > 0);
|
||||
}
|
||||
|
||||
@Test(timeout = 120_000)
|
||||
public void oldCaveOpeningContinuesAtTheSeamAndStopsWithinFourBlocks() throws Exception {
|
||||
Path original = fixture("cave-lowland");
|
||||
Path replacement = fixture("lowland");
|
||||
Path output = temporary.newFolder().toPath().resolve("cave");
|
||||
TransitionProbe.main(new String[]{original.toString(), replacement.toString(), "transition", "1337",
|
||||
"32", "2", output.toString()});
|
||||
Map<Integer, String[]> sections = sections(Files.readAllLines(output.resolve("sections.csv")));
|
||||
int opening = 0;
|
||||
for (int y = 32; y <= 47; y++) {
|
||||
if (sections.get(31)[y].equals("minecraft:air") || sections.get(31)[y].equals("minecraft:cave_air")) {
|
||||
opening++;
|
||||
assertTrue("Historical cave mouth must continue at the first seam column y=" + y,
|
||||
sections.get(32)[y].equals("minecraft:air") || sections.get(32)[y].equals("minecraft:cave_air"));
|
||||
}
|
||||
assertEquals("Cave patch exceeded four blocks at y=" + y, "minecraft:stone", sections.get(35)[y]);
|
||||
assertEquals("Cave must not be extruded across the transition at y=" + y, "minecraft:stone", sections.get(48)[y]);
|
||||
}
|
||||
assertTrue("Fixture must expose an actual cave mouth at the historical edge", opening >= 12);
|
||||
assertEquals("Cave mouth requires a floor", "minecraft:stone", sections.get(31)[31]);
|
||||
assertEquals("Cave mouth requires a roof", "minecraft:stone", sections.get(31)[48]);
|
||||
}
|
||||
|
||||
private static Map<Integer, String[]> sections(List<String> rows) {
|
||||
Map<Integer, String[]> result = new HashMap<>();
|
||||
for (String row : rows.subList(1, rows.size())) {
|
||||
String[] cells = row.split(",", 6);
|
||||
int x = Integer.parseInt(cells[1]);
|
||||
String[] column = result.computeIfAbsent(x, ignored -> new String[128]);
|
||||
String blockKey = cells[5].contains(":") ? cells[5] : "minecraft:" + cells[5];
|
||||
Arrays.fill(column, Integer.parseInt(cells[3]), Integer.parseInt(cells[4]), blockKey);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void compareOrders(String original, String replacement, boolean volumetric) throws Exception {
|
||||
Path originalPack = fixture(original);
|
||||
Path replacementPack = fixture(replacement);
|
||||
Path forward = temporary.newFolder().toPath().resolve("forward");
|
||||
Path reverse = temporary.newFolder().toPath().resolve("reverse");
|
||||
for (Path output : List.of(forward, reverse)) {
|
||||
TransitionProbe.main(new String[]{originalPack.toString(), replacementPack.toString(), "transition", "1337",
|
||||
"64", "4", output.toString(), Boolean.toString(output.equals(reverse))});
|
||||
}
|
||||
assertEquals(chunkContents(forward), chunkContents(reverse));
|
||||
Map<Integer, int[]> transect = columns(Files.readAllLines(forward.resolve("transect.csv")));
|
||||
assertEquals("Transect must cover both joins without missing columns", 160, transect.size());
|
||||
if (volumetric) {
|
||||
assertTrue("Volumetric fixture must contain more than one solid span",
|
||||
transect.values().stream().anyMatch(column -> column[2] > 1));
|
||||
} else {
|
||||
assertTrue("Old-side fixture must have a non-flat slope", transect.get(31)[0] != transect.get(0)[0]);
|
||||
assertTrue("New-side fixture must have a non-flat slope", transect.get(159)[0] != transect.get(96)[0]);
|
||||
assertTrue("Height must meet historical ground at the seam", Math.abs(transect.get(32)[0] - transect.get(31)[0]) <= 2);
|
||||
assertTrue("Height must meet native ground at the end of the band", Math.abs(transect.get(96)[0] - transect.get(95)[0]) <= 2);
|
||||
for (int x = 32; x < 96; x++) {
|
||||
assertTrue("Slope step at x=" + x, Math.abs(transect.get(x)[0] - transect.get(x - 1)[0]) <= 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, List<String>> chunkContents(Path output) throws Exception {
|
||||
Map<String, List<String>> result = new HashMap<>();
|
||||
List<String> rows = Files.readAllLines(output.resolve("chunks.csv"));
|
||||
for (String row : rows.subList(1, rows.size())) {
|
||||
String[] cells = row.split(",");
|
||||
result.put(cells[1] + "," + cells[2], Arrays.asList(cells).subList(7, 11));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void assertFlatSeam(Scenario scenario, List<String> rows) {
|
||||
Map<Integer, int[]> columns = columns(rows);
|
||||
assertEquals("Transect must cover both joins without missing columns", 96, columns.size());
|
||||
int difference = scenario.replacementGround() - scenario.originalGround();
|
||||
int maximumStep = (int) Math.ceil(Math.abs(difference) * 1.875 / 32) + 1;
|
||||
assertTrue("First new column must meet historical ground: old=" + columns.get(31)[0]
|
||||
+ " new=" + columns.get(32)[0], Math.abs(columns.get(32)[0] - columns.get(31)[0]) <= 1);
|
||||
assertTrue("Band must reach native new ground: band=" + columns.get(63)[0]
|
||||
+ " native=" + columns.get(64)[0], Math.abs(columns.get(64)[0] - columns.get(63)[0]) <= 1);
|
||||
for (int x = 32; x < 64; x++) {
|
||||
int[] column = columns.get(x);
|
||||
int step = column[0] - columns.get(x - 1)[0];
|
||||
assertTrue("Excessive terrain step at x=" + x + " step=" + step + " maximum=" + maximumStep, Math.abs(step) <= maximumStep);
|
||||
assertTrue("Terrain overshoot at x=" + x, column[0] >= Math.min(scenario.originalGround(), scenario.replacementGround())
|
||||
&& column[0] <= Math.max(scenario.originalGround(), scenario.replacementGround()));
|
||||
assertEquals("Floating water or a dry gap below water at x=" + x, 0, column[3]);
|
||||
if (difference == 0) {
|
||||
assertEquals("Fluid-level transition must not create a stone dam at x=" + x, scenario.originalGround(), column[0]);
|
||||
assertTrue("Fluid-level transition must remain wet at x=" + x, column[1] > column[0]);
|
||||
assertTrue("Water level must stay within the old/new range at x=" + x,
|
||||
column[1] >= Math.min(scenario.originalFluid(), scenario.replacementFluid())
|
||||
&& column[1] <= Math.max(scenario.originalFluid(), scenario.replacementFluid()));
|
||||
assertTrue("Water level must rise monotonically at x=" + x, column[1] >= columns.get(x - 1)[1]);
|
||||
assertTrue("Water step at x=" + x, column[1] - columns.get(x - 1)[1] <= 1);
|
||||
} else {
|
||||
assertTrue("Transition must not create an isolated wall or pit at x=" + x + " step=" + step,
|
||||
difference > 0 ? step >= 0 : step <= 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<Integer, int[]> columns(List<String> rows) {
|
||||
Map<Integer, int[]> result = new TreeMap<>();
|
||||
for (String row : rows.subList(1, rows.size())) {
|
||||
String[] cells = row.split(",");
|
||||
int[] values = {Integer.parseInt(cells[3]), Integer.parseInt(cells[4]),
|
||||
Integer.parseInt(cells[5]), Integer.parseInt(cells[6])};
|
||||
assertEquals("Duplicate transect column", null, result.put(Integer.parseInt(cells[1]), values));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Map<Path, byte[]> snapshot(Path root) throws Exception {
|
||||
Map<Path, byte[]> entries = new TreeMap<>();
|
||||
try (Stream<Path> paths = Files.walk(root)) {
|
||||
for (Path path : paths.toList()) {
|
||||
entries.put(root.relativize(path), Files.isDirectory(path) ? null : Files.readAllBytes(path));
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static void assertSnapshotEquals(Map<Path, byte[]> expected, Map<Path, byte[]> actual) {
|
||||
assertEquals(expected.keySet(), actual.keySet());
|
||||
for (Map.Entry<Path, byte[]> entry : expected.entrySet()) {
|
||||
assertArrayEquals(entry.getKey().toString(), entry.getValue(), actual.get(entry.getKey()));
|
||||
}
|
||||
}
|
||||
|
||||
private Path fixture(String name) throws Exception {
|
||||
URL resource = Objects.requireNonNull(TransitionFixtureProbeTest.class.getClassLoader()
|
||||
.getResource("transition-packs/" + name), "Transition fixture " + name);
|
||||
Path source = Path.of(resource.toURI());
|
||||
Path pack = temporary.newFolder(name).toPath();
|
||||
try (Stream<Path> paths = Files.walk(source)) {
|
||||
for (Path file : paths.filter(path -> Files.isRegularFile(path) && path.toString().endsWith(".json")
|
||||
&& !source.relativize(path).toString().startsWith(".")).toList()) {
|
||||
Path target = pack.resolve(source.relativize(file));
|
||||
Files.createDirectories(target.getParent());
|
||||
Files.copy(file, target);
|
||||
}
|
||||
}
|
||||
Files.write(pack.resolve(".source-content"), new byte[]{0, 17, -128, 127});
|
||||
return pack;
|
||||
}
|
||||
|
||||
private record Scenario(String original, String replacement, int originalGround, int originalFluid,
|
||||
int replacementGround, int replacementFluid) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.world.history.BoundaryColumnGeometry;
|
||||
import art.arcane.iris.world.history.NativeTerrainReceipt;
|
||||
import art.arcane.iris.world.history.SavedTerrainChunk;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Files;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
public final class TransitionProbeTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporary = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void rejectsWorkloadsOutsideTheBandAndExistingOutputs() throws Exception {
|
||||
Path root = temporary.newFolder().toPath();
|
||||
String[] arguments = {root.toString(), root.toString(), "overworld", "1337", "32", "2", root.resolve("result").toString()};
|
||||
assertEquals(2, TransitionProbe.Configuration.parse(arguments).chunks());
|
||||
arguments[5] = "3";
|
||||
assertThrows(IllegalArgumentException.class, () -> TransitionProbe.Configuration.parse(arguments));
|
||||
arguments[5] = "2";
|
||||
arguments[4] = "8192";
|
||||
assertThrows(IllegalArgumentException.class, () -> TransitionProbe.Configuration.parse(arguments));
|
||||
arguments[4] = "32";
|
||||
arguments[6] = root.toString();
|
||||
assertThrows(IllegalArgumentException.class, () -> TransitionProbe.Configuration.parse(arguments));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingSourcesAreRejectedWithoutCreatingSourceOrOutputDirectories() throws Exception {
|
||||
Path root = temporary.newFolder().toPath();
|
||||
for (int missingIndex : new int[]{0, 1}) {
|
||||
Path missing = root.resolve("missing-" + missingIndex);
|
||||
Path output = root.resolve("output-" + missingIndex);
|
||||
String[] arguments = {root.toString(), root.toString(), "transition", "1337", "32", "2", output.toString()};
|
||||
arguments[missingIndex] = missing.toString();
|
||||
assertThrows(IllegalArgumentException.class, () -> TransitionProbe.main(arguments));
|
||||
assertFalse(Files.exists(missing));
|
||||
assertFalse(Files.exists(output));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void persistsActualReceiptsAcrossNegativeRegionBoundariesAndMultipleChunks() throws Exception {
|
||||
Path world = temporary.newFolder().toPath();
|
||||
SavedTerrainChunk.VoxelSource source = new SavedTerrainChunk.VoxelSource() {
|
||||
@Override
|
||||
public BoundaryColumnGeometry.Voxel voxel(int localX, int worldY, int localZ) {
|
||||
return worldY <= 5
|
||||
? new BoundaryColumnGeometry.Voxel("minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false)
|
||||
: new BoundaryColumnGeometry.Voxel("minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String biome(int localX, int worldY, int localZ) {
|
||||
return "minecraft:plains";
|
||||
}
|
||||
};
|
||||
for (int x : new int[]{-33, -32, -31, 0, 1, 32}) {
|
||||
SavedTerrainChunk chunk = SavedTerrainChunk.captureBoundary(x, -1, 0, 16, "minecraft:full", source);
|
||||
TransitionProbe.writeReceipt(world, x, -1, NativeTerrainReceipt.encode(chunk, 7, "epoch"));
|
||||
}
|
||||
for (int x : new int[]{-33, -32, -31, 0, 1, 32}) {
|
||||
assertEquals("minecraft:full", SavedTerrainChunk.readStatus(world, x, -1));
|
||||
assertEquals(5, SavedTerrainChunk.read(world, x, -1, 0, 16).column(x * 16, -16).oceanFloorHeight());
|
||||
assertEquals(7, NativeTerrainReceipt.decode(SavedTerrainChunk.readReceipt(world, x, -1), "minecraft:full").activationId());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "Terrain",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"layers": [
|
||||
{
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:stone"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{
|
||||
"generator": "flat",
|
||||
"min": 32,
|
||||
"max": 32
|
||||
}
|
||||
]
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "Boundary structure transition fixture",
|
||||
"version": 1,
|
||||
"mode": {
|
||||
"type": "OVERWORLD"
|
||||
},
|
||||
"regions": [
|
||||
"terrain"
|
||||
],
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": {
|
||||
"min": 0,
|
||||
"max": 128
|
||||
},
|
||||
"logicalHeight": 128,
|
||||
"fluidHeight": 40,
|
||||
"caveProfile": {
|
||||
"enabled": false
|
||||
},
|
||||
"structures": [
|
||||
{
|
||||
"structures": [
|
||||
"boundary"
|
||||
],
|
||||
"distribution": "RANDOM_SPREAD",
|
||||
"spacing": 4,
|
||||
"separation": 3,
|
||||
"anchor": "HEIGHT_BAND",
|
||||
"minHeight": 48,
|
||||
"maxHeight": 64,
|
||||
"terrain": {
|
||||
"mode": "BORE",
|
||||
"horizontalPadding": 2,
|
||||
"floorPadding": 10,
|
||||
"ceilingPadding": 2
|
||||
},
|
||||
"stilt": {
|
||||
"maxDepth": 20,
|
||||
"palette": {
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:cobblestone"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"seed": 310,
|
||||
"interpolator": {
|
||||
"function": "NONE",
|
||||
"horizontalScale": 1
|
||||
},
|
||||
"composite": [
|
||||
{
|
||||
"seed": 310,
|
||||
"style": {
|
||||
"style": "FLAT"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"object": "buildings/boundary",
|
||||
"rotatable": false
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"pieces": [
|
||||
{
|
||||
"piece": "boundary",
|
||||
"weight": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Terrain",
|
||||
"landBiomes": [
|
||||
"terrain"
|
||||
],
|
||||
"seaBiomes": [
|
||||
"terrain"
|
||||
],
|
||||
"shoreBiomes": [
|
||||
"terrain"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"startPool": "boundary",
|
||||
"maxDepth": 1,
|
||||
"maxSizeChunks": 2
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "Terrain",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"layers": [
|
||||
{
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:stone"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{
|
||||
"generator": "flat",
|
||||
"min": 32,
|
||||
"max": 32
|
||||
}
|
||||
],
|
||||
"objects": [
|
||||
{
|
||||
"place": [
|
||||
"trees/boundary"
|
||||
],
|
||||
"chance": 1,
|
||||
"density": 2,
|
||||
"rotation": {
|
||||
"enabled": false
|
||||
},
|
||||
"forcePlace": true,
|
||||
"requireSurfaceSupport": false,
|
||||
"translate": {
|
||||
"y": 4
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "Boundary tree transition fixture",
|
||||
"version": 1,
|
||||
"mode": {
|
||||
"type": "OVERWORLD"
|
||||
},
|
||||
"regions": [
|
||||
"terrain"
|
||||
],
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": {
|
||||
"min": 0,
|
||||
"max": 128
|
||||
},
|
||||
"logicalHeight": 128,
|
||||
"fluidHeight": 40,
|
||||
"caveProfile": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"seed": 310,
|
||||
"interpolator": {
|
||||
"function": "NONE",
|
||||
"horizontalScale": 1
|
||||
},
|
||||
"composite": [
|
||||
{
|
||||
"seed": 310,
|
||||
"style": {
|
||||
"style": "FLAT"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Terrain",
|
||||
"landBiomes": [
|
||||
"terrain"
|
||||
],
|
||||
"seaBiomes": [
|
||||
"terrain"
|
||||
],
|
||||
"shoreBiomes": [
|
||||
"terrain"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "Terrain",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"layers": [
|
||||
{
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:calcite"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{
|
||||
"generator": "flat",
|
||||
"min": 64,
|
||||
"max": 64
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "Mountain transition fixture",
|
||||
"version": 1,
|
||||
"mode": {
|
||||
"type": "OVERWORLD"
|
||||
},
|
||||
"regions": [
|
||||
"terrain"
|
||||
],
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": {
|
||||
"min": 0,
|
||||
"max": 128
|
||||
},
|
||||
"logicalHeight": 128,
|
||||
"fluidHeight": 40,
|
||||
"caveProfile": {
|
||||
"enabled": false
|
||||
},
|
||||
"rockPalette": {
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:calcite"
|
||||
}
|
||||
]
|
||||
},
|
||||
"bedrock": false
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"seed": 310,
|
||||
"interpolator": {
|
||||
"function": "NONE",
|
||||
"horizontalScale": 1
|
||||
},
|
||||
"composite": [
|
||||
{
|
||||
"seed": 310,
|
||||
"style": {
|
||||
"style": "FLAT"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Terrain",
|
||||
"landBiomes": [
|
||||
"terrain"
|
||||
],
|
||||
"seaBiomes": [
|
||||
"terrain"
|
||||
],
|
||||
"shoreBiomes": [
|
||||
"terrain"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "Terrain",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"layers": [
|
||||
{
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:stone"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{
|
||||
"generator": "flat",
|
||||
"min": 32,
|
||||
"max": 32
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "Lowland transition fixture",
|
||||
"version": 1,
|
||||
"mode": {
|
||||
"type": "OVERWORLD"
|
||||
},
|
||||
"regions": [
|
||||
"terrain"
|
||||
],
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": {
|
||||
"min": 0,
|
||||
"max": 128
|
||||
},
|
||||
"logicalHeight": 128,
|
||||
"fluidHeight": 40,
|
||||
"caveProfile": {
|
||||
"enabled": true,
|
||||
"verticalRange": {
|
||||
"min": 32,
|
||||
"max": 47
|
||||
},
|
||||
"verticalEdgeFade": 0,
|
||||
"baseDensityStyle": {
|
||||
"style": "FLAT"
|
||||
},
|
||||
"detailWeight": 0,
|
||||
"densityThreshold": {
|
||||
"min": 1,
|
||||
"max": 1
|
||||
},
|
||||
"thresholdBias": 0,
|
||||
"adaptiveSampling": false,
|
||||
"allowSurfaceBreak": false,
|
||||
"allowFluid": false,
|
||||
"allowLava": false
|
||||
},
|
||||
"rockPalette": {
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:stone"
|
||||
}
|
||||
]
|
||||
},
|
||||
"bedrock": false
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"seed": 310,
|
||||
"interpolator": {
|
||||
"function": "NONE",
|
||||
"horizontalScale": 1
|
||||
},
|
||||
"composite": [
|
||||
{
|
||||
"seed": 310,
|
||||
"style": {
|
||||
"style": "FLAT"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Terrain",
|
||||
"landBiomes": [
|
||||
"terrain"
|
||||
],
|
||||
"seaBiomes": [
|
||||
"terrain"
|
||||
],
|
||||
"shoreBiomes": [
|
||||
"terrain"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "Terrain",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"layers": [
|
||||
{
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:granite"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{
|
||||
"generator": "flat",
|
||||
"min": 32,
|
||||
"max": 32
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "Lowland transition fixture",
|
||||
"version": 1,
|
||||
"mode": {
|
||||
"type": "OVERWORLD"
|
||||
},
|
||||
"regions": [
|
||||
"terrain"
|
||||
],
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": {
|
||||
"min": 0,
|
||||
"max": 128
|
||||
},
|
||||
"logicalHeight": 128,
|
||||
"fluidHeight": 40,
|
||||
"caveProfile": {
|
||||
"enabled": false
|
||||
},
|
||||
"rockPalette": {
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:granite"
|
||||
}
|
||||
]
|
||||
},
|
||||
"bedrock": false
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"seed": 310,
|
||||
"interpolator": {
|
||||
"function": "NONE",
|
||||
"horizontalScale": 1
|
||||
},
|
||||
"composite": [
|
||||
{
|
||||
"seed": 310,
|
||||
"style": {
|
||||
"style": "FLAT"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Terrain",
|
||||
"landBiomes": [
|
||||
"terrain"
|
||||
],
|
||||
"seaBiomes": [
|
||||
"terrain"
|
||||
],
|
||||
"shoreBiomes": [
|
||||
"terrain"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "Terrain",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"layers": [
|
||||
{
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:stone"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{
|
||||
"generator": "flat",
|
||||
"min": 32,
|
||||
"max": 32
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "Lowland transition fixture",
|
||||
"version": 1,
|
||||
"mode": {
|
||||
"type": "OVERWORLD"
|
||||
},
|
||||
"regions": [
|
||||
"terrain"
|
||||
],
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": {
|
||||
"min": 0,
|
||||
"max": 128
|
||||
},
|
||||
"logicalHeight": 128,
|
||||
"fluidHeight": 40,
|
||||
"caveProfile": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"seed": 310,
|
||||
"interpolator": {
|
||||
"function": "NONE",
|
||||
"horizontalScale": 1
|
||||
},
|
||||
"composite": [
|
||||
{
|
||||
"seed": 310,
|
||||
"style": {
|
||||
"style": "FLAT"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Terrain",
|
||||
"landBiomes": [
|
||||
"terrain"
|
||||
],
|
||||
"seaBiomes": [
|
||||
"terrain"
|
||||
],
|
||||
"shoreBiomes": [
|
||||
"terrain"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "Terrain",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"layers": [
|
||||
{
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:stone"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{
|
||||
"generator": "flat",
|
||||
"min": 64,
|
||||
"max": 64
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "Mountain transition fixture",
|
||||
"version": 1,
|
||||
"mode": {
|
||||
"type": "OVERWORLD"
|
||||
},
|
||||
"regions": [
|
||||
"terrain"
|
||||
],
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": {
|
||||
"min": 0,
|
||||
"max": 128
|
||||
},
|
||||
"logicalHeight": 128,
|
||||
"fluidHeight": 40,
|
||||
"caveProfile": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"seed": 310,
|
||||
"interpolator": {
|
||||
"function": "NONE",
|
||||
"horizontalScale": 1
|
||||
},
|
||||
"composite": [
|
||||
{
|
||||
"seed": 310,
|
||||
"style": {
|
||||
"style": "FLAT"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Terrain",
|
||||
"landBiomes": [
|
||||
"terrain"
|
||||
],
|
||||
"seaBiomes": [
|
||||
"terrain"
|
||||
],
|
||||
"shoreBiomes": [
|
||||
"terrain"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "Terrain",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"layers": [
|
||||
{
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:stone"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{
|
||||
"generator": "flat",
|
||||
"min": -24,
|
||||
"max": -24
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "Ocean-highwater transition fixture",
|
||||
"version": 1,
|
||||
"mode": {
|
||||
"type": "OVERWORLD"
|
||||
},
|
||||
"regions": [
|
||||
"terrain"
|
||||
],
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": {
|
||||
"min": 0,
|
||||
"max": 128
|
||||
},
|
||||
"logicalHeight": 128,
|
||||
"fluidHeight": 48,
|
||||
"caveProfile": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"seed": 310,
|
||||
"interpolator": {
|
||||
"function": "NONE",
|
||||
"horizontalScale": 1
|
||||
},
|
||||
"composite": [
|
||||
{
|
||||
"seed": 310,
|
||||
"style": {
|
||||
"style": "FLAT"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Terrain",
|
||||
"landBiomes": [
|
||||
"terrain"
|
||||
],
|
||||
"seaBiomes": [
|
||||
"terrain"
|
||||
],
|
||||
"shoreBiomes": [
|
||||
"terrain"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "Terrain",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"layers": [
|
||||
{
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:stone"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{
|
||||
"generator": "flat",
|
||||
"min": -16,
|
||||
"max": -16
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "Ocean transition fixture",
|
||||
"version": 1,
|
||||
"mode": {
|
||||
"type": "OVERWORLD"
|
||||
},
|
||||
"regions": [
|
||||
"terrain"
|
||||
],
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": {
|
||||
"min": 0,
|
||||
"max": 128
|
||||
},
|
||||
"logicalHeight": 128,
|
||||
"fluidHeight": 40,
|
||||
"caveProfile": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"seed": 310,
|
||||
"interpolator": {
|
||||
"function": "NONE",
|
||||
"horizontalScale": 1
|
||||
},
|
||||
"composite": [
|
||||
{
|
||||
"seed": 310,
|
||||
"style": {
|
||||
"style": "FLAT"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Terrain",
|
||||
"landBiomes": [
|
||||
"terrain"
|
||||
],
|
||||
"seaBiomes": [
|
||||
"terrain"
|
||||
],
|
||||
"shoreBiomes": [
|
||||
"terrain"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "Terrain",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"layers": [
|
||||
{
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:stone"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{
|
||||
"generator": "flat",
|
||||
"min": 48,
|
||||
"max": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "Mountain transition fixture",
|
||||
"version": 1,
|
||||
"mode": {
|
||||
"type": "OVERWORLD"
|
||||
},
|
||||
"regions": [
|
||||
"terrain"
|
||||
],
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": {
|
||||
"min": 0,
|
||||
"max": 128
|
||||
},
|
||||
"logicalHeight": 128,
|
||||
"fluidHeight": 40,
|
||||
"caveProfile": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"expression": "0.5 + 0.125 * sin(x / 2)"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"seed": 310,
|
||||
"interpolator": {
|
||||
"function": "NONE",
|
||||
"horizontalScale": 1
|
||||
},
|
||||
"composite": [
|
||||
{
|
||||
"seed": 310,
|
||||
"style": {
|
||||
"expression": "ripple"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Terrain",
|
||||
"landBiomes": [
|
||||
"terrain"
|
||||
],
|
||||
"seaBiomes": [
|
||||
"terrain"
|
||||
],
|
||||
"shoreBiomes": [
|
||||
"terrain"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "Terrain",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"layers": [
|
||||
{
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:stone"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{
|
||||
"generator": "flat",
|
||||
"min": 8,
|
||||
"max": 40
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "Lowland transition fixture",
|
||||
"version": 1,
|
||||
"mode": {
|
||||
"type": "OVERWORLD"
|
||||
},
|
||||
"regions": [
|
||||
"terrain"
|
||||
],
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": {
|
||||
"min": 0,
|
||||
"max": 128
|
||||
},
|
||||
"logicalHeight": 128,
|
||||
"fluidHeight": 40,
|
||||
"caveProfile": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"expression": "0.5 + 0.25 * sin(x / 64) + 0.125 * sin(y / 48)"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"seed": 310,
|
||||
"interpolator": {
|
||||
"function": "NONE",
|
||||
"horizontalScale": 1
|
||||
},
|
||||
"composite": [
|
||||
{
|
||||
"seed": 310,
|
||||
"style": {
|
||||
"expression": "slope"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Terrain",
|
||||
"landBiomes": [
|
||||
"terrain"
|
||||
],
|
||||
"seaBiomes": [
|
||||
"terrain"
|
||||
],
|
||||
"shoreBiomes": [
|
||||
"terrain"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "Terrain",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"layers": [
|
||||
{
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:stone"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{
|
||||
"generator": "flat",
|
||||
"min": 40,
|
||||
"max": 72
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "Mountain transition fixture",
|
||||
"version": 1,
|
||||
"mode": {
|
||||
"type": "OVERWORLD"
|
||||
},
|
||||
"regions": [
|
||||
"terrain"
|
||||
],
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": {
|
||||
"min": 0,
|
||||
"max": 128
|
||||
},
|
||||
"logicalHeight": 128,
|
||||
"fluidHeight": 40,
|
||||
"caveProfile": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"expression": "0.5 + 0.30 * sin(x / 40 + 1) + 0.10 * sin(y / 48) + 0.03 * sin(x / 5)"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"seed": 310,
|
||||
"interpolator": {
|
||||
"function": "NONE",
|
||||
"horizontalScale": 1
|
||||
},
|
||||
"composite": [
|
||||
{
|
||||
"seed": 310,
|
||||
"style": {
|
||||
"expression": "slope"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Terrain",
|
||||
"landBiomes": [
|
||||
"terrain"
|
||||
],
|
||||
"seaBiomes": [
|
||||
"terrain"
|
||||
],
|
||||
"shoreBiomes": [
|
||||
"terrain"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "Terrain",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"layers": [
|
||||
{
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:stone"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{
|
||||
"generator": "flat",
|
||||
"min": 96,
|
||||
"max": 96
|
||||
}
|
||||
],
|
||||
"terrain3D": {
|
||||
"amplitude": 64,
|
||||
"horizontalScale": 96,
|
||||
"verticalScale": 16,
|
||||
"minimumSlope": 0,
|
||||
"slopeFade": 1,
|
||||
"fluidClearance": 0,
|
||||
"fluidFade": 1,
|
||||
"crackDepth": 0
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "Lowland transition fixture",
|
||||
"version": 1,
|
||||
"mode": {
|
||||
"type": "OVERWORLD"
|
||||
},
|
||||
"regions": [
|
||||
"terrain"
|
||||
],
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": {
|
||||
"min": 0,
|
||||
"max": 256
|
||||
},
|
||||
"logicalHeight": 256,
|
||||
"fluidHeight": 0,
|
||||
"caveProfile": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"seed": 310,
|
||||
"interpolator": {
|
||||
"function": "NONE",
|
||||
"horizontalScale": 1
|
||||
},
|
||||
"composite": [
|
||||
{
|
||||
"seed": 310,
|
||||
"style": {
|
||||
"style": "FLAT"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Terrain",
|
||||
"landBiomes": [
|
||||
"terrain"
|
||||
],
|
||||
"seaBiomes": [
|
||||
"terrain"
|
||||
],
|
||||
"shoreBiomes": [
|
||||
"terrain"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "Terrain",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"layers": [
|
||||
{
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:stone"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{
|
||||
"generator": "flat",
|
||||
"min": 128,
|
||||
"max": 128
|
||||
}
|
||||
],
|
||||
"terrain3D": {
|
||||
"amplitude": 64,
|
||||
"horizontalScale": 96,
|
||||
"verticalScale": 16,
|
||||
"minimumSlope": 0,
|
||||
"slopeFade": 1,
|
||||
"fluidClearance": 0,
|
||||
"fluidFade": 1,
|
||||
"crackDepth": 0
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "Mountain transition fixture",
|
||||
"version": 1,
|
||||
"mode": {
|
||||
"type": "OVERWORLD"
|
||||
},
|
||||
"regions": [
|
||||
"terrain"
|
||||
],
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": {
|
||||
"min": 0,
|
||||
"max": 256
|
||||
},
|
||||
"logicalHeight": 256,
|
||||
"fluidHeight": 0,
|
||||
"caveProfile": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"seed": 310,
|
||||
"interpolator": {
|
||||
"function": "NONE",
|
||||
"horizontalScale": 1
|
||||
},
|
||||
"composite": [
|
||||
{
|
||||
"seed": 310,
|
||||
"style": {
|
||||
"style": "FLAT"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Terrain",
|
||||
"landBiomes": [
|
||||
"terrain"
|
||||
],
|
||||
"seaBiomes": [
|
||||
"terrain"
|
||||
],
|
||||
"shoreBiomes": [
|
||||
"terrain"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user