Integrate optional terrain transforms

- Add versioned terrain providers that run before content placement.
- Resolve transformed terrain heights, occupancy, and cave metadata through bounded caches.
- Initialize native structures after binding the modded generation runtime.
- Cover transform behavior, provider resolution, and structure binding with regression tests.
This commit is contained in:
Brian Neumann-Fopiano
2026-10-02 05:39:20 -04:00
parent 74df63f717
commit 25c04c0157
35 changed files with 2160 additions and 21 deletions
@@ -242,6 +242,7 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
this.announced.set(false);
resetRuntimeCaches();
this.engineBinding.complete(replacement);
this.nativeGenerator.initializeStructureState(level);
// Bind time: a feature-order cycle in the new pack is reported here, once, and degrades to features-off.
// Never waits on a build owned by another thread: this method owns the generator monitor and the build
// path can need it.
@@ -442,12 +443,14 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
}
Engine current = engineIfBound();
if (NativeDimensionRuntime.sameWorld(boundLevel, level) && current != null && current.getComplex() != null) {
nativeGenerator.initializeStructureState(level);
return;
}
requireCompletedShutdown(engine);
unloading = false;
Engine bound = bindEngine(level);
nativeGenerator.installVolumeIndex(level, bound);
nativeGenerator.initializeStructureState(level);
// Bind time: a feature-order cycle is reported here, once, and degrades to features-off. Non-waiting for
// the same reason as repointAndBind: this method owns the generator monitor.
importedFeatures.prepareWithoutWaiting(bound);
@@ -1097,6 +1100,12 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
return current.getDimensionStackContext() != null;
}
@Override
public boolean terrainTransformed() {
Engine current = engine;
return current != null && current.getComplex().getTerrainTransform() != null;
}
@Override
public int runtimeId(Engine current) {
return current.getCacheID();
@@ -42,6 +42,7 @@ public class ModdedMixinAuditTest {
"\\[\\[mixins]]\\s*config\\s*=\\s*\"([^\"]+)\"");
// One applied mixin per config proves the config was registered, which is all the audit detects.
private static final Set<String> UNAUDITED = Set.of(
"volmlib.entity.mixins.json/ServerLevelStructureBootstrapMixin",
"volmlib.entity.mixins.json/WorldGenerationWriteGuardMixin",
"volmlib.entity.mixins.json/ChunkMapTerrainReceiptAccess",
"volmlib.entity.mixins.json/ChunkTerrainReceiptMixin",
@@ -0,0 +1,145 @@
package art.arcane.iris.modded;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.EnginePlatformHooks;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedChunkGenerator;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.io.File;
import java.lang.reflect.Field;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ModdedStructureBindingOrderTest {
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void initialBindingPublishesReadyEngineBeforeStartingStructureSearches() throws Exception {
NativeModdedChunkGenerator<?, ?, ?> nativeGenerator = mock(NativeModdedChunkGenerator.class);
ModdedEngineBinding<Engine> binding = new ModdedEngineBinding<>(1L, TimeUnit.MILLISECONDS);
IrisModdedChunkGenerator generator = generator(nativeGenerator, binding);
NativeWorld world = mock(NativeWorld.class);
NativeModdedServer server = mock(NativeModdedServer.class);
Engine engine = readyEngine();
when(nativeGenerator.represents(world)).thenReturn(true);
when(server.generateStructures()).thenReturn(true);
doAnswer(invocation -> {
assertSame(engine, generator.awaitStructureEngine());
assertSame(engine, binding.await("overworld"));
return null;
}).when(nativeGenerator).initializeStructureState(world);
try (MockedStatic<NativeModdedServer> servers = mockStatic(NativeModdedServer.class);
MockedStatic<ModdedWorldEngines> engines = mockStatic(ModdedWorldEngines.class);
MockedStatic<ModdedIrisLog> log = mockStatic(ModdedIrisLog.class)) {
servers.when(() -> NativeModdedServer.forWorld(world)).thenReturn(server);
engines.when(() -> ModdedWorldEngines.get(eq(world), any(), any(), anyLong(), any())).thenReturn(engine);
generator.bindLevel(world);
verify(nativeGenerator).initializeStructureState(world);
}
}
@Test
public void levelBindingStartsDeferredStructuresAfterAnEarlierDataQueryBoundTheEngine() throws Exception {
NativeModdedChunkGenerator<?, ?, ?> nativeGenerator = mock(NativeModdedChunkGenerator.class);
ModdedEngineBinding<Engine> binding = new ModdedEngineBinding<>(1L, TimeUnit.MILLISECONDS);
IrisModdedChunkGenerator generator = generator(nativeGenerator, binding);
NativeWorld world = mock(NativeWorld.class);
Engine engine = readyEngine();
when(nativeGenerator.represents(world)).thenReturn(true);
when(world.nativeHandle()).thenReturn(new Object());
field("engine").set(generator, engine);
field("boundLevel").set(generator, world);
binding.complete(engine);
doAnswer(invocation -> {
assertSame(engine, generator.awaitStructureEngine());
assertSame(engine, binding.await("overworld"));
return null;
}).when(nativeGenerator).initializeStructureState(world);
generator.bindLevel(world);
verify(nativeGenerator).initializeStructureState(world);
}
@Test
public void repointPublishesReplacementBindingBeforeStartingStructureSearches() throws Exception {
NativeModdedChunkGenerator<?, ?, ?> nativeGenerator = mock(NativeModdedChunkGenerator.class);
ModdedEngineBinding<Engine> binding = new ModdedEngineBinding<>(1L, TimeUnit.MILLISECONDS);
IrisModdedChunkGenerator generator = generator(nativeGenerator, binding);
NativeWorld world = mock(NativeWorld.class);
NativeModdedServer server = mock(NativeModdedServer.class);
Engine replacement = readyEngine();
when(nativeGenerator.represents(world)).thenReturn(true);
when(server.generateStructures()).thenReturn(true);
doAnswer(invocation -> {
assertSame(replacement, generator.awaitStructureEngine());
assertSame(replacement, binding.await("overworld"));
return null;
}).when(nativeGenerator).initializeStructureState(world);
try (MockedStatic<NativeModdedServer> servers = mockStatic(NativeModdedServer.class);
MockedStatic<ModdedWorldEngines> engines = mockStatic(ModdedWorldEngines.class)) {
servers.when(() -> NativeModdedServer.forWorld(world)).thenReturn(server);
engines.when(() -> ModdedWorldEngines.prepareReplacement(eq(world), eq("overworld"),
eq("overworld"), eq(42L), any())).thenReturn(replacement);
generator.repointAndBind(world, "overworld", "overworld", 42L);
verify(nativeGenerator).initializeStructureState(world);
}
}
private static IrisModdedChunkGenerator generator(NativeModdedChunkGenerator<?, ?, ?> nativeGenerator,
ModdedEngineBinding<Engine> binding) throws Exception {
IrisModdedChunkGenerator generator = mock(IrisModdedChunkGenerator.class, CALLS_REAL_METHODS);
field("nativeGenerator").set(generator, nativeGenerator);
field("engineBinding").set(generator, binding);
field("dimensionKey").set(generator, "overworld");
field("generationMode").set(generator, ModdedGenerationMode.PERSISTENT_RESTORE);
field("announced").set(generator, new AtomicBoolean());
field("importedFeatures").set(generator, mock(ModdedImportedFeatureStage.class));
return generator;
}
private static Engine readyEngine() {
Engine engine = mock(Engine.class);
IrisData data = mock(IrisData.class);
when(engine.getComplex()).thenReturn(mock(IrisComplex.class));
when(engine.getData()).thenReturn(data);
when(data.getDataFolder()).thenReturn(new File("build/structure-binding-test/pack"));
when(engine.getPlatformHooks()).thenReturn(mock(EnginePlatformHooks.class));
return engine;
}
private static Field field(String name) throws NoSuchFieldException {
Field field = IrisModdedChunkGenerator.class.getDeclaredField(name);
field.setAccessible(true);
return field;
}
}
@@ -0,0 +1,70 @@
package art.arcane.volmlib.nativelib.minecraft26_2.modded.mixin;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedChunkGenerator;
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ServerChunkCache;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class NativeStructureBootstrapTest {
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void levelConstructionDefersNativeStructureInitialization() {
ServerLevelStructureBootstrapMixin mixin = mock(ServerLevelStructureBootstrapMixin.class, CALLS_REAL_METHODS);
ServerChunkCache chunks = mock(ServerChunkCache.class);
NativeModdedChunkGenerator<?, ?, ?> generator = mock(NativeModdedChunkGenerator.class);
ChunkGeneratorStructureState state = mock(ChunkGeneratorStructureState.class);
when(mixin.getChunkSource()).thenReturn(chunks);
when(chunks.getGenerator()).thenReturn(generator);
mixin.iris$initializeStructureState(state);
verify(state, never()).ensureStructuresGenerated();
}
@Test
public void levelConstructionStillInitializesVanillaStructures() {
ServerLevelStructureBootstrapMixin mixin = mock(ServerLevelStructureBootstrapMixin.class, CALLS_REAL_METHODS);
ServerChunkCache chunks = mock(ServerChunkCache.class);
ChunkGenerator generator = mock(ChunkGenerator.class);
ChunkGeneratorStructureState state = mock(ChunkGeneratorStructureState.class);
when(mixin.getChunkSource()).thenReturn(chunks);
when(chunks.getGenerator()).thenReturn(generator);
mixin.iris$initializeStructureState(state);
verify(state).ensureStructuresGenerated();
}
@Test
public void boundNativeWorldInitializesItsDeferredStructures() {
NativeModdedChunkGenerator<?, ?, ?> generator = mock(NativeModdedChunkGenerator.class, CALLS_REAL_METHODS);
NativeWorld world = mock(NativeWorld.class);
ServerLevel level = mock(ServerLevel.class);
ServerChunkCache chunks = mock(ServerChunkCache.class);
ChunkGeneratorStructureState state = mock(ChunkGeneratorStructureState.class);
when(world.nativeHandle()).thenReturn(level);
when(level.getChunkSource()).thenReturn(chunks);
when(chunks.getGeneratorState()).thenReturn(state);
generator.initializeStructureState(world);
verify(state).ensureStructuresGenerated();
}
}
@@ -29,6 +29,7 @@ public final class CaveTerrainSnapshot {
private final MatterCavern[][] caverns;
private final HydrologyCaveCell[][] hydrology;
private boolean hydrologyPresent;
private volatile long retainedBytes;
private CaveTerrainSnapshot(int chunkX, int chunkZ, int sections) {
this.chunkX = chunkX;
@@ -63,6 +64,36 @@ public final class CaveTerrainSnapshot {
return hydrologyPresent;
}
public long estimatedRetainedBytes() {
long cached = retainedBytes;
if (cached != 0L) {
return cached;
}
long bytes = 80L + ((long) caverns.length + hydrology.length) * 8L;
for (MatterCavern[] section : caverns) {
if (section != null) {
bytes += 16L + (long) section.length * 8L;
for (MatterCavern cell : section) {
if (cell != null) {
bytes += 96L + (long) cell.getCustomBiome().length() * 2L;
}
}
}
}
for (HydrologyCaveCell[] section : hydrology) {
if (section != null) {
bytes += 16L + (long) section.length * 8L;
for (HydrologyCaveCell cell : section) {
if (cell != null) {
bytes += 208L + ((long) cell.fluidProfileKey().length() + cell.floodedBiomeKey().length()) * 2L;
}
}
}
}
retainedBytes = bytes;
return bytes;
}
public MatterCavern cavern(int x, int y, int z) {
if (y < 0) {
return null;
@@ -28,6 +28,7 @@ import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.EngineTarget;
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.Terrain3DColumn;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveStorage;
import art.arcane.iris.pack.value.IrisPosition;
@@ -108,6 +109,12 @@ public interface EngineMantle extends MatterGenerator {
}
default int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
IrisComplex complex = getComplex();
int transformed = complex == null || complex.getTerrainTransform() == null ? Integer.MIN_VALUE
: complex.transformedHeight(x, z, ignoreFluid);
if (transformed != Integer.MIN_VALUE) {
return transformed;
}
return ignoreFluid ? trueHeight(x, z) : Math.max(trueHeight(x, z), getFluidHeight(x, z));
}
@@ -116,6 +123,10 @@ public interface EngineMantle extends MatterGenerator {
}
default boolean isCarved(int x, int h, int z) {
Terrain3DColumn transformed = getComplex().transformedColumn(x, z);
if (transformed != null) {
return h >= 0 && h < transformed.topY() && !transformed.isSolid(h);
}
HydrologyCaveCell hydrology = HydrologyCaveStorage.getIfPresent(getMantle(), x, h, z);
if (hydrology != null) {
return hydrology.carves();
@@ -142,6 +153,12 @@ public interface EngineMantle extends MatterGenerator {
}
default int getFluidHeight(int x, int z) {
IrisComplex complex = getComplex();
int transformed = complex == null || complex.getTerrainTransform() == null ? Integer.MIN_VALUE
: complex.transformedFluidHeight(x, z);
if (transformed != Integer.MIN_VALUE) {
return transformed;
}
return (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(x, z));
}
@@ -582,6 +582,10 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
}
public boolean isPrerequisiteCarved(int x, int y, int z) {
Terrain3DColumn transformed = engineMantle.getComplex().transformedColumn(x, z);
if (transformed != null) {
return y >= 0 && y < transformed.topY() && !transformed.isSolid(y);
}
HydrologyCaveCell hydrology = getPrerequisiteDataIfPresent(x, y, z, HydrologyCaveCell.class);
if (hydrology != null) {
return hydrology.carves();
@@ -592,6 +596,10 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
public byte[] getPrerequisiteCarvedColumn(int x, int z, int height) {
int cappedHeight = Math.min(Math.max(height, 0), mantle.getWorldHeight());
Terrain3DColumn transformed = engineMantle.getComplex().transformedColumn(x, z);
if (transformed != null) {
return carvedColumn(transformed, cappedHeight);
}
byte[] carvedColumn = new byte[cappedHeight];
if (cappedHeight <= 0) {
return carvedColumn;
@@ -961,6 +969,10 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
@Override
public boolean isCarved(int x, int y, int z) {
Terrain3DColumn transformed = engineMantle.getComplex().transformedColumn(x, z);
if (transformed != null) {
return y >= 0 && y < transformed.topY() && !transformed.isSolid(y);
}
HydrologyCaveCell hydrology = getDataIfPresent(x, y, z, HydrologyCaveCell.class);
if (hydrology != null) {
return hydrology.carves();
@@ -972,6 +984,10 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
public byte[] getCarvedColumn(int x, int z, int height) {
int cappedHeight = Math.min(Math.max(height, 0), mantle.getWorldHeight());
Terrain3DColumn transformed = engineMantle.getComplex().transformedColumn(x, z);
if (transformed != null) {
return carvedColumn(transformed, cappedHeight);
}
byte[] carvedColumn = new byte[cappedHeight];
if (cappedHeight <= 0) {
return carvedColumn;
@@ -1589,6 +1605,17 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
}
}
private static byte[] carvedColumn(Terrain3DColumn terrain, int height) {
byte[] result = new byte[height];
int maximumY = Math.min(height, terrain.topY());
for (int y = 0; y < maximumY; y++) {
if (!terrain.isSolid(y)) {
result[y] = 1;
}
}
return result;
}
public final class ObjectPlacementCapture implements AutoCloseable {
private final ObjectContinuationBundle.PlacementKey key;
private final Long2ObjectOpenHashMap<Matter> fragments = new Long2ObjectOpenHashMap<>();
@@ -31,6 +31,7 @@ import art.arcane.iris.generation.stage.IrisDepositModifier;
import art.arcane.iris.generation.stage.IrisFloatingChildBiomeModifier;
import art.arcane.iris.generation.stage.IrisPerfectionModifier;
import art.arcane.iris.generation.stage.IrisPostModifier;
import art.arcane.iris.generation.context.ChunkContext;
public class ModeOverworld extends IrisEngineMode implements EngineMode {
public ModeOverworld(Engine engine) {
@@ -45,7 +46,7 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
IrisCustomModifier custom = new IrisCustomModifier(getEngine());
IrisFloatingChildBiomeModifier floatingChildBiomes = new IrisFloatingChildBiomeModifier(getEngine());
EngineStage sBiome = (x, z, k, p, m, c) -> biome.actuate(x, z, p, m, c);
EngineStage sGenMatter = (x, z, k, p, m, c) -> generateTerrainMatter(x >> 4, z >> 4, m || getEngine().isStudio(), c);
EngineStage sGenMatter = (x, z, k, p, m, c) -> generateTerrainMatterForChunk(x >> 4, z >> 4, m, c);
EngineStage sTerrain = (x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c);
EngineStage sDecorant = (x, z, k, p, m, c) -> decorant.actuate(x, z, k, m, c);
EngineStage sCave = (x, z, k, p, m, c) -> cave.modify(x >> 4, z >> 4, k, m, c);
@@ -82,4 +83,10 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
registerStage(sCustom);
}
}
void generateTerrainMatterForChunk(int x, int z, boolean multicore, ChunkContext context) {
boolean studioParallel = getEngine().isStudio()
&& !(context.isNaturalTerrain() && context.getComplex().getTerrainTransform() != null);
generateTerrainMatter(x, z, multicore || studioParallel, context);
}
}
@@ -125,8 +125,8 @@ public final class DimensionTerrainContext implements DataProvider {
engine.getDimension(),
engine.getData(),
engine.getHeight(),
natural ? complex.getNaturalHeightStream() : complex.getHeightStream(),
natural ? complex.getNaturalSlopeStream() : complex.getSlopeStream(),
natural ? complex.getNaturalHeightStream() : complex.getRawHeightStream(),
natural ? complex.getNaturalSlopeStream() : complex.getRawSlopeStream(),
fluidHeightStream,
natural ? complex.getNaturalTrueBiomeStream() : complex.getTrueBiomeStream(),
complex.getRegionStream(),
@@ -653,7 +653,7 @@ public final class DimensionTerrainContext implements DataProvider {
if (selfReferencing) {
return naturalSelf || naturalFallback
? engine.getComplex().naturalTerrainColumn(x, z)
: engine.getComplex().terrainColumn(x, z);
: engine.getComplex().originalTerrainColumn(x, z);
}
if (terrain3D == null) {
return null;
@@ -567,6 +567,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
/** The terrain slope at a column, answered from natural terrain when the server thread may not wait. */
@BlockCoordinates
default double getSlope(int x, int z) {
IrisComplex complex = getComplex();
if (complex != null && complex.getTerrainTransform() != null) {
return complex.getSlopeStream().getDouble(x, z);
}
if (answersFromNaturalTerrain(x, z)) {
return getComplex().naturalSlope(x, z);
}
@@ -580,6 +584,12 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
@BlockCoordinates
default int getHeight(int x, int z, boolean ignoreFluid) {
IrisComplex complex = getComplex();
int transformed = complex == null || complex.getTerrainTransform() == null ? Integer.MIN_VALUE
: complex.transformedHeight(x, z, ignoreFluid);
if (transformed != Integer.MIN_VALUE) {
return transformed;
}
DimensionStackContext dimensionStackContext = getDimensionStackContext();
if (dimensionStackContext != null) {
return ignoreFluid
@@ -595,6 +605,12 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
@BlockCoordinates
static int hostHeight(Engine engine, int x, int z, boolean ignoreFluid) {
IrisComplex complex = engine.getComplex();
int transformed = complex == null || complex.getTerrainTransform() == null ? Integer.MIN_VALUE
: complex.transformedHeight(x, z, ignoreFluid);
if (transformed != Integer.MIN_VALUE) {
return transformed;
}
EngineMantle mantle = engine.getMantle();
if (mantle == null) {
return engine.getHeight(x, z, ignoreFluid);
@@ -213,7 +213,11 @@ public interface EngineMode extends Staged {
EngineStage[] stages = getStages().toArray(new EngineStage[0]);
try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), generationSessionId, ctx)) {
if (getComplex().getTerrainTransform() == null) {
generateTerrain(x, z, blocks, biomes, multicore, ctx);
} else {
getComplex().getTerrainTransform().generate(x, z, blocks, biomes, multicore, ctx);
}
recordNaturalTerrain(x, z, blocks, biomes, ctx);
IrisDimensionStackActuator.clearHostNaturalMetadata(getEngine(), x, z, biomes.getHeight(), ctx);
ctx.beginContent();
@@ -54,6 +54,10 @@ import art.arcane.iris.generation.noise.IrisGenerator;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.terrain.transform.TerrainTransformRegistry;
import art.arcane.iris.generation.terrain.transform.TerrainTransformRuntime;
import art.arcane.iris.generation.terrain.transform.TerrainTransformer;
import art.arcane.iris.generation.noise.IrisInterpolator;
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
import art.arcane.iris.generation.terrain.IrisRegion;
@@ -185,6 +189,20 @@ public class IrisComplex implements DataProvider {
private final ProceduralTerrainHeightSampler proceduralTerrainHeight;
@Getter(AccessLevel.NONE)
private final transient Engine terrainEngine;
@Setter(AccessLevel.NONE)
private final transient TerrainTransformRuntime terrainTransform;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient ProceduralStream<Double> transformedHeightStream;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient ProceduralStream<Integer> transformedRoundedHeightStream;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient ProceduralStream<Double> transformedTopStream;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient ProceduralStream<Double> transformedSlopeStream;
@Getter(AccessLevel.NONE)
private transient Terrain3DRuntime terrain3D;
private transient HydrologyBankTerrainRuntime hydrologyBanks3D;
@@ -494,6 +512,25 @@ public class IrisComplex implements DataProvider {
d.hashCode());
}), "", engine, cacheSize);
//@done
TerrainTransformer transformer = engine.getDimension().getTerrainTransform() == null ? null
: TerrainTransformRegistry.discover().resolve(engine.getDimension().getTerrainTransform());
terrainTransform = transformer == null ? null
: new TerrainTransformRuntime(new TerrainTransformRuntime.Options(engine, this), transformer);
transformedHeightStream = transformer == null ? null : ProceduralStream.ofDouble((x, z) -> {
int transformed = transformedHeight((int) Math.floor(x), (int) Math.floor(z), true);
return transformed == Integer.MIN_VALUE ? heightStream.getDouble(x, z) : transformed;
});
transformedRoundedHeightStream = transformer == null ? null : ProceduralStream.ofDouble((x, z) -> {
int transformed = transformedHeight((int) Math.floor(x), (int) Math.floor(z), true);
return transformed == Integer.MIN_VALUE ? roundedHeighteightStream.get(x, z) : transformed;
}).round();
transformedTopStream = transformer == null ? null : ProceduralStream.ofDouble((x, z) -> {
int transformed = transformedHeight((int) Math.floor(x), (int) Math.floor(z), false);
return transformed == Integer.MIN_VALUE ? heightFluidStream.getDouble(x, z) : transformed;
});
ProceduralStream<Double> effectiveSlope = transformer == null ? null : transformedHeightStream.slope(3);
transformedSlopeStream = transformer == null ? null : ProceduralStream.ofDouble((x, z) ->
isNaturalTerrainContext() ? slopeStream.getDouble(x, z) : effectiveSlope.getDouble(x, z));
}
/**
@@ -689,6 +726,14 @@ public class IrisComplex implements DataProvider {
}
public Terrain3DColumn terrainColumn(int x, int z) {
Terrain3DColumn transformed = transformedColumn(x, z);
if (transformed != null) {
return transformed;
}
return originalTerrainColumn(x, z);
}
Terrain3DColumn originalTerrainColumn(int x, int z) {
if (!hasTerrain3D()) {
return null;
}
@@ -1204,7 +1249,70 @@ public class IrisComplex implements DataProvider {
}
public ProceduralStream<Double> getHeightStream() {
return heightStream;
return terrainTransform == null ? heightStream : transformedHeightStream;
}
ProceduralStream<Double> getRawSlopeStream() {
return terrainEngine.getDimension().getTerrainTransform() == null ? slopeStream : heightStream.slope(3);
}
public ProceduralStream<Integer> getRoundedHeighteightStream() {
return terrainTransform == null ? roundedHeighteightStream : transformedRoundedHeightStream;
}
public ProceduralStream<Double> getHeightFluidStream() {
return terrainTransform == null ? heightFluidStream : transformedTopStream;
}
public ProceduralStream<Double> getSlopeStream() {
return terrainTransform == null ? slopeStream : transformedSlopeStream;
}
public int transformedHeight(int x, int z, boolean ignoreFluid) {
if (terrainTransform == null || isNaturalTerrainContext()) {
return Integer.MIN_VALUE;
}
return readsReadyTerrainOnly()
? terrainTransform.readyHeight(x, z, ignoreFluid)
: terrainTransform.height(x, z, ignoreFluid);
}
public Terrain3DColumn transformedColumn(int x, int z) {
if (terrainTransform == null || isNaturalTerrainContext()) {
return null;
}
return readsReadyTerrainOnly()
? terrainTransform.readyColumn(x, z) : terrainTransform.column(x, z);
}
public int transformedFluidHeight(int x, int z) {
if (terrainTransform == null || isNaturalTerrainContext()) {
return Integer.MIN_VALUE;
}
return readsReadyTerrainOnly()
? terrainTransform.readyFluidHeight(x, z) : terrainTransform.fluidHeight(x, z);
}
private boolean readsReadyTerrainOnly() {
if (!terrainEngine.getPlatformHooks().isMainThread()) {
return false;
}
IrisContext context = IrisContext.get();
if (context == null || context.getEngine() != terrainEngine) {
return true;
}
ChunkContext chunk = context.getChunkContext();
if (chunk != null) {
return chunk.getComplex() != this || chunk.isNaturalTerrain();
}
if (context.getGenerationSessionId() == 0L || terrainEngine.getComplex() != this) {
return true;
}
if (terrainEngine instanceof IrisEngine irisEngine && irisEngine.threadState.assembly() != null
&& irisEngine.threadState.assembly().mode == null) {
return true;
}
return terrainEngine.getMode() == null;
}
boolean isNaturalTerrainContext() {
@@ -2144,6 +2252,9 @@ public class IrisComplex implements DataProvider {
}
public void close() {
if (terrainTransform != null) {
terrainTransform.close();
}
if (hydrologyBanks3D != null) {
hydrologyBanks3D.clear();
}
@@ -36,7 +36,7 @@ public class UpperDimensionContext implements DataProvider {
private UpperDimensionContext(DimensionTerrainContext terrainContext, Engine engine) {
this.terrainContext = terrainContext;
IrisComplex complex = engine.getComplex();
lowerHeightStream = complex.getHeightStream();
lowerHeightStream = complex.getRawHeightStream();
ceilingLayout = new CeilingLayout(engine.getHeight(), engine.getDimension().getUpperDimensionGap());
}
@@ -904,7 +904,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
zone.setFloor(floor);
zone.setCeiling(y - 1);
if (zone.isValid(resolver.height)
&& !getComplex().isTerrain3DOpening(worldX, floor, worldZ)) {
&& !isNaturalTerrainOpening(getComplex(), worldX, floor, worldZ)) {
if (markerRoll(worldX, zone.ceiling, worldZ, 0x9E3779B97F4A7C15L)) {
mantle.set(worldX, zone.ceiling, worldZ, MarkerMatter.CAVE_CEILING);
}
@@ -992,7 +992,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
}
IrisBiome resolveCaveBoundaryBiome(MatterCavern cavern, int worldX, int y, int worldZ, CaveInputs resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Map<String, IrisBiome> customBiomeCache) {
if (resolverState.complex.isTerrain3DOpening(worldX, y, worldZ)) {
if (isNaturalTerrainOpening(resolverState.complex, worldX, y, worldZ)) {
return resolverState.complex.getTrueBiomeStream().get(worldX, worldZ);
}
if (cavern != null && !cavern.getCustomBiome().isEmpty()) {
@@ -1001,6 +1001,18 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
return resolveCaveBiome(caveBiomeCache, worldX, y, worldZ, resolverState);
}
private boolean isNaturalTerrainOpening(IrisComplex complex, int x, int y, int z) {
if (complex.getTerrainTransform() == null) {
return complex.isTerrain3DOpening(x, y, z);
}
if (!complex.hasTerrain3D()) {
return false;
}
Terrain3DColumn column = complex.terrainColumn(x, z, complex.sampleHydrologyColumn(x, z));
return column != null && y >= column.minY() && y < column.topY() && !column.isSolid(y)
&& !getEngine().isAdditionalTerrainOwned(x, y, z);
}
/** The dimension's underground river bed palette, or null when nothing paints. */
private IrisRiverMaterialConfig undergroundBedMaterial() {
IrisHydrology hydrology = getDimension().getHydrology();
@@ -39,6 +39,7 @@ import art.arcane.iris.generation.image.IrisImageMapBinding;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.generation.noise.IrisShapedGeneratorStyle;
import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.iris.generation.terrain.transform.IrisTerrainTransform;
import art.arcane.iris.pack.validation.CompatPools;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.iris.structure.nativegen.IrisImportedFeatureControl;
@@ -218,6 +219,8 @@ public class IrisDimension extends IrisRegistrant {
private IrisHydrology hydrology = new IrisHydrology();
@Description("Dimension-level river placement, routing, profile, biome, and geometry policy.")
private IrisRiverPolicy riverPolicy = new IrisRiverPolicy();
@Description("Optional external terrain transformation with an exact provider version and pack-owned settings.")
private IrisTerrainTransform terrainTransform;
@Description("Refuse to place surface objects and trees over carved surface openings.")
private boolean requireObjectSurfaceSupport = true;
@MinNumber(0)
@@ -1,6 +1,7 @@
package art.arcane.iris.generation.terrain;
import java.util.Arrays;
import java.util.function.IntPredicate;
public final class Terrain3DColumn {
private final double baseHeight;
@@ -20,6 +21,29 @@ public final class Terrain3DColumn {
return new Terrain3DColumn(baseHeight, top + 1, false, new int[]{0, top});
}
public static Terrain3DColumn fromOccupancy(double baseHeight, int height, IntPredicate solid) {
if (height < 1) {
throw new IllegalArgumentException("Terrain column height must be positive");
}
int[] spans = new int[height + 1];
int count = 0;
boolean occupied = false;
for (int y = 0; y < height; y++) {
boolean next = solid.test(y);
if (next && !occupied) {
spans[count++] = y;
} else if (!next && occupied) {
spans[count++] = y - 1;
}
occupied = next;
}
if (occupied) {
spans[count++] = height - 1;
}
return new Terrain3DColumn(baseHeight, 0, true,
count == 0 ? new int[]{0, -1} : Arrays.copyOf(spans, count));
}
public double baseHeight() {
return baseHeight;
}
@@ -0,0 +1,25 @@
package art.arcane.iris.generation.terrain.transform;
import art.arcane.iris.pack.schema.annotation.Required;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.documentation.Description;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Accessors(chain = true)
@NoArgsConstructor
@Data
@Description("An external terrain transformer with its exact implementation version and generation settings.")
public class IrisTerrainTransform {
@Required
@Description("The installed terrain transform provider identifier.")
private String id;
@Required
@Description("The exact provider implementation version required to generate this dimension.")
private String version;
@Description("Provider settings saved with the generation pack. Values are strings interpreted by the provider.")
private KMap<String, String> settings = new KMap<>();
}
@@ -0,0 +1,6 @@
package art.arcane.iris.generation.terrain.transform;
@FunctionalInterface
public interface TerrainColumnTransformer {
void transform(TerrainTransformContext context, int localX, int localZ);
}
@@ -0,0 +1,17 @@
package art.arcane.iris.generation.terrain.transform;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
public interface TerrainTransformContext {
int blockX();
int blockZ();
int minY();
int height();
NativeBlockState original(int worldX, int worldY, int worldZ);
void set(int localX, int localY, int localZ, NativeBlockState state);
}
@@ -0,0 +1,11 @@
package art.arcane.iris.generation.terrain.transform;
import java.util.Map;
public interface TerrainTransformProvider {
String id();
String version();
TerrainTransformer create(Map<String, String> settings);
}
@@ -0,0 +1,62 @@
package art.arcane.iris.generation.terrain.transform;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.ServiceLoader;
public final class TerrainTransformRegistry {
private final Map<ProviderIdentity, TerrainTransformProvider> providers;
public TerrainTransformRegistry(Iterable<TerrainTransformProvider> providers) {
Map<ProviderIdentity, TerrainTransformProvider> indexed = new HashMap<>();
for (TerrainTransformProvider provider : Objects.requireNonNull(providers, "terrain transform providers")) {
Objects.requireNonNull(provider, "terrain transform provider");
ProviderIdentity identity = new ProviderIdentity(provider.id(), provider.version());
if (indexed.putIfAbsent(identity, provider) != null) {
throw new IllegalArgumentException("Duplicate terrain transform provider " + identity + ".");
}
}
this.providers = Map.copyOf(indexed);
}
public static TerrainTransformRegistry discover() {
return new TerrainTransformRegistry(ServiceLoader.load(
TerrainTransformProvider.class, TerrainTransformProvider.class.getClassLoader()));
}
public TerrainTransformer resolve(IrisTerrainTransform descriptor) {
if (descriptor == null) {
return null;
}
ProviderIdentity identity = new ProviderIdentity(descriptor.getId(), descriptor.getVersion());
TerrainTransformProvider provider = providers.get(identity);
if (provider == null) {
throw new IllegalStateException("Required terrain transform provider " + identity + " is unavailable.");
}
Map<String, String> settings = Map.copyOf(Objects.requireNonNull(
descriptor.getSettings(), "terrain transform settings"));
TerrainTransformer transformer = Objects.requireNonNull(
provider.create(settings), "terrain transformer returned by " + identity);
int radius = transformer.radius();
if (radius < 0 || radius > 16) {
throw new IllegalArgumentException("Terrain transform provider " + identity
+ " requires radius " + radius + "; the supported range is 0 through 16 blocks.");
}
return transformer;
}
private static String requireIdentity(String value, String label) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(label + " must not be blank.");
}
return value;
}
private record ProviderIdentity(String id, String version) {
private ProviderIdentity {
id = requireIdentity(id, "Terrain transform provider ID");
version = requireIdentity(version, "Terrain transform provider version");
}
}
}
@@ -0,0 +1,506 @@
package art.arcane.iris.generation.terrain.transform;
import art.arcane.iris.generation.block.B;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.iris.generation.mantle.CaveTerrainSnapshot;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.terrain.Terrain3DColumn;
import art.arcane.iris.world.history.FloatingBiomeOverlay;
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.volmlib.util.hunk.storage.StorageHunk;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.Arrays;
import java.util.Objects;
public final class TerrainTransformRuntime implements AutoCloseable {
private static final long CACHE_BYTES = 64L * 1024 * 1024;
private final Engine engine;
private final IrisComplex complex;
private final TerrainTransformer transformer;
private final TerrainColumnTransformer columnTransformer;
private final int radius;
private final int minY;
private final int height;
private final NativeBlockState air;
private final Cache<Long, Snapshot> originals;
private final Cache<Long, Snapshot> transformed;
private final Cache<Long, ColumnSnapshot> transformedColumns;
private volatile boolean closed;
public TerrainTransformRuntime(Options options, TerrainTransformer transformer) {
engine = Objects.requireNonNull(options, "terrain runtime options").engine();
complex = options.complex();
this.transformer = Objects.requireNonNull(transformer, "terrain transformer");
columnTransformer = transformer.columnTransformer();
radius = transformer.radius();
if (radius < 0 || radius > 16) {
throw new IllegalArgumentException("Terrain transform radius must be between 0 and 16 blocks");
}
minY = engine.getMinHeight();
height = engine.getHeight();
if (height <= 0) {
throw new IllegalArgumentException("Terrain height must be positive");
}
air = Objects.requireNonNull(B.getState("AIR"), "air block state");
originals = newCache();
transformed = newCache();
transformedColumns = columnTransformer == null ? null : Caffeine.newBuilder().maximumWeight(CACHE_BYTES)
.weigher((Long key, ColumnSnapshot snapshot) -> snapshot.weight()).build();
}
public void generate(int x, int z, Hunk<NativeBlockState> blocks, Hunk<NativeBiome> biomes,
boolean multicore, ChunkContext target) {
requireOpen();
if ((x & 15) != 0 || (z & 15) != 0 || blocks.getWidth() != 16 || blocks.getDepth() != 16
|| blocks.getHeight() != height || biomes.getWidth() != 16 || biomes.getDepth() != 16
|| biomes.getHeight() != height || target.getComplex() != complex) {
throw new IllegalArgumentException("Terrain transform output must match its runtime and chunk");
}
Snapshot snapshot = transformed(x >> 4, z >> 4);
target.setTerrainBiomeOutput(biomes);
for (int y = 0; y < height; y++) {
for (int localZ = 0; localZ < 16; localZ++) {
for (int localX = 0; localX < 16; localX++) {
int index = index(localX, y, localZ);
blocks.setRaw(localX, y, localZ, snapshot.blocks[index]);
NativeBiome biome = snapshot.biomes[index];
if (biome != null) {
biomes.setRaw(localX, y, localZ, biome);
}
}
}
}
for (int localZ = 0; localZ < 16; localZ++) {
for (int localX = 0; localX < 16; localX++) {
target.setTerrainHeight(localX, localZ, snapshot.solidHeights[(localZ << 4) | localX]);
}
}
target.setCaveTerrain(snapshot.caves);
target.setFloatingBiomes(snapshot.floating == null ? null : snapshot.floating.copy());
}
public int height(int x, int z, boolean ignoreFluid) {
requireOpen();
Snapshot snapshot = fullForQuery(x, z);
if (snapshot != null) {
return snapshot.height(x & 15, z & 15, ignoreFluid);
}
ColumnSnapshot column = transformedColumn(x, z);
return ignoreFluid ? column.ground : column.top;
}
public int readyHeight(int x, int z, boolean ignoreFluid) {
requireOpen();
Snapshot snapshot = transformed.getIfPresent(key(x >> 4, z >> 4));
if (snapshot != null) {
return snapshot.height(x & 15, z & 15, ignoreFluid);
}
ColumnSnapshot column = readyTransformedColumn(x, z);
return column == null ? Integer.MIN_VALUE : ignoreFluid ? column.ground : column.top;
}
public NativeBlockState block(int worldX, int localY, int worldZ) {
requireOpen();
if (localY < 0 || localY >= height) {
return air;
}
return transformed(worldX >> 4, worldZ >> 4).blocks[index(worldX & 15, localY, worldZ & 15)];
}
public NativeBlockState readyBlock(int worldX, int localY, int worldZ) {
requireOpen();
if (localY < 0 || localY >= height) {
return air;
}
Snapshot snapshot = transformed.getIfPresent(key(worldX >> 4, worldZ >> 4));
return snapshot == null ? null : snapshot.blocks[index(worldX & 15, localY, worldZ & 15)];
}
public Terrain3DColumn column(int x, int z) {
requireOpen();
Snapshot snapshot = fullForQuery(x, z);
return snapshot == null ? transformedColumn(x, z).terrain : snapshot.column(x & 15, z & 15);
}
public Terrain3DColumn readyColumn(int x, int z) {
requireOpen();
Snapshot snapshot = transformed.getIfPresent(key(x >> 4, z >> 4));
if (snapshot != null) {
return snapshot.column(x & 15, z & 15);
}
ColumnSnapshot column = readyTransformedColumn(x, z);
return column == null ? null : column.terrain;
}
public int fluidHeight(int x, int z) {
requireOpen();
Snapshot snapshot = fullForQuery(x, z);
return snapshot == null ? transformedColumn(x, z).fluid : snapshot.fluidHeights[((z & 15) << 4) | (x & 15)];
}
public int readyFluidHeight(int x, int z) {
requireOpen();
Snapshot snapshot = transformed.getIfPresent(key(x >> 4, z >> 4));
if (snapshot != null) {
return snapshot.fluidHeights[((z & 15) << 4) | (x & 15)];
}
ColumnSnapshot column = readyTransformedColumn(x, z);
return column == null ? Integer.MIN_VALUE : column.fluid;
}
@Override
public void close() {
closed = true;
originals.invalidateAll();
transformed.invalidateAll();
originals.cleanUp();
transformed.cleanUp();
if (transformedColumns != null) {
transformedColumns.invalidateAll();
transformedColumns.cleanUp();
}
}
private Cache<Long, Snapshot> newCache() {
return Caffeine.newBuilder().maximumWeight(CACHE_BYTES)
.weigher((Long key, Snapshot snapshot) -> snapshot.weight()).build();
}
private Snapshot transformed(int chunkX, int chunkZ) {
Snapshot snapshot = transformed.get(key(chunkX, chunkZ), ignored -> transform(chunkX, chunkZ));
if (closed) {
transformed.invalidateAll();
requireOpen();
}
return snapshot;
}
private Snapshot original(int chunkX, int chunkZ) {
Snapshot snapshot = originals.get(key(chunkX, chunkZ), ignored -> materialize(chunkX, chunkZ));
if (closed) {
originals.invalidateAll();
requireOpen();
}
return snapshot;
}
private Snapshot fullForQuery(int x, int z) {
return columnTransformer == null ? transformed(x >> 4, z >> 4)
: transformed.getIfPresent(key(x >> 4, z >> 4));
}
private ColumnSnapshot readyTransformedColumn(int x, int z) {
return transformedColumns == null ? null : transformedColumns.getIfPresent(key(x, z));
}
private ColumnSnapshot transformedColumn(int x, int z) {
ColumnSnapshot snapshot = transformedColumns.get(key(x, z), ignored -> transformColumn(x, z));
if (closed) {
transformedColumns.invalidateAll();
requireOpen();
}
return snapshot;
}
private Snapshot materialize(int chunkX, int chunkZ) {
requireOpen();
int x = chunkX << 4;
int z = chunkZ << 4;
NativeBlockState[] states = new NativeBlockState[Math.multiplyExact(256, height)];
Arrays.fill(states, air);
NativeBiome[] biomeStates = new NativeBiome[states.length];
Hunk<NativeBlockState> blocks = new SnapshotHunk<>(states, air);
Hunk<NativeBiome> biomes = new SnapshotHunk<>(biomeStates, null);
long session = engine.getGenerationSessionId();
ChunkContext context = new ChunkContext(x, z, complex, session, true,
ChunkContext.PrefillPlan.NATURAL_TERRAIN, engine.getMetrics(), engine.getDimensionStackContext());
try (IrisContext.Scope scope = IrisContext.open(engine, session, context)) {
engine.getMode().generateTerrain(x, z, blocks, biomes, false, context);
}
FloatingBiomeOverlay floating = context.getFloatingBiomes();
return new Snapshot(new SnapshotData(states, biomeStates, context.getCaveTerrain(),
floating == null ? null : floating.copy()), false);
}
private Snapshot transform(int chunkX, int chunkZ) {
requireOpen();
Snapshot source = original(chunkX, chunkZ);
TransformContext context = new TransformContext(new TransformTarget(chunkX, chunkZ, false, 0, 0), source);
try {
transformer.transform(context);
} finally {
context.active = false;
}
return new Snapshot(new SnapshotData(context.blocks, source.biomes, source.caves, source.floating), true);
}
private ColumnSnapshot transformColumn(int x, int z) {
requireOpen();
Snapshot source = original(x >> 4, z >> 4);
TransformContext context = new TransformContext(
new TransformTarget(x >> 4, z >> 4, true, x & 15, z & 15), source);
try {
columnTransformer.transform(context, x & 15, z & 15);
} finally {
context.active = false;
}
return new ColumnSnapshot(context.blocks);
}
private void requireOpen() {
if (closed) {
throw new IllegalStateException("Terrain transform runtime is closed");
}
}
private static int index(int x, int y, int z) {
return (y << 8) | (z << 4) | x;
}
private static long key(int chunkX, int chunkZ) {
return ((long) chunkX << 32) | (chunkZ & 0xffffffffL);
}
public record Options(Engine engine, IrisComplex complex) {
public Options {
Objects.requireNonNull(engine, "terrain engine");
Objects.requireNonNull(complex, "terrain complex");
}
}
private record TransformTarget(int chunkX, int chunkZ, boolean columnOnly, int localX, int localZ) {
}
private record SnapshotData(NativeBlockState[] blocks, NativeBiome[] biomes,
CaveTerrainSnapshot caves, FloatingBiomeOverlay floating) {
}
private static final class SnapshotHunk<T> extends StorageHunk<T> {
private final T[] values;
private final T fallback;
private SnapshotHunk(T[] values, T fallback) {
super(16, values.length / 256, 16);
this.values = values;
this.fallback = fallback;
}
@Override
public T getRaw(int x, int y, int z) {
return y < 0 || y >= getHeight() ? fallback : values[index(x, y, z)];
}
@Override
public void setRaw(int x, int y, int z, T value) {
if (value != null) {
values[index(x, y, z)] = value;
}
}
}
private final class TransformContext implements TerrainTransformContext {
private final int x;
private final int z;
private final NativeBlockState[] blocks;
private final TransformTarget target;
private final Snapshot[] sources = new Snapshot[9];
private volatile boolean active = true;
private TransformContext(TransformTarget target, Snapshot source) {
this.target = target;
x = target.chunkX() << 4;
z = target.chunkZ() << 4;
if (target.columnOnly()) {
blocks = new NativeBlockState[height];
for (int y = 0; y < height; y++) {
blocks[y] = source.blocks[index(target.localX(), y, target.localZ())];
}
} else {
blocks = source.blocks.clone();
}
sources[4] = source;
}
@Override
public int blockX() {
return x;
}
@Override
public int blockZ() {
return z;
}
@Override
public int minY() {
return minY;
}
@Override
public int height() {
return height;
}
@Override
public NativeBlockState original(int worldX, int worldY, int worldZ) {
requireActive();
long localX = (long) worldX - x;
long localZ = (long) worldZ - z;
if (localX < -radius || localX >= 16L + radius || localZ < -radius || localZ >= 16L + radius) {
throw new IllegalArgumentException("Terrain transform read exceeds its declared radius");
}
long localY = (long) worldY - minY;
if (localY < 0 || localY >= height) {
return air;
}
int sourceX = worldX >> 4;
int sourceZ = worldZ >> 4;
int slot = (sourceZ - (z >> 4) + 1) * 3 + sourceX - (x >> 4) + 1;
Snapshot snapshot = sources[slot];
if (snapshot == null) {
snapshot = TerrainTransformRuntime.this.original(sourceX, sourceZ);
sources[slot] = snapshot;
}
return snapshot.blocks[index(worldX & 15, (int) localY, worldZ & 15)];
}
@Override
public void set(int localX, int localY, int localZ, NativeBlockState state) {
requireActive();
Objects.requireNonNull(state, "terrain transform block state");
if (localX < 0 || localX >= 16 || localZ < 0 || localZ >= 16 || localY < 0 || localY >= height) {
throw new IllegalArgumentException("Terrain transform write exceeds its output chunk");
}
if (target.columnOnly() && (localX != target.localX() || localZ != target.localZ())) {
throw new IllegalArgumentException("Terrain column transform write exceeds its output column");
}
if (complex.allowsMantleWrite(x + localX, z + localZ)) {
blocks[target.columnOnly() ? localY : index(localX, localY, localZ)] = state;
}
}
private void requireActive() {
requireOpen();
if (!active) {
throw new IllegalStateException("Terrain transform context is no longer active");
}
}
}
private static final class ColumnSnapshot {
private final Terrain3DColumn terrain;
private final int ground;
private final int top;
private final int fluid;
private ColumnSnapshot(NativeBlockState[] blocks) {
int solidHeight = -1;
int topHeight = -1;
int fluidHeight = -1;
for (int y = blocks.length - 1; y >= 0; y--) {
NativeBlockState state = blocks[y];
if (state.isAir()) {
continue;
}
if (topHeight == -1) {
topHeight = y;
}
if ((state.isFluid() || state.isWaterLogged()) && fluidHeight == -1) {
fluidHeight = y;
}
if (!state.isFluid() && solidHeight == -1) {
solidHeight = y;
}
}
ground = solidHeight;
top = topHeight;
fluid = fluidHeight;
terrain = Terrain3DColumn.fromOccupancy(ground, blocks.length,
y -> !blocks[y].isAir() && !blocks[y].isFluid());
}
private int weight() {
return 192 + terrain.spanCount() * 8;
}
}
private static final class Snapshot {
private final NativeBlockState[] blocks;
private final NativeBiome[] biomes;
private final CaveTerrainSnapshot caves;
private final FloatingBiomeOverlay floating;
private final int[] solidHeights;
private final int[] topHeights;
private final int[] fluidHeights;
private final Terrain3DColumn[] columns;
private Snapshot(SnapshotData data, boolean effective) {
blocks = data.blocks();
biomes = data.biomes();
caves = data.caves();
floating = data.floating();
solidHeights = effective ? new int[256] : null;
topHeights = effective ? new int[256] : null;
fluidHeights = effective ? new int[256] : null;
columns = effective ? new Terrain3DColumn[256] : null;
if (!effective) {
return;
}
Arrays.fill(solidHeights, -1);
Arrays.fill(topHeights, -1);
Arrays.fill(fluidHeights, -1);
int height = blocks.length / 256;
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
int column = (z << 4) | x;
for (int y = height - 1; y >= 0; y--) {
NativeBlockState state = blocks[index(x, y, z)];
if (state.isAir()) {
continue;
}
if (topHeights[column] == -1) {
topHeights[column] = y;
}
if ((state.isFluid() || state.isWaterLogged()) && fluidHeights[column] == -1) {
fluidHeights[column] = y;
}
if (!state.isFluid() && solidHeights[column] == -1) {
solidHeights[column] = y;
}
}
}
}
}
private int height(int x, int z, boolean ignoreFluid) {
return (ignoreFluid ? solidHeights : topHeights)[(z << 4) | x];
}
private Terrain3DColumn column(int x, int z) {
int index = (z << 4) | x;
Terrain3DColumn column = columns[index];
if (column == null) {
column = Terrain3DColumn.fromOccupancy(solidHeights[index], blocks.length / 256, y -> {
NativeBlockState state = blocks[index(x, y, z)];
return !state.isAir() && !state.isFluid();
});
columns[index] = column;
}
return column;
}
private int weight() {
long blockReferences = (long) blocks.length * 20L;
long caveReferences = caves == null ? 0L : caves.estimatedRetainedBytes();
long floatingReferences = floating == null ? 0L : (long) blocks.length * 4L;
return (int) Math.min(Integer.MAX_VALUE, 16384L + blockReferences + caveReferences + floatingReferences);
}
}
}
@@ -0,0 +1,11 @@
package art.arcane.iris.generation.terrain.transform;
public interface TerrainTransformer {
int radius();
void transform(TerrainTransformContext context);
default TerrainColumnTransformer columnTransformer() {
return null;
}
}
@@ -64,6 +64,13 @@ public final class FloatingBiomeOverlay {
return height;
}
public FloatingBiomeOverlay copy() {
FloatingBiomeOverlay copy = new FloatingBiomeOverlay(height);
System.arraycopy(volume, 0, copy.volume, 0, volume.length);
System.arraycopy(surfaces, 0, copy.surfaces, 0, surfaces.length);
return copy;
}
private int index(int localX, int y, int localZ) {
return ((localX >> 2) * 4 + (localZ >> 2)) * quartHeight + (y >> 2);
}
@@ -81,6 +81,23 @@ public class CaveTerrainSnapshotTest {
assertNull(snapshot.cavern(4, 22, 6));
}
@Test
public void retainedEstimateCountsOnlyAllocatedSectionsAndTheirCells() {
Matter[] sections = new Matter[SECTIONS];
long emptyBytes = CaveTerrainSnapshot.capture(chunk(sections), 0, 0).estimatedRetainedBytes();
assertTrue(emptyBytes < 1024);
sections[1] = new IrisMatter(16, 16, 16);
sections[1].slice(MatterCavern.class).set(0, 0, 0, new MatterCavern(true, "cave", (byte) 0));
CaveTerrainSnapshot sparse = CaveTerrainSnapshot.capture(chunk(sections), 0, 0);
long sparseBytes = sparse.estimatedRetainedBytes();
assertTrue(sparseBytes >= emptyBytes + 4096L * 8L);
assertTrue(sparseBytes < emptyBytes + 4096L * 16L);
sections[1].slice(MatterCavern.class).set(1, 0, 0, new MatterCavern(true, "other", (byte) 0));
long moreCells = CaveTerrainSnapshot.capture(chunk(sections), 0, 0).estimatedRetainedBytes();
assertTrue(moreCells > sparseBytes);
assertEquals(sparseBytes, sparse.estimatedRetainedBytes());
}
private static Map<String, String> legacyResolverInputs(MantleChunk<Matter> chunk) {
Map<String, String> inputs = new HashMap<>();
for (int x = 0; x < 16; x++) {
@@ -0,0 +1,67 @@
package art.arcane.iris.generation.mode;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.iris.generation.mantle.EngineMantle;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.terrain.transform.TerrainTransformRuntime;
import org.junit.Test;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
public class ModeOverworldTerrainTransformTest {
@Test
public void studioRawSnapshotsKeepTerrainMatterOnTheBoundCallingThread() {
Fixture fixture = new Fixture();
doReturn(mock(TerrainTransformRuntime.class)).when(fixture.complex).getTerrainTransform();
Thread owner = Thread.currentThread();
doAnswer(invocation -> {
assertSame(owner, Thread.currentThread());
assertSame(fixture.context, IrisContext.require().getChunkContext());
return null;
}).when(fixture.mantle).generateTerrainMatter(eq(-2), eq(3), anyBoolean(), eq(fixture.context));
try (IrisContext.Scope scope = IrisContext.open(fixture.engine, 41L, fixture.context)) {
fixture.mode.generateTerrainMatterForChunk(-2, 3, false, fixture.context);
}
verify(fixture.mantle).generateTerrainMatter(-2, 3, false, fixture.context);
}
@Test
public void ordinaryStudioTerrainStillUsesParallelMatter() {
Fixture fixture = new Fixture();
fixture.mode.generateTerrainMatterForChunk(-2, 3, false, fixture.context);
verify(fixture.mantle).generateTerrainMatter(-2, 3, true, fixture.context);
}
@Test
public void explicitParallelGenerationRemainsParallelWithATransformer() {
Fixture fixture = new Fixture();
doReturn(mock(TerrainTransformRuntime.class)).when(fixture.complex).getTerrainTransform();
fixture.mode.generateTerrainMatterForChunk(-2, 3, true, fixture.context);
verify(fixture.mantle).generateTerrainMatter(-2, 3, true, fixture.context);
}
private static final class Fixture {
private final ModeOverworld mode = mock(ModeOverworld.class, CALLS_REAL_METHODS);
private final Engine engine = mock(Engine.class);
private final EngineMantle mantle = mock(EngineMantle.class);
private final IrisComplex complex = mock(IrisComplex.class);
private final ChunkContext context = mock(ChunkContext.class);
private Fixture() {
doReturn(engine).when(mode).getEngine();
doReturn(mantle).when(engine).getMantle();
doReturn(true).when(engine).isStudio();
doReturn(complex).when(context).getComplex();
doReturn(true).when(context).isNaturalTerrain();
}
}
}
@@ -1,6 +1,8 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.generation.image.IrisImageMapRuntime;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion;
@@ -54,10 +56,10 @@ public class DimensionTerrainContextFallbackTest {
when(engine.getHeight()).thenReturn(128);
when(dimension.getLoadKey()).thenReturn("root");
when(complex.hasTerrain3D()).thenReturn(true);
when(complex.getSlopeStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 40D));
when(complex.terrainColumn(-17, 8)).thenReturn(Terrain3DColumnFixtures.spans(20, 0, 5, 20, 40));
when(complex.terrainColumn(-14, 8)).thenReturn(Terrain3DColumnFixtures.spans(20, 0, 8, 20, 60));
when(complex.terrainColumn(-17, 11)).thenReturn(Terrain3DColumnFixtures.spans(20, 0, 9, 20, 80));
when(complex.getRawSlopeStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 40D));
when(complex.originalTerrainColumn(-17, 8)).thenReturn(Terrain3DColumnFixtures.spans(20, 0, 5, 20, 40));
when(complex.originalTerrainColumn(-14, 8)).thenReturn(Terrain3DColumnFixtures.spans(20, 0, 8, 20, 60));
when(complex.originalTerrainColumn(-17, 11)).thenReturn(Terrain3DColumnFixtures.spans(20, 0, 9, 20, 80));
DimensionTerrainContext context = DimensionTerrainContext.forStack(engine, dimension);
assertEquals(5D, context.getSurfaceSlopeStream(5).getDouble(-17, 8), 0D);
@@ -90,7 +92,7 @@ public class DimensionTerrainContextFallbackTest {
when(dimension.getLoadKey()).thenReturn("root");
when(dimension.getFluidHeight()).thenReturn(12);
when(complex.getNaturalHeightStream()).thenReturn(naturalHeight);
when(complex.getHeightStream()).thenReturn(resolvedHeight);
when(complex.getRawHeightStream()).thenReturn(resolvedHeight);
when(complex.getRiverWaterSurfaceStream()).thenReturn(resolvedFluidHeight);
when(complex.getNaturalTrueBiomeStream()).thenReturn(naturalBiome);
when(complex.getTrueBiomeStream()).thenReturn(resolvedBiome);
@@ -132,7 +134,7 @@ public class DimensionTerrainContextFallbackTest {
Terrain3DColumn natural = Terrain3DColumnFixtures.spans(30, 0, 15, 25, 40);
Terrain3DColumn resolved = Terrain3DColumnFixtures.spans(30, 0, 12, 28, 35);
when(complex.naturalTerrainColumn(-17, 8)).thenReturn(natural);
when(complex.terrainColumn(-17, 8)).thenReturn(resolved);
when(complex.originalTerrainColumn(-17, 8)).thenReturn(resolved);
DimensionTerrainContext context = DimensionTerrainContext.forStack(engine, dimension);
assertSame(resolved, context.terrainColumn(-17, 8));
@@ -141,4 +143,34 @@ public class DimensionTerrainContextFallbackTest {
when(engine.answersFromNaturalTerrain(-17, 8)).thenReturn(false);
assertSame(resolved, context.terrainColumn(-17, 8));
}
@Test
public void stackInputsRemainOriginalWhenTheCallingChunkBeginsContent() {
Engine engine = mock(Engine.class);
IrisDimension dimension = mock(IrisDimension.class);
IrisComplex complex = mock(IrisComplex.class);
Terrain3DColumn original = Terrain3DColumnFixtures.spans(30, 0, 12, 28, 35);
when(engine.getDimension()).thenReturn(dimension);
when(engine.getComplex()).thenReturn(complex);
when(engine.getHeight()).thenReturn(128);
when(dimension.getLoadKey()).thenReturn("root");
when(complex.getRawHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 35D));
when(complex.getRawSlopeStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 4D));
when(complex.originalTerrainColumn(0, 0)).thenReturn(original);
when(complex.getHeightStream()).thenThrow(new AssertionError("Layout read final terrain height"));
when(complex.getSlopeStream()).thenThrow(new AssertionError("Layout read final terrain slope"));
when(complex.terrainColumn(0, 0)).thenThrow(new AssertionError("Layout read final terrain occupancy"));
DimensionTerrainContext terrain = DimensionTerrainContext.forStack(engine, dimension);
ChunkContext chunk = new ChunkContext(0, 0, complex, 7L, false, ChunkContext.PrefillPlan.NONE, null);
try (IrisContext.Scope ignored = IrisContext.open(engine, 7L, chunk)) {
assertEquals(35D, terrain.getNormalTerrainHeight(0, 0), 0D);
assertEquals(4D, terrain.getSlopeStream().getDouble(0, 0), 0D);
assertSame(original, terrain.terrainColumn(0, 0));
chunk.beginContent();
assertEquals(35D, terrain.getNormalTerrainHeight(0, 0), 0D);
assertEquals(4D, terrain.getSlopeStream().getDouble(0, 0), 0D);
assertSame(original, terrain.terrainColumn(0, 0));
}
}
}
@@ -0,0 +1,341 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.generation.block.B;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.transform.IrisTerrainTransform;
import art.arcane.iris.generation.terrain.transform.TerrainTransformRegistry;
import art.arcane.iris.generation.terrain.transform.TerrainTransformRuntime;
import art.arcane.iris.generation.terrain.transform.TerrainTransformer;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.volmlib.util.stream.ProceduralStream;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.MockedConstruction;
import org.mockito.MockedStatic;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Answers.CALLS_REAL_METHODS;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockConstruction;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class IrisComplexTerrainTransformTest {
@ClassRule public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
private IrisSettings previousSettings;
@Before
public void prepareRuntime() {
previousSettings = IrisSettings.settings;
IrisSettings.settings = new IrisSettings();
IrisServices.register(PreservationRegistry.class, mock(PreservationRegistry.class));
IrisPlatform platform = mock(IrisPlatform.class);
when(platform.platformName()).thenReturn("bukkit");
IrisPlatforms.bind(platform);
}
@After
public void releaseRuntime() {
IrisPlatforms.unbind();
IrisServices.remove(PreservationRegistry.class);
IrisSettings.settings = previousSettings;
}
@Test
public void absentProviderRetainsTheOriginalStreamsAndStackHeights() throws Exception {
try (Fixture fixture = new Fixture(pack(), false)) {
ProceduralStream<Double> heights = ProceduralStream.ofDouble((x, z) -> 80D);
ProceduralStream<Integer> rounded = heights.round();
ProceduralStream<Double> tops = ProceduralStream.ofDouble((x, z) -> 90D);
ProceduralStream<Double> slopes = ProceduralStream.ofDouble((x, z) -> 4D);
fixture.complex.setHeightStream(heights);
fixture.complex.setRoundedHeighteightStream(rounded);
fixture.complex.setHeightFluidStream(tops);
fixture.complex.setSlopeStream(slopes);
assertNull(fixture.complex.getTerrainTransform());
assertSame(heights, fixture.complex.getHeightStream());
assertSame(rounded, fixture.complex.getRoundedHeighteightStream());
assertSame(tops, fixture.complex.getHeightFluidStream());
assertSame(slopes, fixture.complex.getSlopeStream());
assertSame(slopes, fixture.complex.getRawSlopeStream());
fixture.installStack();
assertEquals(40, fixture.engine.getHeight(0, 0, true));
assertEquals(50, fixture.engine.getHeight(0, 0, false));
}
}
@Test
public void capturedStreamsSwitchAtSamplingTimeWhenContentBegins() throws Exception {
try (Fixture fixture = new Fixture(pack(), true)) {
fixture.complex.setHeightStream(ProceduralStream.ofDouble((x, z) -> 80D + 2D * x + 3D * z));
fixture.complex.setRoundedHeighteightStream(ProceduralStream.ofDouble((x, z) -> 81D).round());
fixture.complex.setHeightFluidStream(ProceduralStream.ofDouble((x, z) -> 90D));
fixture.complex.setSlopeStream(ProceduralStream.ofDouble((x, z) -> 9D));
ProceduralStream<Double> heights = fixture.complex.getHeightStream();
ProceduralStream<Integer> rounded = fixture.complex.getRoundedHeighteightStream();
ProceduralStream<Double> tops = fixture.complex.getHeightFluidStream();
ProceduralStream<Double> slopes = fixture.complex.getSlopeStream();
ChunkContext context = fixture.context();
try (IrisContext.Scope ignored = IrisContext.open(fixture.engine, 7L, context)) {
assertEquals(80D, heights.getDouble(0, 0), 0D);
assertEquals(Integer.valueOf(81), rounded.get(0, 0));
assertEquals(90D, tops.getDouble(0, 0), 0D);
assertEquals(9D, slopes.getDouble(0, 0), 0D);
assertEquals(0, fixture.materializations.get());
context.beginContent();
assertEquals(120D, heights.getDouble(0, 0), 0D);
assertEquals(Integer.valueOf(120), rounded.get(0, 0));
assertEquals(125D, tops.getDouble(0, 0), 0D);
assertEquals(0D, slopes.getDouble(0, 0), 0D);
}
}
}
@Test
public void absentProviderRunsTerrainTransitionAndContentInTheirOriginalOrder() throws Exception {
try (Fixture fixture = new Fixture(pack(), false)) {
EngineMode mode = mock(EngineMode.class, CALLS_REAL_METHODS);
doReturn(fixture.engine).when(mode).getEngine();
doReturn(fixture.complex).when(mode).getComplex();
when(fixture.engine.getPlatformHooks().shouldDisableChunkContextCache(fixture.engine)).thenReturn(true);
List<String> stages = new ArrayList<>();
NativeBlockState terrain = mock(NativeBlockState.class);
NativeBlockState transitioned = mock(NativeBlockState.class);
EngineStage natural = (x, z, blocks, biomes, multicore, context) -> {
assertTrue(context.isNaturalTerrain());
blocks.set(0, 1, 0, terrain);
stages.add("terrain");
};
EngineStage transition = (x, z, blocks, biomes, multicore, context) -> {
assertTrue(context.isNaturalTerrain());
assertSame(terrain, blocks.get(0, 1, 0));
blocks.set(0, 1, 0, transitioned);
stages.add("transition");
};
EngineStage content = (x, z, blocks, biomes, multicore, context) -> {
assertFalse(context.isNaturalTerrain());
assertSame(transitioned, blocks.get(0, 1, 0));
stages.add("content");
};
doReturn(new KList<>(natural)).when(mode).getTerrainStages();
doReturn(transition).when(mode).getTransitionStage();
doReturn(new KList<>(content)).when(mode).getStages();
Hunk<NativeBlockState> blocks = Hunk.newArrayHunk(16, 8, 16);
Hunk<NativeBiome> biomes = Hunk.newArrayHunk(16, 8, 16);
mode.generate(0, 0, blocks, biomes, false, 7L);
assertEquals(List.of("terrain", "transition", "content"), stages);
assertSame(transitioned, blocks.get(0, 1, 0));
}
}
@Test
public void transformedHeightsOverrideTheStackOnlyOutsideNaturalGeneration() throws Exception {
try (Fixture fixture = new Fixture(pack(), true)) {
fixture.installStack();
assertEquals(120, fixture.engine.getHeight(0, 0, true));
assertEquals(125, fixture.engine.getHeight(0, 0, false));
assertEquals(120, Engine.hostHeight(fixture.engine, 0, 0, true));
assertEquals(125, Engine.hostHeight(fixture.engine, 0, 0, false));
try (IrisContext.Scope ignored = IrisContext.open(fixture.engine, 7L, fixture.context())) {
assertEquals(40, fixture.engine.getHeight(0, 0, true));
assertEquals(50, fixture.engine.getHeight(0, 0, false));
}
}
}
@Test
public void mainThreadColdQueriesUseRawHeightsWithoutMaterializingTerrain() throws Exception {
try (Fixture fixture = new Fixture(pack(), true)) {
fixture.mainThread.set(true);
doReturn(7L).when(fixture.engine).getGenerationSessionId();
fixture.complex.setHeightStream(ProceduralStream.ofDouble((x, z) -> 80D));
fixture.complex.setHeightFluidStream(ProceduralStream.ofDouble((x, z) -> 90D));
assertEquals(Integer.MIN_VALUE, fixture.complex.transformedHeight(0, 0, true));
assertEquals(80D, fixture.complex.getHeightStream().getDouble(0, 0), 0D);
assertEquals(90D, fixture.complex.getHeightFluidStream().getDouble(0, 0), 0D);
fixture.ready.set(true);
assertEquals(120D, fixture.complex.getHeightStream().getDouble(0, 0), 0D);
assertEquals(125D, fixture.complex.getHeightFluidStream().getDouble(0, 0), 0D);
assertEquals(0, fixture.materializations.get());
}
}
@Test
public void mainThreadContentQueriesMaterializeUncachedNeighborTerrain() throws Exception {
try (Fixture fixture = new Fixture(pack(), true)) {
fixture.mainThread.set(true);
ChunkContext context = fixture.context();
context.beginContent();
try (IrisContext.Scope ignored = IrisContext.open(fixture.engine, 0L, context)) {
assertEquals(120, fixture.complex.transformedHeight(16, 0, true));
assertEquals(125, fixture.complex.transformedHeight(16, 0, false));
assertEquals(2, fixture.materializations.get());
}
}
}
@Test
public void originalSlopeIgnoresContentHeightOverrides() throws Exception {
try (Fixture fixture = new Fixture(pack(), true)) {
fixture.complex.setHeightStream(ProceduralStream.ofDouble((x, z) -> 80D + x + z));
ProceduralStream<Double> originalSlope = fixture.complex.getRawSlopeStream();
ChunkContext context = fixture.context();
context.setTerrainHeight(0, 0, 200);
context.setTerrainHeight(3, 0, 250);
context.setTerrainHeight(0, 3, 300);
context.beginContent();
try (IrisContext.Scope ignored = IrisContext.open(fixture.engine, 7L, context)) {
assertEquals(Math.sqrt(18D), originalSlope.getDouble(0, 0), 0.000001D);
assertEquals(0, fixture.materializations.get());
}
}
}
@Test
public void admittedMainThreadQueriesWaitUntilTheirComplexAndModeArePublished() throws Exception {
try (Fixture fixture = new Fixture(pack(), true)) {
fixture.mainThread.set(true);
doReturn(null).when(fixture.engine).getComplex();
try (IrisContext.Scope ignored = IrisContext.open(fixture.engine, 7L, null)) {
assertEquals(Integer.MIN_VALUE, fixture.complex.transformedHeight(16, 0, true));
doReturn(fixture.complex).when(fixture.engine).getComplex();
assertEquals(Integer.MIN_VALUE, fixture.complex.transformedHeight(16, 0, true));
assertEquals(0, fixture.materializations.get());
doReturn(mock(EngineMode.class)).when(fixture.engine).getMode();
assertEquals(120, fixture.complex.transformedHeight(16, 0, true));
assertEquals(1, fixture.materializations.get());
}
}
}
private Path pack() throws Exception {
Path pack = temporaryFolder.newFolder().toPath();
Files.createDirectories(pack.resolve("dimensions"));
Files.createDirectories(pack.resolve("regions"));
Files.createDirectories(pack.resolve("biomes"));
Files.createDirectories(pack.resolve("generators"));
Files.writeString(pack.resolve("dimensions/main.json"), """
{"focus":"plain","regions":["plain"],"landChance":1,
"carvingEnabled":false,"hydrology":{"rivers":{"enabled":false}}}
""");
Files.writeString(pack.resolve("regions/plain.json"), "{\"landBiomes\":[\"plain\"]}");
Files.writeString(pack.resolve("generators/flat.json"), "{\"composite\":[]}");
Files.writeString(pack.resolve("biomes/plain.json"),
"{\"generators\":[{\"generator\":\"flat\",\"min\":10,\"max\":10}]}");
return pack;
}
private static final class Fixture implements AutoCloseable {
private final Engine engine = mock(Engine.class, CALLS_REAL_METHODS);
private final AtomicBoolean mainThread = new AtomicBoolean();
private final AtomicBoolean ready = new AtomicBoolean();
private final AtomicInteger materializations = new AtomicInteger();
private final MockedStatic<B> blocks = mockStatic(B.class);
private final MockedStatic<TerrainTransformRegistry> registries = mockStatic(TerrainTransformRegistry.class);
private final MockedConstruction<TerrainTransformRuntime> runtimes;
private final IrisData data;
private final IrisComplex complex;
private Fixture(Path pack, boolean transformed) {
runtimes = mockConstruction(TerrainTransformRuntime.class, (runtime, context) -> {
when(runtime.height(anyInt(), anyInt(), anyBoolean())).thenAnswer(invocation -> {
materializations.incrementAndGet();
return invocation.<Boolean>getArgument(2) ? 120 : 125;
});
when(runtime.readyHeight(anyInt(), anyInt(), anyBoolean())).thenAnswer(invocation ->
ready.get() ? invocation.<Boolean>getArgument(2) ? 120 : 125 : Integer.MIN_VALUE);
});
NativeBlockState block = mock(NativeBlockState.class);
when(block.isFluid()).thenReturn(true);
blocks.when(() -> B.getState(anyString())).thenReturn(block);
blocks.when(() -> B.getStateOrNull(anyString(), eq(false))).thenReturn(block);
data = IrisData.openRuntime(pack.toFile());
IrisDimension dimension = data.getDimensionLoader().load("main");
if (transformed) {
IrisTerrainTransform descriptor = new IrisTerrainTransform().setId("test").setVersion("1");
dimension.setTerrainTransform(descriptor);
TerrainTransformRegistry registry = mock(TerrainTransformRegistry.class);
when(registry.resolve(descriptor)).thenReturn(mock(TerrainTransformer.class));
registries.when(TerrainTransformRegistry::discover).thenReturn(registry);
}
doReturn(data).when(engine).getData();
doReturn(dimension).when(engine).getDimension();
doReturn(data.getBiomeLoader().load("plain")).when(engine).getFocus();
doReturn(384).when(engine).getHeight();
doReturn(-64).when(engine).getMinHeight();
doReturn(320).when(engine).getMaxHeight();
EnginePlatformHooks hooks = mock(EnginePlatformHooks.class);
when(hooks.isMainThread()).thenAnswer(ignored -> mainThread.get());
doReturn(hooks).when(engine).getPlatformHooks();
doReturn(new SeedManager(1337L)).when(engine).getSeedManager();
complex = new IrisComplex(engine);
doReturn(complex).when(engine).getComplex();
}
private ChunkContext context() {
return new ChunkContext(0, 0, complex, 7L, false, ChunkContext.PrefillPlan.NONE, null);
}
private void installStack() {
DimensionStackContext stack = mock(DimensionStackContext.class);
when(stack.getStackTerrainHeight(0, 0)).thenReturn(40);
when(stack.getStackTopHeight(0, 0)).thenReturn(50);
doReturn(stack).when(engine).getDimensionStackContext();
}
@Override
public void close() {
try {
complex.close();
data.close();
} finally {
runtimes.close();
registries.close();
blocks.close();
}
}
}
}
@@ -42,7 +42,7 @@ public class UpperDimensionTransitionTest {
Terrain3DColumn terrain = Terrain3DColumnFixtures.spans(80, 0, 30, 40, 60, 100, 110);
when(complex.naturalTerrainColumn(-17, 8)).thenReturn(terrain);
when(complex.getNaturalHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 110D));
when(complex.getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 64D));
when(complex.getRawHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 64D));
Engine engine = mock(Engine.class);
when(engine.getDimension()).thenReturn(dimension);
when(engine.getComplex()).thenReturn(complex);
@@ -63,7 +63,7 @@ public class UpperDimensionTransitionTest {
when(dimension.getUpperDimensionGap()).thenReturn(32);
IrisComplex complex = mock(IrisComplex.class);
when(complex.getNaturalHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 139D));
when(complex.getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 75D + 4D * x));
when(complex.getRawHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 75D + 4D * x));
TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class);
when(complex.getTransitionGenerationPlan()).thenReturn(plan);
Engine engine = mock(Engine.class);
@@ -86,7 +86,7 @@ public class UpperDimensionTransitionTest {
when(dimension.getUpperDimensionGap()).thenReturn(32);
IrisComplex complex = mock(IrisComplex.class);
when(complex.getNaturalHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 80D));
when(complex.getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 64D));
when(complex.getRawHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 64D));
TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class);
when(complex.getTransitionGenerationPlan()).thenReturn(plan);
Engine engine = mock(Engine.class);
@@ -126,4 +126,23 @@ public class UpperDimensionTransitionTest {
assertThrows(IllegalArgumentException.class, () -> new UpperDimensionContext.CeilingLayout(256, -1));
}
@Test
public void upperGapIgnoresTheFinalTransformedHostHeight() {
IrisDimension dimension = mock(IrisDimension.class);
when(dimension.getLoadKey()).thenReturn("main");
when(dimension.getUpperDimensionGap()).thenReturn(32);
IrisComplex complex = mock(IrisComplex.class);
when(complex.getNaturalHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 80D));
when(complex.getRawHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 64D));
when(complex.getHeightStream()).thenThrow(new AssertionError("Upper gap read final terrain height"));
Engine engine = mock(Engine.class);
when(engine.getDimension()).thenReturn(dimension);
when(engine.getComplex()).thenReturn(complex);
when(engine.getHeight()).thenReturn(256);
UpperDimensionContext context = UpperDimensionContext.create(engine, dimension);
assertEquals(175, context.getEffectiveSurfaceY(0, 0));
}
}
@@ -5,6 +5,8 @@ import art.arcane.iris.generation.decoration.IrisCeilingDecorator;
import art.arcane.iris.generation.decoration.IrisSurfaceDecorator;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.SeedManager;
import art.arcane.iris.generation.terrain.transform.TerrainTransformRuntime;
import art.arcane.iris.generation.terrain.Terrain3DColumn;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.mantle.CaveTerrainSnapshot;
import art.arcane.iris.generation.mantle.EngineMantle;
@@ -162,6 +164,64 @@ public class IrisNaturalCaveScanTest {
}
}
@Test
public void transformedCavesDecorateOnlyTheirRemainingFloorAndCeiling() throws Exception {
Fixture fixture = new Fixture();
fixture.transform();
Hunk<NativeBlockState> output = fixture.cavity();
output.setRaw(0, 4, 0, fixture.stone);
output.setRaw(0, 5, 0, fixture.stone);
fixture.modifier.decorateNaturalCaves(-32, 48, output, null);
assertEquals(List.of("floor:0:0:5:1", "ceiling:0:0:8:1"), fixture.calls);
assertSame(fixture.stone, output.getRaw(0, 4, 0));
assertSame(fixture.stone, output.getRaw(0, 5, 0));
assertSame(fixture.floor, output.getRaw(0, 6, 0));
assertSame(fixture.ceiling, output.getRaw(0, 8, 0));
}
@Test
public void transformedFilledCavesAndRemovedBoundariesRemainUndecorated() throws Exception {
Fixture filled = new Fixture();
filled.transform();
Hunk<NativeBlockState> filledOutput = filled.cavity();
for (int y = 4; y <= 8; y++) {
filledOutput.setRaw(0, y, 0, filled.stone);
}
filled.modifier.decorateNaturalCaves(-32, 48, filledOutput, null);
assertTrue(filled.calls.isEmpty());
assertTrue(filled.markers.isEmpty());
Fixture openRoof = new Fixture();
openRoof.transform();
Hunk<NativeBlockState> openRoofOutput = openRoof.cavity();
for (int y = 9; y < 32; y++) {
openRoofOutput.setRaw(0, y, 0, openRoof.air);
}
openRoof.modifier.decorateNaturalCaves(-32, 48, openRoofOutput, null);
assertTrue(openRoof.calls.isEmpty());
Fixture openFloor = new Fixture();
openFloor.transform();
Hunk<NativeBlockState> openFloorOutput = openFloor.cavity();
for (int y = 0; y < 4; y++) {
openFloorOutput.setRaw(0, y, 0, openFloor.air);
}
openFloor.modifier.decorateNaturalCaves(-32, 48, openFloorOutput, null);
assertTrue(openFloor.calls.isEmpty());
}
@Test
public void transformKeepsNaturalTerrainOpeningsOutOfCaveDecoration() throws Exception {
Fixture fixture = new Fixture();
fixture.transform();
doReturn(true).when(fixture.complex).hasTerrain3D();
Terrain3DColumn natural = Terrain3DColumn.fromOccupancy(31, 32, y -> y < 4 || y > 8);
doReturn(natural).when(fixture.complex).terrainColumn(anyInt(), anyInt(), any());
fixture.modifier.decorateNaturalCaves(-32, 48, fixture.cavity(), null);
assertTrue(fixture.calls.isEmpty());
assertTrue(fixture.markers.isEmpty());
}
private static void verifyScan(int width, int depth) throws Exception {
Fixture fixture = new Fixture();
Hunk<NativeBlockState> output = fixture.output(width, depth);
@@ -236,6 +296,7 @@ public class IrisNaturalCaveScanTest {
private final NativeBlockState floor = block("minecraft:moss_block", true);
private final NativeBlockState ceiling = block("minecraft:calcite", true);
private final IrisCarveModifier modifier = mock(IrisCarveModifier.class, CALLS_REAL_METHODS);
private final IrisComplex complex = mock(IrisComplex.class);
private final Mantle<Matter> mantle;
private final MantleChunk<Matter> chunk;
private final List<String> calls = new ArrayList<>();
@@ -252,7 +313,7 @@ public class IrisNaturalCaveScanTest {
mantle = mock(Mantle.class);
chunk = mock(MantleChunk.class);
doReturn(engine).when(modifier).getEngine();
doReturn(mock(IrisComplex.class)).when(modifier).getComplex();
doReturn(complex).when(modifier).getComplex();
doReturn(32).when(engine).getHeight();
doReturn(new SeedManager(1337L)).when(engine).getSeedManager();
doReturn(engineMantle).when(engine).getMantle();
@@ -317,6 +378,20 @@ public class IrisNaturalCaveScanTest {
return output;
}
private void transform() {
doReturn(mock(TerrainTransformRuntime.class)).when(complex).getTerrainTransform();
doReturn(true).when(complex).isTerrain3DOpening(anyInt(), anyInt(), anyInt());
}
private Hunk<NativeBlockState> cavity() {
Hunk<NativeBlockState> output = Hunk.newArrayHunk(1, 32, 1);
output.fill(stone);
for (int y = 4; y <= 8; y++) {
output.setRaw(0, y, 0, air);
}
return output;
}
private Hunk<NativeBlockState> output(int width, int depth) {
Hunk<NativeBlockState> output = Hunk.newArrayHunk(width, 32, depth);
for (int x = 0; x < width; x++) {
@@ -8,6 +8,27 @@ import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class Terrain3DColumnTest {
@Test
public void occupancyPreservesCavitiesAndIsolatedBlocks() {
Terrain3DColumn column = Terrain3DColumn.fromOccupancy(7, 9,
y -> y < 3 || y == 5 || y >= 7);
assertEquals(3, column.spanCount());
assertEquals(8, column.topY());
assertEquals(2, column.surfaceY(1));
assertEquals(5, column.surfaceY(5));
assertFalse(column.isSolid(3));
assertFalse(column.isSolid(6));
assertTrue(column.isSolid(8));
}
@Test
public void emptyOccupancyHasNoSurface() {
Terrain3DColumn column = Terrain3DColumn.fromOccupancy(3, 9, y -> false);
assertEquals(-1, column.topY());
assertEquals(-1, column.highestSolidY(8));
assertFalse(column.isSolid(0));
}
@Test
public void displacementPreservesCavitiesAndUpperShelvesWhileKeepingBedrockGrounded() {
Terrain3DColumn original = new Terrain3DColumn(40D, 25, true, new int[]{0, 30, 36, 42, 50, 54});
@@ -0,0 +1,107 @@
package art.arcane.iris.generation.terrain.transform;
import art.arcane.volmlib.util.collection.KMap;
import org.junit.Test;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
public class TerrainTransformRegistryTest {
@Test
public void absentConfigurationDisablesTransformation() {
assertNull(new TerrainTransformRegistry(List.of()).resolve(null));
}
@Test
public void selectsThePinnedProviderVersion() {
TestProvider first = new TestProvider("1", 0);
TestProvider second = new TestProvider("2", 16);
TerrainTransformRegistry registry = new TerrainTransformRegistry(List.of(first, second));
assertSame(first.transformer, registry.resolve(descriptor("1")));
assertSame(second.transformer, registry.resolve(descriptor("2")));
}
@Test
public void rejectsUnavailableProvidersAndVersions() {
TerrainTransformRegistry registry = new TerrainTransformRegistry(List.of(new TestProvider("1", 1)));
assertThrows(IllegalStateException.class, () -> registry.resolve(descriptor("2")));
assertThrows(IllegalStateException.class, () -> registry.resolve(descriptor("1").setId("missing")));
}
@Test
public void freezesSettingsBeforeCreatingTheTransformer() {
TestProvider provider = new TestProvider("1", 1);
KMap<String, String> settings = new KMap<>();
settings.put("strength", "2");
IrisTerrainTransform descriptor = descriptor("1").setSettings(settings);
new TerrainTransformRegistry(List.of(provider)).resolve(descriptor);
settings.put("strength", "9");
assertEquals("2", provider.settings.get("strength"));
assertThrows(UnsupportedOperationException.class, () -> provider.settings.put("strength", "3"));
}
@Test
public void rejectsDuplicateProviderIdentities() {
assertThrows(IllegalArgumentException.class, () -> new TerrainTransformRegistry(
List.of(new TestProvider("1", 1), new TestProvider("1", 2))));
}
@Test
public void rejectsMissingIdentityAndInvalidNeighborhoodSizes() {
assertThrows(IllegalArgumentException.class,
() -> new TerrainTransformRegistry(List.of(new TestProvider(" ", 1))));
TerrainTransformRegistry registry = new TerrainTransformRegistry(List.of(new TestProvider("1", 1)));
assertThrows(IllegalArgumentException.class, () -> registry.resolve(descriptor(" ")));
assertThrows(IllegalArgumentException.class, () -> registry.resolve(descriptor("1").setId("")));
assertThrows(IllegalArgumentException.class, () -> new TerrainTransformRegistry(
List.of(new TestProvider("1", -1))).resolve(descriptor("1")));
assertThrows(IllegalArgumentException.class, () -> new TerrainTransformRegistry(
List.of(new TestProvider("1", 17))).resolve(descriptor("1")));
}
private static IrisTerrainTransform descriptor(String version) {
return new IrisTerrainTransform().setId("test").setVersion(version);
}
private static final class TestProvider implements TerrainTransformProvider {
private final String version;
private final TerrainTransformer transformer;
private Map<String, String> settings;
private TestProvider(String version, int radius) {
this.version = version;
this.transformer = new TestTransformer(radius);
}
@Override
public String id() {
return "test";
}
@Override
public String version() {
return version;
}
@Override
public TerrainTransformer create(Map<String, String> settings) {
this.settings = settings;
return transformer;
}
}
private record TestTransformer(int radius) implements TerrainTransformer {
@Override
public void transform(TerrainTransformContext context) {
}
}
}
@@ -0,0 +1,338 @@
package art.arcane.iris.generation.terrain.transform;
import art.arcane.iris.generation.block.B;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.EngineMode;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.terrain.Terrain3DColumn;
import art.arcane.iris.world.history.FloatingBiomeOverlay;
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.volmlib.util.stream.ProceduralStream;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class TerrainTransformRuntimeTest {
@Test
public void reusesTerrainAndCopiesOutputMetadata() {
try (Fixture fixture = new Fixture()) {
AtomicInteger transforms = new AtomicInteger();
TerrainTransformRuntime runtime = fixture.runtime(0, context -> {
transforms.incrementAndGet();
context.set(0, 2, 0, fixture.air);
context.set(0, 5, 0, fixture.stone);
context.set(0, 6, 0, fixture.water);
});
assertEquals(Integer.MIN_VALUE, runtime.readyHeight(0, 0, true));
assertEquals(Integer.MIN_VALUE, runtime.readyFluidHeight(0, 0));
assertNull(runtime.readyBlock(0, 0, 0));
assertNull(runtime.readyColumn(0, 0));
assertEquals(5, runtime.height(0, 0, true));
assertEquals(6, runtime.height(0, 0, false));
assertEquals(6, runtime.fluidHeight(0, 0));
assertEquals(6, runtime.readyFluidHeight(0, 0));
assertSame(fixture.air, runtime.block(0, 2, 0));
assertSame(fixture.stone, runtime.readyBlock(0, 5, 0));
assertEquals(5, runtime.column(0, 0).topY());
assertSame(runtime.column(0, 0), runtime.readyColumn(0, 0));
Hunk<NativeBlockState> blocks = Hunk.newArrayHunk(16, 8, 16);
Hunk<NativeBiome> biomes = Hunk.newArrayHunk(16, 8, 16);
ChunkContext first = fixture.context(0);
runtime.generate(0, 0, blocks, biomes, false, first);
assertEquals(5, first.getRoundedHeight(0, 0));
assertSame(fixture.stone, blocks.getRaw(0, 5, 0));
assertSame(fixture.biome, biomes.getRaw(0, 2, 0));
blocks.setRaw(0, 5, 0, fixture.air);
first.getFloatingBiomes().retainHighestSurfaces((x, z) -> -1);
ChunkContext second = fixture.context(0);
runtime.generate(0, 0, blocks, biomes, false, second);
assertSame(fixture.stone, blocks.getRaw(0, 5, 0));
assertNotSame(first.getFloatingBiomes(), second.getFloatingBiomes());
assertEquals(2, second.getFloatingBiomes().surfaceYAt(1, 1));
assertEquals(1, fixture.generations.get());
assertEquals(1, transforms.get());
assertEquals(5, runtime.readyHeight(0, 0, true));
runtime.close();
assertThrows(IllegalStateException.class, () -> runtime.height(0, 0, true));
}
}
@Test
public void neighboringReadsAlwaysUseOriginalTerrainInEitherOrder() {
try (Fixture fixture = new Fixture()) {
TerrainTransformer transformer = fixture.transformer(16, context -> {
int neighbor = context.blockX() == 0 ? 16 : 0;
assertSame(fixture.stone, context.original(neighbor, -2, 0));
assertSame(fixture.air, context.original(neighbor, -5, 0));
context.set(0, 2, 0, fixture.air);
});
TerrainTransformRuntime forward = new TerrainTransformRuntime(new TerrainTransformRuntime.Options(fixture.engine, fixture.complex), transformer);
TerrainTransformRuntime backward = new TerrainTransformRuntime(new TerrainTransformRuntime.Options(fixture.engine, fixture.complex), transformer);
assertEquals(1, forward.height(0, 0, true));
assertEquals(1, forward.height(16, 0, true));
assertEquals(1, backward.height(16, 0, true));
assertEquals(1, backward.height(0, 0, true));
forward.close();
backward.close();
}
}
@Test
public void rejectsReadsBeyondDeclaredRadiusAndWritesBeyondChunk() {
try (Fixture fixture = new Fixture()) {
TerrainTransformRuntime runtime = fixture.runtime(1, context -> {
assertThrows(IllegalArgumentException.class, () -> context.original(17, -2, 0));
assertThrows(IllegalArgumentException.class, () -> context.original(-2, -2, 0));
assertThrows(IllegalArgumentException.class, () -> context.set(16, 0, 0, fixture.stone));
assertThrows(IllegalArgumentException.class, () -> context.set(0, 8, 0, fixture.stone));
assertSame(fixture.stone, context.original(-1, -2, 0));
assertEquals(-4, context.minY());
assertEquals(8, context.height());
});
assertEquals(2, runtime.height(0, 0, true));
runtime.close();
}
}
@Test
public void preservesHistoricalColumnsAndCountsWaterloggedFluid() {
try (Fixture fixture = new Fixture()) {
when(fixture.complex.allowsMantleWrite(0, 0)).thenReturn(false);
NativeBlockState wetSlab = mock(NativeBlockState.class);
when(wetSlab.isWaterLogged()).thenReturn(true);
TerrainTransformRuntime runtime = fixture.runtime(0, context -> {
context.set(0, 5, 0, fixture.stone);
context.set(1, 5, 0, wetSlab);
});
assertEquals(2, runtime.height(0, 0, true));
assertEquals(5, runtime.height(1, 0, true));
assertEquals(5, runtime.fluidHeight(1, 0));
assertEquals(-1, runtime.fluidHeight(0, 0));
runtime.close();
}
}
@Test
public void concurrentNeighborTransformsDoNotDependOnEachOther() throws Exception {
try (Fixture fixture = new Fixture()) {
CountDownLatch entered = new CountDownLatch(2);
TerrainTransformRuntime runtime = fixture.runtime(16, context -> {
entered.countDown();
try {
assertTrue(entered.await(5, TimeUnit.SECONDS));
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new IllegalStateException(failure);
}
int neighbor = context.blockX() == 0 ? 16 : 0;
context.set(0, 4, 0, context.original(neighbor, -2, 0));
});
ExecutorService workers = Executors.newFixedThreadPool(2);
try {
Future<Integer> first = workers.submit(() -> runtime.height(0, 0, true));
Future<Integer> second = workers.submit(() -> runtime.height(16, 0, true));
assertEquals(Integer.valueOf(4), first.get(10, TimeUnit.SECONDS));
assertEquals(Integer.valueOf(4), second.get(10, TimeUnit.SECONDS));
assertEquals(2, fixture.generations.get());
} finally {
workers.shutdownNow();
runtime.close();
}
}
}
@Test
public void sparseColumnQueriesAvoidFullTransformationAndReuseOccupancy() {
try (Fixture fixture = new Fixture()) {
AtomicInteger fullCalls = new AtomicInteger();
AtomicInteger columnCalls = new AtomicInteger();
TerrainTransformRuntime runtime = fixture.columnRuntime(0, (context, x, z) -> {
columnCalls.incrementAndGet();
context.set(x, 2, z, fixture.air);
context.set(x, 5, z, fixture.stone);
context.set(x, 6, z, fixture.water);
assertThrows(IllegalArgumentException.class, () -> context.set((x + 1) & 15, 0, z, fixture.stone));
assertThrows(IllegalArgumentException.class, () -> context.set(x, 0, (z + 1) & 15, fixture.stone));
}, context -> fullCalls.incrementAndGet());
assertEquals(Integer.MIN_VALUE, runtime.readyHeight(3, 5, true));
assertEquals(5, runtime.height(3, 5, true));
assertEquals(6, runtime.height(3, 5, false));
assertEquals(6, runtime.fluidHeight(3, 5));
assertEquals(5, runtime.readyHeight(3, 5, true));
assertEquals(6, runtime.readyHeight(3, 5, false));
assertEquals(6, runtime.readyFluidHeight(3, 5));
assertEquals(5, runtime.column(3, 5).topY());
assertSame(runtime.column(3, 5), runtime.readyColumn(3, 5));
assertNull(runtime.readyBlock(3, 5, 5));
assertEquals(Integer.MIN_VALUE, runtime.readyHeight(4, 5, true));
assertEquals(1, columnCalls.get());
assertEquals(0, fullCalls.get());
assertEquals(1, fixture.generations.get());
runtime.close();
}
}
@Test
public void columnPredictionMatchesBoundaryGenerationAndFullOutputTakesPrecedence() {
try (Fixture fixture = new Fixture()) {
AtomicInteger fullCalls = new AtomicInteger();
AtomicInteger columnCalls = new AtomicInteger();
TerrainColumnTransformer operation = (context, x, z) -> {
NativeBlockState neighbor = context.original(context.blockX() + x + 1,
context.minY() + 2, context.blockZ() + z + 1);
context.set(x, 2, z, fixture.air);
context.set(x, 4, z, neighbor);
context.set(x, 5, z, fixture.water);
};
TerrainTransformRuntime runtime = fixture.columnRuntime(1, (context, x, z) -> {
columnCalls.incrementAndGet();
operation.transform(context, x, z);
}, context -> {
fullCalls.incrementAndGet();
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
operation.transform(context, x, z);
}
}
});
assertEquals(4, runtime.height(-1, 15, true));
assertEquals(5, runtime.height(-1, 15, false));
Terrain3DColumn predicted = runtime.column(-1, 15);
assertEquals(2, fixture.generations.get());
assertEquals(0, fullCalls.get());
Hunk<NativeBlockState> blocks = Hunk.newArrayHunk(16, 8, 16);
runtime.generate(-16, 0, blocks, Hunk.newArrayHunk(16, 8, 16), false, fixture.context(-16));
assertEquals(1, fullCalls.get());
assertEquals(1, columnCalls.get());
assertEquals(4, runtime.height(-1, 15, true));
assertEquals(5, runtime.fluidHeight(-1, 15));
assertEquals(predicted, runtime.column(-1, 15));
assertNotSame(predicted, runtime.column(-1, 15));
assertSame(runtime.column(-1, 15), runtime.readyColumn(-1, 15));
assertSame(fixture.stone, runtime.readyBlock(-1, 4, 15));
assertSame(fixture.water, blocks.getRaw(15, 5, 15));
runtime.close();
}
}
private static final class Fixture implements AutoCloseable {
private final Engine engine = mock(Engine.class);
private final IrisComplex complex = mock(IrisComplex.class);
private final EngineMode mode = mock(EngineMode.class);
private final NativeBlockState air = mock(NativeBlockState.class);
private final NativeBlockState stone = mock(NativeBlockState.class);
private final NativeBlockState water = mock(NativeBlockState.class);
private final NativeBiome biome = mock(NativeBiome.class);
private final AtomicInteger generations = new AtomicInteger();
private final MockedStatic<B> states = mockStatic(B.class);
private Fixture() {
states.when(() -> B.getState("AIR")).thenReturn(air);
when(air.isAir()).thenReturn(true);
when(water.isFluid()).thenReturn(true);
when(engine.getComplex()).thenReturn(complex);
when(engine.getMode()).thenReturn(mode);
when(engine.getHeight()).thenReturn(8);
when(engine.getMinHeight()).thenReturn(-4);
when(engine.getGenerationSessionId()).thenReturn(7L);
when(complex.getRawHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 2));
when(complex.getTrueBiomeStream()).thenReturn(mock(ProceduralStream.class));
when(complex.getRockStream()).thenReturn(mock(ProceduralStream.class));
when(complex.getFluidStream()).thenReturn(mock(ProceduralStream.class));
when(complex.getRegionStream()).thenReturn(mock(ProceduralStream.class));
when(complex.allowsMantleWrite(anyInt(), anyInt())).thenReturn(true);
doAnswer(invocation -> {
generations.incrementAndGet();
Hunk<NativeBlockState> blocks = invocation.getArgument(2);
Hunk<NativeBiome> biomes = invocation.getArgument(3);
ChunkContext context = invocation.getArgument(5);
assertTrue(context.isNaturalTerrain());
assertSame(context, IrisContext.require().getChunkContext());
assertEquals(7L, IrisContext.require().getGenerationSessionId());
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y <= 2; y++) {
blocks.setRaw(x, y, z, stone);
biomes.setRaw(x, y, z, biome);
}
}
}
context.floatingBiomes(8).record(1, 2, 1, new FloatingBiomeOverlay.Identity("island", "region"));
return null;
}).when(mode).generateTerrain(anyInt(), anyInt(), any(), any(), anyBoolean(), any());
}
private TerrainTransformRuntime runtime(int radius, Consumer<TerrainTransformContext> action) {
return new TerrainTransformRuntime(new TerrainTransformRuntime.Options(engine, complex), transformer(radius, action));
}
private TerrainTransformer transformer(int radius, Consumer<TerrainTransformContext> action) {
return new TerrainTransformer() {
@Override
public int radius() {
return radius;
}
@Override
public void transform(TerrainTransformContext context) {
action.accept(context);
}
};
}
private TerrainTransformRuntime columnRuntime(int radius, TerrainColumnTransformer column,
Consumer<TerrainTransformContext> full) {
TerrainTransformer transformer = new TerrainTransformer() {
@Override
public int radius() {
return radius;
}
@Override
public void transform(TerrainTransformContext context) {
full.accept(context);
}
@Override
public TerrainColumnTransformer columnTransformer() {
return column;
}
};
return new TerrainTransformRuntime(new TerrainTransformRuntime.Options(engine, complex), transformer);
}
private ChunkContext context(int x) {
return new ChunkContext(x, 0, complex, 7L, true, ChunkContext.PrefillPlan.NONE, null);
}
@Override
public void close() {
states.close();
}
}
}
@@ -78,7 +78,7 @@ public class SchemaBuilderIdentityTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private static final String EXPECTED_SCHEMA_DIGEST = "9c118c18c2f757f08ffb71d52d8549c53e4dae4939e0e030d36e4e9115794497";
private static final String EXPECTED_SCHEMA_DIGEST = "4d37a4cf8bb02c6e334545b549433a1b75ff5b8aae0bc91cd8b21cd8f62307c0";
private static final List<Class<?>> SCHEMA_ROOTS = List.of(
IrisDimension.class,
@@ -420,7 +420,7 @@ public class IrisStructureLocatorContractTest {
assertEquals(IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED, result.status());
assertFalse(result.found());
verify(engine, times(8_192)).getComplex();
verify(engine, times(16_384)).getComplex();
}
@Test