mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-12 03:24:14 +00:00
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:
@@ -25,6 +25,8 @@ import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
||||
import art.arcane.iris.studio.object.ObjectStudioSaveService;
|
||||
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.nativegen.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.structure.placement.StructureReachability;
|
||||
@@ -89,6 +91,78 @@ public class CommandFind implements DirectorExecutor {
|
||||
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")
|
||||
public void region(
|
||||
@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,
|
||||
S sampler) {
|
||||
if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) {
|
||||
if (isGuaranteedSurfaceBiome(engine, quartY)) {
|
||||
return getSurfaceStructureBiome(engine, quartX, quartZ, sampler);
|
||||
}
|
||||
boolean cacheable = isBiomeCacheable(
|
||||
@@ -512,7 +512,7 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
|
||||
}
|
||||
boolean monumentQuery = isMonumentSurfaceBiomeQuery(
|
||||
y, radius, engine.getMinHeight(), engine.getDimension().getFluidHeight());
|
||||
surfaceQuery = monumentQuery || isGuaranteedSurfaceBiome(minQuartY, engine.getMinHeight());
|
||||
surfaceQuery = monumentQuery || isGuaranteedSurfaceBiome(engine, minQuartY);
|
||||
}
|
||||
if (!surfaceQuery) {
|
||||
return query.fallback().get();
|
||||
@@ -701,6 +701,11 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
|
||||
int blockZ = quartZ << 2;
|
||||
int internalY = blockY - engine.getMinHeight();
|
||||
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
|
||||
? stackLayout == null
|
||||
? resolveDimensionStackLayer(engine, blockX, internalY, blockZ)
|
||||
@@ -846,7 +851,13 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
|
||||
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 caveSwitchY = Math.max(-8 - minHeight, 40);
|
||||
return internalY > caveSwitchY;
|
||||
|
||||
+115
-6
@@ -58,6 +58,7 @@ import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
import art.arcane.volmlib.util.matter.slices.MarkerMatter;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
@@ -511,14 +512,18 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
}
|
||||
|
||||
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)
|
||||
: new RNG(RNG.r.nextLong());
|
||||
|
||||
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.region().getEntitySpawners()), null, chunkX, chunkZ, chunkMobs, initial);
|
||||
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(biome.getEntitySpawners()), null, 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, false);
|
||||
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()) {
|
||||
return;
|
||||
}
|
||||
@@ -533,14 +538,111 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
}
|
||||
int spawned = spawnEntry(level, chosen.entry(), chunkX, chunkZ,
|
||||
remainingCapacity(chosen.entry(), spawner, chunkMobs), rng, new SpawnContext(spawner, null, initial));
|
||||
recordSpawned(chosen.entry(), chunkMobs, spawned);
|
||||
if (spawned > 0 && !initial) {
|
||||
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) {
|
||||
if (spawner == null) {
|
||||
if (spawner == null || (spawner.getGroup() == IrisSpawnGroup.CAVE) != underground) {
|
||||
continue;
|
||||
}
|
||||
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) {
|
||||
IrisEntity entity = entry.getRealEntity(engine);
|
||||
return spawner.remainingCapacity(entity, counts);
|
||||
|
||||
+10
-4
@@ -38,12 +38,18 @@ public class IrisModdedStructureParityTest {
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
private static Engine surfaceEngine(int minHeight) {
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getMinHeight()).thenReturn(minHeight);
|
||||
return engine;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void surfaceBiomeFastPathBeginsAboveTheCaveSwitch() {
|
||||
assertFalse(ModdedBiomePolicy.isGuaranteedSurfaceBiome(-2, -256));
|
||||
assertTrue(ModdedBiomePolicy.isGuaranteedSurfaceBiome(-1, -256));
|
||||
assertFalse(ModdedBiomePolicy.isGuaranteedSurfaceBiome(10, 0));
|
||||
assertTrue(ModdedBiomePolicy.isGuaranteedSurfaceBiome(11, 0));
|
||||
assertFalse(ModdedBiomePolicy.isGuaranteedSurfaceBiome(surfaceEngine(-256), -2));
|
||||
assertTrue(ModdedBiomePolicy.isGuaranteedSurfaceBiome(surfaceEngine(-256), -1));
|
||||
assertFalse(ModdedBiomePolicy.isGuaranteedSurfaceBiome(surfaceEngine(0), 10));
|
||||
assertTrue(ModdedBiomePolicy.isGuaranteedSurfaceBiome(surfaceEngine(0), 11));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+39
@@ -2,6 +2,11 @@ package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
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.GenerationSessionLease;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
@@ -11,20 +16,54 @@ import art.arcane.volmlib.util.stream.interpolation.Interpolated;
|
||||
import org.junit.Test;
|
||||
|
||||
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.Set;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
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
|
||||
public void registryOrderAndHolderIdentitySurviveThePolicyBoundaryAndRefreshOnRepoint() {
|
||||
BiomeToken ocean = new BiomeToken("minecraft:ocean");
|
||||
|
||||
+28
@@ -18,6 +18,7 @@ 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.IrisSpawner;
|
||||
import art.arcane.iris.world.entity.IrisSpawnGroup;
|
||||
import art.arcane.iris.world.entity.IrisMarker;
|
||||
import art.arcane.iris.world.entity.EntitySpawnSeed;
|
||||
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
|
||||
public void initialCompletionFollowsOwnedSpawnPassAndDoesNotRepeatIt() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
|
||||
+114
@@ -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)),
|
||||
data, x, z, realX, lowerY, realZ, mantle);
|
||||
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) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
@@ -210,7 +210,7 @@ final class DecoratorCore {
|
||||
if (applyFixFaces) {
|
||||
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,
|
||||
@@ -258,7 +258,7 @@ final class DecoratorCore {
|
||||
} else if (i == 0 && !opts.underwater && !canGoOn(block, support)) {
|
||||
break;
|
||||
}
|
||||
data.set(x, y, z, stackedVineBlock(block, stack, i));
|
||||
data.set(x, y, z, IrisProceduralBlocks.normalizeWaterlogging(stackedVineBlock(block, stack, i), existing, true));
|
||||
placed++;
|
||||
}
|
||||
if (placed > 0 && placed < stack) {
|
||||
@@ -356,8 +356,8 @@ final class DecoratorCore {
|
||||
try {
|
||||
NativeBlockState upper = bd.withProperty("half", topHalfValue(half));
|
||||
NativeBlockState lower = bd.withProperty("half", bottomHalfValue(half));
|
||||
data.set(xf, lowerY, zf, lower);
|
||||
data.set(xf, upperY, zf, upper);
|
||||
data.set(xf, lowerY, zf, IrisProceduralBlocks.normalizeWaterlogging(lower, data.get(xf, lowerY, zf), true));
|
||||
data.set(xf, upperY, zf, IrisProceduralBlocks.normalizeWaterlogging(upper, data.get(xf, upperY, zf), true));
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
@@ -366,7 +366,7 @@ final class DecoratorCore {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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++;
|
||||
}
|
||||
if (placed > 0 && placed < stack) {
|
||||
@@ -433,6 +433,7 @@ final class DecoratorCore {
|
||||
|
||||
static NativeBlockState fixFacesForHunk(NativeBlockState b, Hunk<NativeBlockState> hunk, int rX, int rZ,
|
||||
int x, int y, int z, EngineMantle mantle) {
|
||||
b = IrisProceduralBlocks.normalizeWaterlogging(b, hunk.get(rX, y, rZ), true);
|
||||
if (!B.isVineBlock(b)) {
|
||||
return b;
|
||||
}
|
||||
@@ -539,7 +540,8 @@ final class DecoratorCore {
|
||||
if (!IrisSpeleothems.canPlace(spike, data, x, z, y, upward, allowWater)) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -123,6 +123,9 @@ public final class IrisProceduralBlocks {
|
||||
* rather than building the search needles, and prefers a "[" match over a "," match.
|
||||
*/
|
||||
private static int propertyStart(String key, String property) {
|
||||
if (key == null) {
|
||||
return -1;
|
||||
}
|
||||
int bracket = key.indexOf('[');
|
||||
if (bracket < 0) {
|
||||
return -1;
|
||||
@@ -142,6 +145,16 @@ public final class IrisProceduralBlocks {
|
||||
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) {
|
||||
if (blocks == null || blocks.isEmpty()) {
|
||||
return null;
|
||||
|
||||
+2
-1
@@ -19,6 +19,7 @@
|
||||
package art.arcane.iris.generation.decoration;
|
||||
|
||||
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.pack.loading.IrisData;
|
||||
@@ -43,7 +44,7 @@ public interface IrisProceduralPlacement {
|
||||
|
||||
IrisObjectPlacement asPlacement();
|
||||
|
||||
IrisObject getVariantObject(IrisData data, RNG rng);
|
||||
IrisObject getVariantObject(IrisData data, RNG rng, SubterrainRoom room);
|
||||
|
||||
KList<IrisObject> getVariantObjects(IrisData data);
|
||||
}
|
||||
|
||||
@@ -81,10 +81,7 @@ public final class IrisSpeleothems {
|
||||
}
|
||||
|
||||
static NativeBlockState orient(NativeBlockState state, NativeBlockState existing, boolean upward) {
|
||||
if (existing != null && existing.isWater() && !state.isWaterLogged()) {
|
||||
state = state.withProperty("waterlogged", "true");
|
||||
}
|
||||
return spikeBlock(state, upward, 0);
|
||||
return spikeBlock(IrisProceduralBlocks.normalizeWaterlogging(state, existing, true), upward, 0);
|
||||
}
|
||||
|
||||
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.decoration.IrisProceduralPlacement;
|
||||
import art.arcane.iris.generation.subterrain.SubterrainRoom;
|
||||
import art.arcane.iris.generation.decoration.IrisStiltSettings;
|
||||
import art.arcane.iris.generation.decoration.IrisVacuumSettings;
|
||||
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);
|
||||
if (baked == null || baked.isEmpty()) {
|
||||
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.decoration.IrisProceduralPlacement;
|
||||
import art.arcane.iris.generation.subterrain.SubterrainRoom;
|
||||
import art.arcane.iris.generation.decoration.IrisStiltSettings;
|
||||
import art.arcane.iris.generation.decoration.IrisVacuumSettings;
|
||||
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);
|
||||
if (baked == null || baked.isEmpty()) {
|
||||
return null;
|
||||
|
||||
+4
@@ -37,6 +37,10 @@ public final class FormationGenerator {
|
||||
int hi = Math.max(f.getHeightMin(), f.getHeightMax());
|
||||
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 wHi = Math.max(f.getBaseWidthMin(), f.getBaseWidthMax());
|
||||
double baseRadius = Math.max(1, rng.i(wLo, wHi + 1));
|
||||
|
||||
+21
-8
@@ -19,6 +19,7 @@
|
||||
package art.arcane.iris.generation.decoration.formation;
|
||||
|
||||
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.IrisStiltSettings;
|
||||
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.")
|
||||
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) {
|
||||
return variantCache.aquire(() -> {
|
||||
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() {
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement();
|
||||
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.decoration.IrisProceduralPlacement;
|
||||
import art.arcane.iris.generation.subterrain.SubterrainRoom;
|
||||
import art.arcane.iris.generation.decoration.IrisStiltSettings;
|
||||
import art.arcane.iris.generation.decoration.IrisVacuumSettings;
|
||||
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);
|
||||
if (baked == null || baked.isEmpty()) {
|
||||
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.decoration.IrisProceduralPlacement;
|
||||
import art.arcane.iris.generation.subterrain.SubterrainRoom;
|
||||
import art.arcane.iris.generation.decoration.IrisStiltSettings;
|
||||
import art.arcane.iris.generation.decoration.IrisVacuumSettings;
|
||||
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);
|
||||
if (baked == null || baked.isEmpty()) {
|
||||
return null;
|
||||
|
||||
+2
-1
@@ -20,6 +20,7 @@ package art.arcane.iris.generation.decoration.tree;
|
||||
|
||||
import art.arcane.iris.generation.cave.CarvingMode;
|
||||
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.IrisVacuumSettings;
|
||||
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);
|
||||
if (baked == null || baked.isEmpty()) {
|
||||
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) {
|
||||
}
|
||||
}
|
||||
+18
-1
@@ -19,6 +19,8 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
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.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
@@ -79,6 +81,11 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
discard();
|
||||
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(
|
||||
mutation.x(), mutation.y(), mutation.z(), HydrologyCaveCell.class);
|
||||
if (hydrology != null && hydrology.protectsPlacement()) {
|
||||
@@ -173,6 +180,15 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
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
|
||||
public boolean isDebugSmartBore() {
|
||||
return delegate.isDebugSmartBore();
|
||||
@@ -261,7 +277,8 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
EMPTY,
|
||||
REJECTED_TRANSITION,
|
||||
REJECTED_BOUNDS,
|
||||
REJECTED_HYDROLOGY
|
||||
REJECTED_HYDROLOGY,
|
||||
REJECTED_SUBTERRAIN
|
||||
}
|
||||
|
||||
private interface BufferedMutation {
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
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.UpperDimensionContext;
|
||||
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.scheduling.PrecisionStopwatch;
|
||||
|
||||
import java.util.TreeSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.IdentityHashMap;
|
||||
@@ -160,6 +164,31 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
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) {
|
||||
@@ -343,22 +372,31 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
continue;
|
||||
}
|
||||
|
||||
TreeSet<Integer> boundaries = new TreeSet<>();
|
||||
int minimumY = engine.getWorld().minHeight();
|
||||
int maximumY = engine.getWorld().maxHeight();
|
||||
boundaries.add(minimumY);
|
||||
boundaries.add(maximumY);
|
||||
for (IrisDimensionCarvingEntry candidate : entries) {
|
||||
if (candidate != null && candidate.getWorldYRange() != null) {
|
||||
boundaries.add(Math.max(minimumY, Math.min(maximumY, (int) Math.ceil(candidate.getWorldYRange().getMin()))));
|
||||
boundaries.add(Math.max(minimumY, Math.min(maximumY, (int) Math.floor(candidate.getWorldYRange().getMax()) + 1)));
|
||||
}
|
||||
}
|
||||
Integer previous = null;
|
||||
for (int boundary : boundaries) {
|
||||
if (previous != null && boundary > previous
|
||||
&& (entry.getWorldYRange() == null || entry.getWorldYRange().contains(previous))) {
|
||||
Map<IrisCaveProfile, double[]> rootProfileColumnWeights = new IdentityHashMap<>();
|
||||
List<IrisCaveProfile> rootProfileOrder = new ArrayList<>();
|
||||
IrisRange worldYRange = entry.getWorldYRange();
|
||||
for (int columnIndex = 0; columnIndex < CHUNK_AREA; columnIndex++) {
|
||||
IrisDimensionCarvingEntry resolvedEntry = IrisDimensionCarvingResolver.resolveFromRoot(
|
||||
engine, entry, baseX + (columnIndex >> 4), baseZ + (columnIndex & 15), resolverState);
|
||||
engine, entry, baseX + (columnIndex >> 4), previous, baseZ + (columnIndex & 15), resolverState);
|
||||
IrisBiome resolvedBiome = IrisDimensionCarvingResolver.resolveEntryBiome(engine, resolvedEntry, resolverState);
|
||||
if (resolvedBiome == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
IrisCaveProfile profile = resolvedBiome.getCaveProfile();
|
||||
IrisCaveProfile profile = resolvedBiome == null ? null : resolvedBiome.getCaveProfile();
|
||||
if (!isProfileEnabled(profile)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
double[] columnWeights = rootProfileColumnWeights.get(profile);
|
||||
if (columnWeights == null) {
|
||||
columnWeights = blendScratch.acquireWeights();
|
||||
@@ -367,11 +405,14 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return weightedProfiles;
|
||||
}
|
||||
@@ -444,7 +485,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
IrisDimensionCarvingEntry rootCarvingEntry = IrisDimensionCarvingResolver.resolveRootEntry(getEngineMantle().getEngine(), worldY, resolverState);
|
||||
IrisBiome resolvedCarvingBiome = 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);
|
||||
if (resolvedCarvingBiome != null) {
|
||||
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.runtime.IrisHydrologyRuntime;
|
||||
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.terrain.IrisDimension;
|
||||
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()) {
|
||||
CavePosition position = entry.getKey();
|
||||
if (featureOwns(writer, context, position)) {
|
||||
continue;
|
||||
}
|
||||
writer.setData(position.x(), position.y(), position.z(), entry.getValue());
|
||||
}
|
||||
for (CavePosition position : sortedPositions(publication.fluidUpdates())) {
|
||||
if (featureOwns(writer, context, position)) {
|
||||
continue;
|
||||
}
|
||||
writer.setData(position.x(), position.y(), position.z(), UpdateMatter.ON);
|
||||
}
|
||||
for (Map.Entry<CavePosition, HydrologyCaveCell> entry : sortedEntries(publication.caveCells())) {
|
||||
CavePosition position = entry.getKey();
|
||||
if (featureOwns(writer, context, position)) {
|
||||
continue;
|
||||
}
|
||||
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) {
|
||||
if (layer.bedY() < 0
|
||||
|| 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.structure.object.IrisObject;
|
||||
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.IrisObjectTranslate;
|
||||
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.structure.object.ObjectPlaceMode;
|
||||
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.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
@@ -69,9 +75,11 @@ import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.HashSet;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
@ComponentFlag(ReservedFlag.OBJECT)
|
||||
public class MantleObjectComponent extends IrisMantleComponent {
|
||||
@@ -250,7 +258,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
+ " regionCavePlacers=" + region.getCarvingObjects().size());
|
||||
}
|
||||
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();
|
||||
IrisDimension dimension = getDimension();
|
||||
if (upperCtx != null && dimension.isUpperDimensionObjects()) {
|
||||
@@ -603,7 +611,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
|
||||
@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 regionCaveProfile = resolveCaveProfile(region.getCaveProfile(), caveBiome == null ? null : caveBiome.getCaveProfile());
|
||||
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());
|
||||
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
|
||||
@@ -660,16 +685,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
IObjectPlacer placer = golden ? new GoldenDebugObjectPlacer(basePlacer, scope + "/" + p.getName()) : basePlacer;
|
||||
int density = Math.max(1, p.getDensity());
|
||||
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;
|
||||
if (carving) {
|
||||
caveAnchor = findCavePlacementAnchor(
|
||||
@@ -694,6 +709,16 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
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 zz = caveAnchor == null ? rng.i(blockZ, blockZ + 16) : caveAnchor.z();
|
||||
int id = rng.i(0, Integer.MAX_VALUE);
|
||||
@@ -730,6 +755,8 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
caveFloorY,
|
||||
zz,
|
||||
effectivePlacement,
|
||||
anchorMode == IrisCaveAnchorMode.FLOOR && effectivePlacement.getMode() != ObjectPlaceMode.CEILING_HANG
|
||||
? IrisObjectPlacementAlignment.FLOOR : IrisObjectPlacementAlignment.CENTER,
|
||||
minDepthBelowSurface,
|
||||
id,
|
||||
"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) {
|
||||
if (chance <= 0.0) {
|
||||
return false;
|
||||
@@ -827,6 +862,11 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
|
||||
private boolean acceptsCaveAnchorAt(int x, int y, int z, boolean underwater,
|
||||
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(),
|
||||
hydrology, y, getDimension().getCaveLavaHeight());
|
||||
}
|
||||
@@ -862,6 +902,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
int anchorY,
|
||||
int z,
|
||||
IrisObjectPlacement placement,
|
||||
IrisObjectPlacementAlignment alignment,
|
||||
int minDepthBelowSurface,
|
||||
int id,
|
||||
String markerContext,
|
||||
@@ -875,12 +916,13 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
CaveObjectPlacementTransaction.CommitResult.REJECTED_TRANSITION);
|
||||
}
|
||||
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) {
|
||||
placeY = Math.max(1, transaction.getCaveCeiling(x, z) - 1 - Math.floorDiv(object.getH(), 2));
|
||||
}
|
||||
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) {
|
||||
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)) {
|
||||
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) {
|
||||
transaction.discard();
|
||||
return new ContainedPlacementResult(result, CaveObjectPlacementTransaction.CommitResult.EMPTY);
|
||||
@@ -1147,6 +1192,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
y,
|
||||
z,
|
||||
effectivePlacement,
|
||||
IrisObjectPlacementAlignment.CENTER,
|
||||
objectMinDepthBelowSurface,
|
||||
id,
|
||||
"cave",
|
||||
@@ -1502,6 +1548,17 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
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) {
|
||||
int maxAnchorY = CaveObjectPlacementTransaction.maxBuriedY(worldHeight, surfaceY, minDepthBelowSurface);
|
||||
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.context.ChunkContext;
|
||||
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.documentation.ChunkCoordinates;
|
||||
import art.arcane.volmlib.util.function.Function3;
|
||||
@@ -364,10 +366,13 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
MantleChunk<Matter> chunk = acquireChunk(cx, cz);
|
||||
if (chunk == null) return;
|
||||
|
||||
if (protectsSubterrainPlacement(x, y, z)) {
|
||||
return;
|
||||
}
|
||||
synchronized (chunk) {
|
||||
Matter matter = chunk.getOrCreate(y >> 4);
|
||||
if ((t instanceof NativeBlockState || t instanceof MatterCavern)
|
||||
&& hasProtectedHydrology(matter, x, y, z)) {
|
||||
&& (hasProtectedHydrology(matter, x, y, z) || protectsSubterrainPlacement(x, y, z))) {
|
||||
return;
|
||||
}
|
||||
if (t instanceof NativeBlockState) {
|
||||
@@ -399,7 +404,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
|
||||
synchronized (chunk) {
|
||||
Matter matter = chunk.getOrCreate(y >> 4);
|
||||
if (hasProtectedHydrology(matter, x, y, z)) {
|
||||
if (hasProtectedHydrology(matter, x, y, z) || protectsSubterrainPlacement(x, y, z)) {
|
||||
return false;
|
||||
}
|
||||
MatterSlice<MatterCavern> cavernSlice = matter.getSlice(MatterCavern.class);
|
||||
@@ -432,7 +437,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
|
||||
synchronized (chunk) {
|
||||
Matter matter = chunk.getOrCreate(y >> 4);
|
||||
if (hasProtectedHydrology(matter, x, y, z)) {
|
||||
if (hasProtectedHydrology(matter, x, y, z) || protectsSubterrainPlacement(x, y, z)) {
|
||||
return false;
|
||||
}
|
||||
MatterSlice<NativeBlockState> blockSlice = matter.getSlice(NativeBlockState.class);
|
||||
@@ -467,7 +472,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
}
|
||||
synchronized (chunk) {
|
||||
Matter matter = chunk.getOrCreate(y >> 4);
|
||||
if (hasProtectedHydrology(matter, x, y, z)) {
|
||||
if (hasProtectedHydrology(matter, x, y, z) || protectsSubterrainPlacement(x, y, z)) {
|
||||
return;
|
||||
}
|
||||
capturePreObjectOriginal(matter, x, y, z, NativeBlockState.class);
|
||||
@@ -497,7 +502,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
if (matter == null) {
|
||||
return;
|
||||
}
|
||||
if (hasProtectedHydrology(matter, x, y, z)) {
|
||||
if (hasProtectedHydrology(matter, x, y, z) || protectsSubterrainPlacement(x, y, z)) {
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
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) {
|
||||
SubterrainCell feature = getEngine().getSubterrainCell(x, y, z);
|
||||
if (feature != null && feature.owned()) {
|
||||
return feature.carve();
|
||||
}
|
||||
Terrain3DColumn transformed = engineMantle.getComplex().transformedColumn(x, z);
|
||||
if (transformed != null) {
|
||||
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) {
|
||||
if (!allowsWrite(x, z) || y < 0 || y >= mantle.getWorldHeight()) {
|
||||
if (!allowsWrite(x, z) || y < 0 || y >= mantle.getWorldHeight() || protectsSubterrainPlacement(x, y, z)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -745,7 +754,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
return;
|
||||
}
|
||||
if ((type == NativeBlockState.class || type == MatterCavern.class)
|
||||
&& hasProtectedHydrology(matter, x, y, z)) {
|
||||
&& (hasProtectedHydrology(matter, x, y, z) || protectsSubterrainPlacement(x, y, z))) {
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
MatterSlice<HydrologyCaveCell> slice = matter.getSlice(HydrologyCaveCell.class);
|
||||
if (slice == null) {
|
||||
@@ -785,7 +800,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
|
||||
synchronized (chunk) {
|
||||
Matter matter = chunk.getOrCreate(y >> 4);
|
||||
if (hasProtectedHydrology(matter, x, y, z)) {
|
||||
if (hasProtectedHydrology(matter, x, y, z) || protectsSubterrainPlacement(x, y, z)) {
|
||||
return;
|
||||
}
|
||||
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
|
||||
// a 16^3 section + slice on a miss the way getData's getOrCreate path does.
|
||||
NativeBlockState block = getDataIfPresent(x, y, z, NativeBlockState.class);
|
||||
if (block == null)
|
||||
return AIR.get();
|
||||
return block;
|
||||
return block == null ? subterrainFallback(x, y, z) : 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
|
||||
@@ -969,6 +987,10 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
|
||||
@Override
|
||||
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);
|
||||
if (transformed != null) {
|
||||
return y >= 0 && y < transformed.topY() && !transformed.isSolid(y);
|
||||
|
||||
+10
-2
@@ -6,6 +6,7 @@ import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
|
||||
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.structure.object.IObjectPlacer;
|
||||
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) {
|
||||
if (data == null || y < 0 || y >= worldHeight) {
|
||||
if (data == null || y < 0 || y >= worldHeight
|
||||
|| protectsSubterrain(x, y, z)) {
|
||||
return;
|
||||
}
|
||||
if (data instanceof NativeBlockState && !canSetBlock(x, y, z)) {
|
||||
@@ -351,7 +353,13 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
if (y == 0 && writer.getEngine().getDimension().isBedrock()) {
|
||||
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) {
|
||||
|
||||
@@ -82,6 +82,7 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
|
||||
if (getEngine().getDimensionStackContext() == null) {
|
||||
registerStage(sCustom);
|
||||
}
|
||||
registerStage((x, z, k, p, m, c) -> cave.preserveSubterrain(x, z, k));
|
||||
}
|
||||
|
||||
void generateTerrainMatterForChunk(int x, int z, boolean multicore, ChunkContext context) {
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
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.volmlib.nativelib.terrain.structure.NativeStructureVolume;
|
||||
@@ -328,6 +330,9 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
@BlockCoordinates
|
||||
default IrisRegion getRegion(int x, int y, int z) {
|
||||
if (getSubterrainCell(x, y, z).occupied()) {
|
||||
return getComplex().getRegionStream().get(x, z);
|
||||
}
|
||||
DimensionStackContext dimensionStackContext = getDimensionStackContext();
|
||||
if (dimensionStackContext != null) {
|
||||
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);
|
||||
|
||||
@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
|
||||
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);
|
||||
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++) {
|
||||
int y = index * step;
|
||||
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 (surface == null) {
|
||||
surface = getSurfaceBiome(x, z);
|
||||
@@ -458,6 +490,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
@BlockCoordinates
|
||||
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);
|
||||
if (configuredBiome != null) {
|
||||
return configuredBiome;
|
||||
@@ -490,6 +526,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
IrisBiome surfaceBiome,
|
||||
int surfaceY
|
||||
) {
|
||||
IrisBiome featureBiome = getSubterrainBiome(x, y, z);
|
||||
if (featureBiome != null) {
|
||||
return featureBiome;
|
||||
}
|
||||
IrisBiome configuredBiome = resolveConfiguredCaveBiome(x, y, z, state);
|
||||
if (configuredBiome != null) {
|
||||
return configuredBiome;
|
||||
@@ -719,6 +759,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
boolean isStudio();
|
||||
|
||||
default IrisBiome getBiome(int x, int y, int z) {
|
||||
IrisBiome featureBiome = getSubterrainBiome(x, y, z);
|
||||
if (featureBiome != null) {
|
||||
return featureBiome;
|
||||
}
|
||||
DimensionStackContext dimensionStackContext = getDimensionStackContext();
|
||||
if (dimensionStackContext != null) {
|
||||
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) {
|
||||
IrisBiome featureBiome = getSubterrainBiome(x, y, z);
|
||||
if (featureBiome != null) {
|
||||
return featureBiome;
|
||||
}
|
||||
DimensionStackContext dimensionStackContext = getDimensionStackContext();
|
||||
if (dimensionStackContext != null) {
|
||||
DimensionStackLayout layout = dimensionStackContext.getLayout(x, z);
|
||||
|
||||
@@ -118,7 +118,7 @@ public final class GenerationCacheWarmer {
|
||||
if (placement == null) {
|
||||
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.CachedStream2D;
|
||||
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;
|
||||
@@ -243,6 +245,8 @@ public class IrisComplex implements DataProvider {
|
||||
private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> generatorBounds;
|
||||
private Set<IrisBiome> generatorBiomes;
|
||||
private IrisHydrologyRuntime hydrologyRuntime;
|
||||
@Setter(AccessLevel.NONE)
|
||||
private final transient SubterrainPlanner subterrainPlanner;
|
||||
private transient IrisImageMapRuntime imageMapRuntime;
|
||||
// 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
|
||||
@@ -264,6 +268,15 @@ public class IrisComplex implements DataProvider {
|
||||
|
||||
IrisComplex(Engine engine, boolean simple, TransitionGenerationPlan transitionGenerationPlan, boolean detached) {
|
||||
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();
|
||||
this.detached = detached;
|
||||
this.transitionGenerationPlan = transitionGenerationPlan;
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
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.EngineRuntimeBuilder.RuntimeAssembly;
|
||||
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;
|
||||
}
|
||||
|
||||
@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
|
||||
@Override
|
||||
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.iris.world.history.SavedBiomeUnavailableException;
|
||||
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.EntitySpawnSeed;
|
||||
import art.arcane.iris.world.entity.IrisMarker;
|
||||
@@ -404,41 +407,94 @@ final class WorldEntitySpawner {
|
||||
if (!ambientAllowed(chunk, initial)) {
|
||||
return;
|
||||
}
|
||||
ChunkCounter counter = initial ? null : new ChunkCounter(chunk.getEntities());
|
||||
ChunkCounter counter = new ChunkCounter(initial ? new Entity[0] : chunk.getEntities());
|
||||
List<SpawnSelection> pool = new ArrayList<>();
|
||||
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()),
|
||||
null, chunk, initial, counter);
|
||||
null, chunk, initial, counter, false);
|
||||
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(),
|
||||
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);
|
||||
if (selected == null) {
|
||||
return;
|
||||
}
|
||||
int capacity = initial ? Math.max(0, selected.spawner().getMaxEntitiesPerChunk())
|
||||
: counter.remainingCapacity(selected.entry(), selected.spawner(), manager.getEngine());
|
||||
int capacity = counter.remainingCapacity(selected.entry(), selected.spawner(), manager.getEngine());
|
||||
int spawned = selected.entry().spawn(manager.getEngine(), chunk, random, capacity,
|
||||
new SpawnContext(selected.spawner(), null, initial));
|
||||
actuallySpawned.addAndGet(spawned);
|
||||
counter.recordSpawned(selected.entry(), manager.getEngine(), spawned);
|
||||
if (spawned > 0 && !initial) {
|
||||
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,
|
||||
Chunk chunk, boolean initial, ChunkCounter counter) {
|
||||
Chunk chunk, boolean initial, ChunkCounter counter, boolean underground) {
|
||||
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)) {
|
||||
continue;
|
||||
}
|
||||
for (IrisEntitySpawn entry : initial ? spawner.getInitialSpawns() : spawner.getSpawns()) {
|
||||
IrisEntity entity = entry.getRealEntity(manager.getEngine());
|
||||
int capacity = initial ? Math.max(0, spawner.getMaxEntitiesPerChunk())
|
||||
: counter.remainingCapacity(entry, spawner, manager.getEngine());
|
||||
int capacity = counter.remainingCapacity(entry, spawner, manager.getEngine());
|
||||
if (entity != null && capacity > 0) {
|
||||
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) {
|
||||
IrisEntity entity = entry.getRealEntity(engine);
|
||||
return spawner.remainingCapacity(entity, counts);
|
||||
|
||||
@@ -19,6 +19,12 @@
|
||||
package art.arcane.iris.generation.stage;
|
||||
|
||||
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.DimensionStackLayout;
|
||||
import art.arcane.iris.generation.runtime.UpperDimensionContext;
|
||||
@@ -144,7 +150,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
worldHeightSpan,
|
||||
caveLavaHeight,
|
||||
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);
|
||||
terrain.forEachCell(carveResolver::apply);
|
||||
@@ -161,7 +169,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
IrisData wallData = getData();
|
||||
walls.forEach((rx, yy, rz, cavern) -> {
|
||||
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;
|
||||
}
|
||||
int worldX = rx + chunkBlockX;
|
||||
@@ -233,6 +242,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
output.setRaw(localX, surfaceY, localZ, AIR);
|
||||
}
|
||||
}
|
||||
restoreSubterrainSolids(output, x, z);
|
||||
} finally {
|
||||
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) {
|
||||
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 localZ = z & 15;
|
||||
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]) {
|
||||
return;
|
||||
}
|
||||
@@ -432,7 +507,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
int worldHeightSpan,
|
||||
int caveLavaHeight,
|
||||
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.setCeiling(y - 1);
|
||||
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)) {
|
||||
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,
|
||||
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();
|
||||
IrisDecorator[] surfaceDecorators = floorBiome == null
|
||||
? new IrisDecorator[0]
|
||||
|
||||
+14
-6
@@ -26,6 +26,7 @@ import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.decoration.FloatingDecorator;
|
||||
import art.arcane.iris.generation.decoration.IrisSeaSurfaceDecorator;
|
||||
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.EngineDecorator;
|
||||
import art.arcane.iris.generation.biome.FloatingBottomPaletteMode;
|
||||
@@ -244,7 +245,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
|
||||
continue;
|
||||
}
|
||||
int y = sample.islandBaseY + k;
|
||||
if (y < 0 || y >= chunkHeight) {
|
||||
if (y < 0 || y >= chunkHeight || featureOwns(wx, y, wz)) {
|
||||
continue;
|
||||
}
|
||||
IrisFloatingChildBiomes entry = sample.entryAt(k);
|
||||
@@ -275,7 +276,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
|
||||
continue;
|
||||
}
|
||||
int y = sample.islandBaseY + k;
|
||||
if (y < 0 || y >= chunkHeight) {
|
||||
if (y < 0 || y >= chunkHeight || featureOwns(wx, y, wz)) {
|
||||
continue;
|
||||
}
|
||||
boolean hasSolidBelow = false;
|
||||
@@ -340,7 +341,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
|
||||
|
||||
int topY = sample.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);
|
||||
if (above == null || above.isAir()) {
|
||||
try {
|
||||
@@ -362,7 +363,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
|
||||
continue;
|
||||
}
|
||||
int y = sample.islandBaseY + k;
|
||||
if (y < 0 || y >= chunkHeight) {
|
||||
if (y < 0 || y >= chunkHeight || featureOwns(wx, y, wz)) {
|
||||
continue;
|
||||
}
|
||||
boolean hasSolidBelow = false;
|
||||
@@ -376,7 +377,9 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
|
||||
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 {
|
||||
seaSurfaceDecorator.decorate(xf, zf, wx, wx + 1, wx - 1, wz, wz + 1, wz - 1, output, target, fluidTopY, chunkHeight);
|
||||
} 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(
|
||||
IrisBiome parent,
|
||||
int wx,
|
||||
@@ -406,7 +414,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
|
||||
continue;
|
||||
}
|
||||
int y = sample.islandBaseY + k;
|
||||
if (y < 0 || y >= chunkHeight) {
|
||||
if (y < 0 || y >= chunkHeight || featureOwns(wx, y, wz)) {
|
||||
continue;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
+95
@@ -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;
|
||||
|
||||
import art.arcane.iris.world.entity.IrisSpawnGroup;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.biome.IrisBiomeCustom;
|
||||
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.tree.IrisTreeSettings;
|
||||
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.IrisRiverHydrology;
|
||||
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.JSONObject;
|
||||
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.RNG;
|
||||
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> cosr = 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<Map<String, IrisDimensionCarvingEntry>> carvingEntryIndex = 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)
|
||||
@Description("Dimension-level cave biome carving overrides with absolute world Y ranges")
|
||||
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")
|
||||
private IrisCaveProfile caveProfile = new IrisCaveProfile();
|
||||
@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<>();
|
||||
Map<String, IrisDimensionCarvingEntry> carvingEntryIndex = getCarvingEntryIndex();
|
||||
while (!pending.isEmpty()) {
|
||||
@@ -1187,6 +1202,44 @@ public class IrisDimension extends IrisRegistrant {
|
||||
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() {
|
||||
return upperDimension != null && !upperDimension.isEmpty() && !upperDimension.equalsIgnoreCase("none");
|
||||
}
|
||||
|
||||
+12
-9
@@ -40,7 +40,7 @@ public final class IrisDimensionCarvingResolver {
|
||||
if (root == 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);
|
||||
}
|
||||
|
||||
@@ -54,14 +54,14 @@ public final class IrisDimensionCarvingResolver {
|
||||
return resolveBoundRootEntry(engine, worldY, data, resolvedState);
|
||||
}
|
||||
|
||||
public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ) {
|
||||
return resolveFromRoot(engine, rootEntry, worldX, worldZ, threadState());
|
||||
public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldY, int worldZ) {
|
||||
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;
|
||||
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) {
|
||||
@@ -109,7 +109,7 @@ public final class IrisDimensionCarvingResolver {
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
@@ -127,7 +127,7 @@ public final class IrisDimensionCarvingResolver {
|
||||
ColumnResolutions columns = resolvedState.columns;
|
||||
int slot = ((worldX & 31) << 5) | (worldZ & 31);
|
||||
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];
|
||||
}
|
||||
|
||||
@@ -138,7 +138,8 @@ public final class IrisDimensionCarvingResolver {
|
||||
while (depth > 0) {
|
||||
cacheable &= fixedChildStyle(current);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -156,6 +157,7 @@ public final class IrisDimensionCarvingResolver {
|
||||
resolvedState.columns = columns;
|
||||
}
|
||||
columns.coordinates[slot] = coordinate;
|
||||
columns.worldYs[slot] = worldY;
|
||||
columns.entries[slot] = current;
|
||||
columns.roots[slot] = rootEntry;
|
||||
}
|
||||
@@ -332,7 +334,7 @@ public final class IrisDimensionCarvingResolver {
|
||||
if (root == 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);
|
||||
}
|
||||
}
|
||||
@@ -399,6 +401,7 @@ public final class IrisDimensionCarvingResolver {
|
||||
|
||||
private static final class ColumnResolutions {
|
||||
private final long[] coordinates = new long[1024];
|
||||
private final int[] worldYs = new int[1024];
|
||||
private final IrisDimensionCarvingEntry[] roots = 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) {
|
||||
return false;
|
||||
}
|
||||
if (engine.getDimension().allowsSubterrainFeatures()
|
||||
&& engine.getDimension().getSubterrainFeatures() != null && !engine.getDimension().getSubterrainFeatures().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
int worldMinHeight = engine.getWorld().minHeight();
|
||||
int internalY = (quartY << 2) - worldMinHeight;
|
||||
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 internalY = blockY - worldMinHeight;
|
||||
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
|
||||
? stackLayout == null
|
||||
? 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) {
|
||||
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() {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package art.arcane.iris.structure.object;
|
||||
|
||||
public enum IrisObjectPlacementAlignment {
|
||||
CENTER,
|
||||
FLOOR
|
||||
}
|
||||
+42
-29
@@ -68,9 +68,11 @@ final class IrisObjectPlacementRunner {
|
||||
private static final ConcurrentHashMap<String, Long> IMPLAUSIBLE_BEDROCK_WARNS = new ConcurrentHashMap<>();
|
||||
|
||||
private final IrisObject self;
|
||||
private final IrisObjectPlacementAlignment alignment;
|
||||
|
||||
IrisObjectPlacementRunner(IrisObject self) {
|
||||
IrisObjectPlacementRunner(IrisObject self, IrisObjectPlacementAlignment alignment) {
|
||||
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) {
|
||||
@@ -169,7 +171,19 @@ final class IrisObjectPlacementRunner {
|
||||
TransformedBounds placementBounds = transformedBounds(spin, translating, translateOffset, ceilingHang, warpMargin);
|
||||
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.
|
||||
y = (self.getH() + 1) + rty;
|
||||
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);
|
||||
CarvingMode carvingMode = config.getCarvingSupport();
|
||||
if (!config.isForcePlace() && !carvingMode.equals(CarvingMode.CARVING_ONLY)) {
|
||||
@@ -608,9 +622,7 @@ final class IrisObjectPlacementRunner {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (waterlogCandidate && IrisProceduralBlocks.hasProperty(data, "waterlogged") && shouldAutoWaterlogBlock(placer, config, yv, xx, yy, zz)) {
|
||||
data = data.withProperty("waterlogged", "true");
|
||||
}
|
||||
data = IrisProceduralBlocks.normalizeWaterlogging(data, placer.get(xx, yy, zz), waterlogCandidate);
|
||||
|
||||
if (!rawStructurePiece && B.isVineBlock(data)) {
|
||||
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 {
|
||||
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;
|
||||
@@ -840,9 +852,7 @@ final class IrisObjectPlacementRunner {
|
||||
|
||||
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;
|
||||
if (settings != null) {
|
||||
@@ -882,7 +892,7 @@ final class IrisObjectPlacementRunner {
|
||||
if (B.isVineBlock(d)) {
|
||||
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;
|
||||
}
|
||||
|
||||
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,
|
||||
boolean ceilingHang, int margin) {
|
||||
int sourceMinX = -self.getCenter().getBlockX();
|
||||
@@ -1218,23 +1248,6 @@ final class IrisObjectPlacementRunner {
|
||||
|| 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) {
|
||||
NativeBlockState result = data;
|
||||
for (String face : IrisProceduralBlocks.FACE_PROPERTIES) {
|
||||
|
||||
@@ -124,6 +124,22 @@ public class IrisEntitySpawn implements Rarity {
|
||||
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,
|
||||
int floor, int top, RNG rng) {
|
||||
if (group == IrisSpawnGroup.CAVE) {
|
||||
@@ -206,7 +222,7 @@ public class IrisEntitySpawn implements Rarity {
|
||||
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();
|
||||
World world = chunk.getWorld();
|
||||
int minimumY = world.getMinHeight() + 1;
|
||||
|
||||
@@ -306,6 +306,10 @@ public final class GenerationSemanticCapture {
|
||||
snapshot = IrisDimensionCarvingResolver.snapshot(engine);
|
||||
minimumY = engine.getWorld().minHeight();
|
||||
}
|
||||
IrisBiome featureBiome = engine.getSubterrainBiome(x, y, z);
|
||||
if (featureBiome != null) {
|
||||
return featureBiome;
|
||||
}
|
||||
IrisBiome configured = snapshot.resolveBiome(x, y + minimumY, z);
|
||||
if (configured != null) {
|
||||
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.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.generation.hydrology.HydrologyFeatureQuery;
|
||||
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 java.math.BigInteger;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalLong;
|
||||
@@ -19,6 +23,57 @@ public final class 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(
|
||||
Engine engine,
|
||||
String key,
|
||||
|
||||
@@ -3,6 +3,7 @@ package art.arcane.iris.world.history;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
@@ -30,9 +31,12 @@ public final class SavedBiomeCapture {
|
||||
stage.chunkX(), stage.chunkZ(), activationId, minimumY, height));
|
||||
TransitionGenerationPlan transition = engine.getComplex().getTransitionGenerationPlan();
|
||||
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);
|
||||
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 localZ = 0; localZ < 16; localZ++) {
|
||||
int blockX = startX + localX;
|
||||
@@ -51,13 +55,13 @@ public final class SavedBiomeCapture {
|
||||
if (transition != null) {
|
||||
surfaceCell = historicalCell(new Sample(blockX, surfaceY, blockZ, true), surfaceCell, transition, saved, historical);
|
||||
}
|
||||
int quartX = localX & ~3;
|
||||
int quartZ = localZ & ~3;
|
||||
int quartKey = quartX * 16 + quartZ;
|
||||
List<SavedBiomeChunk.Span> spans = vertical.get(quartKey);
|
||||
int sampleX = exactVolume ? localX : localX & ~3;
|
||||
int sampleZ = exactVolume ? localZ : localZ & ~3;
|
||||
int columnKey = sampleX * 16 + sampleZ;
|
||||
List<SavedBiomeChunk.Span> spans = vertical.get(columnKey);
|
||||
if (spans == null) {
|
||||
spans = captureVertical(column, startX + quartX, startZ + quartZ, cells, transition, saved, historical, floating);
|
||||
vertical.put(quartKey, spans);
|
||||
spans = captureVertical(column, startX + sampleX, startZ + sampleZ, cells, transition, saved, historical, floating);
|
||||
vertical.put(columnKey, spans);
|
||||
}
|
||||
SavedBiomeChunk.Cell caveBase = cells.cell(engine.getCaveBiome(blockX, blockZ), region);
|
||||
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,
|
||||
TransitionGenerationPlan transition, SavedBiomeRuntime saved,
|
||||
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 maximumY = minimumY + column.height();
|
||||
List<SavedBiomeChunk.Span> spans = new ArrayList<>();
|
||||
SavedBiomeChunk.Cell previous = null;
|
||||
int startY = minimumY;
|
||||
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]);
|
||||
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));
|
||||
}
|
||||
if (transition != null) {
|
||||
@@ -135,9 +139,10 @@ public final class SavedBiomeCapture {
|
||||
Objects.requireNonNull(region, "region").getLoadKey());
|
||||
}
|
||||
|
||||
private record Column(Engine engine, int minimumY, int height, IrisBiome[] biomes, IrisRegion[] regions) {
|
||||
private Column(Engine engine, int minimumY, int height) {
|
||||
this(engine, minimumY, height, new IrisBiome[(height + 3) / 4], new IrisRegion[(height + 3) / 4]);
|
||||
private record Column(Engine engine, int minimumY, int height, int step, IrisBiome[] biomes, IrisRegion[] regions) {
|
||||
private Column(Engine engine, int minimumY, int height, int step) {
|
||||
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.pack.loading.IrisData;
|
||||
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.biome.IrisBiome;
|
||||
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 java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
@@ -17,6 +22,7 @@ import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
@@ -39,6 +45,7 @@ public final class SavedBiomeRuntime implements AutoCloseable {
|
||||
private final GenerationHistory history;
|
||||
private final SavedBiomeStore store;
|
||||
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 Map<Long, PendingQuery> pending = new HashMap<>();
|
||||
private final ReentrantReadWriteLock consumption = new ReentrantReadWriteLock();
|
||||
@@ -135,6 +142,41 @@ public final class SavedBiomeRuntime implements AutoCloseable {
|
||||
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) {
|
||||
int chunkX = Math.floorDiv(blockX, 16);
|
||||
int chunkZ = Math.floorDiv(blockZ, 16);
|
||||
@@ -254,6 +296,7 @@ public final class SavedBiomeRuntime implements AutoCloseable {
|
||||
}
|
||||
}
|
||||
definitions.clear();
|
||||
subterrainPlanners.clear();
|
||||
} finally {
|
||||
consumption.writeLock().unlock();
|
||||
}
|
||||
@@ -466,8 +509,14 @@ public final class SavedBiomeRuntime implements AutoCloseable {
|
||||
if (dimension == null) {
|
||||
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),
|
||||
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();
|
||||
definitions.put(epochId, loaded);
|
||||
return loaded;
|
||||
@@ -540,7 +589,7 @@ public final class SavedBiomeRuntime implements AutoCloseable {
|
||||
}
|
||||
|
||||
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) {
|
||||
IrisBiome biome = data.getBiomeLoader().load(cell.biomeKey());
|
||||
IrisRegion region = data.getRegionLoader().load(cell.regionKey());
|
||||
|
||||
@@ -507,7 +507,7 @@ public class DecoratorCoreTest {
|
||||
|
||||
@Test
|
||||
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,
|
||||
"base", "middle", "middle", "frustum", "tip");
|
||||
@@ -515,7 +515,7 @@ public class DecoratorCoreTest {
|
||||
|
||||
@Test
|
||||
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,
|
||||
"base", "middle", "middle", "frustum", "tip");
|
||||
@@ -524,17 +524,26 @@ public class DecoratorCoreTest {
|
||||
@Test
|
||||
public void shortSulfurSpikesHaveCorrectTipsInBothDirections() {
|
||||
for (boolean upward : new boolean[]{true, false}) {
|
||||
Hunk<NativeBlockState> single = placeSpikeColumn("minecraft:sulfur_spike", 1, upward, false);
|
||||
Hunk<NativeBlockState> pair = placeSpikeColumn("minecraft:sulfur_spike", 2, upward, false);
|
||||
Hunk<NativeBlockState> single = placeSpikeColumn("minecraft:sulfur_spike", 1, upward, false, false);
|
||||
Hunk<NativeBlockState> pair = placeSpikeColumn("minecraft:sulfur_spike", 2, upward, false, false);
|
||||
|
||||
assertSpikeColumn(single, "minecraft:sulfur_spike", upward, false, "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
|
||||
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,
|
||||
"base", "middle", "frustum", "tip");
|
||||
@@ -738,7 +747,7 @@ public class DecoratorCoreTest {
|
||||
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);
|
||||
IrisData data = mock(IrisData.class);
|
||||
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);
|
||||
|
||||
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++) {
|
||||
output.set(0, y, 0, airState());
|
||||
output.set(0, y, 0, target);
|
||||
}
|
||||
NativeBlockState support = sturdyState();
|
||||
output.set(0, upward ? 0 : height + 1, 0, support);
|
||||
DecoratorCore.PlaceOpts opts = new DecoratorCore.PlaceOpts();
|
||||
opts.underwater = submerged;
|
||||
opts.fluidHeight = height;
|
||||
if (upward) {
|
||||
DecoratorCore.placeStackUp(decorator, 0, 0, 0, 0, 0, height, output, new RNG(1L), data, opts);
|
||||
} else {
|
||||
|
||||
+44
@@ -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;
|
||||
}
|
||||
}
|
||||
+62
@@ -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));
|
||||
}
|
||||
}
|
||||
+71
@@ -19,6 +19,9 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
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.structure.object.IObjectPlacer;
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
@@ -112,6 +115,74 @@ public class CaveObjectPlacementTransactionTest {
|
||||
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
|
||||
public void dryHydrologyHeadroomRejectsTheWholePlacement() {
|
||||
IObjectPlacer delegate = createPlacer(128, 80, 20, 90);
|
||||
|
||||
+60
@@ -1,13 +1,21 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
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.HydrologyCaveCell;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class MantleObjectComponentCaveAnchorTest {
|
||||
@Test
|
||||
@@ -37,8 +45,10 @@ public class MantleObjectComponentCaveAnchorTest {
|
||||
public void explicitWetAndForcedAirPlacementsRemainEligible() {
|
||||
MatterCavern water = new MatterCavern(true, "", (byte) 1);
|
||||
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, lava, 20, 8));
|
||||
assertTrue(MantleObjectComponent.acceptsCaveAnchorFluid(false, forcedAir, 0, 8));
|
||||
assertFalse(MantleObjectComponent.acceptsCaveAnchorFluid(
|
||||
true,
|
||||
@@ -63,6 +73,56 @@ public class MantleObjectComponentCaveAnchorTest {
|
||||
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) {
|
||||
IrisBiome biome = new IrisBiome();
|
||||
biome.setLoadKey(loadKey);
|
||||
|
||||
+279
@@ -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) {
|
||||
}
|
||||
}
|
||||
+18
@@ -1,6 +1,9 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
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.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
@@ -48,6 +51,21 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
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
|
||||
public void commonAndGenericMetadataSurviveBlockReplacementAndSourceReplay() {
|
||||
MantleWriter writer = writer();
|
||||
|
||||
+1
@@ -248,6 +248,7 @@ public class DimensionTerrain3DCompositionTest {
|
||||
Engine engine = mock(Engine.class, CALLS_REAL_METHODS);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
doReturn(complex).when(engine).getComplex();
|
||||
doReturn(new IrisDimension()).when(engine).getDimension();
|
||||
DimensionStackContext stack = mock(DimensionStackContext.class);
|
||||
doReturn(stack).when(engine).getDimensionStackContext();
|
||||
DimensionStackLayout.LayerInput bottom = input(24, 0,
|
||||
|
||||
+1
-1
@@ -893,7 +893,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
|
||||
|
||||
private static IrisBiome carvingBiome(IrisEngine engine, IrisDimensionCarvingResolver.State 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);
|
||||
assertSame(expected, IrisDimensionCarvingResolver.resolveBiome(engine, 19, 80, -3, state));
|
||||
return expected;
|
||||
|
||||
+64
@@ -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();
|
||||
}
|
||||
}
|
||||
+45
-3
@@ -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.SpawnContext;
|
||||
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.math.RNG;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.GameRules;
|
||||
import org.bukkit.World;
|
||||
import org.junit.Test;
|
||||
import org.junit.BeforeClass;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
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.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
|
||||
public class WorldEntitySpawnerInitialSeedTest {
|
||||
@BeforeClass
|
||||
@@ -52,6 +60,38 @@ public class WorldEntitySpawnerInitialSeedTest {
|
||||
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
|
||||
public void chunkBatchSeedsAreIndependentOfVisitOrderAndUnrelatedRandomActivity() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
@@ -86,7 +126,7 @@ public class WorldEntitySpawnerInitialSeedTest {
|
||||
private final Engine engine = mock(Engine.class);
|
||||
private final IrisWorldManager manager = mock(IrisWorldManager.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 Map<String, Observation> observed = new HashMap<>();
|
||||
private final BiomeEnvironment environment;
|
||||
@@ -114,7 +154,9 @@ public class WorldEntitySpawnerInitialSeedTest {
|
||||
when(definition.getInitialSpawns()).thenReturn(new KList<>(entry));
|
||||
when(definition.getMaxEntitiesPerChunk()).thenReturn(4);
|
||||
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(entry.spawn(eq(engine), any(Chunk.class), any(RNG.class), anyInt(), any(SpawnContext.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
@@ -123,7 +165,7 @@ public class WorldEntitySpawnerInitialSeedTest {
|
||||
int capacity = invocation.getArgument(3);
|
||||
SpawnContext context = invocation.getArgument(4);
|
||||
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);
|
||||
spawner = new WorldEntitySpawner(manager);
|
||||
|
||||
+116
@@ -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"));
|
||||
}
|
||||
}
|
||||
+129
@@ -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)));
|
||||
}
|
||||
}
|
||||
+304
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
-17
@@ -116,7 +116,7 @@ public class IrisDimensionCarvingResolverParityTest {
|
||||
doReturn(generator).when(parent).getChildrenGenerator(anyLong(), any(IrisData.class));
|
||||
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));
|
||||
verify(generator, times(2)).noiseFast2D(-17D, 23D);
|
||||
}
|
||||
@@ -140,13 +140,30 @@ public class IrisDimensionCarvingResolverParityTest {
|
||||
doReturn(generator).when(parent).getChildrenGenerator(anyLong(), any(IrisData.class));
|
||||
|
||||
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);
|
||||
verify(generator).noiseFast2D(-17D, 23D);
|
||||
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
|
||||
public void repeatedColumnResolutionSamplesChildrenOnceAcrossHeights() {
|
||||
Fixture fixture = createFixture();
|
||||
@@ -161,7 +178,7 @@ public class IrisDimensionCarvingResolverParityTest {
|
||||
for (int height = 0; height < 16; height++) {
|
||||
for (int x = -16; x < 0; x++) {
|
||||
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");
|
||||
IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State();
|
||||
|
||||
assertSame(child, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, -17, 23, state));
|
||||
assertSame(parent, 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, -1500, 23, state));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,11 +221,11 @@ public class IrisDimensionCarvingResolverParityTest {
|
||||
IrisDimensionCarvingEntry firstChild = fixture.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();
|
||||
assertSame(firstChild, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, -17, 23, state));
|
||||
assertSame(replacementChild, IrisDimensionCarvingResolver.resolveFromRoot(replacement.engine, parent, -17, 23, state));
|
||||
assertSame(replacementChild, IrisDimensionCarvingResolver.resolveFromRoot(replacement.engine, parent, -17, 23, state));
|
||||
assertSame(firstChild, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, -17, -1500, 23, state));
|
||||
assertSame(replacementChild, IrisDimensionCarvingResolver.resolveFromRoot(replacement.engine, parent, -17, -1500, 23, state));
|
||||
assertSame(replacementChild, IrisDimensionCarvingResolver.resolveFromRoot(replacement.engine, parent, -17, -1500, 23, state));
|
||||
|
||||
verify(generator, times(3)).noiseFast2D(-17D, 23D);
|
||||
}
|
||||
@@ -225,8 +242,8 @@ public class IrisDimensionCarvingResolverParityTest {
|
||||
|
||||
for (int worldX = -384; worldX <= 384; worldX += 29) {
|
||||
for (int worldZ = -384; worldZ <= 384; worldZ += 31) {
|
||||
IrisDimensionCarvingEntry legacyResolved = legacyResolveFromRoot(fixture.engine, legacyRoot, worldX, worldZ);
|
||||
IrisDimensionCarvingEntry statefulResolved = IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, statefulRoot, worldX, worldZ, state);
|
||||
IrisDimensionCarvingEntry legacyResolved = legacyResolveFromRoot(fixture.engine, legacyRoot, worldX, worldY, worldZ);
|
||||
IrisDimensionCarvingEntry statefulResolved = IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, statefulRoot, worldX, worldY, worldZ, state);
|
||||
assertSame("entry mismatch at worldY=" + worldY + " worldX=" + worldX + " worldZ=" + worldZ, legacyResolved, statefulResolved);
|
||||
assertSame(legacyResolveEntryBiome(fixture.engine, legacyResolved),
|
||||
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 worldZ = -640; worldZ <= 640; worldZ += 83) {
|
||||
IrisDimensionCarvingEntry legacyResolved = legacyResolveFromRoot(fixture.engine, legacyRoot, worldX, worldZ);
|
||||
IrisDimensionCarvingEntry statefulResolved = IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, statefulRoot, worldX, worldZ, state);
|
||||
IrisDimensionCarvingEntry legacyResolved = legacyResolveFromRoot(fixture.engine, legacyRoot, worldX, worldY, worldZ);
|
||||
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(legacyResolveEntryBiome(fixture.engine, legacyResolved),
|
||||
IrisDimensionCarvingResolver.resolveBiome(fixture.engine, worldX, worldY, worldZ, state));
|
||||
@@ -406,7 +423,7 @@ public class IrisDimensionCarvingResolverParityTest {
|
||||
|
||||
Fixture replacement = createMixedDepthFixture();
|
||||
IrisDimensionCarvingEntry expectedEntry = legacyResolveFromRoot(replacement.engine,
|
||||
legacyResolveRootEntry(replacement.engine, 83), -17, 23);
|
||||
legacyResolveRootEntry(replacement.engine, 83), -17, 83, 23);
|
||||
IrisBiome expected = legacyResolveEntryBiome(replacement.engine, expectedEntry);
|
||||
assertSame(expected, IrisDimensionCarvingResolver.resolveBiome(replacement.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++) {
|
||||
int worldZ = (chunkZ << 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;
|
||||
@@ -728,7 +745,7 @@ public class IrisDimensionCarvingResolverParityTest {
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
@@ -748,7 +765,8 @@ public class IrisDimensionCarvingResolverParityTest {
|
||||
int depth = remainingDepth;
|
||||
while (depth > 0) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,9 @@ import art.arcane.iris.pack.loading.ResourceLoader;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.block.IrisBlockData;
|
||||
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.generation.noise.IrisExpression;
|
||||
import art.arcane.iris.generation.noise.IrisGenerator;
|
||||
@@ -59,6 +62,7 @@ import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -70,6 +74,7 @@ import java.util.Map;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -78,7 +83,7 @@ public class SchemaBuilderIdentityTest {
|
||||
@ClassRule
|
||||
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(
|
||||
IrisDimension.class,
|
||||
@@ -127,6 +132,59 @@ public class SchemaBuilderIdentityTest {
|
||||
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) {
|
||||
MessageDigest sha256;
|
||||
try {
|
||||
|
||||
+55
@@ -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;
|
||||
|
||||
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.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockMakers;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
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.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.withSettings;
|
||||
|
||||
public class SavedBiomeCaptureTest {
|
||||
@Test
|
||||
@@ -77,6 +88,7 @@ public class SavedBiomeCaptureTest {
|
||||
@Test
|
||||
public void retainsFloatingChildIdentityWhenItSharesTheHostDerivative() throws Exception {
|
||||
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
|
||||
when(engine.getComplex().getSubterrainPlanner()).thenReturn(null);
|
||||
GenerationHistory.GenerationStage stage = mock(GenerationHistory.GenerationStage.class);
|
||||
SavedBiomeRuntime historical = mock(SavedBiomeRuntime.class);
|
||||
IrisBiome host = biome("host");
|
||||
@@ -118,6 +130,7 @@ public class SavedBiomeCaptureTest {
|
||||
@Test
|
||||
public void recordsExactSurfaceColumnsAndIndependentCaveAndVerticalIdentities() throws Exception {
|
||||
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
|
||||
when(engine.getComplex().getSubterrainPlanner()).thenReturn(null);
|
||||
GenerationHistory.GenerationStage stage = mock(GenerationHistory.GenerationStage.class);
|
||||
SavedBiomeRuntime historical = mock(SavedBiomeRuntime.class);
|
||||
IrisBiome left = biome("left");
|
||||
@@ -155,12 +168,118 @@ public class SavedBiomeCaptureTest {
|
||||
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) {
|
||||
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
|
||||
when(engine.getComplex().getSubterrainPlanner()).thenReturn(null);
|
||||
IrisRegion region = region("flat-region");
|
||||
when(engine.getMinHeight()).thenReturn(-64);
|
||||
when(engine.getHeight()).thenReturn(16);
|
||||
when(engine.getHeight(anyInt(), anyInt())).thenReturn(8);
|
||||
when(engine.getSubterrainBiome(anyInt(), anyInt(), anyInt())).thenReturn(null);
|
||||
when(engine.getDimensionStackContext()).thenReturn(null);
|
||||
when(engine.getComplex().getTransitionGenerationPlan()).thenReturn(null);
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user