mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-11 19:14:11 +00:00
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:
+9
@@ -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",
|
||||
|
||||
+145
@@ -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;
|
||||
}
|
||||
}
|
||||
+70
@@ -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)) {
|
||||
generateTerrain(x, z, blocks, biomes, multicore, 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;
|
||||
}
|
||||
|
||||
+25
@@ -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<>();
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package art.arcane.iris.generation.terrain.transform;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface TerrainColumnTransformer {
|
||||
void transform(TerrainTransformContext context, int localX, int localZ);
|
||||
}
|
||||
+17
@@ -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);
|
||||
}
|
||||
+11
@@ -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);
|
||||
}
|
||||
+62
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+506
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -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++) {
|
||||
|
||||
+67
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
-6
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+341
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
-3
@@ -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});
|
||||
|
||||
+107
@@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
+338
@@ -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,
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user