mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-11 19:14:11 +00:00
Added dimension stacking
- Dimension files define upright top-to-bottom terrain layers - Spacer values set exact air gaps between layers - Optional noise styles shape blended layer boundaries - Runtime queries resolve stacked terrain and biome ownership - Pack validation rejects invalid stack definitions - Project copy and hotload paths include stack dependencies - Superflat fixtures cover gaps, blending, clipping, and fluids - Platform tests cover stacked world generation contracts
This commit is contained in:
+405
-58
@@ -4,6 +4,9 @@ import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.engine.DimensionStackContext;
|
||||
import art.arcane.iris.engine.DimensionStackLayout;
|
||||
import art.arcane.iris.engine.DimensionTerrainContext;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||
@@ -16,6 +19,7 @@ import art.arcane.iris.util.project.context.IrisContext;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.QuartPos;
|
||||
import net.minecraft.core.Registry;
|
||||
@@ -24,6 +28,8 @@ import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.level.LevelReader;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.Climate;
|
||||
@@ -80,27 +86,31 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
this.cacheRuntimeId = engine.getCacheID();
|
||||
}
|
||||
|
||||
private static List<Holder<Biome>> getAllBiomes(Registry<Biome> customRegistry, Registry<Biome> registry, Engine engine) {
|
||||
private static List<Holder<Biome>> getAllBiomes(
|
||||
Registry<Biome> customRegistry,
|
||||
Registry<Biome> registry,
|
||||
Engine engine,
|
||||
boolean includeDimensionStack
|
||||
) {
|
||||
LinkedHashSet<Holder<Biome>> biomes = new LinkedHashSet<>();
|
||||
|
||||
for (IrisBiome i : engine.getAllBiomes()) {
|
||||
for (OwnedBiome ownedBiome : getHostOwnedBiomes(engine)) {
|
||||
IrisBiome i = ownedBiome.biome();
|
||||
Holder<Biome> vanillaHolder = resolveBiomeHolder(registry, i.getStructureDerivativeKey());
|
||||
if (vanillaHolder == null) {
|
||||
throw new IllegalStateException("Iris structure biome derivative '"
|
||||
+ i.getStructureDerivativeKey() + "' is not registered for biome '" + i.getLoadKey() + "'");
|
||||
}
|
||||
biomes.add(vanillaHolder);
|
||||
}
|
||||
|
||||
if (i.isCustom()) {
|
||||
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
|
||||
Holder<Biome> customHolder = resolveCustomBiomeHolder(customRegistry, engine, j.getId());
|
||||
if (customHolder == null) {
|
||||
throw new IllegalStateException("Iris custom structure biome '"
|
||||
+ engine.getDimension().getLoadKey() + ":" + j.getId() + "' is not registered");
|
||||
}
|
||||
biomes.add(customHolder);
|
||||
}
|
||||
}
|
||||
if (includeDimensionStack) {
|
||||
biomes.addAll(getVisibleBiomes(
|
||||
customRegistry,
|
||||
registry,
|
||||
engine,
|
||||
resolveFallbackBiome(registry, customRegistry)
|
||||
));
|
||||
}
|
||||
|
||||
if (biomes.isEmpty()) {
|
||||
@@ -110,6 +120,57 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
return new ArrayList<>(biomes);
|
||||
}
|
||||
|
||||
private static List<Holder<Biome>> getVisibleBiomes(
|
||||
Registry<Biome> customRegistry,
|
||||
Registry<Biome> registry,
|
||||
Engine engine,
|
||||
Holder<Biome> fallback
|
||||
) {
|
||||
LinkedHashSet<Holder<Biome>> biomes = new LinkedHashSet<>();
|
||||
boolean fallbackPossible = false;
|
||||
for (OwnedBiome ownedBiome : getOwnedBiomes(engine)) {
|
||||
IrisBiome irisBiome = ownedBiome.biome();
|
||||
if (irisBiome.isCustom()) {
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
Holder<Biome> customHolder = resolveCustomBiomeHolder(
|
||||
customRegistry, ownedBiome.dimension(), customBiome.getId());
|
||||
if (customHolder == null) {
|
||||
throw new IllegalStateException("Iris custom visible biome '"
|
||||
+ customBiomeKey(ownedBiome.dimension(), customBiome.getId())
|
||||
+ "' is not registered");
|
||||
}
|
||||
biomes.add(customHolder);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
fallbackPossible |= !addVisibleBiomeHolder(
|
||||
biomes, registry, irisBiome.getDerivativeKey());
|
||||
for (String scatter : irisBiome.getBiomeScatter()) {
|
||||
fallbackPossible |= !addVisibleBiomeHolder(biomes, registry, scatter);
|
||||
}
|
||||
for (String scatter : irisBiome.getBiomeSkyScatter()) {
|
||||
fallbackPossible |= !addVisibleBiomeHolder(biomes, registry, scatter);
|
||||
}
|
||||
}
|
||||
if (fallbackPossible && fallback != null) {
|
||||
biomes.add(fallback);
|
||||
}
|
||||
return new ArrayList<>(biomes);
|
||||
}
|
||||
|
||||
private static boolean addVisibleBiomeHolder(
|
||||
Set<Holder<Biome>> biomes,
|
||||
Registry<Biome> registry,
|
||||
String biomeKey
|
||||
) {
|
||||
Holder<Biome> holder = resolveBiomeHolder(registry, biomeKey);
|
||||
if (holder == null) {
|
||||
return false;
|
||||
}
|
||||
biomes.add(holder);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Object getFor(Class<?> type, Object source) {
|
||||
Object o = fieldFor(type, source);
|
||||
|
||||
@@ -175,10 +236,19 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
|
||||
@Override
|
||||
protected Stream<Holder<Biome>> collectPossibleBiomes() {
|
||||
return possibleStructureBiomes().stream();
|
||||
return possibleBiomes(true).stream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Holder<Biome>> possibleBiomes() {
|
||||
return possibleBiomes(true);
|
||||
}
|
||||
|
||||
Set<Holder<Biome>> possibleStructureBiomes() {
|
||||
return possibleBiomes(false);
|
||||
}
|
||||
|
||||
private Set<Holder<Biome>> possibleBiomes(boolean includeDimensionStack) {
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_possible_biomes");
|
||||
if (lease == null) {
|
||||
throw new IllegalStateException("Iris possible biome lookup was rejected during an engine transition");
|
||||
@@ -197,7 +267,8 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
.lookup(Registries.BIOME).orElse(null);
|
||||
Registry<Biome> worldRegistry = ((CraftWorld) world).getHandle().registryAccess()
|
||||
.lookup(Registries.BIOME).orElse(null);
|
||||
return Set.copyOf(getAllBiomes(customRegistry, worldRegistry, engine));
|
||||
return Set.copyOf(getAllBiomes(
|
||||
customRegistry, worldRegistry, engine, includeDimensionStack));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,18 +278,22 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
return m;
|
||||
}
|
||||
|
||||
for (IrisBiome i : engine.getAllBiomes()) {
|
||||
for (OwnedBiome ownedBiome : getOwnedBiomes(engine)) {
|
||||
IrisBiome i = ownedBiome.biome();
|
||||
if (i.isCustom()) {
|
||||
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
|
||||
Holder<Biome> holder = resolveCustomBiomeHolder(customRegistry, engine, j.getId());
|
||||
String key = customBiomeKey(ownedBiome.dimension(), j.getId());
|
||||
Holder<Biome> holder = resolveCustomBiomeHolder(
|
||||
customRegistry, ownedBiome.dimension(), j.getId());
|
||||
if (holder == null) {
|
||||
if (fallback != null) {
|
||||
m.put(j.getId(), fallback);
|
||||
m.put(key, fallback);
|
||||
}
|
||||
IrisLogging.error("Cannot find biome for IrisBiomeCustom " + j.getId() + " from engine " + engine.getName());
|
||||
IrisLogging.error("Cannot find biome for IrisBiomeCustom " + key
|
||||
+ " from engine " + engine.getName());
|
||||
continue;
|
||||
}
|
||||
m.put(j.getId(), holder);
|
||||
m.put(key, holder);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,7 +307,8 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
return Collections.unmodifiableMap(spawnBiomes);
|
||||
}
|
||||
|
||||
for (IrisBiome irisBiome : engine.getAllBiomes()) {
|
||||
for (OwnedBiome ownedBiome : getOwnedBiomes(engine)) {
|
||||
IrisBiome irisBiome = ownedBiome.biome();
|
||||
if (!irisBiome.isCustom()) {
|
||||
continue;
|
||||
}
|
||||
@@ -241,7 +317,8 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
Holder<Biome> customHolder = resolveCustomBiomeHolder(customRegistry, engine, customBiome.getId());
|
||||
Holder<Biome> customHolder = resolveCustomBiomeHolder(
|
||||
customRegistry, ownedBiome.dimension(), customBiome.getId());
|
||||
if (customHolder != null) {
|
||||
spawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
|
||||
}
|
||||
@@ -268,6 +345,34 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
}
|
||||
}
|
||||
|
||||
Holder<Biome> getVisibleSurfaceBiome(int blockX, int blockZ) {
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_surface_spawn_biome");
|
||||
if (lease == null) {
|
||||
return null;
|
||||
}
|
||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!isRuntimeAvailable()) {
|
||||
return null;
|
||||
}
|
||||
ensureCachesCurrent();
|
||||
int quartX = QuartPos.fromBlock(blockX);
|
||||
int quartZ = QuartPos.fromBlock(blockZ);
|
||||
int sampleX = QuartPos.toBlock(quartX);
|
||||
int sampleZ = QuartPos.toBlock(quartZ);
|
||||
boolean cacheable = isBiomeCacheable(engine, sampleX, sampleZ);
|
||||
DimensionStackContext stackContext = engine.getDimensionStackContext();
|
||||
DimensionStackLayout.Layer layer = stackContext == null
|
||||
? null
|
||||
: stackContext.getLayout(sampleX, sampleZ).surfaceLayer();
|
||||
int internalY = layer == null
|
||||
? Engine.hostHeight(engine, sampleX, sampleZ, true)
|
||||
: layer.clippedSurfaceY();
|
||||
int quartY = QuartPos.fromBlock(internalY + engine.getWorld().minHeight());
|
||||
return getVisibleNoiseBiomeWithActiveGenerationLease(
|
||||
quartX, quartY, quartZ, null, null, cacheable);
|
||||
}
|
||||
}
|
||||
|
||||
private RegistryAccess registry() {
|
||||
RegistryAccess access = registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()));
|
||||
|
||||
@@ -294,28 +399,38 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
|
||||
}
|
||||
ensureCachesCurrent();
|
||||
if (isGuaranteedSurfaceBiome(y)) {
|
||||
return getSurfaceStructureBiomeHolder(x, z);
|
||||
}
|
||||
return getStructureNoiseBiomeWithActiveGenerationLease(x, y, z);
|
||||
}
|
||||
}
|
||||
|
||||
long cacheKey = packNoiseKey(x, y, z);
|
||||
private Holder<Biome> getStructureNoiseBiomeWithActiveGenerationLease(int x, int y, int z) {
|
||||
if (isGuaranteedSurfaceBiome(y)) {
|
||||
return getSurfaceStructureBiomeHolder(x, z);
|
||||
}
|
||||
|
||||
boolean cacheable = isBiomeCacheable(x, z);
|
||||
long cacheKey = packNoiseKey(x, y, z);
|
||||
if (cacheable) {
|
||||
Holder<Biome> cachedHolder = structureBiomeCache.get(cacheKey);
|
||||
if (cachedHolder != null) {
|
||||
return cachedHolder;
|
||||
}
|
||||
}
|
||||
|
||||
Holder<Biome> resolvedHolder = resolveStructureBiomeHolder(x, y, z);
|
||||
Holder<Biome> existingHolder = structureBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
|
||||
if (existingHolder != null) {
|
||||
return existingHolder;
|
||||
}
|
||||
|
||||
if (structureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
||||
structureBiomeCache.clear();
|
||||
}
|
||||
|
||||
Holder<Biome> resolvedHolder = resolveStructureBiomeHolder(x, y, z);
|
||||
if (!cacheable) {
|
||||
return resolvedHolder;
|
||||
}
|
||||
Holder<Biome> existingHolder = structureBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
|
||||
if (existingHolder != null) {
|
||||
return existingHolder;
|
||||
}
|
||||
|
||||
if (structureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
||||
structureBiomeCache.clear();
|
||||
}
|
||||
|
||||
return resolvedHolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -346,6 +461,62 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pair<BlockPos, Holder<Biome>> findClosestBiome3d(
|
||||
BlockPos origin,
|
||||
int searchRadius,
|
||||
int sampleResolutionHorizontal,
|
||||
int sampleResolutionVertical,
|
||||
Predicate<Holder<Biome>> allowed,
|
||||
Climate.Sampler sampler,
|
||||
LevelReader level
|
||||
) {
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_locate_visible_biome");
|
||||
if (lease == null) {
|
||||
throw new IllegalStateException("Iris visible biome search was rejected during an engine transition");
|
||||
}
|
||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
prepareVisibleBiomeBatch();
|
||||
DimensionStackContext stackContext = engine.getDimensionStackContext();
|
||||
if (stackContext == null) {
|
||||
return super.findClosestBiome3d(
|
||||
origin, searchRadius, sampleResolutionHorizontal, sampleResolutionVertical,
|
||||
allowed, sampler, level);
|
||||
}
|
||||
Set<Holder<Biome>> candidates = new HashSet<>();
|
||||
for (Holder<Biome> biome : getVisibleBiomes(
|
||||
biomeCustomRegistry, biomeRegistry, engine, fallbackBiome)) {
|
||||
if (allowed.test(biome)) {
|
||||
candidates.add(biome);
|
||||
}
|
||||
}
|
||||
if (candidates.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
int sampleRadius = Math.floorDiv(searchRadius, sampleResolutionHorizontal);
|
||||
int[] sampleYs = Mth.outFromOrigin(
|
||||
origin.getY(), level.getMinY() + 1, level.getMaxY() + 1, sampleResolutionVertical).toArray();
|
||||
for (BlockPos.MutableBlockPos sampleColumn : BlockPos.spiralAround(
|
||||
BlockPos.ZERO, sampleRadius, Direction.EAST, Direction.SOUTH)) {
|
||||
int blockX = origin.getX() + sampleColumn.getX() * sampleResolutionHorizontal;
|
||||
int blockZ = origin.getZ() + sampleColumn.getZ() * sampleResolutionHorizontal;
|
||||
int quartX = QuartPos.fromBlock(blockX);
|
||||
int quartZ = QuartPos.fromBlock(blockZ);
|
||||
DimensionStackLayout stackLayout = stackContext.sample(
|
||||
QuartPos.toBlock(quartX), QuartPos.toBlock(quartZ));
|
||||
for (int blockY : sampleYs) {
|
||||
int quartY = QuartPos.fromBlock(blockY);
|
||||
Holder<Biome> visibleBiome = resolveVisibleBiomeHolder(
|
||||
quartX, quartY, quartZ, null, stackLayout);
|
||||
if (candidates.contains(visibleBiome)) {
|
||||
return Pair.of(new BlockPos(blockX, blockY, blockZ), visibleBiome);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static int horizontalBiomeSearchQuartStep(int blockY, int searchRadius) {
|
||||
return blockY == STRONGHOLD_RING_SEARCH_Y && searchRadius == STRONGHOLD_RING_SEARCH_RADIUS
|
||||
? STRONGHOLD_RING_SEARCH_QUART_STEP
|
||||
@@ -468,7 +639,7 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
private Holder<Biome> resolveSurfaceStructureBiomeHolder(int x, int z) {
|
||||
int blockX = x << 2;
|
||||
int blockZ = z << 2;
|
||||
IrisBiome irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
IrisBiome irisBiome = resolveSurfaceStructureBiome(engine, blockX, blockZ);
|
||||
if (irisBiome == null) {
|
||||
throw new IllegalStateException("Iris returned no surface structure biome at block "
|
||||
+ blockX + "," + blockZ);
|
||||
@@ -532,14 +703,32 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
int z,
|
||||
Climate.Sampler sampler,
|
||||
IrisDimensionCarvingResolver.State resolverState
|
||||
) {
|
||||
boolean cacheable = isBiomeCacheable(x, z);
|
||||
return getVisibleNoiseBiomeWithActiveGenerationLease(
|
||||
x, y, z, sampler, resolverState, cacheable);
|
||||
}
|
||||
|
||||
private Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
Climate.Sampler sampler,
|
||||
IrisDimensionCarvingResolver.State resolverState,
|
||||
boolean cacheable
|
||||
) {
|
||||
long cacheKey = packNoiseKey(x, y, z);
|
||||
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
|
||||
if (cachedHolder != null) {
|
||||
return cachedHolder;
|
||||
if (cacheable) {
|
||||
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
|
||||
if (cachedHolder != null) {
|
||||
return cachedHolder;
|
||||
}
|
||||
}
|
||||
|
||||
Holder<Biome> resolvedHolder = resolveVisibleBiomeHolder(x, y, z, resolverState);
|
||||
if (!cacheable) {
|
||||
return resolvedHolder;
|
||||
}
|
||||
Holder<Biome> existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
|
||||
if (existingHolder != null) {
|
||||
return existingHolder;
|
||||
@@ -552,6 +741,18 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
return resolvedHolder;
|
||||
}
|
||||
|
||||
private boolean isBiomeCacheable(int quartX, int quartZ) {
|
||||
return isBiomeCacheable(
|
||||
engine,
|
||||
QuartPos.toBlock(quartX),
|
||||
QuartPos.toBlock(quartZ)
|
||||
);
|
||||
}
|
||||
|
||||
static boolean isBiomeCacheable(Engine engine, int blockX, int blockZ) {
|
||||
return !engine.answersFromNaturalTerrain(blockX, blockZ);
|
||||
}
|
||||
|
||||
private GenerationSessionLease tryAcquireGenerationLease(String operation) {
|
||||
if (engine.isClosed()) {
|
||||
return null;
|
||||
@@ -597,7 +798,7 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveStructureBiomeHolder(int x, int y, int z) {
|
||||
BiomeResolution resolution = resolveBiomeResolution(x, y, z);
|
||||
BiomeResolution resolution = resolveStructureBiomeResolution(x, y, z);
|
||||
if (resolution == null) {
|
||||
throw new IllegalStateException("Iris returned no structure biome at quart "
|
||||
+ x + "," + y + "," + z);
|
||||
@@ -619,11 +820,26 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
int z,
|
||||
IrisDimensionCarvingResolver.State resolverState
|
||||
) {
|
||||
BiomeResolution resolution = resolveBiomeResolution(x, y, z, resolverState);
|
||||
return resolveVisibleBiomeHolder(x, y, z, resolverState, null);
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveVisibleBiomeHolder(
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
IrisDimensionCarvingResolver.State resolverState,
|
||||
DimensionStackLayout stackLayout
|
||||
) {
|
||||
BiomeResolution resolution = resolveBiomeResolution(
|
||||
x, y, z, resolverState, true, stackLayout);
|
||||
if (resolution == null) {
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
return resolveVisibleBiomeHolder(resolution);
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveVisibleBiomeHolder(BiomeResolution resolution) {
|
||||
if (resolution.irisBiome.isCustom()) {
|
||||
return resolveCustomHolder(resolution);
|
||||
}
|
||||
@@ -642,7 +858,8 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
private Holder<Biome> resolveCustomHolder(BiomeResolution resolution) {
|
||||
IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
|
||||
if (customBiome != null) {
|
||||
Holder<Biome> holder = customBiomes.get(customBiome.getId());
|
||||
Holder<Biome> holder = customBiomes.get(customBiomeKey(
|
||||
resolution.dimension, customBiome.getId()));
|
||||
if (holder != null) {
|
||||
return holder;
|
||||
}
|
||||
@@ -651,8 +868,8 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
private BiomeResolution resolveBiomeResolution(int x, int y, int z) {
|
||||
return resolveBiomeResolution(x, y, z, null);
|
||||
private BiomeResolution resolveStructureBiomeResolution(int x, int y, int z) {
|
||||
return resolveBiomeResolution(x, y, z, null, false);
|
||||
}
|
||||
|
||||
private BiomeResolution resolveBiomeResolution(
|
||||
@@ -660,6 +877,28 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
int y,
|
||||
int z,
|
||||
IrisDimensionCarvingResolver.State resolverState
|
||||
) {
|
||||
return resolveBiomeResolution(x, y, z, resolverState, true);
|
||||
}
|
||||
|
||||
private BiomeResolution resolveBiomeResolution(
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
IrisDimensionCarvingResolver.State resolverState,
|
||||
boolean includeDimensionStack
|
||||
) {
|
||||
return resolveBiomeResolution(
|
||||
x, y, z, resolverState, includeDimensionStack, null);
|
||||
}
|
||||
|
||||
private BiomeResolution resolveBiomeResolution(
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
IrisDimensionCarvingResolver.State resolverState,
|
||||
boolean includeDimensionStack,
|
||||
DimensionStackLayout stackLayout
|
||||
) {
|
||||
if (engine == null || engine.isClosed()) {
|
||||
return null;
|
||||
@@ -675,31 +914,63 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
int worldMinHeight = engine.getWorld().minHeight();
|
||||
int internalY = blockY - worldMinHeight;
|
||||
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
|
||||
DimensionStackLayout.Layer stackLayer = includeDimensionStack
|
||||
? stackLayout == null
|
||||
? resolveDimensionStackLayer(engine, blockX, internalY, blockZ)
|
||||
: stackLayout.layerAt(internalY)
|
||||
: null;
|
||||
IrisDimension owningDimension = stackLayer == null
|
||||
? engine.getDimension()
|
||||
: stackLayer.terrainContext().getDimension();
|
||||
if (stackLayer != null && !stackLayer.terrainContext().isSelfReferencing()) {
|
||||
IrisBiome stackedBiome = stackLayer.biome();
|
||||
return stackedBiome == null
|
||||
? null
|
||||
: createBiomeResolution(
|
||||
stackedBiome, false, owningDimension, blockX, blockY, blockZ);
|
||||
}
|
||||
boolean deepUnderground = internalY <= caveSwitchInternalY;
|
||||
boolean underground = false;
|
||||
IrisBiome irisBiome;
|
||||
if (deepUnderground) {
|
||||
int surfaceInternalY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
|
||||
if (stackLayer != null) {
|
||||
int surfaceInternalY = stackLayer.surfaceY();
|
||||
underground = internalY <= surfaceInternalY - 8;
|
||||
irisBiome = underground
|
||||
? engine.getCaveBiome(
|
||||
blockX,
|
||||
internalY,
|
||||
blockZ,
|
||||
resolverState,
|
||||
stackLayer.biome(),
|
||||
surfaceInternalY
|
||||
)
|
||||
: stackLayer.biome();
|
||||
} else if (deepUnderground) {
|
||||
int surfaceInternalY = Engine.hostHeight(engine, blockX, blockZ, true);
|
||||
underground = internalY <= surfaceInternalY - 8;
|
||||
irisBiome = underground
|
||||
? engine.getCaveBiome(blockX, internalY, blockZ, resolverState)
|
||||
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
: engine.getHostSurfaceBiome(blockX, blockZ);
|
||||
} else {
|
||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
irisBiome = engine.getHostSurfaceBiome(blockX, blockZ);
|
||||
}
|
||||
if (irisBiome == null && underground) {
|
||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
irisBiome = stackLayer == null
|
||||
? engine.getHostSurfaceBiome(blockX, blockZ)
|
||||
: stackLayer.biome();
|
||||
}
|
||||
if (irisBiome == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createBiomeResolution(irisBiome, underground, blockX, blockY, blockZ);
|
||||
return createBiomeResolution(
|
||||
irisBiome, underground, owningDimension, blockX, blockY, blockZ);
|
||||
}
|
||||
|
||||
private BiomeResolution createBiomeResolution(
|
||||
IrisBiome irisBiome,
|
||||
boolean underground,
|
||||
IrisDimension dimension,
|
||||
int blockX,
|
||||
int blockY,
|
||||
int blockZ
|
||||
@@ -708,7 +979,8 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
^ (((long) blockX) * 341873128712L)
|
||||
^ (((long) blockY) * 132897987541L)
|
||||
^ (((long) blockZ) * 42317861L));
|
||||
return new BiomeResolution(irisBiome, underground, blockX, blockY, blockZ, noiseRng);
|
||||
return new BiomeResolution(
|
||||
irisBiome, underground, dimension, blockX, blockY, blockZ, noiseRng);
|
||||
}
|
||||
|
||||
private Holder<Biome> getFallbackBiome() {
|
||||
@@ -734,15 +1006,20 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
return ((long) x << 32) ^ ((long) z & 4294967295L);
|
||||
}
|
||||
|
||||
private static Holder<Biome> resolveCustomBiomeHolder(Registry<Biome> customRegistry, Engine engine, String customBiomeId) {
|
||||
if (customRegistry == null || engine == null || customBiomeId == null || customBiomeId.isBlank()) {
|
||||
private static Holder<Biome> resolveCustomBiomeHolder(
|
||||
Registry<Biome> customRegistry,
|
||||
IrisDimension dimension,
|
||||
String customBiomeId
|
||||
) {
|
||||
if (customRegistry == null || dimension == null
|
||||
|| customBiomeId == null || customBiomeId.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Identifier resourceLocation = Identifier.fromNamespaceAndPath(
|
||||
engine.getDimension().getLoadKey().toLowerCase(java.util.Locale.ROOT),
|
||||
customBiomeId.toLowerCase(java.util.Locale.ROOT)
|
||||
);
|
||||
Identifier resourceLocation = Identifier.tryParse(customBiomeKey(dimension, customBiomeId));
|
||||
if (resourceLocation == null) {
|
||||
return null;
|
||||
}
|
||||
Biome biome = customRegistry.getValue(resourceLocation);
|
||||
if (biome == null) {
|
||||
return null;
|
||||
@@ -761,6 +1038,63 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
return optionalReferenceHolder.get();
|
||||
}
|
||||
|
||||
static String customBiomeKey(IrisDimension dimension, String customBiomeId) {
|
||||
return dimension.getCustomBiomeKey(customBiomeId);
|
||||
}
|
||||
|
||||
private static List<OwnedBiome> getOwnedBiomes(Engine engine) {
|
||||
ArrayList<OwnedBiome> biomes = new ArrayList<>(getHostOwnedBiomes(engine));
|
||||
DimensionStackContext stackContext = engine.getDimensionStackContext();
|
||||
if (stackContext == null) {
|
||||
return biomes;
|
||||
}
|
||||
for (DimensionTerrainContext terrainContext : stackContext.getLayersBottomToTop()) {
|
||||
if (terrainContext.isSelfReferencing()) {
|
||||
continue;
|
||||
}
|
||||
IrisDimension dimension = terrainContext.getDimension();
|
||||
addOwnedBiomes(
|
||||
biomes,
|
||||
dimension,
|
||||
dimension.getReachableBiomes(terrainContext)
|
||||
);
|
||||
}
|
||||
return biomes;
|
||||
}
|
||||
|
||||
private static List<OwnedBiome> getHostOwnedBiomes(Engine engine) {
|
||||
IrisDimension hostDimension = engine.getDimension();
|
||||
ArrayList<OwnedBiome> biomes = new ArrayList<>();
|
||||
addOwnedBiomes(biomes, hostDimension, hostDimension.getReachableBiomes(engine));
|
||||
return biomes;
|
||||
}
|
||||
|
||||
private static void addOwnedBiomes(
|
||||
List<OwnedBiome> target,
|
||||
IrisDimension dimension,
|
||||
Iterable<IrisBiome> biomes
|
||||
) {
|
||||
for (IrisBiome biome : biomes) {
|
||||
if (biome != null) {
|
||||
target.add(new OwnedBiome(dimension, biome));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static DimensionStackLayout.Layer resolveDimensionStackLayer(
|
||||
Engine engine,
|
||||
int blockX,
|
||||
int internalY,
|
||||
int blockZ
|
||||
) {
|
||||
DimensionStackContext stackContext = engine.getDimensionStackContext();
|
||||
return stackContext == null ? null : stackContext.getLayerAt(blockX, internalY, blockZ);
|
||||
}
|
||||
|
||||
private static IrisBiome resolveSurfaceStructureBiome(Engine engine, int blockX, int blockZ) {
|
||||
return engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
}
|
||||
|
||||
private static Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String biomeKey) {
|
||||
if (registry == null || biomeKey == null || biomeKey.isBlank()) {
|
||||
return null;
|
||||
@@ -809,18 +1143,31 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
private static final class BiomeResolution {
|
||||
private final IrisBiome irisBiome;
|
||||
private final boolean underground;
|
||||
private final IrisDimension dimension;
|
||||
private final int blockX;
|
||||
private final int blockY;
|
||||
private final int blockZ;
|
||||
private final RNG rng;
|
||||
|
||||
private BiomeResolution(IrisBiome irisBiome, boolean underground, int blockX, int blockY, int blockZ, RNG rng) {
|
||||
private BiomeResolution(
|
||||
IrisBiome irisBiome,
|
||||
boolean underground,
|
||||
IrisDimension dimension,
|
||||
int blockX,
|
||||
int blockY,
|
||||
int blockZ,
|
||||
RNG rng
|
||||
) {
|
||||
this.irisBiome = irisBiome;
|
||||
this.underground = underground;
|
||||
this.dimension = dimension;
|
||||
this.blockX = blockX;
|
||||
this.blockY = blockY;
|
||||
this.blockZ = blockZ;
|
||||
this.rng = rng;
|
||||
}
|
||||
}
|
||||
|
||||
private record OwnedBiome(IrisDimension dimension, IrisBiome biome) {
|
||||
}
|
||||
}
|
||||
|
||||
+76
-14
@@ -1,5 +1,7 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.engine.DimensionStackContext;
|
||||
import art.arcane.iris.engine.DimensionStackLayout;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.NativeFeatureGenerationPolicy;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
@@ -40,6 +42,9 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* Bukkit twin of the modded imported-feature stage. Same control, same semantics, same seeds: with
|
||||
@@ -198,8 +203,7 @@ final class ImportedFeatureStage {
|
||||
*/
|
||||
private Set<String> visibleBiomeKeys() {
|
||||
Set<String> keys = new LinkedHashSet<>();
|
||||
String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
|
||||
for (IrisBiome irisBiome : engine.getAllBiomes()) {
|
||||
for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) {
|
||||
addKey(keys, irisBiome.getStructureDerivativeKey());
|
||||
addKey(keys, irisBiome.getDerivativeKey());
|
||||
addKey(keys, irisBiome.getVanillaDerivativeKey());
|
||||
@@ -213,7 +217,7 @@ final class ImportedFeatureStage {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
addKey(keys, namespace + ":" + customBiome.getId());
|
||||
addKey(keys, engine.getDimension().getCustomBiomeKey(customBiome.getId()));
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
@@ -226,8 +230,7 @@ final class ImportedFeatureStage {
|
||||
private Map<String, Holder<Biome>> customBiomeDerivatives(Registry<Biome> registry,
|
||||
Map<String, Holder<Biome>> byKey) {
|
||||
Map<String, Holder<Biome>> derivatives = new HashMap<>();
|
||||
String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
|
||||
for (IrisBiome irisBiome : engine.getAllBiomes()) {
|
||||
for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) {
|
||||
if (!irisBiome.isCustom()) {
|
||||
continue;
|
||||
}
|
||||
@@ -246,7 +249,7 @@ final class ImportedFeatureStage {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
derivatives.put(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT), derivative);
|
||||
derivatives.put(engine.getDimension().getCustomBiomeKey(customBiome.getId()), derivative);
|
||||
}
|
||||
}
|
||||
return derivatives;
|
||||
@@ -288,14 +291,24 @@ final class ImportedFeatureStage {
|
||||
List<FeatureSorter.StepFeatureData> steps = table.steps();
|
||||
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
|
||||
long decorationSeed = random.setDecorationSeed(level.getSeed(), origin.getX(), origin.getZ());
|
||||
Set<Holder<Biome>> chunkBiomes = chunkBiomes(level, sectionPos, table);
|
||||
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
|
||||
int staticMinY = engine.getMinHeight();
|
||||
WorldGenLevel placementLevel = staticObjects.isEmpty() ? level : NativeStructureWorldgenAccess.create(
|
||||
DimensionStackContext stackContext = engine.getDimensionStackContext();
|
||||
Set<Holder<Biome>> chunkBiomes = chunkBiomes(level, sectionPos, table, stackContext);
|
||||
IntBinaryOperator surfaceFirstFreeY = stackContext == null
|
||||
? (x, z) -> level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z)
|
||||
: (x, z) -> Engine.hostHeight(engine, x, z, false) + staticMinY + 1;
|
||||
IntBinaryOperator floorFirstFreeY = stackContext == null
|
||||
? (x, z) -> level.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z)
|
||||
: (x, z) -> Engine.hostHeight(engine, x, z, true) + staticMinY + 1;
|
||||
WorldGenLevel placementLevel = staticObjects.isEmpty() && stackContext == null
|
||||
? level
|
||||
: NativeStructureWorldgenAccess.create(
|
||||
level, centerPos,
|
||||
(x, z) -> level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z),
|
||||
(x, z) -> level.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z),
|
||||
position -> staticObjects.contains(position.getX(), position.getY() - staticMinY, position.getZ()));
|
||||
surfaceFirstFreeY,
|
||||
floorFirstFreeY,
|
||||
stackContext != null,
|
||||
importedFeatureProtection(staticObjects, stackContext, staticMinY));
|
||||
|
||||
try {
|
||||
for (int stepIndex = 0; stepIndex < steps.size(); stepIndex++) {
|
||||
@@ -313,6 +326,30 @@ final class ImportedFeatureStage {
|
||||
}
|
||||
}
|
||||
|
||||
private static Predicate<BlockPos> importedFeatureProtection(
|
||||
IrisStaticObjectLayer staticObjects,
|
||||
DimensionStackContext stackContext,
|
||||
int minimumY
|
||||
) {
|
||||
Map<Long, DimensionStackLayout> layouts = new ConcurrentHashMap<>();
|
||||
return position -> {
|
||||
if (!staticObjects.isEmpty() && staticObjects.contains(
|
||||
position.getX(), position.getY() - minimumY, position.getZ())) {
|
||||
return true;
|
||||
}
|
||||
if (stackContext == null) {
|
||||
return false;
|
||||
}
|
||||
long columnKey = ((long) position.getX() << 32)
|
||||
^ (position.getZ() & 0xFFFFFFFFL);
|
||||
DimensionStackLayout layout = layouts.computeIfAbsent(
|
||||
columnKey,
|
||||
ignored -> stackContext.sample(position.getX(), position.getZ())
|
||||
);
|
||||
return layout.isHostFeatureProtectedY(position.getY() - minimumY);
|
||||
};
|
||||
}
|
||||
|
||||
private void placeStep(WorldGenLevel level, FeatureTable table, FeatureSorter.StepFeatureData stepData,
|
||||
Registry<PlacedFeature> featureRegistry, Set<Holder<Biome>> chunkBiomes,
|
||||
ChunkGenerator owner, WorldgenRandom random, long decorationSeed,
|
||||
@@ -353,12 +390,37 @@ final class ImportedFeatureStage {
|
||||
return id == null ? feature.toString() : id.toString();
|
||||
}
|
||||
|
||||
private Set<Holder<Biome>> chunkBiomes(WorldGenLevel level, SectionPos sectionPos, FeatureTable table) {
|
||||
private Set<Holder<Biome>> chunkBiomes(
|
||||
WorldGenLevel level,
|
||||
SectionPos sectionPos,
|
||||
FeatureTable table,
|
||||
DimensionStackContext stackContext
|
||||
) {
|
||||
List<Holder<Biome>> collected = new ArrayList<>();
|
||||
ChunkPos.rangeClosed(sectionPos.chunk(), 1).forEach((ChunkPos chunkPos) -> {
|
||||
ChunkAccess neighbour = level.getChunk(chunkPos.x(), chunkPos.z());
|
||||
for (LevelChunkSection section : neighbour.getSections()) {
|
||||
section.getBiomes().getAll(collected::add);
|
||||
LevelChunkSection[] sections = neighbour.getSections();
|
||||
for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
|
||||
LevelChunkSection section = sections[sectionIndex];
|
||||
if (stackContext == null) {
|
||||
section.getBiomes().getAll(collected::add);
|
||||
continue;
|
||||
}
|
||||
int sectionMinimumY = neighbour.getSectionYFromSectionIndex(sectionIndex) << 4;
|
||||
for (int quartY = 0; quartY < 4; quartY++) {
|
||||
int internalY = sectionMinimumY + (quartY << 2) - level.getMinY();
|
||||
for (int quartX = 0; quartX < 4; quartX++) {
|
||||
int blockX = chunkPos.getMinBlockX() + (quartX << 2);
|
||||
for (int quartZ = 0; quartZ < 4; quartZ++) {
|
||||
int blockZ = chunkPos.getMinBlockZ() + (quartZ << 2);
|
||||
DimensionStackLayout.Layer layer = stackContext.getLayerAt(
|
||||
blockX, internalY, blockZ);
|
||||
if (layer.terrainContext().isSelfReferencing()) {
|
||||
collected.add(section.getBiomes().get(quartX, quartY, quartZ));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Set<Holder<Biome>> present = new LinkedHashSet<>();
|
||||
|
||||
+76
-14
@@ -1,5 +1,7 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.engine.DimensionStackContext;
|
||||
import art.arcane.iris.engine.DimensionStackLayout;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||
@@ -115,6 +117,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
private static final WrappedField<ChunkGenerator, BiomeSource> BIOME_SOURCE;
|
||||
@@ -429,7 +432,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
undergroundStep,
|
||||
decision.preserveSourceY(),
|
||||
decision.yBand(),
|
||||
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight());
|
||||
(x, z) -> Engine.hostHeight(engine, x, z, true) + engine.getMinHeight());
|
||||
StructureStart wrapped = NativeStructureReferenceEnvelope.wrapForPublication(
|
||||
start, structure, start.getReferences(),
|
||||
NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain()),
|
||||
@@ -726,6 +729,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
return (x, z) -> engine.getHeight(x, z, true) + runtimeMinY + 1;
|
||||
}
|
||||
|
||||
private IntBinaryOperator hostWorldgenSurfaceHeight() {
|
||||
return (x, z) -> Engine.hostHeight(engine, x, z, false) + runtimeMinY + 1;
|
||||
}
|
||||
|
||||
private IntBinaryOperator hostWorldgenFloorHeight() {
|
||||
return (x, z) -> Engine.hostHeight(engine, x, z, true) + runtimeMinY + 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(Holder<Biome> holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) {
|
||||
Holder<Biome> vanillaSpawnBiome = customBiomeSource.getVanillaSpawnBiome(holder);
|
||||
@@ -868,17 +879,22 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
}
|
||||
try {
|
||||
WorldgenTerrainHeightmaps.primeStructurePlacement(
|
||||
world, chunkPos, heightmapStarts, worldgenSurfaceHeight(), worldgenFloorHeight());
|
||||
world, chunkPos, heightmapStarts,
|
||||
hostWorldgenSurfaceHeight(), hostWorldgenFloorHeight());
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"heightmap priming", nativeStructureBatchContext(placementGroups),
|
||||
chunkPos.x(), chunkPos.z(), error);
|
||||
}
|
||||
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
|
||||
int staticMinY = engine.getMinHeight();
|
||||
WorldGenLevel boundedWorld = staticObjects.isEmpty() ? world : NativeStructureWorldgenAccess.create(
|
||||
world, chunkPos, worldgenSurfaceHeight(), worldgenFloorHeight(),
|
||||
position -> staticObjects.contains(position.getX(), position.getY() - staticMinY, position.getZ()));
|
||||
Predicate<BlockPos> protectedPosition = nativeStructureProtection(staticObjects);
|
||||
WorldGenLevel boundedWorld = staticObjects.isEmpty()
|
||||
&& engine.getDimensionStackContext() == null
|
||||
? world
|
||||
: NativeStructureWorldgenAccess.create(
|
||||
world, chunkPos, hostWorldgenSurfaceHeight(), hostWorldgenFloorHeight(),
|
||||
engine.getDimensionStackContext() != null,
|
||||
protectedPosition);
|
||||
try {
|
||||
NativeStructureVegetationClearer.clearIntersectingVegetation(
|
||||
boundedWorld, chunk, area, vegetationTargets);
|
||||
@@ -891,7 +907,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
try {
|
||||
vacuumFoundationPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
|
||||
boundedWorld, area, terrainTargets,
|
||||
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight());
|
||||
(x, z) -> Engine.hostHeight(engine, x, z, true) + engine.getMinHeight());
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"terrain integration", nativeStructureBatchContext(placementGroups),
|
||||
@@ -945,17 +961,45 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
BoundingBox area, ChunkPos chunkPos, String structureId, StructureStart start,
|
||||
IrisNativeStructureDecision decision) {
|
||||
WorldGenLevel boundedWorld = world instanceof NativeStructureWorldgenAccess ? world : NativeStructureWorldgenAccess.create(
|
||||
world, chunkPos, worldgenSurfaceHeight(), worldgenFloorHeight(), position -> false);
|
||||
world,
|
||||
chunkPos,
|
||||
hostWorldgenSurfaceHeight(),
|
||||
hostWorldgenFloorHeight(),
|
||||
engine.getDimensionStackContext() != null,
|
||||
nativeStructureProtection(
|
||||
engine.getDimension().getStaticObjectLayer(engine.getData())));
|
||||
world.setCurrentlyGenerating(() -> "Iris native structure " + structureId);
|
||||
try {
|
||||
NativeStructurePostProcessor.place(boundedWorld, structureManager, this, random, area, chunkPos,
|
||||
structureId, start, decision, this::resolvePaletteBlock,
|
||||
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight());
|
||||
(x, z) -> Engine.hostHeight(engine, x, z, true) + engine.getMinHeight());
|
||||
} finally {
|
||||
world.setCurrentlyGenerating(null);
|
||||
}
|
||||
}
|
||||
|
||||
private Predicate<BlockPos> nativeStructureProtection(IrisStaticObjectLayer staticObjects) {
|
||||
DimensionStackContext stackContext = engine.getDimensionStackContext();
|
||||
int minimumY = engine.getMinHeight();
|
||||
Map<Long, DimensionStackLayout> layouts = new ConcurrentHashMap<>();
|
||||
return position -> {
|
||||
if (!staticObjects.isEmpty() && staticObjects.contains(
|
||||
position.getX(), position.getY() - minimumY, position.getZ())) {
|
||||
return true;
|
||||
}
|
||||
if (stackContext == null) {
|
||||
return false;
|
||||
}
|
||||
long columnKey = ((long) position.getX() << 32)
|
||||
^ (position.getZ() & 0xFFFFFFFFL);
|
||||
DimensionStackLayout layout = layouts.computeIfAbsent(
|
||||
columnKey,
|
||||
ignored -> stackContext.sample(position.getX(), position.getZ())
|
||||
);
|
||||
return layout.isHostFeatureProtectedY(position.getY() - minimumY);
|
||||
};
|
||||
}
|
||||
|
||||
private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
|
||||
StructureStepCache cached = structureStepCache;
|
||||
if (cached != null && cached.registry() == registry) {
|
||||
@@ -1041,7 +1085,17 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
@Override
|
||||
public void spawnOriginalMobs(WorldGenRegion region) {
|
||||
ChunkPos center = region.getCenter();
|
||||
Holder<Biome> visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
|
||||
Holder<Biome> visibleBiome;
|
||||
if (engine.getDimensionStackContext() == null) {
|
||||
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
|
||||
} else {
|
||||
visibleBiome = customBiomeSource.getVisibleSurfaceBiome(
|
||||
center.getMinBlockX() + 8,
|
||||
center.getMinBlockZ() + 8);
|
||||
if (visibleBiome == null) {
|
||||
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
|
||||
}
|
||||
}
|
||||
Holder<Biome> vanillaBiome = customBiomeSource.getVanillaSpawnBiome(visibleBiome);
|
||||
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
|
||||
random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ());
|
||||
@@ -1081,9 +1135,12 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
public int getBaseHeight(int i, int j, Heightmap.Types heightmap_type, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
|
||||
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_height");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
boolean ignoreFluid = !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState());
|
||||
int height = engine.getDimensionStackContext() == null
|
||||
? engine.getHeight(i, j, ignoreFluid)
|
||||
: Engine.hostHeight(engine, i, j, ignoreFluid);
|
||||
return levelheightaccessor.getMinY()
|
||||
+ engine.getHeight(i, j, !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState()))
|
||||
+ 1;
|
||||
+ height + 1;
|
||||
} catch (GenerationSessionException e) {
|
||||
throw new IllegalStateException("Iris base height query could not acquire its engine runtime.", e);
|
||||
}
|
||||
@@ -1093,8 +1150,13 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
public NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
|
||||
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_column");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
int block = engine.getHeight(i, j, true);
|
||||
int water = engine.getHeight(i, j, false);
|
||||
boolean dimensionStack = engine.getDimensionStackContext() != null;
|
||||
int block = dimensionStack
|
||||
? Engine.hostHeight(engine, i, j, true)
|
||||
: engine.getHeight(i, j, true);
|
||||
int water = dimensionStack
|
||||
? Engine.hostHeight(engine, i, j, false)
|
||||
: engine.getHeight(i, j, false);
|
||||
BlockState[] column = new BlockState[levelheightaccessor.getHeight()];
|
||||
for (int k = 0; k < column.length; k++) {
|
||||
if (k <= block) {
|
||||
|
||||
+5
@@ -824,6 +824,11 @@ public class NMSBinding implements INMSBinding {
|
||||
return NativeStructureVolumeIndex.volumes(engine, minX, minZ, maxX, maxZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateNativeStructureVolumeIndex(Engine engine) {
|
||||
NativeStructureVolumeIndex.invalidate(engine);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBiomeId(Biome biome) {
|
||||
for (World i : Bukkit.getWorlds()) {
|
||||
|
||||
+6
-2
@@ -64,6 +64,7 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
|
||||
private final ChunkPos generationCenter;
|
||||
private final IntBinaryOperator surfaceFirstFreeY;
|
||||
private final IntBinaryOperator floorFirstFreeY;
|
||||
private final boolean virtualizeHeights;
|
||||
private final Predicate<BlockPos> protectedPosition;
|
||||
private final Holder<Biome> fallbackBiome;
|
||||
private final BiomeManager biomeManager;
|
||||
@@ -81,6 +82,7 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
|
||||
boundary.surfaceFirstFreeY(), "Native structure world access requires a surface resolver");
|
||||
this.floorFirstFreeY = Objects.requireNonNull(
|
||||
boundary.floorFirstFreeY(), "Native structure world access requires an ocean-floor resolver");
|
||||
this.virtualizeHeights = boundary.virtualizeHeights();
|
||||
this.protectedPosition = Objects.requireNonNull(
|
||||
boundary.protectedPosition(), "World access requires a protected-position predicate");
|
||||
BlockPos biomeSample = generationCenter.getMiddleBlockPosition(delegate.getSeaLevel());
|
||||
@@ -103,9 +105,10 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
|
||||
static NativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter,
|
||||
IntBinaryOperator surfaceFirstFreeY,
|
||||
IntBinaryOperator floorFirstFreeY,
|
||||
boolean virtualizeHeights,
|
||||
Predicate<BlockPos> protectedPosition) {
|
||||
return new NativeStructureWorldgenAccess(delegate, new Boundary(
|
||||
generationCenter, surfaceFirstFreeY, floorFirstFreeY, protectedPosition));
|
||||
generationCenter, surfaceFirstFreeY, floorFirstFreeY, virtualizeHeights, protectedPosition));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -250,7 +253,7 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
|
||||
|
||||
@Override
|
||||
public int getHeight(Heightmap.Types type, int x, int z) {
|
||||
if (isInsideGenerationRegion(x >> 4, z >> 4)) {
|
||||
if (!virtualizeHeights && isInsideGenerationRegion(x >> 4, z >> 4)) {
|
||||
return delegate.getHeight(type, x, z);
|
||||
}
|
||||
return height(type, x, z);
|
||||
@@ -549,6 +552,7 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
|
||||
private record Boundary(ChunkPos generationCenter,
|
||||
IntBinaryOperator surfaceFirstFreeY,
|
||||
IntBinaryOperator floorFirstFreeY,
|
||||
boolean virtualizeHeights,
|
||||
Predicate<BlockPos> protectedPosition) {
|
||||
}
|
||||
|
||||
|
||||
+176
-1
@@ -1,15 +1,29 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class CustomBiomeSourceStructureContractTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativeStructuresUseTerrainSafeDerivativeAtEveryBiomeBoundary() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
|
||||
@@ -69,14 +83,53 @@ public class CustomBiomeSourceStructureContractTest {
|
||||
String realResolution = source.substring(realResolutionStart, naturalResolutionStart);
|
||||
String naturalResolution = source.substring(naturalResolutionStart, resolutionEnd);
|
||||
|
||||
assertTrue(realResolution.contains("engine.getComplex().getTrueBiomeStream().get(blockX, blockZ)"));
|
||||
assertTrue(realResolution.contains("resolveSurfaceStructureBiome(engine, blockX, blockZ)"));
|
||||
assertFalse(realResolution.contains("getNaturalTrueBiomeStream()"));
|
||||
assertTrue(naturalResolution.contains(
|
||||
"engine.getComplex().getNaturalTrueBiomeStream().get(blockX, blockZ)"));
|
||||
assertFalse(naturalResolution.contains("getTrueBiomeStream()"));
|
||||
int helperStart = source.indexOf("private static IrisBiome resolveSurfaceStructureBiome(");
|
||||
int helperEnd = source.indexOf("private static Holder<Biome> resolveBiomeHolder(", helperStart);
|
||||
String helper = source.substring(helperStart, helperEnd);
|
||||
assertTrue(helper.contains("engine.getComplex().getTrueBiomeStream().get(blockX, blockZ)"));
|
||||
assertFalse(helper.contains("getDimensionStackContext()"));
|
||||
assertFalse(helper.contains("getSurfaceBiome("));
|
||||
assertFalse(source.contains("studioBootstrapSurfaceStructureBiomeCache"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stackedCustomBiomesUseTheirOwningDimensionNamespace() {
|
||||
IrisDimension host = new IrisDimension();
|
||||
host.setLoadKey("Host");
|
||||
IrisDimension upper = new IrisDimension();
|
||||
upper.setLoadKey("Layers/Upper");
|
||||
|
||||
assertEquals("layers:upper/aurora", CustomBiomeSource.customBiomeKey(upper, "Aurora"));
|
||||
assertNotEquals(
|
||||
CustomBiomeSource.customBiomeKey(host, "Aurora"),
|
||||
CustomBiomeSource.customBiomeKey(upper, "Aurora")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void visibleBiomeResolutionSelectsStackOwnershipBeforeHostCaves() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
|
||||
int resolutionStart = source.indexOf("private BiomeResolution resolveBiomeResolution(");
|
||||
int resolutionEnd = source.indexOf("private BiomeResolution createBiomeResolution(", resolutionStart);
|
||||
|
||||
assertTrue(resolutionStart >= 0);
|
||||
assertTrue(resolutionEnd > resolutionStart);
|
||||
String resolution = source.substring(resolutionStart, resolutionEnd);
|
||||
|
||||
assertTrue(resolution.contains("resolveDimensionStackLayer("));
|
||||
assertTrue(resolution.contains("!stackLayer.terrainContext().isSelfReferencing()"));
|
||||
assertTrue(resolution.indexOf("resolveDimensionStackLayer(")
|
||||
< resolution.indexOf("boolean deepUnderground"));
|
||||
assertTrue(resolution.contains("includeDimensionStack"));
|
||||
assertTrue(source.contains("resolveBiomeResolution(x, y, z, null, false)"));
|
||||
assertTrue(source.contains("resolution.dimension, customBiome.getId()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void visibleBiomeBatchReusesTheCallersGenerationLease() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
|
||||
@@ -94,4 +147,126 @@ public class CustomBiomeSourceStructureContractTest {
|
||||
< lookup.indexOf("getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler)"));
|
||||
assertFalse(source.contains("studioBootstrapNoiseBiomeCache"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stackBiomesAreVisibleButExcludedFromStructureReachability() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
|
||||
|
||||
assertTrue(source.contains("return possibleBiomes(true).stream()"));
|
||||
assertTrue(source.contains("public Set<Holder<Biome>> possibleBiomes()"));
|
||||
assertTrue(source.contains("irisBiome.getDerivativeKey()"));
|
||||
assertTrue(source.contains("irisBiome.getBiomeScatter()"));
|
||||
assertTrue(source.contains("irisBiome.getBiomeSkyScatter()"));
|
||||
assertTrue(source.contains("return possibleBiomes(false)"));
|
||||
assertTrue(source.contains("for (OwnedBiome ownedBiome : getHostOwnedBiomes(engine))"));
|
||||
assertTrue(source.contains("for (OwnedBiome ownedBiome : getOwnedBiomes(engine))"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void biomeLocatorSearchesTheVisibleStack() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
|
||||
int start = source.indexOf("public Pair<BlockPos, Holder<Biome>> findClosestBiome3d(");
|
||||
int end = source.indexOf("static int horizontalBiomeSearchQuartStep", start);
|
||||
String method = source.substring(start, end);
|
||||
|
||||
assertTrue(method.contains("bukkit_locate_visible_biome"));
|
||||
assertTrue(method.contains("return super.findClosestBiome3d("));
|
||||
assertTrue(method.contains("getVisibleBiomes("));
|
||||
assertTrue(method.contains("QuartPos.toBlock(quartX), QuartPos.toBlock(quartZ)"));
|
||||
assertTrue(method.contains("resolveVisibleBiomeHolder("));
|
||||
assertFalse(method.contains("getStructureNoiseBiomeWithActiveGenerationLease("));
|
||||
assertTrue(method.indexOf("tryAcquireGenerationLease(\"bukkit_locate_visible_biome\")")
|
||||
< method.indexOf("engine.getDimensionStackContext()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void surfaceSpawnBiomeUsesTheVisibleQuartCell() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
|
||||
int start = source.indexOf("Holder<Biome> getVisibleSurfaceBiome(");
|
||||
int end = source.indexOf("private RegistryAccess registry()", start);
|
||||
String method = source.substring(start, end);
|
||||
|
||||
assertTrue(method.contains("QuartPos.fromBlock(blockX)"));
|
||||
assertTrue(method.contains("QuartPos.toBlock(quartX)"));
|
||||
assertTrue(method.contains("QuartPos.fromBlock(internalY + engine.getWorld().minHeight())"));
|
||||
assertTrue(method.contains("stackContext.getLayout(sampleX, sampleZ).surfaceLayer()"));
|
||||
assertTrue(method.contains("getVisibleNoiseBiomeWithActiveGenerationLease("));
|
||||
assertTrue(method.contains("quartX, quartY, quartZ, null, null, cacheable"));
|
||||
assertTrue(method.indexOf("boolean cacheable = isBiomeCacheable(engine, sampleX, sampleZ)")
|
||||
< method.indexOf("Engine.hostHeight(engine, sampleX, sampleZ, true)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void temporaryNaturalBiomeAnswersAreNotMemoized() throws IOException {
|
||||
Engine temporary = engineAnsweringNaturalFallback(true, 12, -8);
|
||||
Engine stable = engineAnsweringNaturalFallback(false, 12, -8);
|
||||
assertFalse(CustomBiomeSource.isBiomeCacheable(temporary, 12, -8));
|
||||
assertTrue(CustomBiomeSource.isBiomeCacheable(stable, 12, -8));
|
||||
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
|
||||
int structureStart = source.indexOf(
|
||||
"private Holder<Biome> getStructureNoiseBiomeWithActiveGenerationLease(");
|
||||
int structureEnd = source.indexOf("@Override\n public Pair<BlockPos, Holder<Biome>> findBiomeHorizontal(",
|
||||
structureStart);
|
||||
assertCacheGuarded(source.substring(structureStart, structureEnd), "structureBiomeCache");
|
||||
|
||||
int firstVisible = source.indexOf(
|
||||
"Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(");
|
||||
int visibleStart = source.indexOf(
|
||||
"Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(", firstVisible + 1);
|
||||
int visibleEnd = source.indexOf("private boolean isBiomeCacheable(", visibleStart);
|
||||
assertCacheGuarded(source.substring(visibleStart, visibleEnd), "noiseBiomeCache");
|
||||
|
||||
String predicate = source.substring(
|
||||
visibleEnd,
|
||||
source.indexOf("private GenerationSessionLease tryAcquireGenerationLease(", visibleEnd)
|
||||
);
|
||||
assertTrue(predicate.contains("QuartPos.toBlock(quartX)"));
|
||||
assertTrue(predicate.contains("QuartPos.toBlock(quartZ)"));
|
||||
assertTrue(predicate.contains("return !engine.answersFromNaturalTerrain(blockX, blockZ)"));
|
||||
assertFalse(predicate.contains("getDimensionStackContext()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void importedFeaturesCollectOnlyHostOwnedStackCells() throws IOException {
|
||||
Path biomeSource = Path.of(System.getProperty("iris.customBiomeSource"));
|
||||
String source = Files.readString(biomeSource.getParent().resolve("ImportedFeatureStage.java"));
|
||||
|
||||
assertTrue(source.contains("stackContext.getLayerAt("));
|
||||
assertTrue(source.contains("layer.terrainContext().isSelfReferencing()"));
|
||||
assertTrue(source.contains("section.getBiomes().get(quartX, quartY, quartZ)"));
|
||||
}
|
||||
|
||||
private static void assertCacheGuarded(String method, String cacheName) {
|
||||
int predicate = method.indexOf("boolean cacheable = isBiomeCacheable(");
|
||||
int read = method.indexOf(cacheName + ".get(", predicate);
|
||||
int readGuard = method.lastIndexOf("if (cacheable)", read);
|
||||
int bypass = method.indexOf("if (!cacheable)", read);
|
||||
int write = method.indexOf(cacheName + ".putIfAbsent(", bypass);
|
||||
|
||||
assertTrue(predicate >= 0);
|
||||
assertTrue(read > predicate);
|
||||
assertTrue(readGuard >= predicate);
|
||||
assertTrue(bypass > read);
|
||||
assertTrue(write > bypass);
|
||||
}
|
||||
|
||||
private static Engine engineAnsweringNaturalFallback(
|
||||
boolean naturalFallback,
|
||||
int expectedX,
|
||||
int expectedZ
|
||||
) {
|
||||
return (Engine) Proxy.newProxyInstance(
|
||||
Engine.class.getClassLoader(),
|
||||
new Class<?>[]{Engine.class},
|
||||
(proxy, method, arguments) -> {
|
||||
if (!method.getName().equals("answersFromNaturalTerrain")) {
|
||||
throw new UnsupportedOperationException(method.getName());
|
||||
}
|
||||
assertEquals(Integer.valueOf(expectedX), arguments[0]);
|
||||
assertEquals(Integer.valueOf(expectedZ), arguments[1]);
|
||||
return naturalFallback;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -78,6 +78,8 @@ public class IrisChunkGeneratorFailureContractTest {
|
||||
< placement.indexOf("for (NativePlacementGroup group"));
|
||||
assertTrue(placement.indexOf("repairVacuumFoundations")
|
||||
> placement.indexOf("for (NativePlacementGroup group"));
|
||||
assertTrue(placement.contains("nativeStructureProtection(staticObjects)"));
|
||||
assertTrue(placement.contains("Engine.hostHeight(engine, x, z, true)"));
|
||||
assertFalse(placement.contains("IrisLogging.reportError"));
|
||||
}
|
||||
|
||||
@@ -542,8 +544,8 @@ public class IrisChunkGeneratorFailureContractTest {
|
||||
String spawn = source.substring(spawnStart, spawnEnd);
|
||||
|
||||
assertTrue(spawn.contains("customBiomeSource.getVanillaSpawnBiome(visibleBiome)"));
|
||||
assertTrue(spawn.contains("customBiomeSource.getVisibleSurfaceBiome("));
|
||||
assertTrue(spawn.contains("NaturalSpawner.spawnMobsForChunkGeneration("));
|
||||
assertTrue(spawn.contains("region.getBiome(center.getWorldPosition().atY(region.getMaxY()))"));
|
||||
assertTrue(spawn.contains("new LegacyRandomSource(RandomSupport.generateUniqueSeed())"));
|
||||
assertTrue(spawn.contains("random.setDecorationSeed(region.getSeed()"));
|
||||
assertFalse(spawn.contains("delegate.spawnOriginalMobs"));
|
||||
|
||||
+24
-2
@@ -135,6 +135,24 @@ public class NativeStructureWorldgenAccessTest {
|
||||
assertEquals(1, recording.blockTicks.count());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void virtualizedHeightsHideInRegionTerrainFromTheDelegate() {
|
||||
RecordingDelegate recording = new RecordingDelegate();
|
||||
ChunkPos generationCenter = generationCenter();
|
||||
NativeStructureWorldgenAccess access = NativeStructureWorldgenAccess.create(
|
||||
recording.world(), generationCenter,
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
true,
|
||||
position -> false);
|
||||
int x = generationCenter.getMiddleBlockX();
|
||||
int z = generationCenter.getMiddleBlockZ();
|
||||
|
||||
assertEquals(SURFACE_FIRST_FREE_Y, access.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z));
|
||||
assertEquals(FLOOR_FIRST_FREE_Y, access.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z));
|
||||
assertEquals(0, recording.heightReads);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void protectedCellsRemainReadableButRejectWritesTilesAndTicks() {
|
||||
RecordingDelegate recording = new RecordingDelegate();
|
||||
@@ -142,7 +160,9 @@ public class NativeStructureWorldgenAccessTest {
|
||||
NativeStructureWorldgenAccess access = NativeStructureWorldgenAccess.create(
|
||||
recording.world(), generationCenter(),
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y, position::equals);
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
false,
|
||||
position::equals);
|
||||
|
||||
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
|
||||
assertFalse(access.ensureCanWrite(position));
|
||||
@@ -211,7 +231,9 @@ public class NativeStructureWorldgenAccessTest {
|
||||
return NativeStructureWorldgenAccess.create(
|
||||
recording.world(), generationCenter(),
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y, position -> false);
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
false,
|
||||
position -> false);
|
||||
}
|
||||
|
||||
private static ChunkPos generationCenter() {
|
||||
|
||||
+12
@@ -332,6 +332,18 @@ public class NativeStructureFactoryTest {
|
||||
assertTrue(failureStart < source.indexOf("NativeStructureOwnershipStore.record("));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void manualNativeStructureGenerationUsesTheRootTerrainHeight() throws Exception {
|
||||
String source = Files.readString(
|
||||
Path.of(System.getProperty("iris.nativeStructureStartInjectorSource")),
|
||||
StandardCharsets.UTF_8);
|
||||
|
||||
assertTrue(source.contains(
|
||||
"Engine.hostHeight(context.engine(), x, z, true)"));
|
||||
assertFalse(source.contains(
|
||||
"context.engine().getHeight(x, z, true)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedManualStartPublicationClearsRecordedOwnershipAndPreservesTheFailure() throws Exception {
|
||||
String source = Files.readString(
|
||||
|
||||
+5
@@ -84,6 +84,11 @@ public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
|
||||
"iris");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareRuntimeHotload(Engine engine) {
|
||||
INMS.get().invalidateNativeStructureVolumeIndex(engine);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyWorldBoundary(Engine engine) {
|
||||
IrisDimension expectedDimension = engine.getDimension();
|
||||
|
||||
+1
-1
@@ -163,7 +163,7 @@ public final class NativeStructureOwnershipRecovery {
|
||||
level,
|
||||
biome -> true,
|
||||
generator.getSeaLevel(),
|
||||
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight()
|
||||
(x, z) -> Engine.hostHeight(engine, x, z, true) + engine.getMinHeight()
|
||||
);
|
||||
return NativeStructureFactory.generate(generationContext, holder, plan, references);
|
||||
}
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ public final class NativeStructureStartInjector {
|
||||
chunk,
|
||||
biome -> true,
|
||||
context.generator().getSeaLevel(),
|
||||
(x, z) -> context.engine().getHeight(x, z, true)
|
||||
(x, z) -> Engine.hostHeight(context.engine(), x, z, true)
|
||||
+ context.engine().getMinHeight()
|
||||
);
|
||||
StructureStart generated = NativeStructureFactory.generate(
|
||||
|
||||
+19
-1
@@ -89,6 +89,18 @@ public final class NativeStructureVolumeIndex {
|
||||
}
|
||||
}
|
||||
|
||||
public static void invalidate(Engine engine) {
|
||||
if (engine == null) {
|
||||
return;
|
||||
}
|
||||
synchronized (INDEXES) {
|
||||
NativeStructureVolumeIndex current = INDEXES.get(engine);
|
||||
if (current != null) {
|
||||
INDEXES.put(engine, current.fresh());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static KList<NativeStructureVolume> volumes(Engine engine, int minX, int minZ, int maxX, int maxZ) {
|
||||
NativeStructureVolumeIndex index = engine == null ? null : INDEXES.get(engine);
|
||||
return index == null ? NativeStructureVolume.NONE : index.resolve(engine, minX, minZ, maxX, maxZ);
|
||||
@@ -102,6 +114,12 @@ public final class NativeStructureVolumeIndex {
|
||||
return ORIGIN_REACH_CHUNKS;
|
||||
}
|
||||
|
||||
private NativeStructureVolumeIndex fresh() {
|
||||
return context == null
|
||||
? new NativeStructureVolumeIndex(origins)
|
||||
: new NativeStructureVolumeIndex(context);
|
||||
}
|
||||
|
||||
KList<NativeStructureVolume> resolve(Engine engine, int minX, int minZ, int maxX, int maxZ) {
|
||||
int fromChunkX = Math.min(minX, maxX) >> 4;
|
||||
int toChunkX = Math.max(minX, maxX) >> 4;
|
||||
@@ -323,7 +341,7 @@ public final class NativeStructureVolumeIndex {
|
||||
}
|
||||
|
||||
private static IntBinaryOperator surfaceHeight(Engine engine) {
|
||||
return (x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight();
|
||||
return (x, z) -> Engine.hostHeight(engine, x, z, true) + engine.getMinHeight();
|
||||
}
|
||||
|
||||
static KList<NativeStructureVolume> appendPieces(KList<NativeStructureVolume> volumes,
|
||||
|
||||
+491
-49
@@ -18,16 +18,22 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.DimensionStackContext;
|
||||
import art.arcane.iris.engine.DimensionStackLayout;
|
||||
import art.arcane.iris.engine.DimensionTerrainContext;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.QuartPos;
|
||||
@@ -37,12 +43,15 @@ import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.level.LevelReader;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.Climate;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
@@ -196,11 +205,16 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
|
||||
private List<Holder<Biome>> collectPossibleBiomeHolders() {
|
||||
BiomeKeySets keys = biomeKeySets();
|
||||
Set<String> generatedBiomeKeys = requireConfiguredStructureBiomeKeys(keys.required());
|
||||
Set<String> visibleBiomeKeys = keys.visibleOnly();
|
||||
LinkedHashSet<String> generatedBiomeKeys = new LinkedHashSet<>(
|
||||
requireConfiguredStructureBiomeKeys(keys.structureRequired()));
|
||||
generatedBiomeKeys.addAll(keys.visibleRequired());
|
||||
LinkedHashSet<String> visibleBiomeKeys = new LinkedHashSet<>(keys.visibleOnly());
|
||||
Registry<Biome> registry = biomeRegistry();
|
||||
LinkedHashSet<Holder<Biome>> possible = new LinkedHashSet<>();
|
||||
if (registry == null) {
|
||||
if (keys.visibleFallbackPossible()) {
|
||||
visibleBiomeKeys.add("minecraft:plains");
|
||||
}
|
||||
for (Holder<Biome> biome : serializedSource.possibleBiomes()) {
|
||||
String key = holderKey(biome);
|
||||
if (isGeneratedBiomeKey(key, generatedBiomeKeys)
|
||||
@@ -215,6 +229,9 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
throw new IllegalStateException("Iris structure biomes are not registered: "
|
||||
+ missingBiomeKeys);
|
||||
}
|
||||
if (needsVisibleFallback(keys, registry)) {
|
||||
visibleBiomeKeys.add("minecraft:plains");
|
||||
}
|
||||
// Registry-ordered, never a hash-ordered walk: see possibleBiomes().
|
||||
registry.listElements().forEach((Holder.Reference<Biome> reference) -> {
|
||||
String key = holderKey(reference);
|
||||
@@ -223,7 +240,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
possible.add(reference);
|
||||
}
|
||||
});
|
||||
warnUnregisteredVisibleBiomes(registry, visibleBiomeKeys);
|
||||
warnUnregisteredVisibleBiomes(registry, keys.visibleOnly());
|
||||
}
|
||||
if (possible.isEmpty()) {
|
||||
String phase = registry == null ? "serialized biome bootstrap" : "biome registry";
|
||||
@@ -233,6 +250,36 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
return List.copyOf(possible);
|
||||
}
|
||||
|
||||
private List<Holder<Biome>> collectVisibleBiomeHolders() {
|
||||
BiomeKeySets keys = biomeKeySets();
|
||||
Set<String> visibleBiomeKeys = keys.visibleEmittable();
|
||||
Registry<Biome> registry = biomeRegistry();
|
||||
boolean includeFallback = registry == null
|
||||
? keys.visibleFallbackPossible()
|
||||
: needsVisibleFallback(keys, registry);
|
||||
ArrayList<Holder<Biome>> visible = new ArrayList<>();
|
||||
for (Holder<Biome> biome : resolvePossibleBiomes().ordered()) {
|
||||
String key = holderKey(biome);
|
||||
if (isGeneratedBiomeKey(key, visibleBiomeKeys)
|
||||
|| includeFallback && "minecraft:plains".equals(key)) {
|
||||
visible.add(biome);
|
||||
}
|
||||
}
|
||||
return List.copyOf(visible);
|
||||
}
|
||||
|
||||
private static boolean needsVisibleFallback(BiomeKeySets keys, Registry<Biome> registry) {
|
||||
if (keys.visibleFallbackPossible()) {
|
||||
return true;
|
||||
}
|
||||
for (String key : keys.visibleEmittable()) {
|
||||
if (resolveHolder(registry, key) == null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scatter and derivative keys are advisory: a typo there must not stop a world from loading the way a
|
||||
* missing structure biome does, so they are reported once and skipped.
|
||||
@@ -273,19 +320,91 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pair<BlockPos, Holder<Biome>> findClosestBiome3d(
|
||||
BlockPos origin,
|
||||
int searchRadius,
|
||||
int sampleResolutionHorizontal,
|
||||
int sampleResolutionVertical,
|
||||
Predicate<Holder<Biome>> allowed,
|
||||
Climate.Sampler sampler,
|
||||
LevelReader level
|
||||
) {
|
||||
Engine engine = engineOrNull();
|
||||
if (engine == null) {
|
||||
return serializedSource.findClosestBiome3d(
|
||||
origin, searchRadius, sampleResolutionHorizontal, sampleResolutionVertical,
|
||||
allowed, sampler, level);
|
||||
}
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_locate_visible_biome");
|
||||
if (lease == null) {
|
||||
throw new IllegalStateException("Iris visible biome search was rejected during an engine transition");
|
||||
}
|
||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!isReady(engine)) {
|
||||
throw new IllegalStateException("Iris visible biome search has no active engine runtime");
|
||||
}
|
||||
DimensionStackContext stackContext = engine.getDimensionStackContext();
|
||||
if (stackContext == null) {
|
||||
return super.findClosestBiome3d(
|
||||
origin, searchRadius, sampleResolutionHorizontal, sampleResolutionVertical,
|
||||
allowed, sampler, level);
|
||||
}
|
||||
Set<Holder<Biome>> candidates = new HashSet<>();
|
||||
for (Holder<Biome> biome : collectVisibleBiomeHolders()) {
|
||||
if (allowed.test(biome)) {
|
||||
candidates.add(biome);
|
||||
}
|
||||
}
|
||||
if (candidates.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
int sampleRadius = Math.floorDiv(searchRadius, sampleResolutionHorizontal);
|
||||
int[] sampleYs = Mth.outFromOrigin(
|
||||
origin.getY(), level.getMinY() + 1, level.getMaxY() + 1, sampleResolutionVertical).toArray();
|
||||
for (BlockPos.MutableBlockPos sampleColumn : BlockPos.spiralAround(
|
||||
BlockPos.ZERO, sampleRadius, Direction.EAST, Direction.SOUTH)) {
|
||||
int blockX = origin.getX() + sampleColumn.getX() * sampleResolutionHorizontal;
|
||||
int blockZ = origin.getZ() + sampleColumn.getZ() * sampleResolutionHorizontal;
|
||||
int quartX = QuartPos.fromBlock(blockX);
|
||||
int quartZ = QuartPos.fromBlock(blockZ);
|
||||
DimensionStackLayout stackLayout = stackContext.sample(
|
||||
QuartPos.toBlock(quartX), QuartPos.toBlock(quartZ));
|
||||
for (int blockY : sampleYs) {
|
||||
int quartY = QuartPos.fromBlock(blockY);
|
||||
Holder<Biome> visibleBiome = resolveVisibleBiome(
|
||||
engine, quartX, quartY, quartZ, sampler, stackLayout);
|
||||
if (candidates.contains(visibleBiome)) {
|
||||
return Pair.of(new BlockPos(blockX, blockY, blockZ), visibleBiome);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Holder<Biome> getNoiseBiome(Engine engine, int quartX, int quartY, int quartZ,
|
||||
Climate.Sampler sampler) {
|
||||
if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) {
|
||||
return getSurfaceStructureBiome(engine, quartX, quartZ, sampler);
|
||||
}
|
||||
boolean cacheable = isBiomeCacheable(
|
||||
engine,
|
||||
QuartPos.toBlock(quartX),
|
||||
QuartPos.toBlock(quartZ)
|
||||
);
|
||||
long key = packNoiseKey(quartX, quartY, quartZ);
|
||||
BiomeHolderTable cache = structureBiomeCache;
|
||||
Holder<Biome> cached = cache.get(key);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
if (cacheable) {
|
||||
Holder<Biome> cached = cache.get(key);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
Holder<Biome> resolved = resolveStructureBiome(engine, quartX, quartY, quartZ, sampler);
|
||||
cache.put(key, resolved);
|
||||
if (cacheable) {
|
||||
cache.put(key, resolved);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
@@ -302,14 +421,81 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
if (!isReady(engine)) {
|
||||
throw new IllegalStateException("Iris visible biome lookup has no active engine runtime");
|
||||
}
|
||||
long key = packNoiseKey(quartX, quartY, quartZ);
|
||||
BiomeHolderTable cache = visibleBiomeCache;
|
||||
return getVisibleNoiseBiomeWithActiveGenerationLease(
|
||||
engine, quartX, quartY, quartZ, sampler);
|
||||
}
|
||||
}
|
||||
|
||||
private Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(
|
||||
Engine engine,
|
||||
int quartX,
|
||||
int quartY,
|
||||
int quartZ,
|
||||
Climate.Sampler sampler
|
||||
) {
|
||||
boolean cacheable = isBiomeCacheable(
|
||||
engine,
|
||||
QuartPos.toBlock(quartX),
|
||||
QuartPos.toBlock(quartZ)
|
||||
);
|
||||
long key = packNoiseKey(quartX, quartY, quartZ);
|
||||
BiomeHolderTable cache = visibleBiomeCache;
|
||||
if (cacheable) {
|
||||
Holder<Biome> cached = cache.get(key);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
Holder<Biome> resolved = resolveVisibleBiome(engine, quartX, quartY, quartZ, sampler);
|
||||
}
|
||||
Holder<Biome> resolved = resolveVisibleBiome(engine, quartX, quartY, quartZ, sampler);
|
||||
if (cacheable) {
|
||||
cache.put(key, resolved);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
Holder<Biome> getVisibleSurfaceBiome(int blockX, int blockZ) {
|
||||
Engine engine = engineOrNull();
|
||||
if (engine == null) {
|
||||
return null;
|
||||
}
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_surface_spawn_biome");
|
||||
if (lease == null) {
|
||||
return null;
|
||||
}
|
||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!isReady(engine)) {
|
||||
return null;
|
||||
}
|
||||
Registry<Biome> registry = biomeRegistry();
|
||||
if (registry == null) {
|
||||
return null;
|
||||
}
|
||||
int quartX = QuartPos.fromBlock(blockX);
|
||||
int quartZ = QuartPos.fromBlock(blockZ);
|
||||
int sampleX = QuartPos.toBlock(quartX);
|
||||
int sampleZ = QuartPos.toBlock(quartZ);
|
||||
boolean cacheable = isBiomeCacheable(engine, sampleX, sampleZ);
|
||||
DimensionStackContext stackContext = engine.getDimensionStackContext();
|
||||
DimensionStackLayout.Layer layer = stackContext == null
|
||||
? null
|
||||
: stackContext.getLayout(sampleX, sampleZ).surfaceLayer();
|
||||
int internalY = layer == null
|
||||
? Engine.hostHeight(engine, sampleX, sampleZ, true)
|
||||
: layer.clippedSurfaceY();
|
||||
int quartY = QuartPos.fromBlock(internalY + engine.getMinHeight());
|
||||
long key = packNoiseKey(quartX, quartY, quartZ);
|
||||
BiomeHolderTable cache = visibleBiomeCache;
|
||||
if (cacheable) {
|
||||
Holder<Biome> cached = cache.get(key);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
Holder<Biome> resolved = resolveVisibleBiome(
|
||||
engine, quartX, quartY, quartZ, null);
|
||||
if (cacheable) {
|
||||
cache.put(key, resolved);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
@@ -395,7 +581,9 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
if (registry == null) {
|
||||
throw new IllegalStateException("Iris structure biome lookup has no biome registry");
|
||||
}
|
||||
IrisBiome irisBiome = engine.getComplex().getTrueBiomeStream().get(quartX << 2, quartZ << 2);
|
||||
int blockX = quartX << 2;
|
||||
int blockZ = quartZ << 2;
|
||||
IrisBiome irisBiome = resolveSurfaceStructureBiome(engine, blockX, blockZ);
|
||||
if (irisBiome == null) {
|
||||
throw new IllegalStateException("Iris returned no surface structure biome at quart "
|
||||
+ quartX + "," + quartZ);
|
||||
@@ -412,7 +600,8 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
private Holder<Biome> resolveStructureBiome(Engine engine, int quartX, int quartY, int quartZ,
|
||||
Climate.Sampler sampler) {
|
||||
Registry<Biome> registry = biomeRegistry();
|
||||
BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ);
|
||||
BiomeResolution resolution = resolveStructureBiomeResolution(
|
||||
engine, quartX, quartY, quartZ);
|
||||
if (resolution == null) {
|
||||
throw new IllegalStateException("Iris returned no structure biome at quart "
|
||||
+ quartX + "," + quartY + "," + quartZ);
|
||||
@@ -431,11 +620,36 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
|
||||
private Holder<Biome> resolveVisibleBiome(Engine engine, int quartX, int quartY, int quartZ,
|
||||
Climate.Sampler sampler) {
|
||||
return resolveVisibleBiome(engine, quartX, quartY, quartZ, sampler, null);
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveVisibleBiome(
|
||||
Engine engine,
|
||||
int quartX,
|
||||
int quartY,
|
||||
int quartZ,
|
||||
Climate.Sampler sampler,
|
||||
DimensionStackLayout stackLayout
|
||||
) {
|
||||
Registry<Biome> registry = biomeRegistry();
|
||||
BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ);
|
||||
BiomeResolution resolution = resolveBiomeResolution(
|
||||
engine, quartX, quartY, quartZ, true, stackLayout);
|
||||
if (resolution == null || registry == null) {
|
||||
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
|
||||
}
|
||||
return resolveVisibleBiome(
|
||||
engine, registry, resolution, quartX, quartY, quartZ, sampler);
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveVisibleBiome(
|
||||
Engine engine,
|
||||
Registry<Biome> registry,
|
||||
BiomeResolution resolution,
|
||||
int quartX,
|
||||
int quartY,
|
||||
int quartZ,
|
||||
Climate.Sampler sampler
|
||||
) {
|
||||
String biomeKey;
|
||||
if (resolution.irisBiome().isCustom()) {
|
||||
IrisBiomeCustom customBiome = resolution.irisBiome().getCustomBiome(
|
||||
@@ -444,7 +658,8 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
return fallbackBiome(registry, "custom derivative of '"
|
||||
+ resolution.irisBiome().getLoadKey() + "'", quartX, quartY, quartZ, sampler);
|
||||
}
|
||||
biomeKey = ModdedWorldgenIds.biomeRef(engine, customBiome.getId());
|
||||
biomeKey = customBiomeRef(
|
||||
resolution.packName(), resolution.dimension(), customBiome.getId());
|
||||
} else if (resolution.underground()) {
|
||||
biomeKey = resolution.irisBiome().getGroundBiomeKey(
|
||||
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
|
||||
@@ -459,6 +674,37 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
}
|
||||
|
||||
private BiomeResolution resolveBiomeResolution(Engine engine, int quartX, int quartY, int quartZ) {
|
||||
return resolveBiomeResolution(engine, quartX, quartY, quartZ, true);
|
||||
}
|
||||
|
||||
private BiomeResolution resolveStructureBiomeResolution(
|
||||
Engine engine,
|
||||
int quartX,
|
||||
int quartY,
|
||||
int quartZ
|
||||
) {
|
||||
return resolveBiomeResolution(engine, quartX, quartY, quartZ, false);
|
||||
}
|
||||
|
||||
private BiomeResolution resolveBiomeResolution(
|
||||
Engine engine,
|
||||
int quartX,
|
||||
int quartY,
|
||||
int quartZ,
|
||||
boolean includeDimensionStack
|
||||
) {
|
||||
return resolveBiomeResolution(
|
||||
engine, quartX, quartY, quartZ, includeDimensionStack, null);
|
||||
}
|
||||
|
||||
private BiomeResolution resolveBiomeResolution(
|
||||
Engine engine,
|
||||
int quartX,
|
||||
int quartY,
|
||||
int quartZ,
|
||||
boolean includeDimensionStack,
|
||||
DimensionStackLayout stackLayout
|
||||
) {
|
||||
if (!isReady(engine)) {
|
||||
return null;
|
||||
}
|
||||
@@ -467,27 +713,75 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
int blockZ = quartZ << 2;
|
||||
int internalY = blockY - engine.getMinHeight();
|
||||
int caveSwitchY = Math.max(-8 - engine.getMinHeight(), 40);
|
||||
DimensionStackLayout.Layer stackLayer = includeDimensionStack
|
||||
? stackLayout == null
|
||||
? resolveDimensionStackLayer(engine, blockX, internalY, blockZ)
|
||||
: stackLayout.layerAt(internalY)
|
||||
: null;
|
||||
IrisDimension owningDimension = stackLayer == null
|
||||
? engine.getDimension()
|
||||
: stackLayer.terrainContext().getDimension();
|
||||
IrisData owningData = stackLayer == null
|
||||
? engine.getData()
|
||||
: stackLayer.terrainContext().getData();
|
||||
if (stackLayer != null && !stackLayer.terrainContext().isSelfReferencing()) {
|
||||
IrisBiome stackedBiome = stackLayer.biome();
|
||||
if (stackedBiome == null) {
|
||||
return null;
|
||||
}
|
||||
return createBiomeResolution(
|
||||
engine,
|
||||
stackedBiome,
|
||||
false,
|
||||
owningDimension,
|
||||
owningData,
|
||||
blockX,
|
||||
blockY,
|
||||
blockZ
|
||||
);
|
||||
}
|
||||
boolean underground = false;
|
||||
IrisBiome irisBiome;
|
||||
if (internalY <= caveSwitchY) {
|
||||
int surfaceY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
|
||||
if (stackLayer != null) {
|
||||
int surfaceY = stackLayer.surfaceY();
|
||||
underground = isUnderground(internalY, surfaceY);
|
||||
irisBiome = underground
|
||||
? engine.getCaveBiome(
|
||||
blockX,
|
||||
internalY,
|
||||
blockZ,
|
||||
null,
|
||||
stackLayer.biome(),
|
||||
surfaceY
|
||||
)
|
||||
: stackLayer.biome();
|
||||
} else if (internalY <= caveSwitchY) {
|
||||
int surfaceY = Engine.hostHeight(engine, blockX, blockZ, true);
|
||||
underground = isUnderground(internalY, surfaceY);
|
||||
irisBiome = underground
|
||||
? engine.getCaveBiome(blockX, internalY, blockZ)
|
||||
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
: engine.getHostSurfaceBiome(blockX, blockZ);
|
||||
} else {
|
||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
irisBiome = engine.getHostSurfaceBiome(blockX, blockZ);
|
||||
}
|
||||
if (irisBiome == null && underground) {
|
||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
irisBiome = stackLayer == null
|
||||
? engine.getHostSurfaceBiome(blockX, blockZ)
|
||||
: stackLayer.biome();
|
||||
}
|
||||
if (irisBiome == null) {
|
||||
return null;
|
||||
}
|
||||
IrisModdedChunkGenerator current = generator;
|
||||
long worldSeed = current == null ? engine.getWorld().getRawWorldSeed() : current.visibleBiomeSeed();
|
||||
long seed = biomeResolutionSeed(worldSeed, blockX, blockY, blockZ);
|
||||
return new BiomeResolution(irisBiome, underground, blockX, blockY, blockZ, new RNG(seed));
|
||||
return createBiomeResolution(
|
||||
engine,
|
||||
irisBiome,
|
||||
underground,
|
||||
owningDimension,
|
||||
owningData,
|
||||
blockX,
|
||||
blockY,
|
||||
blockZ
|
||||
);
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveRequiredStructureBiome(int quartX, int quartY, int quartZ) {
|
||||
@@ -510,9 +804,10 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
}
|
||||
IrisBiome irisBiome;
|
||||
if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) {
|
||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(quartX << 2, quartZ << 2);
|
||||
irisBiome = resolveSurfaceStructureBiome(engine, quartX << 2, quartZ << 2);
|
||||
} else {
|
||||
BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ);
|
||||
BiomeResolution resolution = resolveStructureBiomeResolution(
|
||||
engine, quartX, quartY, quartZ);
|
||||
irisBiome = resolution == null ? null : resolution.irisBiome();
|
||||
}
|
||||
if (irisBiome == null) {
|
||||
@@ -557,6 +852,10 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
return internalY <= surfaceY - 8;
|
||||
}
|
||||
|
||||
static boolean isBiomeCacheable(Engine engine, int blockX, int blockZ) {
|
||||
return !engine.answersFromNaturalTerrain(blockX, blockZ);
|
||||
}
|
||||
|
||||
static boolean isMonumentSurfaceBiomeQuery(int blockY, int radius, int minHeight, int fluidHeight) {
|
||||
return radius == 29 && blockY == minHeight + fluidHeight;
|
||||
}
|
||||
@@ -612,7 +911,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
}
|
||||
|
||||
private Set<String> exactStructureBiomeKeys() {
|
||||
return biomeKeySets().required();
|
||||
return biomeKeySets().structureRequired();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -636,11 +935,12 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
private BiomeKeySets collectBiomeKeySets(long generation) {
|
||||
IrisModdedChunkGenerator current = generator;
|
||||
if (current == null) {
|
||||
return new BiomeKeySets(generation, Set.of(), Set.of());
|
||||
return new BiomeKeySets(generation, Set.of(), Set.of(), Set.of(), Set.of(), false);
|
||||
}
|
||||
Engine engine = current.structureEngineOrNull();
|
||||
if (engine == null) {
|
||||
return new BiomeKeySets(generation, current.configuredStructureBiomeKeys(), Set.of());
|
||||
return new BiomeKeySets(
|
||||
generation, current.configuredStructureBiomeKeys(), Set.of(), Set.of(), Set.of(), false);
|
||||
}
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_biome_keys");
|
||||
if (lease == null) {
|
||||
@@ -650,36 +950,154 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
if (!isReady(engine)) {
|
||||
throw new IllegalStateException("Iris structure biome key lookup has no active engine runtime");
|
||||
}
|
||||
LinkedHashSet<String> required = new LinkedHashSet<>();
|
||||
LinkedHashSet<String> visible = new LinkedHashSet<>();
|
||||
for (IrisBiome irisBiome : engine.getAllBiomes()) {
|
||||
String derivative = normalizeKey(irisBiome.getStructureDerivativeKey());
|
||||
if (derivative != null) {
|
||||
required.add(derivative);
|
||||
}
|
||||
Set<String> structureRequired = collectStructureBiomeKeys(
|
||||
engine.getDimension(), engine.getData());
|
||||
LinkedHashSet<String> visibleRequired = new LinkedHashSet<>();
|
||||
LinkedHashSet<String> visibleEmittable = new LinkedHashSet<>();
|
||||
boolean visibleFallbackPossible = false;
|
||||
for (OwnedBiome ownedBiome : getOwnedBiomes(engine)) {
|
||||
IrisBiome irisBiome = ownedBiome.biome();
|
||||
if (irisBiome.isCustom()) {
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
required.add(ModdedWorldgenIds.biomeRef(engine, customBiome.getId()));
|
||||
String customKey = customBiomeRef(
|
||||
ownedBiome.data().getDataFolder().getName(),
|
||||
ownedBiome.dimension(),
|
||||
customBiome.getId()
|
||||
);
|
||||
visibleRequired.add(customKey);
|
||||
visibleEmittable.add(customKey);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
addVisibleBiomeKey(visible, irisBiome.getDerivativeKey());
|
||||
visibleFallbackPossible |= !addVisibleBiomeKey(
|
||||
visibleEmittable, irisBiome.getDerivativeKey());
|
||||
for (String scatter : irisBiome.getBiomeScatter()) {
|
||||
addVisibleBiomeKey(visible, scatter);
|
||||
visibleFallbackPossible |= !addVisibleBiomeKey(visibleEmittable, scatter);
|
||||
}
|
||||
for (String scatter : irisBiome.getBiomeSkyScatter()) {
|
||||
addVisibleBiomeKey(visible, scatter);
|
||||
visibleFallbackPossible |= !addVisibleBiomeKey(visibleEmittable, scatter);
|
||||
}
|
||||
}
|
||||
visible.removeAll(required);
|
||||
return new BiomeKeySets(generation, Set.copyOf(required), Set.copyOf(visible));
|
||||
LinkedHashSet<String> visible = new LinkedHashSet<>(visibleEmittable);
|
||||
visible.removeAll(structureRequired);
|
||||
visible.removeAll(visibleRequired);
|
||||
return new BiomeKeySets(
|
||||
generation,
|
||||
Set.copyOf(structureRequired),
|
||||
Set.copyOf(visibleRequired),
|
||||
Set.copyOf(visible),
|
||||
Set.copyOf(visibleEmittable),
|
||||
visibleFallbackPossible
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addVisibleBiomeKey(Set<String> target, String key) {
|
||||
String normalized = normalizeKey(key);
|
||||
if (normalized != null) {
|
||||
target.add(normalized);
|
||||
static Set<String> collectStructureBiomeKeys(IrisDimension dimension, IrisData data) {
|
||||
LinkedHashSet<String> keys = new LinkedHashSet<>();
|
||||
for (IrisBiome irisBiome : dimension.getReachableBiomes(() -> data)) {
|
||||
String derivative = normalizeKey(irisBiome.getStructureDerivativeKey());
|
||||
if (derivative != null) {
|
||||
keys.add(derivative);
|
||||
}
|
||||
}
|
||||
return Set.copyOf(keys);
|
||||
}
|
||||
|
||||
private static boolean addVisibleBiomeKey(Set<String> target, String key) {
|
||||
String normalized = normalizeKey(key);
|
||||
if (normalized == null) {
|
||||
return false;
|
||||
}
|
||||
target.add(normalized);
|
||||
return true;
|
||||
}
|
||||
|
||||
static String customBiomeRef(String packName, IrisDimension dimension, String customBiomeId) {
|
||||
return ModdedWorldgenIds.biomeRef(packName, dimension.getLoadKey(), customBiomeId);
|
||||
}
|
||||
|
||||
private BiomeResolution createBiomeResolution(
|
||||
Engine engine,
|
||||
IrisBiome irisBiome,
|
||||
boolean underground,
|
||||
IrisDimension dimension,
|
||||
IrisData data,
|
||||
int blockX,
|
||||
int blockY,
|
||||
int blockZ
|
||||
) {
|
||||
IrisModdedChunkGenerator current = generator;
|
||||
long worldSeed = current == null
|
||||
? engine.getWorld().getRawWorldSeed()
|
||||
: current.visibleBiomeSeed();
|
||||
long seed = biomeResolutionSeed(worldSeed, blockX, blockY, blockZ);
|
||||
return new BiomeResolution(
|
||||
irisBiome,
|
||||
underground,
|
||||
dimension,
|
||||
data.getDataFolder().getName(),
|
||||
blockX,
|
||||
blockY,
|
||||
blockZ,
|
||||
new RNG(seed)
|
||||
);
|
||||
}
|
||||
|
||||
private static List<OwnedBiome> getOwnedBiomes(Engine engine) {
|
||||
ArrayList<OwnedBiome> biomes = new ArrayList<>();
|
||||
IrisDimension hostDimension = engine.getDimension();
|
||||
IrisData hostData = engine.getData();
|
||||
addOwnedBiomes(
|
||||
biomes,
|
||||
hostDimension,
|
||||
hostData,
|
||||
hostDimension.getReachableBiomes(engine)
|
||||
);
|
||||
DimensionStackContext stackContext = engine.getDimensionStackContext();
|
||||
if (stackContext == null) {
|
||||
return biomes;
|
||||
}
|
||||
for (DimensionTerrainContext terrainContext : stackContext.getLayersBottomToTop()) {
|
||||
if (terrainContext.isSelfReferencing()) {
|
||||
continue;
|
||||
}
|
||||
IrisDimension dimension = terrainContext.getDimension();
|
||||
IrisData data = terrainContext.getData();
|
||||
addOwnedBiomes(
|
||||
biomes,
|
||||
dimension,
|
||||
data,
|
||||
dimension.getReachableBiomes(terrainContext)
|
||||
);
|
||||
}
|
||||
return biomes;
|
||||
}
|
||||
|
||||
private static void addOwnedBiomes(
|
||||
List<OwnedBiome> target,
|
||||
IrisDimension dimension,
|
||||
IrisData data,
|
||||
Iterable<IrisBiome> biomes
|
||||
) {
|
||||
for (IrisBiome biome : biomes) {
|
||||
if (biome != null) {
|
||||
target.add(new OwnedBiome(dimension, data, biome));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static DimensionStackLayout.Layer resolveDimensionStackLayer(
|
||||
Engine engine,
|
||||
int blockX,
|
||||
int internalY,
|
||||
int blockZ
|
||||
) {
|
||||
DimensionStackContext stackContext = engine.getDimensionStackContext();
|
||||
return stackContext == null ? null : stackContext.getLayerAt(blockX, internalY, blockZ);
|
||||
}
|
||||
|
||||
private static IrisBiome resolveSurfaceStructureBiome(Engine engine, int blockX, int blockZ) {
|
||||
return engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
}
|
||||
|
||||
private static Set<String> registeredBiomeKeys(Registry<Biome> registry) {
|
||||
@@ -726,7 +1144,13 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
Holder<Biome> plains = resolveHolder(registry, "minecraft:plains");
|
||||
warnUnresolvedBiome(unresolvedKey, plains == null ? "the serialized biome source" : "minecraft:plains",
|
||||
quartX, quartY, quartZ);
|
||||
return plains == null ? serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler) : plains;
|
||||
if (plains != null) {
|
||||
return plains;
|
||||
}
|
||||
if (sampler == null) {
|
||||
throw new IllegalStateException("Iris cannot resolve a registered fallback biome");
|
||||
}
|
||||
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
|
||||
}
|
||||
|
||||
private void warnUnresolvedBiome(String unresolvedKey, String fallback,
|
||||
@@ -753,11 +1177,29 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
return ((long) x << 32) ^ ((long) z & 4294967295L);
|
||||
}
|
||||
|
||||
private record BiomeResolution(IrisBiome irisBiome, boolean underground, int blockX, int blockY,
|
||||
int blockZ, RNG rng) {
|
||||
private record BiomeResolution(
|
||||
IrisBiome irisBiome,
|
||||
boolean underground,
|
||||
IrisDimension dimension,
|
||||
String packName,
|
||||
int blockX,
|
||||
int blockY,
|
||||
int blockZ,
|
||||
RNG rng
|
||||
) {
|
||||
}
|
||||
|
||||
private record BiomeKeySets(long generation, Set<String> required, Set<String> visibleOnly) {
|
||||
private record OwnedBiome(IrisDimension dimension, IrisData data, IrisBiome biome) {
|
||||
}
|
||||
|
||||
private record BiomeKeySets(
|
||||
long generation,
|
||||
Set<String> structureRequired,
|
||||
Set<String> visibleRequired,
|
||||
Set<String> visibleOnly,
|
||||
Set<String> visibleEmittable,
|
||||
boolean visibleFallbackPossible
|
||||
) {
|
||||
}
|
||||
|
||||
private record PossibleBiomes(long generation, List<Holder<Biome>> ordered, Set<Holder<Biome>> set) {
|
||||
|
||||
+67
-11
@@ -30,6 +30,7 @@ import art.arcane.iris.engine.object.IrisImportedStructureControl;
|
||||
import art.arcane.iris.nativegen.NativeStructureStartInjector;
|
||||
import art.arcane.iris.nativegen.NativeStructureReferenceRepair;
|
||||
import art.arcane.iris.nativegen.NativeStructureVanillaLocator;
|
||||
import art.arcane.iris.nativegen.NativeStructureVolumeIndex;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
@@ -89,7 +90,9 @@ import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -181,11 +184,18 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
requireGlobalStructureGeneration(
|
||||
level.getServer().getWorldGenSettings().options().generateStructures(), dimensionKey);
|
||||
Engine activeEngine = engineIfBound();
|
||||
Engine replacement = ModdedWorldEngines.prepareReplacement(level, pack, packDimensionKey, seed);
|
||||
try {
|
||||
if (activeEngine != null) {
|
||||
requireStructureBiomeUniverseCompatible(
|
||||
activeEngine.getDimension(), replacement.getDimension());
|
||||
}
|
||||
nativeStructures.installVolumeIndex(level, replacement);
|
||||
ModdedWorldEngines.installReplacement(level, replacement);
|
||||
replacement.getPlatformHooks().applyWorldBoundary(replacement);
|
||||
} catch (Throwable error) {
|
||||
NativeStructureVolumeIndex.uninstall(replacement);
|
||||
try {
|
||||
ModdedWorldEngines.closeUnregistered(replacement);
|
||||
} catch (Throwable cleanupError) {
|
||||
@@ -203,17 +213,14 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
this.activePack = pack;
|
||||
this.activeDimensionKey = packDimensionKey;
|
||||
this.seedOverride = seed;
|
||||
this.engine = replacement;
|
||||
this.configuredStructureBiomeKeys = null;
|
||||
this.configuredPack = null;
|
||||
this.heightMetadata = engineHeights(replacement);
|
||||
this.engineBinding.reset();
|
||||
this.engineBinding.complete(replacement);
|
||||
this.announced.set(false);
|
||||
this.structureBiomeSource.clearCaches();
|
||||
this.importedFeatures.invalidate();
|
||||
this.nativeStructures.clearWorldCheckStructureShifts();
|
||||
this.spawnTables.resetVanillaSpawnBiomes();
|
||||
resetRuntimeCaches();
|
||||
this.engine = replacement;
|
||||
this.engineBinding.complete(replacement);
|
||||
// Bind time: a feature-order cycle in the new pack is reported here, once, and degrades to features-off.
|
||||
// Never waits on a build owned by another thread: this method owns the generator monitor and the build
|
||||
// path can need it.
|
||||
@@ -751,7 +758,38 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
return lastChunkGenAt;
|
||||
}
|
||||
|
||||
public void onHotload() {
|
||||
public void prepareRuntimeHotload(ServerLevel level, Engine current) {
|
||||
if (this.engine != current || level.getChunkSource().getGenerator() != this) {
|
||||
throw new IllegalStateException("Iris generator '" + dimensionKey
|
||||
+ "' cannot prepare caches for an unrelated engine hotload");
|
||||
}
|
||||
resetRuntimeCaches();
|
||||
nativeStructures.installVolumeIndex(level, current);
|
||||
}
|
||||
|
||||
public static void requireStructureBiomeUniverseCompatible(
|
||||
IrisDimension active,
|
||||
IrisDimension replacement
|
||||
) {
|
||||
IrisData activeData = Objects.requireNonNull(active.getLoader(),
|
||||
"Active Iris dimension has no pack loader");
|
||||
IrisData replacementData = Objects.requireNonNull(replacement.getLoader(),
|
||||
"Replacement Iris dimension has no pack loader");
|
||||
requireStructureBiomeUniverseCompatible(
|
||||
IrisModdedBiomeSource.collectStructureBiomeKeys(active, activeData),
|
||||
IrisModdedBiomeSource.collectStructureBiomeKeys(replacement, replacementData));
|
||||
}
|
||||
|
||||
static void requireStructureBiomeUniverseCompatible(Set<String> active, Set<String> replacement) {
|
||||
if (active.equals(replacement)) {
|
||||
return;
|
||||
}
|
||||
throw new IllegalArgumentException("Iris cannot hotload a changed host structure-biome derivative set "
|
||||
+ "from " + new TreeSet<>(active) + " to " + new TreeSet<>(replacement)
|
||||
+ ". Restart the server so Minecraft can rebuild its immutable structure state.");
|
||||
}
|
||||
|
||||
private void resetRuntimeCaches() {
|
||||
configuredStructureBiomeKeys = null;
|
||||
structureBiomeSource.clearCaches();
|
||||
importedFeatures.invalidate();
|
||||
@@ -986,7 +1024,17 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
Registry<Biome> registry = region.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
spawnTables.initializeVanillaSpawnBiomes(registry);
|
||||
ChunkPos center = region.getCenter();
|
||||
Holder<Biome> visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
|
||||
Holder<Biome> visibleBiome;
|
||||
if (engine == null || engine.getDimensionStackContext() == null) {
|
||||
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
|
||||
} else {
|
||||
visibleBiome = structureBiomeSource.getVisibleSurfaceBiome(
|
||||
center.getMinBlockX() + 8,
|
||||
center.getMinBlockZ() + 8);
|
||||
if (visibleBiome == null) {
|
||||
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
|
||||
}
|
||||
}
|
||||
Holder<Biome> vanillaBiome = spawnTables.vanillaSpawnBiome(visibleBiome.value());
|
||||
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
|
||||
random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ());
|
||||
@@ -1029,7 +1077,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
boolean ignoreFluid = !type.isOpaque().test(Blocks.WATER.defaultBlockState());
|
||||
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_base_height");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
return heightAccessor.getMinY() + current.getHeight(x, z, ignoreFluid) + 1;
|
||||
int height = current.getDimensionStackContext() == null
|
||||
? current.getHeight(x, z, ignoreFluid)
|
||||
: Engine.hostHeight(current, x, z, ignoreFluid);
|
||||
return heightAccessor.getMinY() + height + 1;
|
||||
} catch (GenerationSessionException e) {
|
||||
throw new IllegalStateException("Iris base height query could not acquire its engine runtime.", e);
|
||||
}
|
||||
@@ -1043,8 +1094,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_base_column");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
BlockState airState = Blocks.AIR.defaultBlockState();
|
||||
int surface = current.getHeight(x, z, true);
|
||||
int fluid = current.getHeight(x, z, false);
|
||||
boolean dimensionStack = current.getDimensionStackContext() != null;
|
||||
int surface = dimensionStack
|
||||
? Engine.hostHeight(current, x, z, true)
|
||||
: current.getHeight(x, z, true);
|
||||
int fluid = dimensionStack
|
||||
? Engine.hostHeight(current, x, z, false)
|
||||
: current.getHeight(x, z, false);
|
||||
BlockState stone = Blocks.STONE.defaultBlockState();
|
||||
BlockState water = Blocks.WATER.defaultBlockState();
|
||||
int solidEnd = Math.max(0, Math.min(states.length, surface + 1));
|
||||
|
||||
@@ -21,6 +21,7 @@ package art.arcane.iris.modded;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
import art.arcane.iris.spi.PlatformBiomeWriter;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
@@ -97,7 +98,7 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
|
||||
return key;
|
||||
}
|
||||
Engine engine = context.getEngine();
|
||||
String prefix = engine.getDimension().getLoadKey() + ":";
|
||||
String prefix = engine.getDimension().getCustomBiomeKeyPrefix();
|
||||
if (!key.regionMatches(true, 0, prefix, 0, prefix.length())) {
|
||||
return key;
|
||||
}
|
||||
@@ -138,13 +139,7 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
|
||||
}
|
||||
|
||||
private int idForDerivative(Registry<Biome> registry, String key) {
|
||||
int colon = key.indexOf(':');
|
||||
if (colon <= 0 || colon >= key.length() - 1) {
|
||||
return -1;
|
||||
}
|
||||
String dimensionLoadKey = key.substring(0, colon);
|
||||
String customBiomeId = key.substring(colon + 1);
|
||||
IrisBiome owner = findCustomBiomeOwner(dimensionLoadKey, customBiomeId);
|
||||
IrisBiome owner = findCustomBiomeOwner(key);
|
||||
if (owner == null) {
|
||||
return -1;
|
||||
}
|
||||
@@ -155,20 +150,18 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
|
||||
return idForKey(registry, derivativeKey);
|
||||
}
|
||||
|
||||
private IrisBiome findCustomBiomeOwner(String dimensionLoadKey, String customBiomeId) {
|
||||
private IrisBiome findCustomBiomeOwner(String key) {
|
||||
for (Engine engine : ModdedWorldEngines.activeEngines()) {
|
||||
if (engine == null || engine.isClosed()) {
|
||||
continue;
|
||||
}
|
||||
if (!dimensionLoadKey.equalsIgnoreCase(engine.getDimension().getLoadKey())) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiome biome : engine.getDimension().getAllBiomes(engine)) {
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
for (IrisBiome biome : dimension.getAllBiomes(engine)) {
|
||||
if (!biome.isCustom()) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom custom : biome.getCustomDerivitives()) {
|
||||
if (customBiomeId.equals(custom.getId())) {
|
||||
if (key.equalsIgnoreCase(dimension.getCustomBiomeKey(custom.getId()))) {
|
||||
return biome;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -258,7 +258,7 @@ public final class ModdedBlockBreakHandler {
|
||||
if (skipsParents(providers)) {
|
||||
return providers;
|
||||
}
|
||||
IrisRegion region = engine.getRegion(position.getX(), position.getZ());
|
||||
IrisRegion region = engine.getRegion(position.getX(), relativeY, position.getZ());
|
||||
if (region != null) {
|
||||
addMatching(providers, region.getBlockDrops(), brokenState, data);
|
||||
}
|
||||
|
||||
+1
-2
@@ -62,7 +62,6 @@ final class ModdedDimensionMetadata {
|
||||
|
||||
static Set<String> collectConfiguredBiomeKeys(Iterable<IrisBiome> biomes, String dimensionLoadKey) {
|
||||
LinkedHashSet<String> keys = new LinkedHashSet<>();
|
||||
String namespace = dimensionLoadKey.toLowerCase(Locale.ROOT);
|
||||
for (IrisBiome irisBiome : biomes) {
|
||||
if (irisBiome == null) {
|
||||
continue;
|
||||
@@ -75,7 +74,7 @@ final class ModdedDimensionMetadata {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
keys.add(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT));
|
||||
keys.add(IrisDimension.customBiomeKey(dimensionLoadKey, customBiome.getId()));
|
||||
}
|
||||
}
|
||||
return Set.copyOf(keys);
|
||||
|
||||
+1
-1
@@ -226,7 +226,7 @@ public final class ModdedEngineEffects extends EngineAssignedComponent implement
|
||||
int blockY = floor(player.getY()) - getEngine().getWorld().minHeight();
|
||||
int blockZ = floor(player.getZ());
|
||||
state.biome(getEngine().getBiome(blockX, blockY, blockZ));
|
||||
state.region(getEngine().getRegion(blockX, blockZ));
|
||||
state.region(getEngine().getRegion(blockX, blockY, blockZ));
|
||||
}
|
||||
|
||||
private void applyEffects(ServerLevel level, ServerPlayer player, Iterable<IrisEffect> effects) {
|
||||
|
||||
+5
-11
@@ -52,7 +52,6 @@ import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -514,18 +513,13 @@ public final class ModdedForcedDatapack {
|
||||
if (!dimensionsDirectory.isDirectory()) {
|
||||
return false;
|
||||
}
|
||||
File[] dimensionFiles = dimensionsDirectory.listFiles(
|
||||
(File file) -> file.isFile() && file.getName().endsWith(".json"));
|
||||
if (dimensionFiles == null) {
|
||||
throw new IOException("Iris could not read dimensions for pack '" + packName + "'");
|
||||
}
|
||||
if (dimensionFiles.length == 0) {
|
||||
IrisData data = IrisData.get(packFolder);
|
||||
String[] dimensionKeys = data.getDimensionLoader().getPossibleKeys();
|
||||
if (dimensionKeys == null || dimensionKeys.length == 0) {
|
||||
return false;
|
||||
}
|
||||
Arrays.sort(dimensionFiles, Comparator.comparing(File::getName));
|
||||
IrisData data = IrisData.get(packFolder);
|
||||
for (File dimensionFile : dimensionFiles) {
|
||||
String dimensionKey = dimensionFile.getName().substring(0, dimensionFile.getName().length() - ".json".length());
|
||||
List<String> sortedDimensionKeys = Stream.of(dimensionKeys).sorted().toList();
|
||||
for (String dimensionKey : sortedDimensionKeys) {
|
||||
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
|
||||
if (dimension == null) {
|
||||
throw new IllegalStateException("Iris pack '" + packName + "' dimension '"
|
||||
|
||||
+109
-10
@@ -18,6 +18,8 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.engine.DimensionStackContext;
|
||||
import art.arcane.iris.engine.DimensionStackLayout;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||
import art.arcane.iris.engine.framework.NativeFeatureGenerationPolicy;
|
||||
@@ -57,7 +59,10 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* Native placed-feature passthrough for one Iris dimension, gated on {@code importedFeatures.enabled}.
|
||||
@@ -219,7 +224,14 @@ final class ModdedImportedFeatureStage {
|
||||
private FeatureTable buildTable(Engine engine, IrisImportedFeatureControl control, long generation) {
|
||||
// Registry-ordered biome list. FeatureSorter's cycle detection walks it, so an unordered list makes
|
||||
// detection depend on JVM hash order and turns a real cycle into an intermittent one.
|
||||
List<Holder<Biome>> biomes = biomeSource.orderedPossibleBiomes();
|
||||
Set<String> hostBiomeKeys = hostBiomeKeys(engine);
|
||||
List<Holder<Biome>> biomes = new ArrayList<>();
|
||||
for (Holder<Biome> biome : biomeSource.orderedPossibleBiomes()) {
|
||||
String key = holderKey(biome);
|
||||
if (key != null && hostBiomeKeys.contains(key)) {
|
||||
biomes.add(biome);
|
||||
}
|
||||
}
|
||||
if (biomes.isEmpty()) {
|
||||
ModdedIrisLog.error("Iris importedFeatures is on but {} exposes no biomes; features off",
|
||||
dimensionKey(engine));
|
||||
@@ -262,7 +274,7 @@ final class ModdedImportedFeatureStage {
|
||||
*/
|
||||
private Map<String, Holder<Biome>> customBiomeDerivatives(Engine engine, Map<String, Holder<Biome>> byKey) {
|
||||
Map<String, Holder<Biome>> derivatives = new HashMap<>();
|
||||
for (IrisBiome irisBiome : engine.getAllBiomes()) {
|
||||
for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) {
|
||||
if (!irisBiome.isCustom()) {
|
||||
continue;
|
||||
}
|
||||
@@ -322,14 +334,24 @@ final class ModdedImportedFeatureStage {
|
||||
List<FeatureSorter.StepFeatureData> steps = table.steps();
|
||||
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
|
||||
long decorationSeed = random.setDecorationSeed(level.getSeed(), origin.getX(), origin.getZ());
|
||||
Set<Holder<Biome>> chunkBiomes = chunkBiomes(level, sectionPos, table);
|
||||
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
|
||||
int staticMinY = engine.getMinHeight();
|
||||
WorldGenLevel placementLevel = staticObjects.isEmpty() ? level : ModdedNativeStructureWorldgenAccess.create(
|
||||
DimensionStackContext stackContext = engine.getDimensionStackContext();
|
||||
Set<Holder<Biome>> chunkBiomes = chunkBiomes(level, sectionPos, table, stackContext);
|
||||
IntBinaryOperator surfaceFirstFreeY = stackContext == null
|
||||
? (x, z) -> level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z)
|
||||
: (x, z) -> Engine.hostHeight(engine, x, z, false) + staticMinY + 1;
|
||||
IntBinaryOperator floorFirstFreeY = stackContext == null
|
||||
? (x, z) -> level.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z)
|
||||
: (x, z) -> Engine.hostHeight(engine, x, z, true) + staticMinY + 1;
|
||||
WorldGenLevel placementLevel = staticObjects.isEmpty() && stackContext == null
|
||||
? level
|
||||
: ModdedNativeStructureWorldgenAccess.create(
|
||||
level, centerPos,
|
||||
(x, z) -> level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z),
|
||||
(x, z) -> level.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z),
|
||||
position -> staticObjects.contains(position.getX(), position.getY() - staticMinY, position.getZ()));
|
||||
surfaceFirstFreeY,
|
||||
floorFirstFreeY,
|
||||
stackContext != null,
|
||||
importedFeatureProtection(staticObjects, stackContext, staticMinY));
|
||||
|
||||
try {
|
||||
for (int stepIndex = 0; stepIndex < steps.size(); stepIndex++) {
|
||||
@@ -349,6 +371,58 @@ final class ModdedImportedFeatureStage {
|
||||
WorldCheckFeaturePlacement.recordPlacementPass();
|
||||
}
|
||||
|
||||
private static Set<String> hostBiomeKeys(Engine engine) {
|
||||
Set<String> keys = new LinkedHashSet<>();
|
||||
for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) {
|
||||
addKey(keys, irisBiome.getStructureDerivativeKey());
|
||||
addKey(keys, irisBiome.getDerivativeKey());
|
||||
for (String scatter : irisBiome.getBiomeScatter()) {
|
||||
addKey(keys, scatter);
|
||||
}
|
||||
for (String scatter : irisBiome.getBiomeSkyScatter()) {
|
||||
addKey(keys, scatter);
|
||||
}
|
||||
if (!irisBiome.isCustom()) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
addKey(keys, ModdedWorldgenIds.biomeRef(engine, customBiome.getId()));
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
private static void addKey(Set<String> keys, String key) {
|
||||
String normalized = normalizeKey(key);
|
||||
if (normalized != null) {
|
||||
keys.add(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
private static Predicate<BlockPos> importedFeatureProtection(
|
||||
IrisStaticObjectLayer staticObjects,
|
||||
DimensionStackContext stackContext,
|
||||
int minimumY
|
||||
) {
|
||||
Map<Long, DimensionStackLayout> layouts = new ConcurrentHashMap<>();
|
||||
return position -> {
|
||||
if (!staticObjects.isEmpty() && staticObjects.contains(
|
||||
position.getX(), position.getY() - minimumY, position.getZ())) {
|
||||
return true;
|
||||
}
|
||||
if (stackContext == null) {
|
||||
return false;
|
||||
}
|
||||
long columnKey = ((long) position.getX() << 32)
|
||||
^ (position.getZ() & 0xFFFFFFFFL);
|
||||
DimensionStackLayout layout = layouts.computeIfAbsent(
|
||||
columnKey,
|
||||
ignored -> stackContext.sample(position.getX(), position.getZ())
|
||||
);
|
||||
return layout.isHostFeatureProtectedY(position.getY() - minimumY);
|
||||
};
|
||||
}
|
||||
|
||||
private void placeStep(WorldGenLevel level, FeatureTable table, FeatureSorter.StepFeatureData stepData,
|
||||
Registry<PlacedFeature> featureRegistry, Set<Holder<Biome>> chunkBiomes,
|
||||
IrisModdedChunkGenerator owner, WorldgenRandom random, long decorationSeed,
|
||||
@@ -394,12 +468,37 @@ final class ModdedImportedFeatureStage {
|
||||
* shape as vanilla, which drops any section biome its biome source does not claim. Holders are canonicalised
|
||||
* back onto the table's own holders so the index mapping cannot be handed a holder it never saw.
|
||||
*/
|
||||
private Set<Holder<Biome>> chunkBiomes(WorldGenLevel level, SectionPos sectionPos, FeatureTable table) {
|
||||
private Set<Holder<Biome>> chunkBiomes(
|
||||
WorldGenLevel level,
|
||||
SectionPos sectionPos,
|
||||
FeatureTable table,
|
||||
DimensionStackContext stackContext
|
||||
) {
|
||||
List<Holder<Biome>> collected = new ArrayList<>();
|
||||
ChunkPos.rangeClosed(sectionPos.chunk(), 1).forEach((ChunkPos chunkPos) -> {
|
||||
ChunkAccess neighbour = level.getChunk(chunkPos.x(), chunkPos.z());
|
||||
for (LevelChunkSection section : neighbour.getSections()) {
|
||||
section.getBiomes().getAll(collected::add);
|
||||
LevelChunkSection[] sections = neighbour.getSections();
|
||||
for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
|
||||
LevelChunkSection section = sections[sectionIndex];
|
||||
if (stackContext == null) {
|
||||
section.getBiomes().getAll(collected::add);
|
||||
continue;
|
||||
}
|
||||
int sectionMinimumY = neighbour.getSectionYFromSectionIndex(sectionIndex) << 4;
|
||||
for (int quartY = 0; quartY < 4; quartY++) {
|
||||
int internalY = sectionMinimumY + (quartY << 2) - level.getMinY();
|
||||
for (int quartX = 0; quartX < 4; quartX++) {
|
||||
int blockX = chunkPos.getMinBlockX() + (quartX << 2);
|
||||
for (int quartZ = 0; quartZ < 4; quartZ++) {
|
||||
int blockZ = chunkPos.getMinBlockZ() + (quartZ << 2);
|
||||
DimensionStackLayout.Layer layer = stackContext.getLayerAt(
|
||||
blockX, internalY, blockZ);
|
||||
if (layer.terrainContext().isSelfReferencing()) {
|
||||
collected.add(section.getBiomes().get(quartX, quartY, quartZ));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Set<Holder<Biome>> present = new LinkedHashSet<>();
|
||||
|
||||
+48
-9
@@ -18,6 +18,8 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.engine.DimensionStackContext;
|
||||
import art.arcane.iris.engine.DimensionStackLayout;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||
@@ -78,6 +80,7 @@ import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* Native (vanilla registry) structure stage for {@link IrisModdedChunkGenerator}. The generator keeps the
|
||||
@@ -266,7 +269,7 @@ final class ModdedNativeStructureStage {
|
||||
undergroundStep,
|
||||
decision.preserveSourceY(),
|
||||
decision.yBand(),
|
||||
(x, z) -> current.getHeight(x, z, true) + current.getMinHeight());
|
||||
(x, z) -> Engine.hostHeight(current, x, z, true) + current.getMinHeight());
|
||||
StructureStart wrapped = NativeStructureReferenceEnvelope.wrapForPublication(
|
||||
start, structure, start.getReferences(),
|
||||
NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain()),
|
||||
@@ -367,10 +370,14 @@ final class ModdedNativeStructureStage {
|
||||
chunkPos.x(), chunkPos.z(), error);
|
||||
}
|
||||
IrisStaticObjectLayer staticObjects = current.getDimension().getStaticObjectLayer(current.getData());
|
||||
int staticMinY = current.getMinHeight();
|
||||
WorldGenLevel boundedWorld = staticObjects.isEmpty() ? world : ModdedNativeStructureWorldgenAccess.create(
|
||||
Predicate<BlockPos> protectedPosition = nativeStructureProtection(current, staticObjects);
|
||||
WorldGenLevel boundedWorld = staticObjects.isEmpty()
|
||||
&& current.getDimensionStackContext() == null
|
||||
? world
|
||||
: ModdedNativeStructureWorldgenAccess.create(
|
||||
world, chunkPos, worldgenSurfaceHeight(current, world.getMinY()), worldgenFloorHeight(current, world.getMinY()),
|
||||
position -> staticObjects.contains(position.getX(), position.getY() - staticMinY, position.getZ()));
|
||||
current.getDimensionStackContext() != null,
|
||||
protectedPosition);
|
||||
try {
|
||||
NativeStructureVegetationClearer.clearIntersectingVegetation(
|
||||
boundedWorld, chunk, area, vegetationTargets);
|
||||
@@ -383,7 +390,7 @@ final class ModdedNativeStructureStage {
|
||||
try {
|
||||
vacuumFoundationPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
|
||||
boundedWorld, area, terrainTargets,
|
||||
(x, z) -> current.getHeight(x, z, true) + current.getMinHeight());
|
||||
(x, z) -> Engine.hostHeight(current, x, z, true) + current.getMinHeight());
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"terrain integration", nativeStructureBatchContext(placementGroups),
|
||||
@@ -443,18 +450,50 @@ final class ModdedNativeStructureStage {
|
||||
IrisNativeStructureDecision decision) {
|
||||
Engine current = generator.engine();
|
||||
WorldGenLevel boundedWorld = world instanceof ModdedNativeStructureWorldgenAccess ? world : ModdedNativeStructureWorldgenAccess.create(
|
||||
world, chunkPos, worldgenSurfaceHeight(current, world.getMinY()), worldgenFloorHeight(current, world.getMinY()), position -> false);
|
||||
world,
|
||||
chunkPos,
|
||||
worldgenSurfaceHeight(current, world.getMinY()),
|
||||
worldgenFloorHeight(current, world.getMinY()),
|
||||
current.getDimensionStackContext() != null,
|
||||
nativeStructureProtection(
|
||||
current,
|
||||
current.getDimension().getStaticObjectLayer(current.getData())));
|
||||
world.setCurrentlyGenerating(() -> "Iris native structure " + structureId);
|
||||
try {
|
||||
NativeStructurePostProcessor.place(
|
||||
boundedWorld, structureManager, generator, random, area, chunkPos,
|
||||
structureId, start, decision, this::resolvePaletteBlock,
|
||||
(x, z) -> current.getHeight(x, z, true) + current.getMinHeight());
|
||||
(x, z) -> Engine.hostHeight(current, x, z, true) + current.getMinHeight());
|
||||
} finally {
|
||||
world.setCurrentlyGenerating(null);
|
||||
}
|
||||
}
|
||||
|
||||
private static Predicate<BlockPos> nativeStructureProtection(
|
||||
Engine engine,
|
||||
IrisStaticObjectLayer staticObjects
|
||||
) {
|
||||
DimensionStackContext stackContext = engine.getDimensionStackContext();
|
||||
int minimumY = engine.getMinHeight();
|
||||
Map<Long, DimensionStackLayout> layouts = new ConcurrentHashMap<>();
|
||||
return position -> {
|
||||
if (!staticObjects.isEmpty() && staticObjects.contains(
|
||||
position.getX(), position.getY() - minimumY, position.getZ())) {
|
||||
return true;
|
||||
}
|
||||
if (stackContext == null) {
|
||||
return false;
|
||||
}
|
||||
long columnKey = ((long) position.getX() << 32)
|
||||
^ (position.getZ() & 0xFFFFFFFFL);
|
||||
DimensionStackLayout layout = layouts.computeIfAbsent(
|
||||
columnKey,
|
||||
ignored -> stackContext.sample(position.getX(), position.getZ())
|
||||
);
|
||||
return layout.isHostFeatureProtectedY(position.getY() - minimumY);
|
||||
};
|
||||
}
|
||||
|
||||
private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
|
||||
StructureStepCache cached = structureStepCache;
|
||||
if (cached != null && cached.registry() == registry) {
|
||||
@@ -523,11 +562,11 @@ final class ModdedNativeStructureStage {
|
||||
}
|
||||
|
||||
private IntBinaryOperator worldgenSurfaceHeight(Engine generationEngine, int runtimeMinY) {
|
||||
return (x, z) -> generationEngine.getHeight(x, z, false) + runtimeMinY + 1;
|
||||
return (x, z) -> Engine.hostHeight(generationEngine, x, z, false) + runtimeMinY + 1;
|
||||
}
|
||||
|
||||
private IntBinaryOperator worldgenFloorHeight(Engine generationEngine, int runtimeMinY) {
|
||||
return (x, z) -> generationEngine.getHeight(x, z, true) + runtimeMinY + 1;
|
||||
return (x, z) -> Engine.hostHeight(generationEngine, x, z, true) + runtimeMinY + 1;
|
||||
}
|
||||
|
||||
private record NativeStructureStartKey(String structureId, long chunkPosition) {
|
||||
|
||||
+6
-2
@@ -63,6 +63,7 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
|
||||
private final ChunkPos generationCenter;
|
||||
private final IntBinaryOperator surfaceFirstFreeY;
|
||||
private final IntBinaryOperator floorFirstFreeY;
|
||||
private final boolean virtualizeHeights;
|
||||
private final Predicate<BlockPos> protectedPosition;
|
||||
private final Holder<Biome> fallbackBiome;
|
||||
private final BiomeManager biomeManager;
|
||||
@@ -80,6 +81,7 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
|
||||
boundary.surfaceFirstFreeY(), "Native structure world access requires a surface resolver");
|
||||
this.floorFirstFreeY = Objects.requireNonNull(
|
||||
boundary.floorFirstFreeY(), "Native structure world access requires an ocean-floor resolver");
|
||||
this.virtualizeHeights = boundary.virtualizeHeights();
|
||||
this.protectedPosition = Objects.requireNonNull(
|
||||
boundary.protectedPosition(), "World access requires a protected-position predicate");
|
||||
BlockPos biomeSample = generationCenter.getMiddleBlockPosition(delegate.getSeaLevel());
|
||||
@@ -102,9 +104,10 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
|
||||
static ModdedNativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter,
|
||||
IntBinaryOperator surfaceFirstFreeY,
|
||||
IntBinaryOperator floorFirstFreeY,
|
||||
boolean virtualizeHeights,
|
||||
Predicate<BlockPos> protectedPosition) {
|
||||
return new ModdedNativeStructureWorldgenAccess(delegate, new Boundary(
|
||||
generationCenter, surfaceFirstFreeY, floorFirstFreeY, protectedPosition));
|
||||
generationCenter, surfaceFirstFreeY, floorFirstFreeY, virtualizeHeights, protectedPosition));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -241,7 +244,7 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
|
||||
|
||||
@Override
|
||||
public int getHeight(Heightmap.Types type, int x, int z) {
|
||||
if (isInsideGenerationRegion(x >> 4, z >> 4)) {
|
||||
if (!virtualizeHeights && isInsideGenerationRegion(x >> 4, z >> 4)) {
|
||||
return delegate.getHeight(type, x, z);
|
||||
}
|
||||
return height(type, x, z);
|
||||
@@ -526,6 +529,7 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
|
||||
private record Boundary(ChunkPos generationCenter,
|
||||
IntBinaryOperator surfaceFirstFreeY,
|
||||
IntBinaryOperator floorFirstFreeY,
|
||||
boolean virtualizeHeights,
|
||||
Predicate<BlockPos> protectedPosition) {
|
||||
}
|
||||
|
||||
|
||||
@@ -108,12 +108,17 @@ public final class ModdedScheduler implements PlatformScheduler {
|
||||
scheduler.drain();
|
||||
}
|
||||
|
||||
public static boolean isMainThread() {
|
||||
Thread running = mainThread;
|
||||
return running != null && Thread.currentThread() == running;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void global(Runnable task) {
|
||||
if (task == null) {
|
||||
return;
|
||||
}
|
||||
if (onMainThread()) {
|
||||
if (isMainThread()) {
|
||||
runGuarded(task);
|
||||
return;
|
||||
}
|
||||
@@ -250,11 +255,6 @@ public final class ModdedScheduler implements PlatformScheduler {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean onMainThread() {
|
||||
Thread main = mainThread;
|
||||
return main != null && Thread.currentThread() == main;
|
||||
}
|
||||
|
||||
private void runGuarded(Runnable task) {
|
||||
try {
|
||||
task.run();
|
||||
|
||||
+75
-34
@@ -18,10 +18,14 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.DimensionStackContext;
|
||||
import art.arcane.iris.engine.DimensionTerrainContext;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.Registry;
|
||||
@@ -31,32 +35,24 @@ import net.minecraft.world.entity.MobCategory;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.MobSpawnSettings;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Vanilla-derivative spawn table state for {@link IrisModdedChunkGenerator}. Every mutator locks the
|
||||
* generator monitor because the repoint/bind/reset paths already hold it while resetting this state.
|
||||
*/
|
||||
final class ModdedSpawnTableMerger {
|
||||
private final IrisModdedChunkGenerator generator;
|
||||
private final ConcurrentHashMap<Biome, Holder<Biome>> vanillaSpawnBiomes = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
|
||||
private volatile boolean vanillaSpawnBiomesInitialized;
|
||||
private volatile SpawnTableState state = new SpawnTableState();
|
||||
|
||||
ModdedSpawnTableMerger(IrisModdedChunkGenerator generator) {
|
||||
this.generator = generator;
|
||||
}
|
||||
|
||||
void initializeVanillaSpawnBiomes(Registry<Biome> registry) {
|
||||
// Volatile fast path BEFORE the monitor: this runs per mob category per spawn attempt
|
||||
// on the server thread, and the generator monitor is contended by binds/hotloads.
|
||||
if (vanillaSpawnBiomesInitialized) {
|
||||
SpawnTableState active = state;
|
||||
if (active.initialized) {
|
||||
return;
|
||||
}
|
||||
synchronized (generator) {
|
||||
if (vanillaSpawnBiomesInitialized) {
|
||||
synchronized (active) {
|
||||
if (active != state || active.initialized) {
|
||||
return;
|
||||
}
|
||||
Engine current = generator.engineOrNull();
|
||||
@@ -66,29 +62,70 @@ final class ModdedSpawnTableMerger {
|
||||
|
||||
try (GenerationSessionLease lease = generator.requireGenerationLease(current, "modded_spawn_biomes");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
String namespace = current.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
|
||||
for (IrisBiome irisBiome : current.getDimension().getReachableBiomes(current)) {
|
||||
if (irisBiome == null || !irisBiome.isCustom()) {
|
||||
continue;
|
||||
}
|
||||
Holder<Biome> vanillaHolder = resolveBiomeHolder(registry, irisBiome.getVanillaDerivativeKey());
|
||||
if (vanillaHolder == null) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
Holder<Biome> customHolder = resolveBiomeHolder(registry, namespace + ":" + customBiome.getId());
|
||||
if (customHolder != null) {
|
||||
vanillaSpawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
|
||||
IrisDimension hostDimension = current.getDimension();
|
||||
addVanillaSpawnBiomes(
|
||||
registry,
|
||||
hostDimension,
|
||||
current.getData(),
|
||||
hostDimension.getReachableBiomes(current),
|
||||
active
|
||||
);
|
||||
DimensionStackContext stackContext = current.getDimensionStackContext();
|
||||
if (stackContext != null) {
|
||||
for (DimensionTerrainContext terrainContext : stackContext.getLayersBottomToTop()) {
|
||||
if (terrainContext.isSelfReferencing()) {
|
||||
continue;
|
||||
}
|
||||
IrisDimension dimension = terrainContext.getDimension();
|
||||
addVanillaSpawnBiomes(
|
||||
registry,
|
||||
dimension,
|
||||
terrainContext.getData(),
|
||||
dimension.getReachableBiomes(terrainContext),
|
||||
active
|
||||
);
|
||||
}
|
||||
}
|
||||
vanillaSpawnBiomesInitialized = true;
|
||||
active.initialized = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addVanillaSpawnBiomes(
|
||||
Registry<Biome> registry,
|
||||
IrisDimension dimension,
|
||||
IrisData data,
|
||||
Iterable<IrisBiome> biomes,
|
||||
SpawnTableState state
|
||||
) {
|
||||
for (IrisBiome irisBiome : biomes) {
|
||||
if (irisBiome == null || !irisBiome.isCustom()) {
|
||||
continue;
|
||||
}
|
||||
Holder<Biome> vanillaHolder = resolveBiomeHolder(
|
||||
registry, irisBiome.getVanillaDerivativeKey());
|
||||
if (vanillaHolder == null) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
Holder<Biome> customHolder = resolveBiomeHolder(
|
||||
registry,
|
||||
ModdedWorldgenIds.biomeRef(
|
||||
data.getDataFolder().getName(),
|
||||
dimension.getLoadKey(),
|
||||
customBiome.getId()
|
||||
)
|
||||
);
|
||||
if (customHolder == null) {
|
||||
continue;
|
||||
}
|
||||
state.vanillaSpawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Holder<Biome> vanillaSpawnBiome(Biome biome) {
|
||||
return vanillaSpawnBiomes.get(biome);
|
||||
return state.vanillaSpawnBiomes.get(biome);
|
||||
}
|
||||
|
||||
WeightedList<MobSpawnSettings.SpawnerData> mergedSpawnTable(
|
||||
@@ -96,7 +133,8 @@ final class ModdedSpawnTableMerger {
|
||||
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns,
|
||||
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns) {
|
||||
SpawnTableKey key = new SpawnTableKey(biome, category);
|
||||
return mergedSpawnTables.computeIfAbsent(key, ignored -> NativeSpawnTableMerger.merge(vanillaSpawns, explicitSpawns));
|
||||
return state.mergedSpawnTables.computeIfAbsent(
|
||||
key, ignored -> NativeSpawnTableMerger.merge(vanillaSpawns, explicitSpawns));
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String key) {
|
||||
@@ -112,13 +150,16 @@ final class ModdedSpawnTableMerger {
|
||||
}
|
||||
|
||||
void resetVanillaSpawnBiomes() {
|
||||
synchronized (generator) {
|
||||
vanillaSpawnBiomes.clear();
|
||||
mergedSpawnTables.clear();
|
||||
vanillaSpawnBiomesInitialized = false;
|
||||
}
|
||||
state = new SpawnTableState();
|
||||
}
|
||||
|
||||
private record SpawnTableKey(Biome biome, MobCategory category) {
|
||||
}
|
||||
|
||||
private static final class SpawnTableState {
|
||||
private final ConcurrentHashMap<Biome, Holder<Biome>> vanillaSpawnBiomes = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables =
|
||||
new ConcurrentHashMap<>();
|
||||
private volatile boolean initialized;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -84,7 +84,8 @@ final class ModdedEditCommands {
|
||||
}
|
||||
BlockPos pos = player.blockPosition();
|
||||
try {
|
||||
region = engine.getRegion(pos.getX(), pos.getZ());
|
||||
region = engine.getRegion(
|
||||
pos.getX(), pos.getY() - engine.getMinHeight(), pos.getZ());
|
||||
} catch (Throwable e) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_REGION_LOOKUP_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName())));
|
||||
return 0;
|
||||
|
||||
+52
-14
@@ -73,6 +73,8 @@ import org.zeroturnaround.zip.ZipUtil;
|
||||
import java.awt.Desktop;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
@@ -85,6 +87,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.ModdedCommandMessages;
|
||||
@@ -891,21 +894,30 @@ public final class ModdedStudioCommands {
|
||||
LinkedHashSet<String> objectKeys = new LinkedHashSet<>();
|
||||
LinkedHashSet<String> markerKeys = new LinkedHashSet<>();
|
||||
LinkedHashSet<String> structureKeys = new LinkedHashSet<>();
|
||||
List<IrisDimension> dimensions = new ArrayList<>();
|
||||
|
||||
dimension.getAllRegions(() -> dm).forEach((IrisRegion region) -> {
|
||||
if (region != null && region.getLoadKey() != null) {
|
||||
regionKeys.add(region.getLoadKey());
|
||||
for (String dimensionKey : PackExportClosure.collectDimensionKeys(dimension).stream().sorted().toList()) {
|
||||
IrisDimension exportedDimension = dm.getDimensionLoader().load(dimensionKey);
|
||||
if (exportedDimension != null) {
|
||||
dimensions.add(exportedDimension);
|
||||
}
|
||||
});
|
||||
dimension.getReachableBiomes(() -> dm).forEach((IrisBiome biome) -> {
|
||||
if (biome != null && biome.getLoadKey() != null) {
|
||||
biomeKeys.add(biome.getLoadKey());
|
||||
}
|
||||
});
|
||||
lootKeys.addAll(dimension.getLoot().getTables());
|
||||
spawnerKeys.addAll(dimension.getEntitySpawners());
|
||||
collectStructureKeys(structureKeys, dimension.getStructures());
|
||||
objectKeys.addAll(PackExportClosure.collectStaticObjectKeys(dimension.getStaticObjects()));
|
||||
}
|
||||
for (IrisDimension exportedDimension : dimensions) {
|
||||
exportedDimension.getAllRegions(() -> dm).forEach((IrisRegion region) -> {
|
||||
if (region != null && region.getLoadKey() != null) {
|
||||
regionKeys.add(region.getLoadKey());
|
||||
}
|
||||
});
|
||||
exportedDimension.getReachableBiomes(() -> dm).forEach((IrisBiome biome) -> {
|
||||
if (biome != null && biome.getLoadKey() != null) {
|
||||
biomeKeys.add(biome.getLoadKey());
|
||||
}
|
||||
});
|
||||
lootKeys.addAll(exportedDimension.getLoot().getTables());
|
||||
spawnerKeys.addAll(exportedDimension.getEntitySpawners());
|
||||
collectStructureKeys(structureKeys, exportedDimension.getStructures());
|
||||
objectKeys.addAll(PackExportClosure.collectStaticObjectKeys(exportedDimension.getStaticObjects()));
|
||||
}
|
||||
|
||||
for (String regionKey : regionKeys) {
|
||||
IrisRegion region = dm.getRegionLoader().load(regionKey);
|
||||
@@ -975,10 +987,17 @@ public final class ModdedStudioCommands {
|
||||
}
|
||||
}
|
||||
|
||||
hashes.append(copyJson(folder, "dimensions", dimension.getLoadKey(), dm.getDimensionLoader().findFile(dimension.getLoadKey())));
|
||||
for (IrisDimension exportedDimension : dimensions) {
|
||||
String key = exportedDimension.getLoadKey();
|
||||
hashes.append(copyJson(folder, "dimensions", key, dm.getDimensionLoader().findFile(key)));
|
||||
}
|
||||
for (String key : generatorKeys) {
|
||||
hashes.append(copyJson(folder, "generators", key, dm.getGeneratorLoader().findFile(key)));
|
||||
}
|
||||
for (String key : Stream.of(dm.getExpressionLoader().getPossibleKeys()).sorted().toList()) {
|
||||
hashes.append(copyJson(folder, "expressions", key, dm.getExpressionLoader().findFile(key)));
|
||||
}
|
||||
hashes.append(copyStyleSnippets(packFolder, folder));
|
||||
for (String key : regionKeys) {
|
||||
hashes.append(copyJson(folder, "regions", key, dm.getRegionLoader().findFile(key)));
|
||||
}
|
||||
@@ -1041,6 +1060,25 @@ public final class ModdedStudioCommands {
|
||||
}
|
||||
}
|
||||
|
||||
private static String copyStyleSnippets(File packFolder, File folder) throws IOException {
|
||||
Path sourceRoot = new File(packFolder, "snippet/style").toPath();
|
||||
if (!Files.isDirectory(sourceRoot)) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder hashes = new StringBuilder();
|
||||
try (Stream<Path> files = Files.walk(sourceRoot)) {
|
||||
for (Path source : files.filter(Files::isRegularFile)
|
||||
.filter(path -> path.getFileName().toString().endsWith(".json"))
|
||||
.sorted()
|
||||
.toList()) {
|
||||
String relative = sourceRoot.relativize(source).toString().replace(File.separatorChar, '/');
|
||||
String key = relative.substring(0, relative.length() - ".json".length());
|
||||
hashes.append(copyJson(folder, "snippet/style", key, source.toFile()));
|
||||
}
|
||||
}
|
||||
return hashes.toString();
|
||||
}
|
||||
|
||||
private static int regions(CommandSourceStack source, int radius) {
|
||||
ServerPlayer player = source.getPlayer();
|
||||
if (player == null) {
|
||||
|
||||
+2
-1
@@ -216,7 +216,8 @@ public final class ModdedWhatCommands {
|
||||
int centerX = (pos.getX() & ~15) + 8;
|
||||
int centerZ = (pos.getZ() & ~15) + 8;
|
||||
try {
|
||||
IrisRegion region = engine.getRegion(centerX, centerZ);
|
||||
IrisRegion region = engine.getRegion(
|
||||
centerX, pos.getY() - engine.getMinHeight(), centerZ);
|
||||
IrisModdedCommands.ok(source, IrisLanguage.plain(
|
||||
RuntimeUiMessages.WHAT_IRIS_REGION,
|
||||
MessageArgument.untrusted("region", region.getLoadKey()),
|
||||
|
||||
+20
-1
@@ -32,6 +32,7 @@ import art.arcane.iris.engine.object.IrisWorldBoundary;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.ModdedDimensionManager;
|
||||
import art.arcane.iris.modded.ModdedForcedDatapack;
|
||||
import art.arcane.iris.modded.ModdedScheduler;
|
||||
import art.arcane.iris.modded.ModdedWorkspaceGenerator;
|
||||
import art.arcane.iris.nativegen.NativeStructureVolumeIndex;
|
||||
import art.arcane.iris.modded.command.ModdedPregenJob;
|
||||
@@ -100,6 +101,11 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
||||
return NativeStructureVolumeIndex.volumes(engine, minX, minZ, maxX, maxZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMainThread() {
|
||||
return ModdedScheduler.isMainThread();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshDatapackWorkspace(Engine engine) {
|
||||
IrisData data = engine.getData();
|
||||
@@ -121,6 +127,20 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
||||
engine.getDimension(),
|
||||
replacement,
|
||||
"irisworldgen");
|
||||
IrisModdedChunkGenerator.requireStructureBiomeUniverseCompatible(
|
||||
engine.getDimension(), replacement);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareRuntimeHotload(Engine engine) {
|
||||
IrisWorld world = engine.getWorld();
|
||||
if (world == null || !world.hasPlatformWorld()
|
||||
|| !(world.platformWorld().nativeHandle() instanceof ServerLevel level)
|
||||
|| !(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator)) {
|
||||
NativeStructureVolumeIndex.invalidate(engine);
|
||||
return;
|
||||
}
|
||||
generator.prepareRuntimeHotload(level, engine);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -285,7 +305,6 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
||||
long start = System.currentTimeMillis();
|
||||
try {
|
||||
engine.hotloadSilently();
|
||||
generator.onHotload();
|
||||
ModdedIrisLog.info("Iris studio hotload {} pack={} {}ms", dimensionId, engine.getDimension().getLoadKey(), System.currentTimeMillis() - start);
|
||||
} catch (Throwable e) {
|
||||
ModdedIrisLog.error("Iris studio hotload failed for {}", dimensionId, e);
|
||||
|
||||
+1
@@ -59,6 +59,7 @@ public class IrisModdedChunkGeneratorSpawnTest {
|
||||
String spawn = source.substring(spawnStart, spawnEnd);
|
||||
|
||||
assertTrue(spawn.contains("spawnTables.initializeVanillaSpawnBiomes(registry)"));
|
||||
assertTrue(spawn.contains("structureBiomeSource.getVisibleSurfaceBiome("));
|
||||
assertTrue(spawn.contains("spawnTables.vanillaSpawnBiome(visibleBiome.value())"));
|
||||
assertTrue(spawn.contains("NaturalSpawner.spawnMobsForChunkGeneration("));
|
||||
assertTrue(spawn.contains("new LegacyRandomSource(RandomSupport.generateUniqueSeed())"));
|
||||
|
||||
+185
@@ -5,6 +5,7 @@ import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisRange;
|
||||
import art.arcane.iris.engine.object.InferredType;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import net.minecraft.SharedConstants;
|
||||
@@ -29,6 +30,8 @@ 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.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisModdedStructureParityTest {
|
||||
@BeforeClass
|
||||
@@ -110,6 +113,144 @@ public class IrisModdedStructureParityTest {
|
||||
assertEquals(expected, IrisModdedBiomeSource.biomeResolutionSeed(worldSeed, blockX, blockY, blockZ));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stackedCustomBiomesUseTheirOwningDimensionNamespace() {
|
||||
IrisDimension host = new IrisDimension();
|
||||
host.setLoadKey("Host");
|
||||
IrisDimension upper = new IrisDimension();
|
||||
upper.setLoadKey("Upper");
|
||||
|
||||
assertEquals(
|
||||
ModdedWorldgenIds.biomeRef("pack", "Upper", "Aurora"),
|
||||
IrisModdedBiomeSource.customBiomeRef("pack", upper, "Aurora")
|
||||
);
|
||||
assertFalse(IrisModdedBiomeSource.customBiomeRef("pack", host, "Aurora")
|
||||
.equals(IrisModdedBiomeSource.customBiomeRef("pack", upper, "Aurora")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void visibleBiomeResolutionSelectsStackOwnershipBeforeHostCaves() throws IOException {
|
||||
String source = Files.readString(Path.of(
|
||||
System.getProperty("iris.moddedCommonSources"),
|
||||
"art/arcane/iris/modded/IrisModdedBiomeSource.java"));
|
||||
int resolutionStart = source.indexOf("private BiomeResolution resolveBiomeResolution(");
|
||||
int resolutionEnd = source.indexOf("private Holder<Biome> resolveRequiredStructureBiome(", resolutionStart);
|
||||
|
||||
assertTrue(resolutionStart >= 0);
|
||||
assertTrue(resolutionEnd > resolutionStart);
|
||||
String resolution = source.substring(resolutionStart, resolutionEnd);
|
||||
|
||||
assertTrue(resolution.contains("resolveDimensionStackLayer("));
|
||||
assertTrue(resolution.contains("!stackLayer.terrainContext().isSelfReferencing()"));
|
||||
assertTrue(resolution.indexOf("resolveDimensionStackLayer(")
|
||||
< resolution.indexOf("boolean underground"));
|
||||
assertTrue(resolution.contains("includeDimensionStack"));
|
||||
assertTrue(resolution.contains(
|
||||
"return resolveBiomeResolution(engine, quartX, quartY, quartZ, false)"));
|
||||
assertTrue(source.contains("resolution.packName(), resolution.dimension(), customBiome.getId()"));
|
||||
int helperStart = source.indexOf("private static IrisBiome resolveSurfaceStructureBiome(");
|
||||
int helperEnd = source.indexOf("private static Set<String> registeredBiomeKeys(", helperStart);
|
||||
String helper = source.substring(helperStart, helperEnd);
|
||||
assertTrue(helper.contains("engine.getComplex().getTrueBiomeStream().get(blockX, blockZ)"));
|
||||
assertFalse(helper.contains("getDimensionStackContext()"));
|
||||
assertFalse(helper.contains("getSurfaceBiome("));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stackBiomesAreVisibleButExcludedFromStructureReachability() throws IOException {
|
||||
String source = Files.readString(Path.of(
|
||||
System.getProperty("iris.moddedCommonSources"),
|
||||
"art/arcane/iris/modded/IrisModdedBiomeSource.java"));
|
||||
|
||||
assertTrue(source.contains("return biomeKeySets().structureRequired()"));
|
||||
assertTrue(source.contains("generatedBiomeKeys.addAll(keys.visibleRequired())"));
|
||||
assertTrue(source.contains("collectStructureBiomeKeys("));
|
||||
assertTrue(source.contains("dimension.getReachableBiomes(() -> data)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void biomeLocatorSearchesTheVisibleStack() throws IOException {
|
||||
String source = Files.readString(Path.of(
|
||||
System.getProperty("iris.moddedCommonSources"),
|
||||
"art/arcane/iris/modded/IrisModdedBiomeSource.java"));
|
||||
int start = source.indexOf("public Pair<BlockPos, Holder<Biome>> findClosestBiome3d(");
|
||||
int end = source.indexOf("private Holder<Biome> getNoiseBiome(", start);
|
||||
String method = source.substring(start, end);
|
||||
|
||||
assertTrue(method.contains("modded_locate_visible_biome"));
|
||||
assertTrue(method.contains("return super.findClosestBiome3d("));
|
||||
assertTrue(method.contains("collectVisibleBiomeHolders()"));
|
||||
assertTrue(method.contains("QuartPos.toBlock(quartX), QuartPos.toBlock(quartZ)"));
|
||||
assertTrue(method.contains("resolveVisibleBiome("));
|
||||
assertFalse(method.contains("Holder<Biome> structureBiome = getNoiseBiome("));
|
||||
assertTrue(method.indexOf("tryAcquireGenerationLease(engine, \"modded_locate_visible_biome\")")
|
||||
< method.indexOf("engine.getDimensionStackContext()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void surfaceSpawnBiomeUsesTheVisibleQuartCell() throws IOException {
|
||||
String source = Files.readString(Path.of(
|
||||
System.getProperty("iris.moddedCommonSources"),
|
||||
"art/arcane/iris/modded/IrisModdedBiomeSource.java"));
|
||||
int start = source.indexOf("Holder<Biome> getVisibleSurfaceBiome(");
|
||||
int end = source.indexOf("boolean isStructureReachable(", start);
|
||||
String method = source.substring(start, end);
|
||||
|
||||
assertTrue(method.contains("QuartPos.fromBlock(blockX)"));
|
||||
assertTrue(method.contains("QuartPos.toBlock(quartX)"));
|
||||
assertTrue(method.contains("QuartPos.fromBlock(internalY + engine.getMinHeight())"));
|
||||
assertTrue(method.contains("stackContext.getLayout(sampleX, sampleZ).surfaceLayer()"));
|
||||
assertTrue(method.contains("visibleBiomeCache"));
|
||||
assertTrue(method.contains("isBiomeCacheable(engine, sampleX, sampleZ)"));
|
||||
assertTrue(method.indexOf("boolean cacheable = isBiomeCacheable(engine, sampleX, sampleZ)")
|
||||
< method.indexOf("Engine.hostHeight(engine, sampleX, sampleZ, true)"));
|
||||
assertCacheGuarded(method, "visibleBiomeCache");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void temporaryNaturalBiomeAnswersAreNotMemoized() throws IOException {
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.answersFromNaturalTerrain(12, -8)).thenReturn(true);
|
||||
assertFalse(IrisModdedBiomeSource.isBiomeCacheable(engine, 12, -8));
|
||||
when(engine.answersFromNaturalTerrain(12, -8)).thenReturn(false);
|
||||
assertTrue(IrisModdedBiomeSource.isBiomeCacheable(engine, 12, -8));
|
||||
|
||||
String source = Files.readString(Path.of(
|
||||
System.getProperty("iris.moddedCommonSources"),
|
||||
"art/arcane/iris/modded/IrisModdedBiomeSource.java"));
|
||||
int structureStart = source.indexOf("private Holder<Biome> getNoiseBiome(Engine engine");
|
||||
int structureEnd = source.indexOf("Holder<Biome> getVisibleNoiseBiome(", structureStart);
|
||||
String structure = source.substring(structureStart, structureEnd);
|
||||
assertTrue(structure.contains("QuartPos.toBlock(quartX)"));
|
||||
assertTrue(structure.contains("QuartPos.toBlock(quartZ)"));
|
||||
assertCacheGuarded(structure, "structureBiomeCache");
|
||||
|
||||
int visibleStart = source.indexOf(
|
||||
"private Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(");
|
||||
int visibleEnd = source.indexOf("Holder<Biome> getVisibleSurfaceBiome(", visibleStart);
|
||||
String visible = source.substring(visibleStart, visibleEnd);
|
||||
assertTrue(visible.contains("QuartPos.toBlock(quartX)"));
|
||||
assertTrue(visible.contains("QuartPos.toBlock(quartZ)"));
|
||||
assertCacheGuarded(visible, "visibleBiomeCache");
|
||||
|
||||
int predicateStart = source.indexOf("static boolean isBiomeCacheable(");
|
||||
int predicateEnd = source.indexOf("static boolean isMonumentSurfaceBiomeQuery(", predicateStart);
|
||||
String predicate = source.substring(predicateStart, predicateEnd);
|
||||
assertTrue(predicate.contains("return !engine.answersFromNaturalTerrain(blockX, blockZ)"));
|
||||
assertFalse(predicate.contains("getDimensionStackContext()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void importedFeaturesCollectOnlyHostOwnedStackCells() throws IOException {
|
||||
String source = Files.readString(Path.of(
|
||||
System.getProperty("iris.moddedCommonSources"),
|
||||
"art/arcane/iris/modded/ModdedImportedFeatureStage.java"));
|
||||
|
||||
assertTrue(source.contains("stackContext.getLayerAt("));
|
||||
assertTrue(source.contains("layer.terrainContext().isSelfReferencing()"));
|
||||
assertTrue(source.contains("section.getBiomes().get(quartX, quartY, quartZ)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configuredBiomeKeysContainOnlyPackDerivativesAndCustomBiomes() {
|
||||
IrisBiome ocean = new IrisBiome()
|
||||
@@ -132,6 +273,21 @@ public class IrisModdedStructureParityTest {
|
||||
"minecraft:the_void", "overworld:aurora"), keys);
|
||||
assertFalse(keys.contains("minecraft:desert"));
|
||||
assertFalse(keys.contains("minecraft:plains"));
|
||||
|
||||
Set<String> recursiveKeys = ModdedDimensionMetadata.collectConfiguredBiomeKeys(
|
||||
List.of(custom), "Layers/Sky");
|
||||
assertTrue(recursiveKeys.contains("layers:sky/aurora"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void biomeWriterDerivativeFallbackMatchesCanonicalRecursiveKeys() throws IOException {
|
||||
String source = Files.readString(Path.of(
|
||||
System.getProperty("iris.moddedCommonSources"),
|
||||
"art/arcane/iris/modded/ModdedBiomeWriter.java"));
|
||||
|
||||
assertTrue(source.contains(
|
||||
"key.equalsIgnoreCase(dimension.getCustomBiomeKey(custom.getId()))"));
|
||||
assertFalse(source.contains("String dimensionLoadKey = key.substring(0, colon)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -159,6 +315,19 @@ public class IrisModdedStructureParityTest {
|
||||
assertSame(configured, IrisModdedBiomeSource.requireConfiguredStructureBiomeKeys(configured));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void liveReplacementRequiresTheSameStructureBiomeUniverse() {
|
||||
IrisModdedChunkGenerator.requireStructureBiomeUniverseCompatible(
|
||||
Set.of("minecraft:plains", "minecraft:beach"),
|
||||
Set.of("minecraft:beach", "minecraft:plains"));
|
||||
|
||||
IllegalArgumentException error = assertThrows(IllegalArgumentException.class,
|
||||
() -> IrisModdedChunkGenerator.requireStructureBiomeUniverseCompatible(
|
||||
Set.of("minecraft:plains"), Set.of("minecraft:desert")));
|
||||
|
||||
assertTrue(error.getMessage().contains("Restart the server"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configuredDimensionMetadataIsExactBeforeEngineBinding() {
|
||||
IrisDimension dimension = new IrisDimension()
|
||||
@@ -240,4 +409,20 @@ public class IrisModdedStructureParityTest {
|
||||
assertSame(MantleFlag.INITIAL_SPAWNED_MARKER, ModdedWorldManager.INITIAL_SPAWN_COMPLETION_FLAG);
|
||||
}
|
||||
|
||||
private static void assertCacheGuarded(String method, String cacheName) {
|
||||
int predicate = method.indexOf("boolean cacheable = isBiomeCacheable(");
|
||||
int cache = method.indexOf("BiomeHolderTable cache = " + cacheName, predicate);
|
||||
int read = method.indexOf("cache.get(", predicate);
|
||||
int readGuard = method.lastIndexOf("if (cacheable)", read);
|
||||
int write = method.indexOf("cache.put(", read);
|
||||
int writeGuard = method.lastIndexOf("if (cacheable)", write);
|
||||
|
||||
assertTrue(predicate >= 0);
|
||||
assertTrue(cache >= 0);
|
||||
assertTrue(read > predicate);
|
||||
assertTrue(readGuard >= predicate);
|
||||
assertTrue(write > read);
|
||||
assertTrue(writeGuard > read);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+24
-2
@@ -135,6 +135,24 @@ public class ModdedNativeStructureWorldgenAccessTest {
|
||||
assertEquals(1, recording.blockTicks.count());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void virtualizedHeightsHideInRegionTerrainFromTheDelegate() {
|
||||
RecordingDelegate recording = new RecordingDelegate();
|
||||
ChunkPos generationCenter = generationCenter();
|
||||
ModdedNativeStructureWorldgenAccess access = ModdedNativeStructureWorldgenAccess.create(
|
||||
recording.world(), generationCenter,
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
true,
|
||||
position -> false);
|
||||
int x = generationCenter.getMiddleBlockX();
|
||||
int z = generationCenter.getMiddleBlockZ();
|
||||
|
||||
assertEquals(SURFACE_FIRST_FREE_Y, access.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z));
|
||||
assertEquals(FLOOR_FIRST_FREE_Y, access.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z));
|
||||
assertEquals(0, recording.heightReads);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void protectedCellsRemainReadableButRejectWritesTilesAndTicks() {
|
||||
RecordingDelegate recording = new RecordingDelegate();
|
||||
@@ -142,7 +160,9 @@ public class ModdedNativeStructureWorldgenAccessTest {
|
||||
ModdedNativeStructureWorldgenAccess access = ModdedNativeStructureWorldgenAccess.create(
|
||||
recording.world(), generationCenter(),
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y, position::equals);
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
false,
|
||||
position::equals);
|
||||
|
||||
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
|
||||
assertFalse(access.ensureCanWrite(position));
|
||||
@@ -211,7 +231,9 @@ public class ModdedNativeStructureWorldgenAccessTest {
|
||||
return ModdedNativeStructureWorldgenAccess.create(
|
||||
recording.world(), generationCenter(),
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y, position -> false);
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
false,
|
||||
position -> false);
|
||||
}
|
||||
|
||||
private static ChunkPos generationCenter() {
|
||||
|
||||
+5
-2
@@ -95,8 +95,9 @@ public class NativeStructureFailureContractTest {
|
||||
assertTrue(placement.contains("heightmapStarts.add(start);"));
|
||||
assertTrue(placement.indexOf("WorldgenTerrainHeightmaps.primeStructurePlacement(")
|
||||
< placement.indexOf("prepareSurfaceStructures"));
|
||||
assertTrue(source.contains("generationEngine.getHeight(x, z, false) + runtimeMinY + 1"));
|
||||
assertTrue(source.contains("generationEngine.getHeight(x, z, true) + runtimeMinY + 1"));
|
||||
assertTrue(source.contains("Engine.hostHeight(generationEngine, x, z, false) + runtimeMinY + 1"));
|
||||
assertTrue(source.contains("Engine.hostHeight(generationEngine, x, z, true) + runtimeMinY + 1"));
|
||||
assertTrue(placement.contains("current.getDimensionStackContext() != null"));
|
||||
assertTrue(source.contains("int minY = chunk.getMinY() + 1;"));
|
||||
}
|
||||
|
||||
@@ -108,6 +109,8 @@ public class NativeStructureFailureContractTest {
|
||||
String placement = source.substring(placementStart, placementEnd);
|
||||
|
||||
assertTrue(placement.contains("ModdedNativeStructureWorldgenAccess.create("));
|
||||
assertTrue(placement.contains("nativeStructureProtection(current, staticObjects)"));
|
||||
assertTrue(placement.contains("Engine.hostHeight(current, x, z, true)"));
|
||||
assertTrue(placement.contains("placeVanillaStructure(boundedWorld, structureManager"));
|
||||
assertFalse(placement.contains("NativeStructurePostProcessor.place(world,"));
|
||||
assertTrue(placement.contains("world.setCurrentlyGenerating(null);"));
|
||||
|
||||
+19
@@ -1,10 +1,12 @@
|
||||
package art.arcane.iris.modded.service;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.modded.ModdedScheduler;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
@@ -26,4 +28,21 @@ public class ModdedStudioHotloadServiceTest {
|
||||
assertFalse(ModdedStudioHotloadService.hasDatapackImports(null));
|
||||
assertFalse(ModdedStudioHotloadService.hasDatapackImports(List.of(empty)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reportsTheCapturedServerThread() throws InterruptedException {
|
||||
ModdedScheduler scheduler = new ModdedScheduler();
|
||||
scheduler.reset();
|
||||
try {
|
||||
ModdedStudioHotloadService service = new ModdedStudioHotloadService();
|
||||
assertTrue(service.isMainThread());
|
||||
AtomicBoolean offThread = new AtomicBoolean(true);
|
||||
Thread worker = new Thread(() -> offThread.set(service.isMainThread()));
|
||||
worker.start();
|
||||
worker.join();
|
||||
assertFalse(offThread.get());
|
||||
} finally {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,9 +222,45 @@ public final class IrisDatapackCompiler {
|
||||
new LinkedHashSet<>(),
|
||||
requiredDimension,
|
||||
requiredFixer);
|
||||
collectDimensionStackRegistryRequirements(
|
||||
requirements,
|
||||
requiredDimension,
|
||||
requiredFixer);
|
||||
return Map.copyOf(requirements);
|
||||
}
|
||||
|
||||
private static void collectDimensionStackRegistryRequirements(
|
||||
Map<String, String> requirements,
|
||||
IrisDimension hostDimension,
|
||||
IDataFixer fixer
|
||||
) throws IOException {
|
||||
if (!hostDimension.hasDimensionStack()) {
|
||||
return;
|
||||
}
|
||||
IrisData data = hostDimension.getLoader();
|
||||
if (data == null) {
|
||||
throw new IOException("Dimension stack host '" + hostDimension.getLoadKey()
|
||||
+ "' has no pack loader");
|
||||
}
|
||||
LinkedHashSet<String> visited = new LinkedHashSet<>();
|
||||
visited.add(hostDimension.getLoadKey());
|
||||
for (String dimensionKey : hostDimension.getDimensionStack().getDimensions()) {
|
||||
if (dimensionKey == null || dimensionKey.isBlank() || !visited.add(dimensionKey)) {
|
||||
continue;
|
||||
}
|
||||
IrisDimension sourceDimension = data.getDimensionLoader().load(dimensionKey);
|
||||
if (sourceDimension == null) {
|
||||
throw new IOException("Dimension stack host '" + hostDimension.getLoadKey()
|
||||
+ "' references missing dimension '" + dimensionKey + "'");
|
||||
}
|
||||
collectRegistryRequirements(
|
||||
requirements,
|
||||
new LinkedHashSet<>(),
|
||||
sourceDimension,
|
||||
fixer);
|
||||
}
|
||||
}
|
||||
|
||||
private static void collectRegistryRequirements(
|
||||
Map<String, String> requirements,
|
||||
Set<String> installedBiomeKeys,
|
||||
@@ -235,7 +271,6 @@ public final class IrisDatapackCompiler {
|
||||
"dimension_type/iris:" + dimension.getDimensionTypeKey(),
|
||||
fingerprintContent(dimension.getDimensionType().toJson(fixer)));
|
||||
|
||||
String namespace = dimension.getLoadKey().toLowerCase(Locale.ROOT);
|
||||
ContentGate contentGate = dimension.getLoader() == null ? null : dimension.getLoader().getContentGate();
|
||||
for (IrisBiome biome : dimension.getAllBiomes(dimension::getLoader)) {
|
||||
if (biome == null || !biome.isCustom()) {
|
||||
@@ -246,7 +281,7 @@ public final class IrisDatapackCompiler {
|
||||
if (customBiome == null) {
|
||||
continue;
|
||||
}
|
||||
String biomeKey = namespace + ":" + customBiome.getId();
|
||||
String biomeKey = dimension.getCustomBiomeKey(customBiome.getId());
|
||||
if (!installedBiomeKeys.add(biomeKey)) {
|
||||
continue;
|
||||
}
|
||||
@@ -533,7 +568,7 @@ public final class IrisDatapackCompiler {
|
||||
|| !Files.isDirectory(dimensions, LinkOption.NOFOLLOW_LINKS)) {
|
||||
return false;
|
||||
}
|
||||
try (Stream<Path> stream = Files.list(dimensions)) {
|
||||
try (Stream<Path> stream = Files.walk(dimensions)) {
|
||||
return stream.anyMatch(path -> !Files.isSymbolicLink(path)
|
||||
&& Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)
|
||||
&& path.getFileName().toString().endsWith(".json"));
|
||||
|
||||
@@ -1121,7 +1121,7 @@ public class ServerConfigurator {
|
||||
for (IrisBiome i : dimension.getAllBiomes(dimension::getLoader)) {
|
||||
if (i.isCustom()) {
|
||||
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
|
||||
keys.add(dimension.getLoadKey() + ":" + j.getId());
|
||||
keys.add(dimension.getCustomBiomeKey(j.getId()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,8 @@ public class MythicMobsDataProvider extends ExternalDataProvider implements List
|
||||
if (access == null) return false;
|
||||
Engine engine = access.getEngine();
|
||||
if (engine == null) return false;
|
||||
IrisRegion region = engine.getRegion(target.getBlockX(), target.getBlockZ());
|
||||
IrisRegion region = engine.getRegion(
|
||||
target.getBlockX(), target.getBlockY() - engine.getMinHeight(), target.getBlockZ());
|
||||
return regions.contains(region.getLoadKey());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,6 +156,9 @@ public interface INMSBinding {
|
||||
return NativeStructureVolume.NONE;
|
||||
}
|
||||
|
||||
default void invalidateNativeStructureVolumeIndex(Engine engine) {
|
||||
}
|
||||
|
||||
int getBiomeId(Biome biome);
|
||||
|
||||
MCABiomeContainer newBiomeContainer(int min, int max, int[] data);
|
||||
|
||||
@@ -26,7 +26,9 @@ import art.arcane.volmlib.util.json.JSONObject;
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
final class PackDimensionValidator {
|
||||
private PackDimensionValidator() {
|
||||
@@ -35,9 +37,14 @@ final class PackDimensionValidator {
|
||||
static void validateDimensions(File packFolder, File[] dimensionFiles, List<String> blockingErrors, List<String> warnings) {
|
||||
File regionsFolder = new File(packFolder, "regions");
|
||||
File biomesFolder = new File(packFolder, "biomes");
|
||||
File dimensionsFolder = new File(packFolder, PackValidator.DIMENSIONS_FOLDER);
|
||||
Set<String> dimensionKeys = new LinkedHashSet<>();
|
||||
for (File dimensionFile : dimensionFiles) {
|
||||
dimensionKeys.add(PackValidationIo.deriveKey(dimensionsFolder, dimensionFile));
|
||||
}
|
||||
|
||||
for (File dimFile : dimensionFiles) {
|
||||
String dimensionKey = PackValidationIo.stripExtension(dimFile.getName());
|
||||
String dimensionKey = PackValidationIo.deriveKey(dimensionsFolder, dimFile);
|
||||
JSONObject dimJson;
|
||||
try {
|
||||
dimJson = new JSONObject(Files.readString(dimFile.toPath(), StandardCharsets.UTF_8));
|
||||
@@ -50,6 +57,7 @@ final class PackDimensionValidator {
|
||||
validateDimensionHeights(packFolder, dimensionKey, dimJson, blockingErrors);
|
||||
validateWorldBoundary(dimensionKey, dimJson, blockingErrors);
|
||||
validateStaticObjects(packFolder, dimensionKey, dimJson, blockingErrors);
|
||||
validateDimensionStack(packFolder, dimensionKey, dimJson, dimensionKeys, blockingErrors);
|
||||
|
||||
JSONArray regionsArray = dimJson.optJSONArray("regions");
|
||||
if (regionsArray == null || regionsArray.length() == 0) {
|
||||
@@ -94,6 +102,108 @@ final class PackDimensionValidator {
|
||||
}
|
||||
}
|
||||
|
||||
static void validateDimensionStack(File packFolder, String dimensionKey, JSONObject dimension, Set<String> dimensionKeys,
|
||||
List<String> blockingErrors) {
|
||||
if (!dimension.has("dimensionStack")) {
|
||||
return;
|
||||
}
|
||||
String context = "Dimension '" + dimensionKey + "' dimensionStack";
|
||||
JSONObject stack = dimension.optJSONObject("dimensionStack");
|
||||
if (stack == null) {
|
||||
blockingErrors.add(context + " must be an object.");
|
||||
return;
|
||||
}
|
||||
|
||||
String upperDimension = dimension.optString("upperDimension", "none");
|
||||
if (upperDimension != null && !upperDimension.isEmpty()
|
||||
&& !upperDimension.equalsIgnoreCase("none")) {
|
||||
blockingErrors.add(context + " cannot be used with upperDimension.");
|
||||
}
|
||||
|
||||
JSONArray dimensions = stack.optJSONArray("dimensions");
|
||||
if (dimensions == null) {
|
||||
blockingErrors.add(context + ".dimensions must be an array.");
|
||||
} else {
|
||||
int hostOccurrences = 0;
|
||||
if (dimensions.length() < 2) {
|
||||
blockingErrors.add(context + ".dimensions must contain at least two dimension keys.");
|
||||
}
|
||||
for (int index = 0; index < dimensions.length(); index++) {
|
||||
Object rawKey = dimensions.opt(index);
|
||||
if (!(rawKey instanceof String key) || key.isBlank()) {
|
||||
blockingErrors.add(context + ".dimensions[" + index + "] must be a nonblank dimension key.");
|
||||
continue;
|
||||
}
|
||||
if (!dimensionKeys.contains(key)) {
|
||||
blockingErrors.add(context + ".dimensions[" + index + "] references missing dimension '"
|
||||
+ key + "'.");
|
||||
}
|
||||
if (dimensionKey.equals(key)) {
|
||||
hostOccurrences++;
|
||||
}
|
||||
}
|
||||
if (hostOccurrences != 1) {
|
||||
blockingErrors.add(context + ".dimensions must contain its host dimension key '"
|
||||
+ dimensionKey + "' exactly once.");
|
||||
}
|
||||
if (dimensions.length() > 0) {
|
||||
Object rawLast = dimensions.opt(dimensions.length() - 1);
|
||||
if (rawLast instanceof String lastKey && !dimensionKey.equals(lastKey)) {
|
||||
blockingErrors.add(context + ".dimensions must end with its host dimension key '"
|
||||
+ dimensionKey + "'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validateInteger(stack, "spacer", 0, 256, context, blockingErrors);
|
||||
validateDimensionStackBlend(packFolder, context, stack, blockingErrors);
|
||||
}
|
||||
|
||||
private static void validateDimensionStackBlend(File packFolder, String context, JSONObject stack,
|
||||
List<String> blockingErrors) {
|
||||
if (!stack.has("blend")) {
|
||||
return;
|
||||
}
|
||||
JSONObject blend = stack.optJSONObject("blend");
|
||||
if (blend == null) {
|
||||
blockingErrors.add(context + ".blend must be an object.");
|
||||
return;
|
||||
}
|
||||
if (blend.has("amplitude")) {
|
||||
validateInteger(blend, "amplitude", 0, 256, context + ".blend", blockingErrors);
|
||||
}
|
||||
if (blend.has("style")) {
|
||||
Object rawStyle = blend.opt("style");
|
||||
boolean styleObject = rawStyle instanceof JSONObject;
|
||||
boolean styleSnippet = rawStyle instanceof String reference
|
||||
&& reference.startsWith("snippet/style/");
|
||||
if (!styleObject && !styleSnippet) {
|
||||
blockingErrors.add(context + ".blend.style must be an object or style snippet reference.");
|
||||
return;
|
||||
}
|
||||
if (styleSnippet) {
|
||||
validateDimensionStackStyleSnippet(
|
||||
packFolder, context + ".blend.style", (String) rawStyle, blockingErrors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateDimensionStackStyleSnippet(File packFolder, String context, String reference,
|
||||
List<String> blockingErrors) {
|
||||
try {
|
||||
File snippetRoot = new File(packFolder, "snippet/style").getCanonicalFile();
|
||||
File snippetFile = new File(packFolder, reference + ".json").getCanonicalFile();
|
||||
if (!snippetFile.toPath().startsWith(snippetRoot.toPath()) || !snippetFile.isFile()) {
|
||||
blockingErrors.add(context + " references missing style snippet '" + reference + "'.");
|
||||
return;
|
||||
}
|
||||
new JSONObject(Files.readString(snippetFile.toPath(), StandardCharsets.UTF_8));
|
||||
} catch (Throwable error) {
|
||||
blockingErrors.add(context + " references invalid style snippet '" + reference + "': "
|
||||
+ error.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
static void validateStaticObjects(File packFolder, String dimensionKey, JSONObject dimension,
|
||||
List<String> blockingErrors) {
|
||||
if (!dimension.has("staticObjects")) {
|
||||
|
||||
@@ -18,11 +18,16 @@
|
||||
|
||||
package art.arcane.iris.core.pack;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisDimensionStack;
|
||||
import art.arcane.iris.engine.object.IrisObjectMarker;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisStaticObject;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Shared key collection for the pack packagers. Both the Bukkit re-serializing compiler and the
|
||||
* modded verbatim-copy compiler must export the complete ambient-spawning graph: object placements
|
||||
@@ -33,6 +38,48 @@ public final class PackExportClosure {
|
||||
private PackExportClosure() {
|
||||
}
|
||||
|
||||
public static KSet<String> collectDimensionKeys(IrisDimension dimension) {
|
||||
return collectDimensionKeys(dimension, key -> dimension == null || dimension.getLoader() == null
|
||||
? null
|
||||
: dimension.getLoader().getDimensionLoader().load(key));
|
||||
}
|
||||
|
||||
static KSet<String> collectDimensionKeys(
|
||||
IrisDimension dimension,
|
||||
Function<String, IrisDimension> resolver
|
||||
) {
|
||||
KSet<String> dimensionKeys = new KSet<>();
|
||||
if (dimension == null || dimension.getLoadKey() == null || dimension.getLoadKey().isBlank()) {
|
||||
return dimensionKeys;
|
||||
}
|
||||
|
||||
ArrayDeque<IrisDimension> pending = new ArrayDeque<>();
|
||||
pending.add(dimension);
|
||||
while (!pending.isEmpty()) {
|
||||
IrisDimension current = pending.removeFirst();
|
||||
String currentKey = current.getLoadKey();
|
||||
if (currentKey == null || currentKey.isBlank() || !dimensionKeys.add(currentKey)) {
|
||||
continue;
|
||||
}
|
||||
IrisDimensionStack stack = current.getDimensionStack();
|
||||
if (stack == null || stack.getDimensions() == null) {
|
||||
continue;
|
||||
}
|
||||
for (String key : stack.getDimensions()) {
|
||||
if (key == null || key.isBlank() || dimensionKeys.contains(key)) {
|
||||
continue;
|
||||
}
|
||||
IrisDimension referenced = resolver.apply(key);
|
||||
if (referenced == null) {
|
||||
dimensionKeys.add(key);
|
||||
} else {
|
||||
pending.addLast(referenced);
|
||||
}
|
||||
}
|
||||
}
|
||||
return dimensionKeys;
|
||||
}
|
||||
|
||||
public static KSet<String> collectMarkerKeys(Iterable<IrisObjectPlacement> placements) {
|
||||
KSet<String> markerKeys = new KSet<>();
|
||||
if (placements == null) {
|
||||
|
||||
@@ -80,12 +80,13 @@ final class PackHydrologyValidator {
|
||||
|
||||
List<File> sortedDimensions = new ArrayList<>(List.of(dimensionFiles));
|
||||
sortedDimensions.sort(Comparator.comparing(File::getPath));
|
||||
File dimensionsFolder = new File(packFolder, PackValidator.DIMENSIONS_FOLDER);
|
||||
for (File dimensionFile : sortedDimensions) {
|
||||
JSONObject dimension = PackValidationIo.readJson(dimensionFile);
|
||||
if (dimension == null) {
|
||||
continue;
|
||||
}
|
||||
String dimensionKey = PackValidationIo.stripExtension(dimensionFile.getName());
|
||||
String dimensionKey = PackValidationIo.deriveKey(dimensionsFolder, dimensionFile);
|
||||
String dimensionPath = "Dimension '" + dimensionKey + "'";
|
||||
JSONObject dimensionPolicy = validatePolicy(
|
||||
dimensionPath,
|
||||
|
||||
@@ -7,6 +7,7 @@ import art.arcane.iris.engine.image.IrisImageMapValidationException;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeGeneratorLink;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisDimensionStack;
|
||||
import art.arcane.iris.engine.object.IrisExpression;
|
||||
import art.arcane.iris.engine.object.IrisGenerator;
|
||||
import art.arcane.iris.engine.object.IrisImage;
|
||||
@@ -217,11 +218,12 @@ final class PackImageMapValidator {
|
||||
Set<String> warnings
|
||||
) {
|
||||
File[] sortedFiles = Arrays.copyOf(dimensionFiles, dimensionFiles.length);
|
||||
Arrays.sort(sortedFiles, (File first, File second) -> first.getName().compareTo(second.getName()));
|
||||
Arrays.sort(sortedFiles, (File first, File second) -> first.getPath().compareTo(second.getPath()));
|
||||
Map<String, IrisDimension> dimensions = new LinkedHashMap<>();
|
||||
Set<String> bindingReadyDimensions = new LinkedHashSet<>();
|
||||
File dimensionsFolder = new File(packFolder, PackValidator.DIMENSIONS_FOLDER);
|
||||
for (File dimensionFile : sortedFiles) {
|
||||
String dimensionKey = PackValidationIo.stripExtension(dimensionFile.getName());
|
||||
String dimensionKey = PackValidationIo.deriveKey(dimensionsFolder, dimensionFile);
|
||||
JSONObject raw = PackValidationIo.readJson(dimensionFile);
|
||||
if (raw == null) {
|
||||
continue;
|
||||
@@ -247,19 +249,35 @@ final class PackImageMapValidator {
|
||||
validateGeneratorStyleReferences(dimensionKey, dimension, data, resources, blockingErrors);
|
||||
}
|
||||
Map<String, List<Map.Entry<String, IrisDimension>>> upperParents = new TreeMap<>();
|
||||
Map<String, List<Map.Entry<String, IrisDimension>>> stackParents = new TreeMap<>();
|
||||
for (Map.Entry<String, IrisDimension> entry : dimensions.entrySet()) {
|
||||
String upperKey = normalized(entry.getValue().getUpperDimension());
|
||||
if (upperKey == null || upperKey.equalsIgnoreCase("none")) {
|
||||
if (upperKey != null && !upperKey.equalsIgnoreCase("none")) {
|
||||
upperParents.computeIfAbsent(upperKey, ignored -> new ArrayList<>()).add(entry);
|
||||
}
|
||||
IrisDimensionStack stack = entry.getValue().getDimensionStack();
|
||||
if (stack == null || stack.getDimensions() == null) {
|
||||
continue;
|
||||
}
|
||||
upperParents.computeIfAbsent(upperKey, ignored -> new ArrayList<>()).add(entry);
|
||||
Set<String> stackKeys = new LinkedHashSet<>();
|
||||
for (String stackKey : stack.getDimensions()) {
|
||||
String normalizedKey = normalized(stackKey);
|
||||
if (normalizedKey == null || normalizedKey.equals(entry.getKey())
|
||||
|| !stackKeys.add(normalizedKey)) {
|
||||
continue;
|
||||
}
|
||||
stackParents.computeIfAbsent(normalizedKey, ignored -> new ArrayList<>()).add(entry);
|
||||
}
|
||||
}
|
||||
for (Map.Entry<String, IrisDimension> entry : dimensions.entrySet()) {
|
||||
if (!bindingReadyDimensions.contains(entry.getKey())) {
|
||||
continue;
|
||||
}
|
||||
List<Map.Entry<String, IrisDimension>> parents = upperParents.get(entry.getKey());
|
||||
if (parents == null || parents.isEmpty()) {
|
||||
List<Map.Entry<String, IrisDimension>> stackHosts = stackParents.get(entry.getKey());
|
||||
boolean hasUpperParents = parents != null && !parents.isEmpty();
|
||||
boolean hasStackHosts = stackHosts != null && !stackHosts.isEmpty();
|
||||
if (!hasUpperParents && !hasStackHosts) {
|
||||
validateBindings(packFolder, "Dimension '" + entry.getKey() + "'", entry.getValue(),
|
||||
entry.getValue().getWorldBoundary(), data, resources, registries, blockingErrors, warnings);
|
||||
continue;
|
||||
@@ -268,12 +286,23 @@ final class PackImageMapValidator {
|
||||
validateBindings(packFolder, "Dimension '" + entry.getKey() + "'", entry.getValue(),
|
||||
entry.getValue().getWorldBoundary(), data, resources, registries, blockingErrors, warnings);
|
||||
}
|
||||
parents.sort(Map.Entry.comparingByKey());
|
||||
for (Map.Entry<String, IrisDimension> parent : parents) {
|
||||
validateBindings(packFolder,
|
||||
"Dimension '" + parent.getKey() + "' upper dimension '" + entry.getKey() + "'",
|
||||
entry.getValue(), parent.getValue().getWorldBoundary(), data, resources, registries,
|
||||
blockingErrors, warnings);
|
||||
if (hasUpperParents) {
|
||||
parents.sort(Map.Entry.comparingByKey());
|
||||
for (Map.Entry<String, IrisDimension> parent : parents) {
|
||||
validateBindings(packFolder,
|
||||
"Dimension '" + parent.getKey() + "' upper dimension '" + entry.getKey() + "'",
|
||||
entry.getValue(), parent.getValue().getWorldBoundary(), data, resources, registries,
|
||||
blockingErrors, warnings);
|
||||
}
|
||||
}
|
||||
if (hasStackHosts) {
|
||||
stackHosts.sort(Map.Entry.comparingByKey());
|
||||
for (Map.Entry<String, IrisDimension> host : stackHosts) {
|
||||
validateBindings(packFolder,
|
||||
"Dimension '" + host.getKey() + "' dimension stack layer '" + entry.getKey() + "'",
|
||||
entry.getValue(), host.getValue().getWorldBoundary(), data, resources, registries,
|
||||
blockingErrors, warnings);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,11 +111,13 @@ public final class PackValidator {
|
||||
return new PackValidationResult(packName, blockingErrors, warnings, validatedAt);
|
||||
}
|
||||
|
||||
File[] dimensionFiles = dimensionsFolder.listFiles(f -> f.isFile() && f.getName().endsWith(".json"));
|
||||
if (dimensionFiles == null || dimensionFiles.length == 0) {
|
||||
List<File> discoveredDimensions = PackValidationIo.listJsonRecursive(dimensionsFolder);
|
||||
if (discoveredDimensions.isEmpty()) {
|
||||
blockingErrors.add("No dimension JSON files under dimensions/.");
|
||||
return new PackValidationResult(packName, blockingErrors, warnings, validatedAt);
|
||||
}
|
||||
discoveredDimensions.sort((first, second) -> first.getPath().compareTo(second.getPath()));
|
||||
File[] dimensionFiles = discoveredDimensions.toArray(File[]::new);
|
||||
|
||||
PackDimensionValidator.validateDimensions(packFolder, dimensionFiles, blockingErrors, warnings);
|
||||
PackImageMapValidator.Validation imageMaps = PackImageMapValidator.validate(
|
||||
@@ -195,7 +197,7 @@ public final class PackValidator {
|
||||
if (!data.getContentGate().ready()) {
|
||||
return List.of();
|
||||
}
|
||||
boolean dimensionExcluded = loadEverything(data, dimensionFiles);
|
||||
boolean dimensionExcluded = loadEverything(packFolder, data, dimensionFiles);
|
||||
List<CompatFinding> findings = data.getCompatReport().findings();
|
||||
applyCompatFindings(minecraftVersion, dimensionExcluded, findings, blockingErrors, warnings);
|
||||
return findings;
|
||||
@@ -219,11 +221,12 @@ public final class PackValidator {
|
||||
*
|
||||
* @return true when any dimension in the pack cannot generate on this server
|
||||
*/
|
||||
private static boolean loadEverything(IrisData data, File[] dimensionFiles) {
|
||||
private static boolean loadEverything(File packFolder, IrisData data, File[] dimensionFiles) {
|
||||
boolean dimensionExcluded = false;
|
||||
File dimensionsFolder = new File(packFolder, DIMENSIONS_FOLDER);
|
||||
for (File dimensionFile : dimensionFiles) {
|
||||
IrisDimension dimension = data.getDimensionLoader().load(
|
||||
PackValidationIo.stripExtension(dimensionFile.getName()), false);
|
||||
PackValidationIo.deriveKey(dimensionsFolder, dimensionFile), false);
|
||||
if (dimension == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBlockData;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisEntity;
|
||||
import art.arcane.iris.engine.object.IrisExpression;
|
||||
import art.arcane.iris.engine.object.IrisGenerator;
|
||||
import art.arcane.iris.engine.object.IrisLootTable;
|
||||
import art.arcane.iris.core.pack.PackExportClosure;
|
||||
@@ -106,11 +107,19 @@ public class IrisPackageCompiler {
|
||||
throw new IllegalStateException("Failed to create structure package staging folder " + folder.getAbsolutePath());
|
||||
}
|
||||
IrisLogging.info("Packaging Dimension " + dimension.getName() + " " + (obfuscate ? "(Obfuscated)" : ""));
|
||||
KList<IrisDimension> dimensions = new KList<>();
|
||||
for (String dimensionKey : PackExportClosure.collectDimensionKeys(dimension).stream().sorted().toList()) {
|
||||
IrisDimension exportedDimension = dm.getDimensionLoader().load(dimensionKey);
|
||||
if (exportedDimension != null) {
|
||||
dimensions.add(exportedDimension);
|
||||
}
|
||||
}
|
||||
KSet<IrisRegion> regions = new KSet<>();
|
||||
KSet<IrisBiome> biomes = new KSet<>();
|
||||
KSet<IrisEntity> entities = new KSet<>();
|
||||
KSet<IrisSpawner> spawners = new KSet<>();
|
||||
KSet<IrisGenerator> generators = new KSet<>();
|
||||
KSet<IrisExpression> expressions = new KSet<>();
|
||||
KSet<IrisLootTable> loot = new KSet<>();
|
||||
KSet<IrisBlockData> blocks = new KSet<>();
|
||||
|
||||
@@ -119,22 +128,30 @@ public class IrisPackageCompiler {
|
||||
for (String i : dm.getBlockLoader().getPossibleKeys()) {
|
||||
addIfPresent(blocks, dm.getBlockLoader().load(i));
|
||||
}
|
||||
for (String key : dm.getExpressionLoader().getPossibleKeys()) {
|
||||
addIfPresent(expressions, dm.getExpressionLoader().load(key));
|
||||
}
|
||||
|
||||
dimension.getAllRegions(() -> dm).forEach((region) -> addIfPresent(regions, region));
|
||||
dimension.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i)));
|
||||
for (IrisDimension exportedDimension : dimensions) {
|
||||
exportedDimension.getAllRegions(() -> dm).forEach((region) -> addIfPresent(regions, region));
|
||||
exportedDimension.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i)));
|
||||
exportedDimension.getReachableBiomes(() -> dm).forEach((biome) -> addIfPresent(biomes, biome));
|
||||
exportedDimension.getEntitySpawners().forEach((sp) ->
|
||||
addIfPresent(spawners, dm.getSpawnerLoader().load(sp)));
|
||||
}
|
||||
regions.forEach((i) -> biomes.addAll(i.getAllBiomes(() -> dm)));
|
||||
dimension.getReachableBiomes(() -> dm).forEach((biome) -> addIfPresent(biomes, biome));
|
||||
regions.forEach((r) -> r.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i))));
|
||||
regions.forEach((r) -> r.getEntitySpawners().forEach((sp) -> addIfPresent(spawners, dm.getSpawnerLoader().load(sp))));
|
||||
dimension.getEntitySpawners().forEach((sp) -> addIfPresent(spawners, dm.getSpawnerLoader().load(sp)));
|
||||
biomes.forEach((i) -> i.getGenerators().forEach((j) -> addIfPresent(generators, j.getCachedGenerator(() -> dm))));
|
||||
biomes.forEach((r) -> r.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i))));
|
||||
biomes.forEach((r) -> r.getEntitySpawners().forEach((sp) -> addIfPresent(spawners, dm.getSpawnerLoader().load(sp))));
|
||||
KList<IrisObjectPlacement> allPlacements = new KList<>();
|
||||
regions.forEach((r) -> allPlacements.addAll(r.getObjects()));
|
||||
biomes.forEach((i) -> allPlacements.addAll(i.getObjects()));
|
||||
for (IrisStaticObject placement : dimension.getStaticObjects()) {
|
||||
allPlacements.add(placement.toPlacement());
|
||||
for (IrisDimension exportedDimension : dimensions) {
|
||||
for (IrisStaticObject placement : exportedDimension.getStaticObjects()) {
|
||||
allPlacements.add(placement.toPlacement());
|
||||
}
|
||||
}
|
||||
KSet<IrisMarker> markers = new KSet<>();
|
||||
for (String markerKey : PackExportClosure.collectMarkerKeys(allPlacements)) {
|
||||
@@ -148,7 +165,9 @@ public class IrisPackageCompiler {
|
||||
collectSpawnerEntityKeys(spawners).forEach((i) -> addIfPresent(entities, dm.getEntityLoader().load(i)));
|
||||
entities.forEach((e) -> e.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i))));
|
||||
Set<String> structureKeys = new LinkedHashSet<>();
|
||||
collectStructureKeys(structureKeys, dimension.getStructures());
|
||||
for (IrisDimension exportedDimension : dimensions) {
|
||||
collectStructureKeys(structureKeys, exportedDimension.getStructures());
|
||||
}
|
||||
regions.forEach((region) -> collectStructureKeys(structureKeys, region.getStructures()));
|
||||
biomes.forEach((biome) -> collectStructureKeys(structureKeys, biome.getStructures()));
|
||||
KMap<String, String> renameObjects = new KMap<>();
|
||||
@@ -176,8 +195,10 @@ public class IrisPackageCompiler {
|
||||
j.setPlace(newNames);
|
||||
}
|
||||
|
||||
for (IrisStaticObject placement : dimension.getStaticObjects()) {
|
||||
placement.setObject(renameObjects.get(placement.getObject()));
|
||||
for (IrisDimension exportedDimension : dimensions) {
|
||||
for (IrisStaticObject placement : exportedDimension.getStaticObjects()) {
|
||||
placement.setObject(renameObjects.get(placement.getObject()));
|
||||
}
|
||||
}
|
||||
|
||||
KMap<String, KList<String>> lookupObjects = renameObjects.flip();
|
||||
@@ -230,9 +251,12 @@ public class IrisPackageCompiler {
|
||||
}
|
||||
b.append(structureClosure.writeTo(folder, minify));
|
||||
b.append(ImageMapPackageClosure.writeAll(dm, folder, minify));
|
||||
a = new JSONObject(new Gson().toJson(dimension)).toString(minify ? 0 : 4);
|
||||
IO.writeAll(new File(folder, "dimensions/" + dimension.getLoadKey() + ".json"), a);
|
||||
b.append(IO.hash(a));
|
||||
for (IrisDimension exportedDimension : dimensions.stream()
|
||||
.sorted(Comparator.comparing(IrisDimension::getLoadKey)).toList()) {
|
||||
a = new JSONObject(new Gson().toJson(exportedDimension)).toString(minify ? 0 : 4);
|
||||
IO.writeAll(new File(folder, "dimensions/" + exportedDimension.getLoadKey() + ".json"), a);
|
||||
b.append(IO.hash(a));
|
||||
}
|
||||
|
||||
for (IrisGenerator i : generators) {
|
||||
a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4);
|
||||
@@ -240,6 +264,13 @@ public class IrisPackageCompiler {
|
||||
b.append(IO.hash(a));
|
||||
}
|
||||
|
||||
for (IrisExpression i : expressions.stream()
|
||||
.sorted(Comparator.comparing(IrisExpression::getLoadKey)).toList()) {
|
||||
a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4);
|
||||
IO.writeAll(new File(folder, "expressions/" + i.getLoadKey() + ".json"), a);
|
||||
b.append(IO.hash(a));
|
||||
}
|
||||
|
||||
c.append(IO.hash(b.toString()));
|
||||
b = new StringBuilder();
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ package art.arcane.iris.core.project;
|
||||
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
@@ -179,19 +180,58 @@ public final class IrisProjectCopier {
|
||||
}
|
||||
|
||||
private static void transformDimension(Path stage, String sourceKey, String targetKey) throws IOException {
|
||||
Path oldDimension = stage.resolve("dimensions").resolve(sourceKey + ".json");
|
||||
Path newDimension = stage.resolve("dimensions").resolve(targetKey + ".json");
|
||||
Path dimensionsRoot = stage.resolve("dimensions");
|
||||
Path oldDimension = dimensionsRoot.resolve(sourceKey + ".json");
|
||||
Path newDimension = dimensionsRoot.resolve(targetKey + ".json");
|
||||
if (!oldDimension.equals(newDimension)) {
|
||||
Files.move(oldDimension, newDimension);
|
||||
}
|
||||
|
||||
JSONObject json = new JSONObject(IO.readAll(newDimension.toFile()));
|
||||
if (json.has("name")) {
|
||||
json.put("name", Form.capitalizeWords(targetKey.replace('-', ' ')));
|
||||
IO.writeAll(newDimension.toFile(), json.toString(4));
|
||||
try (Stream<Path> files = Files.walk(dimensionsRoot)) {
|
||||
for (Path dimensionFile : files.filter(path -> Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS))
|
||||
.filter(path -> path.getFileName().toString().endsWith(".json"))
|
||||
.sorted()
|
||||
.toList()) {
|
||||
JSONObject json = new JSONObject(IO.readAll(dimensionFile.toFile()));
|
||||
boolean changed = false;
|
||||
if (dimensionFile.equals(newDimension) && json.has("name")) {
|
||||
json.put("name", Form.capitalizeWords(targetKey.replace('-', ' ')));
|
||||
changed = true;
|
||||
}
|
||||
if (rewriteDimensionStackReferences(json, sourceKey, targetKey)) {
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
IO.writeAll(dimensionFile.toFile(), json.toString(4));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean rewriteDimensionStackReferences(JSONObject json, String sourceKey, String targetKey) {
|
||||
JSONObject stack = json.optJSONObject("dimensionStack");
|
||||
JSONArray dimensions = stack == null ? null : stack.optJSONArray("dimensions");
|
||||
if (dimensions == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean changed = false;
|
||||
JSONArray rewritten = new JSONArray();
|
||||
for (int index = 0; index < dimensions.length(); index++) {
|
||||
Object value = dimensions.opt(index);
|
||||
if (sourceKey.equals(value)) {
|
||||
rewritten.put(targetKey);
|
||||
changed = true;
|
||||
} else {
|
||||
rewritten.put(value == null ? JSONObject.NULL : value);
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
stack.put("dimensions", rewritten);
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private static void publish(Path stage, Path target) throws IOException {
|
||||
try {
|
||||
Files.move(stage, target, StandardCopyOption.ATOMIC_MOVE);
|
||||
|
||||
@@ -515,7 +515,7 @@ public class BoardSVC implements IrisService, BoardProvider {
|
||||
}
|
||||
|
||||
lines.add("&7&m ");
|
||||
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_REGION, MessageArgument.untrusted("region", engine.getRegion(x, z).getName())));
|
||||
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_REGION, MessageArgument.untrusted("region", engine.getRegion(x, y, z).getName())));
|
||||
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_BIOME, MessageArgument.untrusted("biome", engine.getBiomeOrMantle(x, y, z).getName())));
|
||||
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_HEIGHT, MessageArgument.trusted("height", Math.round(engine.getHeight(x, z)))));
|
||||
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_SLOPE, MessageArgument.trusted("slope", Form.f(engine.getSlope(x, z), 2))));
|
||||
|
||||
@@ -326,7 +326,10 @@ public class TreeSVC implements IrisService {
|
||||
|
||||
// Add more or find any in the region
|
||||
if (isUseAll || placements.isEmpty()) {
|
||||
IrisRegion region = worldAccess.getEngine().getRegion(location.getBlockX(), location.getBlockZ());
|
||||
IrisRegion region = worldAccess.getEngine().getRegion(
|
||||
location.getBlockX(),
|
||||
location.getBlockY() - worldAccess.getTarget().getWorld().minHeight(),
|
||||
location.getBlockZ());
|
||||
placements.addAll(matchObjectPlacements(region.getObjects(), size, type, worldAccess.getData()));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
package art.arcane.iris.engine;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisDimensionStack;
|
||||
import art.arcane.iris.engine.object.IrisDimensionStackBlend;
|
||||
import art.arcane.iris.engine.object.IrisGeneratorStyle;
|
||||
import art.arcane.iris.util.project.noise.CNG;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public final class DimensionStackContext {
|
||||
private static final int LAYOUT_CACHE_SIZE = 65536;
|
||||
private static final long SEAM_SALT = 0x44494D535441434BL;
|
||||
private static final long HASH_OFFSET = 0xCBF29CE484222325L;
|
||||
private static final long HASH_PRIME = 0x100000001B3L;
|
||||
|
||||
private final Engine engine;
|
||||
private final int outputHeight;
|
||||
private final int spacer;
|
||||
private final List<DimensionTerrainContext> layersTopToBottom;
|
||||
private final List<DimensionTerrainContext> layersBottomToTop;
|
||||
private final List<SeamOffsetSampler> seamOffsetSamplersBottomToTop;
|
||||
private final Cache<Long, DimensionStackLayout> layoutCache;
|
||||
|
||||
private DimensionStackContext(ContextState state) {
|
||||
engine = state.engine();
|
||||
outputHeight = state.outputHeight();
|
||||
spacer = state.spacer();
|
||||
layersBottomToTop = List.copyOf(state.layersBottomToTop());
|
||||
ArrayList<DimensionTerrainContext> reversed = new ArrayList<>(layersBottomToTop);
|
||||
Collections.reverse(reversed);
|
||||
layersTopToBottom = List.copyOf(reversed);
|
||||
seamOffsetSamplersBottomToTop = List.copyOf(state.seamOffsetSamplersBottomToTop());
|
||||
layoutCache = Caffeine.newBuilder().maximumSize(LAYOUT_CACHE_SIZE).build();
|
||||
}
|
||||
|
||||
public static DimensionStackContext create(Engine engine, IrisDimensionStack stack) {
|
||||
if (stack == null) {
|
||||
throw new IllegalArgumentException("Dimension stack configuration is required");
|
||||
}
|
||||
KList<String> dimensionKeys = stack.getDimensions();
|
||||
validateRuntimeContract(engine.getDimension(), dimensionKeys);
|
||||
if (stack.getSpacer() < 0 || stack.getSpacer() > 256) {
|
||||
throw new IllegalArgumentException("Dimension stack spacer must be between 0 and 256");
|
||||
}
|
||||
|
||||
ArrayList<DimensionTerrainContext> layersBottomToTop = new ArrayList<>(dimensionKeys.size());
|
||||
for (int keyIndex = dimensionKeys.size() - 1; keyIndex >= 0; keyIndex--) {
|
||||
String dimensionKey = dimensionKeys.get(keyIndex);
|
||||
IrisDimension dimension = dimensionKey.equals(engine.getDimension().getLoadKey())
|
||||
? engine.getDimension()
|
||||
: engine.getData().getDimensionLoader().load(dimensionKey);
|
||||
if (dimension == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Dimension stack reference does not resolve in this pack: " + dimensionKey);
|
||||
}
|
||||
layersBottomToTop.add(DimensionTerrainContext.forStack(engine, dimension));
|
||||
}
|
||||
|
||||
List<SeamOffsetSampler> seamSamplers = createSeamSamplers(
|
||||
engine,
|
||||
stack.getBlend(),
|
||||
layersBottomToTop
|
||||
);
|
||||
return new DimensionStackContext(new ContextState(
|
||||
engine,
|
||||
engine.getHeight(),
|
||||
stack.getSpacer(),
|
||||
layersBottomToTop,
|
||||
seamSamplers
|
||||
));
|
||||
}
|
||||
|
||||
private static void validateRuntimeContract(IrisDimension host, KList<String> dimensionKeys) {
|
||||
if (dimensionKeys == null || dimensionKeys.size() < 2) {
|
||||
throw new IllegalArgumentException("Dimension stack requires at least two dimensions");
|
||||
}
|
||||
String hostKey = host.getLoadKey();
|
||||
if (!hostKey.equals(dimensionKeys.get(dimensionKeys.size() - 1))) {
|
||||
throw new IllegalArgumentException("Dimension stack host must be its final bottom dimension");
|
||||
}
|
||||
int hostReferences = 0;
|
||||
for (String dimensionKey : dimensionKeys) {
|
||||
if (dimensionKey == null || dimensionKey.isBlank()) {
|
||||
throw new IllegalArgumentException("Dimension stack keys must be nonblank");
|
||||
}
|
||||
if (hostKey.equals(dimensionKey)) {
|
||||
hostReferences++;
|
||||
}
|
||||
}
|
||||
if (hostReferences != 1) {
|
||||
throw new IllegalArgumentException("Dimension stack must contain its host exactly once");
|
||||
}
|
||||
}
|
||||
|
||||
private static List<SeamOffsetSampler> createSeamSamplers(
|
||||
Engine engine,
|
||||
IrisDimensionStackBlend blend,
|
||||
List<DimensionTerrainContext> layersBottomToTop
|
||||
) {
|
||||
int seamCount = layersBottomToTop.size() - 1;
|
||||
ArrayList<SeamOffsetSampler> samplers = new ArrayList<>(seamCount);
|
||||
if (blend == null || blend.getAmplitude() == 0) {
|
||||
for (int seamIndex = 0; seamIndex < seamCount; seamIndex++) {
|
||||
samplers.add((x, z) -> 0);
|
||||
}
|
||||
return samplers;
|
||||
}
|
||||
if (blend.getAmplitude() < 0 || blend.getAmplitude() > 256) {
|
||||
throw new IllegalArgumentException("Dimension stack blend amplitude must be between 0 and 256");
|
||||
}
|
||||
IrisGeneratorStyle style = blend.getStyle();
|
||||
if (style == null) {
|
||||
throw new IllegalArgumentException("Dimension stack blend style is required when amplitude is positive");
|
||||
}
|
||||
|
||||
for (int seamIndex = 0; seamIndex < seamCount; seamIndex++) {
|
||||
DimensionTerrainContext lower = layersBottomToTop.get(seamIndex);
|
||||
DimensionTerrainContext upper = layersBottomToTop.get(seamIndex + 1);
|
||||
long salt = seamSalt(
|
||||
seamIndex,
|
||||
lower.getDimension().getLoadKey(),
|
||||
upper.getDimension().getLoadKey()
|
||||
);
|
||||
RNG seamRng = new RNG(engine.getSeedManager().getTerrain() ^ salt);
|
||||
CNG seamNoise = style.create(seamRng, engine.getData(), engine);
|
||||
int amplitude = blend.getAmplitude();
|
||||
samplers.add((x, z) -> clampSeamOffset(
|
||||
seamNoise.fitDouble(-amplitude, amplitude, x, z), amplitude));
|
||||
}
|
||||
return samplers;
|
||||
}
|
||||
|
||||
static int clampSeamOffset(double sampledOffset, int amplitude) {
|
||||
if (!Double.isFinite(sampledOffset)) {
|
||||
return 0;
|
||||
}
|
||||
double clamped = Math.max(-amplitude, Math.min(amplitude, sampledOffset));
|
||||
return (int) Math.round(clamped);
|
||||
}
|
||||
|
||||
static long seamSalt(int seamIndex, String lowerDimensionKey, String upperDimensionKey) {
|
||||
long hash = HASH_OFFSET ^ SEAM_SALT;
|
||||
hash = hashString(hash, lowerDimensionKey);
|
||||
hash ^= seamIndex;
|
||||
hash *= HASH_PRIME;
|
||||
return hashString(hash, upperDimensionKey);
|
||||
}
|
||||
|
||||
private static long hashString(long hash, String value) {
|
||||
long result = hash;
|
||||
for (int characterIndex = 0; characterIndex < value.length(); characterIndex++) {
|
||||
result ^= value.charAt(characterIndex);
|
||||
result *= HASH_PRIME;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public DimensionStackLayout sample(int x, int z) {
|
||||
return sample(x, z, usesTemporaryNaturalFallback(x, z));
|
||||
}
|
||||
|
||||
private DimensionStackLayout sample(int x, int z, boolean naturalFallback) {
|
||||
ArrayList<DimensionStackLayout.LayerInput> layerInputs =
|
||||
new ArrayList<>(layersBottomToTop.size());
|
||||
for (DimensionTerrainContext terrainContext : layersBottomToTop) {
|
||||
DimensionTerrainContext.ColumnSample column = terrainContext.sampleColumn(
|
||||
x, z, naturalFallback);
|
||||
int normalTerrainHeight = localCoordinate(
|
||||
column.terrainHeight(),
|
||||
terrainContext.getLocalHeight()
|
||||
);
|
||||
int fluidHeight = localCoordinate(
|
||||
column.fluidHeight(),
|
||||
terrainContext.getLocalHeight()
|
||||
);
|
||||
layerInputs.add(new DimensionStackLayout.LayerInput(
|
||||
terrainContext,
|
||||
column.biome(),
|
||||
column.region(),
|
||||
column.rockBlock(),
|
||||
column.fluidBlock(),
|
||||
column.surfaceBlock(),
|
||||
normalTerrainHeight,
|
||||
fluidHeight
|
||||
));
|
||||
}
|
||||
|
||||
int[] seamOffsets = new int[seamOffsetSamplersBottomToTop.size()];
|
||||
for (int seamIndex = 0; seamIndex < seamOffsets.length; seamIndex++) {
|
||||
seamOffsets[seamIndex] = seamOffsetSamplersBottomToTop.get(seamIndex).sample(x, z);
|
||||
}
|
||||
return DimensionStackLayout.create(outputHeight, spacer, layerInputs, seamOffsets);
|
||||
}
|
||||
|
||||
private static int localCoordinate(double sample, int localHeight) {
|
||||
int maximum = Math.max(0, localHeight - 1);
|
||||
long rounded = Math.round(sample);
|
||||
return (int) Math.max(0L, Math.min(maximum, rounded));
|
||||
}
|
||||
|
||||
public int getStackTerrainHeight(int x, int z) {
|
||||
return (int) (sampleTopHeights(x, z) >>> 32);
|
||||
}
|
||||
|
||||
public int getStackTopHeight(int x, int z) {
|
||||
return (int) sampleTopHeights(x, z);
|
||||
}
|
||||
|
||||
public DimensionStackLayout getLayout(int x, int z) {
|
||||
boolean naturalFallback = usesTemporaryNaturalFallback(x, z);
|
||||
long columnKey = ((long) x << 32) ^ (z & 0xFFFFFFFFL);
|
||||
return resolveLayout(
|
||||
layoutCache,
|
||||
columnKey,
|
||||
naturalFallback,
|
||||
() -> sample(x, z, naturalFallback)
|
||||
);
|
||||
}
|
||||
|
||||
public DimensionStackLayout.Layer getLayerAt(int x, int y, int z) {
|
||||
return getLayout(x, z).layerAt(y);
|
||||
}
|
||||
|
||||
static DimensionStackLayout resolveLayout(
|
||||
Cache<Long, DimensionStackLayout> cache,
|
||||
long columnKey,
|
||||
boolean naturalFallback,
|
||||
Supplier<DimensionStackLayout> sampler
|
||||
) {
|
||||
if (naturalFallback) {
|
||||
return sampler.get();
|
||||
}
|
||||
return cache.get(columnKey, ignored -> sampler.get());
|
||||
}
|
||||
|
||||
private long sampleTopHeights(int x, int z) {
|
||||
boolean naturalFallback = usesTemporaryNaturalFallback(x, z);
|
||||
int baseY = 0;
|
||||
int renderedTerrainTopY = Integer.MIN_VALUE;
|
||||
int renderedContentTopY = Integer.MIN_VALUE;
|
||||
int outputMaxY = outputHeight - 1;
|
||||
int previousContentTopY = Integer.MIN_VALUE;
|
||||
int previousRenderMinY = Integer.MIN_VALUE;
|
||||
int previousRenderMaxY = Integer.MIN_VALUE;
|
||||
int previousSurfaceY = Integer.MIN_VALUE;
|
||||
boolean previousVisible = false;
|
||||
for (int layerIndex = 0; layerIndex < layersBottomToTop.size(); layerIndex++) {
|
||||
DimensionTerrainContext terrainContext = layersBottomToTop.get(layerIndex);
|
||||
if (layerIndex > 0) {
|
||||
int gapMinY = (int) Math.max(0L, (long) previousContentTopY + 1L);
|
||||
int gapMaxY = (int) Math.min((long) outputMaxY, (long) baseY - 1L);
|
||||
int previousTerrainY = previousVisible && previousSurfaceY >= previousRenderMinY
|
||||
? Math.min(outputMaxY, previousSurfaceY)
|
||||
: Integer.MIN_VALUE;
|
||||
renderedTerrainTopY = DimensionStackLayout.eraseHeightThroughGap(
|
||||
renderedTerrainTopY,
|
||||
gapMinY,
|
||||
gapMaxY,
|
||||
previousTerrainY
|
||||
);
|
||||
renderedContentTopY = DimensionStackLayout.eraseHeightThroughGap(
|
||||
renderedContentTopY,
|
||||
gapMinY,
|
||||
gapMaxY,
|
||||
previousVisible ? previousRenderMaxY : Integer.MIN_VALUE
|
||||
);
|
||||
}
|
||||
int localTerrainHeight = localCoordinate(
|
||||
terrainContext.getNormalTerrainHeight(x, z, naturalFallback),
|
||||
terrainContext.getLocalHeight()
|
||||
);
|
||||
int localFluidHeight = localCoordinate(
|
||||
terrainContext.getFluidHeight(x, z, naturalFallback),
|
||||
terrainContext.getLocalHeight()
|
||||
);
|
||||
int surfaceY = saturatedAdd(baseY, localTerrainHeight);
|
||||
int fluidY = saturatedAdd(baseY, localFluidHeight);
|
||||
int layerContentTopY = Math.max(surfaceY, fluidY);
|
||||
int renderMinY = Math.max(0, baseY);
|
||||
int renderMaxY = Math.min(outputMaxY, layerContentTopY);
|
||||
if (renderMinY <= renderMaxY) {
|
||||
renderedContentTopY = Math.max(renderedContentTopY, renderMaxY);
|
||||
renderedTerrainTopY = DimensionStackLayout.mergeVisibleTerrainHeight(
|
||||
renderedTerrainTopY,
|
||||
renderMinY,
|
||||
renderMaxY,
|
||||
surfaceY,
|
||||
outputMaxY
|
||||
);
|
||||
}
|
||||
previousContentTopY = layerContentTopY;
|
||||
previousRenderMinY = renderMinY;
|
||||
previousRenderMaxY = renderMaxY;
|
||||
previousSurfaceY = surfaceY;
|
||||
previousVisible = renderMinY <= renderMaxY;
|
||||
if (layerIndex + 1 < layersBottomToTop.size()) {
|
||||
int seamOffset = seamOffsetSamplersBottomToTop.get(layerIndex).sample(x, z);
|
||||
baseY = saturatedAdd(
|
||||
layerContentTopY,
|
||||
saturatedAdd(spacer, saturatedAdd(1, seamOffset))
|
||||
);
|
||||
}
|
||||
}
|
||||
int clippedTerrainTopY = Math.max(0, renderedTerrainTopY);
|
||||
int clippedContentTopY = Math.max(0, renderedContentTopY);
|
||||
return ((long) clippedTerrainTopY << 32) | (clippedContentTopY & 0xFFFFFFFFL);
|
||||
}
|
||||
|
||||
private static int saturatedAdd(int left, int right) {
|
||||
long sum = (long) left + right;
|
||||
if (sum > Integer.MAX_VALUE) {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
if (sum < Integer.MIN_VALUE) {
|
||||
return Integer.MIN_VALUE;
|
||||
}
|
||||
return (int) sum;
|
||||
}
|
||||
|
||||
public int getOutputHeight() {
|
||||
return outputHeight;
|
||||
}
|
||||
|
||||
public int getSpacer() {
|
||||
return spacer;
|
||||
}
|
||||
|
||||
public boolean usesTemporaryNaturalFallback(int x, int z) {
|
||||
return engine.answersFromNaturalTerrain(x, z);
|
||||
}
|
||||
|
||||
public List<DimensionTerrainContext> getLayersTopToBottom() {
|
||||
return layersTopToBottom;
|
||||
}
|
||||
|
||||
public List<DimensionTerrainContext> getLayersBottomToTop() {
|
||||
return layersBottomToTop;
|
||||
}
|
||||
|
||||
private interface SeamOffsetSampler {
|
||||
int sample(double x, double z);
|
||||
}
|
||||
|
||||
private record ContextState(
|
||||
Engine engine,
|
||||
int outputHeight,
|
||||
int spacer,
|
||||
List<DimensionTerrainContext> layersBottomToTop,
|
||||
List<SeamOffsetSampler> seamOffsetSamplersBottomToTop
|
||||
) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
package art.arcane.iris.engine;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class DimensionStackLayout {
|
||||
private final List<Layer> layersBottomToTop;
|
||||
private final List<Layer> layersTopToBottom;
|
||||
private final int stackTerrainTopY;
|
||||
private final int stackTopY;
|
||||
private final int clippedStackTerrainTopY;
|
||||
private final int clippedStackTopY;
|
||||
|
||||
private DimensionStackLayout(LayoutState state) {
|
||||
layersBottomToTop = List.copyOf(state.layersBottomToTop());
|
||||
ArrayList<Layer> reversed = new ArrayList<>(layersBottomToTop);
|
||||
Collections.reverse(reversed);
|
||||
layersTopToBottom = List.copyOf(reversed);
|
||||
stackTerrainTopY = state.stackTerrainTopY();
|
||||
stackTopY = state.stackTopY();
|
||||
clippedStackTerrainTopY = state.clippedStackTerrainTopY();
|
||||
clippedStackTopY = state.clippedStackTopY();
|
||||
}
|
||||
|
||||
static DimensionStackLayout create(
|
||||
int outputHeight,
|
||||
int spacer,
|
||||
List<LayerInput> layerInputsBottomToTop,
|
||||
int[] seamOffsetsBottomToTop
|
||||
) {
|
||||
if (outputHeight < 1) {
|
||||
throw new IllegalArgumentException("Output height must be positive");
|
||||
}
|
||||
if (spacer < 0) {
|
||||
throw new IllegalArgumentException("Dimension stack spacer cannot be negative");
|
||||
}
|
||||
if (layerInputsBottomToTop.isEmpty()) {
|
||||
throw new IllegalArgumentException("Dimension stack requires at least one layer");
|
||||
}
|
||||
if (seamOffsetsBottomToTop.length != layerInputsBottomToTop.size() - 1) {
|
||||
throw new IllegalArgumentException("Dimension stack seam offset count does not match its layers");
|
||||
}
|
||||
|
||||
int outputMaxY = outputHeight - 1;
|
||||
int baseY = 0;
|
||||
int stackTerrainTopY = Integer.MIN_VALUE;
|
||||
int stackTopY = Integer.MIN_VALUE;
|
||||
int renderedTerrainTopY = Integer.MIN_VALUE;
|
||||
int renderedContentTopY = Integer.MIN_VALUE;
|
||||
ArrayList<Layer> layers = new ArrayList<>(layerInputsBottomToTop.size());
|
||||
for (int layerIndex = 0; layerIndex < layerInputsBottomToTop.size(); layerIndex++) {
|
||||
LayerInput input = layerInputsBottomToTop.get(layerIndex);
|
||||
int seamOffsetBelow = layerIndex == 0 ? 0 : seamOffsetsBottomToTop[layerIndex - 1];
|
||||
int spacerBelow = layerIndex == 0 ? 0 : spacer;
|
||||
if (layerIndex > 0) {
|
||||
Layer lower = layers.get(layerIndex - 1);
|
||||
int gapMinY = (int) Math.max(0L, (long) lower.contentTopY() + 1L);
|
||||
int gapMaxY = (int) Math.min((long) outputMaxY, (long) baseY - 1L);
|
||||
renderedTerrainTopY = eraseHeightThroughGap(
|
||||
renderedTerrainTopY,
|
||||
gapMinY,
|
||||
gapMaxY,
|
||||
visibleTerrainY(lower)
|
||||
);
|
||||
renderedContentTopY = eraseHeightThroughGap(
|
||||
renderedContentTopY,
|
||||
gapMinY,
|
||||
gapMaxY,
|
||||
lower.visible() ? lower.renderMaxY() : Integer.MIN_VALUE
|
||||
);
|
||||
}
|
||||
int surfaceY = saturatedAdd(baseY, input.normalTerrainHeight());
|
||||
int fluidY = saturatedAdd(baseY, input.fluidHeight());
|
||||
int contentTopY = Math.max(surfaceY, fluidY);
|
||||
int renderMinY = Math.max(0, baseY);
|
||||
int renderMaxY = Math.min(outputMaxY, contentTopY);
|
||||
boolean visible = renderMinY <= renderMaxY;
|
||||
Layer layer = new Layer(
|
||||
input.terrainContext(),
|
||||
input.biome(),
|
||||
input.region(),
|
||||
input.rockBlock(),
|
||||
input.fluidBlock(),
|
||||
input.surfaceBlock(),
|
||||
baseY,
|
||||
input.normalTerrainHeight(),
|
||||
input.fluidHeight(),
|
||||
surfaceY,
|
||||
fluidY,
|
||||
contentTopY,
|
||||
spacerBelow,
|
||||
seamOffsetBelow,
|
||||
renderMinY,
|
||||
renderMaxY,
|
||||
clip(surfaceY, outputMaxY),
|
||||
clip(fluidY, outputMaxY),
|
||||
visible
|
||||
);
|
||||
layers.add(layer);
|
||||
stackTerrainTopY = Math.max(stackTerrainTopY, surfaceY);
|
||||
stackTopY = Math.max(stackTopY, contentTopY);
|
||||
if (visible) {
|
||||
renderedContentTopY = Math.max(renderedContentTopY, renderMaxY);
|
||||
renderedTerrainTopY = mergeVisibleTerrainHeight(
|
||||
renderedTerrainTopY,
|
||||
renderMinY,
|
||||
renderMaxY,
|
||||
surfaceY,
|
||||
outputMaxY
|
||||
);
|
||||
}
|
||||
if (layerIndex + 1 < layerInputsBottomToTop.size()) {
|
||||
baseY = saturatedAdd(
|
||||
contentTopY,
|
||||
saturatedAdd(spacer, saturatedAdd(1, seamOffsetsBottomToTop[layerIndex]))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return new DimensionStackLayout(new LayoutState(
|
||||
layers,
|
||||
stackTerrainTopY,
|
||||
stackTopY,
|
||||
Math.max(0, renderedTerrainTopY),
|
||||
Math.max(0, renderedContentTopY)
|
||||
));
|
||||
}
|
||||
|
||||
private static int saturatedAdd(int left, int right) {
|
||||
long sum = (long) left + right;
|
||||
if (sum > Integer.MAX_VALUE) {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
if (sum < Integer.MIN_VALUE) {
|
||||
return Integer.MIN_VALUE;
|
||||
}
|
||||
return (int) sum;
|
||||
}
|
||||
|
||||
private static int clip(int value, int outputMaxY) {
|
||||
return Math.max(0, Math.min(outputMaxY, value));
|
||||
}
|
||||
|
||||
static int mergeVisibleTerrainHeight(
|
||||
int currentTerrainTopY,
|
||||
int renderMinY,
|
||||
int renderMaxY,
|
||||
int surfaceY,
|
||||
int outputMaxY
|
||||
) {
|
||||
int terrainTopY = currentTerrainTopY;
|
||||
if (terrainTopY >= renderMinY && terrainTopY <= renderMaxY) {
|
||||
terrainTopY = Integer.MIN_VALUE;
|
||||
}
|
||||
if (surfaceY >= renderMinY) {
|
||||
terrainTopY = Math.max(terrainTopY, Math.min(outputMaxY, surfaceY));
|
||||
}
|
||||
return terrainTopY;
|
||||
}
|
||||
|
||||
static int eraseHeightThroughGap(
|
||||
int currentTopY,
|
||||
int gapMinY,
|
||||
int gapMaxY,
|
||||
int fallbackTopY
|
||||
) {
|
||||
if (currentTopY >= gapMinY && currentTopY <= gapMaxY) {
|
||||
return fallbackTopY;
|
||||
}
|
||||
return currentTopY;
|
||||
}
|
||||
|
||||
private static int visibleTerrainY(Layer layer) {
|
||||
if (!layer.visible() || layer.surfaceY() < layer.renderMinY()) {
|
||||
return Integer.MIN_VALUE;
|
||||
}
|
||||
return layer.clippedSurfaceY();
|
||||
}
|
||||
|
||||
public List<Layer> layersBottomToTop() {
|
||||
return layersBottomToTop;
|
||||
}
|
||||
|
||||
public List<Layer> layersTopToBottom() {
|
||||
return layersTopToBottom;
|
||||
}
|
||||
|
||||
public int stackTerrainTopY() {
|
||||
return stackTerrainTopY;
|
||||
}
|
||||
|
||||
public int stackTopY() {
|
||||
return stackTopY;
|
||||
}
|
||||
|
||||
public int clippedStackTerrainTopY() {
|
||||
return clippedStackTerrainTopY;
|
||||
}
|
||||
|
||||
public int clippedStackTopY() {
|
||||
return clippedStackTopY;
|
||||
}
|
||||
|
||||
public Layer topTerrainLayer() {
|
||||
Layer highest = null;
|
||||
int highestSurfaceY = Integer.MIN_VALUE;
|
||||
for (int layerIndex = 0; layerIndex < layersBottomToTop.size(); layerIndex++) {
|
||||
Layer layer = layersBottomToTop.get(layerIndex);
|
||||
if (layerIndex > 0) {
|
||||
Layer lower = layersBottomToTop.get(layerIndex - 1);
|
||||
if ((long) highestSurfaceY > lower.contentTopY()
|
||||
&& (long) highestSurfaceY < layer.localBaseY()) {
|
||||
highest = visibleTerrainY(lower) == Integer.MIN_VALUE ? null : lower;
|
||||
highestSurfaceY = visibleTerrainY(lower);
|
||||
}
|
||||
}
|
||||
if (layer.containsRenderedY(highestSurfaceY)) {
|
||||
highest = null;
|
||||
highestSurfaceY = Integer.MIN_VALUE;
|
||||
}
|
||||
if (!layer.visible() || layer.surfaceY() < layer.renderMinY()) {
|
||||
continue;
|
||||
}
|
||||
if (layer.clippedSurfaceY() >= highestSurfaceY) {
|
||||
highest = layer;
|
||||
highestSurfaceY = layer.clippedSurfaceY();
|
||||
}
|
||||
}
|
||||
return highest;
|
||||
}
|
||||
|
||||
public Layer surfaceLayer() {
|
||||
Layer terrainLayer = topTerrainLayer();
|
||||
return terrainLayer == null ? topVisibleLayer() : terrainLayer;
|
||||
}
|
||||
|
||||
private Layer topVisibleLayer() {
|
||||
Layer highest = null;
|
||||
int highestContentY = Integer.MIN_VALUE;
|
||||
for (int layerIndex = 0; layerIndex < layersBottomToTop.size(); layerIndex++) {
|
||||
Layer layer = layersBottomToTop.get(layerIndex);
|
||||
if (layerIndex > 0) {
|
||||
Layer lower = layersBottomToTop.get(layerIndex - 1);
|
||||
if ((long) highestContentY > lower.contentTopY()
|
||||
&& (long) highestContentY < layer.localBaseY()) {
|
||||
highest = lower.visible() ? lower : null;
|
||||
highestContentY = lower.visible()
|
||||
? lower.renderMaxY()
|
||||
: Integer.MIN_VALUE;
|
||||
}
|
||||
}
|
||||
if (layer.visible() && layer.renderMaxY() >= highestContentY) {
|
||||
highest = layer;
|
||||
highestContentY = layer.renderMaxY();
|
||||
}
|
||||
}
|
||||
return highest;
|
||||
}
|
||||
|
||||
public Layer layerAt(int y) {
|
||||
Layer bottom = layersBottomToTop.get(0);
|
||||
Layer owner = bottom;
|
||||
for (int layerIndex = 1; layerIndex < layersBottomToTop.size(); layerIndex++) {
|
||||
Layer lower = layersBottomToTop.get(layerIndex - 1);
|
||||
Layer layer = layersBottomToTop.get(layerIndex);
|
||||
if ((long) y > lower.contentTopY() && (long) y < layer.localBaseY()) {
|
||||
owner = bottom;
|
||||
}
|
||||
if (layer.containsRenderedY(y)) {
|
||||
owner = layer;
|
||||
}
|
||||
}
|
||||
return owner;
|
||||
}
|
||||
|
||||
public boolean containsUpperLayerY(int y) {
|
||||
for (int layerIndex = 1; layerIndex < layersBottomToTop.size(); layerIndex++) {
|
||||
if (layersBottomToTop.get(layerIndex).containsRenderedY(y)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isHostFeatureProtectedY(int y) {
|
||||
for (int layerIndex = 1; layerIndex < layersBottomToTop.size(); layerIndex++) {
|
||||
Layer lower = layersBottomToTop.get(layerIndex - 1);
|
||||
Layer upper = layersBottomToTop.get(layerIndex);
|
||||
if (upper.containsRenderedY(y)
|
||||
|| ((long) y > lower.contentTopY() && (long) y < upper.localBaseY())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static record LayerInput(
|
||||
DimensionTerrainContext terrainContext,
|
||||
IrisBiome biome,
|
||||
IrisRegion region,
|
||||
PlatformBlockState rockBlock,
|
||||
PlatformBlockState fluidBlock,
|
||||
PlatformBlockState surfaceBlock,
|
||||
int normalTerrainHeight,
|
||||
int fluidHeight
|
||||
) {
|
||||
}
|
||||
|
||||
public record Layer(
|
||||
DimensionTerrainContext terrainContext,
|
||||
IrisBiome biome,
|
||||
IrisRegion region,
|
||||
PlatformBlockState rockBlock,
|
||||
PlatformBlockState fluidBlock,
|
||||
PlatformBlockState surfaceBlock,
|
||||
int localBaseY,
|
||||
int normalTerrainHeight,
|
||||
int fluidHeight,
|
||||
int surfaceY,
|
||||
int fluidY,
|
||||
int contentTopY,
|
||||
int spacerBelow,
|
||||
int seamOffsetBelow,
|
||||
int renderMinY,
|
||||
int renderMaxY,
|
||||
int clippedSurfaceY,
|
||||
int clippedFluidY,
|
||||
boolean visible
|
||||
) {
|
||||
public boolean containsRenderedY(int y) {
|
||||
return visible && y >= renderMinY && y <= renderMaxY;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private record LayoutState(
|
||||
List<Layer> layersBottomToTop,
|
||||
int stackTerrainTopY,
|
||||
int stackTopY,
|
||||
int clippedStackTerrainTopY,
|
||||
int clippedStackTopY
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,764 @@
|
||||
package art.arcane.iris.engine;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.image.IrisImageMapRuntime;
|
||||
import art.arcane.iris.engine.object.InferredType;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisGenerator;
|
||||
import art.arcane.iris.engine.object.IrisInterpolator;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisShapedGeneratorStyle;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.data.DataProvider;
|
||||
import art.arcane.iris.util.project.interpolation.NoiseBounds;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import art.arcane.iris.util.project.stream.interpolation.Interpolated;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class DimensionTerrainContext implements DataProvider {
|
||||
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
|
||||
|
||||
private final Engine engine;
|
||||
private final IrisDimension dimension;
|
||||
private final IrisData data;
|
||||
private final int localHeight;
|
||||
private final ProceduralStream<Double> heightStream;
|
||||
private final ProceduralStream<Double> slopeStream;
|
||||
private final ProceduralStream<Double> fluidHeightStream;
|
||||
private final ProceduralStream<IrisBiome> biomeStream;
|
||||
private final ProceduralStream<IrisRegion> regionStream;
|
||||
private final ProceduralStream<PlatformBlockState> rockStream;
|
||||
private final FluidBlockSampler fluidBlockSampler;
|
||||
private final IrisImageMapRuntime imageMapRuntime;
|
||||
private final boolean selfReferencing;
|
||||
private final SelfFallback selfFallback;
|
||||
|
||||
private DimensionTerrainContext(ContextState state) {
|
||||
engine = state.engine();
|
||||
dimension = state.dimension();
|
||||
data = state.data();
|
||||
localHeight = state.localHeight();
|
||||
heightStream = state.heightStream();
|
||||
slopeStream = state.slopeStream();
|
||||
fluidHeightStream = state.fluidHeightStream();
|
||||
biomeStream = state.biomeStream();
|
||||
regionStream = state.regionStream();
|
||||
rockStream = state.rockStream();
|
||||
fluidBlockSampler = state.fluidBlockSampler();
|
||||
imageMapRuntime = state.imageMapRuntime();
|
||||
selfReferencing = state.selfReferencing();
|
||||
selfFallback = state.selfFallback();
|
||||
}
|
||||
|
||||
public static DimensionTerrainContext forStack(Engine engine, IrisDimension dimension) {
|
||||
boolean selfReferencing = dimension.getLoadKey().equals(engine.getDimension().getLoadKey());
|
||||
if (selfReferencing) {
|
||||
return createSelfReferencing(engine, false);
|
||||
}
|
||||
|
||||
int localHeight = dimension.getMaxHeight() - dimension.getMinHeight();
|
||||
return createCrossReferencing(
|
||||
engine,
|
||||
dimension,
|
||||
localHeight,
|
||||
dimension.getMinHeight(),
|
||||
"stack-" + dimension.getLoadKey(),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
static DimensionTerrainContext forUpper(Engine engine, IrisDimension dimension) {
|
||||
boolean selfReferencing = dimension.getLoadKey().equals(engine.getDimension().getLoadKey());
|
||||
if (selfReferencing) {
|
||||
return createSelfReferencing(engine, true);
|
||||
}
|
||||
|
||||
return createCrossReferencing(
|
||||
engine,
|
||||
dimension,
|
||||
engine.getHeight(),
|
||||
engine.getMinHeight(),
|
||||
"upper",
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
private static DimensionTerrainContext createSelfReferencing(Engine engine, boolean natural) {
|
||||
IrisComplex complex = engine.getComplex();
|
||||
double fluidHeight = engine.getDimension().getFluidHeight();
|
||||
ProceduralStream<Double> fluidHeightStream = natural
|
||||
? ProceduralStream.ofDouble((x, z) -> fluidHeight)
|
||||
: complex.getRiverWaterSurfaceStream();
|
||||
FluidBlockSampler fluidBlockSampler = natural
|
||||
? (x, z) -> complex.getFluidStream().get(x, z)
|
||||
: complex::resolveSurfaceFluid;
|
||||
SelfFallback selfFallback = natural
|
||||
? null
|
||||
: new SelfFallback(
|
||||
complex.getNaturalHeightStream(),
|
||||
complex.getNaturalTrueBiomeStream(),
|
||||
fluidHeight,
|
||||
(x, z) -> complex.getFluidStream().get(x, z)
|
||||
);
|
||||
return new DimensionTerrainContext(new ContextState(
|
||||
engine,
|
||||
engine.getDimension(),
|
||||
engine.getData(),
|
||||
engine.getHeight(),
|
||||
natural ? complex.getNaturalHeightStream() : complex.getHeightStream(),
|
||||
natural ? complex.getNaturalSlopeStream() : complex.getSlopeStream(),
|
||||
fluidHeightStream,
|
||||
natural ? complex.getNaturalTrueBiomeStream() : complex.getTrueBiomeStream(),
|
||||
complex.getRegionStream(),
|
||||
complex.getRockStream(),
|
||||
fluidBlockSampler,
|
||||
complex.getImageMapRuntime(),
|
||||
true,
|
||||
selfFallback
|
||||
));
|
||||
}
|
||||
|
||||
private static DimensionTerrainContext createCrossReferencing(
|
||||
Engine engine,
|
||||
IrisDimension dimension,
|
||||
int localHeight,
|
||||
int minimumWorldHeight,
|
||||
String cachePrefix,
|
||||
boolean includeFluid
|
||||
) {
|
||||
IrisData resolvedData = dimension.getLoader();
|
||||
if (resolvedData == null) {
|
||||
resolvedData = engine.getData();
|
||||
}
|
||||
IrisData dimensionData = resolvedData;
|
||||
IrisImageMapRuntime imageMapRuntime = IrisImageMapRuntime.compile(
|
||||
dimensionData,
|
||||
dimension,
|
||||
minimumWorldHeight
|
||||
);
|
||||
long seedOffset = dimension.getLoadKey().hashCode();
|
||||
RNG rng = new RNG(engine.getSeedManager().getComplex() ^ seedOffset);
|
||||
double fluidHeight = dimension.getFluidHeight();
|
||||
int cacheSize = IrisSettings.get().getPerformance().getNoiseCacheSize();
|
||||
DataProvider dataProvider = () -> dimensionData;
|
||||
IrisBiome configuredFocusBiome = loadFocusBiome(dimension, dimensionData);
|
||||
IrisBiome focusBiome = configuredFocusBiome == null
|
||||
? null
|
||||
: configuredFocusBiome.withInferredType(InferredType.LAND);
|
||||
IrisRegion focusRegion = focusBiome == null
|
||||
? loadFocusRegion(dimension, dimensionData)
|
||||
: findFocusRegion(focusBiome, dimension, dataProvider, dimensionData);
|
||||
|
||||
Map<IrisInterpolator, Set<IrisGenerator>> generators = new HashMap<>();
|
||||
Set<IrisBiome> allBiomes = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
Map<IrisBiome, IrisComplex.ChildSelectionPlan> childSelectionPlans =
|
||||
Collections.synchronizedMap(new IdentityHashMap<>());
|
||||
KList<IrisRegion> preparedRegions = new KList<>();
|
||||
if (focusRegion != null) {
|
||||
preparedRegions.add(focusRegion);
|
||||
focusRegion.getNaturalBiomes(dataProvider).forEach(biome -> registerBiomeGenerators(
|
||||
biome, dataProvider, allBiomes, generators));
|
||||
} else {
|
||||
for (IrisRegion region : dimension.getAllRegions(dataProvider)) {
|
||||
preparedRegions.add(region);
|
||||
region.getNaturalBiomes(dataProvider).forEach(biome -> registerBiomeGenerators(
|
||||
biome, dataProvider, allBiomes, generators));
|
||||
}
|
||||
for (IrisBiome mappedBiome : imageMapRuntime.getMappedBiomes()) {
|
||||
if (!mappedBiome.isCompatExcluded()) {
|
||||
registerBiomeGenerators(mappedBiome, dataProvider, allBiomes, generators);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (focusBiome != null) {
|
||||
allBiomes.add(focusBiome);
|
||||
}
|
||||
|
||||
IrisComplex.GeneratorGroup[] generatorGroups = IrisComplex.freezeGeneratorGroups(generators);
|
||||
Map<IrisInterpolator, IdentityHashMap<IrisBiome, NoiseBounds>> generatorBounds = new HashMap<>();
|
||||
for (IrisComplex.GeneratorGroup group : generatorGroups) {
|
||||
IdentityHashMap<IrisBiome, NoiseBounds> interpolatorBounds =
|
||||
new IdentityHashMap<>(Math.max(allBiomes.size(), 16));
|
||||
for (IrisBiome biome : allBiomes) {
|
||||
double minimum = 0D;
|
||||
double maximum = 0D;
|
||||
for (IrisGenerator generator : group.generators()) {
|
||||
String key = generator.getLoadKey();
|
||||
if (key == null || key.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
maximum += biome.getGenLinkMax(key, engine);
|
||||
minimum += biome.getGenLinkMin(key, engine);
|
||||
}
|
||||
interpolatorBounds.put(biome, new NoiseBounds(minimum, maximum));
|
||||
}
|
||||
generatorBounds.put(group.interpolator(), interpolatorBounds);
|
||||
}
|
||||
|
||||
ProceduralStream<Double> regionStyleStream = dimension.getRegionStyle()
|
||||
.create(rng.nextParallelRNG(883), dimensionData).stream()
|
||||
.zoom(dimension.getRegionZoom());
|
||||
KList<IrisRegion> regionPool = new KList<>();
|
||||
for (IrisRegion region : dimensionData.getRegionLoader().loadAll(dimension.getRegions())) {
|
||||
if (!region.isCompatExcluded()) {
|
||||
regionPool.add(region);
|
||||
}
|
||||
}
|
||||
ProceduralStream<IrisRegion> proceduralRegionStream = focusRegion == null
|
||||
? regionStyleStream.selectRarity(regionPool)
|
||||
: ProceduralStream.of(
|
||||
(x, z) -> focusRegion,
|
||||
Interpolated.of(value -> 0D, value -> focusRegion)
|
||||
);
|
||||
ProceduralStream<IrisRegion> regionStream = focusRegion == null
|
||||
? proceduralRegionStream
|
||||
.convertAware2D((region, x, z) -> mappedRegion(imageMapRuntime, region, x, z))
|
||||
.cache2D(cachePrefix + "ImageMappedRegionStream", engine, cacheSize)
|
||||
: proceduralRegionStream;
|
||||
Map<IrisRegion, Map<InferredType, ProceduralStream<IrisBiome>>> preparedBiomeStreams =
|
||||
compileInferredBiomeStreams(dimension, dimensionData, rng, preparedRegions);
|
||||
|
||||
ProceduralStream<IrisBiome> landBiomeStream = regionStream
|
||||
.convert(region -> IrisComplex.preparedInferredBiomeStream(
|
||||
preparedBiomeStreams, region, InferredType.LAND))
|
||||
.convertAware2D(ProceduralStream::get);
|
||||
ProceduralStream<IrisBiome> seaBiomeStream = regionStream
|
||||
.convert(region -> IrisComplex.preparedInferredBiomeStream(
|
||||
preparedBiomeStreams, region, InferredType.SEA))
|
||||
.convertAware2D(ProceduralStream::get);
|
||||
ProceduralStream<IrisBiome> shoreBiomeStream = regionStream
|
||||
.convert(region -> IrisComplex.preparedInferredBiomeStream(
|
||||
preparedBiomeStreams, region, InferredType.SHORE))
|
||||
.convertAware2D(ProceduralStream::get);
|
||||
|
||||
Map<InferredType, ProceduralStream<IrisBiome>> inferredStreams = new HashMap<>();
|
||||
inferredStreams.put(InferredType.LAND, landBiomeStream);
|
||||
inferredStreams.put(InferredType.SEA, seaBiomeStream);
|
||||
inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
|
||||
|
||||
ProceduralStream<InferredType> bridgeStream = focusBiome == null
|
||||
? dimension.getContinentalStyle()
|
||||
.create(rng.nextParallelRNG(234234565), dimensionData)
|
||||
.bake().scale(1D / dimension.getContinentZoom()).bake().stream()
|
||||
.convert(value -> value >= dimension.getLandChance()
|
||||
? InferredType.SEA
|
||||
: InferredType.LAND)
|
||||
: ProceduralStream.of(
|
||||
(x, z) -> focusBiome.getInferredType(),
|
||||
Interpolated.of(value -> 0D, value -> focusBiome.getInferredType())
|
||||
);
|
||||
|
||||
ProceduralStream<IrisBiome> proceduralBaseBiomeStream = focusBiome == null
|
||||
? bridgeStream
|
||||
.convertAware2D((type, x, z) -> {
|
||||
ProceduralStream<IrisBiome> stream = inferredStreams.get(type);
|
||||
return stream != null
|
||||
? stream.get(x, z)
|
||||
: inferredStreams.get(InferredType.LAND).get(x, z);
|
||||
})
|
||||
.convertAware2D((biome, x, z) -> implode(
|
||||
biome, x, z, rng, dataProvider, childSelectionPlans, 3))
|
||||
: ProceduralStream.of(
|
||||
(x, z) -> focusBiome,
|
||||
Interpolated.of(value -> 0D, value -> focusBiome)
|
||||
);
|
||||
ProceduralStream<IrisBiome> baseBiomeStream = focusBiome == null
|
||||
? proceduralBaseBiomeStream
|
||||
.convertAware2D((biome, x, z) -> mappedBiome(imageMapRuntime, biome, x, z))
|
||||
.cache2D(cachePrefix + "ImageMappedBaseBiomeStream", engine, cacheSize)
|
||||
: proceduralBaseBiomeStream;
|
||||
|
||||
KList<IrisShapedGeneratorStyle> overlayNoise = dimension.getOverlayNoise();
|
||||
ProceduralStream<Double> overlayStream = overlayNoise.isEmpty()
|
||||
? ProceduralStream.ofDouble((x, z) -> 0D)
|
||||
: ProceduralStream.ofDouble((x, z) -> {
|
||||
double value = 0D;
|
||||
for (IrisShapedGeneratorStyle style : overlayNoise) {
|
||||
value += style.get(rng, dimensionData, x, z);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
long heightSeed = engine.getSeedManager().getHeight() ^ seedOffset;
|
||||
ProceduralStream<Double> heightStream = ProceduralStream.of((x, z) -> {
|
||||
IrisBiome biome = baseBiomeStream.get(x, z);
|
||||
if (biome == null) {
|
||||
return mappedTerrainHeight(imageMapRuntime, fluidHeight, x, z);
|
||||
}
|
||||
double interpolatedHeight = 0D;
|
||||
for (IrisComplex.GeneratorGroup group : generatorGroups) {
|
||||
IrisInterpolator interpolator = group.interpolator();
|
||||
IrisGenerator[] groupGenerators = group.generators();
|
||||
if (groupGenerators.length == 0) {
|
||||
continue;
|
||||
}
|
||||
IdentityHashMap<IrisBiome, NoiseBounds> cachedBounds = generatorBounds.get(interpolator);
|
||||
NoiseBounds sampledBounds = interpolator.interpolateBounds(x, z, (sampleX, sampleZ) -> {
|
||||
try {
|
||||
IrisBiome sampledBiome = baseBiomeStream.get(sampleX, sampleZ);
|
||||
if (sampledBiome == null) {
|
||||
return ZERO_NOISE_BOUNDS;
|
||||
}
|
||||
NoiseBounds bounds = cachedBounds == null ? null : cachedBounds.get(sampledBiome);
|
||||
if (bounds != null) {
|
||||
return bounds;
|
||||
}
|
||||
double minimum = 0D;
|
||||
double maximum = 0D;
|
||||
for (IrisGenerator generator : groupGenerators) {
|
||||
String key = generator.getLoadKey();
|
||||
if (key == null || key.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
maximum += sampledBiome.getGenLinkMax(key, engine);
|
||||
minimum += sampledBiome.getGenLinkMin(key, engine);
|
||||
}
|
||||
return new NoiseBounds(minimum, maximum);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
return ZERO_NOISE_BOUNDS;
|
||||
}
|
||||
});
|
||||
interpolatedHeight += IrisComplex.averageGeneratorHeights(
|
||||
groupGenerators,
|
||||
sampledBounds.min(),
|
||||
sampledBounds.max(),
|
||||
x,
|
||||
z,
|
||||
heightSeed + 239945
|
||||
);
|
||||
}
|
||||
double proceduralHeight = Math.max(
|
||||
Math.min(interpolatedHeight + fluidHeight + overlayStream.get(x, z), localHeight),
|
||||
0D
|
||||
);
|
||||
return mappedTerrainHeight(imageMapRuntime, proceduralHeight, x, z);
|
||||
}, Interpolated.DOUBLE).cache2DDouble(
|
||||
cachePrefix + "ImageMappedHeightStream", engine, cacheSize);
|
||||
ProceduralStream<Double> slopeStream = heightStream.slope(3)
|
||||
.cache2DDouble(cachePrefix + "SlopeStream", engine, cacheSize);
|
||||
|
||||
ProceduralStream<IrisBiome> finalBiomeStream = focusBiome == null
|
||||
? heightStream.convertAware2D((height, x, z) -> {
|
||||
IrisBiome mappedBiome = imageMapRuntime.sampleBiome(x, z);
|
||||
if (mappedBiome != null) {
|
||||
return mappedBiome;
|
||||
}
|
||||
IrisBiome baseBiome = baseBiomeStream.get(x, z);
|
||||
IrisBiome resolvedBiome = IrisComplex.resolveSurfaceBiome(
|
||||
height,
|
||||
baseBiome,
|
||||
regionStream.get(x, z),
|
||||
x,
|
||||
z,
|
||||
fluidHeight,
|
||||
landBiomeStream,
|
||||
seaBiomeStream,
|
||||
shoreBiomeStream
|
||||
);
|
||||
return resolvedBiome == baseBiome
|
||||
? baseBiome
|
||||
: implode(resolvedBiome, x, z, rng, dataProvider, childSelectionPlans, 3);
|
||||
}).cache2D(cachePrefix + "ImageMappedFinalBiomeStream", engine, cacheSize)
|
||||
: ProceduralStream.of(
|
||||
(x, z) -> focusBiome,
|
||||
Interpolated.of(value -> 0D, value -> focusBiome)
|
||||
).cache2D(cachePrefix + "FinalBiomeStreamFocus", engine, cacheSize);
|
||||
|
||||
ProceduralStream<PlatformBlockState> rockStream = dimension.getRockPalette()
|
||||
.getLayerGenerator(rng.nextParallelRNG(45), dimensionData).stream()
|
||||
.select(dimension.getRockPalette().getBlockData(dimensionData));
|
||||
FluidBlockSampler fluidBlockSampler;
|
||||
if (includeFluid) {
|
||||
ProceduralStream<PlatformBlockState> fluidStream = dimension.getFluidPalette()
|
||||
.getLayerGenerator(rng.nextParallelRNG(78), dimensionData).stream()
|
||||
.select(dimension.getFluidPalette().getBlockData(dimensionData));
|
||||
fluidBlockSampler = fluidStream::get;
|
||||
} else {
|
||||
fluidBlockSampler = (x, z) -> null;
|
||||
}
|
||||
|
||||
return new DimensionTerrainContext(new ContextState(
|
||||
engine,
|
||||
dimension,
|
||||
dimensionData,
|
||||
localHeight,
|
||||
heightStream,
|
||||
slopeStream,
|
||||
ProceduralStream.ofDouble((x, z) -> fluidHeight),
|
||||
finalBiomeStream,
|
||||
regionStream,
|
||||
rockStream,
|
||||
fluidBlockSampler,
|
||||
imageMapRuntime,
|
||||
false,
|
||||
null
|
||||
));
|
||||
}
|
||||
|
||||
private static void registerBiomeGenerators(
|
||||
IrisBiome biome,
|
||||
DataProvider dataProvider,
|
||||
Set<IrisBiome> allBiomes,
|
||||
Map<IrisInterpolator, Set<IrisGenerator>> generators
|
||||
) {
|
||||
allBiomes.add(biome);
|
||||
biome.getGenerators().forEach(link -> {
|
||||
IrisGenerator generator = link.getCachedGenerator(dataProvider);
|
||||
if (generator != null) {
|
||||
generators.computeIfAbsent(generator.getInterpolator(), key -> new HashSet<>()).add(generator);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static IrisRegion mappedRegion(
|
||||
IrisImageMapRuntime imageMapRuntime,
|
||||
IrisRegion proceduralRegion,
|
||||
double worldX,
|
||||
double worldZ
|
||||
) {
|
||||
IrisRegion mappedRegion = imageMapRuntime.sampleRegion(worldX, worldZ);
|
||||
return mappedRegion == null ? proceduralRegion : mappedRegion;
|
||||
}
|
||||
|
||||
static double mappedTerrainHeight(
|
||||
IrisImageMapRuntime imageMapRuntime,
|
||||
double proceduralHeight,
|
||||
double worldX,
|
||||
double worldZ
|
||||
) {
|
||||
return imageMapRuntime.sampleTerrainHeight(worldX, worldZ, proceduralHeight);
|
||||
}
|
||||
|
||||
static IrisBiome mappedBiome(
|
||||
IrisImageMapRuntime imageMapRuntime,
|
||||
IrisBiome proceduralBiome,
|
||||
double worldX,
|
||||
double worldZ
|
||||
) {
|
||||
IrisBiome mappedBiome = imageMapRuntime.sampleBiome(worldX, worldZ);
|
||||
return mappedBiome == null ? proceduralBiome : mappedBiome;
|
||||
}
|
||||
|
||||
static PlatformBlockState mappedSurfaceBlock(
|
||||
IrisImageMapRuntime imageMapRuntime,
|
||||
PlatformBlockState proceduralBlock,
|
||||
double worldX,
|
||||
double worldZ
|
||||
) {
|
||||
PlatformBlockState mappedBlock = imageMapRuntime.sampleSurfaceBlock(worldX, worldZ);
|
||||
return mappedBlock == null ? proceduralBlock : mappedBlock;
|
||||
}
|
||||
|
||||
private static Map<IrisRegion, Map<InferredType, ProceduralStream<IrisBiome>>> compileInferredBiomeStreams(
|
||||
IrisDimension dimension,
|
||||
IrisData data,
|
||||
RNG rng,
|
||||
Iterable<IrisRegion> regions
|
||||
) {
|
||||
IdentityHashMap<IrisRegion, Map<InferredType, ProceduralStream<IrisBiome>>> compiled =
|
||||
new IdentityHashMap<>();
|
||||
for (IrisRegion region : regions) {
|
||||
if (compiled.containsKey(region)) {
|
||||
continue;
|
||||
}
|
||||
EnumMap<InferredType, ProceduralStream<IrisBiome>> streams = new EnumMap<>(InferredType.class);
|
||||
streams.put(InferredType.LAND, createInferredBiomeStream(
|
||||
dimension, data, rng, region, InferredType.LAND));
|
||||
streams.put(InferredType.SEA, createInferredBiomeStream(
|
||||
dimension, data, rng, region, InferredType.SEA));
|
||||
streams.put(InferredType.SHORE, createInferredBiomeStream(
|
||||
dimension, data, rng, region, InferredType.SHORE));
|
||||
compiled.put(region, Collections.unmodifiableMap(streams));
|
||||
}
|
||||
return Collections.unmodifiableMap(compiled);
|
||||
}
|
||||
|
||||
private static ProceduralStream<IrisBiome> createInferredBiomeStream(
|
||||
IrisDimension dimension,
|
||||
IrisData data,
|
||||
RNG rng,
|
||||
IrisRegion region,
|
||||
InferredType type
|
||||
) {
|
||||
return switch (type) {
|
||||
case LAND -> dimension.getLandBiomeStyle()
|
||||
.create(rng.nextParallelRNG(InferredType.LAND.ordinal()), data).stream()
|
||||
.zoom(dimension.getBiomeZoom())
|
||||
.zoom(dimension.getLandZoom())
|
||||
.zoom(region.getLandBiomeZoom())
|
||||
.selectRarity(loadInferredBiomes(data, region.getLandBiomes(), InferredType.LAND));
|
||||
case SEA -> dimension.getSeaBiomeStyle()
|
||||
.create(rng.nextParallelRNG(InferredType.SEA.ordinal()), data).stream()
|
||||
.zoom(dimension.getBiomeZoom())
|
||||
.zoom(dimension.getSeaZoom())
|
||||
.zoom(region.getSeaBiomeZoom())
|
||||
.selectRarity(loadInferredBiomes(data, region.getSeaBiomes(), InferredType.SEA));
|
||||
case SHORE -> dimension.getShoreBiomeStyle()
|
||||
.create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), data).stream()
|
||||
.zoom(dimension.getBiomeZoom())
|
||||
.zoom(region.getShoreBiomeZoom())
|
||||
.selectRarity(loadInferredBiomes(data, region.getShoreBiomes(), InferredType.SHORE));
|
||||
default -> throw new IllegalArgumentException("Unsupported stacked terrain biome type: " + type);
|
||||
};
|
||||
}
|
||||
|
||||
private static KList<IrisBiome> loadInferredBiomes(
|
||||
IrisData data,
|
||||
KList<String> keys,
|
||||
InferredType type
|
||||
) {
|
||||
KList<IrisBiome> inferredBiomes = new KList<>();
|
||||
for (IrisBiome biome : data.getBiomeLoader().loadAll(keys)) {
|
||||
if (biome.isCompatExcluded()) {
|
||||
continue;
|
||||
}
|
||||
inferredBiomes.add(biome.withInferredType(type));
|
||||
}
|
||||
return inferredBiomes;
|
||||
}
|
||||
|
||||
private static IrisBiome loadFocusBiome(IrisDimension dimension, IrisData data) {
|
||||
String key = dimension.getFocus();
|
||||
if (key == null || key.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
IrisBiome biome = data.getBiomeLoader().load(key);
|
||||
return biome == null || biome.isCompatExcluded() ? null : biome;
|
||||
}
|
||||
|
||||
private static IrisRegion loadFocusRegion(IrisDimension dimension, IrisData data) {
|
||||
String key = dimension.getFocusRegion();
|
||||
if (key == null || key.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
IrisRegion region = data.getRegionLoader().load(key);
|
||||
return region == null || region.isCompatExcluded() ? null : region;
|
||||
}
|
||||
|
||||
private static IrisRegion findFocusRegion(
|
||||
IrisBiome focusBiome,
|
||||
IrisDimension dimension,
|
||||
DataProvider dataProvider,
|
||||
IrisData data
|
||||
) {
|
||||
for (IrisRegion region : dimension.getAllRegions(dataProvider)) {
|
||||
if (region.getAllBiomeIds().contains(focusBiome.getLoadKey())) {
|
||||
return region;
|
||||
}
|
||||
}
|
||||
String key = UUID.nameUUIDFromBytes((dimension.getLoadKey() + "\u0000" + focusBiome.getLoadKey())
|
||||
.getBytes(StandardCharsets.UTF_8)).toString();
|
||||
IrisRegion region = new IrisRegion();
|
||||
region.getLandBiomes().add(focusBiome.getLoadKey());
|
||||
region.getSeaBiomes().add(focusBiome.getLoadKey());
|
||||
region.getShoreBiomes().add(focusBiome.getLoadKey());
|
||||
region.setLoadKey(key);
|
||||
region.setLoader(data);
|
||||
region.setLoadFile(new File(
|
||||
data.getDataFolder(),
|
||||
data.getRegionLoader().getFolderName() + "/" + key + ".json"
|
||||
));
|
||||
return region;
|
||||
}
|
||||
|
||||
private static IrisBiome implode(
|
||||
IrisBiome biome,
|
||||
double x,
|
||||
double z,
|
||||
RNG rng,
|
||||
DataProvider dataProvider,
|
||||
Map<IrisBiome, IrisComplex.ChildSelectionPlan> childSelectionPlans,
|
||||
int remainingDepth
|
||||
) {
|
||||
if (biome == null || remainingDepth < 0 || biome.getChildren().isEmpty()) {
|
||||
return biome;
|
||||
}
|
||||
|
||||
IrisComplex.ChildSelectionPlan selectionPlan = childSelectionPlans.get(biome);
|
||||
if (selectionPlan == null) {
|
||||
synchronized (childSelectionPlans) {
|
||||
selectionPlan = childSelectionPlans.get(biome);
|
||||
if (selectionPlan == null) {
|
||||
KList<IrisBiome> options = new KList<>();
|
||||
for (IrisBiome child : biome.getRealChildren(dataProvider)) {
|
||||
if (child != null) {
|
||||
options.add(child);
|
||||
}
|
||||
}
|
||||
options.add(biome);
|
||||
selectionPlan = IrisComplex.ChildSelectionPlan.create(options);
|
||||
childSelectionPlans.put(biome, selectionPlan);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IrisBiome selected = selectionPlan.select(
|
||||
biome.getChildrenGenerator(rng, 123, biome.getChildShrinkFactor()), x, z);
|
||||
if (selected == null) {
|
||||
return biome;
|
||||
}
|
||||
return implode(
|
||||
selected.withInferredType(biome.getInferredType()),
|
||||
x,
|
||||
z,
|
||||
rng,
|
||||
dataProvider,
|
||||
childSelectionPlans,
|
||||
remainingDepth - 1
|
||||
);
|
||||
}
|
||||
|
||||
public double getNormalTerrainHeight(double x, double z) {
|
||||
return getNormalTerrainHeight(x, z, usesNaturalFallback(x, z));
|
||||
}
|
||||
|
||||
double getNormalTerrainHeight(double x, double z, boolean naturalFallback) {
|
||||
if (naturalFallback && selfFallback != null) {
|
||||
return selfFallback.heightStream().getDouble(x, z);
|
||||
}
|
||||
return heightStream.getDouble(x, z);
|
||||
}
|
||||
|
||||
public double getFluidHeight(double x, double z) {
|
||||
return getFluidHeight(x, z, usesNaturalFallback(x, z));
|
||||
}
|
||||
|
||||
double getFluidHeight(double x, double z, boolean naturalFallback) {
|
||||
if (naturalFallback && selfFallback != null) {
|
||||
return selfFallback.fluidHeight();
|
||||
}
|
||||
return fluidHeightStream.getDouble(x, z);
|
||||
}
|
||||
|
||||
public ProceduralStream<Double> getSlopeStream() {
|
||||
return slopeStream;
|
||||
}
|
||||
|
||||
public IrisBiome getBiome(double x, double z) {
|
||||
return getBiome(x, z, usesNaturalFallback(x, z));
|
||||
}
|
||||
|
||||
private IrisBiome getBiome(double x, double z, boolean naturalFallback) {
|
||||
if (naturalFallback && selfFallback != null) {
|
||||
return selfFallback.biomeStream().get(x, z);
|
||||
}
|
||||
return biomeStream.get(x, z);
|
||||
}
|
||||
|
||||
public IrisRegion getRegion(double x, double z) {
|
||||
return regionStream == null ? null : regionStream.get(x, z);
|
||||
}
|
||||
|
||||
public PlatformBlockState getRockBlock(double x, double z) {
|
||||
return rockStream.get(x, z);
|
||||
}
|
||||
|
||||
public PlatformBlockState getFluidBlock(double x, double z) {
|
||||
return getFluidBlock(x, z, usesNaturalFallback(x, z));
|
||||
}
|
||||
|
||||
private PlatformBlockState getFluidBlock(double x, double z, boolean naturalFallback) {
|
||||
if (naturalFallback && selfFallback != null) {
|
||||
return selfFallback.fluidBlockSampler().sample(x, z);
|
||||
}
|
||||
return fluidBlockSampler.sample(x, z);
|
||||
}
|
||||
|
||||
public PlatformBlockState getSurfaceBlock(double x, double z) {
|
||||
return imageMapRuntime.sampleSurfaceBlock(x, z);
|
||||
}
|
||||
|
||||
public IrisDimension getDimension() {
|
||||
return dimension;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IrisData getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public int getLocalHeight() {
|
||||
return localHeight;
|
||||
}
|
||||
|
||||
public boolean isSelfReferencing() {
|
||||
return selfReferencing;
|
||||
}
|
||||
|
||||
ColumnSample sampleColumn(double x, double z, boolean naturalFallback) {
|
||||
return new ColumnSample(
|
||||
getNormalTerrainHeight(x, z, naturalFallback),
|
||||
getFluidHeight(x, z, naturalFallback),
|
||||
getBiome(x, z, naturalFallback),
|
||||
getRegion(x, z),
|
||||
getRockBlock(x, z),
|
||||
getFluidBlock(x, z, naturalFallback),
|
||||
getSurfaceBlock(x, z)
|
||||
);
|
||||
}
|
||||
|
||||
private boolean usesNaturalFallback(double x, double z) {
|
||||
return selfFallback != null && engine.answersFromNaturalTerrain(
|
||||
(int) Math.floor(x),
|
||||
(int) Math.floor(z)
|
||||
);
|
||||
}
|
||||
|
||||
private interface FluidBlockSampler {
|
||||
PlatformBlockState sample(double x, double z);
|
||||
}
|
||||
|
||||
private record ContextState(
|
||||
Engine engine,
|
||||
IrisDimension dimension,
|
||||
IrisData data,
|
||||
int localHeight,
|
||||
ProceduralStream<Double> heightStream,
|
||||
ProceduralStream<Double> slopeStream,
|
||||
ProceduralStream<Double> fluidHeightStream,
|
||||
ProceduralStream<IrisBiome> biomeStream,
|
||||
ProceduralStream<IrisRegion> regionStream,
|
||||
ProceduralStream<PlatformBlockState> rockStream,
|
||||
FluidBlockSampler fluidBlockSampler,
|
||||
IrisImageMapRuntime imageMapRuntime,
|
||||
boolean selfReferencing,
|
||||
SelfFallback selfFallback
|
||||
) {
|
||||
}
|
||||
|
||||
private record SelfFallback(
|
||||
ProceduralStream<Double> heightStream,
|
||||
ProceduralStream<IrisBiome> biomeStream,
|
||||
double fluidHeight,
|
||||
FluidBlockSampler fluidBlockSampler
|
||||
) {
|
||||
}
|
||||
|
||||
record ColumnSample(
|
||||
double terrainHeight,
|
||||
double fluidHeight,
|
||||
IrisBiome biome,
|
||||
IrisRegion region,
|
||||
PlatformBlockState rockBlock,
|
||||
PlatformBlockState fluidBlock,
|
||||
PlatformBlockState surfaceBlock
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -59,15 +59,22 @@ final class EngineHotloader {
|
||||
IrisComplex nextComplex = null;
|
||||
try {
|
||||
engine.sealForTransition("complex hotload", false);
|
||||
engine.prepareRuntimeHotload();
|
||||
RuntimeAssembly assembly = new RuntimeAssembly(RuntimeAssembly.nextRuntimeId(), previous.target());
|
||||
engine.runtimeAssembly.set(assembly);
|
||||
EngineRuntime next;
|
||||
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
|
||||
assembly.complex = new IrisComplex(engine);
|
||||
nextComplex = assembly.complex;
|
||||
assembly.dimensionStackContext = engine.runtimeBuilder.buildDimensionStackContext();
|
||||
assembly.upperContext = engine.runtimeBuilder.buildUpperContext();
|
||||
BiomeMaxes biomeMaxes = engine.runtimeBuilder.computeBiomeMaxes();
|
||||
next = previous.withComplex(assembly.cacheId, assembly.complex, assembly.upperContext, biomeMaxes);
|
||||
next = previous.withComplex(
|
||||
assembly.cacheId,
|
||||
assembly.complex,
|
||||
assembly.upperContext,
|
||||
assembly.dimensionStackContext,
|
||||
biomeMaxes);
|
||||
} finally {
|
||||
engine.runtimeAssembly.remove();
|
||||
}
|
||||
@@ -110,6 +117,7 @@ final class EngineHotloader {
|
||||
boolean published = false;
|
||||
try {
|
||||
engine.sealForTransition("hotload", false);
|
||||
engine.prepareRuntimeHotload();
|
||||
replacementData = IrisData.openRuntime(previousData.getDataFolder());
|
||||
IrisDimension replacement = replacementData.getDimensionLoader().load(previousDimension.getLoadKey());
|
||||
if (replacement == null) {
|
||||
|
||||
@@ -34,6 +34,7 @@ record EngineRuntime(
|
||||
EngineTarget target,
|
||||
IrisComplex complex,
|
||||
UpperDimensionContext upperContext,
|
||||
DimensionStackContext dimensionStackContext,
|
||||
EngineEffects effects,
|
||||
EngineMode mode,
|
||||
EngineWorldManager worldManager,
|
||||
@@ -44,6 +45,7 @@ record EngineRuntime(
|
||||
int nextCacheId,
|
||||
IrisComplex nextComplex,
|
||||
UpperDimensionContext nextUpperContext,
|
||||
DimensionStackContext nextDimensionStackContext,
|
||||
BiomeMaxes nextBiomeMaxes
|
||||
) {
|
||||
return new EngineRuntime(
|
||||
@@ -51,6 +53,7 @@ record EngineRuntime(
|
||||
target,
|
||||
nextComplex,
|
||||
nextUpperContext,
|
||||
nextDimensionStackContext,
|
||||
effects,
|
||||
mode,
|
||||
worldManager,
|
||||
|
||||
@@ -22,12 +22,14 @@ import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.engine.EngineRuntime.BiomeMaxes;
|
||||
import art.arcane.iris.engine.IrisEngine.LifecycleState;
|
||||
import art.arcane.iris.engine.actuator.IrisDimensionStackActuator;
|
||||
import art.arcane.iris.engine.framework.EngineEffects;
|
||||
import art.arcane.iris.engine.framework.EngineEffectsProvider;
|
||||
import art.arcane.iris.engine.framework.EngineMode;
|
||||
import art.arcane.iris.engine.framework.EngineTarget;
|
||||
import art.arcane.iris.engine.framework.EngineWorldManager;
|
||||
import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
|
||||
import art.arcane.iris.engine.modifier.IrisCustomModifier;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomePaletteLayer;
|
||||
import art.arcane.iris.engine.object.IrisDecorator;
|
||||
@@ -74,6 +76,9 @@ final class EngineRuntimeBuilder {
|
||||
assembly.complex = new IrisComplex(engine);
|
||||
IrisLogging.debug("[IrisEngine timing] complex=" + (M.ms() - started) + "ms");
|
||||
started = M.ms();
|
||||
assembly.dimensionStackContext = buildDimensionStackContext();
|
||||
IrisLogging.debug("[IrisEngine timing] buildDimensionStackContext=" + (M.ms() - started) + "ms");
|
||||
started = M.ms();
|
||||
assembly.upperContext = buildUpperContext();
|
||||
IrisLogging.debug("[IrisEngine timing] buildUpperContext=" + (M.ms() - started) + "ms");
|
||||
started = M.ms();
|
||||
@@ -91,6 +96,14 @@ final class EngineRuntimeBuilder {
|
||||
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
|
||||
assembly.mode.registerStage((x, z, blocks, biomes, multicore, context) ->
|
||||
staticObjects.apply(engine, x, z, blocks));
|
||||
if (assembly.dimensionStackContext != null) {
|
||||
IrisDimensionStackActuator dimensionStack = new IrisDimensionStackActuator(engine);
|
||||
assembly.mode.registerStage((x, z, blocks, biomes, multicore, context) ->
|
||||
dimensionStack.actuate(x, z, blocks, multicore, context));
|
||||
IrisCustomModifier stackCustom = new IrisCustomModifier(engine);
|
||||
assembly.mode.registerStage((x, z, blocks, biomes, multicore, context) ->
|
||||
stackCustom.modify(x, z, blocks, multicore, context));
|
||||
}
|
||||
IrisLogging.debug("[IrisEngine timing] setupMode=" + (M.ms() - started) + "ms");
|
||||
started = M.ms();
|
||||
assembly.worldManager = IrisServices.get(EngineWorldManagerProvider.class).create(engine);
|
||||
@@ -205,6 +218,21 @@ final class EngineRuntimeBuilder {
|
||||
return null;
|
||||
}
|
||||
|
||||
DimensionStackContext buildDimensionStackContext() {
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
if (!dimension.hasDimensionStack()) {
|
||||
return null;
|
||||
}
|
||||
if (dimension.hasUpperDimension()) {
|
||||
throw new IllegalStateException("Dimension '" + dimension.getLoadKey()
|
||||
+ "' cannot enable both dimensionStack and upperDimension.");
|
||||
}
|
||||
DimensionStackContext context = DimensionStackContext.create(engine, dimension.getDimensionStack());
|
||||
IrisLogging.info("Dimension stack enabled: "
|
||||
+ String.join(" -> ", dimension.getDimensionStack().getDimensions()));
|
||||
return context;
|
||||
}
|
||||
|
||||
private EngineMode createMode() {
|
||||
Throwable configuredFailure = null;
|
||||
try {
|
||||
@@ -300,6 +328,7 @@ final class EngineRuntimeBuilder {
|
||||
}
|
||||
IrisComplex complex;
|
||||
UpperDimensionContext upperContext;
|
||||
DimensionStackContext dimensionStackContext;
|
||||
EngineEffects effects;
|
||||
EngineMode mode;
|
||||
EngineWorldManager worldManager;
|
||||
@@ -319,6 +348,7 @@ final class EngineRuntimeBuilder {
|
||||
target,
|
||||
complex,
|
||||
upperContext,
|
||||
dimensionStackContext,
|
||||
effects,
|
||||
mode,
|
||||
worldManager,
|
||||
|
||||
@@ -501,10 +501,14 @@ public class IrisEngine implements Engine {
|
||||
}
|
||||
|
||||
public void hotloadSilently() {
|
||||
nativeStructureVolumeMemo.clear();
|
||||
hotloader.hotloadSilently();
|
||||
}
|
||||
|
||||
void prepareRuntimeHotload() {
|
||||
nativeStructureVolumeMemo.clear();
|
||||
platformHooks.prepareRuntimeHotload(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KList<NativeStructureVolume> getNativeStructureVolumes(int minX, int minZ, int maxX, int maxZ) {
|
||||
if (!nativeStructureVolumeQueriesEnabled.get()) {
|
||||
@@ -632,6 +636,16 @@ public class IrisEngine implements Engine {
|
||||
return current == null ? null : current.upperContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DimensionStackContext getDimensionStackContext() {
|
||||
RuntimeAssembly assembly = runtimeAssembly.get();
|
||||
if (assembly != null) {
|
||||
return assembly.dimensionStackContext;
|
||||
}
|
||||
EngineRuntime current = runtime;
|
||||
return current == null ? null : current.dimensionStackContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Long> getHash32() {
|
||||
RuntimeAssembly assembly = runtimeAssembly.get();
|
||||
|
||||
@@ -1,311 +1,46 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.engine;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.image.IrisImageMapRuntime;
|
||||
import art.arcane.iris.engine.object.InferredType;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisGenerator;
|
||||
import art.arcane.iris.engine.object.IrisInterpolator;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisShapedGeneratorStyle;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.util.common.data.DataProvider;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.iris.util.project.interpolation.NoiseBounds;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import art.arcane.iris.util.project.stream.interpolation.Interpolated;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import art.arcane.iris.util.common.data.DataProvider;
|
||||
|
||||
public class UpperDimensionContext implements DataProvider {
|
||||
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
|
||||
private final IrisDimension dimension;
|
||||
private final IrisData data;
|
||||
private final DimensionTerrainContext terrainContext;
|
||||
private final int chunkHeight;
|
||||
private final ProceduralStream<Double> heightStream;
|
||||
private final ProceduralStream<IrisBiome> biomeStream;
|
||||
private final ProceduralStream<IrisRegion> regionStream;
|
||||
private final ProceduralStream<PlatformBlockState> rockStream;
|
||||
private final IrisImageMapRuntime imageMapRuntime;
|
||||
private final boolean selfReferencing;
|
||||
|
||||
private UpperDimensionContext(ContextState state) {
|
||||
this.dimension = state.dimension();
|
||||
this.data = state.data();
|
||||
this.chunkHeight = state.chunkHeight();
|
||||
this.heightStream = state.heightStream();
|
||||
this.biomeStream = state.biomeStream();
|
||||
this.regionStream = state.regionStream();
|
||||
this.rockStream = state.rockStream();
|
||||
this.imageMapRuntime = state.imageMapRuntime();
|
||||
this.selfReferencing = state.selfReferencing();
|
||||
private UpperDimensionContext(DimensionTerrainContext terrainContext, int chunkHeight) {
|
||||
this.terrainContext = terrainContext;
|
||||
this.chunkHeight = chunkHeight;
|
||||
}
|
||||
|
||||
public static UpperDimensionContext create(Engine engine, IrisDimension upperDim) {
|
||||
boolean selfRef = upperDim.getLoadKey().equals(engine.getDimension().getLoadKey());
|
||||
int chunkHeight = engine.getHeight();
|
||||
if (selfRef) {
|
||||
return createSelfReferencing(engine, chunkHeight);
|
||||
}
|
||||
return createCrossReferencing(engine, upperDim, chunkHeight);
|
||||
}
|
||||
|
||||
private static UpperDimensionContext createSelfReferencing(Engine engine, int chunkHeight) {
|
||||
IrisComplex complex = engine.getComplex();
|
||||
return new UpperDimensionContext(new ContextState(
|
||||
engine.getDimension(),
|
||||
engine.getData(),
|
||||
chunkHeight,
|
||||
complex.getNaturalHeightStream(),
|
||||
complex.getNaturalTrueBiomeStream(),
|
||||
complex.getRegionStream(),
|
||||
complex.getRockStream(),
|
||||
complex.getImageMapRuntime(),
|
||||
true
|
||||
));
|
||||
}
|
||||
|
||||
private static UpperDimensionContext createCrossReferencing(Engine engine, IrisDimension upperDim, int chunkHeight) {
|
||||
IrisData resolvedData = upperDim.getLoader();
|
||||
if (resolvedData == null) {
|
||||
resolvedData = engine.getData();
|
||||
}
|
||||
IrisData upperData = resolvedData;
|
||||
IrisImageMapRuntime imageMapRuntime = IrisImageMapRuntime.compile(
|
||||
upperData,
|
||||
upperDim,
|
||||
engine.getMinHeight()
|
||||
public static UpperDimensionContext create(Engine engine, IrisDimension upperDimension) {
|
||||
return new UpperDimensionContext(
|
||||
DimensionTerrainContext.forUpper(engine, upperDimension),
|
||||
engine.getHeight()
|
||||
);
|
||||
long seedOffset = upperDim.getLoadKey().hashCode();
|
||||
RNG rng = new RNG(engine.getSeedManager().getComplex() ^ seedOffset);
|
||||
double fluidHeight = upperDim.getFluidHeight();
|
||||
int cacheSize = IrisSettings.get().getPerformance().getNoiseCacheSize();
|
||||
DataProvider dataProvider = () -> upperData;
|
||||
|
||||
Map<IrisInterpolator, Set<IrisGenerator>> generators = new HashMap<>();
|
||||
Set<IrisBiome> allBiomes = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
Map<IrisBiome, IrisComplex.ChildSelectionPlan> childSelectionPlans =
|
||||
Collections.synchronizedMap(new IdentityHashMap<>());
|
||||
upperDim.getRegions().forEach(regionKey -> {
|
||||
IrisRegion region = upperData.getRegionLoader().load(regionKey);
|
||||
if (region != null) {
|
||||
region.getNaturalBiomes(dataProvider).forEach(biome -> registerBiomeGenerators(
|
||||
biome, dataProvider, allBiomes, generators));
|
||||
}
|
||||
});
|
||||
for (IrisRegion mappedRegion : imageMapRuntime.getMappedRegions()) {
|
||||
mappedRegion.getNaturalBiomes(dataProvider).forEach(biome -> registerBiomeGenerators(
|
||||
biome, dataProvider, allBiomes, generators));
|
||||
}
|
||||
for (IrisBiome mappedBiome : imageMapRuntime.getMappedBiomes()) {
|
||||
registerBiomeGenerators(mappedBiome, dataProvider, allBiomes, generators);
|
||||
}
|
||||
|
||||
IrisComplex.GeneratorGroup[] generatorGroups = IrisComplex.freezeGeneratorGroups(generators);
|
||||
Map<IrisInterpolator, IdentityHashMap<IrisBiome, NoiseBounds>> generatorBounds = new HashMap<>();
|
||||
for (IrisComplex.GeneratorGroup group : generatorGroups) {
|
||||
IdentityHashMap<IrisBiome, NoiseBounds> interpolatorBounds = new IdentityHashMap<>(Math.max(allBiomes.size(), 16));
|
||||
for (IrisBiome biome : allBiomes) {
|
||||
double min = 0D;
|
||||
double max = 0D;
|
||||
for (IrisGenerator gen : group.generators()) {
|
||||
String key = gen.getLoadKey();
|
||||
if (key == null || key.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
max += biome.getGenLinkMax(key, engine);
|
||||
min += biome.getGenLinkMin(key, engine);
|
||||
}
|
||||
interpolatorBounds.put(biome, new NoiseBounds(min, max));
|
||||
}
|
||||
generatorBounds.put(group.interpolator(), interpolatorBounds);
|
||||
}
|
||||
|
||||
ProceduralStream<Double> regionStyleStream = upperDim.getRegionStyle()
|
||||
.create(rng.nextParallelRNG(883), upperData).stream()
|
||||
.zoom(upperDim.getRegionZoom());
|
||||
ProceduralStream<IrisRegion> proceduralRegionStream = regionStyleStream
|
||||
.selectRarity(upperData.getRegionLoader().loadAll(upperDim.getRegions()));
|
||||
ProceduralStream<IrisRegion> regionStream = proceduralRegionStream
|
||||
.convertAware2D((region, x, z) -> mappedRegion(imageMapRuntime, region, x, z))
|
||||
.cache2D("upperImageMappedRegionStream", engine, cacheSize);
|
||||
|
||||
ProceduralStream<IrisBiome> landBiomeStream = regionStream
|
||||
.convert(r -> upperDim.getLandBiomeStyle()
|
||||
.create(rng.nextParallelRNG(InferredType.LAND.ordinal()), upperData).stream()
|
||||
.zoom(upperDim.getBiomeZoom())
|
||||
.zoom(upperDim.getLandZoom())
|
||||
.zoom(r.getLandBiomeZoom())
|
||||
.selectRarity(loadInferredBiomes(upperData, r.getLandBiomes(), InferredType.LAND)))
|
||||
.convertAware2D(ProceduralStream::get);
|
||||
ProceduralStream<IrisBiome> seaBiomeStream = regionStream
|
||||
.convert(r -> upperDim.getSeaBiomeStyle()
|
||||
.create(rng.nextParallelRNG(InferredType.SEA.ordinal()), upperData).stream()
|
||||
.zoom(upperDim.getBiomeZoom())
|
||||
.zoom(upperDim.getSeaZoom())
|
||||
.zoom(r.getSeaBiomeZoom())
|
||||
.selectRarity(loadInferredBiomes(upperData, r.getSeaBiomes(), InferredType.SEA)))
|
||||
.convertAware2D(ProceduralStream::get);
|
||||
ProceduralStream<IrisBiome> shoreBiomeStream = regionStream
|
||||
.convert(r -> upperDim.getShoreBiomeStyle()
|
||||
.create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), upperData).stream()
|
||||
.zoom(upperDim.getBiomeZoom())
|
||||
.zoom(r.getShoreBiomeZoom())
|
||||
.selectRarity(loadInferredBiomes(upperData, r.getShoreBiomes(), InferredType.SHORE)))
|
||||
.convertAware2D(ProceduralStream::get);
|
||||
|
||||
Map<InferredType, ProceduralStream<IrisBiome>> inferredStreams = new HashMap<>();
|
||||
inferredStreams.put(InferredType.LAND, landBiomeStream);
|
||||
inferredStreams.put(InferredType.SEA, seaBiomeStream);
|
||||
inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
|
||||
|
||||
ProceduralStream<InferredType> bridgeStream = upperDim.getContinentalStyle()
|
||||
.create(rng.nextParallelRNG(234234565), upperData)
|
||||
.bake().scale(1D / upperDim.getContinentZoom()).bake().stream()
|
||||
.convert(v -> v >= upperDim.getLandChance() ? InferredType.SEA : InferredType.LAND);
|
||||
|
||||
ProceduralStream<IrisBiome> proceduralBaseBiomeStream = bridgeStream
|
||||
.convertAware2D((t, x, z) -> {
|
||||
ProceduralStream<IrisBiome> stream = inferredStreams.get(t);
|
||||
return stream != null ? stream.get(x, z) : inferredStreams.get(InferredType.LAND).get(x, z);
|
||||
})
|
||||
.convertAware2D((biome, x, z) -> implode(
|
||||
biome, x, z, rng, dataProvider, childSelectionPlans, 3));
|
||||
ProceduralStream<IrisBiome> baseBiomeStream = proceduralBaseBiomeStream
|
||||
.convertAware2D((biome, x, z) -> mappedBiome(imageMapRuntime, biome, x, z))
|
||||
.cache2D("upperImageMappedBaseBiomeStream", engine, cacheSize);
|
||||
|
||||
KList<IrisShapedGeneratorStyle> overlayNoise = upperDim.getOverlayNoise();
|
||||
ProceduralStream<Double> overlayStream = overlayNoise.isEmpty()
|
||||
? ProceduralStream.ofDouble((x, z) -> 0.0D)
|
||||
: ProceduralStream.ofDouble((x, z) -> {
|
||||
double value = 0D;
|
||||
for (IrisShapedGeneratorStyle style : overlayNoise) {
|
||||
value += style.get(rng, upperData, x, z);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
long heightSeed = engine.getSeedManager().getHeight() ^ seedOffset;
|
||||
|
||||
ProceduralStream<Double> heightStream = ProceduralStream.of((x, z) -> {
|
||||
IrisBiome b = baseBiomeStream.get(x, z);
|
||||
if (b == null) {
|
||||
return mappedTerrainHeight(imageMapRuntime, fluidHeight, x, z);
|
||||
}
|
||||
double interpolatedHeight = 0;
|
||||
for (IrisComplex.GeneratorGroup group : generatorGroups) {
|
||||
IrisInterpolator interpolator = group.interpolator();
|
||||
IrisGenerator[] groupGenerators = group.generators();
|
||||
if (groupGenerators.length == 0) {
|
||||
continue;
|
||||
}
|
||||
IdentityHashMap<IrisBiome, NoiseBounds> cachedBounds = generatorBounds.get(interpolator);
|
||||
NoiseBounds sampledBounds = interpolator.interpolateBounds(x, z, (xx, zz) -> {
|
||||
try {
|
||||
IrisBiome bx = baseBiomeStream.get(xx, zz);
|
||||
if (bx == null) {
|
||||
return ZERO_NOISE_BOUNDS;
|
||||
}
|
||||
NoiseBounds bounds = cachedBounds != null ? cachedBounds.get(bx) : null;
|
||||
if (bounds != null) {
|
||||
return bounds;
|
||||
}
|
||||
double bMin = 0D;
|
||||
double bMax = 0D;
|
||||
for (IrisGenerator gen : groupGenerators) {
|
||||
String key = gen.getLoadKey();
|
||||
if (key == null || key.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
bMax += bx.getGenLinkMax(key, engine);
|
||||
bMin += bx.getGenLinkMin(key, engine);
|
||||
}
|
||||
return new NoiseBounds(bMin, bMax);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
return ZERO_NOISE_BOUNDS;
|
||||
}
|
||||
});
|
||||
double hi = sampledBounds.max();
|
||||
double lo = sampledBounds.min();
|
||||
interpolatedHeight += IrisComplex.averageGeneratorHeights(
|
||||
groupGenerators,
|
||||
lo,
|
||||
hi,
|
||||
x,
|
||||
z,
|
||||
heightSeed + 239945
|
||||
);
|
||||
}
|
||||
double proceduralHeight = Math.max(
|
||||
Math.min(interpolatedHeight + fluidHeight + overlayStream.get(x, z), chunkHeight),
|
||||
0D
|
||||
);
|
||||
return mappedTerrainHeight(imageMapRuntime, proceduralHeight, x, z);
|
||||
}, Interpolated.DOUBLE).cache2DDouble("upperImageMappedHeightStream", engine, cacheSize);
|
||||
|
||||
ProceduralStream<IrisBiome> finalBiomeStream = heightStream.convertAware2D((height, x, z) -> {
|
||||
IrisBiome mappedBiome = imageMapRuntime.sampleBiome(x, z);
|
||||
if (mappedBiome != null) {
|
||||
return mappedBiome;
|
||||
}
|
||||
IrisBiome baseBiome = baseBiomeStream.get(x, z);
|
||||
IrisBiome resolved = IrisComplex.resolveSurfaceBiome(
|
||||
height,
|
||||
baseBiome,
|
||||
regionStream.get(x, z),
|
||||
x,
|
||||
z,
|
||||
fluidHeight,
|
||||
landBiomeStream,
|
||||
seaBiomeStream,
|
||||
shoreBiomeStream);
|
||||
return resolved == baseBiome
|
||||
? baseBiome
|
||||
: implode(resolved, x, z, rng, dataProvider, childSelectionPlans, 3);
|
||||
}).cache2D("upperImageMappedFinalBiomeStream", engine, cacheSize);
|
||||
|
||||
ProceduralStream<PlatformBlockState> rockStream = upperDim.getRockPalette()
|
||||
.getLayerGenerator(rng.nextParallelRNG(45), upperData).stream()
|
||||
.select(upperDim.getRockPalette().getBlockData(upperData));
|
||||
|
||||
return new UpperDimensionContext(new ContextState(
|
||||
upperDim,
|
||||
upperData,
|
||||
chunkHeight,
|
||||
heightStream,
|
||||
finalBiomeStream,
|
||||
regionStream,
|
||||
rockStream,
|
||||
imageMapRuntime,
|
||||
false
|
||||
));
|
||||
}
|
||||
|
||||
private static void registerBiomeGenerators(
|
||||
IrisBiome biome,
|
||||
DataProvider dataProvider,
|
||||
Set<IrisBiome> allBiomes,
|
||||
Map<IrisInterpolator, Set<IrisGenerator>> generators
|
||||
) {
|
||||
allBiomes.add(biome);
|
||||
biome.getGenerators().forEach(link -> {
|
||||
IrisGenerator generator = link.getCachedGenerator(dataProvider);
|
||||
if (generator != null) {
|
||||
generators.computeIfAbsent(generator.getInterpolator(), key -> new HashSet<>()).add(generator);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static IrisRegion mappedRegion(
|
||||
@@ -314,8 +49,8 @@ public class UpperDimensionContext implements DataProvider {
|
||||
double worldX,
|
||||
double worldZ
|
||||
) {
|
||||
IrisRegion mappedRegion = imageMapRuntime.sampleRegion(worldX, worldZ);
|
||||
return mappedRegion == null ? proceduralRegion : mappedRegion;
|
||||
return DimensionTerrainContext.mappedRegion(
|
||||
imageMapRuntime, proceduralRegion, worldX, worldZ);
|
||||
}
|
||||
|
||||
static double mappedTerrainHeight(
|
||||
@@ -324,7 +59,8 @@ public class UpperDimensionContext implements DataProvider {
|
||||
double worldX,
|
||||
double worldZ
|
||||
) {
|
||||
return imageMapRuntime.sampleTerrainHeight(worldX, worldZ, proceduralHeight);
|
||||
return DimensionTerrainContext.mappedTerrainHeight(
|
||||
imageMapRuntime, proceduralHeight, worldX, worldZ);
|
||||
}
|
||||
|
||||
static IrisBiome mappedBiome(
|
||||
@@ -333,8 +69,8 @@ public class UpperDimensionContext implements DataProvider {
|
||||
double worldX,
|
||||
double worldZ
|
||||
) {
|
||||
IrisBiome mappedBiome = imageMapRuntime.sampleBiome(worldX, worldZ);
|
||||
return mappedBiome == null ? proceduralBiome : mappedBiome;
|
||||
return DimensionTerrainContext.mappedBiome(
|
||||
imageMapRuntime, proceduralBiome, worldX, worldZ);
|
||||
}
|
||||
|
||||
static PlatformBlockState mappedSurfaceBlock(
|
||||
@@ -343,108 +79,40 @@ public class UpperDimensionContext implements DataProvider {
|
||||
double worldX,
|
||||
double worldZ
|
||||
) {
|
||||
PlatformBlockState mappedBlock = imageMapRuntime.sampleSurfaceBlock(worldX, worldZ);
|
||||
return mappedBlock == null ? proceduralBlock : mappedBlock;
|
||||
}
|
||||
|
||||
private static KList<IrisBiome> loadInferredBiomes(IrisData data, KList<String> keys, InferredType type) {
|
||||
KList<IrisBiome> inferred = new KList<>();
|
||||
for (IrisBiome biome : data.getBiomeLoader().loadAll(keys)) {
|
||||
inferred.add(biome.withInferredType(type));
|
||||
}
|
||||
return inferred;
|
||||
}
|
||||
|
||||
private static IrisBiome implode(
|
||||
IrisBiome biome,
|
||||
double x,
|
||||
double z,
|
||||
RNG rng,
|
||||
DataProvider dataProvider,
|
||||
Map<IrisBiome, IrisComplex.ChildSelectionPlan> childSelectionPlans,
|
||||
int remainingDepth
|
||||
) {
|
||||
if (biome == null || remainingDepth < 0 || biome.getChildren().isEmpty()) {
|
||||
return biome;
|
||||
}
|
||||
|
||||
IrisComplex.ChildSelectionPlan selectionPlan = childSelectionPlans.get(biome);
|
||||
if (selectionPlan == null) {
|
||||
synchronized (childSelectionPlans) {
|
||||
selectionPlan = childSelectionPlans.get(biome);
|
||||
if (selectionPlan == null) {
|
||||
KList<IrisBiome> options = new KList<>();
|
||||
for (IrisBiome child : biome.getRealChildren(dataProvider)) {
|
||||
if (child != null) {
|
||||
options.add(child);
|
||||
}
|
||||
}
|
||||
options.add(biome);
|
||||
selectionPlan = IrisComplex.ChildSelectionPlan.create(options);
|
||||
childSelectionPlans.put(biome, selectionPlan);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IrisBiome selected = selectionPlan.select(
|
||||
biome.getChildrenGenerator(rng, 123, biome.getChildShrinkFactor()), x, z);
|
||||
if (selected == null) {
|
||||
return biome;
|
||||
}
|
||||
return implode(
|
||||
selected.withInferredType(biome.getInferredType()),
|
||||
x,
|
||||
z,
|
||||
rng,
|
||||
dataProvider,
|
||||
childSelectionPlans,
|
||||
remainingDepth - 1);
|
||||
return DimensionTerrainContext.mappedSurfaceBlock(
|
||||
imageMapRuntime, proceduralBlock, worldX, worldZ);
|
||||
}
|
||||
|
||||
public int getUpperSurfaceY(int x, int z) {
|
||||
double rawHeight = heightStream.get((double) x, (double) z);
|
||||
return chunkHeight - 1 - (int) Math.round(rawHeight);
|
||||
return chunkHeight - 1 - (int) Math.round(terrainContext.getNormalTerrainHeight(x, z));
|
||||
}
|
||||
|
||||
public IrisBiome getUpperBiome(int x, int z) {
|
||||
return biomeStream.get((double) x, (double) z);
|
||||
return terrainContext.getBiome(x, z);
|
||||
}
|
||||
|
||||
public IrisRegion getUpperRegion(int x, int z) {
|
||||
return regionStream == null ? null : regionStream.get((double) x, (double) z);
|
||||
return terrainContext.getRegion(x, z);
|
||||
}
|
||||
|
||||
public PlatformBlockState getRockBlock(int x, int z) {
|
||||
return rockStream.get((double) x, (double) z);
|
||||
return terrainContext.getRockBlock(x, z);
|
||||
}
|
||||
|
||||
public PlatformBlockState getSurfaceBlock(int x, int z) {
|
||||
return imageMapRuntime.sampleSurfaceBlock(x, z);
|
||||
return terrainContext.getSurfaceBlock(x, z);
|
||||
}
|
||||
|
||||
public IrisDimension getDimension() {
|
||||
return dimension;
|
||||
return terrainContext.getDimension();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IrisData getData() {
|
||||
return data;
|
||||
return terrainContext.getData();
|
||||
}
|
||||
|
||||
public boolean isSelfReferencing() {
|
||||
return selfReferencing;
|
||||
}
|
||||
|
||||
private record ContextState(
|
||||
IrisDimension dimension,
|
||||
IrisData data,
|
||||
int chunkHeight,
|
||||
ProceduralStream<Double> heightStream,
|
||||
ProceduralStream<IrisBiome> biomeStream,
|
||||
ProceduralStream<IrisRegion> regionStream,
|
||||
ProceduralStream<PlatformBlockState> rockStream,
|
||||
IrisImageMapRuntime imageMapRuntime,
|
||||
boolean selfReferencing
|
||||
) {
|
||||
return terrainContext.isSelfReferencing();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,13 @@
|
||||
|
||||
package art.arcane.iris.engine.actuator;
|
||||
|
||||
import art.arcane.iris.engine.DimensionStackContext;
|
||||
import art.arcane.iris.engine.DimensionStackLayout;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineAssignedActuator;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.util.project.context.ChunkContext;
|
||||
import art.arcane.iris.util.project.context.ChunkedDataCache;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
@@ -38,6 +41,8 @@ import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
|
||||
private final RNG rng;
|
||||
private final ChronoLatch cl = new ChronoLatch(5000);
|
||||
@@ -60,33 +65,167 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
|
||||
Engine engine = getEngine();
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
|
||||
DimensionStackContext dimensionStackContext = engine.getDimensionStackContext();
|
||||
|
||||
for (int xf = 0; xf < width; xf++) {
|
||||
IrisBiome ib;
|
||||
for (int zf = 0; zf < depth; zf++) {
|
||||
ib = biomeCache.get(xf, zf);
|
||||
String key;
|
||||
int worldX = x + xf;
|
||||
int worldZ = z + zf;
|
||||
IrisBiome biome = biomeCache.get(xf, zf);
|
||||
ResolvedBiome resolved = resolve(biomeKey(
|
||||
biome,
|
||||
getDimension(),
|
||||
engine,
|
||||
worldX,
|
||||
0,
|
||||
worldZ
|
||||
));
|
||||
PlatformBiome platformBiome = resolved.biome();
|
||||
|
||||
if (ib.isCustom()) {
|
||||
IrisBiomeCustom custom = ib.getCustomBiome(rng, engine, x + xf, 0, z + zf);
|
||||
key = getDimension().getLoadKey() + ":" + custom.getId();
|
||||
} else {
|
||||
key = ib.getSkyBiomeKey(rng, engine, x + xf, 0, z + zf);
|
||||
if (platformBiome != null) {
|
||||
h.set(xf, 0, zf, xf, height - 1, zf, platformBiome);
|
||||
}
|
||||
|
||||
ResolvedBiome resolved = resolve(key);
|
||||
PlatformBiome biome = resolved.biome();
|
||||
|
||||
if (biome != null) {
|
||||
h.set(xf, 0, zf, xf, height - 1, zf, biome);
|
||||
mantle.set(worldX, 0, worldZ, resolved.matter());
|
||||
if (dimensionStackContext != null) {
|
||||
PlatformBiome bottomBiome = platformBiome == null
|
||||
? h.getRaw(xf, 0, zf)
|
||||
: platformBiome;
|
||||
applyDimensionStackBiomes(
|
||||
xf,
|
||||
zf,
|
||||
worldX,
|
||||
worldZ,
|
||||
h,
|
||||
mantle,
|
||||
engine,
|
||||
new ResolvedBiome(bottomBiome, resolved.matter()),
|
||||
context.getDimensionStackLayout(xf, zf)
|
||||
);
|
||||
}
|
||||
|
||||
mantle.set(x + xf, 0, z + zf, resolved.matter());
|
||||
}
|
||||
}
|
||||
engine.getMetrics().getBiome().put(p.getMilliseconds());
|
||||
}
|
||||
|
||||
private void applyDimensionStackBiomes(
|
||||
int localX,
|
||||
int localZ,
|
||||
int worldX,
|
||||
int worldZ,
|
||||
Hunk<PlatformBiome> output,
|
||||
Mantle<Matter> mantle,
|
||||
Engine engine,
|
||||
ResolvedBiome bottom,
|
||||
DimensionStackLayout layout
|
||||
) {
|
||||
List<DimensionStackLayout.Layer> layers = layout.layersBottomToTop();
|
||||
for (int layerIndex = 1; layerIndex < layers.size(); layerIndex++) {
|
||||
DimensionStackLayout.Layer lower = layers.get(layerIndex - 1);
|
||||
DimensionStackLayout.Layer layer = layers.get(layerIndex);
|
||||
int gapMinY = (int) Math.max(0L, (long) lower.contentTopY() + 1L);
|
||||
int gapMaxY = (int) Math.min(
|
||||
(long) output.getHeight() - 1L,
|
||||
(long) layer.localBaseY() - 1L
|
||||
);
|
||||
applyBiomeRange(
|
||||
localX,
|
||||
localZ,
|
||||
worldX,
|
||||
worldZ,
|
||||
gapMinY,
|
||||
gapMaxY,
|
||||
output,
|
||||
mantle,
|
||||
bottom
|
||||
);
|
||||
if (!layer.visible()) {
|
||||
continue;
|
||||
}
|
||||
ResolvedBiome resolved = bottom;
|
||||
if (layer.biome() != null) {
|
||||
IrisDimension dimension = layer.terrainContext().getDimension();
|
||||
resolved = resolve(biomeKey(
|
||||
layer.biome(),
|
||||
dimension,
|
||||
engine,
|
||||
worldX,
|
||||
layer.clippedSurfaceY(),
|
||||
worldZ
|
||||
));
|
||||
}
|
||||
applyBiomeRange(
|
||||
localX,
|
||||
localZ,
|
||||
worldX,
|
||||
worldZ,
|
||||
layer.renderMinY(),
|
||||
layer.renderMaxY(),
|
||||
output,
|
||||
mantle,
|
||||
resolved
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyBiomeRange(
|
||||
int localX,
|
||||
int localZ,
|
||||
int worldX,
|
||||
int worldZ,
|
||||
int minimumY,
|
||||
int maximumY,
|
||||
Hunk<PlatformBiome> output,
|
||||
Mantle<Matter> mantle,
|
||||
ResolvedBiome resolved
|
||||
) {
|
||||
if (minimumY > maximumY) {
|
||||
return;
|
||||
}
|
||||
clearBiomeMatterRange(mantle, worldX, worldZ, minimumY, maximumY);
|
||||
if (resolved.biome() != null) {
|
||||
output.set(
|
||||
localX,
|
||||
minimumY,
|
||||
localZ,
|
||||
localX,
|
||||
maximumY,
|
||||
localZ,
|
||||
resolved.biome()
|
||||
);
|
||||
}
|
||||
if (resolved.matter() != null) {
|
||||
mantle.set(worldX, minimumY, worldZ, resolved.matter());
|
||||
}
|
||||
}
|
||||
|
||||
static void clearBiomeMatterRange(
|
||||
Mantle<Matter> mantle,
|
||||
int worldX,
|
||||
int worldZ,
|
||||
int minimumY,
|
||||
int maximumY
|
||||
) {
|
||||
for (int y = minimumY; y <= maximumY; y++) {
|
||||
mantle.remove(worldX, y, worldZ, MatterBiomeInject.class);
|
||||
}
|
||||
}
|
||||
|
||||
private String biomeKey(
|
||||
IrisBiome biome,
|
||||
IrisDimension dimension,
|
||||
Engine engine,
|
||||
int x,
|
||||
int y,
|
||||
int z
|
||||
) {
|
||||
if (biome.isCustom()) {
|
||||
IrisBiomeCustom custom = biome.getCustomBiome(rng, engine, x, y, z);
|
||||
return dimension.getCustomBiomeKey(custom.getId());
|
||||
}
|
||||
return biome.getSkyBiomeKey(rng, engine, x, y, z);
|
||||
}
|
||||
|
||||
/**
|
||||
* The registry lookup and the biome id only depend on the scatter resolved key, so cache the pair
|
||||
* instead of paying two string keyed lookups per column. Unresolved biomes are never cached, so a
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.engine.actuator;
|
||||
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.DimensionStackLayout;
|
||||
import art.arcane.iris.engine.DimensionTerrainContext;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineAssignedActuator;
|
||||
import art.arcane.iris.engine.framework.TreeBlockMaterial;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.iris.util.project.context.ChunkContext;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.iris.util.project.matter.TileWrapper;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.documentation.BlockCoordinates;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
import art.arcane.volmlib.util.matter.MatterSlice;
|
||||
import art.arcane.volmlib.util.matter.MatterStructurePOI;
|
||||
import art.arcane.volmlib.util.matter.MatterUpdate;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class IrisDimensionStackActuator extends EngineAssignedActuator<PlatformBlockState> {
|
||||
private static final PlatformBlockState AIR = B.getState("AIR");
|
||||
private static final PlatformBlockState BEDROCK = B.getState("BEDROCK");
|
||||
private static final List<Class<?>> REPLACED_METADATA = List.of(
|
||||
TileWrapper.class,
|
||||
Identifier.class,
|
||||
String.class,
|
||||
TreeBlockMaterial.class,
|
||||
MatterCavern.class,
|
||||
HydrologyCaveCell.class,
|
||||
MatterMarker.class,
|
||||
MatterStructurePOI.class,
|
||||
MatterUpdate.class
|
||||
);
|
||||
|
||||
private final RNG rng;
|
||||
|
||||
public IrisDimensionStackActuator(Engine engine) {
|
||||
super(engine, "Dimension Stack");
|
||||
rng = new RNG(engine.getSeedManager().getTerrain());
|
||||
}
|
||||
|
||||
@BlockCoordinates
|
||||
@Override
|
||||
public void onActuate(
|
||||
int x,
|
||||
int z,
|
||||
Hunk<PlatformBlockState> blocks,
|
||||
boolean multicore,
|
||||
ChunkContext context
|
||||
) {
|
||||
if (getEngine().getDimensionStackContext() == null) {
|
||||
return;
|
||||
}
|
||||
MantleChunk<Matter> mantleChunk = getEngine().getMantle().getMantle()
|
||||
.getChunk(x >> 4, z >> 4).use();
|
||||
try {
|
||||
MetadataCleaner metadata = new MetadataCleaner(mantleChunk, blocks.getHeight());
|
||||
for (int localX = 0; localX < blocks.getWidth(); localX++) {
|
||||
int worldX = x + localX;
|
||||
for (int localZ = 0; localZ < blocks.getDepth(); localZ++) {
|
||||
renderLayers(
|
||||
localX,
|
||||
localZ,
|
||||
worldX,
|
||||
z + localZ,
|
||||
blocks,
|
||||
context.getDimensionStackLayout(localX, localZ),
|
||||
metadata
|
||||
);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
mantleChunk.release();
|
||||
}
|
||||
}
|
||||
|
||||
private void renderLayers(
|
||||
int localX,
|
||||
int localZ,
|
||||
int worldX,
|
||||
int worldZ,
|
||||
Hunk<PlatformBlockState> blocks,
|
||||
DimensionStackLayout layout,
|
||||
MetadataCleaner metadata
|
||||
) {
|
||||
if (layout == null) {
|
||||
return;
|
||||
}
|
||||
List<DimensionStackLayout.Layer> layers = layout.layersBottomToTop();
|
||||
for (int layerIndex = 1; layerIndex < layers.size(); layerIndex++) {
|
||||
DimensionStackLayout.Layer layer = layers.get(layerIndex);
|
||||
clearGap(localX, localZ, blocks, layers.get(layerIndex - 1), layer, metadata);
|
||||
if (layer.visible()) {
|
||||
renderLayer(localX, localZ, worldX, worldZ, blocks, layer, metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clearGap(
|
||||
int localX,
|
||||
int localZ,
|
||||
Hunk<PlatformBlockState> output,
|
||||
DimensionStackLayout.Layer lower,
|
||||
DimensionStackLayout.Layer upper,
|
||||
MetadataCleaner metadata
|
||||
) {
|
||||
int minimumY = (int) Math.max(0L, (long) lower.contentTopY() + 1L);
|
||||
int maximumY = (int) Math.min(
|
||||
(long) output.getHeight() - 1L,
|
||||
(long) upper.localBaseY() - 1L
|
||||
);
|
||||
for (int y = minimumY; y <= maximumY; y++) {
|
||||
metadata.clear(localX, y, localZ);
|
||||
output.setRaw(localX, y, localZ, AIR);
|
||||
}
|
||||
}
|
||||
|
||||
private void renderLayer(
|
||||
int localX,
|
||||
int localZ,
|
||||
int worldX,
|
||||
int worldZ,
|
||||
Hunk<PlatformBlockState> output,
|
||||
DimensionStackLayout.Layer layer,
|
||||
MetadataCleaner metadata
|
||||
) {
|
||||
DimensionTerrainContext terrainContext = layer.terrainContext();
|
||||
IrisDimension dimension = terrainContext.getDimension();
|
||||
IrisData data = terrainContext.getData();
|
||||
IrisBiome biome = layer.biome();
|
||||
int surfaceDepth = Math.max(0, layer.normalTerrainHeight());
|
||||
int fluidDepth = Math.max(0, layer.fluidHeight() - layer.normalTerrainHeight());
|
||||
KList<PlatformBlockState> surfaceBlocks = biome == null
|
||||
? null
|
||||
: biome.generateLayersWithSlope(
|
||||
dimension,
|
||||
worldX,
|
||||
worldZ,
|
||||
rng,
|
||||
surfaceDepth,
|
||||
layer.normalTerrainHeight(),
|
||||
data,
|
||||
terrainContext.getSlopeStream()
|
||||
);
|
||||
KList<PlatformBlockState> seaBlocks = biome == null || fluidDepth == 0
|
||||
? null
|
||||
: biome.generateSeaLayers(worldX, worldZ, rng, fluidDepth, data);
|
||||
|
||||
for (int y = layer.renderMaxY(); y >= layer.renderMinY(); y--) {
|
||||
int sourceY = y - layer.localBaseY();
|
||||
if (sourceY == 0 && dimension.isBedrock()) {
|
||||
writeBlock(output, metadata, localX, y, localZ, BEDROCK);
|
||||
continue;
|
||||
}
|
||||
if (sourceY > layer.normalTerrainHeight() && sourceY <= layer.fluidHeight()) {
|
||||
int fluidLayerDepth = layer.fluidY() - y;
|
||||
writeBlock(output, metadata, localX, y, localZ, HydrologyFluidLayerSelector.select(
|
||||
seaBlocks,
|
||||
fluidLayerDepth,
|
||||
layer.fluidBlock(),
|
||||
false
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if (sourceY > layer.normalTerrainHeight()) {
|
||||
continue;
|
||||
}
|
||||
int depth = layer.surfaceY() - y;
|
||||
if (depth == 0 && layer.surfaceBlock() != null) {
|
||||
writeBlock(output, metadata, localX, y, localZ, layer.surfaceBlock());
|
||||
} else if (surfaceBlocks != null && surfaceBlocks.hasIndex(depth)) {
|
||||
writeBlock(output, metadata, localX, y, localZ, surfaceBlocks.get(depth));
|
||||
} else {
|
||||
writeBlock(output, metadata, localX, y, localZ, layer.rockBlock());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void writeBlock(
|
||||
Hunk<PlatformBlockState> output,
|
||||
MetadataCleaner metadata,
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
PlatformBlockState block
|
||||
) {
|
||||
metadata.clear(x, y, z);
|
||||
output.setRaw(x, y, z, block);
|
||||
}
|
||||
|
||||
static final class MetadataCleaner {
|
||||
private final MantleChunk<Matter> chunk;
|
||||
private final List<MatterSlice<?>>[] slicesBySection;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
MetadataCleaner(MantleChunk<Matter> chunk, int height) {
|
||||
this.chunk = chunk;
|
||||
slicesBySection = (List<MatterSlice<?>>[]) new List<?>[(height + 15) >> 4];
|
||||
}
|
||||
|
||||
void clear(int x, int y, int z) {
|
||||
int sectionIndex = y >> 4;
|
||||
List<MatterSlice<?>> slices = slicesBySection[sectionIndex];
|
||||
if (slices == null) {
|
||||
slices = resolveSlices(sectionIndex);
|
||||
slicesBySection[sectionIndex] = slices;
|
||||
}
|
||||
for (MatterSlice<?> slice : slices) {
|
||||
slice.set(x, y & 15, z, null);
|
||||
}
|
||||
}
|
||||
|
||||
private List<MatterSlice<?>> resolveSlices(int sectionIndex) {
|
||||
Matter section = chunk.get(sectionIndex);
|
||||
if (section == null) {
|
||||
return List.of();
|
||||
}
|
||||
ArrayList<MatterSlice<?>> slices = new ArrayList<>(REPLACED_METADATA.size());
|
||||
for (Class<?> type : REPLACED_METADATA) {
|
||||
MatterSlice<?> slice = section.getSlice(type);
|
||||
if (slice != null) {
|
||||
slices.add(slice);
|
||||
}
|
||||
}
|
||||
return List.copyOf(slices);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,9 @@ import art.arcane.iris.core.nms.container.BlockPos;
|
||||
import art.arcane.iris.core.nms.container.Pair;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.UpperDimensionContext;
|
||||
import art.arcane.iris.engine.DimensionStackContext;
|
||||
import art.arcane.iris.engine.DimensionStackLayout;
|
||||
import art.arcane.iris.engine.DimensionTerrainContext;
|
||||
import art.arcane.iris.engine.data.chunk.TerrainChunk;
|
||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
@@ -90,6 +93,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
return null;
|
||||
}
|
||||
|
||||
default @Nullable DimensionStackContext getDimensionStackContext() {
|
||||
return null;
|
||||
}
|
||||
|
||||
EngineMode getMode();
|
||||
|
||||
EnginePlatformHooks getPlatformHooks();
|
||||
@@ -255,6 +262,25 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
@BlockCoordinates
|
||||
default IrisRegion getRegion(int x, int z) {
|
||||
DimensionStackContext dimensionStackContext = getDimensionStackContext();
|
||||
if (dimensionStackContext != null) {
|
||||
DimensionStackLayout.Layer layer = dimensionStackContext.getLayout(x, z).surfaceLayer();
|
||||
if (layer != null && layer.region() != null) {
|
||||
return layer.region();
|
||||
}
|
||||
}
|
||||
return getComplex().getRegionStream().get(x, z);
|
||||
}
|
||||
|
||||
@BlockCoordinates
|
||||
default IrisRegion getRegion(int x, int y, int z) {
|
||||
DimensionStackContext dimensionStackContext = getDimensionStackContext();
|
||||
if (dimensionStackContext != null) {
|
||||
DimensionStackLayout.Layer layer = dimensionStackContext.getLayout(x, z).layerAt(y);
|
||||
if (layer != null && layer.region() != null) {
|
||||
return layer.region();
|
||||
}
|
||||
}
|
||||
return getComplex().getRegionStream().get(x, z);
|
||||
}
|
||||
|
||||
@@ -301,7 +327,45 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
@BlockCoordinates
|
||||
default IrisBiome getCaveBiome(int x, int y, int z, IrisDimensionCarvingResolver.State state) {
|
||||
IrisBiome surfaceBiome = getSurfaceBiome(x, z);
|
||||
IrisBiome configuredBiome = resolveConfiguredCaveBiome(x, y, z, state);
|
||||
if (configuredBiome != null) {
|
||||
return configuredBiome;
|
||||
}
|
||||
boolean naturalFallback = answersFromNaturalTerrain(x, z);
|
||||
DimensionStackContext dimensionStackContext = getDimensionStackContext();
|
||||
IrisBiome surfaceBiome = naturalFallback || dimensionStackContext == null
|
||||
? naturalFallback
|
||||
? getComplex().naturalSurfaceBiome(x, z)
|
||||
: getSurfaceBiome(x, z)
|
||||
: getComplex().getTrueBiomeStream().get(x, z);
|
||||
int surfaceY = naturalFallback
|
||||
? getComplex().naturalTrueHeight(x, z)
|
||||
: getComplex().getHeightStream().get(x, z).intValue();
|
||||
return resolveDepthCaveBiome(x, y, z, surfaceBiome, surfaceY);
|
||||
}
|
||||
|
||||
@BlockCoordinates
|
||||
default IrisBiome getCaveBiome(
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
IrisDimensionCarvingResolver.State state,
|
||||
IrisBiome surfaceBiome,
|
||||
int surfaceY
|
||||
) {
|
||||
IrisBiome configuredBiome = resolveConfiguredCaveBiome(x, y, z, state);
|
||||
if (configuredBiome != null) {
|
||||
return configuredBiome;
|
||||
}
|
||||
return resolveDepthCaveBiome(x, y, z, surfaceBiome, surfaceY);
|
||||
}
|
||||
|
||||
private IrisBiome resolveConfiguredCaveBiome(
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
IrisDimensionCarvingResolver.State state
|
||||
) {
|
||||
int worldY = y + getWorld().minHeight();
|
||||
IrisDimensionCarvingEntry rootCarvingEntry = IrisDimensionCarvingResolver.resolveRootEntry(this, worldY, state);
|
||||
if (rootCarvingEntry != null) {
|
||||
@@ -311,13 +375,21 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
return resolvedCarvingBiome;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private IrisBiome resolveDepthCaveBiome(
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
IrisBiome surfaceBiome,
|
||||
int surfaceY
|
||||
) {
|
||||
IrisBiome caveBiome = getCaveBiome(x, z);
|
||||
if (caveBiome == null) {
|
||||
return surfaceBiome;
|
||||
}
|
||||
|
||||
int surfaceY = getComplex().getHeightStream().get(x, z).intValue();
|
||||
int depthBelowSurface = surfaceY - y;
|
||||
if (depthBelowSurface <= 0) {
|
||||
return surfaceBiome;
|
||||
@@ -333,6 +405,18 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
@BlockCoordinates
|
||||
default IrisBiome getSurfaceBiome(int x, int z) {
|
||||
DimensionStackContext dimensionStackContext = getDimensionStackContext();
|
||||
if (dimensionStackContext != null) {
|
||||
DimensionStackLayout.Layer layer = dimensionStackContext.getLayout(x, z).surfaceLayer();
|
||||
if (layer != null && layer.biome() != null) {
|
||||
return layer.biome();
|
||||
}
|
||||
}
|
||||
return getHostSurfaceBiome(x, z);
|
||||
}
|
||||
|
||||
@BlockCoordinates
|
||||
default IrisBiome getHostSurfaceBiome(int x, int z) {
|
||||
if (answersFromNaturalTerrain(x, z)) {
|
||||
return getComplex().naturalSurfaceBiome(x, z);
|
||||
}
|
||||
@@ -364,6 +448,12 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
@BlockCoordinates
|
||||
default int getHeight(int x, int z, boolean ignoreFluid) {
|
||||
DimensionStackContext dimensionStackContext = getDimensionStackContext();
|
||||
if (dimensionStackContext != null) {
|
||||
return ignoreFluid
|
||||
? dimensionStackContext.getStackTerrainHeight(x, z)
|
||||
: dimensionStackContext.getStackTopHeight(x, z);
|
||||
}
|
||||
if (answersFromNaturalTerrain(x, z)) {
|
||||
int natural = getComplex().naturalTrueHeight(x, z);
|
||||
return ignoreFluid ? natural : Math.max(natural, getMantle().getFluidHeight());
|
||||
@@ -371,6 +461,24 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
return getMantle().getHighest(x, z, getData(), ignoreFluid);
|
||||
}
|
||||
|
||||
@BlockCoordinates
|
||||
static int hostHeight(Engine engine, int x, int z, boolean ignoreFluid) {
|
||||
EngineMantle mantle = engine.getMantle();
|
||||
if (mantle == null) {
|
||||
return engine.getHeight(x, z, ignoreFluid);
|
||||
}
|
||||
if (engine.answersFromNaturalTerrain(x, z)) {
|
||||
int naturalHeight = engine.getComplex().naturalTrueHeight(x, z);
|
||||
return ignoreFluid
|
||||
? naturalHeight
|
||||
: Math.max(naturalHeight, mantle.getFluidHeight());
|
||||
}
|
||||
int terrainHeight = mantle.trueHeight(x, z);
|
||||
return ignoreFluid
|
||||
? terrainHeight
|
||||
: Math.max(terrainHeight, mantle.getFluidHeight(x, z));
|
||||
}
|
||||
|
||||
@BlockCoordinates
|
||||
@Override
|
||||
default void catchBlockUpdates(int x, int y, int z, PlatformBlockState data) {
|
||||
@@ -410,6 +518,15 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
IrisDimension dim = getDimension();
|
||||
dim.getReachableBiomes(this).forEach((i) -> v.put(i.getLoadKey(), i));
|
||||
DimensionStackContext dimensionStackContext = getDimensionStackContext();
|
||||
if (dimensionStackContext != null) {
|
||||
for (DimensionTerrainContext terrainContext : dimensionStackContext.getLayersBottomToTop()) {
|
||||
if (!terrainContext.isSelfReferencing()) {
|
||||
terrainContext.getDimension().getReachableBiomes(terrainContext)
|
||||
.forEach((i) -> v.put(i.getLoadKey(), i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return v.v();
|
||||
}
|
||||
@@ -454,6 +571,22 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
boolean isStudio();
|
||||
|
||||
default IrisBiome getBiome(int x, int y, int z) {
|
||||
DimensionStackContext dimensionStackContext = getDimensionStackContext();
|
||||
if (dimensionStackContext != null) {
|
||||
DimensionStackLayout layout = dimensionStackContext.getLayout(x, z);
|
||||
IrisBiome stackedBiome = dimensionStackBiome(layout, y);
|
||||
if (stackedBiome != null) {
|
||||
return stackedBiome;
|
||||
}
|
||||
DimensionStackLayout.Layer bottomLayer = layout.layersBottomToTop().get(0);
|
||||
if (y > bottomLayer.renderMaxY()) {
|
||||
return bottomLayer.biome();
|
||||
}
|
||||
if (y <= bottomLayer.clippedSurfaceY() - 2) {
|
||||
return getCaveBiome(x, y, z);
|
||||
}
|
||||
return bottomLayer.biome();
|
||||
}
|
||||
if (y <= getHeight(x, z) - 2) {
|
||||
return getCaveBiome(x, y, z);
|
||||
}
|
||||
@@ -462,6 +595,22 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
}
|
||||
|
||||
default IrisBiome getBiomeOrMantle(int x, int y, int z) {
|
||||
DimensionStackContext dimensionStackContext = getDimensionStackContext();
|
||||
if (dimensionStackContext != null) {
|
||||
DimensionStackLayout layout = dimensionStackContext.getLayout(x, z);
|
||||
IrisBiome stackedBiome = dimensionStackBiome(layout, y);
|
||||
if (stackedBiome != null) {
|
||||
return stackedBiome;
|
||||
}
|
||||
DimensionStackLayout.Layer bottomLayer = layout.layersBottomToTop().get(0);
|
||||
if (y > bottomLayer.renderMaxY()) {
|
||||
return bottomLayer.biome();
|
||||
}
|
||||
if (y <= bottomLayer.clippedSurfaceY() - 2) {
|
||||
return getCaveOrMantleBiome(x, y, z);
|
||||
}
|
||||
return bottomLayer.biome();
|
||||
}
|
||||
if (y <= getHeight(x, z) - 2) {
|
||||
return getCaveOrMantleBiome(x, y, z);
|
||||
}
|
||||
@@ -469,6 +618,12 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
return getSurfaceBiome(x, z);
|
||||
}
|
||||
|
||||
private static IrisBiome dimensionStackBiome(DimensionStackLayout layout, int y) {
|
||||
DimensionStackLayout.Layer bottom = layout.layersBottomToTop().get(0);
|
||||
DimensionStackLayout.Layer owner = layout.layerAt(y);
|
||||
return owner == bottom ? null : owner.biome();
|
||||
}
|
||||
|
||||
default String getObjectPlacementKey(int x, int y, int z) {
|
||||
PlacedObject o = getObjectPlacement(x, y, z);
|
||||
|
||||
@@ -524,7 +679,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
}
|
||||
|
||||
|
||||
IrisRegion region = getRegion(x, z);
|
||||
IrisRegion region = getComplex().getRegionStream().get(x, z);
|
||||
|
||||
for (IrisObjectPlacement i : region.getObjects()) {
|
||||
if (i.compatPlace(getData()).contains(object)) {
|
||||
@@ -532,7 +687,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
}
|
||||
}
|
||||
|
||||
IrisBiome biome = getSurfaceBiome(x, z);
|
||||
IrisBiome biome = getComplex().getTrueBiomeStream().get(x, z);
|
||||
|
||||
for (IrisObjectPlacement i : biome.getObjects()) {
|
||||
if (i.compatPlace(getData()).contains(object)) {
|
||||
|
||||
@@ -151,7 +151,16 @@ public interface EngineMode extends Staged {
|
||||
default void generate(int x, int z, Hunk<PlatformBlockState> blocks, Hunk<PlatformBiome> biomes, boolean multicore, long generationSessionId) {
|
||||
boolean cacheContext = !getEngine().getPlatformHooks().shouldDisableChunkContextCache(getEngine());
|
||||
ChunkContext.PrefillPlan prefillPlan = cacheContext ? ChunkContext.PrefillPlan.NO_CAVE : ChunkContext.PrefillPlan.NONE;
|
||||
ChunkContext ctx = new ChunkContext(x, z, getComplex(), generationSessionId, cacheContext, prefillPlan, getEngine().getMetrics());
|
||||
ChunkContext ctx = new ChunkContext(
|
||||
x,
|
||||
z,
|
||||
getComplex(),
|
||||
generationSessionId,
|
||||
cacheContext,
|
||||
prefillPlan,
|
||||
getEngine().getMetrics(),
|
||||
getEngine().getDimensionStackContext()
|
||||
);
|
||||
|
||||
EngineStage[] stages = getStages().toArray(new EngineStage[0]);
|
||||
try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), generationSessionId, ctx)) {
|
||||
|
||||
@@ -46,6 +46,9 @@ public interface EnginePlatformHooks {
|
||||
default void validateDimensionHotload(Engine engine, IrisDimension replacement) {
|
||||
}
|
||||
|
||||
default void prepareRuntimeHotload(Engine engine) {
|
||||
}
|
||||
|
||||
default void applyWorldBoundary(Engine engine) {
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
package art.arcane.iris.engine.framework;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.engine.DimensionStackContext;
|
||||
import art.arcane.iris.engine.DimensionTerrainContext;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
@@ -282,6 +284,13 @@ public final class HintedLocator<T> implements Locator<T> {
|
||||
if (engine.getFocus() != null || engine.getFocusRegion() != null) {
|
||||
return SearchPlan.unpruned();
|
||||
}
|
||||
if (engine.getDimensionStackContext() != null) {
|
||||
for (IrisBiome biome : engine.getAllBiomes()) {
|
||||
if (biome != null && biomeKey.equals(biome.getLoadKey())) {
|
||||
return SearchPlan.unpruned();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IrisComplex complex = engine.getComplex();
|
||||
BiomeSource source = new BiomeSource(engine);
|
||||
@@ -380,6 +389,17 @@ public final class HintedLocator<T> implements Locator<T> {
|
||||
return SearchPlan.unpruned();
|
||||
}
|
||||
|
||||
DimensionStackContext stackContext = engine.getDimensionStackContext();
|
||||
if (stackContext != null) {
|
||||
for (DimensionTerrainContext terrainContext : stackContext.getLayersTopToBottom()) {
|
||||
for (IrisRegion region : terrainContext.getDimension().getAllRegions(terrainContext)) {
|
||||
if (region != null && regionKey.equals(region.getLoadKey())) {
|
||||
return SearchPlan.unpruned();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!engine.getDimension().getRegions().contains(regionKey)) {
|
||||
return SearchPlan.impossible();
|
||||
}
|
||||
|
||||
@@ -655,7 +655,7 @@ public final class IrisStructureLocator {
|
||||
return null;
|
||||
}
|
||||
if (placement.resolvedAnchor() == IrisStructureAnchorMode.SURFACE) {
|
||||
int surfaceY = engine.getHeight(originX, originZ, true) + engine.getMinHeight();
|
||||
int surfaceY = Engine.hostHeight(engine, originX, originZ, true) + engine.getMinHeight();
|
||||
return surfaceY < placement.getMinHeight() || surfaceY > placement.getMaxHeight() ? null : surfaceY;
|
||||
}
|
||||
int bandMin = Math.max(worldMin, Math.min(placement.getMinHeight(), placement.getMaxHeight()));
|
||||
@@ -766,7 +766,7 @@ public final class IrisStructureLocator {
|
||||
if (surfaceHeights.size() >= MAX_BURIAL_COLUMNS) {
|
||||
return Integer.MIN_VALUE;
|
||||
}
|
||||
surfaceY = engine.getHeight(x, z, true) + engine.getMinHeight();
|
||||
surfaceY = Engine.hostHeight(engine, x, z, true) + engine.getMinHeight();
|
||||
surfaceHeights.put(columnKey, surfaceY);
|
||||
}
|
||||
int allowedTopY = surfaceY - UNDERGROUND_SURFACE_CLEARANCE;
|
||||
|
||||
+2
-2
@@ -142,7 +142,7 @@ public final class NativeStructurePlacementPlanner {
|
||||
return null;
|
||||
}
|
||||
if (!placement.isUnderground()) {
|
||||
int surfaceY = engine.getHeight(blockX, blockZ, true) + engine.getMinHeight();
|
||||
int surfaceY = Engine.hostHeight(engine, blockX, blockZ, true) + engine.getMinHeight();
|
||||
if (surfaceY < placement.getMinHeight() || surfaceY > placement.getMaxHeight()) {
|
||||
return null;
|
||||
}
|
||||
@@ -160,7 +160,7 @@ public final class NativeStructurePlacementPlanner {
|
||||
int localFluidHeight = engine.getComplex() == null
|
||||
? engine.getDimension().getFluidHeight()
|
||||
: (int) Math.round(engine.getComplex().getRiverWaterSurfaceStream().get(blockX, blockZ));
|
||||
return engine.getHeight(blockX, blockZ, true) < localFluidHeight;
|
||||
return Engine.hostHeight(engine, blockX, blockZ, true) < localFluidHeight;
|
||||
}
|
||||
|
||||
private static int comparePlacementPriority(IrisStructurePlacement left, IrisStructurePlacement right) {
|
||||
|
||||
@@ -21,6 +21,8 @@ package art.arcane.iris.engine.mantle;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.DimensionStackContext;
|
||||
import art.arcane.iris.engine.DimensionStackLayout;
|
||||
import art.arcane.iris.engine.UpperDimensionContext;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineTarget;
|
||||
@@ -36,6 +38,7 @@ import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.volmlib.util.documentation.BlockCoordinates;
|
||||
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.iris.util.project.context.ChunkContext;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
@@ -196,17 +199,37 @@ public interface EngineMantle extends MatterGenerator {
|
||||
}
|
||||
|
||||
@ChunkCoordinates
|
||||
default void insertMatter(int x, int z, Hunk<PlatformBlockState> blocks, boolean multicore) {
|
||||
default void insertMatter(
|
||||
int x,
|
||||
int z,
|
||||
Hunk<PlatformBlockState> blocks,
|
||||
boolean multicore,
|
||||
ChunkContext context
|
||||
) {
|
||||
if (!getEngine().getDimension().isUseMantle()) {
|
||||
return;
|
||||
}
|
||||
|
||||
UpperDimensionContext upperCtx = getEngine().getUpperContext();
|
||||
boolean protectUpper = upperCtx != null;
|
||||
DimensionStackContext stackContext = getEngine().getDimensionStackContext();
|
||||
|
||||
MantleChunk<Matter> chunk = getMantle().getChunk(x, z).use();
|
||||
try {
|
||||
if (protectUpper) {
|
||||
if (stackContext != null) {
|
||||
DimensionStackLayout[] layouts = new DimensionStackLayout[256];
|
||||
for (int i = 0; i < layouts.length; i++) {
|
||||
int localX = i >> 4;
|
||||
int localZ = i & 15;
|
||||
layouts[i] = context.getDimensionStackLayout(localX, localZ);
|
||||
}
|
||||
chunk.iterate(PlatformBlockState.class, (localX, y, localZ, value) -> {
|
||||
DimensionStackLayout layout = layouts[(localX << 4) | (localZ & 15)];
|
||||
if (!layout.isHostFeatureProtectedY(y)) {
|
||||
blocks.set(localX, y, localZ, value);
|
||||
}
|
||||
});
|
||||
} else if (protectUpper) {
|
||||
int chunkBlockX = x << 4;
|
||||
int chunkBlockZ = z << 4;
|
||||
int gap = getEngine().getDimension().getUpperDimensionGap();
|
||||
|
||||
+1
-1
@@ -227,7 +227,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
if (cached != CACHE_MISS) {
|
||||
return cached;
|
||||
}
|
||||
int surfaceY = engine.getHeight(x, z, true);
|
||||
int surfaceY = Engine.hostHeight(engine, x, z, true);
|
||||
surfaceHeights.put(key, surfaceY);
|
||||
return surfaceY;
|
||||
}
|
||||
|
||||
@@ -286,7 +286,7 @@ public class IrisCaveCarver3D {
|
||||
if (precomputedSurfaceHeights != null && precomputedSurfaceHeights.length > index) {
|
||||
columnSurfaceY = precomputedSurfaceHeights[index];
|
||||
} else {
|
||||
columnSurfaceY = engine.getHeight(x, z);
|
||||
columnSurfaceY = Engine.hostHeight(engine, x, z, true);
|
||||
}
|
||||
int unclampedClearanceTopY = columnSurfaceY - surfaceClearance;
|
||||
int clearanceTopY = Math.min(maxY, Math.max(minY, unclampedClearanceTopY));
|
||||
|
||||
+7
-5
@@ -189,7 +189,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
int zzz = 8 + (z << 4);
|
||||
IrisRegion region = complex.getRegionStream().get(xxx, zzz);
|
||||
IrisBiome surfaceBiome = complex.getTrueBiomeStream().get(xxx, zzz);
|
||||
int surfaceY = getEngineMantle().getEngine().getHeight(xxx, zzz, true);
|
||||
int surfaceY = complex.getRoundedHeighteightStream().get(xxx, zzz);
|
||||
IrisBiome caveBiome = resolveCaveObjectBiome(xxx, zzz, surfaceY, surfaceBiome);
|
||||
if (IrisSettings.get().getGeneral().isDebug() && (x & 31) == 0 && (z & 31) == 0) {
|
||||
int carvedBlocks = 0;
|
||||
@@ -948,7 +948,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
Engine engine = getEngineMantle().getEngine();
|
||||
IrisBiome at = engine.getCaveBiome(x, y, z);
|
||||
IrisBiome surface = engine.getSurfaceBiome(x, z);
|
||||
IrisBiome surface = engine.getComplex().getTrueBiomeStream().get(x, z);
|
||||
return caveAnchorBiomeConflicts(at, surface, expectedCaveBiomeKey);
|
||||
}
|
||||
|
||||
@@ -1631,8 +1631,10 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
public Set<String> guess(int x, int z) {
|
||||
// todo The guess doesnt bring into account that the placer may return -1
|
||||
RNG rng = applyNoise(x, z, Cache.key(x, z) + seed());
|
||||
IrisBiome biome = getEngineMantle().getEngine().getSurfaceBiome((x << 4) + 8, (z << 4) + 8);
|
||||
IrisRegion region = getEngineMantle().getEngine().getRegion((x << 4) + 8, (z << 4) + 8);
|
||||
int blockX = (x << 4) + 8;
|
||||
int blockZ = (z << 4) + 8;
|
||||
IrisBiome biome = getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
IrisRegion region = getComplex().getRegionStream().get(blockX, blockZ);
|
||||
Set<String> v = new KSet<>();
|
||||
for (IrisObjectPlacement i : biome.getSurfaceObjects()) {
|
||||
if (rng.chance(i.getChance() + rng.d(-0.005, 0.005)) && !i.isCompatExcluded(getData())) {
|
||||
@@ -1879,7 +1881,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
return surfaceY;
|
||||
}
|
||||
|
||||
surfaceY = getEngineMantle().getEngine().getHeight(x, z);
|
||||
surfaceY = getEngineMantle().trueHeight(x, z);
|
||||
surfaceHeights.put(columnKey, surfaceY);
|
||||
return surfaceY;
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ import art.arcane.iris.engine.framework.IrisEngineMode;
|
||||
public class ModeEnclosure extends IrisEngineMode implements EngineMode {
|
||||
public ModeEnclosure(Engine engine) {
|
||||
super(engine);
|
||||
var terrain = new IrisTerrainNormalActuator(getEngine());
|
||||
var biome = new IrisBiomeActuator(getEngine());
|
||||
IrisTerrainNormalActuator terrain = new IrisTerrainNormalActuator(getEngine());
|
||||
IrisBiomeActuator biome = new IrisBiomeActuator(getEngine());
|
||||
|
||||
registerStage(burst(
|
||||
(x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c),
|
||||
|
||||
@@ -27,8 +27,8 @@ import art.arcane.iris.engine.framework.IrisEngineMode;
|
||||
public class ModeIslands extends IrisEngineMode implements EngineMode {
|
||||
public ModeIslands(Engine engine) {
|
||||
super(engine);
|
||||
var terrain = new IrisTerrainNormalActuator(getEngine());
|
||||
var biome = new IrisBiomeActuator(getEngine());
|
||||
IrisTerrainNormalActuator terrain = new IrisTerrainNormalActuator(getEngine());
|
||||
IrisBiomeActuator biome = new IrisBiomeActuator(getEngine());
|
||||
|
||||
registerStage(burst(
|
||||
(x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c),
|
||||
|
||||
@@ -87,7 +87,7 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
|
||||
if (shouldBypassMantleStages()) {
|
||||
return;
|
||||
}
|
||||
getMantle().insertMatter(x >> 4, z >> 4, K, m);
|
||||
getMantle().insertMatter(x >> 4, z >> 4, K, m, c);
|
||||
};
|
||||
EngineStage sFloatingTerrainSolid = (x, z, k, p, m, c) -> floatingChildBiomes.modify(x, z, k, m, c);
|
||||
EngineStage sFloatingDecorate = (x, z, k, p, m, c) -> floatingChildBiomes.decorateColumns(x, z, k, m, c);
|
||||
@@ -118,7 +118,9 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
|
||||
registerStage(sDecorant);
|
||||
registerStage(sFloatingDecorate);
|
||||
registerStage(sPerfection);
|
||||
registerStage(sCustom);
|
||||
if (getEngine().getDimensionStackContext() == null) {
|
||||
registerStage(sCustom);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldBypassMantleStages() {
|
||||
|
||||
@@ -27,8 +27,8 @@ import art.arcane.iris.engine.framework.IrisEngineMode;
|
||||
public class ModeSuperFlat extends IrisEngineMode implements EngineMode {
|
||||
public ModeSuperFlat(Engine engine) {
|
||||
super(engine);
|
||||
var terrain = new IrisTerrainNormalActuator(getEngine());
|
||||
var biome = new IrisBiomeActuator(getEngine());
|
||||
IrisTerrainNormalActuator terrain = new IrisTerrainNormalActuator(getEngine());
|
||||
IrisBiomeActuator biome = new IrisBiomeActuator(getEngine());
|
||||
|
||||
registerStage(burst(
|
||||
(x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c),
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
package art.arcane.iris.engine.modifier;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.DimensionStackContext;
|
||||
import art.arcane.iris.engine.DimensionStackLayout;
|
||||
import art.arcane.iris.engine.UpperDimensionContext;
|
||||
import art.arcane.iris.engine.actuator.IrisDecorantActuator;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
@@ -91,6 +93,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
UpperDimensionContext upperCtx = getEngine().getUpperContext();
|
||||
boolean protectUpper = upperCtx != null && !getEngine().getDimension().isUpperDimensionCarving();
|
||||
int[] upperSurfaceHeights = protectUpper ? scratch.getOrCreateUpperSurfaceHeights() : null;
|
||||
DimensionStackContext stackContext = getEngine().getDimensionStackContext();
|
||||
DimensionStackLayout[] stackLayouts = stackContext == null ? null : new DimensionStackLayout[256];
|
||||
int chunkBlockX = PowerOfTwoCoordinates.chunkToBlock(x);
|
||||
int chunkBlockZ = PowerOfTwoCoordinates.chunkToBlock(z);
|
||||
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
|
||||
@@ -104,6 +108,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
int gap = getEngine().getDimension().getUpperDimensionGap();
|
||||
upperSurfaceHeights[columnIndex] = Math.max(rawUpper, surfaceHeights[columnIndex] + gap);
|
||||
}
|
||||
if (stackLayouts != null) {
|
||||
stackLayouts[columnIndex] = context.getDimensionStackLayout(localX, localZ);
|
||||
}
|
||||
}
|
||||
|
||||
MantleChunk<Matter> mantleChunk = mantle.getChunk(x, z).use();
|
||||
@@ -117,6 +124,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
scratch,
|
||||
columnMasks,
|
||||
upperSurfaceHeights,
|
||||
stackLayouts,
|
||||
worldHeightSpan,
|
||||
caveLavaHeight,
|
||||
chunkBlockX,
|
||||
@@ -348,6 +356,10 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
if (context.upperSurfaceHeights() != null && y >= context.upperSurfaceHeights()[columnIndex]) {
|
||||
return;
|
||||
}
|
||||
if (context.stackLayouts() != null
|
||||
&& context.stackLayouts()[columnIndex].isHostFeatureProtectedY(y)) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlatformBlockState current = context.output().getRaw(localX, y, localZ);
|
||||
if (hydrology != null && hydrology.action() == HydrologyCaveAction.SEAL_GUARD) {
|
||||
@@ -405,6 +417,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
IrisCarveScratch scratch,
|
||||
CarveColumnMask[] columnMasks,
|
||||
int[] upperSurfaceHeights,
|
||||
DimensionStackLayout[] stackLayouts,
|
||||
int worldHeightSpan,
|
||||
int caveLavaHeight,
|
||||
int chunkBlockX,
|
||||
|
||||
@@ -20,6 +20,7 @@ package art.arcane.iris.engine.modifier;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineAssignedModifier;
|
||||
import art.arcane.iris.engine.DimensionStackLayout;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDepositGenerator;
|
||||
import art.arcane.iris.engine.object.IrisDepositHeightDistribution;
|
||||
@@ -186,6 +187,11 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
|
||||
continue;
|
||||
}
|
||||
|
||||
DimensionStackLayout stackLayout = context.getDimensionStackLayout(nx, nz);
|
||||
if (stackLayout != null && stackLayout.isHostFeatureProtectedY(ny)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
PlatformBlockState current = data.get(nx, ny, nz);
|
||||
if (!canReplaceDepositTarget(current)) {
|
||||
continue;
|
||||
|
||||
+28
-3
@@ -20,6 +20,7 @@ package art.arcane.iris.engine.modifier;
|
||||
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.DimensionStackLayout;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.decorator.FloatingDecorator;
|
||||
import art.arcane.iris.engine.decorator.IrisSeaSurfaceDecorator;
|
||||
@@ -285,7 +286,15 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
|
||||
}
|
||||
}
|
||||
|
||||
writeIslandSkyBiomes(parent, wx, wz, sample, chunkHeight, data);
|
||||
writeIslandSkyBiomes(
|
||||
parent,
|
||||
wx,
|
||||
wz,
|
||||
sample,
|
||||
chunkHeight,
|
||||
data,
|
||||
context.getDimensionStackLayout(xf, zf)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,7 +376,15 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
|
||||
}
|
||||
}
|
||||
|
||||
private void writeIslandSkyBiomes(IrisBiome parent, int wx, int wz, FloatingIslandSample sample, int chunkHeight, IrisData data) {
|
||||
private void writeIslandSkyBiomes(
|
||||
IrisBiome parent,
|
||||
int wx,
|
||||
int wz,
|
||||
FloatingIslandSample sample,
|
||||
int chunkHeight,
|
||||
IrisData data,
|
||||
DimensionStackLayout stackLayout
|
||||
) {
|
||||
try {
|
||||
IdentityHashMap<IrisFloatingChildBiomes, MatterBiomeInject> matterByEntry = new IdentityHashMap<>();
|
||||
for (int k = 0; k <= sample.topIdx; k++) {
|
||||
@@ -378,6 +395,9 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
|
||||
if (y < 0 || y >= chunkHeight) {
|
||||
continue;
|
||||
}
|
||||
if (!shouldWriteHostBiomeMarker(stackLayout, y)) {
|
||||
continue;
|
||||
}
|
||||
IrisFloatingChildBiomes entry = sample.entryAt(k);
|
||||
MatterBiomeInject matter = matterByEntry.get(entry);
|
||||
if (matter == null) {
|
||||
@@ -395,10 +415,15 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
|
||||
}
|
||||
}
|
||||
|
||||
static boolean shouldWriteHostBiomeMarker(DimensionStackLayout stackLayout, int y) {
|
||||
return stackLayout == null || !stackLayout.isHostFeatureProtectedY(y);
|
||||
}
|
||||
|
||||
private MatterBiomeInject createSkyBiomeMatter(IrisBiome target, int wx, int wz) {
|
||||
if (target.isCustom()) {
|
||||
IrisBiomeCustom custom = target.getCustomBiome(rng, getEngine(), wx, 0, wz);
|
||||
return BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(getDimension().getLoadKey() + ":" + custom.getId()));
|
||||
return BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(
|
||||
getDimension().getCustomBiomeKey(custom.getId())));
|
||||
}
|
||||
|
||||
return BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(target.getSkyBiomeKey(rng, getEngine(), wx, 0, wz)));
|
||||
|
||||
@@ -40,6 +40,7 @@ import art.arcane.iris.util.common.data.DataProvider;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.iris.util.project.noise.CNG;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
@@ -495,6 +496,10 @@ public class IrisBiome extends IrisRegistrant implements IRare {
|
||||
return IrisBiomeLayerGenerator.generateLayers(this, dim, wx, wz, random, maxDepth, height, rdata, complex);
|
||||
}
|
||||
|
||||
public KList<PlatformBlockState> generateLayersWithSlope(IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, ProceduralStream<Double> slopeStream) {
|
||||
return IrisBiomeLayerGenerator.generateLayers(this, dim, wx, wz, random, maxDepth, height, rdata, slopeStream);
|
||||
}
|
||||
|
||||
public KList<PlatformBlockState> generateCeilingLayers(IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex) {
|
||||
return IrisBiomeLayerGenerator.generateCeilingLayers(this, dim, wx, wz, random, maxDepth, height, rdata, complex);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.iris.util.project.noise.CNG;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
|
||||
@@ -42,8 +43,16 @@ final class IrisBiomeLayerGenerator {
|
||||
}
|
||||
|
||||
static KList<PlatformBlockState> generateLayers(IrisBiome biome, IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex) {
|
||||
return generateLayers(biome, dim, wx, wz, random, maxDepth, height, rdata, complex, null);
|
||||
}
|
||||
|
||||
static KList<PlatformBlockState> generateLayers(IrisBiome biome, IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, ProceduralStream<Double> slopeStream) {
|
||||
return generateLayers(biome, dim, wx, wz, random, maxDepth, height, rdata, null, slopeStream);
|
||||
}
|
||||
|
||||
private static KList<PlatformBlockState> generateLayers(IrisBiome biome, IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex, ProceduralStream<Double> slopeStream) {
|
||||
if (biome.isLockLayers()) {
|
||||
return generateLockedLayers(biome, wx, wz, random, maxDepth, height, rdata, complex);
|
||||
return generateLockedLayers(biome, wx, wz, random, maxDepth, height, rdata, complex, slopeStream);
|
||||
}
|
||||
|
||||
KList<PlatformBlockState> data = new KList<>();
|
||||
@@ -70,7 +79,7 @@ final class IrisBiomeLayerGenerator {
|
||||
IrisSlopeClip sc = layer.getSlopeCondition();
|
||||
|
||||
if (!sc.isDefault()) {
|
||||
if (!sc.isValid(complex.getSlopeStream().get(wx, wz))) {
|
||||
if (!sc.isValid(resolveSlopeStream(complex, slopeStream).getDouble(wx, wz))) {
|
||||
d = 0;
|
||||
}
|
||||
}
|
||||
@@ -168,6 +177,10 @@ final class IrisBiomeLayerGenerator {
|
||||
}
|
||||
|
||||
static KList<PlatformBlockState> generateLockedLayers(IrisBiome biome, double wx, double wz, RNG random, int maxDepthf, int height, IrisData rdata, IrisComplex complex) {
|
||||
return generateLockedLayers(biome, wx, wz, random, maxDepthf, height, rdata, complex, null);
|
||||
}
|
||||
|
||||
private static KList<PlatformBlockState> generateLockedLayers(IrisBiome biome, double wx, double wz, RNG random, int maxDepthf, int height, IrisData rdata, IrisComplex complex, ProceduralStream<Double> slopeStream) {
|
||||
KList<PlatformBlockState> data = new KList<>();
|
||||
KList<PlatformBlockState> real = new KList<>();
|
||||
int maxDepth = Math.min(maxDepthf, biome.getLockLayersMax());
|
||||
@@ -190,7 +203,7 @@ final class IrisBiomeLayerGenerator {
|
||||
IrisSlopeClip sc = layer.getSlopeCondition();
|
||||
|
||||
if (!sc.isDefault()) {
|
||||
if (!sc.isValid(complex.getSlopeStream().get(wx, wz))) {
|
||||
if (!sc.isValid(resolveSlopeStream(complex, slopeStream).getDouble(wx, wz))) {
|
||||
d = 0;
|
||||
}
|
||||
}
|
||||
@@ -222,6 +235,13 @@ final class IrisBiomeLayerGenerator {
|
||||
return real;
|
||||
}
|
||||
|
||||
private static ProceduralStream<Double> resolveSlopeStream(
|
||||
IrisComplex complex,
|
||||
ProceduralStream<Double> slopeStream
|
||||
) {
|
||||
return slopeStream == null ? complex.getSlopeStream() : slopeStream;
|
||||
}
|
||||
|
||||
static KList<PlatformBlockState> generateSeaLayers(IrisBiome biome, double wx, double wz, RNG random, int maxDepth, IrisData rdata) {
|
||||
KList<PlatformBlockState> data = new KList<>();
|
||||
|
||||
|
||||
@@ -222,6 +222,8 @@ public class IrisDimension extends IrisRegistrant {
|
||||
private boolean upperDimensionObjects = false;
|
||||
@Desc("When true, upper-dimension objects force-place regardless of placement restrictions (slope, underwater, clamp, collisions, carving). Normal dimension objects always place first; upper objects place second and may clip or occlude lower-dimension placements when this is enabled.")
|
||||
private boolean upperObjectsForcePlace = false;
|
||||
@Desc("Stack dimension terrain upright in declared top-to-bottom order. The final dimension key must reference this dimension.")
|
||||
private IrisDimensionStack dimensionStack = null;
|
||||
@RegistryListResource(IrisBiome.class)
|
||||
@Desc("Keep this either undefined or empty. Setting any biome name into this will force iris to only generate the specified biome. Great for testing.")
|
||||
private String focus = "";
|
||||
@@ -528,6 +530,12 @@ public class IrisDimension extends IrisRegistrant {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasFocusRegion()) {
|
||||
IrisRegion focus = data.getRegionLoader().load(getFocusRegion());
|
||||
if (focus != null && !focus.isCompatExcluded()) {
|
||||
regions.put(getFocusRegion(), focus);
|
||||
}
|
||||
}
|
||||
return regions.v();
|
||||
}
|
||||
|
||||
@@ -564,6 +572,7 @@ public class IrisDimension extends IrisRegistrant {
|
||||
}
|
||||
|
||||
Deque<String> pending = new ArrayDeque<>();
|
||||
addReachableBiomeKey(pending, getFocus());
|
||||
IrisHydrology configuredHydrology = getHydrology();
|
||||
IrisRiverHydrology configuredRivers = configuredHydrology == null ? null : configuredHydrology.getRivers();
|
||||
boolean hydrologyPoliciesActive = configuredRivers != null && configuredRivers.isEnabled();
|
||||
@@ -721,8 +730,14 @@ public class IrisDimension extends IrisRegistrant {
|
||||
}
|
||||
|
||||
public void installBiomes(IDataFixer fixer, DataProvider data, KList<File> datapackRoots, KSet<String> biomes) throws IOException {
|
||||
String namespace = getLoadKey().toLowerCase(Locale.ROOT);
|
||||
installBiomes(fixer, data, datapackRoots, namespace, "", biomes);
|
||||
String keyPrefix = getCustomBiomeKeyPrefix();
|
||||
int separator = keyPrefix.indexOf(':');
|
||||
String namespace = keyPrefix.substring(0, separator);
|
||||
String pathPrefix = keyPrefix.substring(separator + 1);
|
||||
if (pathPrefix.endsWith("/")) {
|
||||
pathPrefix = pathPrefix.substring(0, pathPrefix.length() - 1);
|
||||
}
|
||||
installBiomes(fixer, data, datapackRoots, namespace, pathPrefix, biomes);
|
||||
}
|
||||
|
||||
public void installBiomes(IDataFixer fixer, DataProvider data, KList<File> datapackRoots,
|
||||
@@ -747,18 +762,19 @@ public class IrisDimension extends IrisRegistrant {
|
||||
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
String customBiomeId = customBiome.getId();
|
||||
String customBiomePath = sanitizeRegistryPath(customBiomeId, "biome");
|
||||
String json = customBiome.generateJson(fixer, contentGate);
|
||||
|
||||
synchronized (biomes) {
|
||||
if (!biomes.add(customBiomeId)) {
|
||||
if (!biomes.add(customBiomePath)) {
|
||||
IrisLogging.debug("Duplicate Data Pack Biome: " + getLoadKey() + "/" + customBiomeId);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
String biomePath = pathPrefix.isBlank()
|
||||
? customBiomeId
|
||||
: pathPrefix + "/" + customBiomeId;
|
||||
? customBiomePath
|
||||
: pathPrefix + "/" + customBiomePath;
|
||||
for (File datapackRoot : datapackRoots) {
|
||||
File output = new File(datapackRoot, "data/" + namespace + "/worldgen/biome/" + biomePath + ".json");
|
||||
|
||||
@@ -900,6 +916,28 @@ public class IrisDimension extends IrisRegistrant {
|
||||
return sanitizeDimensionTypeKeyValue(getLoadKey());
|
||||
}
|
||||
|
||||
public String getCustomBiomeKey(String customBiomeId) {
|
||||
return customBiomeKey(getLoadKey(), customBiomeId);
|
||||
}
|
||||
|
||||
public String getCustomBiomeKeyPrefix() {
|
||||
return customBiomeKeyPrefix(getLoadKey());
|
||||
}
|
||||
|
||||
public static String customBiomeKey(String dimensionLoadKey, String customBiomeId) {
|
||||
return customBiomeKeyPrefix(dimensionLoadKey) + sanitizeRegistryPath(customBiomeId, "biome");
|
||||
}
|
||||
|
||||
private static String customBiomeKeyPrefix(String dimensionLoadKey) {
|
||||
String dimensionPath = sanitizeRegistryPath(dimensionLoadKey, "dimension");
|
||||
int separator = dimensionPath.indexOf('/');
|
||||
if (separator < 0) {
|
||||
return sanitizeRegistryNamespace(dimensionPath) + ":";
|
||||
}
|
||||
String namespace = sanitizeRegistryNamespace(dimensionPath.substring(0, separator));
|
||||
return namespace + ":" + dimensionPath.substring(separator + 1) + "/";
|
||||
}
|
||||
|
||||
public static String sanitizeDimensionTypeKeyValue(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return "dimension";
|
||||
@@ -918,6 +956,26 @@ public class IrisDimension extends IrisRegistrant {
|
||||
return sanitized.isBlank() ? "dimension" : sanitized;
|
||||
}
|
||||
|
||||
private static String sanitizeRegistryNamespace(String value) {
|
||||
String sanitized = value.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9_.-]", "_");
|
||||
return sanitized.isBlank() ? "dimension" : sanitized;
|
||||
}
|
||||
|
||||
private static String sanitizeRegistryPath(String value, String fallback) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return fallback;
|
||||
}
|
||||
String sanitized = value.trim().toLowerCase(Locale.ROOT).replace("\\", "/");
|
||||
sanitized = sanitized.replaceAll("[^a-z0-9/._-]", "_");
|
||||
sanitized = sanitized.replaceAll("/+", "/");
|
||||
sanitized = sanitized.replaceAll("^/+", "");
|
||||
sanitized = sanitized.replaceAll("/+$", "");
|
||||
while (sanitized.contains("..")) {
|
||||
sanitized = sanitized.replace("..", "_");
|
||||
}
|
||||
return sanitized.isBlank() ? fallback : sanitized;
|
||||
}
|
||||
|
||||
public IrisDimensionType getDimensionType() {
|
||||
IrisDimensionTypeOptions options = getDimensionOptions();
|
||||
if (fullbright) {
|
||||
@@ -930,6 +988,10 @@ public class IrisDimension extends IrisRegistrant {
|
||||
return upperDimension != null && !upperDimension.isEmpty() && !upperDimension.equalsIgnoreCase("none");
|
||||
}
|
||||
|
||||
public boolean hasDimensionStack() {
|
||||
return dimensionStack != null;
|
||||
}
|
||||
|
||||
public void installDimensionType(IDataFixer fixer, KList<File> datapackRoots) throws IOException {
|
||||
IrisDimensionType type = getDimensionType();
|
||||
String json = type.toJson(fixer);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.ArrayType;
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListResource;
|
||||
import art.arcane.iris.engine.object.annotations.Required;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("Stacks dimension terrain upright in declared top-to-bottom order")
|
||||
@Data
|
||||
public class IrisDimensionStack {
|
||||
@Required
|
||||
@ArrayType(min = 2, type = String.class)
|
||||
@RegistryListResource(IrisDimension.class)
|
||||
@Desc("Dimension load keys ordered from the highest layer to the lowest layer. The final key must be this dimension.")
|
||||
private KList<String> dimensions = new KList<>();
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(256)
|
||||
@Desc("Nominal air gap in blocks between adjacent dimension layers")
|
||||
private int spacer = 32;
|
||||
|
||||
@Desc("Optional noise that blends adjacent layer boundaries. Omit this object for a constant configured gap.")
|
||||
private IrisDimensionStackBlend blend = null;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("Controls noise blending at each dimension stack boundary")
|
||||
@Data
|
||||
public class IrisDimensionStackBlend {
|
||||
@MinNumber(0)
|
||||
@MaxNumber(256)
|
||||
@Desc("Maximum vertical boundary displacement in blocks")
|
||||
private int amplitude = 8;
|
||||
|
||||
@Desc("Noise style sampled independently for each adjacent layer boundary")
|
||||
private IrisGeneratorStyle style = NoiseStyle.SIMPLEX.style();
|
||||
}
|
||||
@@ -553,11 +553,15 @@ public final class EngineBukkitOps {
|
||||
}
|
||||
|
||||
public static IrisRegion getRegion(Engine engine, Location l) {
|
||||
return engine.getRegion(l.getBlockX(), l.getBlockZ());
|
||||
return engine.getRegion(
|
||||
l.getBlockX(), l.getBlockY() - engine.getWorld().minHeight(), l.getBlockZ());
|
||||
}
|
||||
|
||||
public static IrisBiome getBiomeOrMantle(Engine engine, Location l) {
|
||||
return engine.getBiomeOrMantle(l.getBlockX(), l.getBlockY(), l.getBlockZ());
|
||||
return engine.getBiomeOrMantle(
|
||||
l.getBlockX(),
|
||||
l.getBlockY() - engine.getWorld().minHeight(),
|
||||
l.getBlockZ());
|
||||
}
|
||||
|
||||
public static IrisBiome getSurfaceBiome(Engine engine, Chunk c) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package art.arcane.iris.util.project.context;
|
||||
|
||||
import art.arcane.iris.engine.DimensionStackContext;
|
||||
import art.arcane.iris.engine.DimensionStackLayout;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.EngineMetrics;
|
||||
@@ -27,6 +29,8 @@ public class ChunkContext {
|
||||
private final ChunkedDataCache<PlatformBlockState> fluid;
|
||||
private final ChunkedDataCache<IrisRegion> region;
|
||||
private final FloatingIslandBoundarySampler floatingIslandBoundarySampler;
|
||||
private final DimensionStackContext dimensionStackContext;
|
||||
private final DimensionStackLayout[] dimensionStackLayouts;
|
||||
|
||||
public ChunkContext(int x, int z, IrisComplex complex) {
|
||||
this(x, z, complex, 0L, true, PrefillPlan.NO_CAVE, null);
|
||||
@@ -45,6 +49,19 @@ public class ChunkContext {
|
||||
}
|
||||
|
||||
public ChunkContext(int x, int z, IrisComplex complex, long generationSessionId, boolean cache, PrefillPlan prefillPlan, EngineMetrics metrics) {
|
||||
this(x, z, complex, generationSessionId, cache, prefillPlan, metrics, null);
|
||||
}
|
||||
|
||||
public ChunkContext(
|
||||
int x,
|
||||
int z,
|
||||
IrisComplex complex,
|
||||
long generationSessionId,
|
||||
boolean cache,
|
||||
PrefillPlan prefillPlan,
|
||||
EngineMetrics metrics,
|
||||
DimensionStackContext dimensionStackContext
|
||||
) {
|
||||
this.x = x;
|
||||
this.z = z;
|
||||
this.complex = complex;
|
||||
@@ -57,6 +74,8 @@ public class ChunkContext {
|
||||
this.fluid = new ChunkedDataCache<>(complex.getFluidStream(), x, z, cache);
|
||||
this.region = new ChunkedDataCache<>(complex.getRegionStream(), x, z, cache);
|
||||
this.floatingIslandBoundarySampler = new FloatingIslandBoundarySampler((wx, wz) -> complex.getTrueBiomeStream().get(wx, wz));
|
||||
this.dimensionStackContext = dimensionStackContext;
|
||||
this.dimensionStackLayouts = dimensionStackContext == null ? null : new DimensionStackLayout[256];
|
||||
|
||||
if (cache) {
|
||||
PrefillPlan resolvedPlan = prefillPlan == null ? PrefillPlan.NO_CAVE : prefillPlan;
|
||||
@@ -165,6 +184,19 @@ public class ChunkContext {
|
||||
return region;
|
||||
}
|
||||
|
||||
public DimensionStackLayout getDimensionStackLayout(int localX, int localZ) {
|
||||
if (dimensionStackContext == null) {
|
||||
return null;
|
||||
}
|
||||
int index = (localZ << 4) | localX;
|
||||
DimensionStackLayout layout = dimensionStackLayouts[index];
|
||||
if (layout == null) {
|
||||
layout = dimensionStackContext.sample(x + localX, z + localZ);
|
||||
dimensionStackLayouts[index] = layout;
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
|
||||
public enum PrefillPlan {
|
||||
ALL(true, true, true, true, true, true),
|
||||
NO_CAVE(true, true, false, true, true, true),
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package art.arcane.iris.core;
|
||||
|
||||
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.IrisGeneratorBinding;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||
import art.arcane.iris.core.nms.datapack.v1217.DataFixerV1217;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import org.junit.Rule;
|
||||
@@ -88,6 +90,26 @@ public class IrisDatapackCompilerTest {
|
||||
assertTrue(Files.isRegularFile(datapackRoot.resolve("pack.mcmeta")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compilesRecursiveDimensionBiomeToCanonicalRegistryPath() throws Exception {
|
||||
Path packRoot = temporaryFolder.newFolder("recursive-dimension-pack").toPath();
|
||||
Path datapackRoot = temporaryFolder.newFolder("recursive-dimension-datapack").toPath();
|
||||
createPack(packRoot, "layers/sky", "Aurora");
|
||||
|
||||
IrisDatapackCompiler.compile(
|
||||
List.of(packRoot.toFile()),
|
||||
new KList<File>().qadd(datapackRoot.toFile()),
|
||||
List.of(),
|
||||
new DataFixerV1217(),
|
||||
false
|
||||
);
|
||||
|
||||
assertTrue(Files.isRegularFile(
|
||||
datapackRoot.resolve("data/layers/worldgen/biome/sky/aurora.json")));
|
||||
assertTrue(Files.isRegularFile(
|
||||
datapackRoot.resolve("data/iris/dimension_type/layers_sky.json")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void collectsInstalledPacksThroughSymbolicLinkWorkspace() throws Exception {
|
||||
Path dataDirectory = temporaryFolder.newFolder("linked-data").toPath();
|
||||
@@ -252,6 +274,57 @@ public class IrisDatapackCompilerTest {
|
||||
assertFalse(ServerConfigurator.loadedRegistrySatisfies(loaded, changedBiomeTags));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stackRuntimeRequirementsIncludeSourceDimensionRegistries() throws Exception {
|
||||
Path pack = temporaryFolder.newFolder("registry-stack-pack").toPath();
|
||||
createPack(pack, "host", "stack_custom", "NORMAL");
|
||||
Files.writeString(
|
||||
pack.resolve("dimensions/host.json"),
|
||||
"""
|
||||
{
|
||||
"name": "Host",
|
||||
"environment": "NORMAL",
|
||||
"logicalHeight": 256,
|
||||
"dimensionHeight": {"min": -64, "max": 320},
|
||||
"dimensionStack": {
|
||||
"dimensions": ["layers/source", "host"],
|
||||
"spacer": 8
|
||||
}
|
||||
}
|
||||
""",
|
||||
StandardCharsets.UTF_8
|
||||
);
|
||||
Files.createDirectories(pack.resolve("dimensions/layers"));
|
||||
Files.writeString(
|
||||
pack.resolve("dimensions/layers/source.json"),
|
||||
"""
|
||||
{
|
||||
"name": "Source",
|
||||
"environment": "NORMAL",
|
||||
"logicalHeight": 256,
|
||||
"dimensionHeight": {"min": -64, "max": 320}
|
||||
}
|
||||
""",
|
||||
StandardCharsets.UTF_8
|
||||
);
|
||||
|
||||
IrisData data = IrisData.openDatapackCompiler(pack.toFile());
|
||||
try {
|
||||
IrisDimension host = data.getDimensionLoader().load("host");
|
||||
Map<String, String> requirements = IrisDatapackCompiler.computeRegistryRequirements(
|
||||
host,
|
||||
new DataFixerV1217()
|
||||
);
|
||||
|
||||
assertTrue(requirements.containsKey("dimension_type/iris:host"));
|
||||
assertTrue(requirements.containsKey("dimension_type/iris:layers_source"));
|
||||
assertTrue(requirements.containsKey("worldgen/biome/host:stack_custom"));
|
||||
assertTrue(requirements.containsKey("worldgen/biome/layers:source/stack_custom"));
|
||||
} finally {
|
||||
data.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsBindingToMissingDimension() throws Exception {
|
||||
Path packRoot = temporaryFolder.newFolder("missing-binding-pack").toPath();
|
||||
@@ -325,6 +398,7 @@ public class IrisDatapackCompilerTest {
|
||||
) throws Exception {
|
||||
Files.createDirectories(root.resolve("dimensions"));
|
||||
Files.createDirectories(root.resolve("biomes"));
|
||||
Files.createDirectories(root.resolve("dimensions").resolve(dimensionKey).getParent());
|
||||
Files.writeString(
|
||||
root.resolve("dimensions").resolve(dimensionKey + ".json"),
|
||||
"""
|
||||
|
||||
+1
@@ -80,6 +80,7 @@ public class ServerConfiguratorRegistryVerificationTest {
|
||||
IrisDimension dimension = mock(IrisDimension.class);
|
||||
when(dimension.getAllBiomes(any())).thenReturn(new KList<>(biome));
|
||||
when(dimension.getLoadKey()).thenReturn("overworld");
|
||||
when(dimension.getCustomBiomeKey("missing")).thenReturn("overworld:missing");
|
||||
when(dimension.getLoadFile()).thenReturn(
|
||||
new File("packs/overworld/dimensions/overworld.json"));
|
||||
when(dimension.getDimensionTypeKey()).thenReturn("iris:overworld");
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package art.arcane.iris.core.pack;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisDimensionStack;
|
||||
import art.arcane.iris.engine.object.IrisObjectMarker;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisStaticObject;
|
||||
@@ -9,6 +11,7 @@ import org.junit.Test;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
@@ -63,6 +66,38 @@ public class PackExportClosureTest {
|
||||
PackExportClosure.collectStaticObjectKeys(placements).stream().sorted().toList());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void collectsRootAndStackedDimensionKeys() {
|
||||
IrisDimension dimension = new IrisDimension();
|
||||
dimension.setLoadKey("main");
|
||||
dimension.setDimensionStack(new IrisDimensionStack()
|
||||
.setDimensions(new KList<>("sky", "middle", "main", "sky")));
|
||||
|
||||
assertEquals(List.of("main", "middle", "sky"),
|
||||
PackExportClosure.collectDimensionKeys(dimension).stream().sorted().toList());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void collectsTransitiveStackDimensionsWithoutLooping() {
|
||||
IrisDimension main = dimension("main", "sky", "main");
|
||||
IrisDimension sky = dimension("sky", "cloud", "sky");
|
||||
IrisDimension cloud = dimension("cloud", "main", "cloud");
|
||||
Map<String, IrisDimension> dimensions = Map.of(
|
||||
"main", main,
|
||||
"sky", sky,
|
||||
"cloud", cloud);
|
||||
|
||||
assertEquals(List.of("cloud", "main", "sky"),
|
||||
PackExportClosure.collectDimensionKeys(main, dimensions::get).stream().sorted().toList());
|
||||
}
|
||||
|
||||
private static IrisDimension dimension(String key, String... stackKeys) {
|
||||
IrisDimension dimension = new IrisDimension();
|
||||
dimension.setLoadKey(key);
|
||||
dimension.setDimensionStack(new IrisDimensionStack().setDimensions(new KList<>(stackKeys)));
|
||||
return dimension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Source guard: both packagers must export the ambient-spawning graph. Spawner and marker
|
||||
* folders were silently omitted from exports, leaving dangling entitySpawners references.
|
||||
@@ -75,10 +110,16 @@ public class PackExportClosureTest {
|
||||
assertTrue("Bukkit packager must write markers/", bukkit.contains("\"markers/\""));
|
||||
assertTrue("Bukkit packager must export region objects alongside biome objects",
|
||||
bukkit.contains("regions.forEach((r) -> allPlacements.addAll(r.getObjects()))"));
|
||||
assertTrue("Bukkit packager must export static objects", bukkit.contains("dimension.getStaticObjects()"));
|
||||
assertTrue("Bukkit packager must export static objects",
|
||||
bukkit.contains("exportedDimension.getStaticObjects()"));
|
||||
assertTrue("Bukkit obfuscation must rewrite static object references",
|
||||
bukkit.contains("placement.setObject(renameObjects.get(placement.getObject()))"));
|
||||
assertTrue("Bukkit packager must export entity loot tables", bukkit.contains("getLoot().getTables()"));
|
||||
assertTrue("Bukkit packager must include dimension-stack resources",
|
||||
bukkit.contains("PackExportClosure.collectDimensionKeys(dimension)"));
|
||||
assertTrue("Bukkit packager must export expression-backed stack blend styles",
|
||||
bukkit.contains("dm.getExpressionLoader().getPossibleKeys()")
|
||||
&& bukkit.contains("\"expressions/\""));
|
||||
int bukkitValidation = bukkit.indexOf("PackValidator.validateForPackaging(project.getPath())");
|
||||
assertTrue("Bukkit packager must validate before opening or mutating package state",
|
||||
bukkitValidation >= 0
|
||||
@@ -92,7 +133,14 @@ public class PackExportClosureTest {
|
||||
assertTrue("modded packager must include initial spawns", modded.contains("getInitialSpawns"));
|
||||
assertTrue("modded packager must export region objects", modded.contains("region.getObjects()"));
|
||||
assertTrue("modded packager must export static objects",
|
||||
modded.contains("PackExportClosure.collectStaticObjectKeys(dimension.getStaticObjects())"));
|
||||
modded.contains("PackExportClosure.collectStaticObjectKeys(exportedDimension.getStaticObjects())"));
|
||||
assertTrue("modded packager must include dimension-stack resources",
|
||||
modded.contains("PackExportClosure.collectDimensionKeys(dimension)"));
|
||||
assertTrue("modded packager must export expression-backed stack blend styles",
|
||||
modded.contains("dm.getExpressionLoader().getPossibleKeys()")
|
||||
&& modded.contains("\"expressions\""));
|
||||
assertTrue("modded packager must retain referenced style snippets",
|
||||
modded.contains("copyStyleSnippets(packFolder, folder)"));
|
||||
int moddedValidation = modded.indexOf("PackValidator.validateForPackaging(packFolder)");
|
||||
assertTrue("modded packager must validate before mutating package state",
|
||||
moddedValidation >= 0 && moddedValidation < modded.indexOf("IO.delete(folder)"));
|
||||
|
||||
@@ -317,6 +317,48 @@ public class PackImageMapValidatorTest {
|
||||
"Dimension 'upper' image-map 'terrain' source footprint does not cover"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validatesStackLayerCoverageAgainstHostBoundary() throws Exception {
|
||||
File pack = createPack("stack-host-boundary", """
|
||||
{
|
||||
"regions": ["region"],
|
||||
"worldBoundary": {"center": {"x": 0, "z": 0}, "size": 16},
|
||||
"dimensionStack": {
|
||||
"dimensions": ["upper", "upper", "main"],
|
||||
"spacer": 24
|
||||
}
|
||||
}
|
||||
""");
|
||||
write(pack, "dimensions/upper.json", """
|
||||
{
|
||||
"regions": ["region"],
|
||||
"worldBoundary": {"center": {"x": 0, "z": 0}, "size": 2},
|
||||
"imageMaps": [
|
||||
{"key": "terrain", "map": "upper-terrain", "application": "TERRAIN_HEIGHT"}
|
||||
]
|
||||
}
|
||||
""");
|
||||
writeGray(pack, "upper-terrain", 4, 4);
|
||||
write(pack, "image-maps/upper-terrain.json", """
|
||||
{
|
||||
"source": "upper-terrain",
|
||||
"type": "GRAYSCALE_HEIGHT",
|
||||
"origin": {"x": -2, "z": -2},
|
||||
"outOfBounds": "ERROR"
|
||||
}
|
||||
""");
|
||||
|
||||
PackImageMapValidator.Validation validation = validate(pack, false);
|
||||
|
||||
assertTrue(validation.errors().toString(), contains(validation.errors(),
|
||||
"Dimension 'main' dimension stack layer 'upper' image-map 'terrain' source footprint does not cover"));
|
||||
assertEquals(1L, validation.errors().stream()
|
||||
.filter(error -> error.contains("Dimension 'main' dimension stack layer 'upper'"))
|
||||
.count());
|
||||
assertFalse(validation.errors().toString(), contains(validation.errors(),
|
||||
"Dimension 'upper' image-map 'terrain' source footprint does not cover"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptsUpperDimensionWithoutStandaloneBoundaryWhenParentIsCovered() throws Exception {
|
||||
File pack = createPack("upper-parent-only-boundary", """
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
package art.arcane.iris.core.pack;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class PackValidatorDimensionStackTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void acceptsCanonicalTopToBottomStack() throws Exception {
|
||||
PackValidationResult result = validate("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"dimensionStack": {
|
||||
"dimensions": ["sky", "main"],
|
||||
"spacer": 24,
|
||||
"blend": {
|
||||
"amplitude": 8,
|
||||
"style": {"style": "SIMPLEX", "zoom": 128}
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
assertTrue(result.getBlockingErrors().toString(), result.isLoadable());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsMissingBlendStyleSnippet() throws Exception {
|
||||
PackValidationResult result = validate("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"dimensionStack": {
|
||||
"dimensions": ["sky", "main"],
|
||||
"blend": {"style": "snippet/style/missing"}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
assertFalse(result.isLoadable());
|
||||
assertTrue(contains(result, "references missing style snippet 'snippet/style/missing'"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsWrongBlendSnippetType() throws Exception {
|
||||
PackValidationResult result = validate("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"dimensionStack": {
|
||||
"dimensions": ["sky", "main"],
|
||||
"blend": {"style": "snippet/generator/stack-blend"}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
assertFalse(result.isLoadable());
|
||||
assertTrue(contains(result, "blend.style must be an object or style snippet reference"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsMissingLayerAndWrongLowestDimension() throws Exception {
|
||||
PackValidationResult result = validate("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"dimensionStack": {
|
||||
"dimensions": ["missing", "sky"],
|
||||
"spacer": 24
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
assertFalse(result.isLoadable());
|
||||
assertTrue(contains(result, "dimensions[0] references missing dimension 'missing'"));
|
||||
assertTrue(contains(result, "dimensions must end with its host dimension key 'main'"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsUpperDimensionAndUnsafeNumericValues() throws Exception {
|
||||
PackValidationResult result = validate("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"upperDimension": "sky",
|
||||
"dimensionStack": {
|
||||
"dimensions": ["sky", "main"],
|
||||
"spacer": 24.5,
|
||||
"blend": {
|
||||
"amplitude": 257,
|
||||
"style": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
assertFalse(result.isLoadable());
|
||||
assertTrue(contains(result, "dimensionStack cannot be used with upperDimension"));
|
||||
assertTrue(contains(result, "spacer must be an integer between 0 and 256"));
|
||||
assertTrue(contains(result, "amplitude must be an integer between 0 and 256"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsShortStackAndMalformedBlendStyle() throws Exception {
|
||||
PackValidationResult result = validate("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"dimensionStack": {
|
||||
"dimensions": ["main"],
|
||||
"blend": {"style": 42}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
assertFalse(result.isLoadable());
|
||||
assertTrue(contains(result, "dimensions must contain at least two dimension keys"));
|
||||
assertTrue(contains(result, "blend.style must be an object or style snippet reference"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptsBlendStyleSnippetReference() throws Exception {
|
||||
PackValidationResult result = validate("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"dimensionStack": {
|
||||
"dimensions": ["sky", "main"],
|
||||
"blend": {"style": "snippet/style/stack-blend"}
|
||||
}
|
||||
}
|
||||
""", "{\"style\":\"SIMPLEX\",\"zoom\":128}");
|
||||
|
||||
assertTrue(result.getBlockingErrors().toString(), result.isLoadable());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsMalformedBlendStyleSnippet() throws Exception {
|
||||
PackValidationResult result = validate("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"dimensionStack": {
|
||||
"dimensions": ["sky", "main"],
|
||||
"blend": {"style": "snippet/style/stack-blend"}
|
||||
}
|
||||
}
|
||||
""", "{");
|
||||
|
||||
assertFalse(result.isLoadable());
|
||||
assertTrue(contains(result, "references invalid style snippet 'snippet/style/stack-blend'"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptsNestedDimensionKeys() throws Exception {
|
||||
File pack = temporaryFolder.newFolder("nested-pack");
|
||||
write(pack, "dimensions/root/main.json", """
|
||||
{
|
||||
"regions": ["region"],
|
||||
"dimensionStack": {
|
||||
"dimensions": ["sky/clouds", "root/main"]
|
||||
}
|
||||
}
|
||||
""");
|
||||
write(pack, "dimensions/sky/clouds.json", "{\"regions\":[\"region\"]}");
|
||||
write(pack, "regions/region.json", "{\"landBiomes\":[\"biome\"]}");
|
||||
write(pack, "biomes/biome.json", "{\"name\":\"Biome\"}");
|
||||
|
||||
PackValidationResult result = PackValidator.validateForPackaging(pack);
|
||||
|
||||
assertTrue(result.getBlockingErrors().toString(), result.isLoadable());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptsEmptyBlendWithDefaults() throws Exception {
|
||||
PackValidationResult result = validate("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"dimensionStack": {
|
||||
"dimensions": ["sky", "main"],
|
||||
"blend": {}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
assertTrue(result.getBlockingErrors().toString(), result.isLoadable());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptsRepeatedNonHostDimensions() throws Exception {
|
||||
PackValidationResult result = validate("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"dimensionStack": {
|
||||
"dimensions": ["sky", "sky", "main"]
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
assertTrue(result.getBlockingErrors().toString(), result.isLoadable());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsDuplicateHostDimension() throws Exception {
|
||||
PackValidationResult result = validate("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"dimensionStack": {
|
||||
"dimensions": ["main", "sky", "main"]
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
assertFalse(result.isLoadable());
|
||||
assertTrue(contains(result, "host dimension key 'main' exactly once"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsExplicitNullConfiguration() throws Exception {
|
||||
PackValidationResult result = validate("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"dimensionStack": null
|
||||
}
|
||||
""");
|
||||
|
||||
assertFalse(result.isLoadable());
|
||||
assertTrue(contains(result, "dimensionStack must be an object"));
|
||||
}
|
||||
|
||||
private PackValidationResult validate(String mainDimension) throws Exception {
|
||||
return validate(mainDimension, null);
|
||||
}
|
||||
|
||||
private PackValidationResult validate(String mainDimension, String styleSnippet) throws Exception {
|
||||
File pack = temporaryFolder.newFolder("pack-" + System.nanoTime());
|
||||
write(pack, "dimensions/main.json", mainDimension);
|
||||
write(pack, "dimensions/sky.json", "{\"regions\":[\"region\"]}");
|
||||
write(pack, "regions/region.json", "{\"landBiomes\":[\"biome\"]}");
|
||||
write(pack, "biomes/biome.json", "{\"name\":\"Biome\"}");
|
||||
if (styleSnippet != null) {
|
||||
write(pack, "snippet/style/stack-blend.json", styleSnippet);
|
||||
}
|
||||
return PackValidator.validateForPackaging(pack);
|
||||
}
|
||||
|
||||
private static boolean contains(PackValidationResult result, String fragment) {
|
||||
return result.getBlockingErrors().stream().anyMatch(error -> error.contains(fragment));
|
||||
}
|
||||
|
||||
private static void write(File root, String relative, String content) throws Exception {
|
||||
Path target = new File(root, relative).toPath();
|
||||
Files.createDirectories(target.getParent());
|
||||
Files.writeString(target, content, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package art.arcane.iris.core.project;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.IrisRegistrant;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisDimensionStack;
|
||||
import art.arcane.iris.engine.object.IrisDimensionStackBlend;
|
||||
import art.arcane.iris.engine.object.annotations.ArrayType;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListResource;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import com.google.gson.Gson;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisDimensionStackSchemaTest {
|
||||
@Test
|
||||
public void dimensionExposesNullableStackConfiguration() throws Exception {
|
||||
IrisDimension dimension = new IrisDimension();
|
||||
Field stackField = IrisDimension.class.getDeclaredField("dimensionStack");
|
||||
Field dimensionsField = IrisDimensionStack.class.getDeclaredField("dimensions");
|
||||
|
||||
assertNull(dimension.getDimensionStack());
|
||||
assertFalse(dimension.hasDimensionStack());
|
||||
dimension.setDimensionStack(new IrisDimensionStack());
|
||||
assertTrue(dimension.hasDimensionStack());
|
||||
assertEquals(IrisDimensionStack.class, stackField.getType());
|
||||
assertEquals(2, dimensionsField.getAnnotation(ArrayType.class).min());
|
||||
assertEquals(IrisDimension.class,
|
||||
dimensionsField.getAnnotation(RegistryListResource.class).value());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void schemaCarriesStackOrderAndNumericBounds() {
|
||||
JSONObject schema = new SchemaBuilder(IrisDimensionStack.class, schemaData()).construct();
|
||||
JSONObject properties = schema.getJSONObject("properties");
|
||||
JSONObject dimensions = properties.getJSONObject("dimensions");
|
||||
JSONObject spacer = properties.getJSONObject("spacer");
|
||||
JSONObject blend = referencedSchema(schema, properties.getJSONObject("blend"));
|
||||
JSONObject blendProperties = blend.getJSONObject("properties");
|
||||
JSONObject amplitude = blendProperties.getJSONObject("amplitude");
|
||||
|
||||
assertEquals("array", dimensions.getString("type"));
|
||||
assertEquals(2, dimensions.getInt("minItems"));
|
||||
assertEquals(0, spacer.getInt("minimum"));
|
||||
assertEquals(256, spacer.getInt("maximum"));
|
||||
assertEquals(0, amplitude.getInt("minimum"));
|
||||
assertEquals(256, amplitude.getInt("maximum"));
|
||||
assertTrue(contains(schema.getJSONArray("required"), "dimensions"));
|
||||
assertFalse(blend.has("required"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void gsonUsesTheCanonicalNestedShape() {
|
||||
IrisDimensionStack stack = new IrisDimensionStack()
|
||||
.setDimensions(new KList<>("sky", "main"))
|
||||
.setSpacer(24)
|
||||
.setBlend(new IrisDimensionStackBlend().setAmplitude(8));
|
||||
|
||||
JSONObject json = new JSONObject(new Gson().toJson(stack));
|
||||
|
||||
assertEquals(2, json.getJSONArray("dimensions").length());
|
||||
assertEquals("main", json.getJSONArray("dimensions").getString(1));
|
||||
assertEquals(24, json.getInt("spacer"));
|
||||
assertEquals(8, json.getJSONObject("blend").getInt("amplitude"));
|
||||
assertTrue(json.getJSONObject("blend").getJSONObject("style").has("style"));
|
||||
}
|
||||
|
||||
private static JSONObject referencedSchema(JSONObject schema, JSONObject property) {
|
||||
String key = property.getString("$ref").substring("#/definitions/".length());
|
||||
return schema.getJSONObject("definitions").getJSONObject(key);
|
||||
}
|
||||
|
||||
private static boolean contains(JSONArray values, String expected) {
|
||||
for (int index = 0; index < values.length(); index++) {
|
||||
if (expected.equals(values.optString(index, null))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static IrisData schemaData() {
|
||||
IrisData data = mock(IrisData.class);
|
||||
ResourceLoader<IrisDimension> dimensionLoader = mock(ResourceLoader.class);
|
||||
KMap<Class<? extends IrisRegistrant>, ResourceLoader<? extends IrisRegistrant>> loaders = new KMap<>();
|
||||
loaders.put(IrisDimension.class, dimensionLoader);
|
||||
when(data.getLoaders()).thenReturn(loaders);
|
||||
when(dimensionLoader.getFolderName()).thenReturn("dimensions");
|
||||
when(dimensionLoader.getResourceTypeName()).thenReturn("Dimension");
|
||||
when(dimensionLoader.getPossibleKeys()).thenReturn(new String[]{"sky", "main"});
|
||||
when(data.getPossibleSnippets(anyString())).thenReturn(new KList<>());
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -86,6 +86,46 @@ public class IrisProjectCopierTest {
|
||||
assertTrue(Files.isRegularFile(target.resolve("dimensions/the_nether.json")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rewritesDimensionStackReferencesAcrossCopiedDimensionsWhenRenaming() throws Exception {
|
||||
Path workspace = temporaryFolder.newFolder("dimension-stack-reference").toPath();
|
||||
Path source = workspace.resolve("template-pack");
|
||||
Files.createDirectories(source.resolve("dimensions"));
|
||||
Files.writeString(source.resolve("dimensions/overworld.json"), """
|
||||
{
|
||||
"name": "Source",
|
||||
"dimensionStack": {
|
||||
"dimensions": ["layers/sky", "overworld"],
|
||||
"spacer": 24
|
||||
}
|
||||
}
|
||||
""");
|
||||
Files.createDirectories(source.resolve("dimensions/layers"));
|
||||
Files.writeString(source.resolve("dimensions/layers/sky.json"), """
|
||||
{
|
||||
"name": "Sky",
|
||||
"dimensionStack": {
|
||||
"dimensions": ["overworld", "layers/sky"]
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
IrisProjectCopier.copyProject(source.toFile(), workspace.toFile(), "overworld", "new-project");
|
||||
|
||||
JSONObject dimension = new JSONObject(Files.readString(
|
||||
workspace.resolve("new-project/dimensions/new-project.json")));
|
||||
assertEquals("layers/sky", dimension.getJSONObject("dimensionStack")
|
||||
.getJSONArray("dimensions").getString(0));
|
||||
assertEquals("new-project", dimension.getJSONObject("dimensionStack")
|
||||
.getJSONArray("dimensions").getString(1));
|
||||
JSONObject supportingDimension = new JSONObject(Files.readString(
|
||||
workspace.resolve("new-project/dimensions/layers/sky.json")));
|
||||
assertEquals("new-project", supportingDimension.getJSONObject("dimensionStack")
|
||||
.getJSONArray("dimensions").getString(0));
|
||||
assertEquals("layers/sky", supportingDimension.getJSONObject("dimensionStack")
|
||||
.getJSONArray("dimensions").getString(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void existingTargetRemainsUnchanged() throws Exception {
|
||||
Path workspace = temporaryFolder.newFolder("packs").toPath();
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
package art.arcane.iris.engine;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
public class DimensionStackLayoutTest {
|
||||
@Test
|
||||
public void spacerCountsExactAirBlocksBetweenInclusiveLayers() {
|
||||
DimensionStackLayout layout = DimensionStackLayout.create(
|
||||
64,
|
||||
8,
|
||||
List.of(input(15, 15), input(15, 15), input(15, 15)),
|
||||
new int[]{0, 0}
|
||||
);
|
||||
|
||||
List<DimensionStackLayout.Layer> layers = layout.layersBottomToTop();
|
||||
assertEquals(0, layers.get(0).localBaseY());
|
||||
assertEquals(15, layers.get(0).surfaceY());
|
||||
assertEquals(24, layers.get(1).localBaseY());
|
||||
assertEquals(39, layers.get(1).surfaceY());
|
||||
assertEquals(48, layers.get(2).localBaseY());
|
||||
assertEquals(63, layers.get(2).surfaceY());
|
||||
assertEquals(8, layers.get(1).localBaseY() - layers.get(0).contentTopY() - 1);
|
||||
assertEquals(8, layers.get(2).localBaseY() - layers.get(1).contentTopY() - 1);
|
||||
assertEquals(63, layout.stackTerrainTopY());
|
||||
assertEquals(63, layout.stackTopY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void independentlySignedSeamOffsetsShiftOnlyTheirUpperLayer() {
|
||||
DimensionStackLayout layout = DimensionStackLayout.create(
|
||||
64,
|
||||
4,
|
||||
List.of(input(10, 7), input(10, 7), input(10, 7)),
|
||||
new int[]{3, -2}
|
||||
);
|
||||
|
||||
List<DimensionStackLayout.Layer> layers = layout.layersBottomToTop();
|
||||
assertEquals(0, layers.get(0).localBaseY());
|
||||
assertEquals(18, layers.get(1).localBaseY());
|
||||
assertEquals(31, layers.get(2).localBaseY());
|
||||
assertEquals(3, layers.get(1).seamOffsetBelow());
|
||||
assertEquals(-2, layers.get(2).seamOffsetBelow());
|
||||
assertEquals(41, layout.stackTerrainTopY());
|
||||
assertEquals(41, layout.stackTopY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void renderingBoundsClipWithoutChangingRawLayout() {
|
||||
DimensionStackLayout layout = DimensionStackLayout.create(
|
||||
32,
|
||||
8,
|
||||
List.of(input(15, 15), input(15, 15), input(15, 15)),
|
||||
new int[]{0, 0}
|
||||
);
|
||||
|
||||
List<DimensionStackLayout.Layer> layers = layout.layersBottomToTop();
|
||||
DimensionStackLayout.Layer middle = layers.get(1);
|
||||
DimensionStackLayout.Layer top = layers.get(2);
|
||||
assertEquals(39, middle.contentTopY());
|
||||
assertEquals(24, middle.renderMinY());
|
||||
assertEquals(31, middle.renderMaxY());
|
||||
assertEquals(31, middle.clippedSurfaceY());
|
||||
assertTrue(middle.visible());
|
||||
assertTrue(middle.containsRenderedY(31));
|
||||
assertFalse(middle.containsRenderedY(32));
|
||||
assertTrue(layout.containsUpperLayerY(31));
|
||||
assertFalse(layout.containsUpperLayerY(23));
|
||||
assertEquals(48, top.localBaseY());
|
||||
assertFalse(top.visible());
|
||||
assertFalse(top.containsRenderedY(31));
|
||||
assertEquals(63, layout.stackTerrainTopY());
|
||||
assertEquals(63, layout.stackTopY());
|
||||
assertEquals(31, layout.clippedStackTerrainTopY());
|
||||
assertEquals(31, layout.clippedStackTopY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invisibleLayersDoNotRaiseRenderedHeightToTheCeiling() {
|
||||
DimensionStackLayout layout = DimensionStackLayout.create(
|
||||
64,
|
||||
64,
|
||||
List.of(input(15, 15), input(15, 15)),
|
||||
new int[]{0}
|
||||
);
|
||||
|
||||
assertFalse(layout.layersBottomToTop().get(1).visible());
|
||||
assertEquals(15, layout.clippedStackTerrainTopY());
|
||||
assertEquals(15, layout.clippedStackTopY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void layerLookupReturnsRenderedOwnerAndBottomOwnerForAir() {
|
||||
DimensionStackLayout layout = DimensionStackLayout.create(
|
||||
64,
|
||||
8,
|
||||
List.of(input(15, 15), input(15, 15)),
|
||||
new int[]{0}
|
||||
);
|
||||
|
||||
assertSame(layout.layersBottomToTop().get(0), layout.layerAt(8));
|
||||
assertSame(layout.layersBottomToTop().get(0), layout.layerAt(20));
|
||||
assertSame(layout.layersBottomToTop().get(1), layout.layerAt(24));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void laterGapsEraseEarlierUpperLayerOwnership() {
|
||||
DimensionStackLayout layout = DimensionStackLayout.create(
|
||||
128,
|
||||
0,
|
||||
List.of(input(100, 100), input(60, 60), input(10, 10), input(10, 10)),
|
||||
new int[]{-81, -41, 39}
|
||||
);
|
||||
|
||||
List<DimensionStackLayout.Layer> layers = layout.layersBottomToTop();
|
||||
assertSame(layers.get(1), layout.layerAt(30));
|
||||
assertSame(layers.get(2), layout.layerAt(45));
|
||||
assertSame(layers.get(0), layout.layerAt(60));
|
||||
assertSame(layers.get(3), layout.layerAt(95));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hostFeaturesCannotFillStackGapsOrOverwriteUpperLayers() {
|
||||
DimensionStackLayout layout = DimensionStackLayout.create(
|
||||
64,
|
||||
8,
|
||||
List.of(input(15, 15), input(15, 15)),
|
||||
new int[]{0}
|
||||
);
|
||||
|
||||
assertFalse(layout.isHostFeatureProtectedY(15));
|
||||
assertTrue(layout.isHostFeatureProtectedY(20));
|
||||
assertTrue(layout.isHostFeatureProtectedY(24));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fluidExtendsContentWithoutChangingTerrainTop() {
|
||||
DimensionStackLayout layout = DimensionStackLayout.create(
|
||||
64,
|
||||
2,
|
||||
List.of(input(5, 9), input(4, 7)),
|
||||
new int[]{0}
|
||||
);
|
||||
|
||||
List<DimensionStackLayout.Layer> layers = layout.layersBottomToTop();
|
||||
assertEquals(9, layers.get(0).contentTopY());
|
||||
assertEquals(12, layers.get(1).localBaseY());
|
||||
assertEquals(16, layers.get(1).surfaceY());
|
||||
assertEquals(19, layers.get(1).contentTopY());
|
||||
assertEquals(16, layout.stackTerrainTopY());
|
||||
assertEquals(19, layout.stackTopY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void physicallyHighestTerrainLayerWinsWhenBlendEmbedsAnUpperLayer() {
|
||||
DimensionStackLayout layout = DimensionStackLayout.create(
|
||||
64,
|
||||
0,
|
||||
List.of(input(20, 20), input(5, 5)),
|
||||
new int[]{-15}
|
||||
);
|
||||
|
||||
assertSame(layout.layersBottomToTop().get(0), layout.topTerrainLayer());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void declaredUpperLayerWinsWhenRenderedSurfacesTie() {
|
||||
DimensionStackLayout layout = DimensionStackLayout.create(
|
||||
64,
|
||||
0,
|
||||
List.of(input(10, 10), input(5, 5)),
|
||||
new int[]{-6}
|
||||
);
|
||||
|
||||
assertSame(layout.layersBottomToTop().get(1), layout.topTerrainLayer());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void upperFluidOccludesAPhysicallyHigherLowerSurface() {
|
||||
DimensionStackLayout layout = DimensionStackLayout.create(
|
||||
64,
|
||||
0,
|
||||
List.of(input(20, 20), input(5, 15)),
|
||||
new int[]{-11}
|
||||
);
|
||||
|
||||
assertEquals(15, layout.clippedStackTerrainTopY());
|
||||
assertEquals(25, layout.clippedStackTopY());
|
||||
assertSame(layout.layersBottomToTop().get(1), layout.topTerrainLayer());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void laterGapErasesProtrudingTerrainWhenItsUpperLayerIsClipped() {
|
||||
DimensionStackLayout layout = DimensionStackLayout.create(
|
||||
128,
|
||||
0,
|
||||
List.of(input(100, 100), input(10, 10), input(10, 10)),
|
||||
new int[]{-51, 89}
|
||||
);
|
||||
|
||||
assertEquals(50, layout.layersBottomToTop().get(1).localBaseY());
|
||||
assertEquals(150, layout.layersBottomToTop().get(2).localBaseY());
|
||||
assertFalse(layout.layersBottomToTop().get(2).visible());
|
||||
assertEquals(60, layout.clippedStackTerrainTopY());
|
||||
assertEquals(60, layout.clippedStackTopY());
|
||||
assertSame(layout.layersBottomToTop().get(1), layout.topTerrainLayer());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void visibleFluidOwnsSurfaceQueriesWhenAllTerrainIsOccludedBelowTheFloor() {
|
||||
DimensionStackLayout layout = DimensionStackLayout.create(
|
||||
64,
|
||||
0,
|
||||
List.of(input(5, 5), input(2, 30)),
|
||||
new int[]{-20}
|
||||
);
|
||||
|
||||
assertEquals(-14, layout.layersBottomToTop().get(1).localBaseY());
|
||||
assertEquals(0, layout.clippedStackTerrainTopY());
|
||||
assertEquals(16, layout.clippedStackTopY());
|
||||
assertNull(layout.topTerrainLayer());
|
||||
assertSame(layout.layersBottomToTop().get(1), layout.surfaceLayer());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void layoutListsAreImmutableAndExposeBothOrders() {
|
||||
ArrayList<DimensionStackLayout.LayerInput> inputs = new ArrayList<>();
|
||||
DimensionStackLayout.LayerInput bottom = input(3, 3);
|
||||
DimensionStackLayout.LayerInput top = input(4, 4);
|
||||
inputs.add(bottom);
|
||||
inputs.add(top);
|
||||
DimensionStackLayout layout = DimensionStackLayout.create(32, 1, inputs, new int[]{0});
|
||||
inputs.clear();
|
||||
|
||||
assertEquals(2, layout.layersBottomToTop().size());
|
||||
assertEquals(
|
||||
layout.layersBottomToTop().get(0),
|
||||
layout.layersTopToBottom().get(1)
|
||||
);
|
||||
assertThrows(
|
||||
UnsupportedOperationException.class,
|
||||
() -> layout.layersBottomToTop().clear()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void seamSaltsAreStableAndBoundarySpecific() {
|
||||
long first = DimensionStackContext.seamSalt(0, "root", "middle");
|
||||
long repeat = DimensionStackContext.seamSalt(0, "root", "middle");
|
||||
long differentIndex = DimensionStackContext.seamSalt(1, "root", "middle");
|
||||
long differentUpper = DimensionStackContext.seamSalt(0, "root", "top");
|
||||
|
||||
assertEquals(first, repeat);
|
||||
assertNotEquals(first, differentIndex);
|
||||
assertNotEquals(first, differentUpper);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void seamOffsetsClampUnboundedAndNonFiniteNoise() {
|
||||
assertEquals(4, DimensionStackContext.clampSeamOffset(12D, 4));
|
||||
assertEquals(-4, DimensionStackContext.clampSeamOffset(-12D, 4));
|
||||
assertEquals(2, DimensionStackContext.clampSeamOffset(1.6D, 4));
|
||||
assertEquals(0, DimensionStackContext.clampSeamOffset(Double.NaN, 4));
|
||||
assertEquals(0, DimensionStackContext.clampSeamOffset(Double.POSITIVE_INFINITY, 4));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryLayoutsCacheStableColumnsAndBypassTemporaryNaturalFallbacks() {
|
||||
Cache<Long, DimensionStackLayout> cache = Caffeine.newBuilder().maximumSize(4).build();
|
||||
DimensionStackLayout stable = DimensionStackLayout.create(
|
||||
32, 0, List.of(input(10, 10), input(10, 10)), new int[]{0});
|
||||
DimensionStackLayout unusedStable = DimensionStackLayout.create(
|
||||
32, 0, List.of(input(11, 11), input(10, 10)), new int[]{0});
|
||||
AtomicInteger stableSamples = new AtomicInteger();
|
||||
|
||||
DimensionStackLayout first = DimensionStackContext.resolveLayout(
|
||||
cache, 7L, false, () -> {
|
||||
stableSamples.incrementAndGet();
|
||||
return stable;
|
||||
});
|
||||
DimensionStackLayout second = DimensionStackContext.resolveLayout(
|
||||
cache, 7L, false, () -> {
|
||||
stableSamples.incrementAndGet();
|
||||
return unusedStable;
|
||||
});
|
||||
|
||||
assertSame(stable, first);
|
||||
assertSame(stable, second);
|
||||
assertEquals(1, stableSamples.get());
|
||||
|
||||
DimensionStackLayout naturalFirst = DimensionStackLayout.create(
|
||||
32, 0, List.of(input(12, 12), input(10, 10)), new int[]{0});
|
||||
DimensionStackLayout naturalSecond = DimensionStackLayout.create(
|
||||
32, 0, List.of(input(13, 13), input(10, 10)), new int[]{0});
|
||||
AtomicInteger naturalSamples = new AtomicInteger();
|
||||
|
||||
assertSame(naturalFirst, DimensionStackContext.resolveLayout(
|
||||
cache, 7L, true, () -> {
|
||||
naturalSamples.incrementAndGet();
|
||||
return naturalFirst;
|
||||
}));
|
||||
assertSame(naturalSecond, DimensionStackContext.resolveLayout(
|
||||
cache, 7L, true, () -> {
|
||||
naturalSamples.incrementAndGet();
|
||||
return naturalSecond;
|
||||
}));
|
||||
assertEquals(2, naturalSamples.get());
|
||||
assertSame(stable, cache.getIfPresent(7L));
|
||||
}
|
||||
|
||||
private static DimensionStackLayout.LayerInput input(int terrainHeight, int fluidHeight) {
|
||||
return new DimensionStackLayout.LayerInput(
|
||||
mock(DimensionTerrainContext.class),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
terrainHeight,
|
||||
fluidHeight
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package art.arcane.iris.engine;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.image.IrisImageMapRuntime;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class DimensionTerrainContextFallbackTest {
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void selfSamplerUsesNaturalValuesOnlyForNonblockingColumns() {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisDimension dimension = mock(IrisDimension.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
ProceduralStream<Double> naturalHeight = mock(ProceduralStream.class);
|
||||
ProceduralStream<Double> resolvedHeight = mock(ProceduralStream.class);
|
||||
ProceduralStream<Double> resolvedFluidHeight = mock(ProceduralStream.class);
|
||||
ProceduralStream<IrisBiome> naturalBiome = mock(ProceduralStream.class);
|
||||
ProceduralStream<IrisBiome> resolvedBiome = mock(ProceduralStream.class);
|
||||
ProceduralStream<IrisRegion> region = mock(ProceduralStream.class);
|
||||
ProceduralStream<PlatformBlockState> rock = mock(ProceduralStream.class);
|
||||
ProceduralStream<PlatformBlockState> configuredFluid = mock(ProceduralStream.class);
|
||||
IrisImageMapRuntime imageMapRuntime = mock(IrisImageMapRuntime.class);
|
||||
IrisBiome naturalBiomeValue = mock(IrisBiome.class);
|
||||
IrisBiome resolvedBiomeValue = mock(IrisBiome.class);
|
||||
PlatformBlockState naturalFluid = mock(PlatformBlockState.class);
|
||||
PlatformBlockState resolvedFluid = mock(PlatformBlockState.class);
|
||||
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(engine.getHeight()).thenReturn(128);
|
||||
when(dimension.getLoadKey()).thenReturn("root");
|
||||
when(dimension.getFluidHeight()).thenReturn(12);
|
||||
when(complex.getNaturalHeightStream()).thenReturn(naturalHeight);
|
||||
when(complex.getHeightStream()).thenReturn(resolvedHeight);
|
||||
when(complex.getRiverWaterSurfaceStream()).thenReturn(resolvedFluidHeight);
|
||||
when(complex.getNaturalTrueBiomeStream()).thenReturn(naturalBiome);
|
||||
when(complex.getTrueBiomeStream()).thenReturn(resolvedBiome);
|
||||
when(complex.getRegionStream()).thenReturn(region);
|
||||
when(complex.getRockStream()).thenReturn(rock);
|
||||
when(complex.getFluidStream()).thenReturn(configuredFluid);
|
||||
when(complex.getImageMapRuntime()).thenReturn(imageMapRuntime);
|
||||
when(naturalHeight.getDouble(12D, -8D)).thenReturn(15D);
|
||||
when(resolvedHeight.getDouble(12D, -8D)).thenReturn(18D);
|
||||
when(resolvedFluidHeight.getDouble(12D, -8D)).thenReturn(14D);
|
||||
when(naturalBiome.get(12D, -8D)).thenReturn(naturalBiomeValue);
|
||||
when(resolvedBiome.get(12D, -8D)).thenReturn(resolvedBiomeValue);
|
||||
when(configuredFluid.get(12D, -8D)).thenReturn(naturalFluid);
|
||||
when(complex.resolveSurfaceFluid(12D, -8D)).thenReturn(resolvedFluid);
|
||||
|
||||
DimensionTerrainContext context = DimensionTerrainContext.forStack(engine, dimension);
|
||||
when(engine.answersFromNaturalTerrain(12, -8)).thenReturn(true);
|
||||
assertEquals(15D, context.getNormalTerrainHeight(12D, -8D), 0D);
|
||||
assertEquals(12D, context.getFluidHeight(12D, -8D), 0D);
|
||||
assertSame(naturalBiomeValue, context.getBiome(12D, -8D));
|
||||
assertSame(naturalFluid, context.getFluidBlock(12D, -8D));
|
||||
|
||||
when(engine.answersFromNaturalTerrain(12, -8)).thenReturn(false);
|
||||
assertEquals(18D, context.getNormalTerrainHeight(12D, -8D), 0D);
|
||||
assertEquals(14D, context.getFluidHeight(12D, -8D), 0D);
|
||||
assertSame(resolvedBiomeValue, context.getBiome(12D, -8D));
|
||||
assertSame(resolvedFluid, context.getFluidBlock(12D, -8D));
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,20 @@ public class EngineRuntimePublicationContractTest {
|
||||
assertBefore(publish, "sealAndAwait(", "closeRuntime(next, e)");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dimensionStackRunsAfterHostStaticObjects() throws IOException {
|
||||
String source = Files.readString(Path.of(
|
||||
"src/main/java/art/arcane/iris/engine/EngineRuntimeBuilder.java")).replace("\r\n", "\n");
|
||||
int buildStart = source.indexOf("EngineRuntime buildRuntime(EngineTarget runtimeTarget)");
|
||||
int buildEnd = source.indexOf("DimensionStackContext buildDimensionStackContext", buildStart);
|
||||
String build = source.substring(buildStart, buildEnd);
|
||||
|
||||
assertBefore(build, "staticObjects.apply(engine, x, z, blocks)",
|
||||
"dimensionStack.actuate(x, z, blocks, multicore, context)");
|
||||
assertBefore(build, "dimensionStack.actuate(x, z, blocks, multicore, context)",
|
||||
"stackCustom.modify(x, z, blocks, multicore, context)");
|
||||
}
|
||||
|
||||
private static void assertBefore(String source, String first, String second) {
|
||||
int firstIndex = source.indexOf(first);
|
||||
int secondIndex = source.indexOf(second);
|
||||
|
||||
+16
@@ -17,6 +17,7 @@ import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterBiomeInject;
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
|
||||
@@ -30,6 +31,8 @@ import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
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 IrisBiomeActuatorCoordinateTest {
|
||||
@@ -69,6 +72,19 @@ public class IrisBiomeActuatorCoordinateTest {
|
||||
assertEquals(List.of("100,-200", "100,-199", "101,-200", "101,-199"), samples);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void stackedBiomeReplacementRemovesEveryExistingMarkerWithItsTypedSlice() {
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
|
||||
IrisBiomeActuator.clearBiomeMatterRange(mantle, 100, -200, 4, 6);
|
||||
|
||||
verify(mantle).remove(100, 4, -200, MatterBiomeInject.class);
|
||||
verify(mantle).remove(100, 5, -200, MatterBiomeInject.class);
|
||||
verify(mantle).remove(100, 6, -200, MatterBiomeInject.class);
|
||||
verifyNoMoreInteractions(mantle);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Engine engine() {
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package art.arcane.iris.engine.actuator;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterSlice;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisDimensionStackActuatorMetadataTest {
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void overwrittenStackCellsClearHostHydrologyCaveOwnership() {
|
||||
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||
Matter section = mock(Matter.class);
|
||||
MatterSlice<HydrologyCaveCell> hydrology = mock(MatterSlice.class);
|
||||
when(chunk.get(1)).thenReturn(section);
|
||||
doReturn(hydrology).when(section).getSlice(HydrologyCaveCell.class);
|
||||
|
||||
IrisDimensionStackActuator.MetadataCleaner cleaner =
|
||||
new IrisDimensionStackActuator.MetadataCleaner(chunk, 64);
|
||||
cleaner.clear(3, 20, 7);
|
||||
|
||||
verify(hydrology).set(3, 4, 7, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package art.arcane.iris.engine.framework;
|
||||
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.DimensionStackContext;
|
||||
import art.arcane.iris.engine.DimensionStackLayout;
|
||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
import art.arcane.iris.spi.PlatformBiomeWriter;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class EngineHostHeightTest {
|
||||
@Before
|
||||
public void bindPlatform() {
|
||||
IrisPlatforms.unbind();
|
||||
PlatformBiome biome = mock(PlatformBiome.class);
|
||||
PlatformBlockState block = mock(PlatformBlockState.class);
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
when(registries.biome(anyString())).thenReturn(biome);
|
||||
when(registries.block(anyString())).thenReturn(block);
|
||||
PlatformBiomeWriter biomeWriter = mock(PlatformBiomeWriter.class);
|
||||
when(biomeWriter.biomeIdFor(anyString())).thenReturn(1);
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
when(platform.biomeWriter()).thenReturn(biomeWriter);
|
||||
IrisPlatforms.bind(platform);
|
||||
}
|
||||
|
||||
@After
|
||||
public void unbindPlatform() {
|
||||
IrisPlatforms.unbind();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void coldMainThreadQueryUsesNaturalHostTerrain() {
|
||||
Engine engine = mock(Engine.class);
|
||||
EngineMantle mantle = mock(EngineMantle.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
when(engine.getMantle()).thenReturn(mantle);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(engine.answersFromNaturalTerrain(12, -8)).thenReturn(true);
|
||||
when(complex.naturalTrueHeight(12, -8)).thenReturn(42);
|
||||
when(mantle.getFluidHeight()).thenReturn(50);
|
||||
|
||||
assertEquals(42, Engine.hostHeight(engine, 12, -8, true));
|
||||
assertEquals(50, Engine.hostHeight(engine, 12, -8, false));
|
||||
verify(mantle, never()).trueHeight(12, -8);
|
||||
verify(mantle, never()).getFluidHeight(12, -8);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plannedQueryUsesHostColumnTerrainAndFluid() {
|
||||
Engine engine = mock(Engine.class);
|
||||
EngineMantle mantle = mock(EngineMantle.class);
|
||||
when(engine.getMantle()).thenReturn(mantle);
|
||||
when(mantle.trueHeight(3, 7)).thenReturn(45);
|
||||
when(mantle.getFluidHeight(3, 7)).thenReturn(48);
|
||||
|
||||
assertEquals(45, Engine.hostHeight(engine, 3, 7, true));
|
||||
assertEquals(48, Engine.hostHeight(engine, 3, 7, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void yAwareRegionUsesTheOwningStackLayer() {
|
||||
Engine engine = mock(Engine.class, CALLS_REAL_METHODS);
|
||||
DimensionStackContext stack = mock(DimensionStackContext.class);
|
||||
DimensionStackLayout.Layer layer = mock(DimensionStackLayout.Layer.class);
|
||||
DimensionStackLayout layout = mock(DimensionStackLayout.class);
|
||||
IrisRegion expected = mock(IrisRegion.class);
|
||||
when(engine.getDimensionStackContext()).thenReturn(stack);
|
||||
when(stack.getLayout(12, -8)).thenReturn(layout);
|
||||
when(layout.layerAt(96)).thenReturn(layer);
|
||||
when(layer.region()).thenReturn(expected);
|
||||
|
||||
assertEquals(expected, engine.getRegion(12, 96, -8));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stackQueryConsumersUseCacheAwareLayouts() {
|
||||
Engine engine = mock(Engine.class, CALLS_REAL_METHODS);
|
||||
DimensionStackContext stack = mock(DimensionStackContext.class);
|
||||
DimensionStackLayout layout = mock(DimensionStackLayout.class);
|
||||
DimensionStackLayout.Layer bottom = mock(DimensionStackLayout.Layer.class);
|
||||
DimensionStackLayout.Layer upper = mock(DimensionStackLayout.Layer.class);
|
||||
IrisRegion region = mock(IrisRegion.class);
|
||||
IrisBiome biome = mock(IrisBiome.class);
|
||||
when(engine.getDimensionStackContext()).thenReturn(stack);
|
||||
when(stack.getLayout(12, -8)).thenReturn(layout);
|
||||
when(layout.layersBottomToTop()).thenReturn(List.of(bottom, upper));
|
||||
when(layout.surfaceLayer()).thenReturn(upper);
|
||||
when(layout.layerAt(96)).thenReturn(upper);
|
||||
when(upper.region()).thenReturn(region);
|
||||
when(upper.biome()).thenReturn(biome);
|
||||
|
||||
assertSame(region, engine.getRegion(12, -8));
|
||||
assertSame(biome, engine.getSurfaceBiome(12, -8));
|
||||
assertSame(biome, engine.getBiome(12, 96, -8));
|
||||
assertSame(biome, engine.getBiomeOrMantle(12, 96, -8));
|
||||
|
||||
verify(stack, times(4)).getLayout(12, -8);
|
||||
verify(stack, never()).sample(anyInt(), anyInt());
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package art.arcane.iris.engine.framework;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.engine.DimensionStackContext;
|
||||
import art.arcane.iris.engine.DimensionTerrainContext;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
@@ -22,6 +24,7 @@ import org.junit.Test;
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Future;
|
||||
@@ -301,6 +304,20 @@ public class HintedLocatorTest {
|
||||
assertEquals(1, plan.getStrideChunks());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void biomePlanIsUnprunedForStackLayerBiome() {
|
||||
DimensionStackContext stackContext = mock(DimensionStackContext.class);
|
||||
IrisBiome stacked = biome("stacked");
|
||||
when(engine.getDimensionStackContext()).thenReturn(stackContext);
|
||||
when(engine.getAllBiomes()).thenReturn(new KList<>(stacked));
|
||||
|
||||
HintedLocator.SearchPlan plan = HintedLocator.biomePlan(engine, "stacked");
|
||||
|
||||
assertTrue(plan.isPossible());
|
||||
assertNull(plan.getCoarse());
|
||||
assertEquals(1, plan.getStrideChunks());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void regionPlanMatchesOnlyTargetRegionCells() {
|
||||
IrisRegion regionA = region("reg_a");
|
||||
@@ -326,6 +343,24 @@ public class HintedLocatorTest {
|
||||
assertFalse(plan.isPossible());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void regionPlanIsUnprunedForStackLayerRegion() {
|
||||
DimensionStackContext stackContext = mock(DimensionStackContext.class);
|
||||
DimensionTerrainContext terrainContext = mock(DimensionTerrainContext.class);
|
||||
IrisDimension stackedDimension = mock(IrisDimension.class);
|
||||
IrisRegion stackedRegion = region("stacked_region");
|
||||
when(engine.getDimensionStackContext()).thenReturn(stackContext);
|
||||
when(stackContext.getLayersTopToBottom()).thenReturn(List.of(terrainContext));
|
||||
when(terrainContext.getDimension()).thenReturn(stackedDimension);
|
||||
when(stackedDimension.getAllRegions(terrainContext)).thenReturn(new KList<>(stackedRegion));
|
||||
|
||||
HintedLocator.SearchPlan plan = HintedLocator.regionPlan(engine, "stacked_region");
|
||||
|
||||
assertTrue(plan.isPossible());
|
||||
assertNull(plan.getCoarse());
|
||||
assertEquals(1, plan.getStrideChunks());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void objectPlanPrunesToHostBiomes() {
|
||||
IrisRegion regionA = region("reg_a");
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package art.arcane.iris.engine.modifier;
|
||||
|
||||
import art.arcane.iris.engine.DimensionStackLayout;
|
||||
import org.junit.Test;
|
||||
|
||||
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 IrisFloatingChildBiomeModifierStackTest {
|
||||
@Test
|
||||
public void floatingBiomeMarkersSkipStackProtectedCells() {
|
||||
DimensionStackLayout layout = mock(DimensionStackLayout.class);
|
||||
when(layout.isHostFeatureProtectedY(60)).thenReturn(true);
|
||||
when(layout.isHostFeatureProtectedY(30)).thenReturn(false);
|
||||
|
||||
assertFalse(IrisFloatingChildBiomeModifier.shouldWriteHostBiomeMarker(layout, 60));
|
||||
assertTrue(IrisFloatingChildBiomeModifier.shouldWriteHostBiomeMarker(layout, 30));
|
||||
assertTrue(IrisFloatingChildBiomeModifier.shouldWriteHostBiomeMarker(null, 60));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.project.noise.CNG;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public final class IrisBiomeLayerSlopeSourceTest {
|
||||
@Test
|
||||
public void explicitSlopeStreamControlsSurfacePaletteSelection() {
|
||||
assertExplicitSlopeStreamControlsSurfacePaletteSelection(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void explicitSlopeStreamControlsLockedSurfacePaletteSelection() {
|
||||
assertExplicitSlopeStreamControlsSurfacePaletteSelection(true);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void assertExplicitSlopeStreamControlsSurfacePaletteSelection(boolean lockLayers) {
|
||||
IrisData data = mock(IrisData.class);
|
||||
IrisComplex hostComplex = mock(IrisComplex.class);
|
||||
ProceduralStream<Double> hostSlope = mock(ProceduralStream.class);
|
||||
ProceduralStream<Double> sourceSlope = mock(ProceduralStream.class);
|
||||
IrisBiomePaletteLayer paletteLayer = mock(IrisBiomePaletteLayer.class);
|
||||
CNG heightGenerator = mock(CNG.class);
|
||||
PlatformBlockState surfaceBlock = mock(PlatformBlockState.class);
|
||||
RNG rng = new RNG(19L);
|
||||
|
||||
IrisBiome biome = new IrisBiome();
|
||||
biome.setLockLayers(lockLayers);
|
||||
KList<IrisBiomePaletteLayer> paletteLayers = new KList<>();
|
||||
paletteLayers.add(paletteLayer);
|
||||
biome.setLayers(paletteLayers);
|
||||
|
||||
when(hostComplex.getSlopeStream()).thenReturn(hostSlope);
|
||||
when(hostSlope.getDouble(12D, -8D)).thenReturn(0D);
|
||||
when(sourceSlope.getDouble(12D, -8D)).thenReturn(5D);
|
||||
when(paletteLayer.getZoom()).thenReturn(1D);
|
||||
when(paletteLayer.getMinHeight()).thenReturn(1);
|
||||
when(paletteLayer.getMaxHeight()).thenReturn(1);
|
||||
when(paletteLayer.getSlopeCondition()).thenReturn(new IrisSlopeClip(5D, 5D));
|
||||
when(paletteLayer.getHeightGenerator(any(RNG.class), same(data))).thenReturn(heightGenerator);
|
||||
when(heightGenerator.fit(1, 1, 12D, -8D)).thenReturn(1);
|
||||
when(paletteLayer.get(rng, 0, 12D, 0D, -8D, data)).thenReturn(surfaceBlock);
|
||||
|
||||
KList<PlatformBlockState> hostLayers = biome.generateLayers(
|
||||
new IrisDimension(), 12D, -8D, rng, 1, 32, data, hostComplex);
|
||||
KList<PlatformBlockState> sourceLayers = biome.generateLayersWithSlope(
|
||||
new IrisDimension(), 12D, -8D, rng, 1, 32, data, sourceSlope);
|
||||
|
||||
assertTrue(hostLayers.isEmpty());
|
||||
assertEquals(1, sourceLayers.size());
|
||||
assertSame(surfaceBlock, sourceLayers.get(0));
|
||||
}
|
||||
}
|
||||
+37
@@ -16,6 +16,43 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisDimensionReachableBiomesTest {
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void includesOutOfPoolFocusBiomeAndRegion() {
|
||||
IrisDimension dimension = new IrisDimension()
|
||||
.setRegions(new KList<>("declared"))
|
||||
.setFocus("focused-biome")
|
||||
.setFocusRegion("focused-region");
|
||||
IrisRegion declared = new IrisRegion().setLandBiomes(new KList<>("declared-biome"));
|
||||
IrisRegion focused = new IrisRegion().setLandBiomes(new KList<>("focused-region-biome"));
|
||||
declared.setLoadKey("declared");
|
||||
focused.setLoadKey("focused-region");
|
||||
IrisBiome declaredBiome = biome("declared-biome");
|
||||
IrisBiome focusedBiome = biome("focused-biome");
|
||||
IrisBiome focusedRegionBiome = biome("focused-region-biome");
|
||||
|
||||
IrisData data = mock(IrisData.class);
|
||||
ResourceLoader<IrisRegion> regionLoader = mock(ResourceLoader.class);
|
||||
ResourceLoader<IrisBiome> biomeLoader = mock(ResourceLoader.class);
|
||||
when(data.getRegionLoader()).thenReturn(regionLoader);
|
||||
when(data.getBiomeLoader()).thenReturn(biomeLoader);
|
||||
when(regionLoader.load("declared")).thenReturn(declared);
|
||||
when(regionLoader.load("focused-region")).thenReturn(focused);
|
||||
when(biomeLoader.load("declared-biome")).thenReturn(declaredBiome);
|
||||
when(biomeLoader.load("focused-biome")).thenReturn(focusedBiome);
|
||||
when(biomeLoader.load("focused-region-biome")).thenReturn(focusedRegionBiome);
|
||||
|
||||
Set<String> regionKeys = dimension.getAllRegions(() -> data).stream()
|
||||
.map(IrisRegion::getLoadKey)
|
||||
.collect(Collectors.toSet());
|
||||
Set<String> biomeKeys = dimension.getReachableBiomes(() -> data).stream()
|
||||
.map(IrisBiome::getLoadKey)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
assertEquals(Set.of("declared", "focused-region"), regionKeys);
|
||||
assertEquals(Set.of("declared-biome", "focused-biome", "focused-region-biome"), biomeKeys);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void includesExactRecursiveGenerationClosure() {
|
||||
|
||||
@@ -20,4 +20,20 @@ public class IrisDimensionTypeKeyTest {
|
||||
|
||||
assertEquals("worlds_my_pack", dimension.getDimensionTypeKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customBiomeKeyPreservesFlatDimensionNamespace() {
|
||||
IrisDimension dimension = new IrisDimension();
|
||||
dimension.setLoadKey("Overworld");
|
||||
|
||||
assertEquals("overworld:aurora", dimension.getCustomBiomeKey("Aurora"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customBiomeKeyMapsRecursiveDimensionPath() {
|
||||
IrisDimension dimension = new IrisDimension();
|
||||
dimension.setLoadKey("Layers/Sky");
|
||||
|
||||
assertEquals("layers:sky/aurora", dimension.getCustomBiomeKey("Aurora"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package art.arcane.iris.engine.platform;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import org.bukkit.Location;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class EngineBukkitOpsBiomeTest {
|
||||
@Test
|
||||
public void mantleBiomeLookupConvertsWorldYToInternalY() {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisWorld world = IrisWorld.builder().minHeight(-64).maxHeight(320).build();
|
||||
IrisBiome biome = mock(IrisBiome.class);
|
||||
Location location = mock(Location.class);
|
||||
when(engine.getWorld()).thenReturn(world);
|
||||
when(location.getBlockX()).thenReturn(12);
|
||||
when(location.getBlockY()).thenReturn(-25);
|
||||
when(location.getBlockZ()).thenReturn(-7);
|
||||
when(engine.getBiomeOrMantle(12, 39, -7)).thenReturn(biome);
|
||||
|
||||
assertSame(biome, EngineBukkitOps.getBiomeOrMantle(engine, location));
|
||||
verify(engine).getBiomeOrMantle(12, 39, -7);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user