Merge Iris generation changes

- Combine authored subterrain features with native CPU generation.
- Verify dimension JSON and schemas preserve subterrain configuration.
This commit is contained in:
Brian Neumann-Fopiano
2026-10-04 10:11:44 -04:00
76 changed files with 4248 additions and 201 deletions
@@ -25,6 +25,8 @@ import art.arcane.iris.localization.IrisLanguage;
import art.arcane.iris.pack.datapack.DatapackIngestService; import art.arcane.iris.pack.datapack.DatapackIngestService;
import art.arcane.iris.studio.object.ObjectStudioSaveService; import art.arcane.iris.studio.object.ObjectStudioSaveService;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.locator.SubterrainLocator;
import art.arcane.iris.generation.subterrain.IrisSubterrainFamily;
import art.arcane.iris.structure.placement.IrisStructureLocator; import art.arcane.iris.structure.placement.IrisStructureLocator;
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy; import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
import art.arcane.iris.structure.placement.StructureReachability; import art.arcane.iris.structure.placement.StructureReachability;
@@ -89,6 +91,78 @@ public class CommandFind implements DirectorExecutor {
EngineBukkitOps.gotoBiome(e, biome, player(), teleport); EngineBukkitOps.gotoBiome(e, biome, player(), teleport);
} }
@Director(description = "Find an authored underground feature by id or geometry family", sync = true)
public void subterrain(
@Param(description = "Feature definition id or family: tectonic_fault, cenote, lava_tube, travertine_terraces")
String feature,
@Param(description = "Search radius in blocks", defaultValue = "8192")
int radius,
@Param(description = "Should you be teleported", defaultValue = "true")
boolean teleport
) {
String key = feature.trim();
IrisSubterrainFamily family = null;
try {
family = IrisSubterrainFamily.valueOf(key.toUpperCase(Locale.ROOT).replace('-', '_'));
} catch (IllegalArgumentException ignored) {
}
locateSubterrain(new SubterrainLocator.Query(family == null ? key : "", family, ""), radius, teleport);
}
@Director(name = "underground-biome", description = "Find a bounded authored underground biome", sync = true)
public void undergroundBiome(
@Param(description = "The underground biome to look for", customHandler = ReachableBiomeHandler.class)
IrisBiome biome,
@Param(description = "Search radius in blocks", defaultValue = "8192")
int radius,
@Param(description = "Should you be teleported", defaultValue = "true")
boolean teleport
) {
locateSubterrain(new SubterrainLocator.Query("", null, biome.getLoadKey()), radius, teleport);
}
private void locateSubterrain(SubterrainLocator.Query query, int radius, boolean teleport) {
Engine activeEngine = engine();
VolmitSender commandSender = sender();
Player target = player();
if (activeEngine == null || commandSender == null || target == null) {
if (commandSender != null) {
commandSender.sendMessage(C.RED + "Run this command from an Iris world.");
}
return;
}
if (radius < 0 || radius > 32768) {
commandSender.sendMessage(C.RED + "Search radius must be between 0 and 32768 blocks.");
return;
}
Location origin = target.getLocation();
World world = origin.getWorld();
commandSender.sendMessage(C.GRAY + "Searching authored underground plans...");
J.a(() -> {
try {
SubterrainLocator.Result found = GenerationSemanticQueries.nearestSubterrain(activeEngine, query,
origin.getBlockX(), origin.getBlockY(), origin.getBlockZ(), radius).orElse(null);
if (found == null) {
sendStructureMessage(target, commandSender,
C.YELLOW + "No matching authored underground volume found within " + radius + " blocks.");
return;
}
String label = found.family().name().toLowerCase(Locale.ROOT) + " " + found.featureId();
if (!teleport) {
sendStructureMessage(target, commandSender, C.GREEN + "Found " + label + " at "
+ found.x() + ", " + found.y() + ", " + found.z() + ".");
return;
}
prepareStructureTeleport(target, world, commandSender, label,
new Location(world, found.x(), found.y(), found.z()), true);
} catch (Throwable error) {
sendStructureMessage(target, commandSender,
C.RED + "Could not locate authored underground volume: " + error.getMessage());
Iris.reportError("Could not locate authored underground volume.", error);
}
});
}
@Director(description = "Find a region", descriptionKey = "iris.director.commandfind.director.find_region") @Director(description = "Find a region", descriptionKey = "iris.director.commandfind.director.find_region")
public void region( public void region(
@Param(description = "The region to look for", descriptionKey = "iris.director.commandfind.param.region_look", customHandler = ReachableRegionHandler.class) @Param(description = "The region to look for", descriptionKey = "iris.director.commandfind.param.region_look", customHandler = ReachableRegionHandler.class)
@@ -325,7 +325,7 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
private H getNoiseBiome(Engine engine, int quartX, int quartY, int quartZ, private H getNoiseBiome(Engine engine, int quartX, int quartY, int quartZ,
S sampler) { S sampler) {
if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) { if (isGuaranteedSurfaceBiome(engine, quartY)) {
return getSurfaceStructureBiome(engine, quartX, quartZ, sampler); return getSurfaceStructureBiome(engine, quartX, quartZ, sampler);
} }
boolean cacheable = isBiomeCacheable( boolean cacheable = isBiomeCacheable(
@@ -512,7 +512,7 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
} }
boolean monumentQuery = isMonumentSurfaceBiomeQuery( boolean monumentQuery = isMonumentSurfaceBiomeQuery(
y, radius, engine.getMinHeight(), engine.getDimension().getFluidHeight()); y, radius, engine.getMinHeight(), engine.getDimension().getFluidHeight());
surfaceQuery = monumentQuery || isGuaranteedSurfaceBiome(minQuartY, engine.getMinHeight()); surfaceQuery = monumentQuery || isGuaranteedSurfaceBiome(engine, minQuartY);
} }
if (!surfaceQuery) { if (!surfaceQuery) {
return query.fallback().get(); return query.fallback().get();
@@ -701,6 +701,11 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
int blockZ = quartZ << 2; int blockZ = quartZ << 2;
int internalY = blockY - engine.getMinHeight(); int internalY = blockY - engine.getMinHeight();
int caveSwitchY = Math.max(-8 - engine.getMinHeight(), 40); int caveSwitchY = Math.max(-8 - engine.getMinHeight(), 40);
IrisBiome featureBiome = engine.getSubterrainBiome(blockX, internalY, blockZ);
if (featureBiome != null) {
return createBiomeResolution(engine, featureBiome, true, engine.getDimension(), engine.getData(),
blockX, blockY, blockZ);
}
DimensionStackLayout.Layer stackLayer = includeDimensionStack DimensionStackLayout.Layer stackLayer = includeDimensionStack
? stackLayout == null ? stackLayout == null
? resolveDimensionStackLayer(engine, blockX, internalY, blockZ) ? resolveDimensionStackLayer(engine, blockX, internalY, blockZ)
@@ -846,7 +851,13 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
return configuredBiomeKeys; return configuredBiomeKeys;
} }
static boolean isGuaranteedSurfaceBiome(int quartY, int minHeight) { static boolean isGuaranteedSurfaceBiome(Engine engine, int quartY) {
IrisDimension dimension = engine.getDimension();
if (dimension != null && dimension.allowsSubterrainFeatures()
&& !dimension.getSubterrainFeatures().isEmpty()) {
return false;
}
int minHeight = engine.getMinHeight();
int internalY = (quartY << 2) - minHeight; int internalY = (quartY << 2) - minHeight;
int caveSwitchY = Math.max(-8 - minHeight, 40); int caveSwitchY = Math.max(-8 - minHeight, 40);
return internalY > caveSwitchY; return internalY > caveSwitchY;
@@ -58,6 +58,7 @@ import art.arcane.volmlib.util.matter.MatterMarker;
import art.arcane.volmlib.util.matter.slices.MarkerMatter; import art.arcane.volmlib.util.matter.slices.MarkerMatter;
import java.util.Map; import java.util.Map;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.ArrayList; import java.util.ArrayList;
@@ -511,14 +512,18 @@ public final class ModdedWorldManager implements EngineWorldManager {
} }
IrisBiome biome = environment.biome(); IrisBiome biome = environment.biome();
Map<String, Integer> chunkMobs = initial ? Map.of() : NativeSpawnQueries.livingEntityCategories(level, chunkX, chunkZ); Map<String, Integer> chunkMobs = new HashMap<>(initial ? Map.of()
: NativeSpawnQueries.livingEntityCategories(level, chunkX, chunkZ));
RNG rng = initial ? EntitySpawnSeed.chunk(engine.getSeedManager().getEntity(), chunkX, chunkZ) RNG rng = initial ? EntitySpawnSeed.chunk(engine.getSeedManager().getEntity(), chunkX, chunkZ)
: new RNG(RNG.r.nextLong()); : new RNG(RNG.r.nextLong());
KList<SpawnCandidate> pool = new KList<>(); KList<SpawnCandidate> pool = new KList<>();
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.dimension().getEntitySpawners()), biome, chunkX, chunkZ, chunkMobs, initial); collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.dimension().getEntitySpawners()), biome, chunkX, chunkZ, chunkMobs, initial, false);
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.region().getEntitySpawners()), null, chunkX, chunkZ, chunkMobs, initial); collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.region().getEntitySpawners()), null, chunkX, chunkZ, chunkMobs, initial, false);
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(biome.getEntitySpawners()), null, chunkX, chunkZ, chunkMobs, initial); collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(biome.getEntitySpawners()), null, chunkX, chunkZ, chunkMobs, initial, false);
if (environment.dimension().hasUndergroundSpawners(engine)) {
spawnUnderground(level, chunkX, chunkZ, initial, chunkMobs, rng.nextParallelRNG(0x5A17D3B48269C0EFL));
}
if (pool.isEmpty()) { if (pool.isEmpty()) {
return; return;
} }
@@ -533,14 +538,111 @@ public final class ModdedWorldManager implements EngineWorldManager {
} }
int spawned = spawnEntry(level, chosen.entry(), chunkX, chunkZ, int spawned = spawnEntry(level, chosen.entry(), chunkX, chunkZ,
remainingCapacity(chosen.entry(), spawner, chunkMobs), rng, new SpawnContext(spawner, null, initial)); remainingCapacity(chosen.entry(), spawner, chunkMobs), rng, new SpawnContext(spawner, null, initial));
recordSpawned(chosen.entry(), chunkMobs, spawned);
if (spawned > 0 && !initial) { if (spawned > 0 && !initial) {
spawner.spawn(engine, chunkX, chunkZ); spawner.spawn(engine, chunkX, chunkZ);
} }
} }
private void collectSpawns(KList<SpawnCandidate> pool, KList<IrisSpawner> spawners, IrisBiome biomeFilter, int chunkX, int chunkZ, Map<String, Integer> chunkMobs, boolean initial) { private void spawnUnderground(NativeWorld level, int chunkX, int chunkZ, boolean initial,
Map<String, Integer> chunkMobs, RNG random) {
for (IrisSurface surface : List.of(IrisSurface.LAND, IrisSurface.WATER, IrisSurface.LAVA)) {
IrisPosition candidate = findUndergroundCandidate(level, chunkX, chunkZ,
random.nextParallelRNG(surface.ordinal()), surface);
if (candidate == null) {
continue;
}
BiomeEnvironment environment;
try {
environment = engine.getBiomeOrMantleEnvironment(candidate.getX(),
candidate.getY() - engine.getWorld().minHeight(), candidate.getZ());
} catch (SavedBiomeUnavailableException unavailable) {
continue;
}
try (BiomeEnvironment.Scope ignored = engine.openBiomeEnvironmentScope(environment)) {
KList<SpawnCandidate> pool = new KList<>();
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.dimension().getEntitySpawners()),
environment.biome(), chunkX, chunkZ, chunkMobs, initial, true);
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.region().getEntitySpawners()),
null, chunkX, chunkZ, chunkMobs, initial, true);
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.biome().getEntitySpawners()),
null, chunkX, chunkZ, chunkMobs, initial, true);
pool.removeIf(selection -> !matchesUndergroundSurface(selection.entry(), surface));
SpawnCandidate selected = rarityPick(pool, random);
if (selected == null) {
continue;
}
int capacity = remainingCapacity(selected.entry(), selected.spawner(), chunkMobs);
int spawned = spawnUndergroundEntry(level, candidate, selected, capacity, random, initial);
recordSpawned(selected.entry(), chunkMobs, spawned);
if (spawned > 0 && !initial) {
selected.spawner().spawn(engine, chunkX, chunkZ);
}
return;
}
}
}
private boolean matchesUndergroundSurface(IrisEntitySpawn entry, IrisSurface surface) {
IrisEntity entity = entry.getRealEntity(engine);
return entity != null && entity.getSurface().isFluid() == surface.isFluid()
&& (!surface.isFluid() || entity.getSurface() == surface);
}
static IrisPosition findUndergroundCandidate(NativeWorld level, int chunkX, int chunkZ, RNG random, IrisSurface surface) {
int minimumY = level.minHeight() + 1;
for (int attempt = 0; attempt < 8; attempt++) {
int x = (chunkX << 4) + random.i(1, 15);
int z = (chunkZ << 4) + random.i(1, 15);
int maximumY = Math.min(level.maxHeight() - 3, NativeSpawnQueries.surfaceHeight(level, x, z, true) - 1);
if (maximumY < minimumY) {
continue;
}
int span = maximumY - minimumY + 1;
int startY = LootResolver.inclusive(random, minimumY, maximumY);
for (int sample = 0; sample < Math.min(span, 128); sample++) {
int y = minimumY + Math.floorMod(startY - minimumY - sample, span);
NativeBlockState state = level.getBlock(x, y, z);
boolean acceptable = surface.isFluid() ? matchesSurface(surface, state)
: state != null && state.isAir() && NativeSpawnQueries.solid(level.getBlock(x, y - 1, z))
&& level.getBlock(x, y + 1, z).isAir();
if (acceptable) {
return new IrisPosition(x, y, z);
}
}
}
return null;
}
private int spawnUndergroundEntry(NativeWorld level, IrisPosition candidate, SpawnCandidate selection,
int capacity, RNG random, boolean initial) {
IrisEntity entity = selection.entry().getRealEntity(engine);
if (entity == null) {
return 0;
}
int x = candidate.getX();
int y = candidate.getY();
int z = candidate.getZ();
int count = Math.min(Math.max(0, capacity), LootResolver.inclusive(random,
selection.entry().getMinSpawns(), selection.entry().getMaxSpawns()));
int spawned = 0;
for (int ordinal = 0; ordinal < count; ordinal++) {
if (!initial && !lightAllowed(selection.spawner(), level, x, y, z)
|| !surfaceMatches(entity.getSurface(), level, x, y, z)
|| !ModdedEntitySpawner.isAreaClearForSpawn(entityRuntime(level), entity, x, y, z)) {
continue;
}
if (ModdedEntitySpawner.spawn(engine, entity, entityRuntime(level), x, y, z,
EntitySpawnSeed.entity(random.getSeed(), ordinal)) != null) {
spawned++;
}
}
return spawned;
}
private void collectSpawns(KList<SpawnCandidate> pool, KList<IrisSpawner> spawners, IrisBiome biomeFilter, int chunkX, int chunkZ, Map<String, Integer> chunkMobs, boolean initial, boolean underground) {
for (IrisSpawner spawner : spawners) { for (IrisSpawner spawner : spawners) {
if (spawner == null) { if (spawner == null || (spawner.getGroup() == IrisSpawnGroup.CAVE) != underground) {
continue; continue;
} }
if (biomeFilter != null && !spawner.isValid(biomeFilter)) { if (biomeFilter != null && !spawner.isValid(biomeFilter)) {
@@ -558,6 +660,13 @@ public final class ModdedWorldManager implements EngineWorldManager {
} }
} }
private void recordSpawned(IrisEntitySpawn entry, Map<String, Integer> counts, int spawned) {
IrisEntity entity = entry.getRealEntity(engine);
if (spawned > 0 && entity != null) {
counts.merge(entity.spawnCategory(), spawned, Integer::sum);
}
}
private int remainingCapacity(IrisEntitySpawn entry, IrisSpawner spawner, Map<String, Integer> counts) { private int remainingCapacity(IrisEntitySpawn entry, IrisSpawner spawner, Map<String, Integer> counts) {
IrisEntity entity = entry.getRealEntity(engine); IrisEntity entity = entry.getRealEntity(engine);
return spawner.remainingCapacity(entity, counts); return spawner.remainingCapacity(entity, counts);
@@ -38,12 +38,18 @@ public class IrisModdedStructureParityTest {
Bootstrap.bootStrap(); Bootstrap.bootStrap();
} }
private static Engine surfaceEngine(int minHeight) {
Engine engine = mock(Engine.class);
when(engine.getMinHeight()).thenReturn(minHeight);
return engine;
}
@Test @Test
public void surfaceBiomeFastPathBeginsAboveTheCaveSwitch() { public void surfaceBiomeFastPathBeginsAboveTheCaveSwitch() {
assertFalse(ModdedBiomePolicy.isGuaranteedSurfaceBiome(-2, -256)); assertFalse(ModdedBiomePolicy.isGuaranteedSurfaceBiome(surfaceEngine(-256), -2));
assertTrue(ModdedBiomePolicy.isGuaranteedSurfaceBiome(-1, -256)); assertTrue(ModdedBiomePolicy.isGuaranteedSurfaceBiome(surfaceEngine(-256), -1));
assertFalse(ModdedBiomePolicy.isGuaranteedSurfaceBiome(10, 0)); assertFalse(ModdedBiomePolicy.isGuaranteedSurfaceBiome(surfaceEngine(0), 10));
assertTrue(ModdedBiomePolicy.isGuaranteedSurfaceBiome(11, 0)); assertTrue(ModdedBiomePolicy.isGuaranteedSurfaceBiome(surfaceEngine(0), 11));
} }
@Test @Test
@@ -2,6 +2,11 @@ package art.arcane.iris.modded;
import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.DimensionStackLayout;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.subterrain.IrisSubterrainFeature;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.generation.runtime.GenerationSessionException; import art.arcane.iris.generation.runtime.GenerationSessionException;
import art.arcane.iris.generation.runtime.GenerationSessionLease; import art.arcane.iris.generation.runtime.GenerationSessionLease;
import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.runtime.IrisComplex;
@@ -11,20 +16,54 @@ import art.arcane.volmlib.util.stream.interpolation.Interpolated;
import org.junit.Test; import org.junit.Test;
import java.util.ArrayList; import java.util.ArrayList;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer; import java.util.function.Consumer;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame; import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
public class ModdedBiomePolicyTest { public class ModdedBiomePolicyTest {
@Test
public void authoredBiomeWinsAboveSurfaceGateAndBeforeStackOwnership() throws Exception {
Engine engine = mock(Engine.class);
IrisBiome feature = mock(IrisBiome.class);
IrisDimension host = new IrisDimension().setSubterrainFeatures(new KList<>(
new IrisSubterrainFeature().setId("room")));
IrisData data = mock(IrisData.class);
when(data.getDataFolder()).thenReturn(new File("synthetic-pack"));
when(engine.getData()).thenReturn(data);
when(engine.getDimension()).thenReturn(host);
when(engine.getComplex()).thenReturn(mock(IrisComplex.class));
when(engine.getMinHeight()).thenReturn(-64);
when(engine.getSubterrainBiome(12, 224, -20)).thenReturn(feature);
assertFalse(ModdedBiomePolicy.isGuaranteedSurfaceBiome(engine, 40));
ModdedBiomePolicy<BiomeToken, Object> policy = boundTo(engine);
DimensionStackLayout stack = mock(DimensionStackLayout.class);
Method method = ModdedBiomePolicy.class.getDeclaredMethod("resolveBiomeResolution", Engine.class,
int.class, int.class, int.class, boolean.class, DimensionStackLayout.class);
method.setAccessible(true);
Object resolution = method.invoke(policy, engine, 3, 40, -5, true, stack);
Field biome = resolution.getClass().getDeclaredField("irisBiome");
biome.setAccessible(true);
assertSame(feature, biome.get(resolution));
Field dimension = resolution.getClass().getDeclaredField("dimension");
dimension.setAccessible(true);
assertSame(host, dimension.get(resolution));
verifyNoInteractions(stack);
}
@Test @Test
public void registryOrderAndHolderIdentitySurviveThePolicyBoundaryAndRefreshOnRepoint() { public void registryOrderAndHolderIdentitySurviveThePolicyBoundaryAndRefreshOnRepoint() {
BiomeToken ocean = new BiomeToken("minecraft:ocean"); BiomeToken ocean = new BiomeToken("minecraft:ocean");
@@ -18,6 +18,7 @@ import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.world.entity.IrisEntity; import art.arcane.iris.world.entity.IrisEntity;
import art.arcane.iris.world.entity.IrisEntitySpawn; import art.arcane.iris.world.entity.IrisEntitySpawn;
import art.arcane.iris.world.entity.IrisSpawner; import art.arcane.iris.world.entity.IrisSpawner;
import art.arcane.iris.world.entity.IrisSpawnGroup;
import art.arcane.iris.world.entity.IrisMarker; import art.arcane.iris.world.entity.IrisMarker;
import art.arcane.iris.world.entity.EntitySpawnSeed; import art.arcane.iris.world.entity.EntitySpawnSeed;
import art.arcane.iris.structure.placement.LootResolver; import art.arcane.iris.structure.placement.LootResolver;
@@ -97,6 +98,33 @@ public class ModdedInitialEntitySpawnTest {
} }
} }
@Test
public void undergroundAndSurfacePassesShareTheSuccessfulPopulationBudget() {
try (Fixture fixture = new Fixture()) {
BiomeEnvironment environment = fixture.engine.getSurfaceBiomeEnvironment(0, 0);
when(environment.dimension().hasUndergroundSpawners(fixture.engine)).thenReturn(true);
when(fixture.engine.getBiomeOrMantleEnvironment(anyInt(), anyInt(), anyInt())).thenReturn(environment);
NativeBlockState stone = mock(NativeBlockState.class);
NativeBlockState air = mock(NativeBlockState.class);
when(air.isAir()).thenReturn(true);
when(fixture.world.getBlock(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
int y = invocation.getArgument(1);
return y <= -21 || y == 50 ? stone : air;
});
fixture.queries.when(() -> NativeSpawnQueries.solid(stone)).thenReturn(true);
IrisEntity entity = new IrisEntity().setType("minecraft:cow");
IrisEntitySpawn entry = spy(new IrisEntitySpawn().setMinSpawns(2).setMaxSpawns(2));
doReturn(entity).when(entry).getRealEntity(fixture.engine);
fixture.definitions.add(new IrisSpawner().setGroup(IrisSpawnGroup.CAVE)
.setInitialSpawns(new KList<>(entry)).setMaxEntitiesPerChunk(3));
assertTrue(fixture.manager.initialSpawnChunk(fixture.world, -2, 3));
assertEquals(3, fixture.spawned.size());
assertEquals(2, fixture.spawned.stream().filter(spawn -> spawn.y() == -20).count());
assertEquals(1, fixture.spawned.stream().filter(spawn -> spawn.y() == 51).count());
fixture.queries.verify(() -> NativeSpawnQueries.livingEntityCategories(eq(fixture.world), anyInt(), anyInt()), never());
}
}
@Test @Test
public void initialCompletionFollowsOwnedSpawnPassAndDoesNotRepeatIt() { public void initialCompletionFollowsOwnedSpawnPassAndDoesNotRepeatIt() {
try (Fixture fixture = new Fixture()) { try (Fixture fixture = new Fixture()) {
@@ -0,0 +1,114 @@
package art.arcane.iris.modded;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.decoration.IrisSurface;
import art.arcane.iris.generation.runtime.BiomeEnvironment;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.loading.ResourceLoader;
import art.arcane.iris.pack.value.IrisPosition;
import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.world.entity.IrisEntity;
import art.arcane.iris.world.entity.IrisEntitySpawn;
import art.arcane.iris.world.entity.IrisSpawnGroup;
import art.arcane.iris.world.entity.IrisSpawner;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeEntityRuntime;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeSpawnQueries;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
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 ModdedUndergroundSpawnOwnershipTest {
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void candidateSamplingAndPoolSelectionUseAbsoluteUndergroundOwner() throws Exception {
NativeWorld world = mock(NativeWorld.class);
when(world.minHeight()).thenReturn(-64);
when(world.maxHeight()).thenReturn(320);
NativeBlockState stone = mock(NativeBlockState.class);
NativeBlockState air = mock(NativeBlockState.class);
when(air.isAir()).thenReturn(true);
when(world.getBlock(anyInt(), anyInt(), anyInt())).thenAnswer(call ->
(int) call.getArgument(1) <= -21 ? stone : air);
Engine engine = mock(Engine.class);
when(engine.getWorld()).thenReturn(IrisWorld.builder().minHeight(-64).maxHeight(320).build());
IrisBiome cave = new IrisBiome().setEntitySpawners(new KList<>("residents"));
IrisDimension dimension = new IrisDimension();
IrisRegion region = new IrisRegion();
IrisData data = mock(IrisData.class);
@SuppressWarnings("unchecked")
ResourceLoader<IrisSpawner> loader = mock(ResourceLoader.class);
when(data.getSpawnerLoader()).thenReturn(loader);
when(loader.loadAll(dimension.getEntitySpawners())).thenReturn(new KList<>());
when(loader.loadAll(region.getEntitySpawners())).thenReturn(new KList<>());
IrisEntitySpawn entry = mock(IrisEntitySpawn.class);
IrisEntity entity = mock(IrisEntity.class);
when(entry.getRealEntity(engine)).thenReturn(entity);
when(entry.getRarity()).thenReturn(1);
when(entry.getMinSpawns()).thenReturn(1);
when(entry.getMaxSpawns()).thenReturn(1);
when(entity.getSurface()).thenReturn(IrisSurface.LAND);
when(entity.spawnCategory()).thenReturn("creature");
IrisSpawner spawner = new IrisSpawner().setGroup(IrisSpawnGroup.CAVE).setInitialSpawns(new KList<>(entry));
when(loader.loadAll(cave.getEntitySpawners())).thenReturn(new KList<>(spawner));
BiomeEnvironment environment = new BiomeEnvironment(9L, cave, region, dimension, data);
when(engine.getBiomeOrMantleEnvironment(anyInt(), eq(44), anyInt())).thenReturn(environment);
BiomeEnvironment.Scope scope = mock(BiomeEnvironment.Scope.class);
when(engine.openBiomeEnvironmentScope(environment)).thenReturn(scope);
NativeEntityRuntime runtime = mock(NativeEntityRuntime.class);
when(runtime.world()).thenReturn(world);
ModdedWorldManager manager = new ModdedWorldManager(engine);
try (MockedStatic<NativeSpawnQueries> queries = mockStatic(NativeSpawnQueries.class);
MockedStatic<ModdedEntitySpawner> entities = mockStatic(ModdedEntitySpawner.class)) {
queries.when(() -> NativeSpawnQueries.surfaceHeight(eq(world), anyInt(), anyInt(), eq(true))).thenReturn(64);
queries.when(() -> NativeSpawnQueries.solid(stone)).thenReturn(true);
IrisPosition first = ModdedWorldManager.findUndergroundCandidate(world, 2, -1, new RNG(88771L), IrisSurface.LAND);
IrisPosition second = ModdedWorldManager.findUndergroundCandidate(world, 2, -1, new RNG(88771L), IrisSurface.LAND);
assertNotNull(first);
assertEquals(first, second);
assertEquals(-20, first.getY());
assertTrue(first.getX() >= 33 && first.getX() <= 47 && first.getZ() >= -15 && first.getZ() <= -1);
Field field = ModdedWorldManager.class.getDeclaredField("entityRuntime");
field.setAccessible(true);
field.set(manager, runtime);
entities.when(() -> ModdedEntitySpawner.isAreaClearForSpawn(eq(runtime), eq(entity), anyInt(), eq(-20), anyInt()))
.thenReturn(true);
Method spawn = ModdedWorldManager.class.getDeclaredMethod("spawnUnderground", NativeWorld.class,
int.class, int.class, boolean.class, Map.class, RNG.class);
spawn.setAccessible(true);
spawn.invoke(manager, world, 2, -1, true, Map.of(), new RNG(88771L));
entities.verify(() -> ModdedEntitySpawner.spawn(eq(engine), eq(entity), eq(runtime), anyInt(), eq(-20), anyInt(), any(RNG.class)));
} finally {
manager.close();
}
verify(scope).close();
}
}
@@ -163,7 +163,7 @@ final class DecoratorCore {
bd.withProperty("half", bottomHalfValue(half)), bd.withProperty("half", bottomHalfValue(half)),
data, x, z, realX, lowerY, realZ, mantle); data, x, z, realX, lowerY, realZ, mantle);
data.set(x, lowerY, z, lower); data.set(x, lowerY, z, lower);
data.set(x, upperY, z, upper); data.set(x, upperY, z, IrisProceduralBlocks.normalizeWaterlogging(upper, data.get(x, upperY, z), true));
} catch (Throwable e) { } catch (Throwable e) {
GenerationFailures.rethrowEngineFailure(e); GenerationFailures.rethrowEngineFailure(e);
IrisLogging.reportError(e); IrisLogging.reportError(e);
@@ -210,7 +210,7 @@ final class DecoratorCore {
if (applyFixFaces) { if (applyFixFaces) {
bd = fixFacesForHunk(bd, data, x, z, realX, height, realZ, mantle); bd = fixFacesForHunk(bd, data, x, z, realX, height, realZ, mantle);
} }
data.set(x, height, z, bd); data.set(x, height, z, IrisProceduralBlocks.normalizeWaterlogging(bd, data.get(x, height, z), true));
} }
static void placeStackUp(IrisDecorator decorator, int x, int z, int realX, int realZ, static void placeStackUp(IrisDecorator decorator, int x, int z, int realX, int realZ,
@@ -258,7 +258,7 @@ final class DecoratorCore {
} else if (i == 0 && !opts.underwater && !canGoOn(block, support)) { } else if (i == 0 && !opts.underwater && !canGoOn(block, support)) {
break; break;
} }
data.set(x, y, z, stackedVineBlock(block, stack, i)); data.set(x, y, z, IrisProceduralBlocks.normalizeWaterlogging(stackedVineBlock(block, stack, i), existing, true));
placed++; placed++;
} }
if (placed > 0 && placed < stack) { if (placed > 0 && placed < stack) {
@@ -356,8 +356,8 @@ final class DecoratorCore {
try { try {
NativeBlockState upper = bd.withProperty("half", topHalfValue(half)); NativeBlockState upper = bd.withProperty("half", topHalfValue(half));
NativeBlockState lower = bd.withProperty("half", bottomHalfValue(half)); NativeBlockState lower = bd.withProperty("half", bottomHalfValue(half));
data.set(xf, lowerY, zf, lower); data.set(xf, lowerY, zf, IrisProceduralBlocks.normalizeWaterlogging(lower, data.get(xf, lowerY, zf), true));
data.set(xf, upperY, zf, upper); data.set(xf, upperY, zf, IrisProceduralBlocks.normalizeWaterlogging(upper, data.get(xf, upperY, zf), true));
} catch (Throwable e) { } catch (Throwable e) {
GenerationFailures.rethrowEngineFailure(e); GenerationFailures.rethrowEngineFailure(e);
IrisLogging.reportError(e); IrisLogging.reportError(e);
@@ -366,7 +366,7 @@ final class DecoratorCore {
} }
if (max > 1 && height + 1 < data.getHeight()) { if (max > 1 && height + 1 < data.getHeight()) {
data.set(xf, height + 1, zf, bd); data.set(xf, height + 1, zf, IrisProceduralBlocks.normalizeWaterlogging(bd, data.get(xf, height + 1, zf), true));
} }
} }
@@ -411,7 +411,7 @@ final class DecoratorCore {
hasSpikes = true; hasSpikes = true;
} }
bd = stackedVineBlock(bd, stack, i); bd = stackedVineBlock(bd, stack, i);
data.set(xf, h, zf, bd); data.set(xf, h, zf, IrisProceduralBlocks.normalizeWaterlogging(bd, data.get(xf, h, zf), true));
placed++; placed++;
} }
if (placed > 0 && placed < stack) { if (placed > 0 && placed < stack) {
@@ -433,6 +433,7 @@ final class DecoratorCore {
static NativeBlockState fixFacesForHunk(NativeBlockState b, Hunk<NativeBlockState> hunk, int rX, int rZ, static NativeBlockState fixFacesForHunk(NativeBlockState b, Hunk<NativeBlockState> hunk, int rX, int rZ,
int x, int y, int z, EngineMantle mantle) { int x, int y, int z, EngineMantle mantle) {
b = IrisProceduralBlocks.normalizeWaterlogging(b, hunk.get(rX, y, rZ), true);
if (!B.isVineBlock(b)) { if (!B.isVineBlock(b)) {
return b; return b;
} }
@@ -539,7 +540,8 @@ final class DecoratorCore {
if (!IrisSpeleothems.canPlace(spike, data, x, z, y, upward, allowWater)) { if (!IrisSpeleothems.canPlace(spike, data, x, z, y, upward, allowWater)) {
return; return;
} }
data.set(x, y, z, IrisSpeleothems.orient(spike, data.get(x, y, z), upward)); NativeBlockState existing = data.get(x, y, z);
data.set(x, y, z, IrisProceduralBlocks.normalizeWaterlogging(IrisSpeleothems.orient(spike, existing, upward), existing, true));
IrisSpeleothems.finishColumn(data, x, z, y, 1, upward); IrisSpeleothems.finishColumn(data, x, z, y, 1, upward);
} }
@@ -123,6 +123,9 @@ public final class IrisProceduralBlocks {
* rather than building the search needles, and prefers a "[" match over a "," match. * rather than building the search needles, and prefers a "[" match over a "," match.
*/ */
private static int propertyStart(String key, String property) { private static int propertyStart(String key, String property) {
if (key == null) {
return -1;
}
int bracket = key.indexOf('['); int bracket = key.indexOf('[');
if (bracket < 0) { if (bracket < 0) {
return -1; return -1;
@@ -142,6 +145,16 @@ public final class IrisProceduralBlocks {
return -1; return -1;
} }
public static NativeBlockState normalizeWaterlogging(NativeBlockState state, NativeBlockState existing, boolean enabled) {
if (state == null || !hasProperty(state, "waterlogged")) {
return state;
}
boolean authored = "true".equals(propertyValue(state, "waterlogged"));
boolean submerged = existing != null && (existing.isWater() || "true".equals(propertyValue(existing, "waterlogged")));
String value = submerged && (enabled || authored) ? "true" : "false";
return value.equals(propertyValue(state, "waterlogged")) ? state : state.withProperty("waterlogged", value);
}
public static IrisObject assemble(Map<Vector3i, NativeBlockState> blocks) { public static IrisObject assemble(Map<Vector3i, NativeBlockState> blocks) {
if (blocks == null || blocks.isEmpty()) { if (blocks == null || blocks.isEmpty()) {
return null; return null;
@@ -19,6 +19,7 @@
package art.arcane.iris.generation.decoration; package art.arcane.iris.generation.decoration;
import art.arcane.iris.structure.object.IrisObject; import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.generation.subterrain.SubterrainRoom;
import art.arcane.iris.structure.object.IrisObjectPlacement; import art.arcane.iris.structure.object.IrisObjectPlacement;
import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.pack.loading.IrisData;
@@ -43,7 +44,7 @@ public interface IrisProceduralPlacement {
IrisObjectPlacement asPlacement(); IrisObjectPlacement asPlacement();
IrisObject getVariantObject(IrisData data, RNG rng); IrisObject getVariantObject(IrisData data, RNG rng, SubterrainRoom room);
KList<IrisObject> getVariantObjects(IrisData data); KList<IrisObject> getVariantObjects(IrisData data);
} }
@@ -81,10 +81,7 @@ public final class IrisSpeleothems {
} }
static NativeBlockState orient(NativeBlockState state, NativeBlockState existing, boolean upward) { static NativeBlockState orient(NativeBlockState state, NativeBlockState existing, boolean upward) {
if (existing != null && existing.isWater() && !state.isWaterLogged()) { return spikeBlock(IrisProceduralBlocks.normalizeWaterlogging(state, existing, true), upward, 0);
state = state.withProperty("waterlogged", "true");
}
return spikeBlock(state, upward, 0);
} }
static boolean isSturdy(NativeBlockState surface, boolean upward) { static boolean isSturdy(NativeBlockState surface, boolean upward) {
@@ -20,6 +20,7 @@ package art.arcane.iris.generation.decoration.coral;
import art.arcane.iris.generation.cave.CarvingMode; import art.arcane.iris.generation.cave.CarvingMode;
import art.arcane.iris.generation.decoration.IrisProceduralPlacement; import art.arcane.iris.generation.decoration.IrisProceduralPlacement;
import art.arcane.iris.generation.subterrain.SubterrainRoom;
import art.arcane.iris.generation.decoration.IrisStiltSettings; import art.arcane.iris.generation.decoration.IrisStiltSettings;
import art.arcane.iris.generation.decoration.IrisVacuumSettings; import art.arcane.iris.generation.decoration.IrisVacuumSettings;
import art.arcane.iris.generation.terrain.IrisMaterialPalette; import art.arcane.iris.generation.terrain.IrisMaterialPalette;
@@ -220,7 +221,7 @@ public class IrisCoral implements IrisProceduralPlacement {
}); });
} }
public IrisObject getVariantObject(IrisData data, RNG rng) { public IrisObject getVariantObject(IrisData data, RNG rng, SubterrainRoom room) {
KList<IrisObject> baked = getVariantObjects(data); KList<IrisObject> baked = getVariantObjects(data);
if (baked == null || baked.isEmpty()) { if (baked == null || baked.isEmpty()) {
return null; return null;
@@ -20,6 +20,7 @@ package art.arcane.iris.generation.decoration.crystal;
import art.arcane.iris.generation.cave.CarvingMode; import art.arcane.iris.generation.cave.CarvingMode;
import art.arcane.iris.generation.decoration.IrisProceduralPlacement; import art.arcane.iris.generation.decoration.IrisProceduralPlacement;
import art.arcane.iris.generation.subterrain.SubterrainRoom;
import art.arcane.iris.generation.decoration.IrisStiltSettings; import art.arcane.iris.generation.decoration.IrisStiltSettings;
import art.arcane.iris.generation.decoration.IrisVacuumSettings; import art.arcane.iris.generation.decoration.IrisVacuumSettings;
import art.arcane.iris.generation.terrain.IrisMaterialPalette; import art.arcane.iris.generation.terrain.IrisMaterialPalette;
@@ -201,7 +202,7 @@ public class IrisCrystal implements IrisProceduralPlacement {
}); });
} }
public IrisObject getVariantObject(IrisData data, RNG rng) { public IrisObject getVariantObject(IrisData data, RNG rng, SubterrainRoom room) {
KList<IrisObject> baked = getVariantObjects(data); KList<IrisObject> baked = getVariantObjects(data);
if (baked == null || baked.isEmpty()) { if (baked == null || baked.isEmpty()) {
return null; return null;
@@ -37,6 +37,10 @@ public final class FormationGenerator {
int hi = Math.max(f.getHeightMin(), f.getHeightMax()); int hi = Math.max(f.getHeightMin(), f.getHeightMax());
int height = Math.max(3, rng.i(lo, hi + 1)); int height = Math.max(3, rng.i(lo, hi + 1));
return generate(f, variantIndex, rng, data, height);
}
public static IrisObject generate(IrisFormation f, int variantIndex, RNG rng, IrisData data, int height) {
int wLo = Math.min(f.getBaseWidthMin(), f.getBaseWidthMax()); int wLo = Math.min(f.getBaseWidthMin(), f.getBaseWidthMax());
int wHi = Math.max(f.getBaseWidthMin(), f.getBaseWidthMax()); int wHi = Math.max(f.getBaseWidthMin(), f.getBaseWidthMax());
double baseRadius = Math.max(1, rng.i(wLo, wHi + 1)); double baseRadius = Math.max(1, rng.i(wLo, wHi + 1));
@@ -19,6 +19,7 @@
package art.arcane.iris.generation.decoration.formation; package art.arcane.iris.generation.decoration.formation;
import art.arcane.iris.generation.cave.CarvingMode; import art.arcane.iris.generation.cave.CarvingMode;
import art.arcane.iris.generation.subterrain.SubterrainRoom;
import art.arcane.iris.generation.decoration.IrisProceduralPlacement; import art.arcane.iris.generation.decoration.IrisProceduralPlacement;
import art.arcane.iris.generation.decoration.IrisStiltSettings; import art.arcane.iris.generation.decoration.IrisStiltSettings;
import art.arcane.iris.generation.decoration.IrisVacuumSettings; import art.arcane.iris.generation.decoration.IrisVacuumSettings;
@@ -255,6 +256,26 @@ public class IrisFormation implements IrisProceduralPlacement {
@Description("How many blocks the tip of an OVERHANG curls downward.") @Description("How many blocks the tip of an OVERHANG curls downward.")
private int overhangDrop = 3; private int overhangDrop = 3;
@MinNumber(0)
@MaxNumber(1)
@Description("For cave placements in an authored subterrain room, size the formation to this fraction of the room vault. Zero uses heightMin and heightMax.")
private double roomHeightFraction = 0;
@Override
public IrisObject getVariantObject(IrisData data, RNG rng, SubterrainRoom room) {
if (room == null || roomHeightFraction <= 0) {
KList<IrisObject> baked = getVariantObjects(data);
return baked == null || baked.isEmpty() ? null : baked.get(rng.i(baked.size()));
}
IrisObject object = FormationGenerator.generate(this, rng.i(Math.max(1, variants)), rng, data,
Math.max(1, (int) Math.floor(room.vaultHeight() * Math.min(1D, roomHeightFraction))));
if (object != null) {
object.setLoadKey("procedural/" + name + "/" + room.featureId());
object.setLoader(data);
}
return object;
}
public KList<IrisObject> getVariantObjects(IrisData data) { public KList<IrisObject> getVariantObjects(IrisData data) {
return variantCache.aquire(() -> { return variantCache.aquire(() -> {
KList<IrisObject> baked = new KList<>(); KList<IrisObject> baked = new KList<>();
@@ -274,14 +295,6 @@ public class IrisFormation implements IrisProceduralPlacement {
}); });
} }
public IrisObject getVariantObject(IrisData data, RNG rng) {
KList<IrisObject> baked = getVariantObjects(data);
if (baked == null || baked.isEmpty()) {
return null;
}
return baked.get(rng.i(baked.size()));
}
public IrisObjectPlacement asPlacement() { public IrisObjectPlacement asPlacement() {
IrisObjectPlacement placement = new IrisObjectPlacement(); IrisObjectPlacement placement = new IrisObjectPlacement();
placement.setMode(mode); placement.setMode(mode);
@@ -20,6 +20,7 @@ package art.arcane.iris.generation.decoration.fungus;
import art.arcane.iris.generation.cave.CarvingMode; import art.arcane.iris.generation.cave.CarvingMode;
import art.arcane.iris.generation.decoration.IrisProceduralPlacement; import art.arcane.iris.generation.decoration.IrisProceduralPlacement;
import art.arcane.iris.generation.subterrain.SubterrainRoom;
import art.arcane.iris.generation.decoration.IrisStiltSettings; import art.arcane.iris.generation.decoration.IrisStiltSettings;
import art.arcane.iris.generation.decoration.IrisVacuumSettings; import art.arcane.iris.generation.decoration.IrisVacuumSettings;
import art.arcane.iris.generation.terrain.IrisMaterialPalette; import art.arcane.iris.generation.terrain.IrisMaterialPalette;
@@ -233,7 +234,7 @@ public class IrisFungus implements IrisProceduralPlacement {
}); });
} }
public IrisObject getVariantObject(IrisData data, RNG rng) { public IrisObject getVariantObject(IrisData data, RNG rng, SubterrainRoom room) {
KList<IrisObject> baked = getVariantObjects(data); KList<IrisObject> baked = getVariantObjects(data);
if (baked == null || baked.isEmpty()) { if (baked == null || baked.isEmpty()) {
return null; return null;
@@ -20,6 +20,7 @@ package art.arcane.iris.generation.decoration.ruin;
import art.arcane.iris.generation.cave.CarvingMode; import art.arcane.iris.generation.cave.CarvingMode;
import art.arcane.iris.generation.decoration.IrisProceduralPlacement; import art.arcane.iris.generation.decoration.IrisProceduralPlacement;
import art.arcane.iris.generation.subterrain.SubterrainRoom;
import art.arcane.iris.generation.decoration.IrisStiltSettings; import art.arcane.iris.generation.decoration.IrisStiltSettings;
import art.arcane.iris.generation.decoration.IrisVacuumSettings; import art.arcane.iris.generation.decoration.IrisVacuumSettings;
import art.arcane.iris.generation.terrain.IrisMaterialPalette; import art.arcane.iris.generation.terrain.IrisMaterialPalette;
@@ -189,7 +190,7 @@ public class IrisRuin implements IrisProceduralPlacement {
}); });
} }
public IrisObject getVariantObject(IrisData data, RNG rng) { public IrisObject getVariantObject(IrisData data, RNG rng, SubterrainRoom room) {
KList<IrisObject> baked = getVariantObjects(data); KList<IrisObject> baked = getVariantObjects(data);
if (baked == null || baked.isEmpty()) { if (baked == null || baked.isEmpty()) {
return null; return null;
@@ -20,6 +20,7 @@ package art.arcane.iris.generation.decoration.tree;
import art.arcane.iris.generation.cave.CarvingMode; import art.arcane.iris.generation.cave.CarvingMode;
import art.arcane.iris.generation.decoration.IrisProceduralPlacement; import art.arcane.iris.generation.decoration.IrisProceduralPlacement;
import art.arcane.iris.generation.subterrain.SubterrainRoom;
import art.arcane.iris.generation.decoration.IrisStiltSettings; import art.arcane.iris.generation.decoration.IrisStiltSettings;
import art.arcane.iris.generation.decoration.IrisVacuumSettings; import art.arcane.iris.generation.decoration.IrisVacuumSettings;
import art.arcane.iris.generation.terrain.IrisMaterialPalette; import art.arcane.iris.generation.terrain.IrisMaterialPalette;
@@ -311,7 +312,7 @@ public class IrisProceduralTree implements IrisProceduralPlacement {
}); });
} }
public IrisObject getVariantObject(IrisData data, RNG rng) { public IrisObject getVariantObject(IrisData data, RNG rng, SubterrainRoom room) {
KList<IrisObject> baked = getVariantObjects(data); KList<IrisObject> baked = getVariantObjects(data);
if (baked == null || baked.isEmpty()) { if (baked == null || baked.isEmpty()) {
return null; return null;
@@ -0,0 +1,125 @@
package art.arcane.iris.generation.locator;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.subterrain.IrisSubterrainFamily;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainPlan;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.generation.subterrain.SubterrainPosition;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
public final class SubterrainLocator {
private static final int SEARCH_TILE_SIZE = 512;
private SubterrainLocator() {
}
public static Optional<Result> nearest(Engine engine, Query query, int x, int worldY, int z, int maximumDistance) {
return nearest(engine, query, x, worldY, z, maximumDistance, ignored -> true);
}
public static Optional<Result> nearest(Engine engine, Query query, int x, int worldY, int z,
int maximumDistance, Predicate<Result> allowed) {
Objects.requireNonNull(engine, "engine");
return nearest(engine.getComplex().getSubterrainPlanner(), query, x, worldY, z, maximumDistance, allowed);
}
public static Optional<Result> nearest(SubterrainPlanner planner, Query query, int x, int worldY, int z,
int maximumDistance, Predicate<Result> allowed) {
Objects.requireNonNull(planner, "planner");
Objects.requireNonNull(query, "query");
Objects.requireNonNull(allowed, "allowed");
if (maximumDistance < 0 || maximumDistance > 1_048_576) {
throw new IllegalArgumentException("Subterrain search distance must be between 0 and 1048576 blocks");
}
if (planner.isEmpty()) {
return Optional.empty();
}
long centerTileX = Math.floorDiv(x, SEARCH_TILE_SIZE);
long centerTileZ = Math.floorDiv(z, SEARCH_TILE_SIZE);
int tileRadius = maximumDistance / SEARCH_TILE_SIZE + 1;
Set<String> visited = new HashSet<>();
Result nearest = null;
double nearestDistance = (double) maximumDistance * maximumDistance;
int inspectedTiles = 0;
for (int ring = 0; ring <= tileRadius; ring++) {
for (int offsetX = -ring; offsetX <= ring; offsetX++) {
int offsetZStep = Math.abs(offsetX) == ring ? 1 : Math.max(1, ring * 2);
for (int offsetZ = -ring; offsetZ <= ring; offsetZ += offsetZStep) {
long tileX = (centerTileX + offsetX) * SEARCH_TILE_SIZE;
long tileZ = (centerTileZ + offsetZ) * SEARCH_TILE_SIZE;
long tileMaxX = tileX + SEARCH_TILE_SIZE - 1L;
long tileMaxZ = tileZ + SEARCH_TILE_SIZE - 1L;
if (tileMaxX < Integer.MIN_VALUE || tileX > Integer.MAX_VALUE
|| tileMaxZ < Integer.MIN_VALUE || tileZ > Integer.MAX_VALUE) {
continue;
}
double deltaTileX = x < tileX ? tileX - x : x > tileMaxX ? (long) x - tileMaxX : 0;
double deltaTileZ = z < tileZ ? tileZ - z : z > tileMaxZ ? (long) z - tileMaxZ : 0;
if (deltaTileX * deltaTileX + deltaTileZ * deltaTileZ > nearestDistance) {
continue;
}
if (++inspectedTiles > 65_536) {
throw new IllegalStateException("Subterrain search exceeded 65536 analytic tiles; use a smaller search radius");
}
for (SubterrainPlan plan : planner.plansForBounds(
(int) Math.max(Integer.MIN_VALUE, tileX), (int) Math.max(Integer.MIN_VALUE, tileZ),
(int) Math.min(Integer.MAX_VALUE, tileMaxX), (int) Math.min(Integer.MAX_VALUE, tileMaxZ))) {
if (!visited.add(plan.id()) || !query.matches(plan)) {
continue;
}
SubterrainPosition anchor = plan.anchor();
double deltaX = (double) anchor.x() - x;
double deltaY = (double) anchor.y() - worldY;
double deltaZ = (double) anchor.z() - z;
double distance = deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ;
if (distance > nearestDistance) {
continue;
}
SubterrainCell owner = planner.sample(anchor.x(), anchor.y(), anchor.z());
if (!owner.occupied() || !owner.room().featureId().equals(plan.id())) {
continue;
}
Result candidate = new Result(plan.id(), plan.family(), plan.biome(), anchor.x(), anchor.y(), anchor.z());
if (!allowed.test(candidate) || nearest != null && distance == nearestDistance
&& candidate.featureId().compareTo(nearest.featureId()) >= 0) {
continue;
}
nearest = candidate;
nearestDistance = distance;
}
}
}
long leftDistance = (long) x - (centerTileX - ring) * SEARCH_TILE_SIZE + 1;
long rightDistance = (centerTileX + ring + 1) * SEARCH_TILE_SIZE - x;
long frontDistance = (long) z - (centerTileZ - ring) * SEARCH_TILE_SIZE + 1;
long backDistance = (centerTileZ + ring + 1) * SEARCH_TILE_SIZE - z;
long remainingDistance = Math.min(Math.min(leftDistance, rightDistance), Math.min(frontDistance, backDistance));
if ((double) remainingDistance * remainingDistance > nearestDistance) {
break;
}
}
return Optional.ofNullable(nearest);
}
public record Query(String featureId, IrisSubterrainFamily family, String biome) {
public Query {
featureId = Objects.requireNonNullElse(featureId, "").trim();
biome = Objects.requireNonNullElse(biome, "").trim();
}
public boolean matches(SubterrainPlan plan) {
return (featureId.isEmpty() || plan.id().equals(featureId) || plan.id().startsWith(featureId + ":"))
&& (family == null || plan.family() == family)
&& (biome.isEmpty() || plan.biome().equals(biome));
}
}
public record Result(String featureId, IrisSubterrainFamily family, String biome, int x, int y, int z) {
}
}
@@ -19,6 +19,8 @@
package art.arcane.iris.generation.mantle; package art.arcane.iris.generation.mantle;
import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
import art.arcane.iris.generation.cache.Cache; import art.arcane.iris.generation.cache.Cache;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.runtime.IrisComplex;
@@ -79,6 +81,11 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
discard(); discard();
return CommitResult.REJECTED_BOUNDS; return CommitResult.REJECTED_BOUNDS;
} }
SubterrainCell feature = engine.getSubterrainCell(mutation.x(), mutation.y(), mutation.z());
if (SubterrainRasterizer.protectsPlacement(feature)) {
discard();
return CommitResult.REJECTED_SUBTERRAIN;
}
HydrologyCaveCell hydrology = delegate.getData( HydrologyCaveCell hydrology = delegate.getData(
mutation.x(), mutation.y(), mutation.z(), HydrologyCaveCell.class); mutation.x(), mutation.y(), mutation.z(), HydrologyCaveCell.class);
if (hydrology != null && hydrology.protectsPlacement()) { if (hydrology != null && hydrology.protectsPlacement()) {
@@ -173,6 +180,15 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
return delegate.getFluidHeight(); return delegate.getFluidHeight();
} }
@Override
public int getFluidHeight(int x, int z) {
SubterrainCell feature = engine == null ? null : engine.getSubterrainCell(x, anchorY, z);
if (feature != null && feature.fluid()) {
return feature.room().fluidHeadY() + 1 - engine.getMinHeight();
}
return delegate.getFluidHeight(x, z);
}
@Override @Override
public boolean isDebugSmartBore() { public boolean isDebugSmartBore() {
return delegate.isDebugSmartBore(); return delegate.isDebugSmartBore();
@@ -261,7 +277,8 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
EMPTY, EMPTY,
REJECTED_TRANSITION, REJECTED_TRANSITION,
REJECTED_BOUNDS, REJECTED_BOUNDS,
REJECTED_HYDROLOGY REJECTED_HYDROLOGY,
REJECTED_SUBTERRAIN
} }
private interface BufferedMutation { private interface BufferedMutation {
@@ -19,6 +19,9 @@
package art.arcane.iris.generation.mantle; package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.UpperDimensionContext; import art.arcane.iris.generation.runtime.UpperDimensionContext;
import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.biome.IrisBiome;
@@ -38,6 +41,7 @@ import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import art.arcane.volmlib.util.math.PowerOfTwoCoordinates; import art.arcane.volmlib.util.math.PowerOfTwoCoordinates;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import java.util.TreeSet;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.IdentityHashMap; import java.util.IdentityHashMap;
@@ -160,6 +164,31 @@ public class MantleCarvingComponent extends IrisMantleComponent {
surfaceFluidBoundaries surfaceFluidBoundaries
); );
} }
rasterizeSubterrain(writer, x, z, complex);
}
private void rasterizeSubterrain(MantleWriter writer, int chunkX, int chunkZ, IrisComplex complex) {
SubterrainPlanner planner = complex.getSubterrainPlanner();
if (planner == null || planner.isEmpty()) {
return;
}
int minimumY = getEngineMantle().getEngine().getWorld().minHeight();
SubterrainRasterizer.rasterize(planner, chunkX, chunkZ, (x, worldY, z, cell) -> {
int y = worldY - minimumY;
if (y <= 0 || y >= writer.getMantle().getWorldHeight()) {
return;
}
if (cell.solid()) {
writer.clearData(x, y, z, MatterCavern.class);
} else {
writer.setData(x, y, z, SubterrainRasterizer.cavern(cell));
}
if (cell.solid()) {
writer.setData(x, y, z, SubterrainRasterizer.state(cell));
} else {
writer.clearData(x, y, z, NativeBlockState.class);
}
});
} }
private static int maxSurfaceBreakDepth(List<WeightedProfile> weightedProfiles) { private static int maxSurfaceBreakDepth(List<WeightedProfile> weightedProfiles) {
@@ -343,33 +372,45 @@ public class MantleCarvingComponent extends IrisMantleComponent {
continue; continue;
} }
Map<IrisCaveProfile, double[]> rootProfileColumnWeights = new IdentityHashMap<>(); TreeSet<Integer> boundaries = new TreeSet<>();
List<IrisCaveProfile> rootProfileOrder = new ArrayList<>(); int minimumY = engine.getWorld().minHeight();
IrisRange worldYRange = entry.getWorldYRange(); int maximumY = engine.getWorld().maxHeight();
for (int columnIndex = 0; columnIndex < CHUNK_AREA; columnIndex++) { boundaries.add(minimumY);
IrisDimensionCarvingEntry resolvedEntry = IrisDimensionCarvingResolver.resolveFromRoot( boundaries.add(maximumY);
engine, entry, baseX + (columnIndex >> 4), baseZ + (columnIndex & 15), resolverState); for (IrisDimensionCarvingEntry candidate : entries) {
IrisBiome resolvedBiome = IrisDimensionCarvingResolver.resolveEntryBiome(engine, resolvedEntry, resolverState); if (candidate != null && candidate.getWorldYRange() != null) {
if (resolvedBiome == null) { boundaries.add(Math.max(minimumY, Math.min(maximumY, (int) Math.ceil(candidate.getWorldYRange().getMin()))));
continue; boundaries.add(Math.max(minimumY, Math.min(maximumY, (int) Math.floor(candidate.getWorldYRange().getMax()) + 1)));
} }
IrisCaveProfile profile = resolvedBiome.getCaveProfile();
if (!isProfileEnabled(profile)) {
continue;
}
double[] columnWeights = rootProfileColumnWeights.get(profile);
if (columnWeights == null) {
columnWeights = blendScratch.acquireWeights();
rootProfileColumnWeights.put(profile, columnWeights);
rootProfileOrder.add(profile);
}
columnWeights[columnIndex] = 1D;
} }
Integer previous = null;
for (IrisCaveProfile profile : rootProfileOrder) { for (int boundary : boundaries) {
weightedProfiles.add(new WeightedProfile(profile, rootProfileColumnWeights.get(profile), -1D, worldYRange, weightedProfiles.size())); if (previous != null && boundary > previous
&& (entry.getWorldYRange() == null || entry.getWorldYRange().contains(previous))) {
Map<IrisCaveProfile, double[]> rootProfileColumnWeights = new IdentityHashMap<>();
List<IrisCaveProfile> rootProfileOrder = new ArrayList<>();
for (int columnIndex = 0; columnIndex < CHUNK_AREA; columnIndex++) {
IrisDimensionCarvingEntry resolvedEntry = IrisDimensionCarvingResolver.resolveFromRoot(
engine, entry, baseX + (columnIndex >> 4), previous, baseZ + (columnIndex & 15), resolverState);
IrisBiome resolvedBiome = IrisDimensionCarvingResolver.resolveEntryBiome(engine, resolvedEntry, resolverState);
IrisCaveProfile profile = resolvedBiome == null ? null : resolvedBiome.getCaveProfile();
if (!isProfileEnabled(profile)) {
continue;
}
double[] columnWeights = rootProfileColumnWeights.get(profile);
if (columnWeights == null) {
columnWeights = blendScratch.acquireWeights();
rootProfileColumnWeights.put(profile, columnWeights);
rootProfileOrder.add(profile);
}
columnWeights[columnIndex] = 1D;
}
IrisRange worldYRange = new IrisRange(previous, boundary - 1);
for (IrisCaveProfile profile : rootProfileOrder) {
weightedProfiles.add(new WeightedProfile(profile, rootProfileColumnWeights.get(profile), -1D, worldYRange, weightedProfiles.size()));
}
}
previous = boundary;
} }
} }
@@ -444,7 +485,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
IrisDimensionCarvingEntry rootCarvingEntry = IrisDimensionCarvingResolver.resolveRootEntry(getEngineMantle().getEngine(), worldY, resolverState); IrisDimensionCarvingEntry rootCarvingEntry = IrisDimensionCarvingResolver.resolveRootEntry(getEngineMantle().getEngine(), worldY, resolverState);
IrisBiome resolvedCarvingBiome = null; IrisBiome resolvedCarvingBiome = null;
if (rootCarvingEntry != null) { if (rootCarvingEntry != null) {
IrisDimensionCarvingEntry resolvedCarvingEntry = IrisDimensionCarvingResolver.resolveFromRoot(getEngineMantle().getEngine(), rootCarvingEntry, worldX, worldZ, resolverState); IrisDimensionCarvingEntry resolvedCarvingEntry = IrisDimensionCarvingResolver.resolveFromRoot(getEngineMantle().getEngine(), rootCarvingEntry, worldX, worldY, worldZ, resolverState);
resolvedCarvingBiome = IrisDimensionCarvingResolver.resolveEntryBiome(getEngineMantle().getEngine(), resolvedCarvingEntry, resolverState); resolvedCarvingBiome = IrisDimensionCarvingResolver.resolveEntryBiome(getEngineMantle().getEngine(), resolvedCarvingEntry, resolverState);
if (resolvedCarvingBiome != null) { if (resolvedCarvingBiome != null) {
caveBiome = resolvedCarvingBiome; caveBiome = resolvedCarvingBiome;
@@ -12,6 +12,7 @@ import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.generation.hydrology.cave.HydrologyCavePlan; import art.arcane.iris.generation.hydrology.cave.HydrologyCavePlan;
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime; import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.generation.hydrology.IrisDeepFluidConfig; import art.arcane.iris.generation.hydrology.IrisDeepFluidConfig;
import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.hydrology.IrisHydrology; import art.arcane.iris.generation.hydrology.IrisHydrology;
@@ -765,17 +766,33 @@ public final class MantleHydrologyComponent extends IrisMantleComponent {
} }
for (Map.Entry<CavePosition, NativeBlockState> entry : resolvedSurfaceWrites.entrySet()) { for (Map.Entry<CavePosition, NativeBlockState> entry : resolvedSurfaceWrites.entrySet()) {
CavePosition position = entry.getKey(); CavePosition position = entry.getKey();
if (featureOwns(writer, context, position)) {
continue;
}
writer.setData(position.x(), position.y(), position.z(), entry.getValue()); writer.setData(position.x(), position.y(), position.z(), entry.getValue());
} }
for (CavePosition position : sortedPositions(publication.fluidUpdates())) { for (CavePosition position : sortedPositions(publication.fluidUpdates())) {
if (featureOwns(writer, context, position)) {
continue;
}
writer.setData(position.x(), position.y(), position.z(), UpdateMatter.ON); writer.setData(position.x(), position.y(), position.z(), UpdateMatter.ON);
} }
for (Map.Entry<CavePosition, HydrologyCaveCell> entry : sortedEntries(publication.caveCells())) { for (Map.Entry<CavePosition, HydrologyCaveCell> entry : sortedEntries(publication.caveCells())) {
CavePosition position = entry.getKey(); CavePosition position = entry.getKey();
if (featureOwns(writer, context, position)) {
continue;
}
writer.setData(position.x(), position.y(), position.z(), entry.getValue()); writer.setData(position.x(), position.y(), position.z(), entry.getValue());
} }
} }
private static boolean featureOwns(MantleWriter writer, ChunkContext context, CavePosition position) {
IrisComplex complex = context.getComplex();
SubterrainPlanner planner = complex.getSubterrainPlanner();
return planner != null && !planner.isEmpty()
&& planner.sample(position.x(), position.y() + writer.getEngine().getWorld().minHeight(), position.z()).owned();
}
private static void validateLayer(HydrologyColumnLayer layer, int worldHeight) { private static void validateLayer(HydrologyColumnLayer layer, int worldHeight) {
if (layer.bedY() < 0 if (layer.bedY() < 0
|| layer.fluidHeadY() < layer.bedY() || layer.fluidHeadY() < layer.bedY()
@@ -37,6 +37,8 @@ import art.arcane.iris.generation.cave.IrisCaveProfile;
import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.structure.object.IrisObject; import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.structure.object.IrisObjectPlacement; import art.arcane.iris.structure.object.IrisObjectPlacement;
import art.arcane.iris.structure.object.IrisObjectPlacementAlignment;
import art.arcane.volmlib.util.math.BlockPosition;
import art.arcane.iris.structure.object.IrisObjectRotation; import art.arcane.iris.structure.object.IrisObjectRotation;
import art.arcane.iris.structure.object.IrisObjectTranslate; import art.arcane.iris.structure.object.IrisObjectTranslate;
import art.arcane.iris.structure.object.IrisObjectVacuum; import art.arcane.iris.structure.object.IrisObjectVacuum;
@@ -46,6 +48,10 @@ import art.arcane.iris.generation.decoration.tree.IrisProceduralTree;
import art.arcane.iris.generation.terrain.IrisRegion; import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.structure.object.ObjectPlaceMode; import art.arcane.iris.structure.object.ObjectPlaceMode;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell; import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainRoom;
import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
import art.arcane.iris.generation.subterrain.SubterrainPlan;
import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KMap;
@@ -69,9 +75,11 @@ import java.io.IOException;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.HashSet;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiConsumer;
@ComponentFlag(ReservedFlag.OBJECT) @ComponentFlag(ReservedFlag.OBJECT)
public class MantleObjectComponent extends IrisMantleComponent { public class MantleObjectComponent extends IrisMantleComponent {
@@ -250,7 +258,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
+ " regionCavePlacers=" + region.getCarvingObjects().size()); + " regionCavePlacers=" + region.getCarvingObjects().size());
} }
ObjectPlacementSummary summary = placeObjects(writer, rng, x, z, surfaceBiome, caveBiome, region, complex, traceRegen); ObjectPlacementSummary summary = placeObjects(writer, rng, x, z, surfaceBiome, caveBiome, region, complex, traceRegen);
placeProceduralObjects(writer, rng, x, z, surfaceBiome, caveBiome, region); placeProceduralObjects(writer, rng, x, z, surfaceBiome, caveBiome, region, complex);
UpperDimensionContext upperCtx = getEngineMantle().getEngine().getUpperContext(); UpperDimensionContext upperCtx = getEngineMantle().getEngine().getUpperContext();
IrisDimension dimension = getDimension(); IrisDimension dimension = getDimension();
if (upperCtx != null && dimension.isUpperDimensionObjects()) { if (upperCtx != null && dimension.isUpperDimensionObjects()) {
@@ -603,7 +611,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
} }
@ChunkCoordinates @ChunkCoordinates
private void placeProceduralObjects(ObjectPassPlacer writer, RNG rng, int x, int z, IrisBiome surfaceBiome, IrisBiome caveBiome, IrisRegion region) { private void placeProceduralObjects(ObjectPassPlacer writer, RNG rng, int x, int z, IrisBiome surfaceBiome, IrisBiome caveBiome, IrisRegion region, IrisComplex complex) {
IrisCaveProfile surfaceCaveProfile = resolveCaveProfile(surfaceBiome.getCaveProfile(), region.getCaveProfile()); IrisCaveProfile surfaceCaveProfile = resolveCaveProfile(surfaceBiome.getCaveProfile(), region.getCaveProfile());
IrisCaveProfile regionCaveProfile = resolveCaveProfile(region.getCaveProfile(), caveBiome == null ? null : caveBiome.getCaveProfile()); IrisCaveProfile regionCaveProfile = resolveCaveProfile(region.getCaveProfile(), caveBiome == null ? null : caveBiome.getCaveProfile());
placeProceduralFrom(writer, rng, x, z, surfaceBiome.getProceduralObjects(), surfaceBiome.getName(), surfaceCaveProfile, surfaceBiome.getLoadKey()); placeProceduralFrom(writer, rng, x, z, surfaceBiome.getProceduralObjects(), surfaceBiome.getName(), surfaceCaveProfile, surfaceBiome.getLoadKey());
@@ -612,6 +620,23 @@ public class MantleObjectComponent extends IrisMantleComponent {
IrisCaveProfile caveProfile = resolveCaveProfile(caveBiome.getCaveProfile(), region.getCaveProfile()); IrisCaveProfile caveProfile = resolveCaveProfile(caveBiome.getCaveProfile(), region.getCaveProfile());
placeProceduralFrom(writer, rng, x, z, caveBiome.getProceduralObjects(), caveBiome.getName(), caveProfile, caveBiome.getLoadKey()); placeProceduralFrom(writer, rng, x, z, caveBiome.getProceduralObjects(), caveBiome.getName(), caveProfile, caveBiome.getLoadKey());
} }
if (complex.getSubterrainPlanner() != null && !complex.getSubterrainPlanner().isEmpty()) {
Set<String> placed = new HashSet<>();
placed.add(surfaceBiome.getLoadKey());
if (caveBiome != null) {
placed.add(caveBiome.getLoadKey());
}
for (SubterrainPlan plan : complex.getSubterrainPlanner().plansForBounds(x << 4, z << 4, (x << 4) + 15, (z << 4) + 15)) {
if (plan.biome().isEmpty() || !placed.add(plan.biome())) {
continue;
}
IrisBiome featureBiome = getData().getBiomeLoader().load(plan.biome());
if (featureBiome != null) {
placeProceduralFrom(writer, rng, x, z, featureBiome.getProceduralObjects(), featureBiome.getName(),
resolveCaveProfile(featureBiome.getCaveProfile(), region.getCaveProfile()), featureBiome.getLoadKey());
}
}
}
} }
@ChunkCoordinates @ChunkCoordinates
@@ -660,16 +685,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
IObjectPlacer placer = golden ? new GoldenDebugObjectPlacer(basePlacer, scope + "/" + p.getName()) : basePlacer; IObjectPlacer placer = golden ? new GoldenDebugObjectPlacer(basePlacer, scope + "/" + p.getName()) : basePlacer;
int density = Math.max(1, p.getDensity()); int density = Math.max(1, p.getDensity());
for (int i = 0; i < density; i++) { for (int i = 0; i < density; i++) {
IrisObject variant = placement.scaleObject(rng, p.getVariantObject(getData(), rng), getDimension());
if (variant == null) {
if (golden) {
IrisLogging.debug("Goldendebug procedural pick: chunk=" + x + "," + z
+ " placement=" + p.getName()
+ " densityIndex=" + i
+ " variant=null");
}
continue;
}
CavePlacementAnchor caveAnchor = null; CavePlacementAnchor caveAnchor = null;
if (carving) { if (carving) {
caveAnchor = findCavePlacementAnchor( caveAnchor = findCavePlacementAnchor(
@@ -694,6 +709,16 @@ public class MantleObjectComponent extends IrisMantleComponent {
} }
continue; continue;
} }
IrisObject variant = placement.scaleObject(rng, p.getVariantObject(getData(), rng, proceduralRoom(caveAnchor)), getDimension());
if (variant == null) {
if (golden) {
IrisLogging.debug("Goldendebug procedural pick: chunk=" + x + "," + z
+ " placement=" + p.getName()
+ " densityIndex=" + i
+ " variant=null");
}
continue;
}
int xx = caveAnchor == null ? rng.i(blockX, blockX + 16) : caveAnchor.x(); int xx = caveAnchor == null ? rng.i(blockX, blockX + 16) : caveAnchor.x();
int zz = caveAnchor == null ? rng.i(blockZ, blockZ + 16) : caveAnchor.z(); int zz = caveAnchor == null ? rng.i(blockZ, blockZ + 16) : caveAnchor.z();
int id = rng.i(0, Integer.MAX_VALUE); int id = rng.i(0, Integer.MAX_VALUE);
@@ -730,6 +755,8 @@ public class MantleObjectComponent extends IrisMantleComponent {
caveFloorY, caveFloorY,
zz, zz,
effectivePlacement, effectivePlacement,
anchorMode == IrisCaveAnchorMode.FLOOR && effectivePlacement.getMode() != ObjectPlaceMode.CEILING_HANG
? IrisObjectPlacementAlignment.FLOOR : IrisObjectPlacementAlignment.CENTER,
minDepthBelowSurface, minDepthBelowSurface,
id, id,
"procedural", "procedural",
@@ -773,6 +800,14 @@ public class MantleObjectComponent extends IrisMantleComponent {
} }
} }
private SubterrainRoom proceduralRoom(CavePlacementAnchor anchor) {
if (anchor == null) {
return null;
}
SubterrainCell cell = getEngineMantle().getEngine().getSubterrainCell(anchor.x(), anchor.y(), anchor.z());
return cell == null ? null : cell.room();
}
static boolean passesProceduralChance(RNG rng, double chance) { static boolean passesProceduralChance(RNG rng, double chance) {
if (chance <= 0.0) { if (chance <= 0.0) {
return false; return false;
@@ -827,6 +862,11 @@ public class MantleObjectComponent extends IrisMantleComponent {
private boolean acceptsCaveAnchorAt(int x, int y, int z, boolean underwater, private boolean acceptsCaveAnchorAt(int x, int y, int z, boolean underwater,
MatterCavern cavern, HydrologyCaveCell hydrology) { MatterCavern cavern, HydrologyCaveCell hydrology) {
SubterrainCell feature = getEngineMantle().getEngine().getSubterrainCell(x, y, z);
if (feature != null && feature.owned()) {
return !SubterrainRasterizer.protectsPlacement(feature)
&& (underwater ? feature.fluid() : feature.kind() == SubterrainCell.Kind.AIR);
}
return acceptsCaveAnchorFluid(underwater, hydrology == null ? cavern : hydrology.asCavern(), return acceptsCaveAnchorFluid(underwater, hydrology == null ? cavern : hydrology.asCavern(),
hydrology, y, getDimension().getCaveLavaHeight()); hydrology, y, getDimension().getCaveLavaHeight());
} }
@@ -862,6 +902,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
int anchorY, int anchorY,
int z, int z,
IrisObjectPlacement placement, IrisObjectPlacement placement,
IrisObjectPlacementAlignment alignment,
int minDepthBelowSurface, int minDepthBelowSurface,
int id, int id,
String markerContext, String markerContext,
@@ -875,12 +916,13 @@ public class MantleObjectComponent extends IrisMantleComponent {
CaveObjectPlacementTransaction.CommitResult.REJECTED_TRANSITION); CaveObjectPlacementTransaction.CommitResult.REJECTED_TRANSITION);
} }
CaveObjectPlacementTransaction transaction = new CaveObjectPlacementTransaction(placer, anchorY, minDepthBelowSurface); CaveObjectPlacementTransaction transaction = new CaveObjectPlacementTransaction(placer, anchorY, minDepthBelowSurface);
int placeY = anchorY; int placeY = alignment == IrisObjectPlacementAlignment.FLOOR
? proceduralFloorY(placer, x, anchorY, z) : anchorY;
if (placement.getMode() == ObjectPlaceMode.CEILING_HANG) { if (placement.getMode() == ObjectPlaceMode.CEILING_HANG) {
placeY = Math.max(1, transaction.getCaveCeiling(x, z) - 1 - Math.floorDiv(object.getH(), 2)); placeY = Math.max(1, transaction.getCaveCeiling(x, z) - 1 - Math.floorDiv(object.getH(), 2));
} }
String marker = placementMarker(object, id, markerContext); String marker = placementMarker(object, id, markerContext);
int result = object.place(x, placeY, z, transaction, placement, rng, (block, data) -> { BiConsumer<BlockPosition, NativeBlockState> listener = (block, data) -> {
if (marker != null) { if (marker != null) {
transaction.setData(block.getX(), block.getY(), block.getZ(), marker); transaction.setData(block.getX(), block.getY(), block.getZ(), marker);
} }
@@ -890,7 +932,10 @@ public class MantleObjectComponent extends IrisMantleComponent {
if (placement.isDolphinTarget() && placement.isUnderwater() && B.isStorageChest(data)) { if (placement.isDolphinTarget() && placement.isUnderwater() && B.isStorageChest(data)) {
transaction.setData(block.getX(), block.getY(), block.getZ(), MatterStructurePOI.BURIED_TREASURE); transaction.setData(block.getX(), block.getY(), block.getZ(), MatterStructurePOI.BURIED_TREASURE);
} }
}, null, getData()); };
int result = alignment == IrisObjectPlacementAlignment.FLOOR
? object.placeOnFloor(x, placeY, z, transaction, placement, rng, listener, getData())
: object.place(x, placeY, z, transaction, placement, rng, listener, null, getData());
if (result < 0) { if (result < 0) {
transaction.discard(); transaction.discard();
return new ContainedPlacementResult(result, CaveObjectPlacementTransaction.CommitResult.EMPTY); return new ContainedPlacementResult(result, CaveObjectPlacementTransaction.CommitResult.EMPTY);
@@ -1147,6 +1192,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
y, y,
z, z,
effectivePlacement, effectivePlacement,
IrisObjectPlacementAlignment.CENTER,
objectMinDepthBelowSurface, objectMinDepthBelowSurface,
id, id,
"cave", "cave",
@@ -1502,6 +1548,17 @@ public class MantleObjectComponent extends IrisMantleComponent {
return resolvedPlacement; return resolvedPlacement;
} }
static int proceduralFloorY(IObjectPlacer placer, int x, int scannedY, int z) {
int floorY = scannedY;
for (int depth = 0; depth < 3 && floorY > 1; depth++) {
if (!placer.isCarved(x, floorY - 1, z)) {
break;
}
floorY--;
}
return floorY;
}
static int caveAnchorScanUpperBound(int worldHeight, int surfaceY, int minDepthBelowSurface) { static int caveAnchorScanUpperBound(int worldHeight, int surfaceY, int minDepthBelowSurface) {
int maxAnchorY = CaveObjectPlacementTransaction.maxBuriedY(worldHeight, surfaceY, minDepthBelowSurface); int maxAnchorY = CaveObjectPlacementTransaction.maxBuriedY(worldHeight, surfaceY, minDepthBelowSurface);
if (maxAnchorY <= 1) { if (maxAnchorY <= 1) {
@@ -38,6 +38,8 @@ import art.arcane.iris.pack.value.IrisPosition;
import art.arcane.iris.generation.block.TileData; import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.generation.context.ChunkContext; import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell; import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
import art.arcane.volmlib.util.collection.KSet; import art.arcane.volmlib.util.collection.KSet;
import art.arcane.volmlib.util.documentation.ChunkCoordinates; import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.function.Function3; import art.arcane.volmlib.util.function.Function3;
@@ -364,10 +366,13 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
MantleChunk<Matter> chunk = acquireChunk(cx, cz); MantleChunk<Matter> chunk = acquireChunk(cx, cz);
if (chunk == null) return; if (chunk == null) return;
if (protectsSubterrainPlacement(x, y, z)) {
return;
}
synchronized (chunk) { synchronized (chunk) {
Matter matter = chunk.getOrCreate(y >> 4); Matter matter = chunk.getOrCreate(y >> 4);
if ((t instanceof NativeBlockState || t instanceof MatterCavern) if ((t instanceof NativeBlockState || t instanceof MatterCavern)
&& hasProtectedHydrology(matter, x, y, z)) { && (hasProtectedHydrology(matter, x, y, z) || protectsSubterrainPlacement(x, y, z))) {
return; return;
} }
if (t instanceof NativeBlockState) { if (t instanceof NativeBlockState) {
@@ -399,7 +404,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
synchronized (chunk) { synchronized (chunk) {
Matter matter = chunk.getOrCreate(y >> 4); Matter matter = chunk.getOrCreate(y >> 4);
if (hasProtectedHydrology(matter, x, y, z)) { if (hasProtectedHydrology(matter, x, y, z) || protectsSubterrainPlacement(x, y, z)) {
return false; return false;
} }
MatterSlice<MatterCavern> cavernSlice = matter.getSlice(MatterCavern.class); MatterSlice<MatterCavern> cavernSlice = matter.getSlice(MatterCavern.class);
@@ -432,7 +437,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
synchronized (chunk) { synchronized (chunk) {
Matter matter = chunk.getOrCreate(y >> 4); Matter matter = chunk.getOrCreate(y >> 4);
if (hasProtectedHydrology(matter, x, y, z)) { if (hasProtectedHydrology(matter, x, y, z) || protectsSubterrainPlacement(x, y, z)) {
return false; return false;
} }
MatterSlice<NativeBlockState> blockSlice = matter.getSlice(NativeBlockState.class); MatterSlice<NativeBlockState> blockSlice = matter.getSlice(NativeBlockState.class);
@@ -467,7 +472,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
} }
synchronized (chunk) { synchronized (chunk) {
Matter matter = chunk.getOrCreate(y >> 4); Matter matter = chunk.getOrCreate(y >> 4);
if (hasProtectedHydrology(matter, x, y, z)) { if (hasProtectedHydrology(matter, x, y, z) || protectsSubterrainPlacement(x, y, z)) {
return; return;
} }
capturePreObjectOriginal(matter, x, y, z, NativeBlockState.class); capturePreObjectOriginal(matter, x, y, z, NativeBlockState.class);
@@ -497,7 +502,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
if (matter == null) { if (matter == null) {
return; return;
} }
if (hasProtectedHydrology(matter, x, y, z)) { if (hasProtectedHydrology(matter, x, y, z) || protectsSubterrainPlacement(x, y, z)) {
return; return;
} }
MatterSlice<NativeBlockState> blockSlice = matter.getSlice(NativeBlockState.class); MatterSlice<NativeBlockState> blockSlice = matter.getSlice(NativeBlockState.class);
@@ -578,10 +583,14 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
public NativeBlockState getPrerequisiteBlock(int x, int y, int z) { public NativeBlockState getPrerequisiteBlock(int x, int y, int z) {
NativeBlockState block = getPrerequisiteDataIfPresent(x, y, z, NativeBlockState.class); NativeBlockState block = getPrerequisiteDataIfPresent(x, y, z, NativeBlockState.class);
return block == null ? AIR.get() : block; return block == null ? subterrainFallback(x, y, z) : block;
} }
public boolean isPrerequisiteCarved(int x, int y, int z) { public boolean isPrerequisiteCarved(int x, int y, int z) {
SubterrainCell feature = getEngine().getSubterrainCell(x, y, z);
if (feature != null && feature.owned()) {
return feature.carve();
}
Terrain3DColumn transformed = engineMantle.getComplex().transformedColumn(x, z); Terrain3DColumn transformed = engineMantle.getComplex().transformedColumn(x, z);
if (transformed != null) { if (transformed != null) {
return y >= 0 && y < transformed.topY() && !transformed.isSolid(y); return y >= 0 && y < transformed.topY() && !transformed.isSolid(y);
@@ -729,7 +738,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
} }
public void clearData(int x, int y, int z, Class<?> type) { public void clearData(int x, int y, int z, Class<?> type) {
if (!allowsWrite(x, z) || y < 0 || y >= mantle.getWorldHeight()) { if (!allowsWrite(x, z) || y < 0 || y >= mantle.getWorldHeight() || protectsSubterrainPlacement(x, y, z)) {
return; return;
} }
@@ -745,7 +754,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
return; return;
} }
if ((type == NativeBlockState.class || type == MatterCavern.class) if ((type == NativeBlockState.class || type == MatterCavern.class)
&& hasProtectedHydrology(matter, x, y, z)) { && (hasProtectedHydrology(matter, x, y, z) || protectsSubterrainPlacement(x, y, z))) {
return; return;
} }
capturePreObjectOriginal(matter, x, y, z, type); capturePreObjectOriginal(matter, x, y, z, type);
@@ -754,6 +763,12 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
} }
} }
private boolean protectsSubterrainPlacement(int x, int y, int z) {
Integer priority = activeComponentPriority.get();
return priority != null && priority >= OBJECT_COMPONENT_PRIORITY
&& SubterrainRasterizer.protectsPlacement(getEngine().getSubterrainCell(x, y, z));
}
private static boolean hasProtectedHydrology(Matter matter, int x, int y, int z) { private static boolean hasProtectedHydrology(Matter matter, int x, int y, int z) {
MatterSlice<HydrologyCaveCell> slice = matter.getSlice(HydrologyCaveCell.class); MatterSlice<HydrologyCaveCell> slice = matter.getSlice(HydrologyCaveCell.class);
if (slice == null) { if (slice == null) {
@@ -785,7 +800,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
synchronized (chunk) { synchronized (chunk) {
Matter matter = chunk.getOrCreate(y >> 4); Matter matter = chunk.getOrCreate(y >> 4);
if (hasProtectedHydrology(matter, x, y, z)) { if (hasProtectedHydrology(matter, x, y, z) || protectsSubterrainPlacement(x, y, z)) {
return; return;
} }
MatterSlice<NativeBlockState> blockSlice = matter.slice(NativeBlockState.class); MatterSlice<NativeBlockState> blockSlice = matter.slice(NativeBlockState.class);
@@ -957,9 +972,12 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
// Read-only probe: getDataIfPresent returns the identical answer without materializing // Read-only probe: getDataIfPresent returns the identical answer without materializing
// a 16^3 section + slice on a miss the way getData's getOrCreate path does. // a 16^3 section + slice on a miss the way getData's getOrCreate path does.
NativeBlockState block = getDataIfPresent(x, y, z, NativeBlockState.class); NativeBlockState block = getDataIfPresent(x, y, z, NativeBlockState.class);
if (block == null) return block == null ? subterrainFallback(x, y, z) : block;
return AIR.get(); }
return block;
private NativeBlockState subterrainFallback(int x, int y, int z) {
SubterrainCell cell = getEngine().getSubterrainCell(x, y, z);
return cell != null && cell.owned() ? SubterrainRasterizer.state(cell) : AIR.get();
} }
@Override @Override
@@ -969,6 +987,10 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
@Override @Override
public boolean isCarved(int x, int y, int z) { public boolean isCarved(int x, int y, int z) {
SubterrainCell feature = getEngine().getSubterrainCell(x, y, z);
if (feature != null && feature.owned()) {
return feature.carve();
}
Terrain3DColumn transformed = engineMantle.getComplex().transformedColumn(x, z); Terrain3DColumn transformed = engineMantle.getComplex().transformedColumn(x, z);
if (transformed != null) { if (transformed != null) {
return y >= 0 && y < transformed.topY() && !transformed.isSolid(y); return y >= 0 && y < transformed.topY() && !transformed.isSolid(y);
@@ -6,6 +6,7 @@ import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial; import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell; import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
import art.arcane.iris.generation.block.TileData; import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.structure.object.IObjectPlacer; import art.arcane.iris.structure.object.IObjectPlacer;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
@@ -228,7 +229,8 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
} }
private void setData(int x, int y, int z, Object data, SetMutation replayed) { private void setData(int x, int y, int z, Object data, SetMutation replayed) {
if (data == null || y < 0 || y >= worldHeight) { if (data == null || y < 0 || y >= worldHeight
|| protectsSubterrain(x, y, z)) {
return; return;
} }
if (data instanceof NativeBlockState && !canSetBlock(x, y, z)) { if (data instanceof NativeBlockState && !canSetBlock(x, y, z)) {
@@ -351,7 +353,13 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
if (y == 0 && writer.getEngine().getDimension().isBedrock()) { if (y == 0 && writer.getEngine().getDimension().isBedrock()) {
return false; return false;
} }
return !hasProtectedHydrology(x, y, z); return !hasProtectedHydrology(x, y, z)
&& !protectsSubterrain(x, y, z);
}
private boolean protectsSubterrain(int x, int y, int z) {
Engine engine = getEngine();
return engine != null && SubterrainRasterizer.protectsPlacement(engine.getSubterrainCell(x, y, z));
} }
private boolean hasProtectedHydrology(int x, int y, int z) { private boolean hasProtectedHydrology(int x, int y, int z) {
@@ -82,6 +82,7 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
if (getEngine().getDimensionStackContext() == null) { if (getEngine().getDimensionStackContext() == null) {
registerStage(sCustom); registerStage(sCustom);
} }
registerStage((x, z, k, p, m, c) -> cave.preserveSubterrain(x, z, k));
} }
void generateTerrainMatterForChunk(int x, int z, boolean multicore, ChunkContext context) { void generateTerrainMatterForChunk(int x, int z, boolean multicore, ChunkContext context) {
@@ -19,6 +19,8 @@
package art.arcane.iris.generation.runtime; package art.arcane.iris.generation.runtime;
import art.arcane.iris.world.history.ChunkGenerationSemantics; import art.arcane.iris.world.history.ChunkGenerationSemantics;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipStore; import art.arcane.iris.structure.nativegen.NativeStructureOwnershipStore;
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume; import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
@@ -328,6 +330,9 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
@BlockCoordinates @BlockCoordinates
default IrisRegion getRegion(int x, int y, int z) { default IrisRegion getRegion(int x, int y, int z) {
if (getSubterrainCell(x, y, z).occupied()) {
return getComplex().getRegionStream().get(x, z);
}
DimensionStackContext dimensionStackContext = getDimensionStackContext(); DimensionStackContext dimensionStackContext = getDimensionStackContext();
if (dimensionStackContext != null) { if (dimensionStackContext != null) {
DimensionStackLayout.Layer layer = dimensionStackContext.getLayout(x, z).layerAt(y); DimensionStackLayout.Layer layer = dimensionStackContext.getLayout(x, z).layerAt(y);
@@ -340,8 +345,30 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
void generateMatter(int x, int z, boolean multicore, ChunkContext context); void generateMatter(int x, int z, boolean multicore, ChunkContext context);
@BlockCoordinates
default SubterrainCell getSubterrainCell(int x, int y, int z) {
if (getComplex() == null || getDimension() == null
|| !getDimension().allowsSubterrainFeatures()
|| getDimension().getSubterrainFeatures() == null || getDimension().getSubterrainFeatures().isEmpty()) {
return SubterrainCell.OUTSIDE;
}
SubterrainPlanner planner = getComplex().getSubterrainPlanner();
return planner == null ? SubterrainCell.OUTSIDE : planner.sample(x, y + getWorld().minHeight(), z);
}
@BlockCoordinates
default IrisBiome getSubterrainBiome(int x, int y, int z) {
SubterrainCell cell = getSubterrainCell(x, y, z);
return cell.occupied() && !cell.room().biome().isEmpty()
? getData().getBiomeLoader().load(cell.room().biome()) : null;
}
@BlockCoordinates @BlockCoordinates
default IrisBiome getCaveOrMantleBiome(int x, int y, int z) { default IrisBiome getCaveOrMantleBiome(int x, int y, int z) {
IrisBiome featureBiome = getSubterrainBiome(x, y, z);
if (featureBiome != null) {
return featureBiome;
}
IrisBiome mantleBiome = mantleCaveBiome(x, y, z); IrisBiome mantleBiome = mantleCaveBiome(x, y, z);
return mantleBiome != null ? mantleBiome : getCaveBiome(x, y, z); return mantleBiome != null ? mantleBiome : getCaveBiome(x, y, z);
} }
@@ -401,6 +428,11 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
for (int index = 0; index < biomes.length; index++) { for (int index = 0; index < biomes.length; index++) {
int y = index * step; int y = index * step;
regions[index] = region; regions[index] = region;
IrisBiome featureBiome = getSubterrainBiome(x, y, z);
if (featureBiome != null) {
biomes[index] = featureBiome;
continue;
}
if (y > caveTop || complex.isTerrain3DSurface(x, y, z)) { if (y > caveTop || complex.isTerrain3DSurface(x, y, z)) {
if (surface == null) { if (surface == null) {
surface = getSurfaceBiome(x, z); surface = getSurfaceBiome(x, z);
@@ -458,6 +490,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
@BlockCoordinates @BlockCoordinates
default IrisBiome getCaveBiome(int x, int y, int z, IrisDimensionCarvingResolver.State state) { default IrisBiome getCaveBiome(int x, int y, int z, IrisDimensionCarvingResolver.State state) {
IrisBiome featureBiome = getSubterrainBiome(x, y, z);
if (featureBiome != null) {
return featureBiome;
}
IrisBiome configuredBiome = resolveConfiguredCaveBiome(x, y, z, state); IrisBiome configuredBiome = resolveConfiguredCaveBiome(x, y, z, state);
if (configuredBiome != null) { if (configuredBiome != null) {
return configuredBiome; return configuredBiome;
@@ -490,6 +526,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
IrisBiome surfaceBiome, IrisBiome surfaceBiome,
int surfaceY int surfaceY
) { ) {
IrisBiome featureBiome = getSubterrainBiome(x, y, z);
if (featureBiome != null) {
return featureBiome;
}
IrisBiome configuredBiome = resolveConfiguredCaveBiome(x, y, z, state); IrisBiome configuredBiome = resolveConfiguredCaveBiome(x, y, z, state);
if (configuredBiome != null) { if (configuredBiome != null) {
return configuredBiome; return configuredBiome;
@@ -719,6 +759,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
boolean isStudio(); boolean isStudio();
default IrisBiome getBiome(int x, int y, int z) { default IrisBiome getBiome(int x, int y, int z) {
IrisBiome featureBiome = getSubterrainBiome(x, y, z);
if (featureBiome != null) {
return featureBiome;
}
DimensionStackContext dimensionStackContext = getDimensionStackContext(); DimensionStackContext dimensionStackContext = getDimensionStackContext();
if (dimensionStackContext != null) { if (dimensionStackContext != null) {
DimensionStackLayout layout = dimensionStackContext.getLayout(x, z); DimensionStackLayout layout = dimensionStackContext.getLayout(x, z);
@@ -743,6 +787,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
} }
default IrisBiome getBiomeOrMantle(int x, int y, int z) { default IrisBiome getBiomeOrMantle(int x, int y, int z) {
IrisBiome featureBiome = getSubterrainBiome(x, y, z);
if (featureBiome != null) {
return featureBiome;
}
DimensionStackContext dimensionStackContext = getDimensionStackContext(); DimensionStackContext dimensionStackContext = getDimensionStackContext();
if (dimensionStackContext != null) { if (dimensionStackContext != null) {
DimensionStackLayout layout = dimensionStackContext.getLayout(x, z); DimensionStackLayout layout = dimensionStackContext.getLayout(x, z);
@@ -118,7 +118,7 @@ public final class GenerationCacheWarmer {
if (placement == null) { if (placement == null) {
continue; continue;
} }
placement.getVariantObject(data, root.nextParallelRNG(counter[0]++)); placement.getVariantObject(data, root.nextParallelRNG(counter[0]++), null);
} }
} }
} }
@@ -22,6 +22,8 @@ import art.arcane.iris.generation.stream.GenerationStreams;
import art.arcane.iris.generation.stream.CachedDoubleStream2D; import art.arcane.iris.generation.stream.CachedDoubleStream2D;
import art.arcane.iris.generation.stream.CachedStream2D; import art.arcane.iris.generation.stream.CachedStream2D;
import art.arcane.iris.generation.stream.ProvisionalSampling; import art.arcane.iris.generation.stream.ProvisionalSampling;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
import art.arcane.iris.configuration.IrisSettings; import art.arcane.iris.configuration.IrisSettings;
@@ -243,6 +245,8 @@ public class IrisComplex implements DataProvider {
private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> generatorBounds; private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> generatorBounds;
private Set<IrisBiome> generatorBiomes; private Set<IrisBiome> generatorBiomes;
private IrisHydrologyRuntime hydrologyRuntime; private IrisHydrologyRuntime hydrologyRuntime;
@Setter(AccessLevel.NONE)
private final transient SubterrainPlanner subterrainPlanner;
private transient IrisImageMapRuntime imageMapRuntime; private transient IrisImageMapRuntime imageMapRuntime;
// Copy-on-write: reads happen per column on every burst thread; the synchronizedMap // Copy-on-write: reads happen per column on every burst thread; the synchronizedMap
// monitor was taken on every HIT. Writes are once per biome and bounded, so a fresh map // monitor was taken on every HIT. Writes are once per biome and bounded, so a fresh map
@@ -264,6 +268,15 @@ public class IrisComplex implements DataProvider {
IrisComplex(Engine engine, boolean simple, TransitionGenerationPlan transitionGenerationPlan, boolean detached) { IrisComplex(Engine engine, boolean simple, TransitionGenerationPlan transitionGenerationPlan, boolean detached) {
terrainEngine = engine; terrainEngine = engine;
if (engine.getDimension().allowsSubterrainFeatures()) {
SubterrainRasterizer.validateMaterials(engine.getDimension().getSubterrainFeatures());
}
subterrainPlanner = new SubterrainPlanner(new SubterrainPlanner.Options(
engine.getDimension().allowsSubterrainFeatures() ? engine.getDimension().getSubterrainFeatures() : List.of(),
engine.getSeedManager().getBodies(),
engine.getDimension().getMinHeight(), engine.getDimension().getMaxHeight()),
bounds -> transitionGenerationPlan == null || transitionGenerationPlan.allowsNewFootprint(
bounds.minX(), bounds.minZ(), bounds.maxX(), bounds.maxZ()));
biomeBoundsSamplingStep = engine.getDimension().getBiomeBoundsSamplingStep(); biomeBoundsSamplingStep = engine.getDimension().getBiomeBoundsSamplingStep();
this.detached = detached; this.detached = detached;
this.transitionGenerationPlan = transitionGenerationPlan; this.transitionGenerationPlan = transitionGenerationPlan;
@@ -18,6 +18,7 @@
package art.arcane.iris.generation.runtime; package art.arcane.iris.generation.runtime;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.runtime.EngineBackgroundTasks.BackgroundTaskDrain; import art.arcane.iris.generation.runtime.EngineBackgroundTasks.BackgroundTaskDrain;
import art.arcane.iris.generation.runtime.EngineRuntimeBuilder.RuntimeAssembly; import art.arcane.iris.generation.runtime.EngineRuntimeBuilder.RuntimeAssembly;
import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.pack.loading.IrisData;
@@ -591,6 +592,31 @@ public class IrisEngine implements Engine {
return context == null || context.getEngine() != this || context.getChunkContext() == null; return context == null || context.getEngine() != this || context.getChunkContext() == null;
} }
@BlockCoordinates
@Override
public SubterrainCell getSubterrainCell(int x, int y, int z) {
if (!hasGenerationRuntimeScope() && generationHistoryRuntimeRouter != null) {
Optional<SubterrainCell> saved = generationHistoryRuntimeRouter.biomes()
.subterrainCell(x, y + getWorld().minHeight(), z);
if (saved.isPresent()) {
return saved.get();
}
}
return Engine.super.getSubterrainCell(x, y, z);
}
@BlockCoordinates
@Override
public IrisBiome getSubterrainBiome(int x, int y, int z) {
SubterrainCell cell = getSubterrainCell(x, y, z);
if (!cell.occupied() || cell.room().biome().isEmpty()) {
return null;
}
Optional<BiomeEnvironment> saved = resolveSavedBiomeEnvironment(x, y, z, false);
IrisData data = saved.isPresent() ? saved.get().data() : getData();
return data.getBiomeLoader().load(cell.room().biome());
}
@BlockCoordinates @BlockCoordinates
@Override @Override
public IrisBiome getBiome(int x, int y, int z) { public IrisBiome getBiome(int x, int y, int z) {
@@ -24,6 +24,9 @@ import art.arcane.iris.world.IrisToolbelt;
import art.arcane.volmlib.util.math.Rarity; import art.arcane.volmlib.util.math.Rarity;
import art.arcane.iris.world.history.SavedBiomeUnavailableException; import art.arcane.iris.world.history.SavedBiomeUnavailableException;
import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.decoration.IrisSurface;
import art.arcane.iris.world.entity.IrisSpawnGroup;
import org.bukkit.Location;
import art.arcane.iris.world.entity.IrisEntitySpawn; import art.arcane.iris.world.entity.IrisEntitySpawn;
import art.arcane.iris.world.entity.EntitySpawnSeed; import art.arcane.iris.world.entity.EntitySpawnSeed;
import art.arcane.iris.world.entity.IrisMarker; import art.arcane.iris.world.entity.IrisMarker;
@@ -404,41 +407,94 @@ final class WorldEntitySpawner {
if (!ambientAllowed(chunk, initial)) { if (!ambientAllowed(chunk, initial)) {
return; return;
} }
ChunkCounter counter = initial ? null : new ChunkCounter(chunk.getEntities()); ChunkCounter counter = new ChunkCounter(initial ? new Entity[0] : chunk.getEntities());
List<SpawnSelection> pool = new ArrayList<>(); List<SpawnSelection> pool = new ArrayList<>();
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.dimension().getEntitySpawners()), collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.dimension().getEntitySpawners()),
environment.biome(), chunk, initial, counter); environment.biome(), chunk, initial, counter, false);
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.region().getEntitySpawners()), collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.region().getEntitySpawners()),
null, chunk, initial, counter); null, chunk, initial, counter, false);
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.biome().getEntitySpawners()), collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.biome().getEntitySpawners()),
null, chunk, initial, counter); null, chunk, initial, counter, false);
RNG random = initial ? EntitySpawnSeed.chunk(manager.getEngine().getSeedManager().getEntity(), RNG random = initial ? EntitySpawnSeed.chunk(manager.getEngine().getSeedManager().getEntity(),
chunk.getX(), chunk.getZ()) : new RNG(RNG.r.nextLong()); chunk.getX(), chunk.getZ()) : new RNG(RNG.r.nextLong());
if (environment.dimension().hasUndergroundSpawners(manager.getEngine())) {
spawnUnderground(chunk, initial, counter, random.nextParallelRNG(0x5A17D3B48269C0EFL));
}
SpawnSelection selected = selectSpawn(pool, random); SpawnSelection selected = selectSpawn(pool, random);
if (selected == null) { if (selected == null) {
return; return;
} }
int capacity = initial ? Math.max(0, selected.spawner().getMaxEntitiesPerChunk()) int capacity = counter.remainingCapacity(selected.entry(), selected.spawner(), manager.getEngine());
: counter.remainingCapacity(selected.entry(), selected.spawner(), manager.getEngine());
int spawned = selected.entry().spawn(manager.getEngine(), chunk, random, capacity, int spawned = selected.entry().spawn(manager.getEngine(), chunk, random, capacity,
new SpawnContext(selected.spawner(), null, initial)); new SpawnContext(selected.spawner(), null, initial));
actuallySpawned.addAndGet(spawned); actuallySpawned.addAndGet(spawned);
counter.recordSpawned(selected.entry(), manager.getEngine(), spawned);
if (spawned > 0 && !initial) { if (spawned > 0 && !initial) {
selected.spawner().spawn(manager.getEngine(), chunk.getX(), chunk.getZ()); selected.spawner().spawn(manager.getEngine(), chunk.getX(), chunk.getZ());
} }
} }
private static boolean matchesUndergroundSurface(IrisEntitySpawn entry, Engine engine, IrisSurface surface) {
IrisEntity entity = entry.getRealEntity(engine);
return entity != null && entity.getSurface().isFluid() == surface.isFluid()
&& (!surface.isFluid() || entity.getSurface() == surface);
}
private void spawnUnderground(Chunk chunk, boolean initial, ChunkCounter counter, RNG random) {
if (J.isFolia() && !J.isOwnedByCurrentRegion(chunk.getWorld(), chunk.getX(), chunk.getZ())) {
return;
}
for (IrisSurface surface : List.of(IrisSurface.LAND, IrisSurface.WATER, IrisSurface.LAVA)) {
Location candidate = IrisEntitySpawn.findLiveCaveSpawnLocation(
chunk, random.nextParallelRNG(surface.ordinal()), surface);
if (candidate == null) {
continue;
}
Engine engine = manager.getEngine();
BiomeEnvironment environment;
try {
environment = engine.getBiomeOrMantleEnvironment(candidate.getBlockX(),
candidate.getBlockY() - engine.getWorld().minHeight(), candidate.getBlockZ());
} catch (SavedBiomeUnavailableException unavailable) {
continue;
}
try (BiomeEnvironment.Scope ignored = engine.openBiomeEnvironmentScope(environment)) {
List<SpawnSelection> pool = new ArrayList<>();
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.dimension().getEntitySpawners()),
environment.biome(), chunk, initial, counter, true);
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.region().getEntitySpawners()),
null, chunk, initial, counter, true);
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.biome().getEntitySpawners()),
null, chunk, initial, counter, true);
pool.removeIf(selection -> !matchesUndergroundSurface(selection.entry(), engine, surface));
SpawnSelection selected = selectSpawn(pool, random);
if (selected == null) {
continue;
}
int capacity = counter.remainingCapacity(selected.entry(), selected.spawner(), engine);
int spawned = selected.entry().spawn(engine, candidate, random, capacity,
new SpawnContext(selected.spawner(), null, initial));
actuallySpawned.addAndGet(spawned);
counter.recordSpawned(selected.entry(), engine, spawned);
if (spawned > 0 && !initial) {
selected.spawner().spawn(engine, chunk.getX(), chunk.getZ());
}
return;
}
}
}
private void collectSpawns(List<SpawnSelection> pool, KList<IrisSpawner> spawners, IrisBiome biome, private void collectSpawns(List<SpawnSelection> pool, KList<IrisSpawner> spawners, IrisBiome biome,
Chunk chunk, boolean initial, ChunkCounter counter) { Chunk chunk, boolean initial, ChunkCounter counter, boolean underground) {
for (IrisSpawner spawner : spawners) { for (IrisSpawner spawner : spawners) {
if (!spawnerAllowed(spawner, manager.getEngine(), chunk.getX(), chunk.getZ(), initial) if ((spawner.getGroup() == IrisSpawnGroup.CAVE) != underground
|| !spawnerAllowed(spawner, manager.getEngine(), chunk.getX(), chunk.getZ(), initial)
|| biome != null && !spawner.isValid(biome)) { || biome != null && !spawner.isValid(biome)) {
continue; continue;
} }
for (IrisEntitySpawn entry : initial ? spawner.getInitialSpawns() : spawner.getSpawns()) { for (IrisEntitySpawn entry : initial ? spawner.getInitialSpawns() : spawner.getSpawns()) {
IrisEntity entity = entry.getRealEntity(manager.getEngine()); IrisEntity entity = entry.getRealEntity(manager.getEngine());
int capacity = initial ? Math.max(0, spawner.getMaxEntitiesPerChunk()) int capacity = counter.remainingCapacity(entry, spawner, manager.getEngine());
: counter.remainingCapacity(entry, spawner, manager.getEngine());
if (entity != null && capacity > 0) { if (entity != null && capacity > 0) {
pool.add(new SpawnSelection(entry, spawner)); pool.add(new SpawnSelection(entry, spawner));
} }
@@ -503,6 +559,13 @@ final class WorldEntitySpawner {
} }
} }
void recordSpawned(IrisEntitySpawn entry, Engine engine, int spawned) {
IrisEntity entity = entry.getRealEntity(engine);
if (spawned > 0 && entity != null) {
counts.merge(entity.spawnCategory(), spawned, Integer::sum);
}
}
int remainingCapacity(IrisEntitySpawn entry, IrisSpawner spawner, Engine engine) { int remainingCapacity(IrisEntitySpawn entry, IrisSpawner spawner, Engine engine) {
IrisEntity entity = entry.getRealEntity(engine); IrisEntity entity = entry.getRealEntity(engine);
return spawner.remainingCapacity(entity, counts); return spawner.remainingCapacity(entity, counts);
@@ -19,6 +19,12 @@
package art.arcane.iris.generation.stage; package art.arcane.iris.generation.stage;
import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainPlan;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import java.util.List;
import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
import art.arcane.iris.generation.subterrain.SubterrainSurfaceMaterials;
import art.arcane.iris.generation.runtime.DimensionStackContext; import art.arcane.iris.generation.runtime.DimensionStackContext;
import art.arcane.iris.generation.runtime.DimensionStackLayout; import art.arcane.iris.generation.runtime.DimensionStackLayout;
import art.arcane.iris.generation.runtime.UpperDimensionContext; import art.arcane.iris.generation.runtime.UpperDimensionContext;
@@ -144,7 +150,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
worldHeightSpan, worldHeightSpan,
caveLavaHeight, caveLavaHeight,
chunkBlockX, chunkBlockX,
chunkBlockZ chunkBlockZ,
getComplex().getSubterrainPlanner(),
getComplex().getSubterrainPlanner() == null ? List.of() : getComplex().getSubterrainPlanner().plansForBounds(chunkBlockX - 1, chunkBlockZ - 1, chunkBlockX + 16, chunkBlockZ + 16)
); );
CarveResolver carveResolver = new CarveResolver(resolutionContext); CarveResolver carveResolver = new CarveResolver(resolutionContext);
terrain.forEachCell(carveResolver::apply); terrain.forEachCell(carveResolver::apply);
@@ -161,7 +169,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
IrisData wallData = getData(); IrisData wallData = getData();
walls.forEach((rx, yy, rz, cavern) -> { walls.forEach((rx, yy, rz, cavern) -> {
HydrologyCaveCell hydrology = terrain.hydrology(rx, yy, rz); HydrologyCaveCell hydrology = terrain.hydrology(rx, yy, rz);
if (hydrology != null && hydrology.protectsPlacement()) { if (hydrology != null && hydrology.protectsPlacement()
|| subterrainCell(rx + chunkBlockX, yy, rz + chunkBlockZ).owned()) {
return; return;
} }
int worldX = rx + chunkBlockX; int worldX = rx + chunkBlockX;
@@ -233,6 +242,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
output.setRaw(localX, surfaceY, localZ, AIR); output.setRaw(localX, surfaceY, localZ, AIR);
} }
} }
restoreSubterrainSolids(output, x, z);
} finally { } finally {
getEngine().getMetrics().getCarveApply().put(applyStopwatch.getMilliseconds()); getEngine().getMetrics().getCarveApply().put(applyStopwatch.getMilliseconds());
} }
@@ -242,6 +252,58 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
} }
} }
private SubterrainCell subterrainCell(int x, int y, int z) {
SubterrainCell cell = getEngine().getSubterrainCell(x, y, z);
return cell == null ? SubterrainCell.OUTSIDE : cell;
}
public void preserveSubterrain(int blockX, int blockZ, Hunk<NativeBlockState> output) {
SubterrainPlanner planner = getComplex().getSubterrainPlanner();
if (planner == null || planner.isEmpty()) {
return;
}
int minimumY = getEngine().getWorld().minHeight();
SubterrainSurfaceMaterials materials = subterrainMaterials(planner, blockX >> 4, blockZ >> 4);
SubterrainRasterizer.rasterize(planner, blockX >> 4, blockZ >> 4, (worldX, worldY, worldZ, cell) -> {
int y = worldY - minimumY;
if (y <= 0 || y >= output.getHeight()) {
return;
}
int x = worldX & 15;
int z = worldZ & 15;
NativeBlockState existing = output.getRaw(x, y, z);
NativeBlockState planned = materials.state(worldX, worldY, worldZ, cell);
if (cell.solid() || cell.room().reservedPassage() || cell.fluid() && (existing == null || B.isAir(existing) || B.isFluid(existing))) {
output.setRaw(x, y, z, planned);
} else {
output.setRaw(x, y, z, IrisProceduralBlocks.normalizeWaterlogging(existing, cell.kind() == SubterrainCell.Kind.WATER ? planned : null, true));
}
});
}
private void restoreSubterrainSolids(Hunk<NativeBlockState> output, int chunkX, int chunkZ) {
SubterrainPlanner planner = getComplex().getSubterrainPlanner();
if (planner == null || planner.isEmpty()) {
return;
}
int minimumY = getEngine().getWorld().minHeight();
SubterrainSurfaceMaterials materials = subterrainMaterials(planner, chunkX, chunkZ);
SubterrainRasterizer.rasterize(planner, chunkX, chunkZ, (worldX, worldY, worldZ, cell) -> {
int y = worldY - minimumY;
if (cell.solid() && y > 0 && y < output.getHeight()) {
output.setRaw(worldX & 15, y, worldZ & 15, materials.state(worldX, worldY, worldZ, cell));
}
});
}
private SubterrainSurfaceMaterials subterrainMaterials(SubterrainPlanner planner, int chunkX, int chunkZ) {
int blockX = chunkX << 4;
int blockZ = chunkZ << 4;
return new SubterrainSurfaceMaterials(planner,
planner.plansForBounds(blockX - 1, blockZ - 1, blockX + 16, blockZ + 16),
getEngine().getDimension(), getComplex(), getData(), rng, getEngine().getWorld().minHeight());
}
static boolean hasExplicitCarveIntent(MatterCavern cavern) { static boolean hasExplicitCarveIntent(MatterCavern cavern) {
return cavern != null && (isFluidIntent(cavern) || cavern.isLava() || cavern.getLiquid() == 3); return cavern != null && (isFluidIntent(cavern) || cavern.isLava() || cavern.getLiquid() == 3);
} }
@@ -357,6 +419,19 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
int localX = x & 15; int localX = x & 15;
int localZ = z & 15; int localZ = z & 15;
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ); int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
SubterrainCell feature = context.subterrainPlanner() == null ? SubterrainCell.OUTSIDE : context.subterrainPlanner().sample(context.subterrainPlans(),
context.chunkBlockX() + localX, y + getEngine().getWorld().minHeight(), context.chunkBlockZ() + localZ);
if (feature.owned()) {
context.output().setRaw(localX, y, localZ, SubterrainRasterizer.state(feature));
if (feature.carve()) {
context.columnMasks()[columnIndex].add(y);
if (!feature.room().biome().isEmpty()) {
context.scratch().customCaveBiomePresent = true;
}
}
return;
}
if (context.upperSurfaceHeights() != null && y >= context.upperSurfaceHeights()[columnIndex]) { if (context.upperSurfaceHeights() != null && y >= context.upperSurfaceHeights()[columnIndex]) {
return; return;
} }
@@ -432,7 +507,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
int worldHeightSpan, int worldHeightSpan,
int caveLavaHeight, int caveLavaHeight,
int chunkBlockX, int chunkBlockX,
int chunkBlockZ int chunkBlockZ,
SubterrainPlanner subterrainPlanner,
List<SubterrainPlan> subterrainPlans
) { ) {
} }
@@ -904,7 +981,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
zone.setFloor(floor); zone.setFloor(floor);
zone.setCeiling(y - 1); zone.setCeiling(y - 1);
if (zone.isValid(resolver.height) if (zone.isValid(resolver.height)
&& !isNaturalTerrainOpening(getComplex(), worldX, floor, worldZ)) { && (subterrainCell(worldX, floor, worldZ).occupied()
|| !isNaturalTerrainOpening(getComplex(), worldX, floor, worldZ))) {
if (markerRoll(worldX, zone.ceiling, worldZ, 0x9E3779B97F4A7C15L)) { if (markerRoll(worldX, zone.ceiling, worldZ, 0x9E3779B97F4A7C15L)) {
mantle.set(worldX, zone.ceiling, worldZ, MarkerMatter.CAVE_CEILING); mantle.set(worldX, zone.ceiling, worldZ, MarkerMatter.CAVE_CEILING);
} }
@@ -929,10 +1007,15 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
} }
} }
} }
restoreSubterrainSolids(output, blockX >> 4, blockZ >> 4);
} }
private void decorateZone(Hunk<NativeBlockState> output, CaveZone zone, private void decorateZone(Hunk<NativeBlockState> output, CaveZone zone,
int rx, int rz, int xx, int zz, IrisBiome floorBiome, IrisBiome ceilingBiome) { int rx, int rz, int xx, int zz, IrisBiome floorBiome, IrisBiome ceilingBiome) {
SubterrainCell feature = subterrainCell(xx, zone.getFloor(), zz);
if (feature.owned() && feature.room().reservedPassage()) {
return;
}
int maxY = output.getHeight(); int maxY = output.getHeight();
IrisDecorator[] surfaceDecorators = floorBiome == null IrisDecorator[] surfaceDecorators = floorBiome == null
? new IrisDecorator[0] ? new IrisDecorator[0]
@@ -26,6 +26,7 @@ import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.decoration.FloatingDecorator; import art.arcane.iris.generation.decoration.FloatingDecorator;
import art.arcane.iris.generation.decoration.IrisSeaSurfaceDecorator; import art.arcane.iris.generation.decoration.IrisSeaSurfaceDecorator;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.runtime.EngineAssignedModifier; import art.arcane.iris.generation.runtime.EngineAssignedModifier;
import art.arcane.iris.generation.runtime.EngineDecorator; import art.arcane.iris.generation.runtime.EngineDecorator;
import art.arcane.iris.generation.biome.FloatingBottomPaletteMode; import art.arcane.iris.generation.biome.FloatingBottomPaletteMode;
@@ -244,7 +245,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
continue; continue;
} }
int y = sample.islandBaseY + k; int y = sample.islandBaseY + k;
if (y < 0 || y >= chunkHeight) { if (y < 0 || y >= chunkHeight || featureOwns(wx, y, wz)) {
continue; continue;
} }
IrisFloatingChildBiomes entry = sample.entryAt(k); IrisFloatingChildBiomes entry = sample.entryAt(k);
@@ -275,7 +276,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
continue; continue;
} }
int y = sample.islandBaseY + k; int y = sample.islandBaseY + k;
if (y < 0 || y >= chunkHeight) { if (y < 0 || y >= chunkHeight || featureOwns(wx, y, wz)) {
continue; continue;
} }
boolean hasSolidBelow = false; boolean hasSolidBelow = false;
@@ -340,7 +341,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
int topY = sample.topY(); int topY = sample.topY();
int max = Math.max(1, chunkHeight - topY); int max = Math.max(1, chunkHeight - topY);
if (topY + 1 < chunkHeight) { if (topY + 1 < chunkHeight && !featureOwns(wx, topY, wz) && !featureOwns(wx, topY + 1, wz)) {
NativeBlockState above = output.get(xf, topY + 1, zf); NativeBlockState above = output.get(xf, topY + 1, zf);
if (above == null || above.isAir()) { if (above == null || above.isAir()) {
try { try {
@@ -362,7 +363,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
continue; continue;
} }
int y = sample.islandBaseY + k; int y = sample.islandBaseY + k;
if (y < 0 || y >= chunkHeight) { if (y < 0 || y >= chunkHeight || featureOwns(wx, y, wz)) {
continue; continue;
} }
boolean hasSolidBelow = false; boolean hasSolidBelow = false;
@@ -376,7 +377,9 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
fluidTopY = y; fluidTopY = y;
} }
} }
if (fluidTopY > 0 && fluidTopY + 1 < chunkHeight && B.isAir(output.get(xf, fluidTopY + 1, zf))) { if (fluidTopY > 0 && fluidTopY + 1 < chunkHeight
&& !featureOwns(wx, fluidTopY, wz) && !featureOwns(wx, fluidTopY + 1, wz)
&& B.isAir(output.get(xf, fluidTopY + 1, zf))) {
try { try {
seaSurfaceDecorator.decorate(xf, zf, wx, wx + 1, wx - 1, wz, wz + 1, wz - 1, output, target, fluidTopY, chunkHeight); seaSurfaceDecorator.decorate(xf, zf, wx, wx + 1, wx - 1, wz, wz + 1, wz - 1, output, target, fluidTopY, chunkHeight);
} catch (Throwable e) { } catch (Throwable e) {
@@ -389,6 +392,11 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
} }
} }
private boolean featureOwns(int x, int y, int z) {
SubterrainCell cell = getEngine().getSubterrainCell(x, y, z);
return cell != null && cell.owned();
}
private void writeIslandSkyBiomes( private void writeIslandSkyBiomes(
IrisBiome parent, IrisBiome parent,
int wx, int wx,
@@ -406,7 +414,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
continue; continue;
} }
int y = sample.islandBaseY + k; int y = sample.islandBaseY + k;
if (y < 0 || y >= chunkHeight) { if (y < 0 || y >= chunkHeight || featureOwns(wx, y, wz)) {
continue; continue;
} }
if (!shouldWriteHostBiomeMarker(stackLayout, y)) { if (!shouldWriteHostBiomeMarker(stackLayout, y)) {
@@ -0,0 +1,8 @@
package art.arcane.iris.generation.subterrain;
public enum IrisSubterrainFamily {
TECTONIC_FAULT,
CENOTE,
LAVA_TUBE,
TRAVERTINE_TERRACES
}
@@ -0,0 +1,77 @@
package art.arcane.iris.generation.subterrain;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.pack.schema.annotation.MaxNumber;
import art.arcane.iris.pack.schema.annotation.MinNumber;
import art.arcane.iris.pack.schema.annotation.RegistryListResource;
import art.arcane.iris.pack.schema.annotation.Snippet;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.volmlib.util.documentation.Description;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@NoArgsConstructor
@Accessors(chain = true)
@Snippet("subterrain-feature")
@Description("Bounded, deterministic underground rooms, passages, retained fluids and formations.")
public class IrisSubterrainFeature {
@Description("Unique stable feature definition id. Placement is derived from this id and the world seed.")
private String id = "";
@Description("Enable this feature definition.")
private boolean enabled = true;
@Description("Deliberate underground geometry family.")
private IrisSubterrainFamily family = IrisSubterrainFamily.CENOTE;
@RegistryListResource(IrisBiome.class)
@Description("Biome that owns occupied cells in this feature. Solid rims and surrounding terrain retain their biome.")
private String biome = "";
@Description("Inclusive absolute world Y bounds. The entire feature including seals must fit inside this band.")
private IrisRange worldYRange = new IrisRange(-48, 48);
@MinNumber(32)
@MaxNumber(8192)
@Description("Placement grid spacing in blocks. Each grid cell supplies at most one seed-derived candidate.")
private int spacing = 512;
@MinNumber(0)
@MaxNumber(1)
@Description("Probability that each placement grid cell contains this feature.")
private double probability = 0.35;
@MinNumber(16)
@MaxNumber(2048)
@Description("Passage or fault length in blocks. Tectonic faults require at least 200 blocks.")
private int length = 256;
@MinNumber(6)
@MaxNumber(256)
@Description("Cenote horizontal radius, or half-width of faults, tubes and terraces.")
private int radius = 32;
@MinNumber(8)
@MaxNumber(192)
@Description("Main room height in blocks.")
private int height = 32;
@Description("Retained fluid. When omitted, lava tubes use LAVA and cenotes and terraces use WATER. Faults remain dry.")
private IrisSubterrainFluid fluid;
@MinNumber(0)
@MaxNumber(32)
@Description("Depth of retained water or lava above the basin floor. Faults remain dry.")
private int fluidDepth = 4;
@MinNumber(0)
@MaxNumber(96)
@Description("Height of connected lava-tube hornito shafts above the tube vault.")
private int chimneyHeight = 16;
@MinNumber(2)
@MaxNumber(16)
@Description("Number of stepped travertine basins.")
private int terraceCount = 6;
@MinNumber(0)
@MaxNumber(128)
@Description("Grid distance between continuous floor-to-ceiling pillars. Zero disables pillars.")
private int pillarSpacing = 24;
@MinNumber(0)
@MaxNumber(0.4)
@Description("Fraction of available vault height occupied by each stalactite or stalagmite. Zero disables formations.")
private double formationFraction = 0.15;
@Description("Block state used for seals, pillars, rims, walkways and formations.")
private String solid = "minecraft:stone";
@Description("Priority between authored features. Higher values win; stable feature ids break ties.")
private int priority = 0;
}
@@ -0,0 +1,36 @@
package art.arcane.iris.generation.subterrain;
import com.google.gson.JsonParseException;
import com.google.gson.TypeAdapter;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
@JsonAdapter(IrisSubterrainFluid.Adapter.class)
public enum IrisSubterrainFluid {
WATER,
LAVA;
public static final class Adapter extends TypeAdapter<IrisSubterrainFluid> {
@Override
public void write(JsonWriter writer, IrisSubterrainFluid value) throws IOException {
writer.value(value.name());
}
@Override
public IrisSubterrainFluid read(JsonReader reader) throws IOException {
if (reader.peek() != JsonToken.STRING) {
throw new JsonParseException("Subterrain fluid must be WATER or LAVA");
}
String value = reader.nextString();
try {
return IrisSubterrainFluid.valueOf(value);
} catch (IllegalArgumentException error) {
throw new JsonParseException("Unknown subterrain fluid: " + value + "; expected WATER or LAVA", error);
}
}
}
}
@@ -0,0 +1,11 @@
package art.arcane.iris.generation.subterrain;
public record SubterrainBounds(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
public boolean contains(int x, int y, int z) {
return x >= minX && x <= maxX && y >= minY && y <= maxY && z >= minZ && z <= maxZ;
}
public boolean intersects(int lowX, int lowZ, int highX, int highZ) {
return minX <= highX && maxX >= lowX && minZ <= highZ && maxZ >= lowZ;
}
}
@@ -0,0 +1,29 @@
package art.arcane.iris.generation.subterrain;
public record SubterrainCell(Kind kind, String material, SubterrainRoom room) {
public static final SubterrainCell OUTSIDE = new SubterrainCell(Kind.OUTSIDE, "", null);
public boolean owned() {
return kind != Kind.OUTSIDE;
}
public boolean occupied() {
return kind == Kind.AIR || fluid();
}
public boolean solid() {
return kind == Kind.SOLID;
}
public boolean carve() {
return occupied();
}
public boolean fluid() {
return kind == Kind.WATER || kind == Kind.LAVA;
}
public enum Kind {
OUTSIDE, SOLID, AIR, WATER, LAVA
}
}
@@ -0,0 +1,265 @@
package art.arcane.iris.generation.subterrain;
import java.util.Objects;
public final class SubterrainPlan {
private final SubterrainPlanner.Definition definition;
private final int centerX;
private final int centerY;
private final int centerZ;
private final boolean rotated;
private final String id;
private final SubterrainBounds bounds;
SubterrainPlan(Options options) {
definition = Objects.requireNonNull(options.definition());
centerX = options.centerX();
centerY = options.centerY();
centerZ = options.centerZ();
rotated = options.rotated();
id = options.id();
int along = family() == IrisSubterrainFamily.CENOTE ? definition.radius() : definition.length() / 2;
int across = definition.radius();
int extentX = (rotated ? across : along) + 2;
int extentZ = (rotated ? along : across) + 2;
bounds = new SubterrainBounds(centerX - extentX, baseY() - 2, centerZ - extentZ,
centerX + extentX, baseY() + definition.height() + 2
+ (family() == IrisSubterrainFamily.LAVA_TUBE ? definition.chimneyHeight() : 0), centerZ + extentZ);
}
public String id() {
return id;
}
public IrisSubterrainFamily family() {
return definition.family();
}
public String biome() {
return definition.biome();
}
public int priority() {
return definition.priority();
}
public SubterrainBounds bounds() {
return bounds;
}
public int centerX() {
return centerX;
}
public int centerY() {
return centerY;
}
public int centerZ() {
return centerZ;
}
public SubterrainPosition anchor() {
int y = family() == IrisSubterrainFamily.LAVA_TUBE ? centerY : baseY() + definition.fluidDepth() + 2;
SubterrainPosition anchor = occupiedAnchor(centerX, y, centerZ);
if (anchor != null) {
return anchor;
}
if (family() == IrisSubterrainFamily.TRAVERTINE_TERRACES) {
int path = definition.radius() - 4;
anchor = occupiedAnchor(rotated ? centerX + path : centerX, y,
rotated ? centerZ : centerZ + path);
if (anchor != null) {
return anchor;
}
int firstTerrace = -definition.length() / 2 + 1;
anchor = occupiedAnchor(rotated ? centerX + path : centerX + firstTerrace, y,
rotated ? centerZ + firstTerrace : centerZ + path);
if (anchor != null) {
return anchor;
}
}
throw new IllegalStateException("Subterrain plan has no occupied anchor: " + id);
}
public SubterrainCell sample(int x, int y, int z) {
if (!bounds.contains(x, y, z)) {
return SubterrainCell.OUTSIDE;
}
int u = rotated ? z - centerZ : x - centerX;
int v = rotated ? x - centerX : z - centerZ;
Column column = switch (family()) {
case CENOTE -> cenote(u, v);
case TECTONIC_FAULT -> fault(u, v);
case LAVA_TUBE -> tube(u, v, y);
case TRAVERTINE_TERRACES -> terraces(u, v);
};
if (column == null || y < column.floor() - 2 || y > column.ceiling() + 2) {
return SubterrainCell.OUTSIDE;
}
SubterrainCell.Kind kind = classify(column, u, v, y);
int pathX = rotated ? centerX + column.pathOffset() : x;
int pathZ = rotated ? z : centerZ + column.pathOffset();
boolean passage = kind == SubterrainCell.Kind.AIR && column.pathDistance() <= 2.0;
SubterrainRoom room = new SubterrainRoom(id, biome(), family(), centerX, centerY, centerZ,
pathX, Math.max(column.floor() + 2, column.head() + 2), pathZ,
column.floor(), column.ceiling(), column.boundary(), column.head(), passage,
kind == SubterrainCell.Kind.SOLID, kind);
String material = switch (kind) {
case SOLID -> definition.solid();
case AIR -> "minecraft:cave_air";
case WATER -> "minecraft:water";
case LAVA -> "minecraft:lava";
case OUTSIDE -> "";
};
return new SubterrainCell(kind, material, room);
}
private SubterrainCell.Kind classify(Column column, int u, int v, int y) {
if (!column.interior() || y <= column.floor() || y >= column.ceiling() || column.rim()) {
return SubterrainCell.Kind.SOLID;
}
if (formation(column, u, v, y)) {
return SubterrainCell.Kind.SOLID;
}
if (y <= column.head()) {
return switch (definition.fluid()) {
case WATER -> SubterrainCell.Kind.WATER;
case LAVA -> SubterrainCell.Kind.LAVA;
};
}
return SubterrainCell.Kind.AIR;
}
private boolean formation(Column column, int u, int v, int y) {
if (column.pathDistance() <= 3 || column.boundary() < 4) {
return false;
}
int spacing = definition.pillarSpacing();
if (spacing > 0 && Math.abs(Math.floorMod(u + spacing / 2, spacing) - spacing / 2) <= 1
&& Math.abs(Math.floorMod(v + spacing / 2, spacing) - spacing / 2) <= 1) {
return true;
}
if (column.head() >= y) {
return false;
}
int cellU = Math.floorDiv(u, 12);
int cellV = Math.floorDiv(v, 12);
long hash = SubterrainPlanner.mix(definition.seedSalt() ^ cellU * 73428767L ^ cellV * 912931L);
int anchorU = cellU * 12 + 3 + (int) Math.floorMod(hash, 6);
int anchorV = cellV * 12 + 3 + (int) Math.floorMod(hash >>> 8, 6);
int distance = Math.max(Math.abs(u - anchorU), Math.abs(v - anchorV));
int height = (int) Math.floor((column.ceiling() - column.floor() - 1) * definition.formationFraction());
int taperHeight = Math.max(0, height - distance * Math.max(1, height / 3));
return distance <= 2 && taperHeight > 0
&& (y <= column.floor() + taperHeight || y >= column.ceiling() - taperHeight);
}
private Column cenote(int u, int v) {
double distance = Math.hypot(u, v);
int radius = definition.radius();
if (distance > radius + 2) {
return null;
}
double normalized = Math.min(1, distance / radius);
int ceiling = baseY() + Math.max(3, (int) Math.floor(definition.height() * Math.sqrt(1 - normalized * normalized)));
int head = baseY() + definition.fluidDepth();
boolean rim = definition.fluidDepth() > 0 && ceiling <= head + 2;
return new Column(baseY(), ceiling, head, distance < radius, rim,
radius - distance, Math.abs(v), 0);
}
private Column fault(int u, int v) {
int halfLength = definition.length() / 2;
int radius = definition.radius();
if (Math.abs(u) > halfLength + 2 || Math.abs(v) > radius + 2) {
return null;
}
int step = Math.max(2, radius / 4);
int shelf = Math.max(0, (Math.abs(v) - radius / 2) / step) * Math.max(2, definition.height() / 8);
return new Column(baseY() + shelf, baseY() + definition.height(), baseY() - 1,
Math.abs(u) < halfLength && Math.abs(v) < radius, false,
Math.min(halfLength - Math.abs(u), radius - Math.abs(v)), Math.abs(v), 0);
}
private Column tube(int u, int v, int y) {
int halfLength = definition.length() / 2;
int path = (int) Math.round(Math.sin(u * Math.PI / Math.max(1, halfLength)) * definition.radius() * 0.15);
int width = Math.max(6, (int) Math.floor(definition.radius() * 0.8));
int offset = v - path;
if (Math.abs(u) > halfLength + 2 || Math.abs(offset) > width + 2) {
return null;
}
double arch = Math.sqrt(Math.max(0, 1 - Math.pow(Math.min(1, Math.abs(offset) / (double) width), 2)));
int floor = centerY - Math.max(2, (int) Math.floor(definition.height() * 0.5 * arch));
int ceiling = centerY + Math.max(2, (int) Math.floor(definition.height() * 0.5 * arch));
int head = baseY() + definition.fluidDepth();
if (Math.abs(offset) >= width * 0.55) {
floor = Math.max(floor, head + 2);
}
int chimneyAlong = Math.min(Math.abs(u - halfLength / 2), Math.abs(u + halfLength / 2));
double chimneyRadius = Math.hypot(chimneyAlong, offset);
if (definition.chimneyHeight() > 0 && chimneyRadius <= 5) {
int tubeCeiling = ceiling;
if (chimneyRadius < 3) {
ceiling = baseY() + definition.height() + definition.chimneyHeight();
} else if (y >= tubeCeiling - 1) {
return new Column(tubeCeiling - 1, baseY() + definition.height() + definition.chimneyHeight(),
head, false, true, 5 - chimneyRadius, Math.abs(offset), path);
}
}
boolean rim = definition.fluidDepth() > 0 && floor >= head && floor < head + 2;
return new Column(floor, ceiling, head, Math.abs(u) < halfLength && Math.abs(offset) < width,
rim, Math.min(halfLength - Math.abs(u), width - Math.abs(offset)), Math.abs(offset), path);
}
private Column terraces(int u, int v) {
int halfLength = definition.length() / 2;
int radius = definition.radius();
if (Math.abs(u) > halfLength + 2 || Math.abs(v) > radius + 2) {
return null;
}
int segment = Math.max(4, definition.length() / definition.terraceCount());
int index = Math.max(0, Math.min(definition.terraceCount() - 1, Math.floorDiv(u + halfLength, segment)));
int rise = Math.max(1, (definition.height() / 3) / definition.terraceCount());
int floor = baseY() + index * rise;
int head = floor + definition.fluidDepth();
int local = Math.floorMod(u + halfLength, segment);
boolean rim = definition.fluidDepth() > 0 && (local <= 1 || local >= segment - 2 || Math.abs(v) >= radius - 2);
int ceiling = baseY() + definition.height();
if (rim) {
floor = head + 1;
}
double pathDistance = Math.abs(Math.abs(v) - (radius - 4));
if (pathDistance <= 2) {
floor = Math.max(floor, head + 1);
}
return new Column(floor, ceiling, head, Math.abs(u) < halfLength && Math.abs(v) < radius,
false, Math.min(halfLength - Math.abs(u), radius - Math.abs(v)), pathDistance,
v < 0 ? -radius + 4 : radius - 4);
}
private SubterrainPosition occupiedAnchor(int x, int preferredY, int z) {
if (sample(x, preferredY, z).occupied()) {
return new SubterrainPosition(x, preferredY, z);
}
for (int candidateY = bounds.minY(); candidateY <= bounds.maxY(); candidateY++) {
if (sample(x, candidateY, z).occupied()) {
return new SubterrainPosition(x, candidateY, z);
}
}
return null;
}
private int baseY() {
return centerY - definition.height() / 2;
}
record Options(SubterrainPlanner.Definition definition, int centerX, int centerY, int centerZ,
boolean rotated, String id) {
}
private record Column(int floor, int ceiling, int head, boolean interior, boolean rim,
double boundary, double pathDistance, int pathOffset) {
}
}
@@ -0,0 +1,253 @@
package art.arcane.iris.generation.subterrain;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
public final class SubterrainPlanner {
private final List<Definition> definitions;
private final long seed;
private final int minY;
private final int maxYExclusive;
private final ThreadLocal<CandidateTile> sampleTile = new ThreadLocal<>();
private final Predicate<SubterrainBounds> allowedBounds;
public SubterrainPlanner(Options options) {
this(options, bounds -> true);
}
public SubterrainPlanner(Options options, Predicate<SubterrainBounds> allowedBounds) {
Objects.requireNonNull(options);
this.allowedBounds = Objects.requireNonNull(allowedBounds);
seed = options.seed();
minY = options.minY();
maxYExclusive = options.maxYExclusive();
if (minY >= maxYExclusive) {
throw new IllegalArgumentException("Subterrain world height is empty");
}
if (options.features().size() > 64) {
throw new IllegalArgumentException("Subterrain supports at most 64 feature definitions");
}
List<Definition> loaded = new ArrayList<>();
Set<String> identifiers = new HashSet<>();
for (IrisSubterrainFeature feature : options.features()) {
if (feature == null || !feature.isEnabled()) {
continue;
}
Definition definition = Definition.from(feature);
if (!identifiers.add(definition.id())) {
throw new IllegalArgumentException("Duplicate subterrain id: " + definition.id());
}
loaded.add(definition);
}
definitions = List.copyOf(loaded);
}
private SubterrainPlanner(SubterrainPlanner source, Predicate<SubterrainBounds> allowedBounds) {
definitions = source.definitions;
seed = source.seed;
minY = source.minY;
maxYExclusive = source.maxYExclusive;
this.allowedBounds = source.allowedBounds.and(Objects.requireNonNull(allowedBounds));
}
public SubterrainPlanner restrictTo(Predicate<SubterrainBounds> allowedBounds) {
return new SubterrainPlanner(this, allowedBounds);
}
public boolean isEmpty() {
return definitions.isEmpty();
}
public List<SubterrainPlan> plansForBounds(int minX, int minZ, int maxX, int maxZ) {
if (minX > maxX || minZ > maxZ) {
throw new IllegalArgumentException("Subterrain query bounds are inverted");
}
List<SubterrainPlan> result = new ArrayList<>();
for (Definition definition : definitions) {
long firstX = Math.floorDiv(minX, definition.spacing()) - 1L;
long lastX = Math.floorDiv(maxX, definition.spacing()) + 1L;
long firstZ = Math.floorDiv(minZ, definition.spacing()) - 1L;
long lastZ = Math.floorDiv(maxZ, definition.spacing()) + 1L;
if ((lastX - firstX + 1L) * (lastZ - firstZ + 1L) > 65_536L) {
throw new IllegalArgumentException("Subterrain candidate query exceeds 65536 grid cells; use bounded search tiles");
}
for (long gridX = firstX; gridX <= lastX; gridX++) {
for (long gridZ = firstZ; gridZ <= lastZ; gridZ++) {
SubterrainPlan plan = create(definition, gridX, gridZ);
if (plan != null && plan.bounds().intersects(minX, minZ, maxX, maxZ)) {
result.add(plan);
}
}
}
}
result.sort(Comparator.comparingInt(SubterrainPlan::priority).reversed().thenComparing(SubterrainPlan::id));
return List.copyOf(result);
}
public SubterrainCell sample(int x, int y, int z) {
if (isEmpty() || y < minY || y >= maxYExclusive) {
return SubterrainCell.OUTSIDE;
}
int chunkX = Math.floorDiv(x, 16);
int chunkZ = Math.floorDiv(z, 16);
CandidateTile tile = sampleTile.get();
if (tile == null || tile.chunkX() != chunkX || tile.chunkZ() != chunkZ) {
long baseX = (long) chunkX * 16;
long baseZ = (long) chunkZ * 16;
tile = new CandidateTile(chunkX, chunkZ, plansForBounds(
(int) Math.max(Integer.MIN_VALUE, baseX - 1),
(int) Math.max(Integer.MIN_VALUE, baseZ - 1),
(int) Math.min(Integer.MAX_VALUE, baseX + 16),
(int) Math.min(Integer.MAX_VALUE, baseZ + 16)));
sampleTile.set(tile);
}
return sample(tile.plans(), x, y, z);
}
public SubterrainCell sample(List<SubterrainPlan> candidates, int x, int y, int z) {
SubterrainCell selected = raw(candidates, x, y, z);
if (!selected.fluid()) {
return selected;
}
if (exposed(raw(candidates, x - 1, y, z), selected)
|| exposed(raw(candidates, x + 1, y, z), selected)
|| exposed(raw(candidates, x, y, z - 1), selected)
|| exposed(raw(candidates, x, y, z + 1), selected)
|| exposed(raw(candidates, x, y - 1, z), selected)) {
return solid(selected);
}
return selected;
}
private SubterrainCell raw(List<SubterrainPlan> candidates, int x, int y, int z) {
SubterrainCell selected = SubterrainCell.OUTSIDE;
for (SubterrainPlan plan : candidates) {
SubterrainCell cell = plan.sample(x, y, z);
if (cell.solid()) {
return cell;
}
if (!selected.owned() && cell.owned()) {
selected = cell;
}
}
return selected;
}
private boolean exposed(SubterrainCell neighbor, SubterrainCell fluid) {
return !neighbor.solid() && neighbor.kind() != fluid.kind();
}
private SubterrainCell solid(SubterrainCell fluid) {
SubterrainRoom room = fluid.room();
SubterrainRoom sealed = new SubterrainRoom(room.featureId(), room.biome(), room.family(),
room.centerX(), room.centerY(), room.centerZ(), room.pathX(), room.pathY(), room.pathZ(),
room.floorY(), room.ceilingY(), room.boundaryDistance(), room.fluidHeadY(), false, true,
SubterrainCell.Kind.SOLID);
for (Definition definition : definitions) {
if (room.featureId().startsWith(definition.id() + ":")) {
return new SubterrainCell(SubterrainCell.Kind.SOLID, definition.solid(), sealed);
}
}
throw new IllegalStateException("Unknown subterrain fluid owner: " + room.featureId());
}
private SubterrainPlan create(Definition definition, long gridX, long gridZ) {
long value = mix(seed ^ definition.seedSalt() ^ mix(gridX) ^ Long.rotateLeft(mix(gridZ), 23));
if (unit(value) >= definition.probability()) {
return null;
}
int spacing = definition.spacing();
long x = gridX * spacing + spacing / 4L + Math.floorMod(mix(value + 1), Math.max(1, spacing / 2));
long z = gridZ * spacing + spacing / 4L + Math.floorMod(mix(value + 2), Math.max(1, spacing / 2));
int bottomMargin = definition.height() / 2 + 3;
int topMargin = definition.height() - definition.height() / 2 + 3
+ (definition.family() == IrisSubterrainFamily.LAVA_TUBE ? definition.chimneyHeight() : 0);
long low = (long) Math.max(minY, definition.minY()) + bottomMargin;
long high = (long) Math.min(maxYExclusive - 1, definition.maxY()) - topMargin;
if (low > high || x < Integer.MIN_VALUE + spacing || x > Integer.MAX_VALUE - spacing
|| z < Integer.MIN_VALUE + spacing || z > Integer.MAX_VALUE - spacing) {
return null;
}
int y = (int) (low + Math.floorMod(mix(value + 3), high - low + 1));
SubterrainPlan plan = new SubterrainPlan(new SubterrainPlan.Options(definition, (int) x, y, (int) z,
(value & 1L) != 0, definition.id() + ":" + Long.toUnsignedString(value, 16)));
return allowedBounds.test(plan.bounds()) ? plan : null;
}
static long mix(long value) {
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
return value ^ (value >>> 31);
}
private static double unit(long value) {
return (value >>> 11) * 0x1.0p-53;
}
public record Options(List<IrisSubterrainFeature> features, long seed, int minY, int maxYExclusive) {
public Options {
features = List.copyOf(Objects.requireNonNull(features));
}
}
private record CandidateTile(int chunkX, int chunkZ, List<SubterrainPlan> plans) {
}
record Definition(String id, IrisSubterrainFamily family, IrisSubterrainFluid fluid, String biome, int spacing, double probability,
int length, int radius, int height, int fluidDepth, int chimneyHeight, int terraceCount,
int pillarSpacing, double formationFraction, String solid, int priority,
int minY, int maxY, long seedSalt) {
static Definition from(IrisSubterrainFeature feature) {
String id = Objects.requireNonNull(feature.getId(), "Subterrain id").trim();
IrisSubterrainFamily family = Objects.requireNonNull(feature.getFamily(), "Subterrain family");
IrisSubterrainFluid fluid = Objects.requireNonNullElse(feature.getFluid(),
family == IrisSubterrainFamily.LAVA_TUBE ? IrisSubterrainFluid.LAVA : IrisSubterrainFluid.WATER);
int length = feature.getLength();
int radius = feature.getRadius();
int height = feature.getHeight();
int chimney = feature.getChimneyHeight();
int horizontalReach = family == IrisSubterrainFamily.CENOTE ? radius : Math.max(length / 2, radius);
if (!id.matches("[a-zA-Z0-9_/-]+") || length < 16 || length > 2048 || radius < 6 || radius > 256
|| height < 8 || height > 192 || chimney < 0 || chimney > 96
|| feature.getSpacing() < Math.max(32, 2 * horizontalReach + 8) || feature.getSpacing() > 8192
|| !Double.isFinite(feature.getProbability()) || feature.getProbability() < 0 || feature.getProbability() > 1
|| feature.getFluidDepth() < 0 || feature.getFluidDepth() > Math.min(32, height / 3)
|| feature.getTerraceCount() < 2 || feature.getTerraceCount() > 16
|| feature.getPillarSpacing() < 0 || feature.getPillarSpacing() > 128
|| !Double.isFinite(feature.getFormationFraction()) || feature.getFormationFraction() < 0
|| feature.getFormationFraction() > 0.4 || (family == IrisSubterrainFamily.TECTONIC_FAULT && length < 200)) {
throw new IllegalArgumentException("Invalid subterrain dimensions or placement settings: " + id);
}
if (feature.getWorldYRange() == null || !Double.isFinite(feature.getWorldYRange().getMin())
|| !Double.isFinite(feature.getWorldYRange().getMax())
|| feature.getWorldYRange().getMin() < Integer.MIN_VALUE
|| feature.getWorldYRange().getMax() > Integer.MAX_VALUE
|| feature.getWorldYRange().getMin() > feature.getWorldYRange().getMax()) {
throw new IllegalArgumentException("Invalid subterrain world Y range: " + id);
}
String solid = Objects.requireNonNull(feature.getSolid(), "Subterrain solid").trim();
if (!solid.matches("(?:[a-z0-9_.-]+:)?[a-z0-9_/.-]+(?:\\[[a-z0-9_=,.-]+\\])?")
|| solid.matches("(?:minecraft:)?(?:air|cave_air|void_air|water|lava)(?:\\[.*\\])?")) {
throw new IllegalArgumentException("Invalid subterrain solid block state: " + id);
}
String biome = Objects.requireNonNullElse(feature.getBiome(), "").trim();
if (!biome.isEmpty() && !biome.matches("[a-zA-Z0-9_/-]+")) {
throw new IllegalArgumentException("Invalid subterrain biome key: " + id);
}
long salt = 0xcbf29ce484222325L;
for (int i = 0; i < id.length(); i++) {
salt = (salt ^ id.charAt(i)) * 0x100000001b3L;
}
return new Definition(id, family, fluid, biome,
feature.getSpacing(), feature.getProbability(), length, radius, height, feature.getFluidDepth(),
chimney, feature.getTerraceCount(), feature.getPillarSpacing(), feature.getFormationFraction(),
solid, feature.getPriority(), (int) Math.ceil(feature.getWorldYRange().getMin()),
(int) Math.floor(feature.getWorldYRange().getMax()), salt);
}
}
}
@@ -0,0 +1,4 @@
package art.arcane.iris.generation.subterrain;
public record SubterrainPosition(int x, int y, int z) {
}
@@ -0,0 +1,84 @@
package art.arcane.iris.generation.subterrain;
import art.arcane.iris.generation.block.B;
import art.arcane.iris.generation.decoration.IrisProceduralBlocks;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.matter.MatterCavern;
import java.util.List;
import java.util.Objects;
public final class SubterrainRasterizer {
private SubterrainRasterizer() {
}
public static void validateMaterials(List<IrisSubterrainFeature> features) {
for (IrisSubterrainFeature feature : features) {
if (feature == null || !feature.isEnabled()) {
continue;
}
NativeBlockState state = B.getStateOrNull(feature.getSolid(), false);
if (!isRetainedSolid(state)) {
throw new IllegalArgumentException("Subterrain feature " + feature.getId()
+ " requires a full, opaque, dry vanilla solid block without gravity: " + feature.getSolid());
}
}
}
public static boolean isRetainedSolid(NativeBlockState state) {
return state != null && state.isSolid() && state.isOccluding() && !state.isFluid()
&& !state.isWaterLogged() && !state.isCustom() && !IrisProceduralBlocks.isGravityAffected(state);
}
public static void rasterize(SubterrainPlanner planner, int chunkX, int chunkZ, CellConsumer consumer) {
Objects.requireNonNull(planner);
Objects.requireNonNull(consumer);
int baseX = chunkX << 4;
int baseZ = chunkZ << 4;
List<SubterrainPlan> plans = planner.plansForBounds(baseX - 1, baseZ - 1, baseX + 16, baseZ + 16);
if (plans.isEmpty()) {
return;
}
int lowY = Integer.MAX_VALUE;
int highY = Integer.MIN_VALUE;
for (SubterrainPlan plan : plans) {
lowY = Math.min(lowY, plan.bounds().minY());
highY = Math.max(highY, plan.bounds().maxY());
}
for (int x = baseX; x < baseX + 16; x++) {
for (int z = baseZ; z < baseZ + 16; z++) {
for (int y = lowY; y <= highY; y++) {
SubterrainCell cell = planner.sample(plans, x, y, z);
if (cell.owned()) {
consumer.accept(x, y, z, cell);
}
}
}
}
}
public static MatterCavern cavern(SubterrainCell cell) {
if (!cell.carve()) {
return null;
}
byte liquid = switch (cell.kind()) {
case WATER -> 1;
case LAVA -> 2;
default -> 3;
};
return new MatterCavern(true, cell.room().biome(), liquid);
}
public static NativeBlockState state(SubterrainCell cell) {
return IrisProceduralBlocks.normalizeWaterlogging(B.getState(cell.material()), null, false);
}
public static boolean protectsPlacement(SubterrainCell cell) {
return cell != null && cell.owned() && (cell.solid() || cell.room().reservedPassage());
}
@FunctionalInterface
public interface CellConsumer {
void accept(int x, int worldY, int z, SubterrainCell cell);
}
}
@@ -0,0 +1,10 @@
package art.arcane.iris.generation.subterrain;
public record SubterrainRoom(String featureId, String biome, IrisSubterrainFamily family,
int centerX, int centerY, int centerZ, int pathX, int pathY, int pathZ,
int floorY, int ceilingY, double boundaryDistance, int fluidHeadY,
boolean reservedPassage, boolean reservedSolid, SubterrainCell.Kind occupancy) {
public int vaultHeight() {
return Math.max(0, ceilingY - floorY - 1);
}
}
@@ -0,0 +1,95 @@
package art.arcane.iris.generation.subterrain;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public final class SubterrainSurfaceMaterials {
private final SubterrainPlanner planner;
private final List<SubterrainPlan> plans;
private final IrisDimension dimension;
private final IrisComplex complex;
private final IrisData data;
private final RNG random;
private final int minimumY;
private final Map<String, IrisBiome> biomes = new HashMap<>();
public SubterrainSurfaceMaterials(SubterrainPlanner planner, List<SubterrainPlan> plans,
IrisDimension dimension, IrisComplex complex, IrisData data,
RNG random, int minimumY) {
this.planner = planner;
this.plans = plans;
this.dimension = dimension;
this.complex = complex;
this.data = data;
this.random = random;
this.minimumY = minimumY;
}
public NativeBlockState state(int x, int worldY, int z, SubterrainCell cell) {
NativeBlockState fallback = SubterrainRasterizer.state(cell);
if (!cell.solid() || cell.room().biome().isEmpty()) {
return fallback;
}
Surface surface = surface(x, worldY, z, cell);
if (surface == Surface.INTERIOR) {
return fallback;
}
String key = cell.room().biome();
if (!biomes.containsKey(key)) {
biomes.put(key, data.getBiomeLoader().load(key));
}
IrisBiome biome = biomes.get(key);
if (biome == null) {
return fallback;
}
NativeBlockState candidate;
if (surface == Surface.WALL) {
candidate = biome.getWall().get(random, x, worldY - minimumY, z, data);
} else {
KList<NativeBlockState> layers = surface == Surface.FLOOR
? biome.generateLayers(dimension, x, z, random, 1, worldY + 1 - minimumY, data, complex)
: biome.generateCeilingLayers(dimension, x, z, random, 1, worldY - 1 - minimumY, data, complex);
candidate = layers.isEmpty() ? null : layers.getFirst();
}
return SubterrainRasterizer.isRetainedSolid(candidate) ? candidate : fallback;
}
public Surface surface(int x, int worldY, int z, SubterrainCell cell) {
if (!cell.solid()) {
return Surface.INTERIOR;
}
if (sameRoom(planner.sample(plans, x, worldY + 1, z), cell)) {
return Surface.FLOOR;
}
if (sameRoom(planner.sample(plans, x, worldY - 1, z), cell)) {
return Surface.CEILING;
}
if (sameRoom(planner.sample(plans, x - 1, worldY, z), cell)
|| sameRoom(planner.sample(plans, x + 1, worldY, z), cell)
|| sameRoom(planner.sample(plans, x, worldY, z - 1), cell)
|| sameRoom(planner.sample(plans, x, worldY, z + 1), cell)) {
return Surface.WALL;
}
return Surface.INTERIOR;
}
private static boolean sameRoom(SubterrainCell neighbor, SubterrainCell cell) {
return neighbor.occupied() && neighbor.room().featureId().equals(cell.room().featureId());
}
public enum Surface {
FLOOR,
CEILING,
WALL,
INTERIOR
}
}
@@ -18,6 +18,7 @@
package art.arcane.iris.generation.terrain; package art.arcane.iris.generation.terrain;
import art.arcane.iris.world.entity.IrisSpawnGroup;
import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.biome.IrisBiomeCustom; import art.arcane.iris.generation.biome.IrisBiomeCustom;
import art.arcane.iris.generation.biome.IrisCustomBiomeAliasResolver; import art.arcane.iris.generation.biome.IrisCustomBiomeAliasResolver;
@@ -30,6 +31,7 @@ import art.arcane.iris.generation.decoration.IrisOreGenerator;
import art.arcane.iris.generation.decoration.IrisOreBands; import art.arcane.iris.generation.decoration.IrisOreBands;
import art.arcane.iris.generation.decoration.tree.IrisTreeSettings; import art.arcane.iris.generation.decoration.tree.IrisTreeSettings;
import art.arcane.iris.generation.hydrology.IrisDeepFluidConfig; import art.arcane.iris.generation.hydrology.IrisDeepFluidConfig;
import art.arcane.iris.generation.subterrain.IrisSubterrainFeature;
import art.arcane.iris.generation.hydrology.IrisHydrology; import art.arcane.iris.generation.hydrology.IrisHydrology;
import art.arcane.iris.generation.hydrology.IrisRiverHydrology; import art.arcane.iris.generation.hydrology.IrisRiverHydrology;
import art.arcane.iris.generation.hydrology.IrisRiverPolicy; import art.arcane.iris.generation.hydrology.IrisRiverPolicy;
@@ -86,6 +88,7 @@ import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.json.JSONArray; import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject; import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.volmlib.util.mantle.flag.MantleFlag; import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import art.arcane.volmlib.util.math.Position2; import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.noise.CNG; import art.arcane.volmlib.util.noise.CNG;
@@ -143,6 +146,7 @@ public class IrisDimension extends IrisRegistrant {
private final transient AtomicCache<Double> sinr = new AtomicCache<>(); private final transient AtomicCache<Double> sinr = new AtomicCache<>();
private final transient AtomicCache<Double> cosr = new AtomicCache<>(); private final transient AtomicCache<Double> cosr = new AtomicCache<>();
private final transient AtomicCache<Double> rad = new AtomicCache<>(); private final transient AtomicCache<Double> rad = new AtomicCache<>();
private final transient AtomicCache<Boolean> undergroundSpawnTables = new AtomicCache<>();
private final transient AtomicCache<Boolean> featuresUsed = new AtomicCache<>(); private final transient AtomicCache<Boolean> featuresUsed = new AtomicCache<>();
private final transient AtomicCache<Map<String, IrisDimensionCarvingEntry>> carvingEntryIndex = new AtomicCache<>(); private final transient AtomicCache<Map<String, IrisDimensionCarvingEntry>> carvingEntryIndex = new AtomicCache<>();
private final transient AtomicCache<KList<IrisOreGenerator>> surfaceOreCache = new AtomicCache<>(); private final transient AtomicCache<KList<IrisOreGenerator>> surfaceOreCache = new AtomicCache<>();
@@ -222,6 +226,9 @@ public class IrisDimension extends IrisRegistrant {
@ArrayType(type = IrisDimensionCarvingEntry.class, min = 1) @ArrayType(type = IrisDimensionCarvingEntry.class, min = 1)
@Description("Dimension-level cave biome carving overrides with absolute world Y ranges") @Description("Dimension-level cave biome carving overrides with absolute world Y ranges")
private KList<IrisDimensionCarvingEntry> carving = new KList<>(); private KList<IrisDimensionCarvingEntry> carving = new KList<>();
@ArrayType(type = IrisSubterrainFeature.class, min = 1)
@Description("Deterministic bounded underground features with owned geometry, biome, fluids and room formations.")
private KList<IrisSubterrainFeature> subterrainFeatures = new KList<>();
@Description("Profile-driven 3D cave configuration") @Description("Profile-driven 3D cave configuration")
private IrisCaveProfile caveProfile = new IrisCaveProfile(); private IrisCaveProfile caveProfile = new IrisCaveProfile();
@Description("Dimension-owned surface, underground, grotto, and deep-fluid hydrology configuration.") @Description("Dimension-owned surface, underground, grotto, and deep-fluid hydrology configuration.")
@@ -733,6 +740,14 @@ public class IrisDimension extends IrisRegistrant {
} }
} }
if (getSubterrainFeatures() != null) {
for (IrisSubterrainFeature feature : getSubterrainFeatures()) {
if (feature != null && feature.isEnabled()) {
addReachableBiomeKey(pending, feature.getBiome());
}
}
}
Set<String> visited = new HashSet<>(); Set<String> visited = new HashSet<>();
Map<String, IrisDimensionCarvingEntry> carvingEntryIndex = getCarvingEntryIndex(); Map<String, IrisDimensionCarvingEntry> carvingEntryIndex = getCarvingEntryIndex();
while (!pending.isEmpty()) { while (!pending.isEmpty()) {
@@ -1187,6 +1202,44 @@ public class IrisDimension extends IrisRegistrant {
return new IrisDimensionType(getBaseDimension(), options, getLogicalHeight(), getMaxHeight() - getMinHeight(), getMinHeight()); return new IrisDimensionType(getBaseDimension(), options, getLogicalHeight(), getMaxHeight() - getMinHeight(), getMinHeight());
} }
public boolean hasUndergroundSpawners(DataProvider provider) {
return undergroundSpawnTables.aquire(() -> {
IrisData data = provider.getData();
if (hasCaveSpawners(data, entitySpawners)) {
return true;
}
for (IrisRegion region : getAllRegions(provider)) {
if (region != null && hasCaveSpawners(data, region.getEntitySpawners())) {
return true;
}
}
for (IrisBiome biome : getReachableBiomes(provider)) {
if (biome != null && hasCaveSpawners(data, biome.getEntitySpawners())) {
return true;
}
}
return false;
});
}
private static boolean hasCaveSpawners(IrisData data, KList<String> keys) {
if (keys == null || keys.isEmpty() || data.getSpawnerLoader() == null) {
return false;
}
KList<IrisSpawner> spawners = data.getSpawnerLoader().loadAll(keys);
for (IrisSpawner spawner : spawners) {
if (spawner != null && !spawner.isCompatExcluded() && spawner.getGroup() == IrisSpawnGroup.CAVE) {
return true;
}
}
return false;
}
public boolean allowsSubterrainFeatures() {
return useMantle && carvingEnabled && mode != null && mode.getType() == IrisDimensionModeType.OVERWORLD
&& !disabledComponents.contains(ReservedFlag.CARVED);
}
public boolean hasUpperDimension() { public boolean hasUpperDimension() {
return upperDimension != null && !upperDimension.isEmpty() && !upperDimension.equalsIgnoreCase("none"); return upperDimension != null && !upperDimension.isEmpty() && !upperDimension.equalsIgnoreCase("none");
} }
@@ -40,7 +40,7 @@ public final class IrisDimensionCarvingResolver {
if (root == null) { if (root == null) {
return null; return null;
} }
IrisDimensionCarvingEntry resolved = resolveBoundFromRoot(engine, root, worldX, worldZ, data, resolvedState); IrisDimensionCarvingEntry resolved = resolveBoundFromRoot(engine, root, worldX, worldY, worldZ, data, resolvedState);
return resolved == null ? null : resolveBoundEntryBiome(data, resolved, resolvedState); return resolved == null ? null : resolveBoundEntryBiome(data, resolved, resolvedState);
} }
@@ -54,14 +54,14 @@ public final class IrisDimensionCarvingResolver {
return resolveBoundRootEntry(engine, worldY, data, resolvedState); return resolveBoundRootEntry(engine, worldY, data, resolvedState);
} }
public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ) { public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldY, int worldZ) {
return resolveFromRoot(engine, rootEntry, worldX, worldZ, threadState()); return resolveFromRoot(engine, rootEntry, worldX, worldY, worldZ, threadState());
} }
public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ, State state) { public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldY, int worldZ, State state) {
State resolvedState = state == null ? threadState() : state; State resolvedState = state == null ? threadState() : state;
IrisData data = resolvedState.bind(engine); IrisData data = resolvedState.bind(engine);
return resolveBoundFromRoot(engine, rootEntry, worldX, worldZ, data, resolvedState); return resolveBoundFromRoot(engine, rootEntry, worldX, worldY, worldZ, data, resolvedState);
} }
public static IrisBiome resolveEntryBiome(Engine engine, IrisDimensionCarvingEntry entry) { public static IrisBiome resolveEntryBiome(Engine engine, IrisDimensionCarvingEntry entry) {
@@ -109,7 +109,7 @@ public final class IrisDimensionCarvingResolver {
return resolved; return resolved;
} }
private static IrisDimensionCarvingEntry resolveBoundFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ, IrisData data, State resolvedState) { private static IrisDimensionCarvingEntry resolveBoundFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldY, int worldZ, IrisData data, State resolvedState) {
if (rootEntry == null) { if (rootEntry == null) {
return null; return null;
} }
@@ -127,7 +127,7 @@ public final class IrisDimensionCarvingResolver {
ColumnResolutions columns = resolvedState.columns; ColumnResolutions columns = resolvedState.columns;
int slot = ((worldX & 31) << 5) | (worldZ & 31); int slot = ((worldX & 31) << 5) | (worldZ & 31);
long coordinate = ((long) worldX << 32) | (worldZ & 0xffffffffL); long coordinate = ((long) worldX << 32) | (worldZ & 0xffffffffL);
if (columns != null && columns.roots[slot] == rootEntry && columns.coordinates[slot] == coordinate) { if (columns != null && columns.roots[slot] == rootEntry && columns.coordinates[slot] == coordinate && columns.worldYs[slot] == worldY) {
return columns.entries[slot]; return columns.entries[slot];
} }
@@ -138,7 +138,8 @@ public final class IrisDimensionCarvingResolver {
while (depth > 0) { while (depth > 0) {
cacheable &= fixedChildStyle(current); cacheable &= fixedChildStyle(current);
IrisDimensionCarvingEntry selected = selectChild(engine, data, current, worldX, worldZ, entryIndex, resolvedState); IrisDimensionCarvingEntry selected = selectChild(engine, data, current, worldX, worldZ, entryIndex, resolvedState);
if (selected == null || selected == current) { if (selected == null || selected == current || selected.getWorldYRange() != null
&& !selected.getWorldYRange().contains(worldY)) {
break; break;
} }
@@ -156,6 +157,7 @@ public final class IrisDimensionCarvingResolver {
resolvedState.columns = columns; resolvedState.columns = columns;
} }
columns.coordinates[slot] = coordinate; columns.coordinates[slot] = coordinate;
columns.worldYs[slot] = worldY;
columns.entries[slot] = current; columns.entries[slot] = current;
columns.roots[slot] = rootEntry; columns.roots[slot] = rootEntry;
} }
@@ -332,7 +334,7 @@ public final class IrisDimensionCarvingResolver {
if (root == null) { if (root == null) {
return null; return null;
} }
IrisDimensionCarvingEntry resolved = resolveBoundFromRoot(engine, root, worldX, worldZ, data, state); IrisDimensionCarvingEntry resolved = resolveBoundFromRoot(engine, root, worldX, worldY, worldZ, data, state);
return resolved == null ? null : resolveBoundEntryBiome(data, resolved, state); return resolved == null ? null : resolveBoundEntryBiome(data, resolved, state);
} }
} }
@@ -399,6 +401,7 @@ public final class IrisDimensionCarvingResolver {
private static final class ColumnResolutions { private static final class ColumnResolutions {
private final long[] coordinates = new long[1024]; private final long[] coordinates = new long[1024];
private final int[] worldYs = new int[1024];
private final IrisDimensionCarvingEntry[] roots = new IrisDimensionCarvingEntry[1024]; private final IrisDimensionCarvingEntry[] roots = new IrisDimensionCarvingEntry[1024];
private final IrisDimensionCarvingEntry[] entries = new IrisDimensionCarvingEntry[1024]; private final IrisDimensionCarvingEntry[] entries = new IrisDimensionCarvingEntry[1024];
} }
@@ -538,6 +538,10 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
if (engine == null || engine.isClosed() || engine.getComplex() == null) { if (engine == null || engine.isClosed() || engine.getComplex() == null) {
return false; return false;
} }
if (engine.getDimension().allowsSubterrainFeatures()
&& engine.getDimension().getSubterrainFeatures() != null && !engine.getDimension().getSubterrainFeatures().isEmpty()) {
return false;
}
int worldMinHeight = engine.getWorld().minHeight(); int worldMinHeight = engine.getWorld().minHeight();
int internalY = (quartY << 2) - worldMinHeight; int internalY = (quartY << 2) - worldMinHeight;
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40); int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
@@ -888,6 +892,10 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
int worldMinHeight = engine.getWorld().minHeight(); int worldMinHeight = engine.getWorld().minHeight();
int internalY = blockY - worldMinHeight; int internalY = blockY - worldMinHeight;
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40); int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
IrisBiome featureBiome = engine.getSubterrainBiome(blockX, internalY, blockZ);
if (featureBiome != null) {
return createBiomeResolution(featureBiome, true, engine.getDimension(), blockX, blockY, blockZ);
}
DimensionStackLayout.Layer stackLayer = includeDimensionStack DimensionStackLayout.Layer stackLayer = includeDimensionStack
? stackLayout == null ? stackLayout == null
? resolveDimensionStackLayer(engine, blockX, internalY, blockZ) ? resolveDimensionStackLayer(engine, blockX, internalY, blockZ)
@@ -337,7 +337,14 @@ public class IrisObject extends IrisRegistrant {
public int place(int x, int yv, int z, IObjectPlacer oplacer, IrisObjectPlacement config, RNG rng, BiConsumer<BlockPosition, NativeBlockState> listener, CarveResult c, IrisData rdata) { public int place(int x, int yv, int z, IObjectPlacer oplacer, IrisObjectPlacement config, RNG rng, BiConsumer<BlockPosition, NativeBlockState> listener, CarveResult c, IrisData rdata) {
IrisObject placedObject = config.isSmartBore() ? IrisObjectShaping.smartBoredVariant(this) : this; IrisObject placedObject = config.isSmartBore() ? IrisObjectShaping.smartBoredVariant(this) : this;
return new IrisObjectPlacementRunner(placedObject).place(x, yv, z, oplacer, config, rng, listener, c, rdata); return new IrisObjectPlacementRunner(placedObject, IrisObjectPlacementAlignment.CENTER).place(x, yv, z, oplacer, config, rng, listener, c, rdata);
}
public int placeOnFloor(int x, int firstAirY, int z, IObjectPlacer placer, IrisObjectPlacement config,
RNG rng, BiConsumer<BlockPosition, NativeBlockState> listener, IrisData data) {
IrisObject placedObject = config.isSmartBore() ? IrisObjectShaping.smartBoredVariant(this) : this;
return new IrisObjectPlacementRunner(placedObject, IrisObjectPlacementAlignment.FLOOR)
.place(x, firstAirY, z, placer, config, rng, listener, null, data);
} }
KList<IrisBlockVector> getSurfaceSupportOffsets() { KList<IrisBlockVector> getSurfaceSupportOffsets() {
@@ -0,0 +1,6 @@
package art.arcane.iris.structure.object;
public enum IrisObjectPlacementAlignment {
CENTER,
FLOOR
}
@@ -68,9 +68,11 @@ final class IrisObjectPlacementRunner {
private static final ConcurrentHashMap<String, Long> IMPLAUSIBLE_BEDROCK_WARNS = new ConcurrentHashMap<>(); private static final ConcurrentHashMap<String, Long> IMPLAUSIBLE_BEDROCK_WARNS = new ConcurrentHashMap<>();
private final IrisObject self; private final IrisObject self;
private final IrisObjectPlacementAlignment alignment;
IrisObjectPlacementRunner(IrisObject self) { IrisObjectPlacementRunner(IrisObject self, IrisObjectPlacementAlignment alignment) {
this.self = self; this.self = self;
this.alignment = Objects.requireNonNull(alignment);
} }
int place(int x, int yv, int z, IObjectPlacer oplacer, IrisObjectPlacement config, RNG rng, BiConsumer<BlockPosition, NativeBlockState> listener, CarveResult c, IrisData rdata) { int place(int x, int yv, int z, IObjectPlacer oplacer, IrisObjectPlacement config, RNG rng, BiConsumer<BlockPosition, NativeBlockState> listener, CarveResult c, IrisData rdata) {
@@ -169,7 +171,19 @@ final class IrisObjectPlacementRunner {
TransformedBounds placementBounds = transformedBounds(spin, translating, translateOffset, ceilingHang, warpMargin); TransformedBounds placementBounds = transformedBounds(spin, translating, translateOffset, ceilingHang, warpMargin);
boolean bail = false; boolean bail = false;
if (config.isFromBottom()) { if (alignment == IrisObjectPlacementAlignment.FLOOR) {
if (yv < 0 || ceilingHang || rawStructurePiece) {
throw new IllegalArgumentException("Floor alignment requires an explicit floor Y and a floor placement mode.");
}
int minimumY = minimumOccupiedY(spin);
if (minimumY == Integer.MAX_VALUE) {
return -1;
}
y = yv - minimumY;
if (!config.isForcePlace() && shouldBailForCarvingAnchor(placer, config, x, yv, z)) {
bail = true;
}
} else if (config.isFromBottom()) {
// todo Convert this to a dedicated mode. // todo Convert this to a dedicated mode.
y = (self.getH() + 1) + rty; y = (self.getH() + 1) + rty;
if (!config.isForcePlace()) { if (!config.isForcePlace()) {
@@ -290,7 +304,7 @@ final class IrisObjectPlacementRunner {
} }
} }
if (yv >= 0 && config.isBottom() && !rawStructurePiece) { if (yv >= 0 && config.isBottom() && !rawStructurePiece && alignment != IrisObjectPlacementAlignment.FLOOR) {
y += Math.floorDiv(self.h, 2); y += Math.floorDiv(self.h, 2);
CarvingMode carvingMode = config.getCarvingSupport(); CarvingMode carvingMode = config.getCarvingSupport();
if (!config.isForcePlace() && !carvingMode.equals(CarvingMode.CARVING_ONLY)) { if (!config.isForcePlace() && !carvingMode.equals(CarvingMode.CARVING_ONLY)) {
@@ -608,9 +622,7 @@ final class IrisObjectPlacementRunner {
continue; continue;
} }
if (waterlogCandidate && IrisProceduralBlocks.hasProperty(data, "waterlogged") && shouldAutoWaterlogBlock(placer, config, yv, xx, yy, zz)) { data = IrisProceduralBlocks.normalizeWaterlogging(data, placer.get(xx, yy, zz), waterlogCandidate);
data = data.withProperty("waterlogged", "true");
}
if (!rawStructurePiece && B.isVineBlock(data)) { if (!rawStructurePiece && B.isVineBlock(data)) {
data = attachVineFaces(placer, data, xx, yy, zz); data = attachVineFaces(placer, data, xx, yy, zz);
@@ -810,7 +822,7 @@ final class IrisObjectPlacementRunner {
} }
} }
} }
placer.set(xx, j, zz, d); placer.set(xx, j, zz, IrisProceduralBlocks.normalizeWaterlogging(d, placer.get(xx, j, zz), waterlogCandidate));
} }
} else { } else {
int scan = 0; int scan = 0;
@@ -832,7 +844,7 @@ final class IrisObjectPlacementRunner {
} }
} }
} }
placer.set(xx, j, zz, d); placer.set(xx, j, zz, IrisProceduralBlocks.normalizeWaterlogging(d, placer.get(xx, j, zz), waterlogCandidate));
} }
} }
continue; continue;
@@ -840,9 +852,7 @@ final class IrisObjectPlacementRunner {
int highest = placer.getHighest(xx, zz, self.getLoader(), true); int highest = placer.getHighest(xx, zz, self.getLoader(), true);
if (IrisProceduralBlocks.hasProperty(d, "waterlogged") && shouldAutoWaterlogBlock(placer, config, yv, xx, highest, zz)) {
d = d.withProperty("waterlogged", "true");
}
int lowerBound = highest - 1; int lowerBound = highest - 1;
if (settings != null) { if (settings != null) {
@@ -882,7 +892,7 @@ final class IrisObjectPlacementRunner {
if (B.isVineBlock(d)) { if (B.isVineBlock(d)) {
d = attachVineFaces(placer, d, xx, j, zz); d = attachVineFaces(placer, d, xx, j, zz);
} }
placer.set(xx, j, zz, d); placer.set(xx, j, zz, IrisProceduralBlocks.normalizeWaterlogging(d, placer.get(xx, j, zz), waterlogCandidate));
} }
} }
@@ -940,6 +950,26 @@ final class IrisObjectPlacementRunner {
return offsets; return offsets;
} }
private int minimumOccupiedY(SpinKernel spin) {
int minimumY = Integer.MAX_VALUE;
self.readLock.lock();
try {
VectorMap<NativeBlockState>.Cursor cursor = self.blocks.cursor();
while (cursor.next()) {
NativeBlockState state = cursor.value();
if (state == null || state.isAir()) {
continue;
}
IrisBlockVector position = cursor.key().clone();
spin.rotate(position);
minimumY = Math.min(minimumY, (int) Math.round(position.getY()));
}
} finally {
self.readLock.unlock();
}
return minimumY;
}
private TransformedBounds transformedBounds(SpinKernel spin, boolean translating, IrisBlockVector translateOffset, private TransformedBounds transformedBounds(SpinKernel spin, boolean translating, IrisBlockVector translateOffset,
boolean ceilingHang, int margin) { boolean ceilingHang, int margin) {
int sourceMinX = -self.getCenter().getBlockX(); int sourceMinX = -self.getCenter().getBlockX();
@@ -1218,23 +1248,6 @@ final class IrisObjectPlacementRunner {
|| placer.isCarved(x, y - 3, z); || placer.isCarved(x, y - 3, z);
} }
private boolean shouldAutoWaterlogBlock(IObjectPlacer placer, IrisObjectPlacement placement, int yv, int x, int y, int z) {
if (!(placement.isWaterloggable() || placement.isUnderwater())) {
return false;
}
if (yv >= 0 && placement.getCarvingSupport().equals(CarvingMode.CARVING_ONLY)) {
return false;
}
NativeBlockState existing = placer.get(x, y, z);
if (existing == null) {
return false;
}
return B.isWater(existing) || B.isWaterLogged(existing);
}
private static NativeBlockState attachVineFaces(IObjectPlacer placer, NativeBlockState data, int x, int y, int z) { private static NativeBlockState attachVineFaces(IObjectPlacer placer, NativeBlockState data, int x, int y, int z) {
NativeBlockState result = data; NativeBlockState result = data;
for (String face : IrisProceduralBlocks.FACE_PROPERTIES) { for (String face : IrisProceduralBlocks.FACE_PROPERTIES) {
@@ -124,6 +124,22 @@ public class IrisEntitySpawn implements Rarity {
return spawned; return spawned;
} }
public int spawn(Engine engine, Location candidate, RNG random, int remainingCapacity, SpawnContext context) {
if (remainingCapacity <= 0) {
return 0;
}
int count = Math.min(remainingCapacity, LootResolver.inclusive(random, minSpawns, maxSpawns));
long batchSeed = random.getSeed();
int spawned = 0;
for (int ordinal = 0; ordinal < count; ordinal++) {
if (lightAllowed(candidate, context)
&& spawn100(engine, candidate, EntitySpawnSeed.entity(batchSeed, ordinal), false) != null) {
spawned++;
}
}
return spawned;
}
public static Integer selectSurfaceSpawnY(IrisSpawnGroup group, IrisSurface surface, public static Integer selectSurfaceSpawnY(IrisSpawnGroup group, IrisSurface surface,
int floor, int top, RNG rng) { int floor, int top, RNG rng) {
if (group == IrisSpawnGroup.CAVE) { if (group == IrisSpawnGroup.CAVE) {
@@ -206,7 +222,7 @@ public class IrisEntitySpawn implements Rarity {
return selectCaveSpawnLocation(markers, chunk.getWorld(), rng); return selectCaveSpawnLocation(markers, chunk.getWorld(), rng);
} }
static Location findLiveCaveSpawnLocation(Chunk chunk, RNG rng, IrisSurface surface) { public static Location findLiveCaveSpawnLocation(Chunk chunk, RNG rng, IrisSurface surface) {
boolean fluid = surface.isFluid(); boolean fluid = surface.isFluid();
World world = chunk.getWorld(); World world = chunk.getWorld();
int minimumY = world.getMinHeight() + 1; int minimumY = world.getMinHeight() + 1;
@@ -306,6 +306,10 @@ public final class GenerationSemanticCapture {
snapshot = IrisDimensionCarvingResolver.snapshot(engine); snapshot = IrisDimensionCarvingResolver.snapshot(engine);
minimumY = engine.getWorld().minHeight(); minimumY = engine.getWorld().minHeight();
} }
IrisBiome featureBiome = engine.getSubterrainBiome(x, y, z);
if (featureBiome != null) {
return featureBiome;
}
IrisBiome configured = snapshot.resolveBiome(x, y + minimumY, z); IrisBiome configured = snapshot.resolveBiome(x, y + minimumY, z);
if (configured != null) { if (configured != null) {
return configured; return configured;
@@ -2,6 +2,8 @@ package art.arcane.iris.world.history;
import art.arcane.iris.generation.runtime.IrisEngine; import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.locator.SubterrainLocator;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.structure.placement.IrisStructureLocator; import art.arcane.iris.structure.placement.IrisStructureLocator;
import art.arcane.iris.generation.hydrology.HydrologyFeatureQuery; import art.arcane.iris.generation.hydrology.HydrologyFeatureQuery;
import art.arcane.iris.generation.hydrology.HydrologyFeatureRef; import art.arcane.iris.generation.hydrology.HydrologyFeatureRef;
@@ -9,6 +11,8 @@ import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime; import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
import java.math.BigInteger; import java.math.BigInteger;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.OptionalLong; import java.util.OptionalLong;
@@ -19,6 +23,57 @@ public final class GenerationSemanticQueries {
private GenerationSemanticQueries() { private GenerationSemanticQueries() {
} }
public static Optional<SubterrainLocator.Result> nearestSubterrain(
Engine engine, SubterrainLocator.Query query, int x, int worldY, int z, int maximumDistance) {
Engine requiredEngine = Objects.requireNonNull(engine, "engine");
Objects.requireNonNull(query, "query");
GenerationHistoryRuntimeRouter router = requiredEngine instanceof IrisEngine irisEngine
? irisEngine.getGenerationHistoryRuntimeRouter().orElse(null) : null;
if (router == null) {
return SubterrainLocator.nearest(requiredEngine, query, x, worldY, z, maximumDistance);
}
GenerationHistory history = router.history();
SubterrainLocator.Result nearest = null;
try {
for (GenerationActivation activation : history.manifest().activations()) {
if (history.manifest().pendingActivation().map(pending ->
pending.activationId() == activation.activationId()).orElse(false)) {
continue;
}
SubterrainPlanner planner = router.biomes().subterrainPlanner(activation.activationId());
SubterrainLocator.Result candidate = SubterrainLocator.nearest(planner, query, x, worldY, z,
maximumDistance, result -> allowsSubterrainPrediction(requiredEngine, history,
activation.activationId(), result)).orElse(null);
if (candidate != null && (nearest == null || distanceSquared(x, worldY, z, candidate)
.compareTo(distanceSquared(x, worldY, z, nearest)) < 0)) {
nearest = candidate;
}
}
} catch (IOException failure) {
throw new UncheckedIOException("Unable to locate saved subterrain features", failure);
}
return Optional.ofNullable(nearest);
}
private static boolean allowsSubterrainPrediction(Engine engine, GenerationHistory history,
long activationId, SubterrainLocator.Result result) {
int chunkX = Math.floorDiv(result.x(), 16);
int chunkZ = Math.floorDiv(result.z(), 16);
GenerationActivation owner = history.resolveActivation(chunkX, chunkZ);
if (owner.activationId() != activationId) {
return false;
}
if (history.isActiveUnowned(chunkX, chunkZ)) {
return engine.getComplex().allowsNewDiscreteContentAt(result.x(), result.z());
}
return true;
}
private static BigInteger distanceSquared(int x, int y, int z, SubterrainLocator.Result result) {
BigInteger deltaY = BigInteger.valueOf((long) y - result.y());
return horizontalDistanceSquared(x, z, result.x(), result.z()).add(deltaY.multiply(deltaY));
}
public static IrisStructureLocator.LocateResult nearestStructure( public static IrisStructureLocator.LocateResult nearestStructure(
Engine engine, Engine engine,
String key, String key,
@@ -3,6 +3,7 @@ package art.arcane.iris.world.history;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.terrain.IrisRegion; import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@@ -30,9 +31,12 @@ public final class SavedBiomeCapture {
stage.chunkX(), stage.chunkZ(), activationId, minimumY, height)); stage.chunkX(), stage.chunkZ(), activationId, minimumY, height));
TransitionGenerationPlan transition = engine.getComplex().getTransitionGenerationPlan(); TransitionGenerationPlan transition = engine.getComplex().getTransitionGenerationPlan();
Map<Long, Optional<SavedBiomeChunk>> historical = new HashMap<>(); Map<Long, Optional<SavedBiomeChunk>> historical = new HashMap<>();
Map<Integer, List<SavedBiomeChunk.Span>> vertical = new HashMap<>(16); SubterrainPlanner planner = engine.getComplex().getSubterrainPlanner();
boolean exactVolume = planner != null && !planner.plansForBounds(startX, startZ,
Math.addExact(startX, 15), Math.addExact(startZ, 15)).isEmpty();
Map<Integer, List<SavedBiomeChunk.Span>> vertical = new HashMap<>(exactVolume ? 256 : 16);
Cells cells = new Cells(activationId); Cells cells = new Cells(activationId);
Column column = new Column(engine, minimumY, height); Column column = new Column(engine, minimumY, height, exactVolume ? 1 : 4);
for (int localX = 0; localX < 16; localX++) { for (int localX = 0; localX < 16; localX++) {
for (int localZ = 0; localZ < 16; localZ++) { for (int localZ = 0; localZ < 16; localZ++) {
int blockX = startX + localX; int blockX = startX + localX;
@@ -51,13 +55,13 @@ public final class SavedBiomeCapture {
if (transition != null) { if (transition != null) {
surfaceCell = historicalCell(new Sample(blockX, surfaceY, blockZ, true), surfaceCell, transition, saved, historical); surfaceCell = historicalCell(new Sample(blockX, surfaceY, blockZ, true), surfaceCell, transition, saved, historical);
} }
int quartX = localX & ~3; int sampleX = exactVolume ? localX : localX & ~3;
int quartZ = localZ & ~3; int sampleZ = exactVolume ? localZ : localZ & ~3;
int quartKey = quartX * 16 + quartZ; int columnKey = sampleX * 16 + sampleZ;
List<SavedBiomeChunk.Span> spans = vertical.get(quartKey); List<SavedBiomeChunk.Span> spans = vertical.get(columnKey);
if (spans == null) { if (spans == null) {
spans = captureVertical(column, startX + quartX, startZ + quartZ, cells, transition, saved, historical, floating); spans = captureVertical(column, startX + sampleX, startZ + sampleZ, cells, transition, saved, historical, floating);
vertical.put(quartKey, spans); vertical.put(columnKey, spans);
} }
SavedBiomeChunk.Cell caveBase = cells.cell(engine.getCaveBiome(blockX, blockZ), region); SavedBiomeChunk.Cell caveBase = cells.cell(engine.getCaveBiome(blockX, blockZ), region);
result.column(localX, localZ, new SavedBiomeChunk.Column(surfaceCell, caveBase, spans)); result.column(localX, localZ, new SavedBiomeChunk.Column(surfaceCell, caveBase, spans));
@@ -69,16 +73,16 @@ public final class SavedBiomeCapture {
private static List<SavedBiomeChunk.Span> captureVertical(Column column, int blockX, int blockZ, Cells cells, private static List<SavedBiomeChunk.Span> captureVertical(Column column, int blockX, int blockZ, Cells cells,
TransitionGenerationPlan transition, SavedBiomeRuntime saved, TransitionGenerationPlan transition, SavedBiomeRuntime saved,
Map<Long, Optional<SavedBiomeChunk>> historical, FloatingBiomeOverlay floating) throws IOException { Map<Long, Optional<SavedBiomeChunk>> historical, FloatingBiomeOverlay floating) throws IOException {
column.engine().getBiomeOrMantleColumn(blockX, blockZ, 4, column.biomes(), column.regions()); column.engine().getBiomeOrMantleColumn(blockX, blockZ, column.step(), column.biomes(), column.regions());
int minimumY = column.minimumY(); int minimumY = column.minimumY();
int maximumY = minimumY + column.height(); int maximumY = minimumY + column.height();
List<SavedBiomeChunk.Span> spans = new ArrayList<>(); List<SavedBiomeChunk.Span> spans = new ArrayList<>();
SavedBiomeChunk.Cell previous = null; SavedBiomeChunk.Cell previous = null;
int startY = minimumY; int startY = minimumY;
for (int index = 0; index < column.biomes().length; index++) { for (int index = 0; index < column.biomes().length; index++) {
int worldY = minimumY + index * 4; int worldY = minimumY + index * column.step();
SavedBiomeChunk.Cell current = cells.cell(column.biomes()[index], column.regions()[index]); SavedBiomeChunk.Cell current = cells.cell(column.biomes()[index], column.regions()[index]);
if (floating != null) { if (floating != null && !column.engine().getSubterrainCell(blockX, index * column.step(), blockZ).occupied()) {
current = overlay(current, floating.volumeAt(blockX & 15, worldY - minimumY, blockZ & 15)); current = overlay(current, floating.volumeAt(blockX & 15, worldY - minimumY, blockZ & 15));
} }
if (transition != null) { if (transition != null) {
@@ -135,9 +139,10 @@ public final class SavedBiomeCapture {
Objects.requireNonNull(region, "region").getLoadKey()); Objects.requireNonNull(region, "region").getLoadKey());
} }
private record Column(Engine engine, int minimumY, int height, IrisBiome[] biomes, IrisRegion[] regions) { private record Column(Engine engine, int minimumY, int height, int step, IrisBiome[] biomes, IrisRegion[] regions) {
private Column(Engine engine, int minimumY, int height) { private Column(Engine engine, int minimumY, int height, int step) {
this(engine, minimumY, height, new IrisBiome[(height + 3) / 4], new IrisRegion[(height + 3) / 4]); this(engine, minimumY, height, step, new IrisBiome[(height + step - 1) / step],
new IrisRegion[(height + step - 1) / step]);
} }
} }
@@ -3,6 +3,10 @@ package art.arcane.iris.world.history;
import art.arcane.iris.generation.runtime.GenerationClosedException; import art.arcane.iris.generation.runtime.GenerationClosedException;
import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.runtime.IrisEngine; import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.SeedManager;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
import art.arcane.iris.generation.runtime.BiomeEnvironment; import art.arcane.iris.generation.runtime.BiomeEnvironment;
import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.iris.generation.terrain.IrisDimension;
@@ -10,6 +14,7 @@ import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisLogging;
import java.io.IOException; import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.LinkOption; import java.nio.file.LinkOption;
import java.nio.file.Path; import java.nio.file.Path;
@@ -17,6 +22,7 @@ import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@@ -39,6 +45,7 @@ public final class SavedBiomeRuntime implements AutoCloseable {
private final GenerationHistory history; private final GenerationHistory history;
private final SavedBiomeStore store; private final SavedBiomeStore store;
private final Map<String, Definitions> definitions = new ConcurrentHashMap<>(); private final Map<String, Definitions> definitions = new ConcurrentHashMap<>();
private final Map<Long, SubterrainPlanner> subterrainPlanners = new ConcurrentHashMap<>();
private final LinkedHashMap<Long, PreparedQuery> queries = new LinkedHashMap<>(32, 0.75F, true); private final LinkedHashMap<Long, PreparedQuery> queries = new LinkedHashMap<>(32, 0.75F, true);
private final Map<Long, PendingQuery> pending = new HashMap<>(); private final Map<Long, PendingQuery> pending = new HashMap<>();
private final ReentrantReadWriteLock consumption = new ReentrantReadWriteLock(); private final ReentrantReadWriteLock consumption = new ReentrantReadWriteLock();
@@ -135,6 +142,41 @@ public final class SavedBiomeRuntime implements AutoCloseable {
return resolve(blockX, 0, blockZ, QueryKind.CAVE_BASE); return resolve(blockX, 0, blockZ, QueryKind.CAVE_BASE);
} }
public Optional<SubterrainCell> subterrainCell(int blockX, int worldY, int blockZ) {
int chunkX = Math.floorDiv(blockX, 16);
int chunkZ = Math.floorDiv(blockZ, 16);
if (history.isActiveUnowned(chunkX, chunkZ) && history.semantics(chunkX, chunkZ).isEmpty()) {
return Optional.empty();
}
try {
long activationId = history.resolveActivation(chunkX, chunkZ).activationId();
return Optional.of(subterrainPlanner(activationId).sample(blockX, worldY, blockZ));
} catch (IOException failure) {
throw new UncheckedIOException("Unable to read saved subterrain definitions", failure);
}
}
public SubterrainPlanner subterrainPlanner(long activationId) throws IOException {
consumption.readLock().lock();
try {
requireOpen();
SubterrainPlanner cached = subterrainPlanners.get(activationId);
if (cached != null) {
return cached;
}
GenerationActivation activation = history.manifest().activation(activationId).orElseThrow(() ->
new IOException("Saved subterrain generation is missing: " + activationId));
TransitionGenerationPlan transition = activation.isInitial() ? null : history.transitionPlan(activationId);
SubterrainPlanner planner = definitions(activationId).subterrainPlanner().restrictTo(bounds ->
transition == null || transition.allowsNewFootprint(
bounds.minX(), bounds.minZ(), bounds.maxX(), bounds.maxZ()));
SubterrainPlanner existing = subterrainPlanners.putIfAbsent(activationId, planner);
return existing == null ? planner : existing;
} finally {
consumption.readLock().unlock();
}
}
private Optional<BiomeEnvironment> resolve(int blockX, int worldY, int blockZ, QueryKind kind) { private Optional<BiomeEnvironment> resolve(int blockX, int worldY, int blockZ, QueryKind kind) {
int chunkX = Math.floorDiv(blockX, 16); int chunkX = Math.floorDiv(blockX, 16);
int chunkZ = Math.floorDiv(blockZ, 16); int chunkZ = Math.floorDiv(blockZ, 16);
@@ -254,6 +296,7 @@ public final class SavedBiomeRuntime implements AutoCloseable {
} }
} }
definitions.clear(); definitions.clear();
subterrainPlanners.clear();
} finally { } finally {
consumption.writeLock().unlock(); consumption.writeLock().unlock();
} }
@@ -466,8 +509,14 @@ public final class SavedBiomeRuntime implements AutoCloseable {
if (dimension == null) { if (dimension == null) {
throw new IOException("Historical dimension is no longer supported: " + epoch.dimensionContract().dimensionKey()); throw new IOException("Historical dimension is no longer supported: " + epoch.dimensionContract().dimensionKey());
} }
if (dimension.allowsSubterrainFeatures()) {
SubterrainRasterizer.validateMaterials(dimension.getSubterrainFeatures());
}
Definitions loaded = new Definitions(data, dimension, NativeBiomeSpawnSelection.retainedDerivatives(data), Definitions loaded = new Definitions(data, dimension, NativeBiomeSpawnSelection.retainedDerivatives(data),
resolveFocusRegions(data, dimension)); resolveFocusRegions(data, dimension), new SubterrainPlanner(new SubterrainPlanner.Options(
dimension.allowsSubterrainFeatures() ? dimension.getSubterrainFeatures() : List.of(),
new SeedManager(epoch.worldSeed()).getBodies(),
dimension.getMinHeight(), dimension.getMaxHeight())));
data.prepareBlockDropRules(); data.prepareBlockDropRules();
definitions.put(epochId, loaded); definitions.put(epochId, loaded);
return loaded; return loaded;
@@ -540,7 +589,7 @@ public final class SavedBiomeRuntime implements AutoCloseable {
} }
private record Definitions(IrisData data, IrisDimension dimension, Map<String, String> nativeDerivatives, private record Definitions(IrisData data, IrisDimension dimension, Map<String, String> nativeDerivatives,
Map<String, IrisRegion> focusRegions) { Map<String, IrisRegion> focusRegions, SubterrainPlanner subterrainPlanner) {
private BiomeEnvironment environment(SavedBiomeChunk.Cell cell) { private BiomeEnvironment environment(SavedBiomeChunk.Cell cell) {
IrisBiome biome = data.getBiomeLoader().load(cell.biomeKey()); IrisBiome biome = data.getBiomeLoader().load(cell.biomeKey());
IrisRegion region = data.getRegionLoader().load(cell.regionKey()); IrisRegion region = data.getRegionLoader().load(cell.regionKey());
@@ -507,7 +507,7 @@ public class DecoratorCoreTest {
@Test @Test
public void sulfurStalagmiteKeepsSulfurMaterialAndTapersToOneTip() { public void sulfurStalagmiteKeepsSulfurMaterialAndTapersToOneTip() {
Hunk<NativeBlockState> output = placeSpikeColumn("minecraft:sulfur_spike", 5, true, false); Hunk<NativeBlockState> output = placeSpikeColumn("minecraft:sulfur_spike", 5, true, false, false);
assertSpikeColumn(output, "minecraft:sulfur_spike", true, false, assertSpikeColumn(output, "minecraft:sulfur_spike", true, false,
"base", "middle", "middle", "frustum", "tip"); "base", "middle", "middle", "frustum", "tip");
@@ -515,7 +515,7 @@ public class DecoratorCoreTest {
@Test @Test
public void sulfurStalactiteKeepsWaterloggingAndPointsDown() { public void sulfurStalactiteKeepsWaterloggingAndPointsDown() {
Hunk<NativeBlockState> output = placeSpikeColumn("minecraft:sulfur_spike", 5, false, true); Hunk<NativeBlockState> output = placeSpikeColumn("minecraft:sulfur_spike", 5, false, true, true);
assertSpikeColumn(output, "minecraft:sulfur_spike", false, true, assertSpikeColumn(output, "minecraft:sulfur_spike", false, true,
"base", "middle", "middle", "frustum", "tip"); "base", "middle", "middle", "frustum", "tip");
@@ -524,17 +524,26 @@ public class DecoratorCoreTest {
@Test @Test
public void shortSulfurSpikesHaveCorrectTipsInBothDirections() { public void shortSulfurSpikesHaveCorrectTipsInBothDirections() {
for (boolean upward : new boolean[]{true, false}) { for (boolean upward : new boolean[]{true, false}) {
Hunk<NativeBlockState> single = placeSpikeColumn("minecraft:sulfur_spike", 1, upward, false); Hunk<NativeBlockState> single = placeSpikeColumn("minecraft:sulfur_spike", 1, upward, false, false);
Hunk<NativeBlockState> pair = placeSpikeColumn("minecraft:sulfur_spike", 2, upward, false); Hunk<NativeBlockState> pair = placeSpikeColumn("minecraft:sulfur_spike", 2, upward, false, false);
assertSpikeColumn(single, "minecraft:sulfur_spike", upward, false, "tip"); assertSpikeColumn(single, "minecraft:sulfur_spike", upward, false, "tip");
assertSpikeColumn(pair, "minecraft:sulfur_spike", upward, false, "frustum", "tip"); assertSpikeColumn(pair, "minecraft:sulfur_spike", upward, false, "frustum", "tip");
} }
} }
@Test
public void authoredWaterloggedSpikesBecomeDryInAirInBothDirections() {
for (boolean upward : new boolean[]{true, false}) {
Hunk<NativeBlockState> output = placeSpikeColumn("minecraft:pointed_dripstone", 4, upward, true, false);
assertSpikeColumn(output, "minecraft:pointed_dripstone", upward, false,
"base", "middle", "frustum", "tip");
}
}
@Test @Test
public void pointedDripstoneRetainsWaterloggingWhenStacked() { public void pointedDripstoneRetainsWaterloggingWhenStacked() {
Hunk<NativeBlockState> output = placeSpikeColumn("minecraft:pointed_dripstone", 4, true, true); Hunk<NativeBlockState> output = placeSpikeColumn("minecraft:pointed_dripstone", 4, true, true, true);
assertSpikeColumn(output, "minecraft:pointed_dripstone", true, true, assertSpikeColumn(output, "minecraft:pointed_dripstone", true, true,
"base", "middle", "frustum", "tip"); "base", "middle", "frustum", "tip");
@@ -738,7 +747,7 @@ public class DecoratorCoreTest {
return air; return air;
} }
private Hunk<NativeBlockState> placeSpikeColumn(String material, int height, boolean upward, boolean waterlogged) { private Hunk<NativeBlockState> placeSpikeColumn(String material, int height, boolean upward, boolean waterlogged, boolean submerged) {
IrisDecorator decorator = mock(IrisDecorator.class); IrisDecorator decorator = mock(IrisDecorator.class);
IrisData data = mock(IrisData.class); IrisData data = mock(IrisData.class);
NativeBlockState spike = spikeState(material, Map.of( NativeBlockState spike = spikeState(material, Map.of(
@@ -750,12 +759,21 @@ public class DecoratorCoreTest {
when(decorator.pickBlockDataTop(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(spike); when(decorator.pickBlockDataTop(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(spike);
Hunk<NativeBlockState> output = Hunk.newArrayHunk(1, height + 2, 1); Hunk<NativeBlockState> output = Hunk.newArrayHunk(1, height + 2, 1);
NativeBlockState target = airState();
if (submerged) {
target = mock(NativeBlockState.class);
when(target.key()).thenReturn("minecraft:water");
when(target.isWater()).thenReturn(true);
when(target.isFluid()).thenReturn(true);
}
for (int y = 1; y <= height; y++) { for (int y = 1; y <= height; y++) {
output.set(0, y, 0, airState()); output.set(0, y, 0, target);
} }
NativeBlockState support = sturdyState(); NativeBlockState support = sturdyState();
output.set(0, upward ? 0 : height + 1, 0, support); output.set(0, upward ? 0 : height + 1, 0, support);
DecoratorCore.PlaceOpts opts = new DecoratorCore.PlaceOpts(); DecoratorCore.PlaceOpts opts = new DecoratorCore.PlaceOpts();
opts.underwater = submerged;
opts.fluidHeight = height;
if (upward) { if (upward) {
DecoratorCore.placeStackUp(decorator, 0, 0, 0, 0, 0, height, output, new RNG(1L), data, opts); DecoratorCore.placeStackUp(decorator, 0, 0, 0, 0, 0, height, output, new RNG(1L), data, opts);
} else { } else {
@@ -0,0 +1,44 @@
package art.arcane.iris.generation.decoration;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.hunk.Hunk;
import org.junit.Test;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisPlacementWaterloggingTest {
@Test
public void authoredWetStateIsClearedForDryAndLavaTargets() {
NativeBlockState wet = state("minecraft:oak_slab[type=bottom,waterlogged=true]");
NativeBlockState dry = state("minecraft:oak_slab[type=bottom,waterlogged=false]");
when(wet.withProperty("waterlogged", "false")).thenReturn(dry);
assertSame(dry, IrisProceduralBlocks.normalizeWaterlogging(wet, state("minecraft:cave_air"), false));
assertSame(dry, IrisProceduralBlocks.normalizeWaterlogging(wet, state("minecraft:lava"), true));
}
@Test
public void waterloggedDecorationRequiresActualWaterAtItsOwnVoxel() {
NativeBlockState dry = state("minecraft:oak_slab[type=bottom,waterlogged=false]");
NativeBlockState wet = state("minecraft:oak_slab[type=bottom,waterlogged=true]");
NativeBlockState water = state("minecraft:water");
when(water.isWater()).thenReturn(true);
when(dry.withProperty("waterlogged", "true")).thenReturn(wet);
assertSame(wet, IrisProceduralBlocks.normalizeWaterlogging(dry, water, true));
assertSame(dry, IrisProceduralBlocks.normalizeWaterlogging(dry, water, false));
assertSame(wet, IrisProceduralBlocks.normalizeWaterlogging(wet, water, false));
Hunk<NativeBlockState> hunk = Hunk.newArrayHunk(16, 64, 16);
hunk.set(3, 28, 5, water);
hunk.set(3, 29, 5, state("minecraft:cave_air"));
when(wet.withProperty("waterlogged", "false")).thenReturn(dry);
assertSame(wet, DecoratorCore.fixFacesForHunk(dry, hunk, 3, 5, 3, 28, 5, null));
assertSame(dry, DecoratorCore.fixFacesForHunk(wet, hunk, 3, 5, 3, 29, 5, null));
}
private NativeBlockState state(String key) {
NativeBlockState state = mock(NativeBlockState.class);
when(state.key()).thenReturn(key);
return state;
}
}
@@ -0,0 +1,62 @@
package art.arcane.iris.generation.decoration.formation;
import art.arcane.iris.generation.block.B;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.iris.generation.subterrain.IrisSubterrainFamily;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainRoom;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class ContextualFormationTest {
@Test
public void roomVaultSizesFormationAfterAnchorWithoutUsingOrdinaryHeightRange() {
IrisFormation formation = new IrisFormation().setName("vault-spire").setRoomHeightFraction(0.5)
.setHeightMin(80).setHeightMax(80).setBaseWidthMin(2).setBaseWidthMax(2)
.setRoughness(0).setLean(0);
NativeBlockState rock = mock(NativeBlockState.class);
when(rock.key()).thenReturn("minecraft:stone");
try (MockedStatic<B> blocks = mockStatic(B.class)) {
blocks.when(() -> B.getStateOrNull("minecraft:stone", false)).thenReturn(rock);
IrisData data = mock(IrisData.class);
IrisObject shortRoom = formation.getVariantObject(data, new RNG(143L), room(12));
IrisObject tallRoom = formation.getVariantObject(data, new RNG(143L), room(40));
IrisObject repeat = formation.getVariantObject(data, new RNG(143L), room(40));
assertNotNull(shortRoom);
assertNotNull(tallRoom);
assertTrue(shortRoom.getH() <= 12);
assertTrue(tallRoom.getH() <= 40);
assertTrue(tallRoom.getH() > shortRoom.getH());
assertNotNull(repeat);
assertEquals(tallRoom.getW(), repeat.getW());
assertEquals(tallRoom.getH(), repeat.getH());
assertEquals(tallRoom.getD(), repeat.getD());
assertEquals(tallRoom.getBlocks().size(), repeat.getBlocks().size());
for (Map.Entry<IrisBlockVector, NativeBlockState> entry : tallRoom.getBlocks()) {
NativeBlockState repeated = repeat.getBlocks().get(entry.getKey());
assertNotNull(repeated);
assertEquals(entry.getValue().key(), repeated.key());
}
assertEquals("procedural/vault-spire/cenote-room", tallRoom.getLoadKey());
}
}
private SubterrainRoom room(int vaultHeight) {
return new SubterrainRoom("cenote-room", "cave-biome", IrisSubterrainFamily.CENOTE,
0, 20, 0, 0, 12, 0, 10, 11 + vaultHeight, 20, 10,
false, false, SubterrainCell.Kind.AIR);
}
}
@@ -0,0 +1,74 @@
package art.arcane.iris.generation.locator;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.subterrain.IrisSubterrainFamily;
import art.arcane.iris.generation.subterrain.IrisSubterrainFeature;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.pack.value.IrisRange;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
public class SubterrainLocatorTest {
@Test
public void wideDistanceSearchReturnsOwnedAbsolutePositionWithoutGenerating() {
SubterrainPlanner planner = planner();
Engine engine = engine(planner);
SubterrainLocator.Result result = SubterrainLocator.nearest(engine,
new SubterrainLocator.Query("basin", null, "wet-cave"), 100_000, -24, -100_000, 8192).orElseThrow();
SubterrainCell sampled = planner.sample(result.x(), result.y(), result.z());
assertTrue(sampled.occupied());
assertEquals(result.featureId(), sampled.room().featureId());
assertEquals("wet-cave", result.biome());
assertTrue(result.y() >= -48 && result.y() <= 48);
verify(engine).getComplex();
verifyNoMoreInteractions(engine);
}
@Test
public void filtersAndDistanceBoundsRejectUnavailableOwnership() {
Engine engine = engine(planner());
assertFalse(SubterrainLocator.nearest(engine,
new SubterrainLocator.Query("missing", null, ""), 0, 0, 0, 1024).isPresent());
assertFalse(SubterrainLocator.nearest(engine,
new SubterrainLocator.Query("", IrisSubterrainFamily.LAVA_TUBE, ""), 0, 0, 0, 1024).isPresent());
assertFalse(SubterrainLocator.nearest(engine,
new SubterrainLocator.Query("", null, "missing"), 0, 0, 0, 1024).isPresent());
assertFalse(SubterrainLocator.nearest(engine,
new SubterrainLocator.Query("", null, ""), 0, 10_000, 0, 1024).isPresent());
assertFalse(SubterrainLocator.nearest(engine,
new SubterrainLocator.Query("", null, ""), 0, 0, 0, 1024, ignored -> false).isPresent());
}
@Test
public void repeatedSearchAndLargeNegativeCoordinatesAreStable() {
Engine engine = engine(planner());
SubterrainLocator.Query query = new SubterrainLocator.Query("", null, "wet-cave");
assertEquals(SubterrainLocator.nearest(engine, query, -1_500_000, -10, -2_000_000, 4096),
SubterrainLocator.nearest(engine, query, -1_500_000, -10, -2_000_000, 4096));
}
private static Engine engine(SubterrainPlanner planner) {
Engine engine = mock(Engine.class);
IrisComplex complex = mock(IrisComplex.class);
when(engine.getComplex()).thenReturn(complex);
when(complex.getSubterrainPlanner()).thenReturn(planner);
return engine;
}
private static SubterrainPlanner planner() {
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("basin").setBiome("wet-cave")
.setProbability(1).setWorldYRange(new IrisRange(-48, 48));
return new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 74119L, -64, 320));
}
}
@@ -19,6 +19,9 @@
package art.arcane.iris.generation.mantle; package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainRoom;
import art.arcane.iris.generation.subterrain.IrisSubterrainFamily;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.structure.object.IObjectPlacer; import art.arcane.iris.structure.object.IObjectPlacer;
import art.arcane.iris.generation.block.TileData; import art.arcane.iris.generation.block.TileData;
@@ -112,6 +115,74 @@ public class CaveObjectPlacementTransactionTest {
assertEquals(61, MantleObjectComponent.caveAnchorScanUpperBound(128, 80, 20)); assertEquals(61, MantleObjectComponent.caveAnchorScanUpperBound(128, 80, 20));
} }
@Test
public void ownedDryAndWetRoomDecorationCanCommitWithoutWeakeningSolidGuards() {
for (SubterrainCell.Kind kind : new SubterrainCell.Kind[]{SubterrainCell.Kind.AIR, SubterrainCell.Kind.WATER}) {
IObjectPlacer delegate = createPlacer(128, 80, 20, 60);
SubterrainRoom room = new SubterrainRoom("cenote", "wet-cave", IrisSubterrainFamily.CENOTE,
4, 35, 7, 4, 21, 7, 19, 61, 8, 34, false, false, kind);
when(delegate.getEngine().getSubterrainCell(4, 30, 7))
.thenReturn(new SubterrainCell(kind, kind == SubterrainCell.Kind.WATER ? "minecraft:water" : "minecraft:cave_air", room));
NativeBlockState decoration = mock(NativeBlockState.class);
CaveObjectPlacementTransaction transaction = new CaveObjectPlacementTransaction(delegate, 20, 10);
transaction.set(4, 30, 7, decoration);
assertEquals(CaveObjectPlacementTransaction.CommitResult.COMMITTED, transaction.commit());
verify(delegate).set(4, 30, 7, decoration);
}
}
@Test
public void ownedWaterAndLavaUseTheirExclusiveAbsoluteFluidHeadAtTheOriginalAnchor() {
for (int minimumY : new int[]{-64, 32}) {
for (SubterrainCell.Kind kind : new SubterrainCell.Kind[]{SubterrainCell.Kind.WATER, SubterrainCell.Kind.LAVA}) {
IObjectPlacer delegate = createPlacer(256, 220, 120, 160);
Engine engine = delegate.getEngine();
when(engine.getMinHeight()).thenReturn(minimumY);
when(delegate.getFluidHeight(4, 7)).thenReturn(16);
SubterrainRoom room = new SubterrainRoom("raised-room", "wet-cave", IrisSubterrainFamily.CENOTE,
4, minimumY + 130, 7, 4, minimumY + 121, 7, minimumY + 119,
minimumY + 161, 8, minimumY + 126, false, false, kind);
when(engine.getSubterrainCell(4, 123, 7)).thenReturn(new SubterrainCell(kind,
kind == SubterrainCell.Kind.WATER ? "minecraft:water" : "minecraft:lava", room));
CaveObjectPlacementTransaction transaction = new CaveObjectPlacementTransaction(delegate, 123, 10);
assertEquals(127, transaction.getFluidHeight(4, 7));
verify(engine).getSubterrainCell(4, 123, 7);
verify(delegate, never()).getFluidHeight(4, 7);
}
}
}
@Test
public void nonFluidAnchorsPreserveTheDelegateCoordinateFluidHeight() {
for (SubterrainCell.Kind kind : new SubterrainCell.Kind[]{SubterrainCell.Kind.AIR, SubterrainCell.Kind.SOLID, SubterrainCell.Kind.OUTSIDE}) {
IObjectPlacer delegate = createPlacer(128, 80, 20, 60);
when(delegate.getFluidHeight(4, 7)).thenReturn(51);
SubterrainRoom room = new SubterrainRoom("dry-room", "dry-cave", IrisSubterrainFamily.CENOTE,
4, 35, 7, 4, 21, 7, 19, 61, 8, 24, false, false, kind);
SubterrainCell cell = kind == SubterrainCell.Kind.OUTSIDE ? SubterrainCell.OUTSIDE
: new SubterrainCell(kind, kind == SubterrainCell.Kind.AIR ? "minecraft:cave_air" : "minecraft:stone", room);
when(delegate.getEngine().getSubterrainCell(4, 20, 7)).thenReturn(cell);
CaveObjectPlacementTransaction transaction = new CaveObjectPlacementTransaction(delegate, 20, 10);
assertEquals(51, transaction.getFluidHeight(4, 7));
verify(delegate).getFluidHeight(4, 7);
}
}
@Test
public void featurePillarRejectsAllObjectMutations() {
IObjectPlacer delegate = createPlacer(128, 80, 20, 60);
SubterrainRoom room = new SubterrainRoom("cenote", "wet-cave", IrisSubterrainFamily.CENOTE,
4, 35, 7, 4, 21, 7, 19, 61, 8, 24, false, true, SubterrainCell.Kind.SOLID);
when(delegate.getEngine().getSubterrainCell(4, 30, 7))
.thenReturn(new SubterrainCell(SubterrainCell.Kind.SOLID, "minecraft:stone", room));
CaveObjectPlacementTransaction transaction = new CaveObjectPlacementTransaction(delegate, 20, 10);
transaction.set(4, 30, 7, mock(NativeBlockState.class));
transaction.setData(5, 30, 7, "decoration");
assertEquals(CaveObjectPlacementTransaction.CommitResult.REJECTED_SUBTERRAIN, transaction.commit());
verify(delegate, never()).set(anyInt(), anyInt(), anyInt(), any());
verify(delegate, never()).setData(anyInt(), anyInt(), anyInt(), any());
}
@Test @Test
public void dryHydrologyHeadroomRejectsTheWholePlacement() { public void dryHydrologyHeadroomRejectsTheWholePlacement() {
IObjectPlacer delegate = createPlacer(128, 80, 20, 90); IObjectPlacer delegate = createPlacer(128, 80, 20, 90);
@@ -1,13 +1,21 @@
package art.arcane.iris.generation.mantle; package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.subterrain.IrisSubterrainFamily;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainRoom;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction; import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell; import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.volmlib.util.matter.MatterCavern; import art.arcane.volmlib.util.matter.MatterCavern;
import org.junit.Test; import org.junit.Test;
import java.lang.reflect.Method;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class MantleObjectComponentCaveAnchorTest { public class MantleObjectComponentCaveAnchorTest {
@Test @Test
@@ -37,8 +45,10 @@ public class MantleObjectComponentCaveAnchorTest {
public void explicitWetAndForcedAirPlacementsRemainEligible() { public void explicitWetAndForcedAirPlacementsRemainEligible() {
MatterCavern water = new MatterCavern(true, "", (byte) 1); MatterCavern water = new MatterCavern(true, "", (byte) 1);
MatterCavern forcedAir = new MatterCavern(true, "", (byte) 3); MatterCavern forcedAir = new MatterCavern(true, "", (byte) 3);
MatterCavern lava = new MatterCavern(true, "", (byte) 2);
assertTrue(MantleObjectComponent.acceptsCaveAnchorFluid(true, water, 20, 8)); assertTrue(MantleObjectComponent.acceptsCaveAnchorFluid(true, water, 20, 8));
assertTrue(MantleObjectComponent.acceptsCaveAnchorFluid(true, lava, 20, 8));
assertTrue(MantleObjectComponent.acceptsCaveAnchorFluid(false, forcedAir, 0, 8)); assertTrue(MantleObjectComponent.acceptsCaveAnchorFluid(false, forcedAir, 0, 8));
assertFalse(MantleObjectComponent.acceptsCaveAnchorFluid( assertFalse(MantleObjectComponent.acceptsCaveAnchorFluid(
true, true,
@@ -63,6 +73,56 @@ public class MantleObjectComponentCaveAnchorTest {
false, forcedAir, HydrologyCaveCell.of(HydrologyCaveAction.DRY_AIR), 20, 8)); false, forcedAir, HydrologyCaveCell.of(HydrologyCaveAction.DRY_AIR), 20, 8));
} }
@Test
public void authoredWetAnchorsAcceptWaterAndLavaWhileDryAnchorsRequireAir() throws Exception {
for (SubterrainCell.Kind kind : new SubterrainCell.Kind[]{SubterrainCell.Kind.WATER, SubterrainCell.Kind.LAVA}) {
SubterrainCell cell = featureCell(kind, false);
assertTrue(acceptsAuthoredAnchor(cell, true));
assertFalse(acceptsAuthoredAnchor(cell, false));
}
SubterrainCell air = featureCell(SubterrainCell.Kind.AIR, false);
assertTrue(acceptsAuthoredAnchor(air, false));
assertFalse(acceptsAuthoredAnchor(air, true));
}
@Test
public void authoredWetAnchorsPreserveReservedPassageAndSolidGuards() throws Exception {
for (SubterrainCell.Kind kind : new SubterrainCell.Kind[]{SubterrainCell.Kind.AIR, SubterrainCell.Kind.WATER, SubterrainCell.Kind.LAVA}) {
SubterrainCell reserved = featureCell(kind, true);
assertFalse(acceptsAuthoredAnchor(reserved, false));
assertFalse(acceptsAuthoredAnchor(reserved, true));
}
SubterrainCell solid = featureCell(SubterrainCell.Kind.SOLID, false);
assertFalse(acceptsAuthoredAnchor(solid, false));
assertFalse(acceptsAuthoredAnchor(solid, true));
}
private static boolean acceptsAuthoredAnchor(SubterrainCell cell, boolean underwater) throws Exception {
Engine engine = mock(Engine.class);
EngineMantle mantle = mock(EngineMantle.class);
when(mantle.getEngine()).thenReturn(engine);
when(engine.getSubterrainCell(17, 4, -9)).thenReturn(cell);
MantleObjectComponent component = new MantleObjectComponent(mantle);
Method method = MantleObjectComponent.class.getDeclaredMethod("acceptsCaveAnchorAt",
int.class, int.class, int.class, boolean.class, MatterCavern.class, HydrologyCaveCell.class);
method.setAccessible(true);
return (boolean) method.invoke(component, 17, 4, -9, underwater, null, null);
}
private static SubterrainCell featureCell(SubterrainCell.Kind kind, boolean reservedPassage) {
SubterrainRoom room = new SubterrainRoom("basin:0:0", "subterrain/basin", IrisSubterrainFamily.CENOTE,
17, 4, -9, 17, 4, -9, 0, 20, 8, 8,
reservedPassage, kind == SubterrainCell.Kind.SOLID, kind);
String material = switch (kind) {
case AIR -> "minecraft:air";
case WATER -> "minecraft:water";
case LAVA -> "minecraft:lava";
case SOLID -> "minecraft:stone";
case OUTSIDE -> "";
};
return new SubterrainCell(kind, material, room);
}
private static IrisBiome biome(String loadKey) { private static IrisBiome biome(String loadKey) {
IrisBiome biome = new IrisBiome(); IrisBiome biome = new IrisBiome();
biome.setLoadKey(loadKey); biome.setLoadKey(loadKey);
@@ -0,0 +1,279 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.cave.CarvingMode;
import art.arcane.iris.generation.decoration.IrisProceduralPlacement;
import art.arcane.iris.generation.decoration.coral.IrisCoral;
import art.arcane.iris.generation.decoration.coral.IrisCoralForm;
import art.arcane.iris.generation.decoration.crystal.IrisCrystal;
import art.arcane.iris.generation.decoration.formation.IrisFormation;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.subterrain.IrisSubterrainFeature;
import art.arcane.iris.generation.subterrain.IrisSubterrainFluid;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainPlan;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.generation.subterrain.SubterrainPosition;
import art.arcane.iris.generation.subterrain.SubterrainRoom;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.structure.object.IObjectPlacer;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.structure.object.IrisObjectPlacement;
import art.arcane.iris.structure.object.IrisObjectRotation;
import art.arcane.iris.structure.object.IrisObjectTranslate;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.nativelib.terrain.BlockStateKey;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.math.RNG;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class NaturalCaveProceduralPlacementTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private final Map<String, NativeBlockState> states = new HashMap<>();
@Before
public void bindPlatform() {
IrisPlatforms.unbind();
PlatformRegistries registries = mock(PlatformRegistries.class);
when(registries.block(anyString())).thenAnswer(invocation -> state(invocation.getArgument(0)));
when(registries.blockOrNull(anyString())).thenAnswer(invocation -> state(invocation.getArgument(0)));
when(registries.blockOrNull(anyString(), eq(false))).thenAnswer(invocation -> state(invocation.getArgument(0)));
IrisPlatform platform = mock(IrisPlatform.class);
when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform);
}
@After
public void unbindPlatform() {
IrisPlatforms.unbind();
states.clear();
}
@Test
public void roomProportionalFormationRestsOnActualFloorWithoutTranslation() {
Fixture fixture = fixture(new FixtureOptions(IrisSubterrainFluid.WATER, 0));
IrisFormation formation = new IrisFormation().setName("natural-vault").setRoomHeightFraction(0.3)
.setHeightMin(90).setHeightMax(90).setBaseWidthMin(1).setBaseWidthMax(1)
.setRoughness(0).setLean(0).setJitter(0).setCarvingSupport(CarvingMode.CARVING_ONLY);
IrisObject object = variant(formation, fixture);
assertTrue(object.getH() <= Math.floor(fixture.room().vaultHeight() * 0.3) + 2);
assertTrue(object.getH() > 3);
assertTrue(object.getLoadKey().endsWith(fixture.room().featureId()));
assertFloorPlacement(object, formation.asPlacement().setRotation(IrisObjectRotation.of(0, 0, 0)), fixture);
}
@Test
public void crystalBaseAndRotatedShardsRestAboveProtectedCenoteFloor() {
for (double angle : List.of(0D, 30D)) {
Fixture fixture = fixture(new FixtureOptions(IrisSubterrainFluid.WATER, 0));
IrisCrystal crystal = new IrisCrystal().setName("natural-crystal").setVariants(1)
.setBaseRadius(1.5).setBaseNoise(0).setShardCountMin(3).setShardCountMax(3)
.setShardLengthMin(5).setShardLengthMax(5).setShardBaseRadius(0.5)
.setSpreadAngle(10).setJitter(0);
IrisObject object = variant(crystal, fixture);
assertTrue(object.getH() > 3);
IrisObjectPlacement placement = crystal.asPlacement().setRotation(IrisObjectRotation.of(angle, 0, 0));
assertFloorPlacement(object, placement, fixture);
}
}
@Test
public void submergedCoralUsesOwnedFluidHeadAboveGlobalWaterAndWaterlogsOnlyInWater() {
for (IrisSubterrainFluid fluid : IrisSubterrainFluid.values()) {
Fixture fixture = fixture(new FixtureOptions(fluid, 12));
assertTrue(fixture.room().floorY() > fixture.placer().getFluidHeight(fixture.x(), fixture.z()));
IrisCoral coral = coral();
IrisObject object = variant(coral, fixture);
assertFloorPlacement(object, coral.asPlacement().setRotation(IrisObjectRotation.of(0, 0, 0)), fixture);
SubterrainCell.Kind expected = fluid == IrisSubterrainFluid.WATER ? SubterrainCell.Kind.WATER : SubterrainCell.Kind.LAVA;
for (Map.Entry<SubterrainPosition, NativeBlockState> entry : fixture.writes().entrySet()) {
SubterrainPosition position = entry.getKey();
assertEquals(expected, fixture.planner().sample(position.x(), position.y(), position.z()).kind());
assertEquals(entry.getValue().key(), fluid == IrisSubterrainFluid.WATER, entry.getValue().isWaterLogged());
}
}
}
@Test
public void authoredDownwardTranslationStillRejectsWholeObjectAtProtectedFloor() {
Fixture fixture = fixture(new FixtureOptions(IrisSubterrainFluid.WATER, 0));
IrisCrystal crystal = new IrisCrystal().setVariants(1).setBaseRadius(1)
.setShardCountMin(1).setShardCountMax(1).setShardLengthMin(4).setShardLengthMax(4)
.setSpreadAngle(0).setJitter(0);
IrisObject object = variant(crystal, fixture);
IrisObjectPlacement placement = crystal.asPlacement().setRotation(IrisObjectRotation.of(0, 0, 0))
.setTranslate(new IrisObjectTranslate().setY(-1));
CaveObjectPlacementTransaction transaction = transaction(fixture);
int firstAirY = MantleObjectComponent.proceduralFloorY(fixture.placer(), fixture.x(), fixture.floorY() + 3, fixture.z());
int result = object.placeOnFloor(fixture.x(), firstAirY, fixture.z(), transaction, placement, new RNG(17L),
(position, block) -> transaction.setData(position.getX(), position.getY(), position.getZ(), "natural-crystal@1"), fixture.data());
assertTrue(result >= 0);
assertEquals(CaveObjectPlacementTransaction.CommitResult.REJECTED_SUBTERRAIN, transaction.commit());
assertTrue(fixture.writes().isEmpty());
assertTrue(fixture.metadata().isEmpty());
assertTrue(fixture.placer().get(fixture.x(), fixture.floorY(), fixture.z()).isSolid());
}
private void assertFloorPlacement(IrisObject object, IrisObjectPlacement placement, Fixture fixture) {
assertFalse(placement.isBottom());
assertEquals(0, placement.getTranslate().getY());
CaveObjectPlacementTransaction transaction = transaction(fixture);
int firstAirY = MantleObjectComponent.proceduralFloorY(fixture.placer(), fixture.x(), fixture.floorY() + 3, fixture.z());
assertEquals(fixture.floorY() + 1, firstAirY);
int result = object.placeOnFloor(fixture.x(), firstAirY, fixture.z(), transaction, placement, new RNG(17L),
(position, block) -> transaction.setData(position.getX(), position.getY(), position.getZ(), "natural@1"), fixture.data());
assertTrue(result >= 0);
assertTrue(fixture.writes().isEmpty());
assertTrue(fixture.metadata().isEmpty());
assertEquals(CaveObjectPlacementTransaction.CommitResult.COMMITTED, transaction.commit());
assertFalse(fixture.writes().isEmpty());
int lowest = Integer.MAX_VALUE;
for (SubterrainPosition position : fixture.writes().keySet()) {
lowest = Math.min(lowest, position.y());
SubterrainCell cell = fixture.planner().sample(position.x(), position.y(), position.z());
assertTrue(cell.occupied());
assertFalse(cell.room().reservedPassage());
assertTrue(position.y() > cell.room().floorY());
assertTrue(position.y() < cell.room().ceilingY());
}
assertEquals(fixture.floorY() + 1, lowest);
boolean supported = false;
for (SubterrainPosition position : fixture.writes().keySet()) {
if (position.y() == lowest) {
assertTrue(fixture.placer().get(position.x(), lowest - 1, position.z()).isSolid());
assertFalse(fixture.writes().containsKey(new SubterrainPosition(position.x(), lowest - 1, position.z())));
supported = true;
}
}
assertTrue(supported);
assertFalse(fixture.metadata().isEmpty());
assertFalse(placement.isBottom());
}
private CaveObjectPlacementTransaction transaction(Fixture fixture) {
return new CaveObjectPlacementTransaction(fixture.placer(), fixture.floorY() + 3, 6);
}
private IrisObject variant(IrisProceduralPlacement procedural, Fixture fixture) {
IrisObject object = procedural.getVariantObject(fixture.data(), new RNG(819L), fixture.room());
assertNotNull(object);
assertFalse(object.getBlocks().isEmpty());
return object;
}
private IrisCoral coral() {
return new IrisCoral().setName("natural-coral").setVariants(1).setForm(IrisCoralForm.PILLAR)
.setHeightMin(6).setHeightMax(6).setPillarRadius(1).setTipClusterRadius(0)
.setBlock("minecraft:tube_coral[waterlogged=true]").setCarvingSupport(CarvingMode.CARVING_ONLY);
}
private Fixture fixture(FixtureOptions options) {
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("natural-room").setProbability(1)
.setRadius(32).setHeight(40).setFluid(options.fluid()).setFluidDepth(options.fluidDepth())
.setPillarSpacing(0).setFormationFraction(0).setWorldYRange(new IrisRange(20, 120));
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 83L, 0, 256));
SubterrainPlan plan = planner.plansForBounds(0, 0, 512, 512).getFirst();
int x = plan.centerX() + 8;
int z = plan.centerZ() + 8;
SubterrainRoom room = plan.sample(x, plan.centerY(), z).room();
assertNotNull(room);
int floorY = room.floorY();
Engine engine = mock(Engine.class);
when(engine.getHeight()).thenReturn(256);
when(engine.getMinHeight()).thenReturn(0);
when(engine.getHeight(anyInt(), anyInt(), eq(true))).thenReturn(220);
when(engine.getSubterrainCell(anyInt(), anyInt(), anyInt())).thenAnswer(invocation ->
planner.sample(invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2)));
IrisData data = mock(IrisData.class);
when(data.getEngine()).thenReturn(engine);
Map<SubterrainPosition, NativeBlockState> writes = new HashMap<>();
Map<SubterrainPosition, Object> metadata = new HashMap<>();
IObjectPlacer placer = mock(IObjectPlacer.class);
when(placer.getEngine()).thenReturn(engine);
when(placer.getFluidHeight()).thenReturn(0);
when(placer.getFluidHeight(anyInt(), anyInt())).thenReturn(0);
when(placer.isUnderwater(anyInt(), anyInt())).thenReturn(options.fluidDepth() > 0);
when(placer.get(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
SubterrainPosition position = new SubterrainPosition(invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2));
NativeBlockState written = writes.get(position);
if (written != null) {
return written;
}
SubterrainCell cell = planner.sample(position.x(), position.y(), position.z());
return state(cell.owned() ? cell.material() : "minecraft:stone");
});
when(placer.isCarved(anyInt(), anyInt(), anyInt())).thenAnswer(invocation ->
planner.sample(invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2)).occupied());
when(placer.isSolid(anyInt(), anyInt(), anyInt())).thenAnswer(invocation ->
placer.get(invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2)).isSolid());
when(placer.isSurfaceSolid(anyInt(), anyInt(), anyInt())).thenAnswer(invocation ->
placer.isSolid(invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2)));
doAnswer(invocation -> {
writes.put(new SubterrainPosition(invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2)), invocation.getArgument(3));
return null;
}).when(placer).set(anyInt(), anyInt(), anyInt(), any());
doAnswer(invocation -> {
metadata.put(new SubterrainPosition(invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2)), invocation.getArgument(3));
return null;
}).when(placer).setData(anyInt(), anyInt(), anyInt(), any());
return new Fixture(planner, placer, data, room, x, z, floorY, writes, metadata);
}
private NativeBlockState state(String key) {
NativeBlockState cached = states.get(key);
if (cached != null) {
return cached;
}
NativeBlockState block = mock(NativeBlockState.class);
int properties = key.indexOf('[');
String material = properties < 0 ? key : key.substring(0, properties);
boolean water = material.equals("minecraft:water");
boolean lava = material.equals("minecraft:lava");
boolean air = material.endsWith("air");
when(block.key()).thenReturn(key);
when(block.materialKey()).thenReturn(material);
when(block.isSolid()).thenReturn(!air && !water && !lava);
when(block.isOccluding()).thenReturn(!air && !water && !lava);
when(block.isAir()).thenReturn(air);
when(block.isFluid()).thenReturn(water || lava);
when(block.isWater()).thenReturn(water);
when(block.isAirOrFluid()).thenReturn(air || water || lava);
when(block.isWaterLogged()).thenReturn(key.contains("waterlogged=true"));
when(block.withProperty(anyString(), anyString())).thenAnswer(invocation ->
state(BlockStateKey.withProperty(key, invocation.getArgument(0), invocation.getArgument(1))));
states.put(key, block);
return block;
}
private record FixtureOptions(IrisSubterrainFluid fluid, int fluidDepth) {
}
private record Fixture(SubterrainPlanner planner, IObjectPlacer placer, IrisData data, SubterrainRoom room,
int x, int z, int floorY, Map<SubterrainPosition, NativeBlockState> writes,
Map<SubterrainPosition, Object> metadata) {
}
}
@@ -1,6 +1,9 @@
package art.arcane.iris.generation.mantle; package art.arcane.iris.generation.mantle;
import art.arcane.iris.integration.Identifier; import art.arcane.iris.integration.Identifier;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainRoom;
import art.arcane.iris.generation.subterrain.IrisSubterrainFamily;
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial; import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
@@ -48,6 +51,21 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
public class ObjectDestinationTransactionTest { public class ObjectDestinationTransactionTest {
@Test
public void sourceCaptureCannotReplaceAuthoredRoomSolids() {
MantleWriter writer = writer();
SubterrainRoom room = new SubterrainRoom("cenote", "wet-cave", IrisSubterrainFamily.CENOTE,
4, 35, 7, 4, 21, 7, 19, 61, 8, 24, false, true, SubterrainCell.Kind.SOLID);
when(writer.getEngine().getSubterrainCell(4, 30, 7))
.thenReturn(new SubterrainCell(SubterrainCell.Kind.SOLID, "minecraft:stone", room));
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, 0, 0);
int checkpoint = transaction.mutationCheckpoint();
transaction.set(4, 30, 7, mock(NativeBlockState.class));
transaction.setData(4, 30, 7, "object@1");
transaction.setData(4, 30, 7, new MatterCavern(true, "", (byte) 3));
assertEquals(checkpoint, transaction.mutationCheckpoint());
}
@Test @Test
public void commonAndGenericMetadataSurviveBlockReplacementAndSourceReplay() { public void commonAndGenericMetadataSurviveBlockReplacementAndSourceReplay() {
MantleWriter writer = writer(); MantleWriter writer = writer();
@@ -248,6 +248,7 @@ public class DimensionTerrain3DCompositionTest {
Engine engine = mock(Engine.class, CALLS_REAL_METHODS); Engine engine = mock(Engine.class, CALLS_REAL_METHODS);
IrisComplex complex = mock(IrisComplex.class); IrisComplex complex = mock(IrisComplex.class);
doReturn(complex).when(engine).getComplex(); doReturn(complex).when(engine).getComplex();
doReturn(new IrisDimension()).when(engine).getDimension();
DimensionStackContext stack = mock(DimensionStackContext.class); DimensionStackContext stack = mock(DimensionStackContext.class);
doReturn(stack).when(engine).getDimensionStackContext(); doReturn(stack).when(engine).getDimensionStackContext();
DimensionStackLayout.LayerInput bottom = input(24, 0, DimensionStackLayout.LayerInput bottom = input(24, 0,
@@ -893,7 +893,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
private static IrisBiome carvingBiome(IrisEngine engine, IrisDimensionCarvingResolver.State state) { private static IrisBiome carvingBiome(IrisEngine engine, IrisDimensionCarvingResolver.State state) {
IrisDimensionCarvingEntry root = IrisDimensionCarvingResolver.resolveRootEntry(engine, 80, state); IrisDimensionCarvingEntry root = IrisDimensionCarvingResolver.resolveRootEntry(engine, 80, state);
IrisDimensionCarvingEntry child = IrisDimensionCarvingResolver.resolveFromRoot(engine, root, 19, -3, state); IrisDimensionCarvingEntry child = IrisDimensionCarvingResolver.resolveFromRoot(engine, root, 19, 80, -3, state);
IrisBiome expected = IrisDimensionCarvingResolver.resolveEntryBiome(engine, child, state); IrisBiome expected = IrisDimensionCarvingResolver.resolveEntryBiome(engine, child, state);
assertSame(expected, IrisDimensionCarvingResolver.resolveBiome(engine, 19, 80, -3, state)); assertSame(expected, IrisDimensionCarvingResolver.resolveBiome(engine, 19, 80, -3, state));
return expected; return expected;
@@ -0,0 +1,64 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.subterrain.IrisSubterrainFeature;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainPlan;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.generation.subterrain.SubterrainPosition;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.loading.ResourceLoader;
import art.arcane.iris.world.IrisWorld;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.Answers.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
public class SubterrainBiomeOwnershipTest {
@Test
public void occupiedFeatureOwnsExactVolumeAndYWithoutSurfaceDepthGate() {
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("basin").setBiome("basin-biome").setProbability(1);
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 1191L, -64, 320));
SubterrainPlan plan = planner.plansForBounds(0, 0, 512, 512).getFirst();
Engine engine = mock(Engine.class, CALLS_REAL_METHODS);
IrisComplex complex = mock(IrisComplex.class);
IrisData data = mock(IrisData.class);
@SuppressWarnings("unchecked")
ResourceLoader<IrisBiome> loader = mock(ResourceLoader.class);
IrisBiome biome = new IrisBiome();
IrisDimension dimension = new IrisDimension().setSubterrainFeatures(new KList<>(feature));
doReturn(dimension).when(engine).getDimension();
doReturn(complex).when(engine).getComplex();
doReturn(planner).when(complex).getSubterrainPlanner();
doReturn(data).when(engine).getData();
doReturn(mock(DimensionStackContext.class)).when(engine).getDimensionStackContext();
doReturn(loader).when(data).getBiomeLoader();
doReturn(biome).when(loader).load("basin-biome");
doReturn(IrisWorld.builder().minHeight(-64).maxHeight(320).build()).when(engine).getWorld();
SubterrainPosition anchor = plan.anchor();
int internalY = anchor.y() + 64;
assertSame(biome, engine.getBiome(anchor.x(), internalY, anchor.z()));
assertSame(biome, engine.getBiomeOrMantle(anchor.x(), internalY, anchor.z()));
assertEquals(planner.sample(anchor.x(), anchor.y(), anchor.z()), engine.getSubterrainCell(anchor.x(), internalY, anchor.z()));
assertNull(engine.getSubterrainBiome(anchor.x(), plan.bounds().maxY() + 65, anchor.z()));
assertNull(engine.getSubterrainBiome(plan.bounds().maxX() + 1, internalY, anchor.z()));
for (int y = plan.bounds().minY(); y <= plan.bounds().maxY(); y++) {
SubterrainCell cell = planner.sample(anchor.x(), y, anchor.z());
if (cell.solid()) {
assertNull(engine.getSubterrainBiome(anchor.x(), y + 64, anchor.z()));
}
}
verify(engine, never()).getMantle();
verify(engine, never()).getDimensionStackContext();
}
}
@@ -9,13 +9,19 @@ import art.arcane.iris.world.entity.IrisEntity;
import art.arcane.iris.world.entity.IrisEntitySpawn; import art.arcane.iris.world.entity.IrisEntitySpawn;
import art.arcane.iris.world.entity.IrisEntitySpawn.SpawnContext; import art.arcane.iris.world.entity.IrisEntitySpawn.SpawnContext;
import art.arcane.iris.world.entity.IrisSpawner; import art.arcane.iris.world.entity.IrisSpawner;
import art.arcane.iris.world.entity.IrisSpawnGroup;
import art.arcane.iris.generation.decoration.IrisSurface;
import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.world.task.J;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
import org.bukkit.Chunk; import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.GameRules; import org.bukkit.GameRules;
import org.bukkit.World; import org.bukkit.World;
import org.junit.Test; import org.junit.Test;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.mockito.MockedStatic;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.HashMap; import java.util.HashMap;
@@ -31,6 +37,8 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.mockStatic;
public class WorldEntitySpawnerInitialSeedTest { public class WorldEntitySpawnerInitialSeedTest {
@BeforeClass @BeforeClass
@@ -52,6 +60,38 @@ public class WorldEntitySpawnerInitialSeedTest {
verify(fixture.lastChunk, never()).getEntities(); verify(fixture.lastChunk, never()).getEntities();
} }
@Test
public void successfulUndergroundPopulationReducesSurfaceCapacityWithoutCountingFailures() throws Exception {
for (int undergroundSuccesses : new int[]{0, 2, 4}) {
Fixture fixture = new Fixture();
IrisEntitySpawn undergroundEntry = mock(IrisEntitySpawn.class);
IrisEntity entity = mock(IrisEntity.class);
when(entity.getSurface()).thenReturn(IrisSurface.LAND);
when(entity.spawnCategory()).thenReturn("creature");
when(undergroundEntry.getRealEntity(fixture.engine)).thenReturn(entity);
when(undergroundEntry.getRarity()).thenReturn(1);
IrisSpawner cave = new IrisSpawner().setGroup(IrisSpawnGroup.CAVE)
.setMaxEntitiesPerChunk(4).setInitialSpawns(new KList<>(undergroundEntry));
when(fixture.environment.data().getSpawnerLoader().loadAll(fixture.environment.biome().getEntitySpawners()))
.thenReturn(new KList<>(fixture.definition, cave));
when(fixture.environment.dimension().hasUndergroundSpawners(fixture.engine)).thenReturn(true);
when(fixture.engine.getWorld()).thenReturn(IrisWorld.builder().minHeight(-64).maxHeight(320).build());
when(fixture.engine.getBiomeOrMantleEnvironment(anyInt(), anyInt(), anyInt())).thenReturn(fixture.environment);
when(fixture.engine.openBiomeEnvironmentScope(fixture.environment)).thenReturn(mock(BiomeEnvironment.Scope.class));
Location candidate = new Location(fixture.world, 35, -20, -45);
when(undergroundEntry.spawn(eq(fixture.engine), eq(candidate), any(RNG.class), eq(4), any(SpawnContext.class)))
.thenReturn(undergroundSuccesses);
try (MockedStatic<J> scheduler = mockStatic(J.class);
MockedStatic<IrisEntitySpawn> positions = mockStatic(IrisEntitySpawn.class)) {
positions.when(() -> IrisEntitySpawn.findLiveCaveSpawnLocation(any(Chunk.class), any(RNG.class), eq(IrisSurface.LAND)))
.thenReturn(candidate);
fixture.spawn(2, -3);
}
assertEquals(4 - undergroundSuccesses, fixture.observed.get("2,-3").capacity());
verify(fixture.lastChunk, never()).getEntities();
}
}
@Test @Test
public void chunkBatchSeedsAreIndependentOfVisitOrderAndUnrelatedRandomActivity() throws Exception { public void chunkBatchSeedsAreIndependentOfVisitOrderAndUnrelatedRandomActivity() throws Exception {
Fixture fixture = new Fixture(); Fixture fixture = new Fixture();
@@ -86,7 +126,7 @@ public class WorldEntitySpawnerInitialSeedTest {
private final Engine engine = mock(Engine.class); private final Engine engine = mock(Engine.class);
private final IrisWorldManager manager = mock(IrisWorldManager.class); private final IrisWorldManager manager = mock(IrisWorldManager.class);
private final World world = mock(World.class); private final World world = mock(World.class);
private final IrisSpawner definition = mock(IrisSpawner.class); private final IrisSpawner definition = spy(new IrisSpawner().setMaxEntitiesPerChunk(4));
private final IrisEntitySpawn entry = mock(IrisEntitySpawn.class); private final IrisEntitySpawn entry = mock(IrisEntitySpawn.class);
private final Map<String, Observation> observed = new HashMap<>(); private final Map<String, Observation> observed = new HashMap<>();
private final BiomeEnvironment environment; private final BiomeEnvironment environment;
@@ -114,7 +154,9 @@ public class WorldEntitySpawnerInitialSeedTest {
when(definition.getInitialSpawns()).thenReturn(new KList<>(entry)); when(definition.getInitialSpawns()).thenReturn(new KList<>(entry));
when(definition.getMaxEntitiesPerChunk()).thenReturn(4); when(definition.getMaxEntitiesPerChunk()).thenReturn(4);
when(entry.getRarity()).thenReturn(1); when(entry.getRarity()).thenReturn(1);
when(entry.getRealEntity(engine)).thenReturn(mock(IrisEntity.class)); IrisEntity entity = mock(IrisEntity.class);
when(entity.spawnCategory()).thenReturn("creature");
when(entry.getRealEntity(engine)).thenReturn(entity);
when(world.getGameRuleValue(GameRules.SPAWN_MOBS)).thenReturn(true); when(world.getGameRuleValue(GameRules.SPAWN_MOBS)).thenReturn(true);
when(entry.spawn(eq(engine), any(Chunk.class), any(RNG.class), anyInt(), any(SpawnContext.class))) when(entry.spawn(eq(engine), any(Chunk.class), any(RNG.class), anyInt(), any(SpawnContext.class)))
.thenAnswer(invocation -> { .thenAnswer(invocation -> {
@@ -123,7 +165,7 @@ public class WorldEntitySpawnerInitialSeedTest {
int capacity = invocation.getArgument(3); int capacity = invocation.getArgument(3);
SpawnContext context = invocation.getArgument(4); SpawnContext context = invocation.getArgument(4);
observed.put(chunk.getX() + "," + chunk.getZ(), new Observation(rng.getSeed(), capacity, context.initial())); observed.put(chunk.getX() + "," + chunk.getZ(), new Observation(rng.getSeed(), capacity, context.initial()));
return 4; return capacity;
}); });
environment = new BiomeEnvironment(1L, biome, region, dimension, data); environment = new BiomeEnvironment(1L, biome, region, dimension, data);
spawner = new WorldEntitySpawner(manager); spawner = new WorldEntitySpawner(manager);
@@ -0,0 +1,116 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.decoration.IrisSurface;
import art.arcane.iris.generation.subterrain.IrisSubterrainFeature;
import art.arcane.iris.generation.subterrain.SubterrainPlan;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.generation.subterrain.SubterrainPosition;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.loading.ResourceLoader;
import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.world.entity.IrisEntity;
import art.arcane.iris.world.entity.IrisEntitySpawn;
import art.arcane.iris.world.entity.IrisSpawnGroup;
import art.arcane.iris.world.entity.IrisSpawner;
import art.arcane.iris.world.task.J;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.stream.ProceduralStream;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.World;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.lang.reflect.Method;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.junit.Assert.assertSame;
import static org.mockito.Answers.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class WorldEntitySpawnerUndergroundOwnershipTest {
@Test
public void cavePoolUsesActualCandidateBiomeAndPlacesInsideItsVolume() throws Exception {
Engine engine = mock(Engine.class, CALLS_REAL_METHODS);
IrisWorldManager manager = mock(IrisWorldManager.class);
when(manager.getEngine()).thenReturn(engine);
doReturn(IrisWorld.builder().minHeight(-64).maxHeight(320).build()).when(engine).getWorld();
World world = mock(World.class);
Chunk chunk = mock(Chunk.class);
when(chunk.getWorld()).thenReturn(world);
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("basin").setBiome("basin-biome").setProbability(1);
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 1191L, -64, 320));
SubterrainPlan plan = planner.plansForBounds(0, 0, 512, 512).getFirst();
SubterrainPosition anchor = plan.anchor();
Location candidate = new Location(world, anchor.x(), anchor.y(), anchor.z());
IrisBiome underground = new IrisBiome();
IrisDimension dimension = new IrisDimension().setSubterrainFeatures(new KList<>(feature));
IrisRegion region = new IrisRegion().setEntitySpawners(new KList<>("cave-residents"));
IrisRegion stackedRegion = new IrisRegion().setEntitySpawners(new KList<>("wrong-pack-residents"));
IrisComplex complex = mock(IrisComplex.class);
DimensionStackContext stack = mock(DimensionStackContext.class);
DimensionStackLayout layout = mock(DimensionStackLayout.class);
DimensionStackLayout.Layer layer = mock(DimensionStackLayout.Layer.class);
@SuppressWarnings("unchecked")
ProceduralStream<IrisRegion> regionStream = mock(ProceduralStream.class);
doReturn(complex).when(engine).getComplex();
doReturn(dimension).when(engine).getDimension();
doReturn(planner).when(complex).getSubterrainPlanner();
doReturn(regionStream).when(complex).getRegionStream();
when(regionStream.get(anchor.x(), anchor.z())).thenReturn(region);
doReturn(stack).when(engine).getDimensionStackContext();
when(stack.getLayout(anchor.x(), anchor.z())).thenReturn(layout);
when(layout.layerAt(anyInt())).thenReturn(layer);
when(layer.region()).thenReturn(stackedRegion);
IrisData data = mock(IrisData.class);
@SuppressWarnings("unchecked")
ResourceLoader<IrisSpawner> loader = mock(ResourceLoader.class);
doReturn(data).when(engine).getData();
@SuppressWarnings("unchecked")
ResourceLoader<IrisBiome> biomeLoader = mock(ResourceLoader.class);
when(data.getBiomeLoader()).thenReturn(biomeLoader);
when(biomeLoader.load("basin-biome")).thenReturn(underground);
when(data.getSpawnerLoader()).thenReturn(loader);
when(loader.loadAll(dimension.getEntitySpawners())).thenReturn(new KList<>());
when(loader.loadAll(underground.getEntitySpawners())).thenReturn(new KList<>());
IrisEntitySpawn entry = mock(IrisEntitySpawn.class);
IrisEntity entity = mock(IrisEntity.class);
when(entity.getSurface()).thenReturn(IrisSurface.LAND);
when(entry.getRealEntity(engine)).thenReturn(entity);
when(entry.getRarity()).thenReturn(1);
IrisSpawner spawner = new IrisSpawner().setGroup(IrisSpawnGroup.CAVE).setInitialSpawns(new KList<>(entry));
when(loader.loadAll(region.getEntitySpawners())).thenReturn(new KList<>(spawner));
BiomeEnvironment environment = engine.getBiomeOrMantleEnvironment(anchor.x(), anchor.y() + 64, anchor.z());
assertSame(underground, environment.biome());
assertSame(region, environment.region());
BiomeEnvironment.Scope scope = mock(BiomeEnvironment.Scope.class);
when(engine.openBiomeEnvironmentScope(environment)).thenReturn(scope);
try (MockedStatic<J> scheduler = mockStatic(J.class);
MockedStatic<IrisEntitySpawn> positions = mockStatic(IrisEntitySpawn.class)) {
positions.when(() -> IrisEntitySpawn.findLiveCaveSpawnLocation(eq(chunk), any(RNG.class), eq(IrisSurface.LAND)))
.thenReturn(candidate);
Method method = WorldEntitySpawner.class.getDeclaredMethod("spawnUnderground",
Chunk.class, boolean.class, WorldEntitySpawner.ChunkCounter.class, RNG.class);
method.setAccessible(true);
method.invoke(new WorldEntitySpawner(manager), chunk, true, new WorldEntitySpawner.ChunkCounter(new org.bukkit.entity.Entity[0]), new RNG(17719L));
}
verify(loader, never()).loadAll(stackedRegion.getEntitySpawners());
verify(engine, never()).getSurfaceBiomeEnvironment(anchor.x(), anchor.z());
assertSame(stackedRegion, engine.getRegion(anchor.x(), plan.bounds().maxY() + 65, anchor.z()));
verify(entry).spawn(eq(engine), eq(candidate), any(RNG.class), eq(spawner.getMaxEntitiesPerChunk()),
any(IrisEntitySpawn.SpawnContext.class));
verify(scope).close();
}
}
@@ -0,0 +1,382 @@
package art.arcane.iris.generation.subterrain;
import art.arcane.iris.pack.value.IrisRange;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class SubterrainPlannerTest {
@Test
public void terraceAnchorsUseOccupiedPathsWhenCenterIsPillarOrUpperStep() {
for (IrisSubterrainFluid fluid : IrisSubterrainFluid.values()) {
IrisSubterrainFeature feature = compactFeature(IrisSubterrainFamily.TRAVERTINE_TERRACES).setFluid(fluid);
SubterrainPlan pillar = direct(feature);
for (int y = pillar.bounds().minY(); y <= pillar.bounds().maxY(); y++) {
assertFalse(pillar.sample(0, y, 0).occupied());
}
SubterrainPosition pillarAnchor = pillar.anchor();
assertTrue(pillar.sample(pillarAnchor.x(), pillarAnchor.y(), pillarAnchor.z()).occupied());
assertTrue(pillar.sample(pillarAnchor.x(), pillarAnchor.y(), pillarAnchor.z()).room().reservedPassage());
feature.setHeight(8).setFluidDepth(2).setTerraceCount(16);
for (boolean rotated : List.of(false, true)) {
SubterrainPlan lowVault = new SubterrainPlan(new SubterrainPlan.Options(
SubterrainPlanner.Definition.from(feature), 0, 0, 0, rotated, "terrace:low-vault"));
SubterrainPosition anchor = lowVault.anchor();
SubterrainCell cell = lowVault.sample(anchor.x(), anchor.y(), anchor.z());
assertTrue(cell.occupied());
assertTrue(cell.room().reservedPassage());
assertTrue((rotated ? anchor.z() : anchor.x()) < 0);
assertEquals(anchor, lowVault.anchor());
}
}
}
@Test
public void explicitFluidChangesOnlyLiquidKindAndMaterialForEveryWetFamily() {
for (IrisSubterrainFamily family : List.of(IrisSubterrainFamily.CENOTE,
IrisSubterrainFamily.LAVA_TUBE, IrisSubterrainFamily.TRAVERTINE_TERRACES)) {
IrisSubterrainFeature water = compactFeature(family).setFluid(IrisSubterrainFluid.WATER);
IrisSubterrainFeature lava = compactFeature(family).setFluid(IrisSubterrainFluid.LAVA);
SubterrainPlanner waterPlanner = planner(water);
SubterrainPlanner lavaPlanner = planner(lava);
SubterrainPlan waterPlaced = first(waterPlanner);
SubterrainPlan lavaPlaced = first(lavaPlanner);
assertEquals(waterPlaced.id(), lavaPlaced.id());
assertEquals(waterPlaced.bounds(), lavaPlaced.bounds());
assertEquals(waterPlaced.anchor(), lavaPlaced.anchor());
SubterrainPlan waterPlan = direct(water);
SubterrainPlan lavaPlan = direct(lava);
SubterrainBounds bounds = waterPlan.bounds();
int fluidCells = 0;
for (int x = bounds.minX(); x <= bounds.maxX(); x++) {
for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) {
for (int y = bounds.minY(); y <= bounds.maxY(); y++) {
SubterrainCell original = waterPlanner.sample(List.of(waterPlan), x, y, z);
SubterrainCell changed = lavaPlanner.sample(List.of(lavaPlan), x, y, z);
if (original.fluid()) {
fluidCells++;
assertEquals(SubterrainCell.Kind.WATER, original.kind());
assertEquals("minecraft:water", original.material());
assertEquals(SubterrainCell.Kind.LAVA, changed.kind());
assertEquals("minecraft:lava", changed.material());
SubterrainRoom room = original.room();
assertEquals(new SubterrainRoom(room.featureId(), room.biome(), room.family(),
room.centerX(), room.centerY(), room.centerZ(), room.pathX(), room.pathY(), room.pathZ(),
room.floorY(), room.ceilingY(), room.boundaryDistance(), room.fluidHeadY(),
room.reservedPassage(), room.reservedSolid(), SubterrainCell.Kind.LAVA), changed.room());
} else {
assertEquals(original, changed);
}
}
}
}
assertTrue(family.toString(), fluidCells > 100);
assertEquals(assertRetained(waterPlanner, List.of(waterPlan), SubterrainCell.Kind.WATER),
assertRetained(lavaPlanner, List.of(lavaPlan), SubterrainCell.Kind.LAVA));
}
}
@Test
public void omittedAndNullFluidsPreserveFamilyDefaults() {
Gson gson = new Gson();
for (IrisSubterrainFamily family : IrisSubterrainFamily.values()) {
IrisSubterrainFluid expected = family == IrisSubterrainFamily.LAVA_TUBE
? IrisSubterrainFluid.LAVA : IrisSubterrainFluid.WATER;
IrisSubterrainFeature omitted = gson.fromJson("{\"family\":\"" + family + "\"}", IrisSubterrainFeature.class);
IrisSubterrainFeature nullable = gson.fromJson("{\"family\":\"" + family + "\",\"fluid\":null}", IrisSubterrainFeature.class);
assertNull(omitted.getFluid());
assertNull(nullable.getFluid());
omitted.setId("default");
nullable.setId("default");
assertEquals(expected, SubterrainPlanner.Definition.from(omitted).fluid());
assertEquals(SubterrainPlanner.Definition.from(omitted), SubterrainPlanner.Definition.from(nullable));
SubterrainPlan implicit = direct(feature(family));
SubterrainPlan explicit = direct(feature(family).setFluid(expected));
assertEquals(implicit.bounds(), explicit.bounds());
for (int x = implicit.bounds().minX(); x <= implicit.bounds().maxX(); x += 7) {
for (int z = implicit.bounds().minZ(); z <= implicit.bounds().maxZ(); z += 7) {
for (int y = implicit.bounds().minY(); y <= implicit.bounds().maxY(); y++) {
assertEquals(implicit.sample(x, y, z), explicit.sample(x, y, z));
}
}
}
}
}
@Test
public void faultRemainsDryForEveryConfiguredFluid() {
SubterrainPlan original = direct(feature(IrisSubterrainFamily.TECTONIC_FAULT));
for (IrisSubterrainFluid fluid : IrisSubterrainFluid.values()) {
SubterrainPlan configured = direct(feature(IrisSubterrainFamily.TECTONIC_FAULT).setFluid(fluid));
for (int x = original.bounds().minX(); x <= original.bounds().maxX(); x += 2) {
for (int z = original.bounds().minZ(); z <= original.bounds().maxZ(); z += 2) {
for (int y = original.bounds().minY(); y <= original.bounds().maxY(); y++) {
SubterrainCell cell = configured.sample(x, y, z);
assertFalse(cell.fluid());
assertEquals(original.sample(x, y, z), cell);
}
}
}
}
}
@Test
public void plansSnapshotResolvedFluidAcrossConfigurationMutation() {
IrisSubterrainFeature feature = compactFeature(IrisSubterrainFamily.CENOTE);
SubterrainPlan implicit = direct(feature);
feature.setFluid(IrisSubterrainFluid.LAVA);
SubterrainPlan explicit = direct(feature);
feature.setFluid(IrisSubterrainFluid.WATER);
assertEquals(SubterrainCell.Kind.WATER, implicit.sample(0, -10, 0).kind());
assertEquals(SubterrainCell.Kind.LAVA, explicit.sample(0, -10, 0).kind());
}
@Test
public void fluidJsonOnlyAcceptsTypedWaterOrLava() {
Gson gson = new Gson();
for (IrisSubterrainFluid fluid : IrisSubterrainFluid.values()) {
assertEquals(fluid, gson.fromJson("{\"fluid\":\"" + fluid + "\"}", IrisSubterrainFeature.class).getFluid());
assertEquals("\"" + fluid + "\"", gson.toJson(fluid));
}
for (String invalid : List.of("\"OIL\"", "\"minecraft:water\"", "\"water\"", "5", "true", "{}", "[]")) {
assertThrows(invalid, JsonParseException.class,
() -> gson.fromJson("{\"fluid\":" + invalid + "}", IrisSubterrainFeature.class));
}
}
@Test
public void allFamiliesHaveBoundedOccupiedAnchorsAndAbsoluteOwnership() {
for (IrisSubterrainFamily family : IrisSubterrainFamily.values()) {
SubterrainPlanner planner = planner(feature(family));
SubterrainPlan plan = first(planner);
SubterrainPosition anchor = plan.anchor();
assertTrue(family.toString(), plan.sample(anchor.x(), anchor.y(), anchor.z()).occupied());
assertTrue(plan.bounds().minY() >= -64);
assertTrue(plan.bounds().maxY() <= 96);
assertFalse(planner.sample(anchor.x(), -65, anchor.z()).owned());
assertFalse(planner.sample(anchor.x(), 97, anchor.z()).owned());
assertFalse(plan.sample(plan.bounds().maxX() + 1, anchor.y(), anchor.z()).owned());
assertNotNull(plan.sample(anchor.x(), anchor.y(), anchor.z()).room());
}
}
@Test
public void faultWallsRemainParallelBeyondTwoHundredBlocksAndHaveShelves() {
SubterrainPlan plan = direct(feature(IrisSubterrainFamily.TECTONIC_FAULT));
for (int x = -110; x <= 110; x++) {
assertTrue(plan.sample(x, 0, 0).occupied());
assertTrue(plan.sample(x, 0, 32).solid());
assertTrue(plan.sample(x, 0, -32).solid());
}
int centralFloor = plan.sample(0, 0, 0).room().floorY();
int ledgeFloor = plan.sample(0, 0, 28).room().floorY();
assertTrue(ledgeFloor > centralFloor);
assertTrue(plan.sample(0, ledgeFloor, 28).solid());
}
@Test
public void cenoteDomeAndSealedWaterTableHaveNoExposedFluidSidesOrBottom() {
SubterrainPlan plan = direct(feature(IrisSubterrainFamily.CENOTE));
SubterrainPlanner planner = planner(feature(IrisSubterrainFamily.CENOTE));
assertTrue(plan.sample(0, 0, 0).room().ceilingY() > plan.sample(20, 0, 0).room().ceilingY());
assertTrue(assertRetained(planner, List.of(plan), SubterrainCell.Kind.WATER) > 1000);
assertTrue(plan.sample(0, -16, 0).solid());
}
@Test
public void lavaTubeHasConnectedHornitosAndDryElevatedWalkways() {
SubterrainPlan plan = direct(feature(IrisSubterrainFamily.LAVA_TUBE));
int chimneyX = 64;
int pathZ = 5;
for (int y = 0; y < 32; y++) {
assertEquals("chimney y=" + y, SubterrainCell.Kind.AIR, plan.sample(chimneyX, y, pathZ).kind());
}
assertTrue(plan.sample(chimneyX + 4, 25, pathZ).solid());
SubterrainCell walkway = plan.sample(0, 0, 18);
assertTrue(walkway.room().floorY() > walkway.room().fluidHeadY());
assertTrue(plan.sample(0, walkway.room().floorY(), 18).solid());
SubterrainPlanner planner = planner(feature(IrisSubterrainFamily.LAVA_TUBE));
assertTrue(assertRetained(planner, List.of(plan), SubterrainCell.Kind.LAVA) > 1000);
}
@Test
public void travertineBasinsStepAndRetainFluidBehindRims() {
SubterrainPlan plan = direct(feature(IrisSubterrainFamily.TRAVERTINE_TERRACES));
assertTrue(plan.sample(90, 0, 0).room().floorY() > plan.sample(-90, 0, 0).room().floorY());
SubterrainPlanner planner = planner(feature(IrisSubterrainFamily.TRAVERTINE_TERRACES));
assertTrue(assertRetained(planner, List.of(plan), SubterrainCell.Kind.WATER) > 1000);
int rimX = -128 + 42;
assertTrue(plan.sample(rimX, -10, 0).solid());
assertTrue(plan.sample(0, 0, 28).room().floorY() > plan.sample(0, 0, 28).room().fluidHeadY());
}
@Test
public void pillarsSpanTheWholeVaultAndFormationsScaleWithVault() {
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.CENOTE).setPillarSpacing(12).setFormationFraction(0.3);
SubterrainPlan plan = direct(feature);
SubterrainRoom room = plan.sample(12, 0, 12).room();
for (int y = room.floorY(); y <= room.ceilingY(); y++) {
assertTrue("pillar y=" + y, plan.sample(12, y, 12).solid());
}
assertTrue(plan.sample(0, 0, 0).room().reservedPassage());
assertFalse(plan.sample(0, 0, 0).solid());
int formationCells = 0;
for (int x = -20; x <= 20; x++) {
for (int z = -20; z <= 20; z++) {
SubterrainCell middle = plan.sample(x, 0, z);
if (!middle.occupied()) {
continue;
}
SubterrainRoom context = middle.room();
int quarter = Math.max(1, context.vaultHeight() / 4);
if (plan.sample(x, context.ceilingY() - quarter, z).solid()) {
formationCells++;
}
}
}
assertTrue(formationCells > 0);
}
@Test
public void configOrderAndChunkTraversalDoNotChangeIdentityOrCells() {
IrisSubterrainFeature cenote = feature(IrisSubterrainFamily.CENOTE);
IrisSubterrainFeature fault = feature(IrisSubterrainFamily.TECTONIC_FAULT);
List<IrisSubterrainFeature> features = new ArrayList<>(List.of(cenote, fault));
SubterrainPlanner first = new SubterrainPlanner(new SubterrainPlanner.Options(features, 7123, -64, 97));
Collections.reverse(features);
SubterrainPlanner reordered = new SubterrainPlanner(new SubterrainPlanner.Options(features, 7123, -64, 97));
List<SubterrainPlan> plans = first.plansForBounds(-1024, -1024, 1024, 1024);
assertEquals(plans.stream().map(SubterrainPlan::id).toList(),
reordered.plansForBounds(-1024, -1024, 1024, 1024).stream().map(SubterrainPlan::id).toList());
for (SubterrainPlan plan : plans) {
for (int x = plan.bounds().minX(); x <= plan.bounds().maxX(); x += 13) {
for (int z = plan.bounds().minZ(); z <= plan.bounds().maxZ(); z += 13) {
int chunkX = Math.floorDiv(x, 16) * 16;
int chunkZ = Math.floorDiv(z, 16) * 16;
List<SubterrainPlan> chunk = first.plansForBounds(chunkX - 1, chunkZ - 1, chunkX + 16, chunkZ + 16);
for (int y = plan.bounds().minY(); y <= plan.bounds().maxY(); y += 7) {
assertEquals(first.sample(x, y, z), first.sample(chunk, x, y, z));
assertEquals(first.sample(x, y, z), reordered.sample(x, y, z));
}
}
}
}
}
@Test
public void immutableSnapshotsAndSeedsControlPlans() {
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.CENOTE);
SubterrainPlanner planner = planner(feature);
List<String> before = planner.plansForBounds(-1024, -1024, 1024, 1024).stream().map(SubterrainPlan::id).toList();
feature.setRadius(8).setBiome("changed").setId("changed");
assertEquals(before, planner.plansForBounds(-1024, -1024, 1024, 1024).stream().map(SubterrainPlan::id).toList());
assertEquals("subterranean/example", first(planner).biome());
SubterrainPlanner anotherSeed = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 999, -64, 97));
assertNotEquals(before, anotherSeed.plansForBounds(-1024, -1024, 1024, 1024).stream().map(SubterrainPlan::id).toList());
}
@Test
public void narrowYBandNeverClipsSealsIntoWorld() {
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.CENOTE).setWorldYRange(new IrisRange(0, 10));
assertTrue(planner(feature).plansForBounds(-2048, -2048, 2048, 2048).isEmpty());
}
@Test
public void solidSealsWinOverHigherPriorityUnrelatedCarving() {
SubterrainPlan lower = direct(feature(IrisSubterrainFamily.CENOTE));
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.TECTONIC_FAULT).setPriority(100);
SubterrainPlanner.Definition definition = SubterrainPlanner.Definition.from(feature);
SubterrainPlan upper = new SubterrainPlan(new SubterrainPlan.Options(definition, 32, 0, 0, false, "fault:other"));
SubterrainPlanner planner = planner(feature);
assertTrue(upper.sample(32, -14, 0).occupied());
assertTrue(lower.sample(32, -14, 0).solid());
assertTrue(planner.sample(List.of(upper, lower), 32, -14, 0).solid());
}
@Test(expected = IllegalArgumentException.class)
public void shortFaultDefinitionsAreRejected() {
planner(feature(IrisSubterrainFamily.TECTONIC_FAULT).setLength(120));
}
@Test
public void footprintAdmissionRejectsTheEntireFeatureAcrossSavedOwnership() {
SubterrainPlanner original = planner(feature(IrisSubterrainFamily.CENOTE));
SubterrainPlan plan = first(original);
SubterrainPosition anchor = plan.anchor();
int protectedX = plan.bounds().maxX();
int protectedZ = plan.bounds().maxZ();
SubterrainPlanner restricted = original.restrictTo(bounds ->
!bounds.intersects(protectedX, protectedZ, protectedX, protectedZ));
assertTrue(original.sample(anchor.x(), anchor.y(), anchor.z()).occupied());
assertFalse(restricted.plansForBounds(plan.bounds().minX(), plan.bounds().minZ(),
plan.bounds().maxX(), plan.bounds().maxZ()).stream().anyMatch(candidate -> candidate.id().equals(plan.id())));
assertFalse(restricted.sample(anchor.x(), anchor.y(), anchor.z()).owned());
assertTrue(original.sample(anchor.x(), anchor.y(), anchor.z()).occupied());
}
@Test(expected = IllegalArgumentException.class)
public void duplicateIdentifiersAreRejected() {
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.CENOTE);
new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature, feature), 1, -64, 97));
}
private int assertRetained(SubterrainPlanner planner, List<SubterrainPlan> candidates, SubterrainCell.Kind kind) {
int count = 0;
SubterrainBounds bounds = candidates.getFirst().bounds();
int[][] offsets = {{-1, 0, 0}, {1, 0, 0}, {0, 0, -1}, {0, 0, 1}, {0, -1, 0}};
for (int x = bounds.minX(); x <= bounds.maxX(); x++) {
for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) {
for (int y = bounds.minY(); y <= bounds.maxY(); y++) {
SubterrainCell cell = planner.sample(candidates, x, y, z);
if (cell.kind() != kind) {
continue;
}
count++;
for (int[] offset : offsets) {
SubterrainCell neighbor = planner.sample(candidates, x + offset[0], y + offset[1], z + offset[2]);
assertTrue("fluid at " + x + "," + y + "," + z, neighbor.solid() || neighbor.kind() == kind);
}
}
}
}
return count;
}
private IrisSubterrainFeature feature(IrisSubterrainFamily family) {
return new IrisSubterrainFeature().setId(family.name().toLowerCase()).setFamily(family)
.setProbability(1).setWorldYRange(new IrisRange(-64, 96)).setBiome("subterranean/example")
.setPillarSpacing(0).setFormationFraction(0);
}
private IrisSubterrainFeature compactFeature(IrisSubterrainFamily family) {
return feature(family).setLength(64).setRadius(12).setHeight(24).setChimneyHeight(8)
.setPillarSpacing(12).setFormationFraction(0.2);
}
private SubterrainPlanner planner(IrisSubterrainFeature feature) {
return new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 7123, -64, 97));
}
private SubterrainPlan first(SubterrainPlanner planner) {
List<SubterrainPlan> plans = planner.plansForBounds(-1024, -1024, 1024, 1024);
assertFalse(plans.isEmpty());
return plans.getFirst();
}
private SubterrainPlan direct(IrisSubterrainFeature feature) {
return new SubterrainPlan(new SubterrainPlan.Options(SubterrainPlanner.Definition.from(feature),
0, 0, 0, false, feature.getId() + ":test"));
}
}
@@ -0,0 +1,129 @@
package art.arcane.iris.generation.subterrain;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.iris.generation.block.B;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.matter.MatterCavern;
import org.mockito.MockedStatic;
import org.junit.Test;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class SubterrainRasterizerTest {
@Test
public void lavaOverridePublishesLavaBlocksAndCavernStateForCenotesAndTerraces() {
NativeBlockState lava = mock(NativeBlockState.class);
when(lava.key()).thenReturn("minecraft:lava");
try (MockedStatic<B> blocks = mockStatic(B.class)) {
blocks.when(() -> B.getState("minecraft:lava")).thenReturn(lava);
for (IrisSubterrainFamily family : List.of(IrisSubterrainFamily.CENOTE, IrisSubterrainFamily.TRAVERTINE_TERRACES)) {
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("lava-room").setFamily(family)
.setFluid(IrisSubterrainFluid.LAVA).setProbability(1).setLength(64).setRadius(12)
.setHeight(24).setPillarSpacing(0).setFormationFraction(0)
.setBiome("underground/lava").setWorldYRange(new IrisRange(-48, 60));
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 83L, -64, 128));
SubterrainPlan plan = planner.plansForBounds(-512, -512, 512, 512).getFirst();
int[] fluids = {0};
SubterrainRasterizer.rasterize(planner, plan.centerX() >> 4, plan.centerZ() >> 4, (x, y, z, cell) -> {
if (cell.fluid()) {
fluids[0]++;
assertEquals(SubterrainCell.Kind.LAVA, cell.kind());
assertEquals("minecraft:lava", cell.material());
assertEquals(lava, SubterrainRasterizer.state(cell));
MatterCavern cavern = SubterrainRasterizer.cavern(cell);
assertEquals(2, cavern.getLiquid());
assertTrue(cavern.isLava());
assertFalse(cavern.isWater());
assertEquals("underground/lava", cavern.getCustomBiome());
assertEquals(cell, planner.sample(x, y, z));
}
});
assertTrue(family.toString(), fluids[0] > 0);
}
}
}
@Test
public void sealingMaterialRejectsPartialAndUnresolvedBlocks() {
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("sealed-room").setSolid("minecraft:oak_slab");
NativeBlockState partial = mock(NativeBlockState.class);
when(partial.isSolid()).thenReturn(true);
try (MockedStatic<B> blocks = mockStatic(B.class)) {
blocks.when(() -> B.getStateOrNull("minecraft:oak_slab", false)).thenReturn(partial);
assertThrows(IllegalArgumentException.class, () -> SubterrainRasterizer.validateMaterials(List.of(feature)));
feature.setSolid("minecraft:missing_block");
assertThrows(IllegalArgumentException.class, () -> SubterrainRasterizer.validateMaterials(List.of(feature)));
NativeBlockState full = mock(NativeBlockState.class);
when(full.key()).thenReturn("minecraft:stone");
when(full.isSolid()).thenReturn(true);
when(full.isOccluding()).thenReturn(true);
feature.setSolid("minecraft:stone");
blocks.when(() -> B.getStateOrNull("minecraft:stone", false)).thenReturn(full);
SubterrainRasterizer.validateMaterials(List.of(feature));
when(full.isWaterLogged()).thenReturn(true);
assertThrows(IllegalArgumentException.class, () -> SubterrainRasterizer.validateMaterials(List.of(feature)));
when(full.isWaterLogged()).thenReturn(false);
when(full.key()).thenReturn("minecraft:gravel");
feature.setSolid("minecraft:gravel");
blocks.when(() -> B.getStateOrNull("minecraft:gravel", false)).thenReturn(full);
assertThrows(IllegalArgumentException.class, () -> SubterrainRasterizer.validateMaterials(List.of(feature)));
}
}
@Test
public void chunksRasterizeTheSameCellsInEitherOrderAndMatchQueries() {
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("retained-cenote")
.setProbability(1).setRadius(20).setHeight(24).setPillarSpacing(12)
.setWorldYRange(new IrisRange(-48, 60));
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 83L, -64, 128));
SubterrainPlan plan = planner.plansForBounds(-512, -512, 512, 512).getFirst();
int firstX = plan.centerX() >> 4;
int firstZ = plan.centerZ() >> 4;
Map<SubterrainPosition, SubterrainCell> forward = new HashMap<>();
Map<SubterrainPosition, SubterrainCell> reverse = new HashMap<>();
for (int x = firstX - 1; x <= firstX + 1; x++) {
SubterrainRasterizer.rasterize(planner, x, firstZ, (xx, y, z, cell) -> forward.put(new SubterrainPosition(xx, y, z), cell));
}
for (int x = firstX + 1; x >= firstX - 1; x--) {
SubterrainRasterizer.rasterize(planner, x, firstZ, (xx, y, z, cell) -> reverse.put(new SubterrainPosition(xx, y, z), cell));
}
assertFalse(forward.isEmpty());
assertEquals(forward, reverse);
int wet = 0;
for (Map.Entry<SubterrainPosition, SubterrainCell> entry : forward.entrySet()) {
SubterrainPosition position = entry.getKey();
SubterrainCell cell = entry.getValue();
assertEquals(cell, planner.sample(position.x(), position.y(), position.z()));
if (cell.fluid()) {
wet++;
for (int[] offset : List.of(new int[]{1, 0, 0}, new int[]{-1, 0, 0}, new int[]{0, 0, 1}, new int[]{0, 0, -1}, new int[]{0, -1, 0})) {
SubterrainCell neighbor = planner.sample(position.x() + offset[0], position.y() + offset[1], position.z() + offset[2]);
assertTrue(neighbor.solid() || neighbor.kind() == cell.kind());
}
}
}
assertTrue(wet > 0);
}
@Test
public void protectionKeepsSolidAndPassageWhileAllowingRoomDecoration() {
SubterrainRoom room = new SubterrainRoom("room", "biome", IrisSubterrainFamily.CENOTE,
0, 0, 0, 0, 0, 0, -8, 12, 10, -4, false, false, SubterrainCell.Kind.AIR);
assertFalse(SubterrainRasterizer.protectsPlacement(SubterrainCell.OUTSIDE));
assertFalse(SubterrainRasterizer.protectsPlacement(new SubterrainCell(SubterrainCell.Kind.AIR, "minecraft:cave_air", room)));
assertTrue(SubterrainRasterizer.protectsPlacement(new SubterrainCell(SubterrainCell.Kind.SOLID, "minecraft:stone", room)));
SubterrainRoom passage = new SubterrainRoom("room", "biome", IrisSubterrainFamily.CENOTE,
0, 0, 0, 0, 0, 0, -8, 12, 10, -4, true, false, SubterrainCell.Kind.AIR);
assertTrue(SubterrainRasterizer.protectsPlacement(new SubterrainCell(SubterrainCell.Kind.AIR, "minecraft:cave_air", passage)));
}
}
@@ -0,0 +1,304 @@
package art.arcane.iris.generation.subterrain;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.biome.IrisBiomePaletteLayer;
import art.arcane.iris.generation.block.B;
import art.arcane.iris.generation.block.IrisBlockData;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.stage.IrisCarveModifier;
import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.loading.ResourceLoader;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.hunk.Hunk;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.times;
public class SubterrainSurfaceMaterialsTest {
@Test
public void everyGeometryUsesOwningBiomeFloorCeilingAndWallWithoutChangingOccupancy() {
for (IrisSubterrainFamily family : IrisSubterrainFamily.values()) {
try (Fixture fixture = new Fixture(family)) {
Map<SubterrainSurfaceMaterials.Surface, SubterrainPosition> surfaces = fixture.surfaces();
for (SubterrainSurfaceMaterials.Surface surface : List.of(SubterrainSurfaceMaterials.Surface.FLOOR,
SubterrainSurfaceMaterials.Surface.CEILING, SubterrainSurfaceMaterials.Surface.WALL)) {
SubterrainPosition position = surfaces.get(surface);
assertTrue(family + " " + surface, position != null);
SubterrainCell before = fixture.cell(position);
NativeBlockState expected = switch (surface) {
case FLOOR -> fixture.floor;
case CEILING -> fixture.ceiling;
case WALL -> fixture.wall;
default -> fixture.fallback;
};
assertSame(expected, fixture.materials.state(position.x(), position.y(), position.z(), before));
assertTrue(SubterrainRasterizer.protectsPlacement(before));
assertEquals(before, fixture.cell(position));
int chunkX = position.x() >> 4;
int chunkZ = position.z() >> 4;
List<SubterrainPlan> local = fixture.planner.plansForBounds((chunkX << 4) - 1, (chunkZ << 4) - 1,
(chunkX << 4) + 16, (chunkZ << 4) + 16);
SubterrainSurfaceMaterials chunk = fixture.materials(local);
assertSame(expected, chunk.state(position.x(), position.y(), position.z(), before));
}
SubterrainPosition anchor = fixture.plan.anchor();
SubterrainCell occupied = fixture.cell(anchor);
assertTrue(occupied.occupied());
assertSame(SubterrainRasterizer.state(occupied), fixture.materials.state(anchor.x(), anchor.y(), anchor.z(), occupied));
}
}
}
@Test
public void finalPreservationRestoresBiomeMaterialRatherThanErasingIt() throws Exception {
try (Fixture fixture = new Fixture(IrisSubterrainFamily.CENOTE)) {
Engine engine = mock(Engine.class);
when(engine.getComplex()).thenReturn(fixture.complex);
when(engine.getData()).thenReturn(fixture.data);
when(engine.getDimension()).thenReturn(fixture.dimension);
when(engine.getWorld()).thenReturn(IrisWorld.builder().minHeight(-64).maxHeight(128).build());
when(fixture.complex.getSubterrainPlanner()).thenReturn(fixture.planner);
IrisCarveModifier modifier = mock(IrisCarveModifier.class, CALLS_REAL_METHODS);
doReturn(engine).when(modifier).getEngine();
Field random = IrisCarveModifier.class.getDeclaredField("rng");
random.setAccessible(true);
random.set(modifier, fixture.random);
for (Map.Entry<SubterrainSurfaceMaterials.Surface, SubterrainPosition> entry : fixture.surfaces().entrySet()) {
SubterrainPosition position = entry.getValue();
Hunk<NativeBlockState> output = Hunk.newArrayHunk(16, 192, 16);
int x = position.x() & 15;
int y = position.y() + 64;
int z = position.z() & 15;
output.setRaw(x, y, z, null);
modifier.preserveSubterrain((position.x() >> 4) << 4, (position.z() >> 4) << 4, output);
NativeBlockState expected = fixture.materials.state(position.x(), position.y(), position.z(), fixture.cell(position));
assertSame(expected, output.getRaw(x, y, z));
output.setRaw(x, y, z, fixture.fallback);
modifier.preserveSubterrain((position.x() >> 4) << 4, (position.z() >> 4) << 4, output);
assertSame(expected, output.getRaw(x, y, z));
}
}
}
@Test
public void realMultiEntryPalettesRestoreDeterministicallyAcrossChunkVisitOrders() throws Exception {
try (Fixture fixture = new Fixture(IrisSubterrainFamily.CENOTE)) {
Map<SubterrainPosition, String> forward = fixture.restoredMaterials(false);
Map<SubterrainPosition, String> reverse = fixture.restoredMaterials(true);
Map<SubterrainPosition, String> repeated = fixture.restoredMaterials(false);
assertEquals(forward, reverse);
assertEquals(forward, repeated);
assertTrue(forward.size() > 100);
assertTrue(forward.values().stream().distinct().count() >= 6);
}
}
@Test
public void missingBiomeRetainsTheShellAndIsLookedUpOncePerResolver() {
try (Fixture fixture = new Fixture(IrisSubterrainFamily.CENOTE)) {
when(fixture.data.getBiomeLoader().load("room-biome")).thenReturn(null);
for (SubterrainPosition position : fixture.surfaces().values()) {
assertSame(fixture.fallback, fixture.materials.state(position.x(), position.y(), position.z(), fixture.cell(position)));
}
verify(fixture.data.getBiomeLoader(), times(1)).load("room-biome");
}
}
@Test
public void unsafeSurfaceChoicesFallBackToTheRetainedSolid() {
try (Fixture fixture = new Fixture(IrisSubterrainFamily.CENOTE)) {
SubterrainPosition position = fixture.surfaces().get(SubterrainSurfaceMaterials.Surface.FLOOR);
NativeBlockState partial = solid("minecraft:oak_slab");
when(partial.isOccluding()).thenReturn(false);
NativeBlockState wet = solid("minecraft:stone[waterlogged=true]");
when(wet.isWaterLogged()).thenReturn(true);
NativeBlockState fluid = solid("minecraft:water");
when(fluid.isFluid()).thenReturn(true);
NativeBlockState custom = solid("minecraft:stone");
when(custom.isCustom()).thenReturn(true);
NativeBlockState gravity = solid("minecraft:gravel");
for (NativeBlockState unsafe : List.of(partial, wet, fluid, custom, gravity)) {
fixture.floorPalette(unsafe);
assertSame(fixture.fallback, fixture.materials.state(position.x(), position.y(), position.z(), fixture.cell(position)));
}
fixture.floorPalette(null);
assertSame(fixture.fallback, fixture.materials.state(position.x(), position.y(), position.z(), fixture.cell(position)));
}
}
private static NativeBlockState solid(String key) {
NativeBlockState state = mock(NativeBlockState.class);
when(state.key()).thenReturn(key);
when(state.isSolid()).thenReturn(true);
when(state.isOccluding()).thenReturn(true);
return state;
}
private static final class Fixture implements AutoCloseable {
private final NativeBlockState fallback = solid("minecraft:stone");
private final NativeBlockState floor = solid("minecraft:clay");
private final NativeBlockState ceiling = solid("minecraft:calcite");
private final NativeBlockState wall = solid("minecraft:andesite");
private final IrisDimension dimension = new IrisDimension();
private final IrisComplex complex = mock(IrisComplex.class);
private final IrisData data = mock(IrisData.class);
private final IrisBiome biome = mock(IrisBiome.class);
private final RNG random = new RNG(44339L);
private final SubterrainPlanner planner;
private final SubterrainPlan plan;
private final List<SubterrainPlan> plans;
private final SubterrainSurfaceMaterials materials;
private final MockedStatic<B> blocks = mockStatic(B.class);
@SuppressWarnings("unchecked")
private Fixture(IrisSubterrainFamily family) {
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("room").setBiome("room-biome")
.setFamily(family).setProbability(1).setRadius(12).setHeight(16).setLength(200)
.setChimneyHeight(8).setWorldYRange(new IrisRange(-48, 64));
planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 83L, -64, 128));
plan = planner.plansForBounds(-512, -512, 512, 512).getFirst();
plans = planner.plansForBounds(plan.bounds().minX() - 1, plan.bounds().minZ() - 1,
plan.bounds().maxX() + 1, plan.bounds().maxZ() + 1);
ResourceLoader<IrisBiome> loader = mock(ResourceLoader.class);
when(data.getBiomeLoader()).thenReturn(loader);
when(loader.load("room-biome")).thenReturn(biome);
IrisBiomePaletteLayer wallPalette = mock(IrisBiomePaletteLayer.class);
when(biome.getWall()).thenReturn(wallPalette);
when(wallPalette.get(eq(random), anyDouble(), anyDouble(), anyDouble(), eq(data))).thenReturn(wall);
when(biome.generateCeilingLayers(eq(dimension), anyDouble(), anyDouble(), eq(random), eq(1), anyInt(), eq(data), eq(complex)))
.thenReturn(new KList<>(ceiling));
floorPalette(floor);
NativeBlockState air = mock(NativeBlockState.class);
when(air.key()).thenReturn("minecraft:cave_air");
NativeBlockState water = mock(NativeBlockState.class);
when(water.key()).thenReturn("minecraft:water");
NativeBlockState lava = mock(NativeBlockState.class);
when(lava.key()).thenReturn("minecraft:lava");
blocks.when(() -> B.getState(anyString())).thenAnswer(invocation -> switch ((String) invocation.getArgument(0)) {
case "minecraft:water" -> water;
case "minecraft:lava" -> lava;
case "minecraft:cave_air" -> air;
default -> fallback;
});
materials = materials(plans);
}
private Map<SubterrainPosition, String> restoredMaterials(boolean reverse) throws Exception {
IrisBiome authored = new IrisBiome().setLayers(new KList<>(palette("minecraft:clay", "minecraft:terracotta")))
.setCaveCeilingLayers(new KList<>(palette("minecraft:calcite", "minecraft:white_concrete")))
.setWall(palette("minecraft:andesite", "minecraft:tuff"));
when(data.getBiomeLoader().load("room-biome")).thenReturn(authored);
Engine engine = mock(Engine.class);
when(engine.getComplex()).thenReturn(complex);
when(engine.getData()).thenReturn(data);
when(engine.getDimension()).thenReturn(dimension);
when(engine.getWorld()).thenReturn(IrisWorld.builder().minHeight(-64).maxHeight(128).build());
when(complex.getSubterrainPlanner()).thenReturn(planner);
IrisCarveModifier modifier = mock(IrisCarveModifier.class, CALLS_REAL_METHODS);
doReturn(engine).when(modifier).getEngine();
Field randomField = IrisCarveModifier.class.getDeclaredField("rng");
randomField.setAccessible(true);
randomField.set(modifier, random);
List<SubterrainPosition> chunks = new ArrayList<>();
SubterrainBounds bounds = plan.bounds();
for (int x = bounds.minX() >> 4; x <= bounds.maxX() >> 4; x++) {
for (int z = bounds.minZ() >> 4; z <= bounds.maxZ() >> 4; z++) {
chunks.add(new SubterrainPosition(x, 0, z));
}
}
if (reverse) {
Collections.reverse(chunks);
}
Map<SubterrainPosition, String> result = new HashMap<>();
for (SubterrainPosition chunk : chunks) {
Hunk<NativeBlockState> output = Hunk.newArrayHunk(16, 192, 16);
for (int draw = 0; draw < 19; draw++) {
random.nextLong();
}
modifier.preserveSubterrain(chunk.x() << 4, chunk.z() << 4, output);
modifier.preserveSubterrain(chunk.x() << 4, chunk.z() << 4, output);
SubterrainRasterizer.rasterize(planner, chunk.x(), chunk.z(), (x, y, z, cell) -> {
if (cell.solid() && materials.surface(x, y, z, cell) != SubterrainSurfaceMaterials.Surface.INTERIOR) {
result.put(new SubterrainPosition(x, y, z), output.getRaw(x & 15, y + 64, z & 15).key());
}
});
}
return result;
}
private IrisBiomePaletteLayer palette(String first, String second) {
NativeBlockState firstState = solid(first);
NativeBlockState secondState = solid(second);
blocks.when(() -> B.getStateOrNull(first, false)).thenReturn(firstState);
blocks.when(() -> B.getStateOrNull(second, false)).thenReturn(secondState);
return new IrisBiomePaletteLayer().setZoom(1)
.setPalette(new KList<>(new IrisBlockData(first), new IrisBlockData(second)));
}
private void floorPalette(NativeBlockState state) {
when(biome.generateLayers(eq(dimension), anyDouble(), anyDouble(), eq(random), eq(1), anyInt(), eq(data), eq(complex)))
.thenReturn(state == null ? new KList<>() : new KList<>(state));
}
private SubterrainSurfaceMaterials materials(List<SubterrainPlan> candidates) {
return new SubterrainSurfaceMaterials(planner, candidates, dimension, complex, data, random, -64);
}
private SubterrainCell cell(SubterrainPosition position) {
return planner.sample(plans, position.x(), position.y(), position.z());
}
private Map<SubterrainSurfaceMaterials.Surface, SubterrainPosition> surfaces() {
Map<SubterrainSurfaceMaterials.Surface, SubterrainPosition> result = new EnumMap<>(SubterrainSurfaceMaterials.Surface.class);
SubterrainBounds bounds = plan.bounds();
for (int x = bounds.minX(); x <= bounds.maxX(); x++) {
for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) {
for (int y = bounds.minY(); y <= bounds.maxY(); y++) {
SubterrainCell cell = planner.sample(plans, x, y, z);
if (!cell.solid()) {
continue;
}
result.putIfAbsent(materials.surface(x, y, z, cell), new SubterrainPosition(x, y, z));
if (result.size() == SubterrainSurfaceMaterials.Surface.values().length) {
return result;
}
}
}
}
return result;
}
@Override
public void close() {
blocks.close();
}
}
}
@@ -116,7 +116,7 @@ public class IrisDimensionCarvingResolverParityTest {
doReturn(generator).when(parent).getChildrenGenerator(anyLong(), any(IrisData.class)); doReturn(generator).when(parent).getChildrenGenerator(anyLong(), any(IrisData.class));
IrisDimensionCarvingResolver.Snapshot snapshot = IrisDimensionCarvingResolver.snapshot(fixture.engine); IrisDimensionCarvingResolver.Snapshot snapshot = IrisDimensionCarvingResolver.snapshot(fixture.engine);
assertSame(fixture.engine.getData().getBiomeLoader().load("child-a"), snapshot.resolveBiome(-17, 40, 23)); assertSame(fixture.engine.getData().getBiomeLoader().load("root-low"), snapshot.resolveBiome(-17, 40, 23));
assertSame(fixture.engine.getData().getBiomeLoader().load("root-low"), snapshot.resolveBiome(-17, 41, 23)); assertSame(fixture.engine.getData().getBiomeLoader().load("root-low"), snapshot.resolveBiome(-17, 41, 23));
verify(generator, times(2)).noiseFast2D(-17D, 23D); verify(generator, times(2)).noiseFast2D(-17D, 23D);
} }
@@ -140,13 +140,30 @@ public class IrisDimensionCarvingResolverParityTest {
doReturn(generator).when(parent).getChildrenGenerator(anyLong(), any(IrisData.class)); doReturn(generator).when(parent).getChildrenGenerator(anyLong(), any(IrisData.class));
IrisDimensionCarvingEntry selected = IrisDimensionCarvingResolver.resolveFromRoot( IrisDimensionCarvingEntry selected = IrisDimensionCarvingResolver.resolveFromRoot(
engine, parent, -17, 23, new IrisDimensionCarvingResolver.State()); engine, parent, -17, -1500, 23, new IrisDimensionCarvingResolver.State());
assertSame(engine.getDimension().getCarvingEntryIndex().get("child-a"), selected); assertSame(engine.getDimension().getCarvingEntryIndex().get("child-a"), selected);
verify(generator).noiseFast2D(-17D, 23D); verify(generator).noiseFast2D(-17D, 23D);
verify(generator, never()).fit2D(anyInt(), anyInt(), anyDouble(), anyDouble()); verify(generator, never()).fit2D(anyInt(), anyInt(), anyDouble(), anyDouble());
} }
@Test
public void childWorldYBandIsCheckedAndCachedPerHeight() {
Fixture fixture = createFixture();
IrisDimensionCarvingEntry parent = spy(fixture.engine.getDimension().getCarvingEntryIndex().get("root-low"));
parent.setChildRecursionDepth(1);
parent.setChildren(new KList<>("child-a"));
IrisDimensionCarvingEntry child = fixture.engine.getDimension().getCarvingEntryIndex().get("child-a");
child.setWorldYRange(new IrisRange(-20, 20));
CNG generator = mock(CNG.class);
doReturn(0.5D).when(generator).noiseFast2D(anyDouble(), anyDouble());
doReturn(generator).when(parent).getChildrenGenerator(anyLong(), any(IrisData.class));
IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State();
assertSame(child, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, 7, 0, 9, state));
assertSame(parent, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, 7, 30, 9, state));
assertSame(child, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, 7, 0, 9, state));
}
@Test @Test
public void repeatedColumnResolutionSamplesChildrenOnceAcrossHeights() { public void repeatedColumnResolutionSamplesChildrenOnceAcrossHeights() {
Fixture fixture = createFixture(); Fixture fixture = createFixture();
@@ -161,7 +178,7 @@ public class IrisDimensionCarvingResolverParityTest {
for (int height = 0; height < 16; height++) { for (int height = 0; height < 16; height++) {
for (int x = -16; x < 0; x++) { for (int x = -16; x < 0; x++) {
for (int z = 0; z < 16; z++) { for (int z = 0; z < 16; z++) {
assertSame(child, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, x, z, state)); assertSame(child, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, x, -1500, z, state));
} }
} }
} }
@@ -186,8 +203,8 @@ public class IrisDimensionCarvingResolverParityTest {
IrisDimensionCarvingEntry child = fixture.engine.getDimension().getCarvingEntryIndex().get("child-a"); IrisDimensionCarvingEntry child = fixture.engine.getDimension().getCarvingEntryIndex().get("child-a");
IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State(); IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State();
assertSame(child, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, -17, 23, state)); assertSame(child, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, -17, -1500, 23, state));
assertSame(parent, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, -17, 23, state)); assertSame(parent, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, -17, -1500, 23, state));
} }
} }
@@ -204,11 +221,11 @@ public class IrisDimensionCarvingResolverParityTest {
IrisDimensionCarvingEntry firstChild = fixture.engine.getDimension().getCarvingEntryIndex().get("child-a"); IrisDimensionCarvingEntry firstChild = fixture.engine.getDimension().getCarvingEntryIndex().get("child-a");
IrisDimensionCarvingEntry replacementChild = replacement.engine.getDimension().getCarvingEntryIndex().get("child-a"); IrisDimensionCarvingEntry replacementChild = replacement.engine.getDimension().getCarvingEntryIndex().get("child-a");
assertSame(firstChild, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, -17, 23, state)); assertSame(firstChild, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, -17, -1500, 23, state));
doReturn(replacement.engine.getData()).when(fixture.engine).getData(); doReturn(replacement.engine.getData()).when(fixture.engine).getData();
assertSame(firstChild, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, -17, 23, state)); assertSame(firstChild, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, -17, -1500, 23, state));
assertSame(replacementChild, IrisDimensionCarvingResolver.resolveFromRoot(replacement.engine, parent, -17, 23, state)); assertSame(replacementChild, IrisDimensionCarvingResolver.resolveFromRoot(replacement.engine, parent, -17, -1500, 23, state));
assertSame(replacementChild, IrisDimensionCarvingResolver.resolveFromRoot(replacement.engine, parent, -17, 23, state)); assertSame(replacementChild, IrisDimensionCarvingResolver.resolveFromRoot(replacement.engine, parent, -17, -1500, 23, state));
verify(generator, times(3)).noiseFast2D(-17D, 23D); verify(generator, times(3)).noiseFast2D(-17D, 23D);
} }
@@ -225,8 +242,8 @@ public class IrisDimensionCarvingResolverParityTest {
for (int worldX = -384; worldX <= 384; worldX += 29) { for (int worldX = -384; worldX <= 384; worldX += 29) {
for (int worldZ = -384; worldZ <= 384; worldZ += 31) { for (int worldZ = -384; worldZ <= 384; worldZ += 31) {
IrisDimensionCarvingEntry legacyResolved = legacyResolveFromRoot(fixture.engine, legacyRoot, worldX, worldZ); IrisDimensionCarvingEntry legacyResolved = legacyResolveFromRoot(fixture.engine, legacyRoot, worldX, worldY, worldZ);
IrisDimensionCarvingEntry statefulResolved = IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, statefulRoot, worldX, worldZ, state); IrisDimensionCarvingEntry statefulResolved = IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, statefulRoot, worldX, worldY, worldZ, state);
assertSame("entry mismatch at worldY=" + worldY + " worldX=" + worldX + " worldZ=" + worldZ, legacyResolved, statefulResolved); assertSame("entry mismatch at worldY=" + worldY + " worldX=" + worldX + " worldZ=" + worldZ, legacyResolved, statefulResolved);
assertSame(legacyResolveEntryBiome(fixture.engine, legacyResolved), assertSame(legacyResolveEntryBiome(fixture.engine, legacyResolved),
IrisDimensionCarvingResolver.resolveBiome(fixture.engine, worldX, worldY, worldZ, state)); IrisDimensionCarvingResolver.resolveBiome(fixture.engine, worldX, worldY, worldZ, state));
@@ -248,8 +265,8 @@ public class IrisDimensionCarvingResolverParityTest {
for (int worldX = -640; worldX <= 640; worldX += 79) { for (int worldX = -640; worldX <= 640; worldX += 79) {
for (int worldZ = -640; worldZ <= 640; worldZ += 83) { for (int worldZ = -640; worldZ <= 640; worldZ += 83) {
IrisDimensionCarvingEntry legacyResolved = legacyResolveFromRoot(fixture.engine, legacyRoot, worldX, worldZ); IrisDimensionCarvingEntry legacyResolved = legacyResolveFromRoot(fixture.engine, legacyRoot, worldX, worldY, worldZ);
IrisDimensionCarvingEntry statefulResolved = IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, statefulRoot, worldX, worldZ, state); IrisDimensionCarvingEntry statefulResolved = IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, statefulRoot, worldX, worldY, worldZ, state);
assertSame("mixed entry mismatch at worldY=" + worldY + " worldX=" + worldX + " worldZ=" + worldZ, legacyResolved, statefulResolved); assertSame("mixed entry mismatch at worldY=" + worldY + " worldX=" + worldX + " worldZ=" + worldZ, legacyResolved, statefulResolved);
assertSame(legacyResolveEntryBiome(fixture.engine, legacyResolved), assertSame(legacyResolveEntryBiome(fixture.engine, legacyResolved),
IrisDimensionCarvingResolver.resolveBiome(fixture.engine, worldX, worldY, worldZ, state)); IrisDimensionCarvingResolver.resolveBiome(fixture.engine, worldX, worldY, worldZ, state));
@@ -406,7 +423,7 @@ public class IrisDimensionCarvingResolverParityTest {
Fixture replacement = createMixedDepthFixture(); Fixture replacement = createMixedDepthFixture();
IrisDimensionCarvingEntry expectedEntry = legacyResolveFromRoot(replacement.engine, IrisDimensionCarvingEntry expectedEntry = legacyResolveFromRoot(replacement.engine,
legacyResolveRootEntry(replacement.engine, 83), -17, 23); legacyResolveRootEntry(replacement.engine, 83), -17, 83, 23);
IrisBiome expected = legacyResolveEntryBiome(replacement.engine, expectedEntry); IrisBiome expected = legacyResolveEntryBiome(replacement.engine, expectedEntry);
assertSame(expected, IrisDimensionCarvingResolver.resolveBiome(replacement.engine, -17, 83, 23, state)); assertSame(expected, IrisDimensionCarvingResolver.resolveBiome(replacement.engine, -17, 83, 23, state));
assertSame(null, IrisDimensionCarvingResolver.resolveBiome(fixture.engine, -17, 83, 23, state)); assertSame(null, IrisDimensionCarvingResolver.resolveBiome(fixture.engine, -17, 83, 23, state));
@@ -692,7 +709,7 @@ public class IrisDimensionCarvingResolverParityTest {
for (int localZ = 0; localZ < 16; localZ++) { for (int localZ = 0; localZ < 16; localZ++) {
int worldZ = (chunkZ << 4) + localZ; int worldZ = (chunkZ << 4) + localZ;
int columnIndex = (localX << 4) | localZ; int columnIndex = (localX << 4) | localZ;
plan[columnIndex] = IrisDimensionCarvingResolver.resolveFromRoot(engine, rootEntry, worldX, worldZ, state); plan[columnIndex] = IrisDimensionCarvingResolver.resolveFromRoot(engine, rootEntry, worldX, 40, worldZ, state);
} }
} }
return plan; return plan;
@@ -728,7 +745,7 @@ public class IrisDimensionCarvingResolverParityTest {
return resolved; return resolved;
} }
private IrisDimensionCarvingEntry legacyResolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ) { private IrisDimensionCarvingEntry legacyResolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldY, int worldZ) {
if (rootEntry == null) { if (rootEntry == null) {
return null; return null;
} }
@@ -748,7 +765,8 @@ public class IrisDimensionCarvingResolverParityTest {
int depth = remainingDepth; int depth = remainingDepth;
while (depth > 0) { while (depth > 0) {
IrisDimensionCarvingEntry selected = legacySelectChild(engine, current, worldX, worldZ, entryIndex); IrisDimensionCarvingEntry selected = legacySelectChild(engine, current, worldX, worldZ, entryIndex);
if (selected == null || selected == current) { if (selected == null || selected == current || selected.getWorldYRange() != null
&& !selected.getWorldYRange().contains(worldY)) {
break; break;
} }
@@ -24,6 +24,9 @@ import art.arcane.iris.pack.loading.ResourceLoader;
import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.block.IrisBlockData; import art.arcane.iris.generation.block.IrisBlockData;
import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.subterrain.IrisSubterrainFeature;
import art.arcane.iris.generation.subterrain.IrisSubterrainFamily;
import art.arcane.iris.generation.subterrain.IrisSubterrainFluid;
import art.arcane.iris.world.entity.IrisEntity; import art.arcane.iris.world.entity.IrisEntity;
import art.arcane.iris.generation.noise.IrisExpression; import art.arcane.iris.generation.noise.IrisExpression;
import art.arcane.iris.generation.noise.IrisGenerator; import art.arcane.iris.generation.noise.IrisGenerator;
@@ -59,6 +62,7 @@ import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.ClassRule; import org.junit.ClassRule;
import org.junit.Test; import org.junit.Test;
import com.google.gson.Gson;
import java.io.File; import java.io.File;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@@ -70,6 +74,7 @@ import java.util.Map;
import java.util.TreeSet; import java.util.TreeSet;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -78,7 +83,7 @@ public class SchemaBuilderIdentityTest {
@ClassRule @ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean(); public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private static final String EXPECTED_SCHEMA_DIGEST = "d77cc4ea8255cbfdc4f8c49e04a3c422ca3e094399a514799ec703217f7797da"; private static final String EXPECTED_SCHEMA_DIGEST = "6f6dae0cd1077c45fe3f5ba512c1da4b0ab43bc8516f2d759bc8f7a798eb1c58";
private static final List<Class<?>> SCHEMA_ROOTS = List.of( private static final List<Class<?>> SCHEMA_ROOTS = List.of(
IrisDimension.class, IrisDimension.class,
@@ -127,6 +132,59 @@ public class SchemaBuilderIdentityTest {
assertEquals(EXPECTED_SCHEMA_DIGEST, digest(canonical.toString())); assertEquals(EXPECTED_SCHEMA_DIGEST, digest(canonical.toString()));
} }
@Test
public void dimensionParsesAndSerializesNestedSubterrainFeatures() {
Gson gson = new Gson();
IrisDimension dimension = gson.fromJson("""
{"subterrainFeatures":[{"id":"flooded-vault","family":"CENOTE",
"biome":"pack/cavern","fluid":"LAVA","worldYRange":{"min":-40,"max":24},
"spacing":384,"probability":0.75,"radius":28,"fluidDepth":6}]}
""", IrisDimension.class);
assertEquals(1, dimension.getSubterrainFeatures().size());
IrisSubterrainFeature feature = dimension.getSubterrainFeatures().get(0);
assertEquals("flooded-vault", feature.getId());
assertEquals(IrisSubterrainFamily.CENOTE, feature.getFamily());
assertEquals("pack/cavern", feature.getBiome());
assertEquals(IrisSubterrainFluid.LAVA, feature.getFluid());
assertEquals(-40D, feature.getWorldYRange().getMin(), 0D);
assertEquals(24D, feature.getWorldYRange().getMax(), 0D);
assertEquals(384, feature.getSpacing());
assertEquals(0.75D, feature.getProbability(), 0D);
assertEquals(28, feature.getRadius());
assertEquals(6, feature.getFluidDepth());
IrisDimension restored = gson.fromJson(gson.toJson(dimension), IrisDimension.class);
assertEquals(feature, restored.getSubterrainFeatures().get(0));
assertTrue(new IrisDimension().getSubterrainFeatures().isEmpty());
}
@Test
public void dimensionSchemaExposesNestedSubterrainFeatureConfiguration() {
JSONObject schema = new SchemaBuilder(IrisDimension.class, schemaData()).construct();
JSONObject features = schema.getJSONObject("properties").getJSONObject("subterrainFeatures");
assertEquals("array", features.getString("type"));
assertEquals(1, features.getInt("minItems"));
String reference = features.getJSONObject("items").getString("$ref");
JSONObject featureSchema = schema.getJSONObject("definitions")
.getJSONObject(reference.substring("#/definitions/".length()));
JSONArray alternatives = featureSchema.getJSONArray("anyOf");
assertEquals(2, alternatives.length());
assertEquals("object", alternatives.getJSONObject(0).getString("type"));
assertEquals("string", alternatives.getJSONObject(1).getString("type"));
assertEquals("#/definitions/enum-snippet-subterrain-feature",
alternatives.getJSONObject(1).getString("$ref"));
JSONObject properties = alternatives.getJSONObject(0).getJSONObject("properties");
assertTrue(properties.has("id"));
assertTrue(properties.has("family"));
assertTrue(properties.has("biome"));
assertTrue(properties.has("fluid"));
assertTrue(properties.has("worldYRange"));
assertEquals(32, properties.getJSONObject("spacing").getInt("minimum"));
assertEquals(8192, properties.getJSONObject("spacing").getInt("maximum"));
assertEquals(0D, properties.getJSONObject("probability").getDouble("minimum"), 0D);
assertEquals(1D, properties.getJSONObject("probability").getDouble("maximum"), 0D);
assertEquals(32, properties.getJSONObject("fluidDepth").getInt("maximum"));
}
private static String digest(String value) { private static String digest(String value) {
MessageDigest sha256; MessageDigest sha256;
try { try {
@@ -0,0 +1,55 @@
package art.arcane.iris.platform.bukkit.nms;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.runtime.DimensionStackLayout;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisDimensionCarvingResolver;
import art.arcane.iris.world.IrisWorld;
import org.junit.Test;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.mockito.Answers.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public class BukkitSubterrainBiomePrecedenceTest {
@Test
public void featureOwnsPublishedBiomeAboveDepthGateAndBeforeStackOwnership() throws Exception {
Engine engine = mock(Engine.class);
IrisBiome feature = mock(IrisBiome.class);
IrisDimension host = new IrisDimension();
when(engine.getComplex()).thenReturn(mock(IrisComplex.class));
when(engine.getWorld()).thenReturn(IrisWorld.builder().minHeight(-64).maxHeight(320).build());
when(engine.getDimension()).thenReturn(host);
when(engine.getSubterrainBiome(12, 224, -20)).thenReturn(feature);
@SuppressWarnings("unchecked")
BukkitBiomePolicy<String, String> policy = mock(BukkitBiomePolicy.class, CALLS_REAL_METHODS);
Field owner = BukkitBiomePolicy.class.getDeclaredField("engine");
owner.setAccessible(true);
owner.set(policy, engine);
DimensionStackLayout stack = mock(DimensionStackLayout.class);
Method resolve = BukkitBiomePolicy.class.getDeclaredMethod("resolveBiomeResolution", int.class, int.class,
int.class, IrisDimensionCarvingResolver.State.class, boolean.class, DimensionStackLayout.class);
resolve.setAccessible(true);
Object resolved = resolve.invoke(policy, 3, 40, -5, null, true, stack);
assertSame(feature, field(resolved, "irisBiome"));
assertSame(host, field(resolved, "dimension"));
assertEquals(true, field(resolved, "underground"));
verify(engine).getSubterrainBiome(12, 224, -20);
verifyNoInteractions(stack);
}
private static Object field(Object object, String name) throws Exception {
Field field = object.getClass().getDeclaredField(name);
field.setAccessible(true);
return field.get(object);
}
}
@@ -1,10 +1,20 @@
package art.arcane.iris.world.history; package art.arcane.iris.world.history;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.DimensionStackContext;
import art.arcane.iris.generation.runtime.DimensionStackLayout;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.IrisSubterrainFeature;
import art.arcane.iris.generation.subterrain.SubterrainPlan;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.generation.subterrain.SubterrainPosition;
import java.util.List;
import art.arcane.iris.generation.terrain.InferredType; import art.arcane.iris.generation.terrain.InferredType;
import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.terrain.IrisRegion; import art.arcane.iris.generation.terrain.IrisRegion;
import org.junit.Test; import org.junit.Test;
import org.mockito.MockMakers;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
@@ -20,6 +30,7 @@ import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
public class SavedBiomeCaptureTest { public class SavedBiomeCaptureTest {
@Test @Test
@@ -77,6 +88,7 @@ public class SavedBiomeCaptureTest {
@Test @Test
public void retainsFloatingChildIdentityWhenItSharesTheHostDerivative() throws Exception { public void retainsFloatingChildIdentityWhenItSharesTheHostDerivative() throws Exception {
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS); Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
when(engine.getComplex().getSubterrainPlanner()).thenReturn(null);
GenerationHistory.GenerationStage stage = mock(GenerationHistory.GenerationStage.class); GenerationHistory.GenerationStage stage = mock(GenerationHistory.GenerationStage.class);
SavedBiomeRuntime historical = mock(SavedBiomeRuntime.class); SavedBiomeRuntime historical = mock(SavedBiomeRuntime.class);
IrisBiome host = biome("host"); IrisBiome host = biome("host");
@@ -118,6 +130,7 @@ public class SavedBiomeCaptureTest {
@Test @Test
public void recordsExactSurfaceColumnsAndIndependentCaveAndVerticalIdentities() throws Exception { public void recordsExactSurfaceColumnsAndIndependentCaveAndVerticalIdentities() throws Exception {
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS); Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
when(engine.getComplex().getSubterrainPlanner()).thenReturn(null);
GenerationHistory.GenerationStage stage = mock(GenerationHistory.GenerationStage.class); GenerationHistory.GenerationStage stage = mock(GenerationHistory.GenerationStage.class);
SavedBiomeRuntime historical = mock(SavedBiomeRuntime.class); SavedBiomeRuntime historical = mock(SavedBiomeRuntime.class);
IrisBiome left = biome("left"); IrisBiome left = biome("left");
@@ -155,12 +168,118 @@ public class SavedBiomeCaptureTest {
verifyNoInteractions(historical); verifyNoInteractions(historical);
} }
@Test
public void preservesHostFeatureBiomeAndRegionAgainstStackAndFloatingOverlay() throws Exception {
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
when(engine.getComplex().getSubterrainPlanner()).thenReturn(null);
IrisBiome upper = biome("upper-biome");
IrisBiome authored = biome("authored-biome");
IrisRegion upperRegion = region("upper-region");
IrisRegion hostRegion = region("host-region");
DimensionStackContext stack = mock(DimensionStackContext.class);
DimensionStackLayout layout = mock(DimensionStackLayout.class);
DimensionStackLayout.Layer layer = mock(DimensionStackLayout.Layer.class);
when(engine.getDimensionStackContext()).thenReturn(stack);
when(stack.getLayout(anyInt(), anyInt())).thenReturn(layout);
when(layout.layerAt(anyInt())).thenReturn(layer);
when(layer.region()).thenReturn(upperRegion);
when(engine.getComplex().getRegionStream().get(anyDouble(), anyDouble())).thenReturn(hostRegion);
when(engine.getComplex().getTransitionGenerationPlan()).thenReturn(null);
when(engine.getMinHeight()).thenReturn(-64);
when(engine.getHeight()).thenReturn(16);
when(engine.getRegion(anyInt(), anyInt())).thenReturn(upperRegion);
doCallRealMethod().when(engine).getRegion(anyInt(), anyInt(), anyInt());
when(engine.getSubterrainCell(anyInt(), anyInt(), anyInt())).thenAnswer(call ->
(int) call.getArgument(1) == 8 ? new SubterrainCell(SubterrainCell.Kind.AIR, "", null) : SubterrainCell.OUTSIDE);
when(engine.getSurfaceBiome(anyInt(), anyInt())).thenReturn(upper);
when(engine.getCaveBiome(anyInt(), anyInt())).thenReturn(upper);
when(engine.getBiomeOrMantle(anyInt(), anyInt(), anyInt())).thenAnswer(call ->
(int) call.getArgument(1) == 8 ? authored : upper);
columnsFromPointLookups(engine);
FloatingBiomeOverlay floating = new FloatingBiomeOverlay(16);
floating.record(0, 8, 0, new FloatingBiomeOverlay.Identity("upper-biome", "upper-region"));
SavedBiomeChunk chunk = SavedBiomeCapture.capture(engine, stage(), mock(SavedBiomeRuntime.class), floating);
assertEquals("authored-biome", chunk.biomeAt(0, -56, 0).biomeKey());
assertEquals("host-region", chunk.biomeAt(0, -56, 0).regionKey());
assertEquals("upper-region", chunk.biomeAt(0, -60, 0).regionKey());
assertEquals("upper-region", chunk.surfaceAt(0, 0).regionKey());
}
@Test
public void capturesFeatureBoundariesAtExactBlockCoordinates() throws Exception {
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("exact-basin").setBiome("authored-biome")
.setProbability(1).setPillarSpacing(0).setFormationFraction(0);
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 1191L, -64, 320));
SubterrainPlan plan = planner.plansForBounds(0, 0, 512, 512).getFirst();
SubterrainPosition anchor = plan.anchor();
int edgeX = plan.bounds().minX();
while (!planner.sample(edgeX, anchor.y(), anchor.z()).occupied()) {
edgeX++;
}
int chunkX = Math.floorDiv(edgeX, 16);
int chunkZ = Math.floorDiv(anchor.z(), 16);
Engine engine = mock(Engine.class, withSettings().defaultAnswer(RETURNS_DEEP_STUBS)
.mockMaker(MockMakers.SUBCLASS));
IrisBiome fallback = biome("base-biome");
IrisBiome authored = biome("authored-biome");
IrisRegion host = region("host-region");
IrisRegion stacked = region("stacked-region");
DimensionStackContext stack = mock(DimensionStackContext.class);
DimensionStackLayout layout = mock(DimensionStackLayout.class);
DimensionStackLayout.Layer layer = mock(DimensionStackLayout.Layer.class);
when(engine.getDimensionStackContext()).thenReturn(stack);
when(stack.getLayout(anyInt(), anyInt())).thenReturn(layout);
when(layout.layerAt(anyInt())).thenReturn(layer);
when(layer.region()).thenReturn(stacked);
when(engine.getComplex().getRegionStream().get(anyDouble(), anyDouble())).thenReturn(host);
when(engine.getComplex().getSubterrainPlanner()).thenReturn(planner);
when(engine.getComplex().getTransitionGenerationPlan()).thenReturn(null);
when(engine.getMinHeight()).thenReturn(-64);
when(engine.getHeight()).thenReturn(384);
when(engine.getRegion(anyInt(), anyInt())).thenReturn(stacked);
when(engine.getSubterrainCell(anyInt(), anyInt(), anyInt())).thenAnswer(call ->
planner.sample(call.getArgument(0), (int) call.getArgument(1) - 64, call.getArgument(2)));
doCallRealMethod().when(engine).getRegion(anyInt(), anyInt(), anyInt());
when(engine.getSurfaceBiome(anyInt(), anyInt())).thenReturn(fallback);
when(engine.getCaveBiome(anyInt(), anyInt())).thenReturn(fallback);
when(engine.getBiomeOrMantle(anyInt(), anyInt(), anyInt())).thenAnswer(call ->
planner.sample(call.getArgument(0), (int) call.getArgument(1) - 64, call.getArgument(2)).occupied()
? authored : fallback);
columnsFromPointLookups(engine);
GenerationHistory.GenerationStage stage = stage();
when(stage.chunkX()).thenReturn(chunkX);
when(stage.chunkZ()).thenReturn(chunkZ);
SavedBiomeChunk chunk = SavedBiomeCapture.capture(engine, stage, mock(SavedBiomeRuntime.class), null);
boolean nonQuartOccupied = false;
boolean outsideOccupiedVolume = false;
for (int localX = 0; localX < 16; localX++) {
for (int localZ = 0; localZ < 16; localZ++) {
for (int y = plan.bounds().minY(); y <= plan.bounds().maxY(); y++) {
boolean occupied = planner.sample(chunkX * 16 + localX, y, chunkZ * 16 + localZ).occupied();
SavedBiomeChunk.Cell recorded = chunk.biomeAt(localX, y, localZ);
assertEquals(occupied ? "authored-biome" : "base-biome", recorded.biomeKey());
assertEquals(occupied ? "host-region" : "stacked-region", recorded.regionKey());
nonQuartOccupied |= occupied && (localX % 4 != 0 || localZ % 4 != 0 || Math.floorMod(y, 4) != 0);
outsideOccupiedVolume |= !occupied;
}
}
}
assertTrue(nonQuartOccupied);
assertTrue(outsideOccupiedVolume);
}
private static Engine caveCaptureEngine(IrisBiome surface, IrisBiome cave) { private static Engine caveCaptureEngine(IrisBiome surface, IrisBiome cave) {
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS); Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
when(engine.getComplex().getSubterrainPlanner()).thenReturn(null);
IrisRegion region = region("flat-region"); IrisRegion region = region("flat-region");
when(engine.getMinHeight()).thenReturn(-64); when(engine.getMinHeight()).thenReturn(-64);
when(engine.getHeight()).thenReturn(16); when(engine.getHeight()).thenReturn(16);
when(engine.getHeight(anyInt(), anyInt())).thenReturn(8); when(engine.getHeight(anyInt(), anyInt())).thenReturn(8);
when(engine.getSubterrainBiome(anyInt(), anyInt(), anyInt())).thenReturn(null);
when(engine.getDimensionStackContext()).thenReturn(null); when(engine.getDimensionStackContext()).thenReturn(null);
when(engine.getComplex().getTransitionGenerationPlan()).thenReturn(null); when(engine.getComplex().getTransitionGenerationPlan()).thenReturn(null);
when(engine.getComplex().getCaveBiomeStream().get(anyDouble(), anyDouble())).thenReturn(cave); when(engine.getComplex().getCaveBiomeStream().get(anyDouble(), anyDouble())).thenReturn(cave);
@@ -0,0 +1,97 @@
package art.arcane.iris.world.history;
import art.arcane.iris.generation.locator.SubterrainLocator;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.subterrain.IrisSubterrainFeature;
import art.arcane.iris.generation.subterrain.SubterrainPlan;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.generation.subterrain.SubterrainPosition;
import org.junit.Test;
import java.util.List;
import java.util.Optional;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
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 SavedSubterrainQueriesTest {
@Test
public void locateUsesRetainedDefinitionOnlyInsideItsOwnedChunks() throws Exception {
IrisEngine engine = mock(IrisEngine.class);
IrisComplex complex = mock(IrisComplex.class);
GenerationHistory history = mock(GenerationHistory.class);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class);
GenerationManifest manifest = mock(GenerationManifest.class);
GenerationActivation old = GenerationActivation.initial("a".repeat(64), 1L);
GenerationActivation active = GenerationActivation.next(2L, "b".repeat(64), 1L, 2L, 64);
SubterrainPlanner historical = planner("old-basin");
SubterrainPlan plan = historical.plansForBounds(0, 0, 511, 511).getFirst();
SubterrainPosition anchor = plan.anchor();
when(engine.getComplex()).thenReturn(complex);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(router.history()).thenReturn(history);
when(router.biomes()).thenReturn(saved);
when(history.manifest()).thenReturn(manifest);
when(manifest.activations()).thenReturn(List.of(old, active));
when(manifest.pendingActivation()).thenReturn(Optional.empty());
when(saved.subterrainPlanner(1L)).thenReturn(historical);
when(saved.subterrainPlanner(2L)).thenReturn(planner("new-basin"));
when(history.resolveActivation(anyInt(), anyInt())).thenReturn(active);
when(history.resolveActivation(Math.floorDiv(anchor.x(), 16), Math.floorDiv(anchor.z(), 16))).thenReturn(old);
SubterrainLocator.Query query = new SubterrainLocator.Query("old-basin", null, "");
SubterrainLocator.Result found = GenerationSemanticQueries.nearestSubterrain(engine, query,
anchor.x(), anchor.y(), anchor.z(), 256).orElseThrow();
assertEquals(plan.id(), found.featureId());
assertEquals(anchor.y(), found.y());
verify(engine, never()).getMantle();
verify(router, never()).openRoute(anyInt(), anyInt());
verify(router, never()).openReadScope(anyInt(), anyInt());
when(history.resolveActivation(anyInt(), anyInt())).thenReturn(active);
assertTrue(GenerationSemanticQueries.nearestSubterrain(engine, query,
anchor.x(), anchor.y(), anchor.z(), 256).isEmpty());
}
@Test
public void newPredictionCannotCrossProtectedGenerationTransition() throws Exception {
IrisEngine engine = mock(IrisEngine.class);
IrisComplex complex = mock(IrisComplex.class);
GenerationHistory history = mock(GenerationHistory.class);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class);
GenerationManifest manifest = mock(GenerationManifest.class);
GenerationActivation active = GenerationActivation.initial("a".repeat(64), 1L);
SubterrainPlanner planner = planner("basin");
SubterrainPosition anchor = planner.plansForBounds(0, 0, 511, 511).getFirst().anchor();
when(engine.getComplex()).thenReturn(complex);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(router.history()).thenReturn(history);
when(router.biomes()).thenReturn(saved);
when(history.manifest()).thenReturn(manifest);
when(manifest.activations()).thenReturn(List.of(active));
when(manifest.pendingActivation()).thenReturn(Optional.empty());
when(saved.subterrainPlanner(1L)).thenReturn(planner);
when(history.resolveActivation(anyInt(), anyInt())).thenReturn(active);
when(history.isActiveUnowned(anyInt(), anyInt())).thenReturn(true);
SubterrainLocator.Query query = new SubterrainLocator.Query("basin", null, "");
assertTrue(GenerationSemanticQueries.nearestSubterrain(engine, query,
anchor.x(), anchor.y(), anchor.z(), 64).isEmpty());
when(complex.allowsNewDiscreteContentAt(anchor.x(), anchor.z())).thenReturn(true);
assertTrue(GenerationSemanticQueries.nearestSubterrain(engine, query,
anchor.x(), anchor.y(), anchor.z(), 64).isPresent());
verify(engine, never()).getMantle();
}
private static SubterrainPlanner planner(String id) {
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId(id).setProbability(1D);
return new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 1191L, -64, 320));
}
}