mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 14:00:26 +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:
1 parent
366033a535
commit
6549b40797
119 files changed
+6874
-743
No files matched your search
+405
-58
@@ -4,6 +4,9 @@ import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
|||||||
import com.mojang.datafixers.util.Pair;
|
import com.mojang.datafixers.util.Pair;
|
||||||
import com.mojang.serialization.MapCodec;
|
import com.mojang.serialization.MapCodec;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
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.data.cache.AtomicCache;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
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.collection.KMap;
|
||||||
import art.arcane.volmlib.util.math.RNG;
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
import net.minecraft.core.Holder;
|
import net.minecraft.core.Holder;
|
||||||
import net.minecraft.core.QuartPos;
|
import net.minecraft.core.QuartPos;
|
||||||
import net.minecraft.core.Registry;
|
import net.minecraft.core.Registry;
|
||||||
@@ -24,6 +28,8 @@ import net.minecraft.core.registries.Registries;
|
|||||||
import net.minecraft.resources.Identifier;
|
import net.minecraft.resources.Identifier;
|
||||||
import net.minecraft.resources.ResourceKey;
|
import net.minecraft.resources.ResourceKey;
|
||||||
import net.minecraft.util.RandomSource;
|
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.Biome;
|
||||||
import net.minecraft.world.level.biome.BiomeSource;
|
import net.minecraft.world.level.biome.BiomeSource;
|
||||||
import net.minecraft.world.level.biome.Climate;
|
import net.minecraft.world.level.biome.Climate;
|
||||||
@@ -80,27 +86,31 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
this.cacheRuntimeId = engine.getCacheID();
|
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<>();
|
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());
|
Holder<Biome> vanillaHolder = resolveBiomeHolder(registry, i.getStructureDerivativeKey());
|
||||||
if (vanillaHolder == null) {
|
if (vanillaHolder == null) {
|
||||||
throw new IllegalStateException("Iris structure biome derivative '"
|
throw new IllegalStateException("Iris structure biome derivative '"
|
||||||
+ i.getStructureDerivativeKey() + "' is not registered for biome '" + i.getLoadKey() + "'");
|
+ i.getStructureDerivativeKey() + "' is not registered for biome '" + i.getLoadKey() + "'");
|
||||||
}
|
}
|
||||||
biomes.add(vanillaHolder);
|
biomes.add(vanillaHolder);
|
||||||
|
}
|
||||||
|
|
||||||
if (i.isCustom()) {
|
if (includeDimensionStack) {
|
||||||
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
|
biomes.addAll(getVisibleBiomes(
|
||||||
Holder<Biome> customHolder = resolveCustomBiomeHolder(customRegistry, engine, j.getId());
|
customRegistry,
|
||||||
if (customHolder == null) {
|
registry,
|
||||||
throw new IllegalStateException("Iris custom structure biome '"
|
engine,
|
||||||
+ engine.getDimension().getLoadKey() + ":" + j.getId() + "' is not registered");
|
resolveFallbackBiome(registry, customRegistry)
|
||||||
}
|
));
|
||||||
biomes.add(customHolder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (biomes.isEmpty()) {
|
if (biomes.isEmpty()) {
|
||||||
@@ -110,6 +120,57 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
return new ArrayList<>(biomes);
|
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) {
|
private static Object getFor(Class<?> type, Object source) {
|
||||||
Object o = fieldFor(type, source);
|
Object o = fieldFor(type, source);
|
||||||
|
|
||||||
@@ -175,10 +236,19 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Stream<Holder<Biome>> collectPossibleBiomes() {
|
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() {
|
Set<Holder<Biome>> possibleStructureBiomes() {
|
||||||
|
return possibleBiomes(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<Holder<Biome>> possibleBiomes(boolean includeDimensionStack) {
|
||||||
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_possible_biomes");
|
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_possible_biomes");
|
||||||
if (lease == null) {
|
if (lease == null) {
|
||||||
throw new IllegalStateException("Iris possible biome lookup was rejected during an engine transition");
|
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);
|
.lookup(Registries.BIOME).orElse(null);
|
||||||
Registry<Biome> worldRegistry = ((CraftWorld) world).getHandle().registryAccess()
|
Registry<Biome> worldRegistry = ((CraftWorld) world).getHandle().registryAccess()
|
||||||
.lookup(Registries.BIOME).orElse(null);
|
.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;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (IrisBiome i : engine.getAllBiomes()) {
|
for (OwnedBiome ownedBiome : getOwnedBiomes(engine)) {
|
||||||
|
IrisBiome i = ownedBiome.biome();
|
||||||
if (i.isCustom()) {
|
if (i.isCustom()) {
|
||||||
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
|
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 (holder == null) {
|
||||||
if (fallback != 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;
|
continue;
|
||||||
}
|
}
|
||||||
m.put(j.getId(), holder);
|
m.put(key, holder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -232,7 +307,8 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
return Collections.unmodifiableMap(spawnBiomes);
|
return Collections.unmodifiableMap(spawnBiomes);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (IrisBiome irisBiome : engine.getAllBiomes()) {
|
for (OwnedBiome ownedBiome : getOwnedBiomes(engine)) {
|
||||||
|
IrisBiome irisBiome = ownedBiome.biome();
|
||||||
if (!irisBiome.isCustom()) {
|
if (!irisBiome.isCustom()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -241,7 +317,8 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
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) {
|
if (customHolder != null) {
|
||||||
spawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
|
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() {
|
private RegistryAccess registry() {
|
||||||
RegistryAccess access = registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()));
|
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");
|
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
|
||||||
}
|
}
|
||||||
ensureCachesCurrent();
|
ensureCachesCurrent();
|
||||||
if (isGuaranteedSurfaceBiome(y)) {
|
return getStructureNoiseBiomeWithActiveGenerationLease(x, y, z);
|
||||||
return getSurfaceStructureBiomeHolder(x, 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);
|
Holder<Biome> cachedHolder = structureBiomeCache.get(cacheKey);
|
||||||
if (cachedHolder != null) {
|
if (cachedHolder != null) {
|
||||||
return cachedHolder;
|
return cachedHolder;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Holder<Biome> resolvedHolder = resolveStructureBiomeHolder(x, y, z);
|
Holder<Biome> resolvedHolder = resolveStructureBiomeHolder(x, y, z);
|
||||||
Holder<Biome> existingHolder = structureBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
|
if (!cacheable) {
|
||||||
if (existingHolder != null) {
|
|
||||||
return existingHolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (structureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
|
||||||
structureBiomeCache.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolvedHolder;
|
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
|
@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) {
|
static int horizontalBiomeSearchQuartStep(int blockY, int searchRadius) {
|
||||||
return blockY == STRONGHOLD_RING_SEARCH_Y && searchRadius == STRONGHOLD_RING_SEARCH_RADIUS
|
return blockY == STRONGHOLD_RING_SEARCH_Y && searchRadius == STRONGHOLD_RING_SEARCH_RADIUS
|
||||||
? STRONGHOLD_RING_SEARCH_QUART_STEP
|
? STRONGHOLD_RING_SEARCH_QUART_STEP
|
||||||
@@ -468,7 +639,7 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
private Holder<Biome> resolveSurfaceStructureBiomeHolder(int x, int z) {
|
private Holder<Biome> resolveSurfaceStructureBiomeHolder(int x, int z) {
|
||||||
int blockX = x << 2;
|
int blockX = x << 2;
|
||||||
int blockZ = z << 2;
|
int blockZ = z << 2;
|
||||||
IrisBiome irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
IrisBiome irisBiome = resolveSurfaceStructureBiome(engine, blockX, blockZ);
|
||||||
if (irisBiome == null) {
|
if (irisBiome == null) {
|
||||||
throw new IllegalStateException("Iris returned no surface structure biome at block "
|
throw new IllegalStateException("Iris returned no surface structure biome at block "
|
||||||
+ blockX + "," + blockZ);
|
+ blockX + "," + blockZ);
|
||||||
@@ -532,14 +703,32 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
int z,
|
int z,
|
||||||
Climate.Sampler sampler,
|
Climate.Sampler sampler,
|
||||||
IrisDimensionCarvingResolver.State resolverState
|
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);
|
long cacheKey = packNoiseKey(x, y, z);
|
||||||
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
|
if (cacheable) {
|
||||||
if (cachedHolder != null) {
|
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
|
||||||
return cachedHolder;
|
if (cachedHolder != null) {
|
||||||
|
return cachedHolder;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Holder<Biome> resolvedHolder = resolveVisibleBiomeHolder(x, y, z, resolverState);
|
Holder<Biome> resolvedHolder = resolveVisibleBiomeHolder(x, y, z, resolverState);
|
||||||
|
if (!cacheable) {
|
||||||
|
return resolvedHolder;
|
||||||
|
}
|
||||||
Holder<Biome> existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
|
Holder<Biome> existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
|
||||||
if (existingHolder != null) {
|
if (existingHolder != null) {
|
||||||
return existingHolder;
|
return existingHolder;
|
||||||
@@ -552,6 +741,18 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
return resolvedHolder;
|
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) {
|
private GenerationSessionLease tryAcquireGenerationLease(String operation) {
|
||||||
if (engine.isClosed()) {
|
if (engine.isClosed()) {
|
||||||
return null;
|
return null;
|
||||||
@@ -597,7 +798,7 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Holder<Biome> resolveStructureBiomeHolder(int x, int y, int z) {
|
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) {
|
if (resolution == null) {
|
||||||
throw new IllegalStateException("Iris returned no structure biome at quart "
|
throw new IllegalStateException("Iris returned no structure biome at quart "
|
||||||
+ x + "," + y + "," + z);
|
+ x + "," + y + "," + z);
|
||||||
@@ -619,11 +820,26 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
int z,
|
int z,
|
||||||
IrisDimensionCarvingResolver.State resolverState
|
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) {
|
if (resolution == null) {
|
||||||
return getFallbackBiome();
|
return getFallbackBiome();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return resolveVisibleBiomeHolder(resolution);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Holder<Biome> resolveVisibleBiomeHolder(BiomeResolution resolution) {
|
||||||
if (resolution.irisBiome.isCustom()) {
|
if (resolution.irisBiome.isCustom()) {
|
||||||
return resolveCustomHolder(resolution);
|
return resolveCustomHolder(resolution);
|
||||||
}
|
}
|
||||||
@@ -642,7 +858,8 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
private Holder<Biome> resolveCustomHolder(BiomeResolution resolution) {
|
private Holder<Biome> resolveCustomHolder(BiomeResolution resolution) {
|
||||||
IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
|
IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
|
||||||
if (customBiome != null) {
|
if (customBiome != null) {
|
||||||
Holder<Biome> holder = customBiomes.get(customBiome.getId());
|
Holder<Biome> holder = customBiomes.get(customBiomeKey(
|
||||||
|
resolution.dimension, customBiome.getId()));
|
||||||
if (holder != null) {
|
if (holder != null) {
|
||||||
return holder;
|
return holder;
|
||||||
}
|
}
|
||||||
@@ -651,8 +868,8 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
return getFallbackBiome();
|
return getFallbackBiome();
|
||||||
}
|
}
|
||||||
|
|
||||||
private BiomeResolution resolveBiomeResolution(int x, int y, int z) {
|
private BiomeResolution resolveStructureBiomeResolution(int x, int y, int z) {
|
||||||
return resolveBiomeResolution(x, y, z, null);
|
return resolveBiomeResolution(x, y, z, null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private BiomeResolution resolveBiomeResolution(
|
private BiomeResolution resolveBiomeResolution(
|
||||||
@@ -660,6 +877,28 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
int y,
|
int y,
|
||||||
int z,
|
int z,
|
||||||
IrisDimensionCarvingResolver.State resolverState
|
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()) {
|
if (engine == null || engine.isClosed()) {
|
||||||
return null;
|
return null;
|
||||||
@@ -675,31 +914,63 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
int worldMinHeight = engine.getWorld().minHeight();
|
int worldMinHeight = engine.getWorld().minHeight();
|
||||||
int internalY = blockY - worldMinHeight;
|
int internalY = blockY - worldMinHeight;
|
||||||
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
|
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
|
||||||
|
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 deepUnderground = internalY <= caveSwitchInternalY;
|
||||||
boolean underground = false;
|
boolean underground = false;
|
||||||
IrisBiome irisBiome;
|
IrisBiome irisBiome;
|
||||||
if (deepUnderground) {
|
if (stackLayer != null) {
|
||||||
int surfaceInternalY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
|
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;
|
underground = internalY <= surfaceInternalY - 8;
|
||||||
irisBiome = underground
|
irisBiome = underground
|
||||||
? engine.getCaveBiome(blockX, internalY, blockZ, resolverState)
|
? engine.getCaveBiome(blockX, internalY, blockZ, resolverState)
|
||||||
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
: engine.getHostSurfaceBiome(blockX, blockZ);
|
||||||
} else {
|
} else {
|
||||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
irisBiome = engine.getHostSurfaceBiome(blockX, blockZ);
|
||||||
}
|
}
|
||||||
if (irisBiome == null && underground) {
|
if (irisBiome == null && underground) {
|
||||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
irisBiome = stackLayer == null
|
||||||
|
? engine.getHostSurfaceBiome(blockX, blockZ)
|
||||||
|
: stackLayer.biome();
|
||||||
}
|
}
|
||||||
if (irisBiome == null) {
|
if (irisBiome == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return createBiomeResolution(irisBiome, underground, blockX, blockY, blockZ);
|
return createBiomeResolution(
|
||||||
|
irisBiome, underground, owningDimension, blockX, blockY, blockZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
private BiomeResolution createBiomeResolution(
|
private BiomeResolution createBiomeResolution(
|
||||||
IrisBiome irisBiome,
|
IrisBiome irisBiome,
|
||||||
boolean underground,
|
boolean underground,
|
||||||
|
IrisDimension dimension,
|
||||||
int blockX,
|
int blockX,
|
||||||
int blockY,
|
int blockY,
|
||||||
int blockZ
|
int blockZ
|
||||||
@@ -708,7 +979,8 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
^ (((long) blockX) * 341873128712L)
|
^ (((long) blockX) * 341873128712L)
|
||||||
^ (((long) blockY) * 132897987541L)
|
^ (((long) blockY) * 132897987541L)
|
||||||
^ (((long) blockZ) * 42317861L));
|
^ (((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() {
|
private Holder<Biome> getFallbackBiome() {
|
||||||
@@ -734,15 +1006,20 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
return ((long) x << 32) ^ ((long) z & 4294967295L);
|
return ((long) x << 32) ^ ((long) z & 4294967295L);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Holder<Biome> resolveCustomBiomeHolder(Registry<Biome> customRegistry, Engine engine, String customBiomeId) {
|
private static Holder<Biome> resolveCustomBiomeHolder(
|
||||||
if (customRegistry == null || engine == null || customBiomeId == null || customBiomeId.isBlank()) {
|
Registry<Biome> customRegistry,
|
||||||
|
IrisDimension dimension,
|
||||||
|
String customBiomeId
|
||||||
|
) {
|
||||||
|
if (customRegistry == null || dimension == null
|
||||||
|
|| customBiomeId == null || customBiomeId.isBlank()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Identifier resourceLocation = Identifier.fromNamespaceAndPath(
|
Identifier resourceLocation = Identifier.tryParse(customBiomeKey(dimension, customBiomeId));
|
||||||
engine.getDimension().getLoadKey().toLowerCase(java.util.Locale.ROOT),
|
if (resourceLocation == null) {
|
||||||
customBiomeId.toLowerCase(java.util.Locale.ROOT)
|
return null;
|
||||||
);
|
}
|
||||||
Biome biome = customRegistry.getValue(resourceLocation);
|
Biome biome = customRegistry.getValue(resourceLocation);
|
||||||
if (biome == null) {
|
if (biome == null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -761,6 +1038,63 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
return optionalReferenceHolder.get();
|
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) {
|
private static Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String biomeKey) {
|
||||||
if (registry == null || biomeKey == null || biomeKey.isBlank()) {
|
if (registry == null || biomeKey == null || biomeKey.isBlank()) {
|
||||||
return null;
|
return null;
|
||||||
@@ -809,18 +1143,31 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
private static final class BiomeResolution {
|
private static final class BiomeResolution {
|
||||||
private final IrisBiome irisBiome;
|
private final IrisBiome irisBiome;
|
||||||
private final boolean underground;
|
private final boolean underground;
|
||||||
|
private final IrisDimension dimension;
|
||||||
private final int blockX;
|
private final int blockX;
|
||||||
private final int blockY;
|
private final int blockY;
|
||||||
private final int blockZ;
|
private final int blockZ;
|
||||||
private final RNG rng;
|
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.irisBiome = irisBiome;
|
||||||
this.underground = underground;
|
this.underground = underground;
|
||||||
|
this.dimension = dimension;
|
||||||
this.blockX = blockX;
|
this.blockX = blockX;
|
||||||
this.blockY = blockY;
|
this.blockY = blockY;
|
||||||
this.blockZ = blockZ;
|
this.blockZ = blockZ;
|
||||||
this.rng = rng;
|
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;
|
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.Engine;
|
||||||
import art.arcane.iris.engine.framework.NativeFeatureGenerationPolicy;
|
import art.arcane.iris.engine.framework.NativeFeatureGenerationPolicy;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
@@ -40,6 +42,9 @@ import java.util.List;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.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
|
* 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() {
|
private Set<String> visibleBiomeKeys() {
|
||||||
Set<String> keys = new LinkedHashSet<>();
|
Set<String> keys = new LinkedHashSet<>();
|
||||||
String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
|
for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) {
|
||||||
for (IrisBiome irisBiome : engine.getAllBiomes()) {
|
|
||||||
addKey(keys, irisBiome.getStructureDerivativeKey());
|
addKey(keys, irisBiome.getStructureDerivativeKey());
|
||||||
addKey(keys, irisBiome.getDerivativeKey());
|
addKey(keys, irisBiome.getDerivativeKey());
|
||||||
addKey(keys, irisBiome.getVanillaDerivativeKey());
|
addKey(keys, irisBiome.getVanillaDerivativeKey());
|
||||||
@@ -213,7 +217,7 @@ final class ImportedFeatureStage {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||||
addKey(keys, namespace + ":" + customBiome.getId());
|
addKey(keys, engine.getDimension().getCustomBiomeKey(customBiome.getId()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return keys;
|
return keys;
|
||||||
@@ -226,8 +230,7 @@ final class ImportedFeatureStage {
|
|||||||
private Map<String, Holder<Biome>> customBiomeDerivatives(Registry<Biome> registry,
|
private Map<String, Holder<Biome>> customBiomeDerivatives(Registry<Biome> registry,
|
||||||
Map<String, Holder<Biome>> byKey) {
|
Map<String, Holder<Biome>> byKey) {
|
||||||
Map<String, Holder<Biome>> derivatives = new HashMap<>();
|
Map<String, Holder<Biome>> derivatives = new HashMap<>();
|
||||||
String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
|
for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) {
|
||||||
for (IrisBiome irisBiome : engine.getAllBiomes()) {
|
|
||||||
if (!irisBiome.isCustom()) {
|
if (!irisBiome.isCustom()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -246,7 +249,7 @@ final class ImportedFeatureStage {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||||
derivatives.put(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT), derivative);
|
derivatives.put(engine.getDimension().getCustomBiomeKey(customBiome.getId()), derivative);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return derivatives;
|
return derivatives;
|
||||||
@@ -288,14 +291,24 @@ final class ImportedFeatureStage {
|
|||||||
List<FeatureSorter.StepFeatureData> steps = table.steps();
|
List<FeatureSorter.StepFeatureData> steps = table.steps();
|
||||||
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
|
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
|
||||||
long decorationSeed = random.setDecorationSeed(level.getSeed(), origin.getX(), origin.getZ());
|
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());
|
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
|
||||||
int staticMinY = engine.getMinHeight();
|
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,
|
level, centerPos,
|
||||||
(x, z) -> level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z),
|
surfaceFirstFreeY,
|
||||||
(x, z) -> level.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z),
|
floorFirstFreeY,
|
||||||
position -> staticObjects.contains(position.getX(), position.getY() - staticMinY, position.getZ()));
|
stackContext != null,
|
||||||
|
importedFeatureProtection(staticObjects, stackContext, staticMinY));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (int stepIndex = 0; stepIndex < steps.size(); stepIndex++) {
|
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,
|
private void placeStep(WorldGenLevel level, FeatureTable table, FeatureSorter.StepFeatureData stepData,
|
||||||
Registry<PlacedFeature> featureRegistry, Set<Holder<Biome>> chunkBiomes,
|
Registry<PlacedFeature> featureRegistry, Set<Holder<Biome>> chunkBiomes,
|
||||||
ChunkGenerator owner, WorldgenRandom random, long decorationSeed,
|
ChunkGenerator owner, WorldgenRandom random, long decorationSeed,
|
||||||
@@ -353,12 +390,37 @@ final class ImportedFeatureStage {
|
|||||||
return id == null ? feature.toString() : id.toString();
|
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<>();
|
List<Holder<Biome>> collected = new ArrayList<>();
|
||||||
ChunkPos.rangeClosed(sectionPos.chunk(), 1).forEach((ChunkPos chunkPos) -> {
|
ChunkPos.rangeClosed(sectionPos.chunk(), 1).forEach((ChunkPos chunkPos) -> {
|
||||||
ChunkAccess neighbour = level.getChunk(chunkPos.x(), chunkPos.z());
|
ChunkAccess neighbour = level.getChunk(chunkPos.x(), chunkPos.z());
|
||||||
for (LevelChunkSection section : neighbour.getSections()) {
|
LevelChunkSection[] sections = neighbour.getSections();
|
||||||
section.getBiomes().getAll(collected::add);
|
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<>();
|
Set<Holder<Biome>> present = new LinkedHashSet<>();
|
||||||
|
|||||||
+76
-14
@@ -1,5 +1,7 @@
|
|||||||
package art.arcane.iris.core.nms.v26_2_R1;
|
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.Engine;
|
||||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
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.CompletionException;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.IntBinaryOperator;
|
import java.util.function.IntBinaryOperator;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
public class IrisChunkGenerator extends CustomChunkGenerator {
|
public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||||
private static final WrappedField<ChunkGenerator, BiomeSource> BIOME_SOURCE;
|
private static final WrappedField<ChunkGenerator, BiomeSource> BIOME_SOURCE;
|
||||||
@@ -429,7 +432,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
undergroundStep,
|
undergroundStep,
|
||||||
decision.preserveSourceY(),
|
decision.preserveSourceY(),
|
||||||
decision.yBand(),
|
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(
|
StructureStart wrapped = NativeStructureReferenceEnvelope.wrapForPublication(
|
||||||
start, structure, start.getReferences(),
|
start, structure, start.getReferences(),
|
||||||
NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain()),
|
NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain()),
|
||||||
@@ -726,6 +729,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
return (x, z) -> engine.getHeight(x, z, true) + runtimeMinY + 1;
|
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
|
@Override
|
||||||
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(Holder<Biome> holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) {
|
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(Holder<Biome> holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) {
|
||||||
Holder<Biome> vanillaSpawnBiome = customBiomeSource.getVanillaSpawnBiome(holder);
|
Holder<Biome> vanillaSpawnBiome = customBiomeSource.getVanillaSpawnBiome(holder);
|
||||||
@@ -868,17 +879,22 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
WorldgenTerrainHeightmaps.primeStructurePlacement(
|
WorldgenTerrainHeightmaps.primeStructurePlacement(
|
||||||
world, chunkPos, heightmapStarts, worldgenSurfaceHeight(), worldgenFloorHeight());
|
world, chunkPos, heightmapStarts,
|
||||||
|
hostWorldgenSurfaceHeight(), hostWorldgenFloorHeight());
|
||||||
} catch (Throwable error) {
|
} catch (Throwable error) {
|
||||||
throw NativeStructureGenerationException.failure(
|
throw NativeStructureGenerationException.failure(
|
||||||
"heightmap priming", nativeStructureBatchContext(placementGroups),
|
"heightmap priming", nativeStructureBatchContext(placementGroups),
|
||||||
chunkPos.x(), chunkPos.z(), error);
|
chunkPos.x(), chunkPos.z(), error);
|
||||||
}
|
}
|
||||||
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
|
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
|
||||||
int staticMinY = engine.getMinHeight();
|
Predicate<BlockPos> protectedPosition = nativeStructureProtection(staticObjects);
|
||||||
WorldGenLevel boundedWorld = staticObjects.isEmpty() ? world : NativeStructureWorldgenAccess.create(
|
WorldGenLevel boundedWorld = staticObjects.isEmpty()
|
||||||
world, chunkPos, worldgenSurfaceHeight(), worldgenFloorHeight(),
|
&& engine.getDimensionStackContext() == null
|
||||||
position -> staticObjects.contains(position.getX(), position.getY() - staticMinY, position.getZ()));
|
? world
|
||||||
|
: NativeStructureWorldgenAccess.create(
|
||||||
|
world, chunkPos, hostWorldgenSurfaceHeight(), hostWorldgenFloorHeight(),
|
||||||
|
engine.getDimensionStackContext() != null,
|
||||||
|
protectedPosition);
|
||||||
try {
|
try {
|
||||||
NativeStructureVegetationClearer.clearIntersectingVegetation(
|
NativeStructureVegetationClearer.clearIntersectingVegetation(
|
||||||
boundedWorld, chunk, area, vegetationTargets);
|
boundedWorld, chunk, area, vegetationTargets);
|
||||||
@@ -891,7 +907,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
try {
|
try {
|
||||||
vacuumFoundationPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
|
vacuumFoundationPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
|
||||||
boundedWorld, area, terrainTargets,
|
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) {
|
} catch (Throwable error) {
|
||||||
throw NativeStructureGenerationException.failure(
|
throw NativeStructureGenerationException.failure(
|
||||||
"terrain integration", nativeStructureBatchContext(placementGroups),
|
"terrain integration", nativeStructureBatchContext(placementGroups),
|
||||||
@@ -945,17 +961,45 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
BoundingBox area, ChunkPos chunkPos, String structureId, StructureStart start,
|
BoundingBox area, ChunkPos chunkPos, String structureId, StructureStart start,
|
||||||
IrisNativeStructureDecision decision) {
|
IrisNativeStructureDecision decision) {
|
||||||
WorldGenLevel boundedWorld = world instanceof NativeStructureWorldgenAccess ? world : NativeStructureWorldgenAccess.create(
|
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);
|
world.setCurrentlyGenerating(() -> "Iris native structure " + structureId);
|
||||||
try {
|
try {
|
||||||
NativeStructurePostProcessor.place(boundedWorld, structureManager, this, random, area, chunkPos,
|
NativeStructurePostProcessor.place(boundedWorld, structureManager, this, random, area, chunkPos,
|
||||||
structureId, start, decision, this::resolvePaletteBlock,
|
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 {
|
} finally {
|
||||||
world.setCurrentlyGenerating(null);
|
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) {
|
private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
|
||||||
StructureStepCache cached = structureStepCache;
|
StructureStepCache cached = structureStepCache;
|
||||||
if (cached != null && cached.registry() == registry) {
|
if (cached != null && cached.registry() == registry) {
|
||||||
@@ -1041,7 +1085,17 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
@Override
|
@Override
|
||||||
public void spawnOriginalMobs(WorldGenRegion region) {
|
public void spawnOriginalMobs(WorldGenRegion region) {
|
||||||
ChunkPos center = region.getCenter();
|
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);
|
Holder<Biome> vanillaBiome = customBiomeSource.getVanillaSpawnBiome(visibleBiome);
|
||||||
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
|
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
|
||||||
random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ());
|
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) {
|
public int getBaseHeight(int i, int j, Heightmap.Types heightmap_type, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
|
||||||
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_height");
|
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_height");
|
||||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
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()
|
return levelheightaccessor.getMinY()
|
||||||
+ engine.getHeight(i, j, !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState()))
|
+ height + 1;
|
||||||
+ 1;
|
|
||||||
} catch (GenerationSessionException e) {
|
} catch (GenerationSessionException e) {
|
||||||
throw new IllegalStateException("Iris base height query could not acquire its engine runtime.", 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) {
|
public NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
|
||||||
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_column");
|
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_column");
|
||||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
int block = engine.getHeight(i, j, true);
|
boolean dimensionStack = engine.getDimensionStackContext() != null;
|
||||||
int water = engine.getHeight(i, j, false);
|
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()];
|
BlockState[] column = new BlockState[levelheightaccessor.getHeight()];
|
||||||
for (int k = 0; k < column.length; k++) {
|
for (int k = 0; k < column.length; k++) {
|
||||||
if (k <= block) {
|
if (k <= block) {
|
||||||
|
|||||||
+5
@@ -824,6 +824,11 @@ public class NMSBinding implements INMSBinding {
|
|||||||
return NativeStructureVolumeIndex.volumes(engine, minX, minZ, maxX, maxZ);
|
return NativeStructureVolumeIndex.volumes(engine, minX, minZ, maxX, maxZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void invalidateNativeStructureVolumeIndex(Engine engine) {
|
||||||
|
NativeStructureVolumeIndex.invalidate(engine);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getBiomeId(Biome biome) {
|
public int getBiomeId(Biome biome) {
|
||||||
for (World i : Bukkit.getWorlds()) {
|
for (World i : Bukkit.getWorlds()) {
|
||||||
|
|||||||
+6
-2
@@ -64,6 +64,7 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
|
|||||||
private final ChunkPos generationCenter;
|
private final ChunkPos generationCenter;
|
||||||
private final IntBinaryOperator surfaceFirstFreeY;
|
private final IntBinaryOperator surfaceFirstFreeY;
|
||||||
private final IntBinaryOperator floorFirstFreeY;
|
private final IntBinaryOperator floorFirstFreeY;
|
||||||
|
private final boolean virtualizeHeights;
|
||||||
private final Predicate<BlockPos> protectedPosition;
|
private final Predicate<BlockPos> protectedPosition;
|
||||||
private final Holder<Biome> fallbackBiome;
|
private final Holder<Biome> fallbackBiome;
|
||||||
private final BiomeManager biomeManager;
|
private final BiomeManager biomeManager;
|
||||||
@@ -81,6 +82,7 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
|
|||||||
boundary.surfaceFirstFreeY(), "Native structure world access requires a surface resolver");
|
boundary.surfaceFirstFreeY(), "Native structure world access requires a surface resolver");
|
||||||
this.floorFirstFreeY = Objects.requireNonNull(
|
this.floorFirstFreeY = Objects.requireNonNull(
|
||||||
boundary.floorFirstFreeY(), "Native structure world access requires an ocean-floor resolver");
|
boundary.floorFirstFreeY(), "Native structure world access requires an ocean-floor resolver");
|
||||||
|
this.virtualizeHeights = boundary.virtualizeHeights();
|
||||||
this.protectedPosition = Objects.requireNonNull(
|
this.protectedPosition = Objects.requireNonNull(
|
||||||
boundary.protectedPosition(), "World access requires a protected-position predicate");
|
boundary.protectedPosition(), "World access requires a protected-position predicate");
|
||||||
BlockPos biomeSample = generationCenter.getMiddleBlockPosition(delegate.getSeaLevel());
|
BlockPos biomeSample = generationCenter.getMiddleBlockPosition(delegate.getSeaLevel());
|
||||||
@@ -103,9 +105,10 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
|
|||||||
static NativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter,
|
static NativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter,
|
||||||
IntBinaryOperator surfaceFirstFreeY,
|
IntBinaryOperator surfaceFirstFreeY,
|
||||||
IntBinaryOperator floorFirstFreeY,
|
IntBinaryOperator floorFirstFreeY,
|
||||||
|
boolean virtualizeHeights,
|
||||||
Predicate<BlockPos> protectedPosition) {
|
Predicate<BlockPos> protectedPosition) {
|
||||||
return new NativeStructureWorldgenAccess(delegate, new Boundary(
|
return new NativeStructureWorldgenAccess(delegate, new Boundary(
|
||||||
generationCenter, surfaceFirstFreeY, floorFirstFreeY, protectedPosition));
|
generationCenter, surfaceFirstFreeY, floorFirstFreeY, virtualizeHeights, protectedPosition));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -250,7 +253,7 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getHeight(Heightmap.Types type, int x, int z) {
|
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 delegate.getHeight(type, x, z);
|
||||||
}
|
}
|
||||||
return height(type, x, z);
|
return height(type, x, z);
|
||||||
@@ -549,6 +552,7 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
|
|||||||
private record Boundary(ChunkPos generationCenter,
|
private record Boundary(ChunkPos generationCenter,
|
||||||
IntBinaryOperator surfaceFirstFreeY,
|
IntBinaryOperator surfaceFirstFreeY,
|
||||||
IntBinaryOperator floorFirstFreeY,
|
IntBinaryOperator floorFirstFreeY,
|
||||||
|
boolean virtualizeHeights,
|
||||||
Predicate<BlockPos> protectedPosition) {
|
Predicate<BlockPos> protectedPosition) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+176
-1
@@ -1,15 +1,29 @@
|
|||||||
package art.arcane.iris.core.nms.v26_2_R1;
|
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 org.junit.Test;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class CustomBiomeSourceStructureContractTest {
|
public class CustomBiomeSourceStructureContractTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraftRegistries() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void nativeStructuresUseTerrainSafeDerivativeAtEveryBiomeBoundary() throws IOException {
|
public void nativeStructuresUseTerrainSafeDerivativeAtEveryBiomeBoundary() throws IOException {
|
||||||
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
|
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
|
||||||
@@ -69,14 +83,53 @@ public class CustomBiomeSourceStructureContractTest {
|
|||||||
String realResolution = source.substring(realResolutionStart, naturalResolutionStart);
|
String realResolution = source.substring(realResolutionStart, naturalResolutionStart);
|
||||||
String naturalResolution = source.substring(naturalResolutionStart, resolutionEnd);
|
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()"));
|
assertFalse(realResolution.contains("getNaturalTrueBiomeStream()"));
|
||||||
assertTrue(naturalResolution.contains(
|
assertTrue(naturalResolution.contains(
|
||||||
"engine.getComplex().getNaturalTrueBiomeStream().get(blockX, blockZ)"));
|
"engine.getComplex().getNaturalTrueBiomeStream().get(blockX, blockZ)"));
|
||||||
assertFalse(naturalResolution.contains("getTrueBiomeStream()"));
|
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"));
|
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
|
@Test
|
||||||
public void visibleBiomeBatchReusesTheCallersGenerationLease() throws IOException {
|
public void visibleBiomeBatchReusesTheCallersGenerationLease() throws IOException {
|
||||||
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
|
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
|
||||||
@@ -94,4 +147,126 @@ public class CustomBiomeSourceStructureContractTest {
|
|||||||
< lookup.indexOf("getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler)"));
|
< lookup.indexOf("getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler)"));
|
||||||
assertFalse(source.contains("studioBootstrapNoiseBiomeCache"));
|
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"));
|
< placement.indexOf("for (NativePlacementGroup group"));
|
||||||
assertTrue(placement.indexOf("repairVacuumFoundations")
|
assertTrue(placement.indexOf("repairVacuumFoundations")
|
||||||
> placement.indexOf("for (NativePlacementGroup group"));
|
> placement.indexOf("for (NativePlacementGroup group"));
|
||||||
|
assertTrue(placement.contains("nativeStructureProtection(staticObjects)"));
|
||||||
|
assertTrue(placement.contains("Engine.hostHeight(engine, x, z, true)"));
|
||||||
assertFalse(placement.contains("IrisLogging.reportError"));
|
assertFalse(placement.contains("IrisLogging.reportError"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -542,8 +544,8 @@ public class IrisChunkGeneratorFailureContractTest {
|
|||||||
String spawn = source.substring(spawnStart, spawnEnd);
|
String spawn = source.substring(spawnStart, spawnEnd);
|
||||||
|
|
||||||
assertTrue(spawn.contains("customBiomeSource.getVanillaSpawnBiome(visibleBiome)"));
|
assertTrue(spawn.contains("customBiomeSource.getVanillaSpawnBiome(visibleBiome)"));
|
||||||
|
assertTrue(spawn.contains("customBiomeSource.getVisibleSurfaceBiome("));
|
||||||
assertTrue(spawn.contains("NaturalSpawner.spawnMobsForChunkGeneration("));
|
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("new LegacyRandomSource(RandomSupport.generateUniqueSeed())"));
|
||||||
assertTrue(spawn.contains("random.setDecorationSeed(region.getSeed()"));
|
assertTrue(spawn.contains("random.setDecorationSeed(region.getSeed()"));
|
||||||
assertFalse(spawn.contains("delegate.spawnOriginalMobs"));
|
assertFalse(spawn.contains("delegate.spawnOriginalMobs"));
|
||||||
|
|||||||
+24
-2
@@ -135,6 +135,24 @@ public class NativeStructureWorldgenAccessTest {
|
|||||||
assertEquals(1, recording.blockTicks.count());
|
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
|
@Test
|
||||||
public void protectedCellsRemainReadableButRejectWritesTilesAndTicks() {
|
public void protectedCellsRemainReadableButRejectWritesTilesAndTicks() {
|
||||||
RecordingDelegate recording = new RecordingDelegate();
|
RecordingDelegate recording = new RecordingDelegate();
|
||||||
@@ -142,7 +160,9 @@ public class NativeStructureWorldgenAccessTest {
|
|||||||
NativeStructureWorldgenAccess access = NativeStructureWorldgenAccess.create(
|
NativeStructureWorldgenAccess access = NativeStructureWorldgenAccess.create(
|
||||||
recording.world(), generationCenter(),
|
recording.world(), generationCenter(),
|
||||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
(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());
|
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
|
||||||
assertFalse(access.ensureCanWrite(position));
|
assertFalse(access.ensureCanWrite(position));
|
||||||
@@ -211,7 +231,9 @@ public class NativeStructureWorldgenAccessTest {
|
|||||||
return NativeStructureWorldgenAccess.create(
|
return NativeStructureWorldgenAccess.create(
|
||||||
recording.world(), generationCenter(),
|
recording.world(), generationCenter(),
|
||||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
(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() {
|
private static ChunkPos generationCenter() {
|
||||||
|
|||||||
+12
@@ -332,6 +332,18 @@ public class NativeStructureFactoryTest {
|
|||||||
assertTrue(failureStart < source.indexOf("NativeStructureOwnershipStore.record("));
|
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
|
@Test
|
||||||
public void failedManualStartPublicationClearsRecordedOwnershipAndPreservesTheFailure() throws Exception {
|
public void failedManualStartPublicationClearsRecordedOwnershipAndPreservesTheFailure() throws Exception {
|
||||||
String source = Files.readString(
|
String source = Files.readString(
|
||||||
|
|||||||
+5
@@ -84,6 +84,11 @@ public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
|
|||||||
"iris");
|
"iris");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void prepareRuntimeHotload(Engine engine) {
|
||||||
|
INMS.get().invalidateNativeStructureVolumeIndex(engine);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void applyWorldBoundary(Engine engine) {
|
public void applyWorldBoundary(Engine engine) {
|
||||||
IrisDimension expectedDimension = engine.getDimension();
|
IrisDimension expectedDimension = engine.getDimension();
|
||||||
|
|||||||
+1
-1
@@ -163,7 +163,7 @@ public final class NativeStructureOwnershipRecovery {
|
|||||||
level,
|
level,
|
||||||
biome -> true,
|
biome -> true,
|
||||||
generator.getSeaLevel(),
|
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);
|
return NativeStructureFactory.generate(generationContext, holder, plan, references);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -90,7 +90,7 @@ public final class NativeStructureStartInjector {
|
|||||||
chunk,
|
chunk,
|
||||||
biome -> true,
|
biome -> true,
|
||||||
context.generator().getSeaLevel(),
|
context.generator().getSeaLevel(),
|
||||||
(x, z) -> context.engine().getHeight(x, z, true)
|
(x, z) -> Engine.hostHeight(context.engine(), x, z, true)
|
||||||
+ context.engine().getMinHeight()
|
+ context.engine().getMinHeight()
|
||||||
);
|
);
|
||||||
StructureStart generated = NativeStructureFactory.generate(
|
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) {
|
public static KList<NativeStructureVolume> volumes(Engine engine, int minX, int minZ, int maxX, int maxZ) {
|
||||||
NativeStructureVolumeIndex index = engine == null ? null : INDEXES.get(engine);
|
NativeStructureVolumeIndex index = engine == null ? null : INDEXES.get(engine);
|
||||||
return index == null ? NativeStructureVolume.NONE : index.resolve(engine, minX, minZ, maxX, maxZ);
|
return index == null ? NativeStructureVolume.NONE : index.resolve(engine, minX, minZ, maxX, maxZ);
|
||||||
@@ -102,6 +114,12 @@ public final class NativeStructureVolumeIndex {
|
|||||||
return ORIGIN_REACH_CHUNKS;
|
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) {
|
KList<NativeStructureVolume> resolve(Engine engine, int minX, int minZ, int maxX, int maxZ) {
|
||||||
int fromChunkX = Math.min(minX, maxX) >> 4;
|
int fromChunkX = Math.min(minX, maxX) >> 4;
|
||||||
int toChunkX = Math.max(minX, maxX) >> 4;
|
int toChunkX = Math.max(minX, maxX) >> 4;
|
||||||
@@ -323,7 +341,7 @@ public final class NativeStructureVolumeIndex {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static IntBinaryOperator surfaceHeight(Engine engine) {
|
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,
|
static KList<NativeStructureVolume> appendPieces(KList<NativeStructureVolume> volumes,
|
||||||
|
|||||||
+491
-49
@@ -18,16 +18,22 @@
|
|||||||
|
|
||||||
package art.arcane.iris.modded;
|
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.Engine;
|
||||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
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.iris.util.project.context.IrisContext;
|
||||||
import art.arcane.volmlib.util.math.RNG;
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
import com.mojang.datafixers.util.Pair;
|
import com.mojang.datafixers.util.Pair;
|
||||||
import com.mojang.serialization.MapCodec;
|
import com.mojang.serialization.MapCodec;
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
import net.minecraft.core.Holder;
|
import net.minecraft.core.Holder;
|
||||||
import net.minecraft.core.HolderLookup;
|
import net.minecraft.core.HolderLookup;
|
||||||
import net.minecraft.core.QuartPos;
|
import net.minecraft.core.QuartPos;
|
||||||
@@ -37,12 +43,15 @@ import net.minecraft.resources.Identifier;
|
|||||||
import net.minecraft.resources.ResourceKey;
|
import net.minecraft.resources.ResourceKey;
|
||||||
import net.minecraft.server.MinecraftServer;
|
import net.minecraft.server.MinecraftServer;
|
||||||
import net.minecraft.util.RandomSource;
|
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.Biome;
|
||||||
import net.minecraft.world.level.biome.BiomeSource;
|
import net.minecraft.world.level.biome.BiomeSource;
|
||||||
import net.minecraft.world.level.biome.Climate;
|
import net.minecraft.world.level.biome.Climate;
|
||||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
@@ -196,11 +205,16 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
|
|
||||||
private List<Holder<Biome>> collectPossibleBiomeHolders() {
|
private List<Holder<Biome>> collectPossibleBiomeHolders() {
|
||||||
BiomeKeySets keys = biomeKeySets();
|
BiomeKeySets keys = biomeKeySets();
|
||||||
Set<String> generatedBiomeKeys = requireConfiguredStructureBiomeKeys(keys.required());
|
LinkedHashSet<String> generatedBiomeKeys = new LinkedHashSet<>(
|
||||||
Set<String> visibleBiomeKeys = keys.visibleOnly();
|
requireConfiguredStructureBiomeKeys(keys.structureRequired()));
|
||||||
|
generatedBiomeKeys.addAll(keys.visibleRequired());
|
||||||
|
LinkedHashSet<String> visibleBiomeKeys = new LinkedHashSet<>(keys.visibleOnly());
|
||||||
Registry<Biome> registry = biomeRegistry();
|
Registry<Biome> registry = biomeRegistry();
|
||||||
LinkedHashSet<Holder<Biome>> possible = new LinkedHashSet<>();
|
LinkedHashSet<Holder<Biome>> possible = new LinkedHashSet<>();
|
||||||
if (registry == null) {
|
if (registry == null) {
|
||||||
|
if (keys.visibleFallbackPossible()) {
|
||||||
|
visibleBiomeKeys.add("minecraft:plains");
|
||||||
|
}
|
||||||
for (Holder<Biome> biome : serializedSource.possibleBiomes()) {
|
for (Holder<Biome> biome : serializedSource.possibleBiomes()) {
|
||||||
String key = holderKey(biome);
|
String key = holderKey(biome);
|
||||||
if (isGeneratedBiomeKey(key, generatedBiomeKeys)
|
if (isGeneratedBiomeKey(key, generatedBiomeKeys)
|
||||||
@@ -215,6 +229,9 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
throw new IllegalStateException("Iris structure biomes are not registered: "
|
throw new IllegalStateException("Iris structure biomes are not registered: "
|
||||||
+ missingBiomeKeys);
|
+ missingBiomeKeys);
|
||||||
}
|
}
|
||||||
|
if (needsVisibleFallback(keys, registry)) {
|
||||||
|
visibleBiomeKeys.add("minecraft:plains");
|
||||||
|
}
|
||||||
// Registry-ordered, never a hash-ordered walk: see possibleBiomes().
|
// Registry-ordered, never a hash-ordered walk: see possibleBiomes().
|
||||||
registry.listElements().forEach((Holder.Reference<Biome> reference) -> {
|
registry.listElements().forEach((Holder.Reference<Biome> reference) -> {
|
||||||
String key = holderKey(reference);
|
String key = holderKey(reference);
|
||||||
@@ -223,7 +240,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
possible.add(reference);
|
possible.add(reference);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
warnUnregisteredVisibleBiomes(registry, visibleBiomeKeys);
|
warnUnregisteredVisibleBiomes(registry, keys.visibleOnly());
|
||||||
}
|
}
|
||||||
if (possible.isEmpty()) {
|
if (possible.isEmpty()) {
|
||||||
String phase = registry == null ? "serialized biome bootstrap" : "biome registry";
|
String phase = registry == null ? "serialized biome bootstrap" : "biome registry";
|
||||||
@@ -233,6 +250,36 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
return List.copyOf(possible);
|
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
|
* 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.
|
* 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,
|
private Holder<Biome> getNoiseBiome(Engine engine, int quartX, int quartY, int quartZ,
|
||||||
Climate.Sampler sampler) {
|
Climate.Sampler sampler) {
|
||||||
if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) {
|
if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) {
|
||||||
return getSurfaceStructureBiome(engine, quartX, quartZ, sampler);
|
return getSurfaceStructureBiome(engine, quartX, quartZ, sampler);
|
||||||
}
|
}
|
||||||
|
boolean cacheable = isBiomeCacheable(
|
||||||
|
engine,
|
||||||
|
QuartPos.toBlock(quartX),
|
||||||
|
QuartPos.toBlock(quartZ)
|
||||||
|
);
|
||||||
long key = packNoiseKey(quartX, quartY, quartZ);
|
long key = packNoiseKey(quartX, quartY, quartZ);
|
||||||
BiomeHolderTable cache = structureBiomeCache;
|
BiomeHolderTable cache = structureBiomeCache;
|
||||||
Holder<Biome> cached = cache.get(key);
|
if (cacheable) {
|
||||||
if (cached != null) {
|
Holder<Biome> cached = cache.get(key);
|
||||||
return cached;
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Holder<Biome> resolved = resolveStructureBiome(engine, quartX, quartY, quartZ, sampler);
|
Holder<Biome> resolved = resolveStructureBiome(engine, quartX, quartY, quartZ, sampler);
|
||||||
cache.put(key, resolved);
|
if (cacheable) {
|
||||||
|
cache.put(key, resolved);
|
||||||
|
}
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,14 +421,81 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
if (!isReady(engine)) {
|
if (!isReady(engine)) {
|
||||||
throw new IllegalStateException("Iris visible biome lookup has no active engine runtime");
|
throw new IllegalStateException("Iris visible biome lookup has no active engine runtime");
|
||||||
}
|
}
|
||||||
long key = packNoiseKey(quartX, quartY, quartZ);
|
return getVisibleNoiseBiomeWithActiveGenerationLease(
|
||||||
BiomeHolderTable cache = visibleBiomeCache;
|
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);
|
Holder<Biome> cached = cache.get(key);
|
||||||
if (cached != null) {
|
if (cached != null) {
|
||||||
return cached;
|
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);
|
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;
|
return resolved;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -395,7 +581,9 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
if (registry == null) {
|
if (registry == null) {
|
||||||
throw new IllegalStateException("Iris structure biome lookup has no biome registry");
|
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) {
|
if (irisBiome == null) {
|
||||||
throw new IllegalStateException("Iris returned no surface structure biome at quart "
|
throw new IllegalStateException("Iris returned no surface structure biome at quart "
|
||||||
+ quartX + "," + quartZ);
|
+ quartX + "," + quartZ);
|
||||||
@@ -412,7 +600,8 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
private Holder<Biome> resolveStructureBiome(Engine engine, int quartX, int quartY, int quartZ,
|
private Holder<Biome> resolveStructureBiome(Engine engine, int quartX, int quartY, int quartZ,
|
||||||
Climate.Sampler sampler) {
|
Climate.Sampler sampler) {
|
||||||
Registry<Biome> registry = biomeRegistry();
|
Registry<Biome> registry = biomeRegistry();
|
||||||
BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ);
|
BiomeResolution resolution = resolveStructureBiomeResolution(
|
||||||
|
engine, quartX, quartY, quartZ);
|
||||||
if (resolution == null) {
|
if (resolution == null) {
|
||||||
throw new IllegalStateException("Iris returned no structure biome at quart "
|
throw new IllegalStateException("Iris returned no structure biome at quart "
|
||||||
+ quartX + "," + quartY + "," + quartZ);
|
+ quartX + "," + quartY + "," + quartZ);
|
||||||
@@ -431,11 +620,36 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
|
|
||||||
private Holder<Biome> resolveVisibleBiome(Engine engine, int quartX, int quartY, int quartZ,
|
private Holder<Biome> resolveVisibleBiome(Engine engine, int quartX, int quartY, int quartZ,
|
||||||
Climate.Sampler sampler) {
|
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();
|
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) {
|
if (resolution == null || registry == null) {
|
||||||
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
|
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;
|
String biomeKey;
|
||||||
if (resolution.irisBiome().isCustom()) {
|
if (resolution.irisBiome().isCustom()) {
|
||||||
IrisBiomeCustom customBiome = resolution.irisBiome().getCustomBiome(
|
IrisBiomeCustom customBiome = resolution.irisBiome().getCustomBiome(
|
||||||
@@ -444,7 +658,8 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
return fallbackBiome(registry, "custom derivative of '"
|
return fallbackBiome(registry, "custom derivative of '"
|
||||||
+ resolution.irisBiome().getLoadKey() + "'", quartX, quartY, quartZ, sampler);
|
+ 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()) {
|
} else if (resolution.underground()) {
|
||||||
biomeKey = resolution.irisBiome().getGroundBiomeKey(
|
biomeKey = resolution.irisBiome().getGroundBiomeKey(
|
||||||
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
|
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) {
|
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)) {
|
if (!isReady(engine)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -467,27 +713,75 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
int blockZ = quartZ << 2;
|
int blockZ = quartZ << 2;
|
||||||
int internalY = blockY - engine.getMinHeight();
|
int internalY = blockY - engine.getMinHeight();
|
||||||
int caveSwitchY = Math.max(-8 - engine.getMinHeight(), 40);
|
int caveSwitchY = Math.max(-8 - engine.getMinHeight(), 40);
|
||||||
|
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;
|
boolean underground = false;
|
||||||
IrisBiome irisBiome;
|
IrisBiome irisBiome;
|
||||||
if (internalY <= caveSwitchY) {
|
if (stackLayer != null) {
|
||||||
int surfaceY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
|
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);
|
underground = isUnderground(internalY, surfaceY);
|
||||||
irisBiome = underground
|
irisBiome = underground
|
||||||
? engine.getCaveBiome(blockX, internalY, blockZ)
|
? engine.getCaveBiome(blockX, internalY, blockZ)
|
||||||
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
: engine.getHostSurfaceBiome(blockX, blockZ);
|
||||||
} else {
|
} else {
|
||||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
irisBiome = engine.getHostSurfaceBiome(blockX, blockZ);
|
||||||
}
|
}
|
||||||
if (irisBiome == null && underground) {
|
if (irisBiome == null && underground) {
|
||||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
irisBiome = stackLayer == null
|
||||||
|
? engine.getHostSurfaceBiome(blockX, blockZ)
|
||||||
|
: stackLayer.biome();
|
||||||
}
|
}
|
||||||
if (irisBiome == null) {
|
if (irisBiome == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
IrisModdedChunkGenerator current = generator;
|
return createBiomeResolution(
|
||||||
long worldSeed = current == null ? engine.getWorld().getRawWorldSeed() : current.visibleBiomeSeed();
|
engine,
|
||||||
long seed = biomeResolutionSeed(worldSeed, blockX, blockY, blockZ);
|
irisBiome,
|
||||||
return new BiomeResolution(irisBiome, underground, blockX, blockY, blockZ, new RNG(seed));
|
underground,
|
||||||
|
owningDimension,
|
||||||
|
owningData,
|
||||||
|
blockX,
|
||||||
|
blockY,
|
||||||
|
blockZ
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Holder<Biome> resolveRequiredStructureBiome(int quartX, int quartY, int quartZ) {
|
private Holder<Biome> resolveRequiredStructureBiome(int quartX, int quartY, int quartZ) {
|
||||||
@@ -510,9 +804,10 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
}
|
}
|
||||||
IrisBiome irisBiome;
|
IrisBiome irisBiome;
|
||||||
if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) {
|
if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) {
|
||||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(quartX << 2, quartZ << 2);
|
irisBiome = resolveSurfaceStructureBiome(engine, quartX << 2, quartZ << 2);
|
||||||
} else {
|
} else {
|
||||||
BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ);
|
BiomeResolution resolution = resolveStructureBiomeResolution(
|
||||||
|
engine, quartX, quartY, quartZ);
|
||||||
irisBiome = resolution == null ? null : resolution.irisBiome();
|
irisBiome = resolution == null ? null : resolution.irisBiome();
|
||||||
}
|
}
|
||||||
if (irisBiome == null) {
|
if (irisBiome == null) {
|
||||||
@@ -557,6 +852,10 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
return internalY <= surfaceY - 8;
|
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) {
|
static boolean isMonumentSurfaceBiomeQuery(int blockY, int radius, int minHeight, int fluidHeight) {
|
||||||
return radius == 29 && blockY == minHeight + fluidHeight;
|
return radius == 29 && blockY == minHeight + fluidHeight;
|
||||||
}
|
}
|
||||||
@@ -612,7 +911,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Set<String> exactStructureBiomeKeys() {
|
private Set<String> exactStructureBiomeKeys() {
|
||||||
return biomeKeySets().required();
|
return biomeKeySets().structureRequired();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -636,11 +935,12 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
private BiomeKeySets collectBiomeKeySets(long generation) {
|
private BiomeKeySets collectBiomeKeySets(long generation) {
|
||||||
IrisModdedChunkGenerator current = generator;
|
IrisModdedChunkGenerator current = generator;
|
||||||
if (current == null) {
|
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();
|
Engine engine = current.structureEngineOrNull();
|
||||||
if (engine == null) {
|
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");
|
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_biome_keys");
|
||||||
if (lease == null) {
|
if (lease == null) {
|
||||||
@@ -650,36 +950,154 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
if (!isReady(engine)) {
|
if (!isReady(engine)) {
|
||||||
throw new IllegalStateException("Iris structure biome key lookup has no active engine runtime");
|
throw new IllegalStateException("Iris structure biome key lookup has no active engine runtime");
|
||||||
}
|
}
|
||||||
LinkedHashSet<String> required = new LinkedHashSet<>();
|
Set<String> structureRequired = collectStructureBiomeKeys(
|
||||||
LinkedHashSet<String> visible = new LinkedHashSet<>();
|
engine.getDimension(), engine.getData());
|
||||||
for (IrisBiome irisBiome : engine.getAllBiomes()) {
|
LinkedHashSet<String> visibleRequired = new LinkedHashSet<>();
|
||||||
String derivative = normalizeKey(irisBiome.getStructureDerivativeKey());
|
LinkedHashSet<String> visibleEmittable = new LinkedHashSet<>();
|
||||||
if (derivative != null) {
|
boolean visibleFallbackPossible = false;
|
||||||
required.add(derivative);
|
for (OwnedBiome ownedBiome : getOwnedBiomes(engine)) {
|
||||||
}
|
IrisBiome irisBiome = ownedBiome.biome();
|
||||||
if (irisBiome.isCustom()) {
|
if (irisBiome.isCustom()) {
|
||||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
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()) {
|
for (String scatter : irisBiome.getBiomeScatter()) {
|
||||||
addVisibleBiomeKey(visible, scatter);
|
visibleFallbackPossible |= !addVisibleBiomeKey(visibleEmittable, scatter);
|
||||||
}
|
}
|
||||||
for (String scatter : irisBiome.getBiomeSkyScatter()) {
|
for (String scatter : irisBiome.getBiomeSkyScatter()) {
|
||||||
addVisibleBiomeKey(visible, scatter);
|
visibleFallbackPossible |= !addVisibleBiomeKey(visibleEmittable, scatter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
visible.removeAll(required);
|
LinkedHashSet<String> visible = new LinkedHashSet<>(visibleEmittable);
|
||||||
return new BiomeKeySets(generation, Set.copyOf(required), Set.copyOf(visible));
|
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) {
|
static Set<String> collectStructureBiomeKeys(IrisDimension dimension, IrisData data) {
|
||||||
String normalized = normalizeKey(key);
|
LinkedHashSet<String> keys = new LinkedHashSet<>();
|
||||||
if (normalized != null) {
|
for (IrisBiome irisBiome : dimension.getReachableBiomes(() -> data)) {
|
||||||
target.add(normalized);
|
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) {
|
private static Set<String> registeredBiomeKeys(Registry<Biome> registry) {
|
||||||
@@ -726,7 +1144,13 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
Holder<Biome> plains = resolveHolder(registry, "minecraft:plains");
|
Holder<Biome> plains = resolveHolder(registry, "minecraft:plains");
|
||||||
warnUnresolvedBiome(unresolvedKey, plains == null ? "the serialized biome source" : "minecraft:plains",
|
warnUnresolvedBiome(unresolvedKey, plains == null ? "the serialized biome source" : "minecraft:plains",
|
||||||
quartX, quartY, quartZ);
|
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,
|
private void warnUnresolvedBiome(String unresolvedKey, String fallback,
|
||||||
@@ -753,11 +1177,29 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
return ((long) x << 32) ^ ((long) z & 4294967295L);
|
return ((long) x << 32) ^ ((long) z & 4294967295L);
|
||||||
}
|
}
|
||||||
|
|
||||||
private record BiomeResolution(IrisBiome irisBiome, boolean underground, int blockX, int blockY,
|
private record BiomeResolution(
|
||||||
int blockZ, RNG rng) {
|
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) {
|
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.NativeStructureStartInjector;
|
||||||
import art.arcane.iris.nativegen.NativeStructureReferenceRepair;
|
import art.arcane.iris.nativegen.NativeStructureReferenceRepair;
|
||||||
import art.arcane.iris.nativegen.NativeStructureVanillaLocator;
|
import art.arcane.iris.nativegen.NativeStructureVanillaLocator;
|
||||||
|
import art.arcane.iris.nativegen.NativeStructureVolumeIndex;
|
||||||
import art.arcane.iris.spi.IrisPlatforms;
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
import art.arcane.iris.spi.PlatformBiome;
|
import art.arcane.iris.spi.PlatformBiome;
|
||||||
import art.arcane.iris.spi.PlatformBlockState;
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
@@ -89,7 +90,9 @@ import java.util.EnumSet;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
@@ -181,11 +184,18 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
}
|
}
|
||||||
requireGlobalStructureGeneration(
|
requireGlobalStructureGeneration(
|
||||||
level.getServer().getWorldGenSettings().options().generateStructures(), dimensionKey);
|
level.getServer().getWorldGenSettings().options().generateStructures(), dimensionKey);
|
||||||
|
Engine activeEngine = engineIfBound();
|
||||||
Engine replacement = ModdedWorldEngines.prepareReplacement(level, pack, packDimensionKey, seed);
|
Engine replacement = ModdedWorldEngines.prepareReplacement(level, pack, packDimensionKey, seed);
|
||||||
try {
|
try {
|
||||||
|
if (activeEngine != null) {
|
||||||
|
requireStructureBiomeUniverseCompatible(
|
||||||
|
activeEngine.getDimension(), replacement.getDimension());
|
||||||
|
}
|
||||||
|
nativeStructures.installVolumeIndex(level, replacement);
|
||||||
ModdedWorldEngines.installReplacement(level, replacement);
|
ModdedWorldEngines.installReplacement(level, replacement);
|
||||||
replacement.getPlatformHooks().applyWorldBoundary(replacement);
|
replacement.getPlatformHooks().applyWorldBoundary(replacement);
|
||||||
} catch (Throwable error) {
|
} catch (Throwable error) {
|
||||||
|
NativeStructureVolumeIndex.uninstall(replacement);
|
||||||
try {
|
try {
|
||||||
ModdedWorldEngines.closeUnregistered(replacement);
|
ModdedWorldEngines.closeUnregistered(replacement);
|
||||||
} catch (Throwable cleanupError) {
|
} catch (Throwable cleanupError) {
|
||||||
@@ -203,17 +213,14 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
this.activePack = pack;
|
this.activePack = pack;
|
||||||
this.activeDimensionKey = packDimensionKey;
|
this.activeDimensionKey = packDimensionKey;
|
||||||
this.seedOverride = seed;
|
this.seedOverride = seed;
|
||||||
this.engine = replacement;
|
|
||||||
this.configuredStructureBiomeKeys = null;
|
this.configuredStructureBiomeKeys = null;
|
||||||
this.configuredPack = null;
|
this.configuredPack = null;
|
||||||
this.heightMetadata = engineHeights(replacement);
|
this.heightMetadata = engineHeights(replacement);
|
||||||
this.engineBinding.reset();
|
this.engineBinding.reset();
|
||||||
this.engineBinding.complete(replacement);
|
|
||||||
this.announced.set(false);
|
this.announced.set(false);
|
||||||
this.structureBiomeSource.clearCaches();
|
resetRuntimeCaches();
|
||||||
this.importedFeatures.invalidate();
|
this.engine = replacement;
|
||||||
this.nativeStructures.clearWorldCheckStructureShifts();
|
this.engineBinding.complete(replacement);
|
||||||
this.spawnTables.resetVanillaSpawnBiomes();
|
|
||||||
// Bind time: a feature-order cycle in the new pack is reported here, once, and degrades to features-off.
|
// 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
|
// Never waits on a build owned by another thread: this method owns the generator monitor and the build
|
||||||
// path can need it.
|
// path can need it.
|
||||||
@@ -751,7 +758,38 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
return lastChunkGenAt;
|
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;
|
configuredStructureBiomeKeys = null;
|
||||||
structureBiomeSource.clearCaches();
|
structureBiomeSource.clearCaches();
|
||||||
importedFeatures.invalidate();
|
importedFeatures.invalidate();
|
||||||
@@ -986,7 +1024,17 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
Registry<Biome> registry = region.registryAccess().lookupOrThrow(Registries.BIOME);
|
Registry<Biome> registry = region.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||||
spawnTables.initializeVanillaSpawnBiomes(registry);
|
spawnTables.initializeVanillaSpawnBiomes(registry);
|
||||||
ChunkPos center = region.getCenter();
|
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());
|
Holder<Biome> vanillaBiome = spawnTables.vanillaSpawnBiome(visibleBiome.value());
|
||||||
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
|
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
|
||||||
random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ());
|
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());
|
boolean ignoreFluid = !type.isOpaque().test(Blocks.WATER.defaultBlockState());
|
||||||
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_base_height");
|
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_base_height");
|
||||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
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) {
|
} catch (GenerationSessionException e) {
|
||||||
throw new IllegalStateException("Iris base height query could not acquire its engine runtime.", 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");
|
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_base_column");
|
||||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||||
BlockState airState = Blocks.AIR.defaultBlockState();
|
BlockState airState = Blocks.AIR.defaultBlockState();
|
||||||
int surface = current.getHeight(x, z, true);
|
boolean dimensionStack = current.getDimensionStackContext() != null;
|
||||||
int fluid = current.getHeight(x, z, false);
|
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 stone = Blocks.STONE.defaultBlockState();
|
||||||
BlockState water = Blocks.WATER.defaultBlockState();
|
BlockState water = Blocks.WATER.defaultBlockState();
|
||||||
int solidEnd = Math.max(0, Math.min(states.length, surface + 1));
|
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.framework.Engine;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
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.PlatformBiome;
|
||||||
import art.arcane.iris.spi.PlatformBiomeWriter;
|
import art.arcane.iris.spi.PlatformBiomeWriter;
|
||||||
import art.arcane.iris.util.project.context.IrisContext;
|
import art.arcane.iris.util.project.context.IrisContext;
|
||||||
@@ -97,7 +98,7 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
|
|||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
Engine engine = context.getEngine();
|
Engine engine = context.getEngine();
|
||||||
String prefix = engine.getDimension().getLoadKey() + ":";
|
String prefix = engine.getDimension().getCustomBiomeKeyPrefix();
|
||||||
if (!key.regionMatches(true, 0, prefix, 0, prefix.length())) {
|
if (!key.regionMatches(true, 0, prefix, 0, prefix.length())) {
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
@@ -138,13 +139,7 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private int idForDerivative(Registry<Biome> registry, String key) {
|
private int idForDerivative(Registry<Biome> registry, String key) {
|
||||||
int colon = key.indexOf(':');
|
IrisBiome owner = findCustomBiomeOwner(key);
|
||||||
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);
|
|
||||||
if (owner == null) {
|
if (owner == null) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -155,20 +150,18 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
|
|||||||
return idForKey(registry, derivativeKey);
|
return idForKey(registry, derivativeKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IrisBiome findCustomBiomeOwner(String dimensionLoadKey, String customBiomeId) {
|
private IrisBiome findCustomBiomeOwner(String key) {
|
||||||
for (Engine engine : ModdedWorldEngines.activeEngines()) {
|
for (Engine engine : ModdedWorldEngines.activeEngines()) {
|
||||||
if (engine == null || engine.isClosed()) {
|
if (engine == null || engine.isClosed()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!dimensionLoadKey.equalsIgnoreCase(engine.getDimension().getLoadKey())) {
|
IrisDimension dimension = engine.getDimension();
|
||||||
continue;
|
for (IrisBiome biome : dimension.getAllBiomes(engine)) {
|
||||||
}
|
|
||||||
for (IrisBiome biome : engine.getDimension().getAllBiomes(engine)) {
|
|
||||||
if (!biome.isCustom()) {
|
if (!biome.isCustom()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (IrisBiomeCustom custom : biome.getCustomDerivitives()) {
|
for (IrisBiomeCustom custom : biome.getCustomDerivitives()) {
|
||||||
if (customBiomeId.equals(custom.getId())) {
|
if (key.equalsIgnoreCase(dimension.getCustomBiomeKey(custom.getId()))) {
|
||||||
return biome;
|
return biome;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -258,7 +258,7 @@ public final class ModdedBlockBreakHandler {
|
|||||||
if (skipsParents(providers)) {
|
if (skipsParents(providers)) {
|
||||||
return providers;
|
return providers;
|
||||||
}
|
}
|
||||||
IrisRegion region = engine.getRegion(position.getX(), position.getZ());
|
IrisRegion region = engine.getRegion(position.getX(), relativeY, position.getZ());
|
||||||
if (region != null) {
|
if (region != null) {
|
||||||
addMatching(providers, region.getBlockDrops(), brokenState, data);
|
addMatching(providers, region.getBlockDrops(), brokenState, data);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -62,7 +62,6 @@ final class ModdedDimensionMetadata {
|
|||||||
|
|
||||||
static Set<String> collectConfiguredBiomeKeys(Iterable<IrisBiome> biomes, String dimensionLoadKey) {
|
static Set<String> collectConfiguredBiomeKeys(Iterable<IrisBiome> biomes, String dimensionLoadKey) {
|
||||||
LinkedHashSet<String> keys = new LinkedHashSet<>();
|
LinkedHashSet<String> keys = new LinkedHashSet<>();
|
||||||
String namespace = dimensionLoadKey.toLowerCase(Locale.ROOT);
|
|
||||||
for (IrisBiome irisBiome : biomes) {
|
for (IrisBiome irisBiome : biomes) {
|
||||||
if (irisBiome == null) {
|
if (irisBiome == null) {
|
||||||
continue;
|
continue;
|
||||||
@@ -75,7 +74,7 @@ final class ModdedDimensionMetadata {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||||
keys.add(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT));
|
keys.add(IrisDimension.customBiomeKey(dimensionLoadKey, customBiome.getId()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Set.copyOf(keys);
|
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 blockY = floor(player.getY()) - getEngine().getWorld().minHeight();
|
||||||
int blockZ = floor(player.getZ());
|
int blockZ = floor(player.getZ());
|
||||||
state.biome(getEngine().getBiome(blockX, blockY, blockZ));
|
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) {
|
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.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HexFormat;
|
import java.util.HexFormat;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
@@ -514,18 +513,13 @@ public final class ModdedForcedDatapack {
|
|||||||
if (!dimensionsDirectory.isDirectory()) {
|
if (!dimensionsDirectory.isDirectory()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
File[] dimensionFiles = dimensionsDirectory.listFiles(
|
IrisData data = IrisData.get(packFolder);
|
||||||
(File file) -> file.isFile() && file.getName().endsWith(".json"));
|
String[] dimensionKeys = data.getDimensionLoader().getPossibleKeys();
|
||||||
if (dimensionFiles == null) {
|
if (dimensionKeys == null || dimensionKeys.length == 0) {
|
||||||
throw new IOException("Iris could not read dimensions for pack '" + packName + "'");
|
|
||||||
}
|
|
||||||
if (dimensionFiles.length == 0) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Arrays.sort(dimensionFiles, Comparator.comparing(File::getName));
|
List<String> sortedDimensionKeys = Stream.of(dimensionKeys).sorted().toList();
|
||||||
IrisData data = IrisData.get(packFolder);
|
for (String dimensionKey : sortedDimensionKeys) {
|
||||||
for (File dimensionFile : dimensionFiles) {
|
|
||||||
String dimensionKey = dimensionFile.getName().substring(0, dimensionFile.getName().length() - ".json".length());
|
|
||||||
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
|
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
|
||||||
if (dimension == null) {
|
if (dimension == null) {
|
||||||
throw new IllegalStateException("Iris pack '" + packName + "' dimension '"
|
throw new IllegalStateException("Iris pack '" + packName + "' dimension '"
|
||||||
|
|||||||
+109
-10
@@ -18,6 +18,8 @@
|
|||||||
|
|
||||||
package art.arcane.iris.modded;
|
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.Engine;
|
||||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||||
import art.arcane.iris.engine.framework.NativeFeatureGenerationPolicy;
|
import art.arcane.iris.engine.framework.NativeFeatureGenerationPolicy;
|
||||||
@@ -57,7 +59,10 @@ import java.util.List;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
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}.
|
* 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) {
|
private FeatureTable buildTable(Engine engine, IrisImportedFeatureControl control, long generation) {
|
||||||
// Registry-ordered biome list. FeatureSorter's cycle detection walks it, so an unordered list makes
|
// 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.
|
// 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()) {
|
if (biomes.isEmpty()) {
|
||||||
ModdedIrisLog.error("Iris importedFeatures is on but {} exposes no biomes; features off",
|
ModdedIrisLog.error("Iris importedFeatures is on but {} exposes no biomes; features off",
|
||||||
dimensionKey(engine));
|
dimensionKey(engine));
|
||||||
@@ -262,7 +274,7 @@ final class ModdedImportedFeatureStage {
|
|||||||
*/
|
*/
|
||||||
private Map<String, Holder<Biome>> customBiomeDerivatives(Engine engine, Map<String, Holder<Biome>> byKey) {
|
private Map<String, Holder<Biome>> customBiomeDerivatives(Engine engine, Map<String, Holder<Biome>> byKey) {
|
||||||
Map<String, Holder<Biome>> derivatives = new HashMap<>();
|
Map<String, Holder<Biome>> derivatives = new HashMap<>();
|
||||||
for (IrisBiome irisBiome : engine.getAllBiomes()) {
|
for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) {
|
||||||
if (!irisBiome.isCustom()) {
|
if (!irisBiome.isCustom()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -322,14 +334,24 @@ final class ModdedImportedFeatureStage {
|
|||||||
List<FeatureSorter.StepFeatureData> steps = table.steps();
|
List<FeatureSorter.StepFeatureData> steps = table.steps();
|
||||||
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
|
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
|
||||||
long decorationSeed = random.setDecorationSeed(level.getSeed(), origin.getX(), origin.getZ());
|
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());
|
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
|
||||||
int staticMinY = engine.getMinHeight();
|
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,
|
level, centerPos,
|
||||||
(x, z) -> level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z),
|
surfaceFirstFreeY,
|
||||||
(x, z) -> level.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z),
|
floorFirstFreeY,
|
||||||
position -> staticObjects.contains(position.getX(), position.getY() - staticMinY, position.getZ()));
|
stackContext != null,
|
||||||
|
importedFeatureProtection(staticObjects, stackContext, staticMinY));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (int stepIndex = 0; stepIndex < steps.size(); stepIndex++) {
|
for (int stepIndex = 0; stepIndex < steps.size(); stepIndex++) {
|
||||||
@@ -349,6 +371,58 @@ final class ModdedImportedFeatureStage {
|
|||||||
WorldCheckFeaturePlacement.recordPlacementPass();
|
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,
|
private void placeStep(WorldGenLevel level, FeatureTable table, FeatureSorter.StepFeatureData stepData,
|
||||||
Registry<PlacedFeature> featureRegistry, Set<Holder<Biome>> chunkBiomes,
|
Registry<PlacedFeature> featureRegistry, Set<Holder<Biome>> chunkBiomes,
|
||||||
IrisModdedChunkGenerator owner, WorldgenRandom random, long decorationSeed,
|
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
|
* 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.
|
* 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<>();
|
List<Holder<Biome>> collected = new ArrayList<>();
|
||||||
ChunkPos.rangeClosed(sectionPos.chunk(), 1).forEach((ChunkPos chunkPos) -> {
|
ChunkPos.rangeClosed(sectionPos.chunk(), 1).forEach((ChunkPos chunkPos) -> {
|
||||||
ChunkAccess neighbour = level.getChunk(chunkPos.x(), chunkPos.z());
|
ChunkAccess neighbour = level.getChunk(chunkPos.x(), chunkPos.z());
|
||||||
for (LevelChunkSection section : neighbour.getSections()) {
|
LevelChunkSection[] sections = neighbour.getSections();
|
||||||
section.getBiomes().getAll(collected::add);
|
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<>();
|
Set<Holder<Biome>> present = new LinkedHashSet<>();
|
||||||
|
|||||||
+48
-9
@@ -18,6 +18,8 @@
|
|||||||
|
|
||||||
package art.arcane.iris.modded;
|
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.Engine;
|
||||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||||
@@ -78,6 +80,7 @@ import java.util.Map;
|
|||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.IntBinaryOperator;
|
import java.util.function.IntBinaryOperator;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Native (vanilla registry) structure stage for {@link IrisModdedChunkGenerator}. The generator keeps the
|
* Native (vanilla registry) structure stage for {@link IrisModdedChunkGenerator}. The generator keeps the
|
||||||
@@ -266,7 +269,7 @@ final class ModdedNativeStructureStage {
|
|||||||
undergroundStep,
|
undergroundStep,
|
||||||
decision.preserveSourceY(),
|
decision.preserveSourceY(),
|
||||||
decision.yBand(),
|
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(
|
StructureStart wrapped = NativeStructureReferenceEnvelope.wrapForPublication(
|
||||||
start, structure, start.getReferences(),
|
start, structure, start.getReferences(),
|
||||||
NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain()),
|
NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain()),
|
||||||
@@ -367,10 +370,14 @@ final class ModdedNativeStructureStage {
|
|||||||
chunkPos.x(), chunkPos.z(), error);
|
chunkPos.x(), chunkPos.z(), error);
|
||||||
}
|
}
|
||||||
IrisStaticObjectLayer staticObjects = current.getDimension().getStaticObjectLayer(current.getData());
|
IrisStaticObjectLayer staticObjects = current.getDimension().getStaticObjectLayer(current.getData());
|
||||||
int staticMinY = current.getMinHeight();
|
Predicate<BlockPos> protectedPosition = nativeStructureProtection(current, staticObjects);
|
||||||
WorldGenLevel boundedWorld = staticObjects.isEmpty() ? world : ModdedNativeStructureWorldgenAccess.create(
|
WorldGenLevel boundedWorld = staticObjects.isEmpty()
|
||||||
|
&& current.getDimensionStackContext() == null
|
||||||
|
? world
|
||||||
|
: ModdedNativeStructureWorldgenAccess.create(
|
||||||
world, chunkPos, worldgenSurfaceHeight(current, world.getMinY()), worldgenFloorHeight(current, world.getMinY()),
|
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 {
|
try {
|
||||||
NativeStructureVegetationClearer.clearIntersectingVegetation(
|
NativeStructureVegetationClearer.clearIntersectingVegetation(
|
||||||
boundedWorld, chunk, area, vegetationTargets);
|
boundedWorld, chunk, area, vegetationTargets);
|
||||||
@@ -383,7 +390,7 @@ final class ModdedNativeStructureStage {
|
|||||||
try {
|
try {
|
||||||
vacuumFoundationPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
|
vacuumFoundationPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
|
||||||
boundedWorld, area, terrainTargets,
|
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) {
|
} catch (Throwable error) {
|
||||||
throw NativeStructureGenerationException.failure(
|
throw NativeStructureGenerationException.failure(
|
||||||
"terrain integration", nativeStructureBatchContext(placementGroups),
|
"terrain integration", nativeStructureBatchContext(placementGroups),
|
||||||
@@ -443,18 +450,50 @@ final class ModdedNativeStructureStage {
|
|||||||
IrisNativeStructureDecision decision) {
|
IrisNativeStructureDecision decision) {
|
||||||
Engine current = generator.engine();
|
Engine current = generator.engine();
|
||||||
WorldGenLevel boundedWorld = world instanceof ModdedNativeStructureWorldgenAccess ? world : ModdedNativeStructureWorldgenAccess.create(
|
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);
|
world.setCurrentlyGenerating(() -> "Iris native structure " + structureId);
|
||||||
try {
|
try {
|
||||||
NativeStructurePostProcessor.place(
|
NativeStructurePostProcessor.place(
|
||||||
boundedWorld, structureManager, generator, random, area, chunkPos,
|
boundedWorld, structureManager, generator, random, area, chunkPos,
|
||||||
structureId, start, decision, this::resolvePaletteBlock,
|
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 {
|
} finally {
|
||||||
world.setCurrentlyGenerating(null);
|
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) {
|
private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
|
||||||
StructureStepCache cached = structureStepCache;
|
StructureStepCache cached = structureStepCache;
|
||||||
if (cached != null && cached.registry() == registry) {
|
if (cached != null && cached.registry() == registry) {
|
||||||
@@ -523,11 +562,11 @@ final class ModdedNativeStructureStage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private IntBinaryOperator worldgenSurfaceHeight(Engine generationEngine, int runtimeMinY) {
|
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) {
|
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) {
|
private record NativeStructureStartKey(String structureId, long chunkPosition) {
|
||||||
|
|||||||
+6
-2
@@ -63,6 +63,7 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
|
|||||||
private final ChunkPos generationCenter;
|
private final ChunkPos generationCenter;
|
||||||
private final IntBinaryOperator surfaceFirstFreeY;
|
private final IntBinaryOperator surfaceFirstFreeY;
|
||||||
private final IntBinaryOperator floorFirstFreeY;
|
private final IntBinaryOperator floorFirstFreeY;
|
||||||
|
private final boolean virtualizeHeights;
|
||||||
private final Predicate<BlockPos> protectedPosition;
|
private final Predicate<BlockPos> protectedPosition;
|
||||||
private final Holder<Biome> fallbackBiome;
|
private final Holder<Biome> fallbackBiome;
|
||||||
private final BiomeManager biomeManager;
|
private final BiomeManager biomeManager;
|
||||||
@@ -80,6 +81,7 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
|
|||||||
boundary.surfaceFirstFreeY(), "Native structure world access requires a surface resolver");
|
boundary.surfaceFirstFreeY(), "Native structure world access requires a surface resolver");
|
||||||
this.floorFirstFreeY = Objects.requireNonNull(
|
this.floorFirstFreeY = Objects.requireNonNull(
|
||||||
boundary.floorFirstFreeY(), "Native structure world access requires an ocean-floor resolver");
|
boundary.floorFirstFreeY(), "Native structure world access requires an ocean-floor resolver");
|
||||||
|
this.virtualizeHeights = boundary.virtualizeHeights();
|
||||||
this.protectedPosition = Objects.requireNonNull(
|
this.protectedPosition = Objects.requireNonNull(
|
||||||
boundary.protectedPosition(), "World access requires a protected-position predicate");
|
boundary.protectedPosition(), "World access requires a protected-position predicate");
|
||||||
BlockPos biomeSample = generationCenter.getMiddleBlockPosition(delegate.getSeaLevel());
|
BlockPos biomeSample = generationCenter.getMiddleBlockPosition(delegate.getSeaLevel());
|
||||||
@@ -102,9 +104,10 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
|
|||||||
static ModdedNativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter,
|
static ModdedNativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter,
|
||||||
IntBinaryOperator surfaceFirstFreeY,
|
IntBinaryOperator surfaceFirstFreeY,
|
||||||
IntBinaryOperator floorFirstFreeY,
|
IntBinaryOperator floorFirstFreeY,
|
||||||
|
boolean virtualizeHeights,
|
||||||
Predicate<BlockPos> protectedPosition) {
|
Predicate<BlockPos> protectedPosition) {
|
||||||
return new ModdedNativeStructureWorldgenAccess(delegate, new Boundary(
|
return new ModdedNativeStructureWorldgenAccess(delegate, new Boundary(
|
||||||
generationCenter, surfaceFirstFreeY, floorFirstFreeY, protectedPosition));
|
generationCenter, surfaceFirstFreeY, floorFirstFreeY, virtualizeHeights, protectedPosition));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -241,7 +244,7 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getHeight(Heightmap.Types type, int x, int z) {
|
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 delegate.getHeight(type, x, z);
|
||||||
}
|
}
|
||||||
return height(type, x, z);
|
return height(type, x, z);
|
||||||
@@ -526,6 +529,7 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
|
|||||||
private record Boundary(ChunkPos generationCenter,
|
private record Boundary(ChunkPos generationCenter,
|
||||||
IntBinaryOperator surfaceFirstFreeY,
|
IntBinaryOperator surfaceFirstFreeY,
|
||||||
IntBinaryOperator floorFirstFreeY,
|
IntBinaryOperator floorFirstFreeY,
|
||||||
|
boolean virtualizeHeights,
|
||||||
Predicate<BlockPos> protectedPosition) {
|
Predicate<BlockPos> protectedPosition) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -108,12 +108,17 @@ public final class ModdedScheduler implements PlatformScheduler {
|
|||||||
scheduler.drain();
|
scheduler.drain();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isMainThread() {
|
||||||
|
Thread running = mainThread;
|
||||||
|
return running != null && Thread.currentThread() == running;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void global(Runnable task) {
|
public void global(Runnable task) {
|
||||||
if (task == null) {
|
if (task == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (onMainThread()) {
|
if (isMainThread()) {
|
||||||
runGuarded(task);
|
runGuarded(task);
|
||||||
return;
|
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) {
|
private void runGuarded(Runnable task) {
|
||||||
try {
|
try {
|
||||||
task.run();
|
task.run();
|
||||||
|
|||||||
+75
-34
@@ -18,10 +18,14 @@
|
|||||||
|
|
||||||
package art.arcane.iris.modded;
|
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.Engine;
|
||||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
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.iris.util.project.context.IrisContext;
|
||||||
import net.minecraft.core.Holder;
|
import net.minecraft.core.Holder;
|
||||||
import net.minecraft.core.Registry;
|
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.Biome;
|
||||||
import net.minecraft.world.level.biome.MobSpawnSettings;
|
import net.minecraft.world.level.biome.MobSpawnSettings;
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
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 {
|
final class ModdedSpawnTableMerger {
|
||||||
private final IrisModdedChunkGenerator generator;
|
private final IrisModdedChunkGenerator generator;
|
||||||
private final ConcurrentHashMap<Biome, Holder<Biome>> vanillaSpawnBiomes = new ConcurrentHashMap<>();
|
private volatile SpawnTableState state = new SpawnTableState();
|
||||||
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
|
|
||||||
private volatile boolean vanillaSpawnBiomesInitialized;
|
|
||||||
|
|
||||||
ModdedSpawnTableMerger(IrisModdedChunkGenerator generator) {
|
ModdedSpawnTableMerger(IrisModdedChunkGenerator generator) {
|
||||||
this.generator = generator;
|
this.generator = generator;
|
||||||
}
|
}
|
||||||
|
|
||||||
void initializeVanillaSpawnBiomes(Registry<Biome> registry) {
|
void initializeVanillaSpawnBiomes(Registry<Biome> registry) {
|
||||||
// Volatile fast path BEFORE the monitor: this runs per mob category per spawn attempt
|
SpawnTableState active = state;
|
||||||
// on the server thread, and the generator monitor is contended by binds/hotloads.
|
if (active.initialized) {
|
||||||
if (vanillaSpawnBiomesInitialized) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
synchronized (generator) {
|
synchronized (active) {
|
||||||
if (vanillaSpawnBiomesInitialized) {
|
if (active != state || active.initialized) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Engine current = generator.engineOrNull();
|
Engine current = generator.engineOrNull();
|
||||||
@@ -66,29 +62,70 @@ final class ModdedSpawnTableMerger {
|
|||||||
|
|
||||||
try (GenerationSessionLease lease = generator.requireGenerationLease(current, "modded_spawn_biomes");
|
try (GenerationSessionLease lease = generator.requireGenerationLease(current, "modded_spawn_biomes");
|
||||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||||
String namespace = current.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
|
IrisDimension hostDimension = current.getDimension();
|
||||||
for (IrisBiome irisBiome : current.getDimension().getReachableBiomes(current)) {
|
addVanillaSpawnBiomes(
|
||||||
if (irisBiome == null || !irisBiome.isCustom()) {
|
registry,
|
||||||
continue;
|
hostDimension,
|
||||||
}
|
current.getData(),
|
||||||
Holder<Biome> vanillaHolder = resolveBiomeHolder(registry, irisBiome.getVanillaDerivativeKey());
|
hostDimension.getReachableBiomes(current),
|
||||||
if (vanillaHolder == null) {
|
active
|
||||||
continue;
|
);
|
||||||
}
|
DimensionStackContext stackContext = current.getDimensionStackContext();
|
||||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
if (stackContext != null) {
|
||||||
Holder<Biome> customHolder = resolveBiomeHolder(registry, namespace + ":" + customBiome.getId());
|
for (DimensionTerrainContext terrainContext : stackContext.getLayersBottomToTop()) {
|
||||||
if (customHolder != null) {
|
if (terrainContext.isSelfReferencing()) {
|
||||||
vanillaSpawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
|
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) {
|
Holder<Biome> vanillaSpawnBiome(Biome biome) {
|
||||||
return vanillaSpawnBiomes.get(biome);
|
return state.vanillaSpawnBiomes.get(biome);
|
||||||
}
|
}
|
||||||
|
|
||||||
WeightedList<MobSpawnSettings.SpawnerData> mergedSpawnTable(
|
WeightedList<MobSpawnSettings.SpawnerData> mergedSpawnTable(
|
||||||
@@ -96,7 +133,8 @@ final class ModdedSpawnTableMerger {
|
|||||||
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns,
|
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns,
|
||||||
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns) {
|
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns) {
|
||||||
SpawnTableKey key = new SpawnTableKey(biome, category);
|
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) {
|
private Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String key) {
|
||||||
@@ -112,13 +150,16 @@ final class ModdedSpawnTableMerger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void resetVanillaSpawnBiomes() {
|
void resetVanillaSpawnBiomes() {
|
||||||
synchronized (generator) {
|
state = new SpawnTableState();
|
||||||
vanillaSpawnBiomes.clear();
|
|
||||||
mergedSpawnTables.clear();
|
|
||||||
vanillaSpawnBiomesInitialized = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private record SpawnTableKey(Biome biome, MobCategory category) {
|
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();
|
BlockPos pos = player.blockPosition();
|
||||||
try {
|
try {
|
||||||
region = engine.getRegion(pos.getX(), pos.getZ());
|
region = engine.getRegion(
|
||||||
|
pos.getX(), pos.getY() - engine.getMinHeight(), pos.getZ());
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_REGION_LOOKUP_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName())));
|
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_REGION_LOOKUP_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName())));
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
+52
-14
@@ -73,6 +73,8 @@ import org.zeroturnaround.zip.ZipUtil;
|
|||||||
import java.awt.Desktop;
|
import java.awt.Desktop;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -85,6 +87,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import art.arcane.iris.core.localization.IrisLanguage;
|
import art.arcane.iris.core.localization.IrisLanguage;
|
||||||
import art.arcane.iris.core.localization.ModdedCommandMessages;
|
import art.arcane.iris.core.localization.ModdedCommandMessages;
|
||||||
@@ -891,21 +894,30 @@ public final class ModdedStudioCommands {
|
|||||||
LinkedHashSet<String> objectKeys = new LinkedHashSet<>();
|
LinkedHashSet<String> objectKeys = new LinkedHashSet<>();
|
||||||
LinkedHashSet<String> markerKeys = new LinkedHashSet<>();
|
LinkedHashSet<String> markerKeys = new LinkedHashSet<>();
|
||||||
LinkedHashSet<String> structureKeys = new LinkedHashSet<>();
|
LinkedHashSet<String> structureKeys = new LinkedHashSet<>();
|
||||||
|
List<IrisDimension> dimensions = new ArrayList<>();
|
||||||
|
|
||||||
dimension.getAllRegions(() -> dm).forEach((IrisRegion region) -> {
|
for (String dimensionKey : PackExportClosure.collectDimensionKeys(dimension).stream().sorted().toList()) {
|
||||||
if (region != null && region.getLoadKey() != null) {
|
IrisDimension exportedDimension = dm.getDimensionLoader().load(dimensionKey);
|
||||||
regionKeys.add(region.getLoadKey());
|
if (exportedDimension != null) {
|
||||||
|
dimensions.add(exportedDimension);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
dimension.getReachableBiomes(() -> dm).forEach((IrisBiome biome) -> {
|
for (IrisDimension exportedDimension : dimensions) {
|
||||||
if (biome != null && biome.getLoadKey() != null) {
|
exportedDimension.getAllRegions(() -> dm).forEach((IrisRegion region) -> {
|
||||||
biomeKeys.add(biome.getLoadKey());
|
if (region != null && region.getLoadKey() != null) {
|
||||||
}
|
regionKeys.add(region.getLoadKey());
|
||||||
});
|
}
|
||||||
lootKeys.addAll(dimension.getLoot().getTables());
|
});
|
||||||
spawnerKeys.addAll(dimension.getEntitySpawners());
|
exportedDimension.getReachableBiomes(() -> dm).forEach((IrisBiome biome) -> {
|
||||||
collectStructureKeys(structureKeys, dimension.getStructures());
|
if (biome != null && biome.getLoadKey() != null) {
|
||||||
objectKeys.addAll(PackExportClosure.collectStaticObjectKeys(dimension.getStaticObjects()));
|
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) {
|
for (String regionKey : regionKeys) {
|
||||||
IrisRegion region = dm.getRegionLoader().load(regionKey);
|
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) {
|
for (String key : generatorKeys) {
|
||||||
hashes.append(copyJson(folder, "generators", key, dm.getGeneratorLoader().findFile(key)));
|
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) {
|
for (String key : regionKeys) {
|
||||||
hashes.append(copyJson(folder, "regions", key, dm.getRegionLoader().findFile(key)));
|
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) {
|
private static int regions(CommandSourceStack source, int radius) {
|
||||||
ServerPlayer player = source.getPlayer();
|
ServerPlayer player = source.getPlayer();
|
||||||
if (player == null) {
|
if (player == null) {
|
||||||
|
|||||||
+2
-1
@@ -216,7 +216,8 @@ public final class ModdedWhatCommands {
|
|||||||
int centerX = (pos.getX() & ~15) + 8;
|
int centerX = (pos.getX() & ~15) + 8;
|
||||||
int centerZ = (pos.getZ() & ~15) + 8;
|
int centerZ = (pos.getZ() & ~15) + 8;
|
||||||
try {
|
try {
|
||||||
IrisRegion region = engine.getRegion(centerX, centerZ);
|
IrisRegion region = engine.getRegion(
|
||||||
|
centerX, pos.getY() - engine.getMinHeight(), centerZ);
|
||||||
IrisModdedCommands.ok(source, IrisLanguage.plain(
|
IrisModdedCommands.ok(source, IrisLanguage.plain(
|
||||||
RuntimeUiMessages.WHAT_IRIS_REGION,
|
RuntimeUiMessages.WHAT_IRIS_REGION,
|
||||||
MessageArgument.untrusted("region", region.getLoadKey()),
|
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.IrisModdedChunkGenerator;
|
||||||
import art.arcane.iris.modded.ModdedDimensionManager;
|
import art.arcane.iris.modded.ModdedDimensionManager;
|
||||||
import art.arcane.iris.modded.ModdedForcedDatapack;
|
import art.arcane.iris.modded.ModdedForcedDatapack;
|
||||||
|
import art.arcane.iris.modded.ModdedScheduler;
|
||||||
import art.arcane.iris.modded.ModdedWorkspaceGenerator;
|
import art.arcane.iris.modded.ModdedWorkspaceGenerator;
|
||||||
import art.arcane.iris.nativegen.NativeStructureVolumeIndex;
|
import art.arcane.iris.nativegen.NativeStructureVolumeIndex;
|
||||||
import art.arcane.iris.modded.command.ModdedPregenJob;
|
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);
|
return NativeStructureVolumeIndex.volumes(engine, minX, minZ, maxX, maxZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isMainThread() {
|
||||||
|
return ModdedScheduler.isMainThread();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void refreshDatapackWorkspace(Engine engine) {
|
public void refreshDatapackWorkspace(Engine engine) {
|
||||||
IrisData data = engine.getData();
|
IrisData data = engine.getData();
|
||||||
@@ -121,6 +127,20 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
|||||||
engine.getDimension(),
|
engine.getDimension(),
|
||||||
replacement,
|
replacement,
|
||||||
"irisworldgen");
|
"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
|
@Override
|
||||||
@@ -285,7 +305,6 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
|||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
try {
|
try {
|
||||||
engine.hotloadSilently();
|
engine.hotloadSilently();
|
||||||
generator.onHotload();
|
|
||||||
ModdedIrisLog.info("Iris studio hotload {} pack={} {}ms", dimensionId, engine.getDimension().getLoadKey(), System.currentTimeMillis() - start);
|
ModdedIrisLog.info("Iris studio hotload {} pack={} {}ms", dimensionId, engine.getDimension().getLoadKey(), System.currentTimeMillis() - start);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
ModdedIrisLog.error("Iris studio hotload failed for {}", dimensionId, e);
|
ModdedIrisLog.error("Iris studio hotload failed for {}", dimensionId, e);
|
||||||
|
|||||||
+1
@@ -59,6 +59,7 @@ public class IrisModdedChunkGeneratorSpawnTest {
|
|||||||
String spawn = source.substring(spawnStart, spawnEnd);
|
String spawn = source.substring(spawnStart, spawnEnd);
|
||||||
|
|
||||||
assertTrue(spawn.contains("spawnTables.initializeVanillaSpawnBiomes(registry)"));
|
assertTrue(spawn.contains("spawnTables.initializeVanillaSpawnBiomes(registry)"));
|
||||||
|
assertTrue(spawn.contains("structureBiomeSource.getVisibleSurfaceBiome("));
|
||||||
assertTrue(spawn.contains("spawnTables.vanillaSpawnBiome(visibleBiome.value())"));
|
assertTrue(spawn.contains("spawnTables.vanillaSpawnBiome(visibleBiome.value())"));
|
||||||
assertTrue(spawn.contains("NaturalSpawner.spawnMobsForChunkGeneration("));
|
assertTrue(spawn.contains("NaturalSpawner.spawnMobsForChunkGeneration("));
|
||||||
assertTrue(spawn.contains("new LegacyRandomSource(RandomSupport.generateUniqueSeed())"));
|
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.IrisDimension;
|
||||||
import art.arcane.iris.engine.object.IrisRange;
|
import art.arcane.iris.engine.object.IrisRange;
|
||||||
import art.arcane.iris.engine.object.InferredType;
|
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.collection.KList;
|
||||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||||
import net.minecraft.SharedConstants;
|
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.assertSame;
|
||||||
import static org.junit.Assert.assertThrows;
|
import static org.junit.Assert.assertThrows;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class IrisModdedStructureParityTest {
|
public class IrisModdedStructureParityTest {
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
@@ -110,6 +113,144 @@ public class IrisModdedStructureParityTest {
|
|||||||
assertEquals(expected, IrisModdedBiomeSource.biomeResolutionSeed(worldSeed, blockX, blockY, blockZ));
|
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
|
@Test
|
||||||
public void configuredBiomeKeysContainOnlyPackDerivativesAndCustomBiomes() {
|
public void configuredBiomeKeysContainOnlyPackDerivativesAndCustomBiomes() {
|
||||||
IrisBiome ocean = new IrisBiome()
|
IrisBiome ocean = new IrisBiome()
|
||||||
@@ -132,6 +273,21 @@ public class IrisModdedStructureParityTest {
|
|||||||
"minecraft:the_void", "overworld:aurora"), keys);
|
"minecraft:the_void", "overworld:aurora"), keys);
|
||||||
assertFalse(keys.contains("minecraft:desert"));
|
assertFalse(keys.contains("minecraft:desert"));
|
||||||
assertFalse(keys.contains("minecraft:plains"));
|
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
|
@Test
|
||||||
@@ -159,6 +315,19 @@ public class IrisModdedStructureParityTest {
|
|||||||
assertSame(configured, IrisModdedBiomeSource.requireConfiguredStructureBiomeKeys(configured));
|
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
|
@Test
|
||||||
public void configuredDimensionMetadataIsExactBeforeEngineBinding() {
|
public void configuredDimensionMetadataIsExactBeforeEngineBinding() {
|
||||||
IrisDimension dimension = new IrisDimension()
|
IrisDimension dimension = new IrisDimension()
|
||||||
@@ -240,4 +409,20 @@ public class IrisModdedStructureParityTest {
|
|||||||
assertSame(MantleFlag.INITIAL_SPAWNED_MARKER, ModdedWorldManager.INITIAL_SPAWN_COMPLETION_FLAG);
|
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());
|
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
|
@Test
|
||||||
public void protectedCellsRemainReadableButRejectWritesTilesAndTicks() {
|
public void protectedCellsRemainReadableButRejectWritesTilesAndTicks() {
|
||||||
RecordingDelegate recording = new RecordingDelegate();
|
RecordingDelegate recording = new RecordingDelegate();
|
||||||
@@ -142,7 +160,9 @@ public class ModdedNativeStructureWorldgenAccessTest {
|
|||||||
ModdedNativeStructureWorldgenAccess access = ModdedNativeStructureWorldgenAccess.create(
|
ModdedNativeStructureWorldgenAccess access = ModdedNativeStructureWorldgenAccess.create(
|
||||||
recording.world(), generationCenter(),
|
recording.world(), generationCenter(),
|
||||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
(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());
|
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
|
||||||
assertFalse(access.ensureCanWrite(position));
|
assertFalse(access.ensureCanWrite(position));
|
||||||
@@ -211,7 +231,9 @@ public class ModdedNativeStructureWorldgenAccessTest {
|
|||||||
return ModdedNativeStructureWorldgenAccess.create(
|
return ModdedNativeStructureWorldgenAccess.create(
|
||||||
recording.world(), generationCenter(),
|
recording.world(), generationCenter(),
|
||||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
(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() {
|
private static ChunkPos generationCenter() {
|
||||||
|
|||||||
+5
-2
@@ -95,8 +95,9 @@ public class NativeStructureFailureContractTest {
|
|||||||
assertTrue(placement.contains("heightmapStarts.add(start);"));
|
assertTrue(placement.contains("heightmapStarts.add(start);"));
|
||||||
assertTrue(placement.indexOf("WorldgenTerrainHeightmaps.primeStructurePlacement(")
|
assertTrue(placement.indexOf("WorldgenTerrainHeightmaps.primeStructurePlacement(")
|
||||||
< placement.indexOf("prepareSurfaceStructures"));
|
< placement.indexOf("prepareSurfaceStructures"));
|
||||||
assertTrue(source.contains("generationEngine.getHeight(x, z, false) + runtimeMinY + 1"));
|
assertTrue(source.contains("Engine.hostHeight(generationEngine, x, z, false) + runtimeMinY + 1"));
|
||||||
assertTrue(source.contains("generationEngine.getHeight(x, z, true) + 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;"));
|
assertTrue(source.contains("int minY = chunk.getMinY() + 1;"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +109,8 @@ public class NativeStructureFailureContractTest {
|
|||||||
String placement = source.substring(placementStart, placementEnd);
|
String placement = source.substring(placementStart, placementEnd);
|
||||||
|
|
||||||
assertTrue(placement.contains("ModdedNativeStructureWorldgenAccess.create("));
|
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"));
|
assertTrue(placement.contains("placeVanillaStructure(boundedWorld, structureManager"));
|
||||||
assertFalse(placement.contains("NativeStructurePostProcessor.place(world,"));
|
assertFalse(placement.contains("NativeStructurePostProcessor.place(world,"));
|
||||||
assertTrue(placement.contains("world.setCurrentlyGenerating(null);"));
|
assertTrue(placement.contains("world.setCurrentlyGenerating(null);"));
|
||||||
|
|||||||
+19
@@ -1,10 +1,12 @@
|
|||||||
package art.arcane.iris.modded.service;
|
package art.arcane.iris.modded.service;
|
||||||
|
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
|
import art.arcane.iris.modded.ModdedScheduler;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
@@ -26,4 +28,21 @@ public class ModdedStudioHotloadServiceTest {
|
|||||||
assertFalse(ModdedStudioHotloadService.hasDatapackImports(null));
|
assertFalse(ModdedStudioHotloadService.hasDatapackImports(null));
|
||||||
assertFalse(ModdedStudioHotloadService.hasDatapackImports(List.of(empty)));
|
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<>(),
|
new LinkedHashSet<>(),
|
||||||
requiredDimension,
|
requiredDimension,
|
||||||
requiredFixer);
|
requiredFixer);
|
||||||
|
collectDimensionStackRegistryRequirements(
|
||||||
|
requirements,
|
||||||
|
requiredDimension,
|
||||||
|
requiredFixer);
|
||||||
return Map.copyOf(requirements);
|
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(
|
private static void collectRegistryRequirements(
|
||||||
Map<String, String> requirements,
|
Map<String, String> requirements,
|
||||||
Set<String> installedBiomeKeys,
|
Set<String> installedBiomeKeys,
|
||||||
@@ -235,7 +271,6 @@ public final class IrisDatapackCompiler {
|
|||||||
"dimension_type/iris:" + dimension.getDimensionTypeKey(),
|
"dimension_type/iris:" + dimension.getDimensionTypeKey(),
|
||||||
fingerprintContent(dimension.getDimensionType().toJson(fixer)));
|
fingerprintContent(dimension.getDimensionType().toJson(fixer)));
|
||||||
|
|
||||||
String namespace = dimension.getLoadKey().toLowerCase(Locale.ROOT);
|
|
||||||
ContentGate contentGate = dimension.getLoader() == null ? null : dimension.getLoader().getContentGate();
|
ContentGate contentGate = dimension.getLoader() == null ? null : dimension.getLoader().getContentGate();
|
||||||
for (IrisBiome biome : dimension.getAllBiomes(dimension::getLoader)) {
|
for (IrisBiome biome : dimension.getAllBiomes(dimension::getLoader)) {
|
||||||
if (biome == null || !biome.isCustom()) {
|
if (biome == null || !biome.isCustom()) {
|
||||||
@@ -246,7 +281,7 @@ public final class IrisDatapackCompiler {
|
|||||||
if (customBiome == null) {
|
if (customBiome == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
String biomeKey = namespace + ":" + customBiome.getId();
|
String biomeKey = dimension.getCustomBiomeKey(customBiome.getId());
|
||||||
if (!installedBiomeKeys.add(biomeKey)) {
|
if (!installedBiomeKeys.add(biomeKey)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -533,7 +568,7 @@ public final class IrisDatapackCompiler {
|
|||||||
|| !Files.isDirectory(dimensions, LinkOption.NOFOLLOW_LINKS)) {
|
|| !Files.isDirectory(dimensions, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
try (Stream<Path> stream = Files.list(dimensions)) {
|
try (Stream<Path> stream = Files.walk(dimensions)) {
|
||||||
return stream.anyMatch(path -> !Files.isSymbolicLink(path)
|
return stream.anyMatch(path -> !Files.isSymbolicLink(path)
|
||||||
&& Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)
|
&& Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)
|
||||||
&& path.getFileName().toString().endsWith(".json"));
|
&& path.getFileName().toString().endsWith(".json"));
|
||||||
|
|||||||
@@ -1121,7 +1121,7 @@ public class ServerConfigurator {
|
|||||||
for (IrisBiome i : dimension.getAllBiomes(dimension::getLoader)) {
|
for (IrisBiome i : dimension.getAllBiomes(dimension::getLoader)) {
|
||||||
if (i.isCustom()) {
|
if (i.isCustom()) {
|
||||||
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
|
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;
|
if (access == null) return false;
|
||||||
Engine engine = access.getEngine();
|
Engine engine = access.getEngine();
|
||||||
if (engine == null) return false;
|
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());
|
return regions.contains(region.getLoadKey());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,6 +156,9 @@ public interface INMSBinding {
|
|||||||
return NativeStructureVolume.NONE;
|
return NativeStructureVolume.NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default void invalidateNativeStructureVolumeIndex(Engine engine) {
|
||||||
|
}
|
||||||
|
|
||||||
int getBiomeId(Biome biome);
|
int getBiomeId(Biome biome);
|
||||||
|
|
||||||
MCABiomeContainer newBiomeContainer(int min, int max, int[] data);
|
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.io.File;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
final class PackDimensionValidator {
|
final class PackDimensionValidator {
|
||||||
private PackDimensionValidator() {
|
private PackDimensionValidator() {
|
||||||
@@ -35,9 +37,14 @@ final class PackDimensionValidator {
|
|||||||
static void validateDimensions(File packFolder, File[] dimensionFiles, List<String> blockingErrors, List<String> warnings) {
|
static void validateDimensions(File packFolder, File[] dimensionFiles, List<String> blockingErrors, List<String> warnings) {
|
||||||
File regionsFolder = new File(packFolder, "regions");
|
File regionsFolder = new File(packFolder, "regions");
|
||||||
File biomesFolder = new File(packFolder, "biomes");
|
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) {
|
for (File dimFile : dimensionFiles) {
|
||||||
String dimensionKey = PackValidationIo.stripExtension(dimFile.getName());
|
String dimensionKey = PackValidationIo.deriveKey(dimensionsFolder, dimFile);
|
||||||
JSONObject dimJson;
|
JSONObject dimJson;
|
||||||
try {
|
try {
|
||||||
dimJson = new JSONObject(Files.readString(dimFile.toPath(), StandardCharsets.UTF_8));
|
dimJson = new JSONObject(Files.readString(dimFile.toPath(), StandardCharsets.UTF_8));
|
||||||
@@ -50,6 +57,7 @@ final class PackDimensionValidator {
|
|||||||
validateDimensionHeights(packFolder, dimensionKey, dimJson, blockingErrors);
|
validateDimensionHeights(packFolder, dimensionKey, dimJson, blockingErrors);
|
||||||
validateWorldBoundary(dimensionKey, dimJson, blockingErrors);
|
validateWorldBoundary(dimensionKey, dimJson, blockingErrors);
|
||||||
validateStaticObjects(packFolder, dimensionKey, dimJson, blockingErrors);
|
validateStaticObjects(packFolder, dimensionKey, dimJson, blockingErrors);
|
||||||
|
validateDimensionStack(packFolder, dimensionKey, dimJson, dimensionKeys, blockingErrors);
|
||||||
|
|
||||||
JSONArray regionsArray = dimJson.optJSONArray("regions");
|
JSONArray regionsArray = dimJson.optJSONArray("regions");
|
||||||
if (regionsArray == null || regionsArray.length() == 0) {
|
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,
|
static void validateStaticObjects(File packFolder, String dimensionKey, JSONObject dimension,
|
||||||
List<String> blockingErrors) {
|
List<String> blockingErrors) {
|
||||||
if (!dimension.has("staticObjects")) {
|
if (!dimension.has("staticObjects")) {
|
||||||
|
|||||||
@@ -18,11 +18,16 @@
|
|||||||
|
|
||||||
package art.arcane.iris.core.pack;
|
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.IrisObjectMarker;
|
||||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||||
import art.arcane.iris.engine.object.IrisStaticObject;
|
import art.arcane.iris.engine.object.IrisStaticObject;
|
||||||
import art.arcane.volmlib.util.collection.KSet;
|
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
|
* 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
|
* modded verbatim-copy compiler must export the complete ambient-spawning graph: object placements
|
||||||
@@ -33,6 +38,48 @@ public final class PackExportClosure {
|
|||||||
private 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) {
|
public static KSet<String> collectMarkerKeys(Iterable<IrisObjectPlacement> placements) {
|
||||||
KSet<String> markerKeys = new KSet<>();
|
KSet<String> markerKeys = new KSet<>();
|
||||||
if (placements == null) {
|
if (placements == null) {
|
||||||
|
|||||||
@@ -80,12 +80,13 @@ final class PackHydrologyValidator {
|
|||||||
|
|
||||||
List<File> sortedDimensions = new ArrayList<>(List.of(dimensionFiles));
|
List<File> sortedDimensions = new ArrayList<>(List.of(dimensionFiles));
|
||||||
sortedDimensions.sort(Comparator.comparing(File::getPath));
|
sortedDimensions.sort(Comparator.comparing(File::getPath));
|
||||||
|
File dimensionsFolder = new File(packFolder, PackValidator.DIMENSIONS_FOLDER);
|
||||||
for (File dimensionFile : sortedDimensions) {
|
for (File dimensionFile : sortedDimensions) {
|
||||||
JSONObject dimension = PackValidationIo.readJson(dimensionFile);
|
JSONObject dimension = PackValidationIo.readJson(dimensionFile);
|
||||||
if (dimension == null) {
|
if (dimension == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
String dimensionKey = PackValidationIo.stripExtension(dimensionFile.getName());
|
String dimensionKey = PackValidationIo.deriveKey(dimensionsFolder, dimensionFile);
|
||||||
String dimensionPath = "Dimension '" + dimensionKey + "'";
|
String dimensionPath = "Dimension '" + dimensionKey + "'";
|
||||||
JSONObject dimensionPolicy = validatePolicy(
|
JSONObject dimensionPolicy = validatePolicy(
|
||||||
dimensionPath,
|
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.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisBiomeGeneratorLink;
|
import art.arcane.iris.engine.object.IrisBiomeGeneratorLink;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
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.IrisExpression;
|
||||||
import art.arcane.iris.engine.object.IrisGenerator;
|
import art.arcane.iris.engine.object.IrisGenerator;
|
||||||
import art.arcane.iris.engine.object.IrisImage;
|
import art.arcane.iris.engine.object.IrisImage;
|
||||||
@@ -217,11 +218,12 @@ final class PackImageMapValidator {
|
|||||||
Set<String> warnings
|
Set<String> warnings
|
||||||
) {
|
) {
|
||||||
File[] sortedFiles = Arrays.copyOf(dimensionFiles, dimensionFiles.length);
|
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<>();
|
Map<String, IrisDimension> dimensions = new LinkedHashMap<>();
|
||||||
Set<String> bindingReadyDimensions = new LinkedHashSet<>();
|
Set<String> bindingReadyDimensions = new LinkedHashSet<>();
|
||||||
|
File dimensionsFolder = new File(packFolder, PackValidator.DIMENSIONS_FOLDER);
|
||||||
for (File dimensionFile : sortedFiles) {
|
for (File dimensionFile : sortedFiles) {
|
||||||
String dimensionKey = PackValidationIo.stripExtension(dimensionFile.getName());
|
String dimensionKey = PackValidationIo.deriveKey(dimensionsFolder, dimensionFile);
|
||||||
JSONObject raw = PackValidationIo.readJson(dimensionFile);
|
JSONObject raw = PackValidationIo.readJson(dimensionFile);
|
||||||
if (raw == null) {
|
if (raw == null) {
|
||||||
continue;
|
continue;
|
||||||
@@ -247,19 +249,35 @@ final class PackImageMapValidator {
|
|||||||
validateGeneratorStyleReferences(dimensionKey, dimension, data, resources, blockingErrors);
|
validateGeneratorStyleReferences(dimensionKey, dimension, data, resources, blockingErrors);
|
||||||
}
|
}
|
||||||
Map<String, List<Map.Entry<String, IrisDimension>>> upperParents = new TreeMap<>();
|
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()) {
|
for (Map.Entry<String, IrisDimension> entry : dimensions.entrySet()) {
|
||||||
String upperKey = normalized(entry.getValue().getUpperDimension());
|
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;
|
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()) {
|
for (Map.Entry<String, IrisDimension> entry : dimensions.entrySet()) {
|
||||||
if (!bindingReadyDimensions.contains(entry.getKey())) {
|
if (!bindingReadyDimensions.contains(entry.getKey())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
List<Map.Entry<String, IrisDimension>> parents = upperParents.get(entry.getKey());
|
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(),
|
validateBindings(packFolder, "Dimension '" + entry.getKey() + "'", entry.getValue(),
|
||||||
entry.getValue().getWorldBoundary(), data, resources, registries, blockingErrors, warnings);
|
entry.getValue().getWorldBoundary(), data, resources, registries, blockingErrors, warnings);
|
||||||
continue;
|
continue;
|
||||||
@@ -268,12 +286,23 @@ final class PackImageMapValidator {
|
|||||||
validateBindings(packFolder, "Dimension '" + entry.getKey() + "'", entry.getValue(),
|
validateBindings(packFolder, "Dimension '" + entry.getKey() + "'", entry.getValue(),
|
||||||
entry.getValue().getWorldBoundary(), data, resources, registries, blockingErrors, warnings);
|
entry.getValue().getWorldBoundary(), data, resources, registries, blockingErrors, warnings);
|
||||||
}
|
}
|
||||||
parents.sort(Map.Entry.comparingByKey());
|
if (hasUpperParents) {
|
||||||
for (Map.Entry<String, IrisDimension> parent : parents) {
|
parents.sort(Map.Entry.comparingByKey());
|
||||||
validateBindings(packFolder,
|
for (Map.Entry<String, IrisDimension> parent : parents) {
|
||||||
"Dimension '" + parent.getKey() + "' upper dimension '" + entry.getKey() + "'",
|
validateBindings(packFolder,
|
||||||
entry.getValue(), parent.getValue().getWorldBoundary(), data, resources, registries,
|
"Dimension '" + parent.getKey() + "' upper dimension '" + entry.getKey() + "'",
|
||||||
blockingErrors, warnings);
|
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);
|
return new PackValidationResult(packName, blockingErrors, warnings, validatedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
File[] dimensionFiles = dimensionsFolder.listFiles(f -> f.isFile() && f.getName().endsWith(".json"));
|
List<File> discoveredDimensions = PackValidationIo.listJsonRecursive(dimensionsFolder);
|
||||||
if (dimensionFiles == null || dimensionFiles.length == 0) {
|
if (discoveredDimensions.isEmpty()) {
|
||||||
blockingErrors.add("No dimension JSON files under dimensions/.");
|
blockingErrors.add("No dimension JSON files under dimensions/.");
|
||||||
return new PackValidationResult(packName, blockingErrors, warnings, validatedAt);
|
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);
|
PackDimensionValidator.validateDimensions(packFolder, dimensionFiles, blockingErrors, warnings);
|
||||||
PackImageMapValidator.Validation imageMaps = PackImageMapValidator.validate(
|
PackImageMapValidator.Validation imageMaps = PackImageMapValidator.validate(
|
||||||
@@ -195,7 +197,7 @@ public final class PackValidator {
|
|||||||
if (!data.getContentGate().ready()) {
|
if (!data.getContentGate().ready()) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
boolean dimensionExcluded = loadEverything(data, dimensionFiles);
|
boolean dimensionExcluded = loadEverything(packFolder, data, dimensionFiles);
|
||||||
List<CompatFinding> findings = data.getCompatReport().findings();
|
List<CompatFinding> findings = data.getCompatReport().findings();
|
||||||
applyCompatFindings(minecraftVersion, dimensionExcluded, findings, blockingErrors, warnings);
|
applyCompatFindings(minecraftVersion, dimensionExcluded, findings, blockingErrors, warnings);
|
||||||
return findings;
|
return findings;
|
||||||
@@ -219,11 +221,12 @@ public final class PackValidator {
|
|||||||
*
|
*
|
||||||
* @return true when any dimension in the pack cannot generate on this server
|
* @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;
|
boolean dimensionExcluded = false;
|
||||||
|
File dimensionsFolder = new File(packFolder, DIMENSIONS_FOLDER);
|
||||||
for (File dimensionFile : dimensionFiles) {
|
for (File dimensionFile : dimensionFiles) {
|
||||||
IrisDimension dimension = data.getDimensionLoader().load(
|
IrisDimension dimension = data.getDimensionLoader().load(
|
||||||
PackValidationIo.stripExtension(dimensionFile.getName()), false);
|
PackValidationIo.deriveKey(dimensionsFolder, dimensionFile), false);
|
||||||
if (dimension == null) {
|
if (dimension == null) {
|
||||||
continue;
|
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.IrisBlockData;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.engine.object.IrisEntity;
|
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.IrisGenerator;
|
||||||
import art.arcane.iris.engine.object.IrisLootTable;
|
import art.arcane.iris.engine.object.IrisLootTable;
|
||||||
import art.arcane.iris.core.pack.PackExportClosure;
|
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());
|
throw new IllegalStateException("Failed to create structure package staging folder " + folder.getAbsolutePath());
|
||||||
}
|
}
|
||||||
IrisLogging.info("Packaging Dimension " + dimension.getName() + " " + (obfuscate ? "(Obfuscated)" : ""));
|
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<IrisRegion> regions = new KSet<>();
|
||||||
KSet<IrisBiome> biomes = new KSet<>();
|
KSet<IrisBiome> biomes = new KSet<>();
|
||||||
KSet<IrisEntity> entities = new KSet<>();
|
KSet<IrisEntity> entities = new KSet<>();
|
||||||
KSet<IrisSpawner> spawners = new KSet<>();
|
KSet<IrisSpawner> spawners = new KSet<>();
|
||||||
KSet<IrisGenerator> generators = new KSet<>();
|
KSet<IrisGenerator> generators = new KSet<>();
|
||||||
|
KSet<IrisExpression> expressions = new KSet<>();
|
||||||
KSet<IrisLootTable> loot = new KSet<>();
|
KSet<IrisLootTable> loot = new KSet<>();
|
||||||
KSet<IrisBlockData> blocks = new KSet<>();
|
KSet<IrisBlockData> blocks = new KSet<>();
|
||||||
|
|
||||||
@@ -119,22 +128,30 @@ public class IrisPackageCompiler {
|
|||||||
for (String i : dm.getBlockLoader().getPossibleKeys()) {
|
for (String i : dm.getBlockLoader().getPossibleKeys()) {
|
||||||
addIfPresent(blocks, dm.getBlockLoader().load(i));
|
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));
|
for (IrisDimension exportedDimension : dimensions) {
|
||||||
dimension.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i)));
|
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)));
|
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.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i))));
|
||||||
regions.forEach((r) -> r.getEntitySpawners().forEach((sp) -> addIfPresent(spawners, dm.getSpawnerLoader().load(sp))));
|
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((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.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i))));
|
||||||
biomes.forEach((r) -> r.getEntitySpawners().forEach((sp) -> addIfPresent(spawners, dm.getSpawnerLoader().load(sp))));
|
biomes.forEach((r) -> r.getEntitySpawners().forEach((sp) -> addIfPresent(spawners, dm.getSpawnerLoader().load(sp))));
|
||||||
KList<IrisObjectPlacement> allPlacements = new KList<>();
|
KList<IrisObjectPlacement> allPlacements = new KList<>();
|
||||||
regions.forEach((r) -> allPlacements.addAll(r.getObjects()));
|
regions.forEach((r) -> allPlacements.addAll(r.getObjects()));
|
||||||
biomes.forEach((i) -> allPlacements.addAll(i.getObjects()));
|
biomes.forEach((i) -> allPlacements.addAll(i.getObjects()));
|
||||||
for (IrisStaticObject placement : dimension.getStaticObjects()) {
|
for (IrisDimension exportedDimension : dimensions) {
|
||||||
allPlacements.add(placement.toPlacement());
|
for (IrisStaticObject placement : exportedDimension.getStaticObjects()) {
|
||||||
|
allPlacements.add(placement.toPlacement());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
KSet<IrisMarker> markers = new KSet<>();
|
KSet<IrisMarker> markers = new KSet<>();
|
||||||
for (String markerKey : PackExportClosure.collectMarkerKeys(allPlacements)) {
|
for (String markerKey : PackExportClosure.collectMarkerKeys(allPlacements)) {
|
||||||
@@ -148,7 +165,9 @@ public class IrisPackageCompiler {
|
|||||||
collectSpawnerEntityKeys(spawners).forEach((i) -> addIfPresent(entities, dm.getEntityLoader().load(i)));
|
collectSpawnerEntityKeys(spawners).forEach((i) -> addIfPresent(entities, dm.getEntityLoader().load(i)));
|
||||||
entities.forEach((e) -> e.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i))));
|
entities.forEach((e) -> e.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i))));
|
||||||
Set<String> structureKeys = new LinkedHashSet<>();
|
Set<String> structureKeys = new LinkedHashSet<>();
|
||||||
collectStructureKeys(structureKeys, dimension.getStructures());
|
for (IrisDimension exportedDimension : dimensions) {
|
||||||
|
collectStructureKeys(structureKeys, exportedDimension.getStructures());
|
||||||
|
}
|
||||||
regions.forEach((region) -> collectStructureKeys(structureKeys, region.getStructures()));
|
regions.forEach((region) -> collectStructureKeys(structureKeys, region.getStructures()));
|
||||||
biomes.forEach((biome) -> collectStructureKeys(structureKeys, biome.getStructures()));
|
biomes.forEach((biome) -> collectStructureKeys(structureKeys, biome.getStructures()));
|
||||||
KMap<String, String> renameObjects = new KMap<>();
|
KMap<String, String> renameObjects = new KMap<>();
|
||||||
@@ -176,8 +195,10 @@ public class IrisPackageCompiler {
|
|||||||
j.setPlace(newNames);
|
j.setPlace(newNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (IrisStaticObject placement : dimension.getStaticObjects()) {
|
for (IrisDimension exportedDimension : dimensions) {
|
||||||
placement.setObject(renameObjects.get(placement.getObject()));
|
for (IrisStaticObject placement : exportedDimension.getStaticObjects()) {
|
||||||
|
placement.setObject(renameObjects.get(placement.getObject()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
KMap<String, KList<String>> lookupObjects = renameObjects.flip();
|
KMap<String, KList<String>> lookupObjects = renameObjects.flip();
|
||||||
@@ -230,9 +251,12 @@ public class IrisPackageCompiler {
|
|||||||
}
|
}
|
||||||
b.append(structureClosure.writeTo(folder, minify));
|
b.append(structureClosure.writeTo(folder, minify));
|
||||||
b.append(ImageMapPackageClosure.writeAll(dm, folder, minify));
|
b.append(ImageMapPackageClosure.writeAll(dm, folder, minify));
|
||||||
a = new JSONObject(new Gson().toJson(dimension)).toString(minify ? 0 : 4);
|
for (IrisDimension exportedDimension : dimensions.stream()
|
||||||
IO.writeAll(new File(folder, "dimensions/" + dimension.getLoadKey() + ".json"), a);
|
.sorted(Comparator.comparing(IrisDimension::getLoadKey)).toList()) {
|
||||||
b.append(IO.hash(a));
|
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) {
|
for (IrisGenerator i : generators) {
|
||||||
a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4);
|
a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4);
|
||||||
@@ -240,6 +264,13 @@ public class IrisPackageCompiler {
|
|||||||
b.append(IO.hash(a));
|
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()));
|
c.append(IO.hash(b.toString()));
|
||||||
b = new StringBuilder();
|
b = new StringBuilder();
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ package art.arcane.iris.core.project;
|
|||||||
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
||||||
import art.arcane.volmlib.util.format.Form;
|
import art.arcane.volmlib.util.format.Form;
|
||||||
import art.arcane.volmlib.util.io.IO;
|
import art.arcane.volmlib.util.io.IO;
|
||||||
|
import art.arcane.volmlib.util.json.JSONArray;
|
||||||
import art.arcane.volmlib.util.json.JSONObject;
|
import art.arcane.volmlib.util.json.JSONObject;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -179,19 +180,58 @@ public final class IrisProjectCopier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void transformDimension(Path stage, String sourceKey, String targetKey) throws IOException {
|
private static void transformDimension(Path stage, String sourceKey, String targetKey) throws IOException {
|
||||||
Path oldDimension = stage.resolve("dimensions").resolve(sourceKey + ".json");
|
Path dimensionsRoot = stage.resolve("dimensions");
|
||||||
Path newDimension = stage.resolve("dimensions").resolve(targetKey + ".json");
|
Path oldDimension = dimensionsRoot.resolve(sourceKey + ".json");
|
||||||
|
Path newDimension = dimensionsRoot.resolve(targetKey + ".json");
|
||||||
if (!oldDimension.equals(newDimension)) {
|
if (!oldDimension.equals(newDimension)) {
|
||||||
Files.move(oldDimension, newDimension);
|
Files.move(oldDimension, newDimension);
|
||||||
}
|
}
|
||||||
|
|
||||||
JSONObject json = new JSONObject(IO.readAll(newDimension.toFile()));
|
try (Stream<Path> files = Files.walk(dimensionsRoot)) {
|
||||||
if (json.has("name")) {
|
for (Path dimensionFile : files.filter(path -> Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS))
|
||||||
json.put("name", Form.capitalizeWords(targetKey.replace('-', ' ')));
|
.filter(path -> path.getFileName().toString().endsWith(".json"))
|
||||||
IO.writeAll(newDimension.toFile(), json.toString(4));
|
.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 {
|
private static void publish(Path stage, Path target) throws IOException {
|
||||||
try {
|
try {
|
||||||
Files.move(stage, target, StandardCopyOption.ATOMIC_MOVE);
|
Files.move(stage, target, StandardCopyOption.ATOMIC_MOVE);
|
||||||
|
|||||||
@@ -515,7 +515,7 @@ public class BoardSVC implements IrisService, BoardProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lines.add("&7&m ");
|
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_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_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))));
|
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
|
// Add more or find any in the region
|
||||||
if (isUseAll || placements.isEmpty()) {
|
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()));
|
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;
|
IrisComplex nextComplex = null;
|
||||||
try {
|
try {
|
||||||
engine.sealForTransition("complex hotload", false);
|
engine.sealForTransition("complex hotload", false);
|
||||||
|
engine.prepareRuntimeHotload();
|
||||||
RuntimeAssembly assembly = new RuntimeAssembly(RuntimeAssembly.nextRuntimeId(), previous.target());
|
RuntimeAssembly assembly = new RuntimeAssembly(RuntimeAssembly.nextRuntimeId(), previous.target());
|
||||||
engine.runtimeAssembly.set(assembly);
|
engine.runtimeAssembly.set(assembly);
|
||||||
EngineRuntime next;
|
EngineRuntime next;
|
||||||
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
|
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
|
||||||
assembly.complex = new IrisComplex(engine);
|
assembly.complex = new IrisComplex(engine);
|
||||||
nextComplex = assembly.complex;
|
nextComplex = assembly.complex;
|
||||||
|
assembly.dimensionStackContext = engine.runtimeBuilder.buildDimensionStackContext();
|
||||||
assembly.upperContext = engine.runtimeBuilder.buildUpperContext();
|
assembly.upperContext = engine.runtimeBuilder.buildUpperContext();
|
||||||
BiomeMaxes biomeMaxes = engine.runtimeBuilder.computeBiomeMaxes();
|
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 {
|
} finally {
|
||||||
engine.runtimeAssembly.remove();
|
engine.runtimeAssembly.remove();
|
||||||
}
|
}
|
||||||
@@ -110,6 +117,7 @@ final class EngineHotloader {
|
|||||||
boolean published = false;
|
boolean published = false;
|
||||||
try {
|
try {
|
||||||
engine.sealForTransition("hotload", false);
|
engine.sealForTransition("hotload", false);
|
||||||
|
engine.prepareRuntimeHotload();
|
||||||
replacementData = IrisData.openRuntime(previousData.getDataFolder());
|
replacementData = IrisData.openRuntime(previousData.getDataFolder());
|
||||||
IrisDimension replacement = replacementData.getDimensionLoader().load(previousDimension.getLoadKey());
|
IrisDimension replacement = replacementData.getDimensionLoader().load(previousDimension.getLoadKey());
|
||||||
if (replacement == null) {
|
if (replacement == null) {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ record EngineRuntime(
|
|||||||
EngineTarget target,
|
EngineTarget target,
|
||||||
IrisComplex complex,
|
IrisComplex complex,
|
||||||
UpperDimensionContext upperContext,
|
UpperDimensionContext upperContext,
|
||||||
|
DimensionStackContext dimensionStackContext,
|
||||||
EngineEffects effects,
|
EngineEffects effects,
|
||||||
EngineMode mode,
|
EngineMode mode,
|
||||||
EngineWorldManager worldManager,
|
EngineWorldManager worldManager,
|
||||||
@@ -44,6 +45,7 @@ record EngineRuntime(
|
|||||||
int nextCacheId,
|
int nextCacheId,
|
||||||
IrisComplex nextComplex,
|
IrisComplex nextComplex,
|
||||||
UpperDimensionContext nextUpperContext,
|
UpperDimensionContext nextUpperContext,
|
||||||
|
DimensionStackContext nextDimensionStackContext,
|
||||||
BiomeMaxes nextBiomeMaxes
|
BiomeMaxes nextBiomeMaxes
|
||||||
) {
|
) {
|
||||||
return new EngineRuntime(
|
return new EngineRuntime(
|
||||||
@@ -51,6 +53,7 @@ record EngineRuntime(
|
|||||||
target,
|
target,
|
||||||
nextComplex,
|
nextComplex,
|
||||||
nextUpperContext,
|
nextUpperContext,
|
||||||
|
nextDimensionStackContext,
|
||||||
effects,
|
effects,
|
||||||
mode,
|
mode,
|
||||||
worldManager,
|
worldManager,
|
||||||
|
|||||||
@@ -22,12 +22,14 @@ import art.arcane.iris.core.loader.IrisData;
|
|||||||
import art.arcane.iris.core.loader.ResourceLoader;
|
import art.arcane.iris.core.loader.ResourceLoader;
|
||||||
import art.arcane.iris.engine.EngineRuntime.BiomeMaxes;
|
import art.arcane.iris.engine.EngineRuntime.BiomeMaxes;
|
||||||
import art.arcane.iris.engine.IrisEngine.LifecycleState;
|
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.EngineEffects;
|
||||||
import art.arcane.iris.engine.framework.EngineEffectsProvider;
|
import art.arcane.iris.engine.framework.EngineEffectsProvider;
|
||||||
import art.arcane.iris.engine.framework.EngineMode;
|
import art.arcane.iris.engine.framework.EngineMode;
|
||||||
import art.arcane.iris.engine.framework.EngineTarget;
|
import art.arcane.iris.engine.framework.EngineTarget;
|
||||||
import art.arcane.iris.engine.framework.EngineWorldManager;
|
import art.arcane.iris.engine.framework.EngineWorldManager;
|
||||||
import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
|
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.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisBiomePaletteLayer;
|
import art.arcane.iris.engine.object.IrisBiomePaletteLayer;
|
||||||
import art.arcane.iris.engine.object.IrisDecorator;
|
import art.arcane.iris.engine.object.IrisDecorator;
|
||||||
@@ -74,6 +76,9 @@ final class EngineRuntimeBuilder {
|
|||||||
assembly.complex = new IrisComplex(engine);
|
assembly.complex = new IrisComplex(engine);
|
||||||
IrisLogging.debug("[IrisEngine timing] complex=" + (M.ms() - started) + "ms");
|
IrisLogging.debug("[IrisEngine timing] complex=" + (M.ms() - started) + "ms");
|
||||||
started = M.ms();
|
started = M.ms();
|
||||||
|
assembly.dimensionStackContext = buildDimensionStackContext();
|
||||||
|
IrisLogging.debug("[IrisEngine timing] buildDimensionStackContext=" + (M.ms() - started) + "ms");
|
||||||
|
started = M.ms();
|
||||||
assembly.upperContext = buildUpperContext();
|
assembly.upperContext = buildUpperContext();
|
||||||
IrisLogging.debug("[IrisEngine timing] buildUpperContext=" + (M.ms() - started) + "ms");
|
IrisLogging.debug("[IrisEngine timing] buildUpperContext=" + (M.ms() - started) + "ms");
|
||||||
started = M.ms();
|
started = M.ms();
|
||||||
@@ -91,6 +96,14 @@ final class EngineRuntimeBuilder {
|
|||||||
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
|
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
|
||||||
assembly.mode.registerStage((x, z, blocks, biomes, multicore, context) ->
|
assembly.mode.registerStage((x, z, blocks, biomes, multicore, context) ->
|
||||||
staticObjects.apply(engine, x, z, blocks));
|
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");
|
IrisLogging.debug("[IrisEngine timing] setupMode=" + (M.ms() - started) + "ms");
|
||||||
started = M.ms();
|
started = M.ms();
|
||||||
assembly.worldManager = IrisServices.get(EngineWorldManagerProvider.class).create(engine);
|
assembly.worldManager = IrisServices.get(EngineWorldManagerProvider.class).create(engine);
|
||||||
@@ -205,6 +218,21 @@ final class EngineRuntimeBuilder {
|
|||||||
return null;
|
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() {
|
private EngineMode createMode() {
|
||||||
Throwable configuredFailure = null;
|
Throwable configuredFailure = null;
|
||||||
try {
|
try {
|
||||||
@@ -300,6 +328,7 @@ final class EngineRuntimeBuilder {
|
|||||||
}
|
}
|
||||||
IrisComplex complex;
|
IrisComplex complex;
|
||||||
UpperDimensionContext upperContext;
|
UpperDimensionContext upperContext;
|
||||||
|
DimensionStackContext dimensionStackContext;
|
||||||
EngineEffects effects;
|
EngineEffects effects;
|
||||||
EngineMode mode;
|
EngineMode mode;
|
||||||
EngineWorldManager worldManager;
|
EngineWorldManager worldManager;
|
||||||
@@ -319,6 +348,7 @@ final class EngineRuntimeBuilder {
|
|||||||
target,
|
target,
|
||||||
complex,
|
complex,
|
||||||
upperContext,
|
upperContext,
|
||||||
|
dimensionStackContext,
|
||||||
effects,
|
effects,
|
||||||
mode,
|
mode,
|
||||||
worldManager,
|
worldManager,
|
||||||
|
|||||||
@@ -501,10 +501,14 @@ public class IrisEngine implements Engine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void hotloadSilently() {
|
public void hotloadSilently() {
|
||||||
nativeStructureVolumeMemo.clear();
|
|
||||||
hotloader.hotloadSilently();
|
hotloader.hotloadSilently();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void prepareRuntimeHotload() {
|
||||||
|
nativeStructureVolumeMemo.clear();
|
||||||
|
platformHooks.prepareRuntimeHotload(this);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public KList<NativeStructureVolume> getNativeStructureVolumes(int minX, int minZ, int maxX, int maxZ) {
|
public KList<NativeStructureVolume> getNativeStructureVolumes(int minX, int minZ, int maxX, int maxZ) {
|
||||||
if (!nativeStructureVolumeQueriesEnabled.get()) {
|
if (!nativeStructureVolumeQueriesEnabled.get()) {
|
||||||
@@ -632,6 +636,16 @@ public class IrisEngine implements Engine {
|
|||||||
return current == null ? null : current.upperContext();
|
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
|
@Override
|
||||||
public CompletableFuture<Long> getHash32() {
|
public CompletableFuture<Long> getHash32() {
|
||||||
RuntimeAssembly assembly = runtimeAssembly.get();
|
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;
|
package art.arcane.iris.engine;
|
||||||
|
|
||||||
import art.arcane.iris.core.IrisSettings;
|
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.image.IrisImageMapRuntime;
|
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.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
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.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.spi.PlatformBlockState;
|
||||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
import art.arcane.iris.util.common.data.DataProvider;
|
||||||
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;
|
|
||||||
|
|
||||||
public class UpperDimensionContext implements DataProvider {
|
public class UpperDimensionContext implements DataProvider {
|
||||||
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
|
private final DimensionTerrainContext terrainContext;
|
||||||
private final IrisDimension dimension;
|
|
||||||
private final IrisData data;
|
|
||||||
private final int chunkHeight;
|
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) {
|
private UpperDimensionContext(DimensionTerrainContext terrainContext, int chunkHeight) {
|
||||||
this.dimension = state.dimension();
|
this.terrainContext = terrainContext;
|
||||||
this.data = state.data();
|
this.chunkHeight = chunkHeight;
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static UpperDimensionContext create(Engine engine, IrisDimension upperDim) {
|
public static UpperDimensionContext create(Engine engine, IrisDimension upperDimension) {
|
||||||
boolean selfRef = upperDim.getLoadKey().equals(engine.getDimension().getLoadKey());
|
return new UpperDimensionContext(
|
||||||
int chunkHeight = engine.getHeight();
|
DimensionTerrainContext.forUpper(engine, upperDimension),
|
||||||
if (selfRef) {
|
engine.getHeight()
|
||||||
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()
|
|
||||||
);
|
);
|
||||||
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(
|
static IrisRegion mappedRegion(
|
||||||
@@ -314,8 +49,8 @@ public class UpperDimensionContext implements DataProvider {
|
|||||||
double worldX,
|
double worldX,
|
||||||
double worldZ
|
double worldZ
|
||||||
) {
|
) {
|
||||||
IrisRegion mappedRegion = imageMapRuntime.sampleRegion(worldX, worldZ);
|
return DimensionTerrainContext.mappedRegion(
|
||||||
return mappedRegion == null ? proceduralRegion : mappedRegion;
|
imageMapRuntime, proceduralRegion, worldX, worldZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
static double mappedTerrainHeight(
|
static double mappedTerrainHeight(
|
||||||
@@ -324,7 +59,8 @@ public class UpperDimensionContext implements DataProvider {
|
|||||||
double worldX,
|
double worldX,
|
||||||
double worldZ
|
double worldZ
|
||||||
) {
|
) {
|
||||||
return imageMapRuntime.sampleTerrainHeight(worldX, worldZ, proceduralHeight);
|
return DimensionTerrainContext.mappedTerrainHeight(
|
||||||
|
imageMapRuntime, proceduralHeight, worldX, worldZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
static IrisBiome mappedBiome(
|
static IrisBiome mappedBiome(
|
||||||
@@ -333,8 +69,8 @@ public class UpperDimensionContext implements DataProvider {
|
|||||||
double worldX,
|
double worldX,
|
||||||
double worldZ
|
double worldZ
|
||||||
) {
|
) {
|
||||||
IrisBiome mappedBiome = imageMapRuntime.sampleBiome(worldX, worldZ);
|
return DimensionTerrainContext.mappedBiome(
|
||||||
return mappedBiome == null ? proceduralBiome : mappedBiome;
|
imageMapRuntime, proceduralBiome, worldX, worldZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
static PlatformBlockState mappedSurfaceBlock(
|
static PlatformBlockState mappedSurfaceBlock(
|
||||||
@@ -343,108 +79,40 @@ public class UpperDimensionContext implements DataProvider {
|
|||||||
double worldX,
|
double worldX,
|
||||||
double worldZ
|
double worldZ
|
||||||
) {
|
) {
|
||||||
PlatformBlockState mappedBlock = imageMapRuntime.sampleSurfaceBlock(worldX, worldZ);
|
return DimensionTerrainContext.mappedSurfaceBlock(
|
||||||
return mappedBlock == null ? proceduralBlock : mappedBlock;
|
imageMapRuntime, proceduralBlock, worldX, worldZ);
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getUpperSurfaceY(int x, int z) {
|
public int getUpperSurfaceY(int x, int z) {
|
||||||
double rawHeight = heightStream.get((double) x, (double) z);
|
return chunkHeight - 1 - (int) Math.round(terrainContext.getNormalTerrainHeight(x, z));
|
||||||
return chunkHeight - 1 - (int) Math.round(rawHeight);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IrisBiome getUpperBiome(int x, int 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) {
|
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) {
|
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) {
|
public PlatformBlockState getSurfaceBlock(int x, int z) {
|
||||||
return imageMapRuntime.sampleSurfaceBlock(x, z);
|
return terrainContext.getSurfaceBlock(x, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IrisDimension getDimension() {
|
public IrisDimension getDimension() {
|
||||||
return dimension;
|
return terrainContext.getDimension();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IrisData getData() {
|
public IrisData getData() {
|
||||||
return data;
|
return terrainContext.getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isSelfReferencing() {
|
public boolean isSelfReferencing() {
|
||||||
return selfReferencing;
|
return terrainContext.isSelfReferencing();
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
) {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,10 +18,13 @@
|
|||||||
|
|
||||||
package art.arcane.iris.engine.actuator;
|
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.Engine;
|
||||||
import art.arcane.iris.engine.framework.EngineAssignedActuator;
|
import art.arcane.iris.engine.framework.EngineAssignedActuator;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
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.ChunkContext;
|
||||||
import art.arcane.iris.util.project.context.ChunkedDataCache;
|
import art.arcane.iris.util.project.context.ChunkedDataCache;
|
||||||
import art.arcane.volmlib.util.collection.KMap;
|
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.IrisPlatforms;
|
||||||
import art.arcane.iris.spi.PlatformBiome;
|
import art.arcane.iris.spi.PlatformBiome;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
|
public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
|
||||||
private final RNG rng;
|
private final RNG rng;
|
||||||
private final ChronoLatch cl = new ChronoLatch(5000);
|
private final ChronoLatch cl = new ChronoLatch(5000);
|
||||||
@@ -60,33 +65,167 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
|
|||||||
Engine engine = getEngine();
|
Engine engine = getEngine();
|
||||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||||
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
|
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
|
||||||
|
DimensionStackContext dimensionStackContext = engine.getDimensionStackContext();
|
||||||
|
|
||||||
for (int xf = 0; xf < width; xf++) {
|
for (int xf = 0; xf < width; xf++) {
|
||||||
IrisBiome ib;
|
|
||||||
for (int zf = 0; zf < depth; zf++) {
|
for (int zf = 0; zf < depth; zf++) {
|
||||||
ib = biomeCache.get(xf, zf);
|
int worldX = x + xf;
|
||||||
String key;
|
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()) {
|
if (platformBiome != null) {
|
||||||
IrisBiomeCustom custom = ib.getCustomBiome(rng, engine, x + xf, 0, z + zf);
|
h.set(xf, 0, zf, xf, height - 1, zf, platformBiome);
|
||||||
key = getDimension().getLoadKey() + ":" + custom.getId();
|
|
||||||
} else {
|
|
||||||
key = ib.getSkyBiomeKey(rng, engine, x + xf, 0, z + zf);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ResolvedBiome resolved = resolve(key);
|
mantle.set(worldX, 0, worldZ, resolved.matter());
|
||||||
PlatformBiome biome = resolved.biome();
|
if (dimensionStackContext != null) {
|
||||||
|
PlatformBiome bottomBiome = platformBiome == null
|
||||||
if (biome != null) {
|
? h.getRaw(xf, 0, zf)
|
||||||
h.set(xf, 0, zf, xf, height - 1, zf, biome);
|
: 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());
|
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
|
* 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
|
* 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.core.nms.container.Pair;
|
||||||
import art.arcane.iris.engine.IrisComplex;
|
import art.arcane.iris.engine.IrisComplex;
|
||||||
import art.arcane.iris.engine.UpperDimensionContext;
|
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.data.chunk.TerrainChunk;
|
||||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
@@ -90,6 +93,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default @Nullable DimensionStackContext getDimensionStackContext() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
EngineMode getMode();
|
EngineMode getMode();
|
||||||
|
|
||||||
EnginePlatformHooks getPlatformHooks();
|
EnginePlatformHooks getPlatformHooks();
|
||||||
@@ -255,6 +262,25 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
|||||||
|
|
||||||
@BlockCoordinates
|
@BlockCoordinates
|
||||||
default IrisRegion getRegion(int x, int z) {
|
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);
|
return getComplex().getRegionStream().get(x, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +327,45 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
|||||||
|
|
||||||
@BlockCoordinates
|
@BlockCoordinates
|
||||||
default IrisBiome getCaveBiome(int x, int y, int z, IrisDimensionCarvingResolver.State state) {
|
default IrisBiome getCaveBiome(int x, int y, int z, IrisDimensionCarvingResolver.State state) {
|
||||||
IrisBiome 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();
|
int worldY = y + getWorld().minHeight();
|
||||||
IrisDimensionCarvingEntry rootCarvingEntry = IrisDimensionCarvingResolver.resolveRootEntry(this, worldY, state);
|
IrisDimensionCarvingEntry rootCarvingEntry = IrisDimensionCarvingResolver.resolveRootEntry(this, worldY, state);
|
||||||
if (rootCarvingEntry != null) {
|
if (rootCarvingEntry != null) {
|
||||||
@@ -311,13 +375,21 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
|||||||
return resolvedCarvingBiome;
|
return resolvedCarvingBiome;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IrisBiome resolveDepthCaveBiome(
|
||||||
|
int x,
|
||||||
|
int y,
|
||||||
|
int z,
|
||||||
|
IrisBiome surfaceBiome,
|
||||||
|
int surfaceY
|
||||||
|
) {
|
||||||
IrisBiome caveBiome = getCaveBiome(x, z);
|
IrisBiome caveBiome = getCaveBiome(x, z);
|
||||||
if (caveBiome == null) {
|
if (caveBiome == null) {
|
||||||
return surfaceBiome;
|
return surfaceBiome;
|
||||||
}
|
}
|
||||||
|
|
||||||
int surfaceY = getComplex().getHeightStream().get(x, z).intValue();
|
|
||||||
int depthBelowSurface = surfaceY - y;
|
int depthBelowSurface = surfaceY - y;
|
||||||
if (depthBelowSurface <= 0) {
|
if (depthBelowSurface <= 0) {
|
||||||
return surfaceBiome;
|
return surfaceBiome;
|
||||||
@@ -333,6 +405,18 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
|||||||
|
|
||||||
@BlockCoordinates
|
@BlockCoordinates
|
||||||
default IrisBiome getSurfaceBiome(int x, int z) {
|
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)) {
|
if (answersFromNaturalTerrain(x, z)) {
|
||||||
return getComplex().naturalSurfaceBiome(x, z);
|
return getComplex().naturalSurfaceBiome(x, z);
|
||||||
}
|
}
|
||||||
@@ -364,6 +448,12 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
|||||||
|
|
||||||
@BlockCoordinates
|
@BlockCoordinates
|
||||||
default int getHeight(int x, int z, boolean ignoreFluid) {
|
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)) {
|
if (answersFromNaturalTerrain(x, z)) {
|
||||||
int natural = getComplex().naturalTrueHeight(x, z);
|
int natural = getComplex().naturalTrueHeight(x, z);
|
||||||
return ignoreFluid ? natural : Math.max(natural, getMantle().getFluidHeight());
|
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);
|
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
|
@BlockCoordinates
|
||||||
@Override
|
@Override
|
||||||
default void catchBlockUpdates(int x, int y, int z, PlatformBlockState data) {
|
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();
|
IrisDimension dim = getDimension();
|
||||||
dim.getReachableBiomes(this).forEach((i) -> v.put(i.getLoadKey(), i));
|
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();
|
return v.v();
|
||||||
}
|
}
|
||||||
@@ -454,6 +571,22 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
|||||||
boolean isStudio();
|
boolean isStudio();
|
||||||
|
|
||||||
default IrisBiome getBiome(int x, int y, int z) {
|
default IrisBiome getBiome(int x, int y, int z) {
|
||||||
|
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) {
|
if (y <= getHeight(x, z) - 2) {
|
||||||
return getCaveBiome(x, y, z);
|
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) {
|
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) {
|
if (y <= getHeight(x, z) - 2) {
|
||||||
return getCaveOrMantleBiome(x, y, z);
|
return getCaveOrMantleBiome(x, y, z);
|
||||||
}
|
}
|
||||||
@@ -469,6 +618,12 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
|||||||
return getSurfaceBiome(x, z);
|
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) {
|
default String getObjectPlacementKey(int x, int y, int z) {
|
||||||
PlacedObject o = getObjectPlacement(x, y, 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()) {
|
for (IrisObjectPlacement i : region.getObjects()) {
|
||||||
if (i.compatPlace(getData()).contains(object)) {
|
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()) {
|
for (IrisObjectPlacement i : biome.getObjects()) {
|
||||||
if (i.compatPlace(getData()).contains(object)) {
|
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) {
|
default void generate(int x, int z, Hunk<PlatformBlockState> blocks, Hunk<PlatformBiome> biomes, boolean multicore, long generationSessionId) {
|
||||||
boolean cacheContext = !getEngine().getPlatformHooks().shouldDisableChunkContextCache(getEngine());
|
boolean cacheContext = !getEngine().getPlatformHooks().shouldDisableChunkContextCache(getEngine());
|
||||||
ChunkContext.PrefillPlan prefillPlan = cacheContext ? ChunkContext.PrefillPlan.NO_CAVE : ChunkContext.PrefillPlan.NONE;
|
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]);
|
EngineStage[] stages = getStages().toArray(new EngineStage[0]);
|
||||||
try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), generationSessionId, ctx)) {
|
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 validateDimensionHotload(Engine engine, IrisDimension replacement) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default void prepareRuntimeHotload(Engine engine) {
|
||||||
|
}
|
||||||
|
|
||||||
default void applyWorldBoundary(Engine engine) {
|
default void applyWorldBoundary(Engine engine) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
package art.arcane.iris.engine.framework;
|
package art.arcane.iris.engine.framework;
|
||||||
|
|
||||||
import art.arcane.iris.core.IrisSettings;
|
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.engine.IrisComplex;
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
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) {
|
if (engine.getFocus() != null || engine.getFocusRegion() != null) {
|
||||||
return SearchPlan.unpruned();
|
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();
|
IrisComplex complex = engine.getComplex();
|
||||||
BiomeSource source = new BiomeSource(engine);
|
BiomeSource source = new BiomeSource(engine);
|
||||||
@@ -380,6 +389,17 @@ public final class HintedLocator<T> implements Locator<T> {
|
|||||||
return SearchPlan.unpruned();
|
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)) {
|
if (!engine.getDimension().getRegions().contains(regionKey)) {
|
||||||
return SearchPlan.impossible();
|
return SearchPlan.impossible();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -655,7 +655,7 @@ public final class IrisStructureLocator {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (placement.resolvedAnchor() == IrisStructureAnchorMode.SURFACE) {
|
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;
|
return surfaceY < placement.getMinHeight() || surfaceY > placement.getMaxHeight() ? null : surfaceY;
|
||||||
}
|
}
|
||||||
int bandMin = Math.max(worldMin, Math.min(placement.getMinHeight(), placement.getMaxHeight()));
|
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) {
|
if (surfaceHeights.size() >= MAX_BURIAL_COLUMNS) {
|
||||||
return Integer.MIN_VALUE;
|
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);
|
surfaceHeights.put(columnKey, surfaceY);
|
||||||
}
|
}
|
||||||
int allowedTopY = surfaceY - UNDERGROUND_SURFACE_CLEARANCE;
|
int allowedTopY = surfaceY - UNDERGROUND_SURFACE_CLEARANCE;
|
||||||
|
|||||||
+2
-2
@@ -142,7 +142,7 @@ public final class NativeStructurePlacementPlanner {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (!placement.isUnderground()) {
|
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()) {
|
if (surfaceY < placement.getMinHeight() || surfaceY > placement.getMaxHeight()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -160,7 +160,7 @@ public final class NativeStructurePlacementPlanner {
|
|||||||
int localFluidHeight = engine.getComplex() == null
|
int localFluidHeight = engine.getComplex() == null
|
||||||
? engine.getDimension().getFluidHeight()
|
? engine.getDimension().getFluidHeight()
|
||||||
: (int) Math.round(engine.getComplex().getRiverWaterSurfaceStream().get(blockX, blockZ));
|
: (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) {
|
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.IrisSettings;
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
import art.arcane.iris.engine.IrisComplex;
|
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.UpperDimensionContext;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.framework.EngineTarget;
|
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.BlockCoordinates;
|
||||||
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
|
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
|
||||||
import art.arcane.iris.util.project.hunk.Hunk;
|
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.Mantle;
|
||||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||||
@@ -196,17 +199,37 @@ public interface EngineMantle extends MatterGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ChunkCoordinates
|
@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()) {
|
if (!getEngine().getDimension().isUseMantle()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
UpperDimensionContext upperCtx = getEngine().getUpperContext();
|
UpperDimensionContext upperCtx = getEngine().getUpperContext();
|
||||||
boolean protectUpper = upperCtx != null;
|
boolean protectUpper = upperCtx != null;
|
||||||
|
DimensionStackContext stackContext = getEngine().getDimensionStackContext();
|
||||||
|
|
||||||
MantleChunk<Matter> chunk = getMantle().getChunk(x, z).use();
|
MantleChunk<Matter> chunk = getMantle().getChunk(x, z).use();
|
||||||
try {
|
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 chunkBlockX = x << 4;
|
||||||
int chunkBlockZ = z << 4;
|
int chunkBlockZ = z << 4;
|
||||||
int gap = getEngine().getDimension().getUpperDimensionGap();
|
int gap = getEngine().getDimension().getUpperDimensionGap();
|
||||||
|
|||||||
+1
-1
@@ -227,7 +227,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
if (cached != CACHE_MISS) {
|
if (cached != CACHE_MISS) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
int surfaceY = engine.getHeight(x, z, true);
|
int surfaceY = Engine.hostHeight(engine, x, z, true);
|
||||||
surfaceHeights.put(key, surfaceY);
|
surfaceHeights.put(key, surfaceY);
|
||||||
return surfaceY;
|
return surfaceY;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ public class IrisCaveCarver3D {
|
|||||||
if (precomputedSurfaceHeights != null && precomputedSurfaceHeights.length > index) {
|
if (precomputedSurfaceHeights != null && precomputedSurfaceHeights.length > index) {
|
||||||
columnSurfaceY = precomputedSurfaceHeights[index];
|
columnSurfaceY = precomputedSurfaceHeights[index];
|
||||||
} else {
|
} else {
|
||||||
columnSurfaceY = engine.getHeight(x, z);
|
columnSurfaceY = Engine.hostHeight(engine, x, z, true);
|
||||||
}
|
}
|
||||||
int unclampedClearanceTopY = columnSurfaceY - surfaceClearance;
|
int unclampedClearanceTopY = columnSurfaceY - surfaceClearance;
|
||||||
int clearanceTopY = Math.min(maxY, Math.max(minY, unclampedClearanceTopY));
|
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);
|
int zzz = 8 + (z << 4);
|
||||||
IrisRegion region = complex.getRegionStream().get(xxx, zzz);
|
IrisRegion region = complex.getRegionStream().get(xxx, zzz);
|
||||||
IrisBiome surfaceBiome = complex.getTrueBiomeStream().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);
|
IrisBiome caveBiome = resolveCaveObjectBiome(xxx, zzz, surfaceY, surfaceBiome);
|
||||||
if (IrisSettings.get().getGeneral().isDebug() && (x & 31) == 0 && (z & 31) == 0) {
|
if (IrisSettings.get().getGeneral().isDebug() && (x & 31) == 0 && (z & 31) == 0) {
|
||||||
int carvedBlocks = 0;
|
int carvedBlocks = 0;
|
||||||
@@ -948,7 +948,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
|||||||
}
|
}
|
||||||
Engine engine = getEngineMantle().getEngine();
|
Engine engine = getEngineMantle().getEngine();
|
||||||
IrisBiome at = engine.getCaveBiome(x, y, z);
|
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);
|
return caveAnchorBiomeConflicts(at, surface, expectedCaveBiomeKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1631,8 +1631,10 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
|||||||
public Set<String> guess(int x, int z) {
|
public Set<String> guess(int x, int z) {
|
||||||
// todo The guess doesnt bring into account that the placer may return -1
|
// todo The guess doesnt bring into account that the placer may return -1
|
||||||
RNG rng = applyNoise(x, z, Cache.key(x, z) + seed());
|
RNG rng = applyNoise(x, z, Cache.key(x, z) + seed());
|
||||||
IrisBiome biome = getEngineMantle().getEngine().getSurfaceBiome((x << 4) + 8, (z << 4) + 8);
|
int blockX = (x << 4) + 8;
|
||||||
IrisRegion region = getEngineMantle().getEngine().getRegion((x << 4) + 8, (z << 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<>();
|
Set<String> v = new KSet<>();
|
||||||
for (IrisObjectPlacement i : biome.getSurfaceObjects()) {
|
for (IrisObjectPlacement i : biome.getSurfaceObjects()) {
|
||||||
if (rng.chance(i.getChance() + rng.d(-0.005, 0.005)) && !i.isCompatExcluded(getData())) {
|
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;
|
return surfaceY;
|
||||||
}
|
}
|
||||||
|
|
||||||
surfaceY = getEngineMantle().getEngine().getHeight(x, z);
|
surfaceY = getEngineMantle().trueHeight(x, z);
|
||||||
surfaceHeights.put(columnKey, surfaceY);
|
surfaceHeights.put(columnKey, surfaceY);
|
||||||
return surfaceY;
|
return surfaceY;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ import art.arcane.iris.engine.framework.IrisEngineMode;
|
|||||||
public class ModeEnclosure extends IrisEngineMode implements EngineMode {
|
public class ModeEnclosure extends IrisEngineMode implements EngineMode {
|
||||||
public ModeEnclosure(Engine engine) {
|
public ModeEnclosure(Engine engine) {
|
||||||
super(engine);
|
super(engine);
|
||||||
var terrain = new IrisTerrainNormalActuator(getEngine());
|
IrisTerrainNormalActuator terrain = new IrisTerrainNormalActuator(getEngine());
|
||||||
var biome = new IrisBiomeActuator(getEngine());
|
IrisBiomeActuator biome = new IrisBiomeActuator(getEngine());
|
||||||
|
|
||||||
registerStage(burst(
|
registerStage(burst(
|
||||||
(x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c),
|
(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 class ModeIslands extends IrisEngineMode implements EngineMode {
|
||||||
public ModeIslands(Engine engine) {
|
public ModeIslands(Engine engine) {
|
||||||
super(engine);
|
super(engine);
|
||||||
var terrain = new IrisTerrainNormalActuator(getEngine());
|
IrisTerrainNormalActuator terrain = new IrisTerrainNormalActuator(getEngine());
|
||||||
var biome = new IrisBiomeActuator(getEngine());
|
IrisBiomeActuator biome = new IrisBiomeActuator(getEngine());
|
||||||
|
|
||||||
registerStage(burst(
|
registerStage(burst(
|
||||||
(x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c),
|
(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()) {
|
if (shouldBypassMantleStages()) {
|
||||||
return;
|
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 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);
|
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(sDecorant);
|
||||||
registerStage(sFloatingDecorate);
|
registerStage(sFloatingDecorate);
|
||||||
registerStage(sPerfection);
|
registerStage(sPerfection);
|
||||||
registerStage(sCustom);
|
if (getEngine().getDimensionStackContext() == null) {
|
||||||
|
registerStage(sCustom);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean shouldBypassMantleStages() {
|
private boolean shouldBypassMantleStages() {
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ import art.arcane.iris.engine.framework.IrisEngineMode;
|
|||||||
public class ModeSuperFlat extends IrisEngineMode implements EngineMode {
|
public class ModeSuperFlat extends IrisEngineMode implements EngineMode {
|
||||||
public ModeSuperFlat(Engine engine) {
|
public ModeSuperFlat(Engine engine) {
|
||||||
super(engine);
|
super(engine);
|
||||||
var terrain = new IrisTerrainNormalActuator(getEngine());
|
IrisTerrainNormalActuator terrain = new IrisTerrainNormalActuator(getEngine());
|
||||||
var biome = new IrisBiomeActuator(getEngine());
|
IrisBiomeActuator biome = new IrisBiomeActuator(getEngine());
|
||||||
|
|
||||||
registerStage(burst(
|
registerStage(burst(
|
||||||
(x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c),
|
(x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c),
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
package art.arcane.iris.engine.modifier;
|
package art.arcane.iris.engine.modifier;
|
||||||
|
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
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.UpperDimensionContext;
|
||||||
import art.arcane.iris.engine.actuator.IrisDecorantActuator;
|
import art.arcane.iris.engine.actuator.IrisDecorantActuator;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
@@ -91,6 +93,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
|||||||
UpperDimensionContext upperCtx = getEngine().getUpperContext();
|
UpperDimensionContext upperCtx = getEngine().getUpperContext();
|
||||||
boolean protectUpper = upperCtx != null && !getEngine().getDimension().isUpperDimensionCarving();
|
boolean protectUpper = upperCtx != null && !getEngine().getDimension().isUpperDimensionCarving();
|
||||||
int[] upperSurfaceHeights = protectUpper ? scratch.getOrCreateUpperSurfaceHeights() : null;
|
int[] upperSurfaceHeights = protectUpper ? scratch.getOrCreateUpperSurfaceHeights() : null;
|
||||||
|
DimensionStackContext stackContext = getEngine().getDimensionStackContext();
|
||||||
|
DimensionStackLayout[] stackLayouts = stackContext == null ? null : new DimensionStackLayout[256];
|
||||||
int chunkBlockX = PowerOfTwoCoordinates.chunkToBlock(x);
|
int chunkBlockX = PowerOfTwoCoordinates.chunkToBlock(x);
|
||||||
int chunkBlockZ = PowerOfTwoCoordinates.chunkToBlock(z);
|
int chunkBlockZ = PowerOfTwoCoordinates.chunkToBlock(z);
|
||||||
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
|
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
|
||||||
@@ -104,6 +108,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
|||||||
int gap = getEngine().getDimension().getUpperDimensionGap();
|
int gap = getEngine().getDimension().getUpperDimensionGap();
|
||||||
upperSurfaceHeights[columnIndex] = Math.max(rawUpper, surfaceHeights[columnIndex] + gap);
|
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();
|
MantleChunk<Matter> mantleChunk = mantle.getChunk(x, z).use();
|
||||||
@@ -117,6 +124,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
|||||||
scratch,
|
scratch,
|
||||||
columnMasks,
|
columnMasks,
|
||||||
upperSurfaceHeights,
|
upperSurfaceHeights,
|
||||||
|
stackLayouts,
|
||||||
worldHeightSpan,
|
worldHeightSpan,
|
||||||
caveLavaHeight,
|
caveLavaHeight,
|
||||||
chunkBlockX,
|
chunkBlockX,
|
||||||
@@ -348,6 +356,10 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
|||||||
if (context.upperSurfaceHeights() != null && y >= context.upperSurfaceHeights()[columnIndex]) {
|
if (context.upperSurfaceHeights() != null && y >= context.upperSurfaceHeights()[columnIndex]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (context.stackLayouts() != null
|
||||||
|
&& context.stackLayouts()[columnIndex].isHostFeatureProtectedY(y)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
PlatformBlockState current = context.output().getRaw(localX, y, localZ);
|
PlatformBlockState current = context.output().getRaw(localX, y, localZ);
|
||||||
if (hydrology != null && hydrology.action() == HydrologyCaveAction.SEAL_GUARD) {
|
if (hydrology != null && hydrology.action() == HydrologyCaveAction.SEAL_GUARD) {
|
||||||
@@ -405,6 +417,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
|||||||
IrisCarveScratch scratch,
|
IrisCarveScratch scratch,
|
||||||
CarveColumnMask[] columnMasks,
|
CarveColumnMask[] columnMasks,
|
||||||
int[] upperSurfaceHeights,
|
int[] upperSurfaceHeights,
|
||||||
|
DimensionStackLayout[] stackLayouts,
|
||||||
int worldHeightSpan,
|
int worldHeightSpan,
|
||||||
int caveLavaHeight,
|
int caveLavaHeight,
|
||||||
int chunkBlockX,
|
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.Engine;
|
||||||
import art.arcane.iris.engine.framework.EngineAssignedModifier;
|
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.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisDepositGenerator;
|
import art.arcane.iris.engine.object.IrisDepositGenerator;
|
||||||
import art.arcane.iris.engine.object.IrisDepositHeightDistribution;
|
import art.arcane.iris.engine.object.IrisDepositHeightDistribution;
|
||||||
@@ -186,6 +187,11 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DimensionStackLayout stackLayout = context.getDimensionStackLayout(nx, nz);
|
||||||
|
if (stackLayout != null && stackLayout.isHostFeatureProtectedY(ny)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
PlatformBlockState current = data.get(nx, ny, nz);
|
PlatformBlockState current = data.get(nx, ny, nz);
|
||||||
if (!canReplaceDepositTarget(current)) {
|
if (!canReplaceDepositTarget(current)) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
+28
-3
@@ -20,6 +20,7 @@ package art.arcane.iris.engine.modifier;
|
|||||||
|
|
||||||
|
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.engine.DimensionStackLayout;
|
||||||
import art.arcane.iris.engine.IrisComplex;
|
import art.arcane.iris.engine.IrisComplex;
|
||||||
import art.arcane.iris.engine.decorator.FloatingDecorator;
|
import art.arcane.iris.engine.decorator.FloatingDecorator;
|
||||||
import art.arcane.iris.engine.decorator.IrisSeaSurfaceDecorator;
|
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 {
|
try {
|
||||||
IdentityHashMap<IrisFloatingChildBiomes, MatterBiomeInject> matterByEntry = new IdentityHashMap<>();
|
IdentityHashMap<IrisFloatingChildBiomes, MatterBiomeInject> matterByEntry = new IdentityHashMap<>();
|
||||||
for (int k = 0; k <= sample.topIdx; k++) {
|
for (int k = 0; k <= sample.topIdx; k++) {
|
||||||
@@ -378,6 +395,9 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
|
|||||||
if (y < 0 || y >= chunkHeight) {
|
if (y < 0 || y >= chunkHeight) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (!shouldWriteHostBiomeMarker(stackLayout, y)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
IrisFloatingChildBiomes entry = sample.entryAt(k);
|
IrisFloatingChildBiomes entry = sample.entryAt(k);
|
||||||
MatterBiomeInject matter = matterByEntry.get(entry);
|
MatterBiomeInject matter = matterByEntry.get(entry);
|
||||||
if (matter == null) {
|
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) {
|
private MatterBiomeInject createSkyBiomeMatter(IrisBiome target, int wx, int wz) {
|
||||||
if (target.isCustom()) {
|
if (target.isCustom()) {
|
||||||
IrisBiomeCustom custom = target.getCustomBiome(rng, getEngine(), wx, 0, wz);
|
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)));
|
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.volmlib.util.math.RNG;
|
||||||
import art.arcane.iris.util.project.noise.CNG;
|
import art.arcane.iris.util.project.noise.CNG;
|
||||||
import art.arcane.iris.util.project.context.IrisContext;
|
import art.arcane.iris.util.project.context.IrisContext;
|
||||||
|
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
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);
|
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) {
|
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);
|
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.spi.PlatformBlockState;
|
||||||
import art.arcane.iris.util.common.data.B;
|
import art.arcane.iris.util.common.data.B;
|
||||||
import art.arcane.iris.util.project.noise.CNG;
|
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.collection.KList;
|
||||||
import art.arcane.volmlib.util.math.RNG;
|
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) {
|
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()) {
|
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<>();
|
KList<PlatformBlockState> data = new KList<>();
|
||||||
@@ -70,7 +79,7 @@ final class IrisBiomeLayerGenerator {
|
|||||||
IrisSlopeClip sc = layer.getSlopeCondition();
|
IrisSlopeClip sc = layer.getSlopeCondition();
|
||||||
|
|
||||||
if (!sc.isDefault()) {
|
if (!sc.isDefault()) {
|
||||||
if (!sc.isValid(complex.getSlopeStream().get(wx, wz))) {
|
if (!sc.isValid(resolveSlopeStream(complex, slopeStream).getDouble(wx, wz))) {
|
||||||
d = 0;
|
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) {
|
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> data = new KList<>();
|
||||||
KList<PlatformBlockState> real = new KList<>();
|
KList<PlatformBlockState> real = new KList<>();
|
||||||
int maxDepth = Math.min(maxDepthf, biome.getLockLayersMax());
|
int maxDepth = Math.min(maxDepthf, biome.getLockLayersMax());
|
||||||
@@ -190,7 +203,7 @@ final class IrisBiomeLayerGenerator {
|
|||||||
IrisSlopeClip sc = layer.getSlopeCondition();
|
IrisSlopeClip sc = layer.getSlopeCondition();
|
||||||
|
|
||||||
if (!sc.isDefault()) {
|
if (!sc.isDefault()) {
|
||||||
if (!sc.isValid(complex.getSlopeStream().get(wx, wz))) {
|
if (!sc.isValid(resolveSlopeStream(complex, slopeStream).getDouble(wx, wz))) {
|
||||||
d = 0;
|
d = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,6 +235,13 @@ final class IrisBiomeLayerGenerator {
|
|||||||
return real;
|
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) {
|
static KList<PlatformBlockState> generateSeaLayers(IrisBiome biome, double wx, double wz, RNG random, int maxDepth, IrisData rdata) {
|
||||||
KList<PlatformBlockState> data = new KList<>();
|
KList<PlatformBlockState> data = new KList<>();
|
||||||
|
|
||||||
|
|||||||
@@ -222,6 +222,8 @@ public class IrisDimension extends IrisRegistrant {
|
|||||||
private boolean upperDimensionObjects = false;
|
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.")
|
@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;
|
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)
|
@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.")
|
@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 = "";
|
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();
|
return regions.v();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -564,6 +572,7 @@ public class IrisDimension extends IrisRegistrant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Deque<String> pending = new ArrayDeque<>();
|
Deque<String> pending = new ArrayDeque<>();
|
||||||
|
addReachableBiomeKey(pending, getFocus());
|
||||||
IrisHydrology configuredHydrology = getHydrology();
|
IrisHydrology configuredHydrology = getHydrology();
|
||||||
IrisRiverHydrology configuredRivers = configuredHydrology == null ? null : configuredHydrology.getRivers();
|
IrisRiverHydrology configuredRivers = configuredHydrology == null ? null : configuredHydrology.getRivers();
|
||||||
boolean hydrologyPoliciesActive = configuredRivers != null && configuredRivers.isEnabled();
|
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 {
|
public void installBiomes(IDataFixer fixer, DataProvider data, KList<File> datapackRoots, KSet<String> biomes) throws IOException {
|
||||||
String namespace = getLoadKey().toLowerCase(Locale.ROOT);
|
String keyPrefix = getCustomBiomeKeyPrefix();
|
||||||
installBiomes(fixer, data, datapackRoots, namespace, "", biomes);
|
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,
|
public void installBiomes(IDataFixer fixer, DataProvider data, KList<File> datapackRoots,
|
||||||
@@ -747,18 +762,19 @@ public class IrisDimension extends IrisRegistrant {
|
|||||||
|
|
||||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||||
String customBiomeId = customBiome.getId();
|
String customBiomeId = customBiome.getId();
|
||||||
|
String customBiomePath = sanitizeRegistryPath(customBiomeId, "biome");
|
||||||
String json = customBiome.generateJson(fixer, contentGate);
|
String json = customBiome.generateJson(fixer, contentGate);
|
||||||
|
|
||||||
synchronized (biomes) {
|
synchronized (biomes) {
|
||||||
if (!biomes.add(customBiomeId)) {
|
if (!biomes.add(customBiomePath)) {
|
||||||
IrisLogging.debug("Duplicate Data Pack Biome: " + getLoadKey() + "/" + customBiomeId);
|
IrisLogging.debug("Duplicate Data Pack Biome: " + getLoadKey() + "/" + customBiomeId);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String biomePath = pathPrefix.isBlank()
|
String biomePath = pathPrefix.isBlank()
|
||||||
? customBiomeId
|
? customBiomePath
|
||||||
: pathPrefix + "/" + customBiomeId;
|
: pathPrefix + "/" + customBiomePath;
|
||||||
for (File datapackRoot : datapackRoots) {
|
for (File datapackRoot : datapackRoots) {
|
||||||
File output = new File(datapackRoot, "data/" + namespace + "/worldgen/biome/" + biomePath + ".json");
|
File output = new File(datapackRoot, "data/" + namespace + "/worldgen/biome/" + biomePath + ".json");
|
||||||
|
|
||||||
@@ -900,6 +916,28 @@ public class IrisDimension extends IrisRegistrant {
|
|||||||
return sanitizeDimensionTypeKeyValue(getLoadKey());
|
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) {
|
public static String sanitizeDimensionTypeKeyValue(String value) {
|
||||||
if (value == null || value.isBlank()) {
|
if (value == null || value.isBlank()) {
|
||||||
return "dimension";
|
return "dimension";
|
||||||
@@ -918,6 +956,26 @@ public class IrisDimension extends IrisRegistrant {
|
|||||||
return sanitized.isBlank() ? "dimension" : sanitized;
|
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() {
|
public IrisDimensionType getDimensionType() {
|
||||||
IrisDimensionTypeOptions options = getDimensionOptions();
|
IrisDimensionTypeOptions options = getDimensionOptions();
|
||||||
if (fullbright) {
|
if (fullbright) {
|
||||||
@@ -930,6 +988,10 @@ public class IrisDimension extends IrisRegistrant {
|
|||||||
return upperDimension != null && !upperDimension.isEmpty() && !upperDimension.equalsIgnoreCase("none");
|
return upperDimension != null && !upperDimension.isEmpty() && !upperDimension.equalsIgnoreCase("none");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasDimensionStack() {
|
||||||
|
return dimensionStack != null;
|
||||||
|
}
|
||||||
|
|
||||||
public void installDimensionType(IDataFixer fixer, KList<File> datapackRoots) throws IOException {
|
public void installDimensionType(IDataFixer fixer, KList<File> datapackRoots) throws IOException {
|
||||||
IrisDimensionType type = getDimensionType();
|
IrisDimensionType type = getDimensionType();
|
||||||
String json = type.toJson(fixer);
|
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) {
|
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) {
|
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) {
|
public static IrisBiome getSurfaceBiome(Engine engine, Chunk c) {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package art.arcane.iris.util.project.context;
|
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.spi.IrisPlatforms;
|
||||||
import art.arcane.iris.engine.IrisComplex;
|
import art.arcane.iris.engine.IrisComplex;
|
||||||
import art.arcane.iris.engine.framework.EngineMetrics;
|
import art.arcane.iris.engine.framework.EngineMetrics;
|
||||||
@@ -27,6 +29,8 @@ public class ChunkContext {
|
|||||||
private final ChunkedDataCache<PlatformBlockState> fluid;
|
private final ChunkedDataCache<PlatformBlockState> fluid;
|
||||||
private final ChunkedDataCache<IrisRegion> region;
|
private final ChunkedDataCache<IrisRegion> region;
|
||||||
private final FloatingIslandBoundarySampler floatingIslandBoundarySampler;
|
private final FloatingIslandBoundarySampler floatingIslandBoundarySampler;
|
||||||
|
private final DimensionStackContext dimensionStackContext;
|
||||||
|
private final DimensionStackLayout[] dimensionStackLayouts;
|
||||||
|
|
||||||
public ChunkContext(int x, int z, IrisComplex complex) {
|
public ChunkContext(int x, int z, IrisComplex complex) {
|
||||||
this(x, z, complex, 0L, true, PrefillPlan.NO_CAVE, null);
|
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) {
|
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.x = x;
|
||||||
this.z = z;
|
this.z = z;
|
||||||
this.complex = complex;
|
this.complex = complex;
|
||||||
@@ -57,6 +74,8 @@ public class ChunkContext {
|
|||||||
this.fluid = new ChunkedDataCache<>(complex.getFluidStream(), x, z, cache);
|
this.fluid = new ChunkedDataCache<>(complex.getFluidStream(), x, z, cache);
|
||||||
this.region = new ChunkedDataCache<>(complex.getRegionStream(), x, z, cache);
|
this.region = new ChunkedDataCache<>(complex.getRegionStream(), x, z, cache);
|
||||||
this.floatingIslandBoundarySampler = new FloatingIslandBoundarySampler((wx, wz) -> complex.getTrueBiomeStream().get(wx, wz));
|
this.floatingIslandBoundarySampler = new FloatingIslandBoundarySampler((wx, wz) -> complex.getTrueBiomeStream().get(wx, wz));
|
||||||
|
this.dimensionStackContext = dimensionStackContext;
|
||||||
|
this.dimensionStackLayouts = dimensionStackContext == null ? null : new DimensionStackLayout[256];
|
||||||
|
|
||||||
if (cache) {
|
if (cache) {
|
||||||
PrefillPlan resolvedPlan = prefillPlan == null ? PrefillPlan.NO_CAVE : prefillPlan;
|
PrefillPlan resolvedPlan = prefillPlan == null ? PrefillPlan.NO_CAVE : prefillPlan;
|
||||||
@@ -165,6 +184,19 @@ public class ChunkContext {
|
|||||||
return region;
|
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 {
|
public enum PrefillPlan {
|
||||||
ALL(true, true, true, true, true, true),
|
ALL(true, true, true, true, true, true),
|
||||||
NO_CAVE(true, true, false, true, true, true),
|
NO_CAVE(true, true, false, true, true, true),
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package art.arcane.iris.core;
|
package art.arcane.iris.core;
|
||||||
|
|
||||||
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.IrisGeneratorBinding;
|
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.DataVersion;
|
||||||
import art.arcane.iris.core.nms.datapack.v1217.DataFixerV1217;
|
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.collection.KList;
|
||||||
import art.arcane.volmlib.util.json.JSONObject;
|
import art.arcane.volmlib.util.json.JSONObject;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
@@ -88,6 +90,26 @@ public class IrisDatapackCompilerTest {
|
|||||||
assertTrue(Files.isRegularFile(datapackRoot.resolve("pack.mcmeta")));
|
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
|
@Test
|
||||||
public void collectsInstalledPacksThroughSymbolicLinkWorkspace() throws Exception {
|
public void collectsInstalledPacksThroughSymbolicLinkWorkspace() throws Exception {
|
||||||
Path dataDirectory = temporaryFolder.newFolder("linked-data").toPath();
|
Path dataDirectory = temporaryFolder.newFolder("linked-data").toPath();
|
||||||
@@ -252,6 +274,57 @@ public class IrisDatapackCompilerTest {
|
|||||||
assertFalse(ServerConfigurator.loadedRegistrySatisfies(loaded, changedBiomeTags));
|
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
|
@Test
|
||||||
public void rejectsBindingToMissingDimension() throws Exception {
|
public void rejectsBindingToMissingDimension() throws Exception {
|
||||||
Path packRoot = temporaryFolder.newFolder("missing-binding-pack").toPath();
|
Path packRoot = temporaryFolder.newFolder("missing-binding-pack").toPath();
|
||||||
@@ -325,6 +398,7 @@ public class IrisDatapackCompilerTest {
|
|||||||
) throws Exception {
|
) throws Exception {
|
||||||
Files.createDirectories(root.resolve("dimensions"));
|
Files.createDirectories(root.resolve("dimensions"));
|
||||||
Files.createDirectories(root.resolve("biomes"));
|
Files.createDirectories(root.resolve("biomes"));
|
||||||
|
Files.createDirectories(root.resolve("dimensions").resolve(dimensionKey).getParent());
|
||||||
Files.writeString(
|
Files.writeString(
|
||||||
root.resolve("dimensions").resolve(dimensionKey + ".json"),
|
root.resolve("dimensions").resolve(dimensionKey + ".json"),
|
||||||
"""
|
"""
|
||||||
|
|||||||
+1
@@ -80,6 +80,7 @@ public class ServerConfiguratorRegistryVerificationTest {
|
|||||||
IrisDimension dimension = mock(IrisDimension.class);
|
IrisDimension dimension = mock(IrisDimension.class);
|
||||||
when(dimension.getAllBiomes(any())).thenReturn(new KList<>(biome));
|
when(dimension.getAllBiomes(any())).thenReturn(new KList<>(biome));
|
||||||
when(dimension.getLoadKey()).thenReturn("overworld");
|
when(dimension.getLoadKey()).thenReturn("overworld");
|
||||||
|
when(dimension.getCustomBiomeKey("missing")).thenReturn("overworld:missing");
|
||||||
when(dimension.getLoadFile()).thenReturn(
|
when(dimension.getLoadFile()).thenReturn(
|
||||||
new File("packs/overworld/dimensions/overworld.json"));
|
new File("packs/overworld/dimensions/overworld.json"));
|
||||||
when(dimension.getDimensionTypeKey()).thenReturn("iris:overworld");
|
when(dimension.getDimensionTypeKey()).thenReturn("iris:overworld");
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package art.arcane.iris.core.pack;
|
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.IrisObjectMarker;
|
||||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||||
import art.arcane.iris.engine.object.IrisStaticObject;
|
import art.arcane.iris.engine.object.IrisStaticObject;
|
||||||
@@ -9,6 +11,7 @@ import org.junit.Test;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
@@ -63,6 +66,38 @@ public class PackExportClosureTest {
|
|||||||
PackExportClosure.collectStaticObjectKeys(placements).stream().sorted().toList());
|
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
|
* Source guard: both packagers must export the ambient-spawning graph. Spawner and marker
|
||||||
* folders were silently omitted from exports, leaving dangling entitySpawners references.
|
* 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 write markers/", bukkit.contains("\"markers/\""));
|
||||||
assertTrue("Bukkit packager must export region objects alongside biome objects",
|
assertTrue("Bukkit packager must export region objects alongside biome objects",
|
||||||
bukkit.contains("regions.forEach((r) -> allPlacements.addAll(r.getObjects()))"));
|
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",
|
assertTrue("Bukkit obfuscation must rewrite static object references",
|
||||||
bukkit.contains("placement.setObject(renameObjects.get(placement.getObject()))"));
|
bukkit.contains("placement.setObject(renameObjects.get(placement.getObject()))"));
|
||||||
assertTrue("Bukkit packager must export entity loot tables", bukkit.contains("getLoot().getTables()"));
|
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())");
|
int bukkitValidation = bukkit.indexOf("PackValidator.validateForPackaging(project.getPath())");
|
||||||
assertTrue("Bukkit packager must validate before opening or mutating package state",
|
assertTrue("Bukkit packager must validate before opening or mutating package state",
|
||||||
bukkitValidation >= 0
|
bukkitValidation >= 0
|
||||||
@@ -92,7 +133,14 @@ public class PackExportClosureTest {
|
|||||||
assertTrue("modded packager must include initial spawns", modded.contains("getInitialSpawns"));
|
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 region objects", modded.contains("region.getObjects()"));
|
||||||
assertTrue("modded packager must export static objects",
|
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)");
|
int moddedValidation = modded.indexOf("PackValidator.validateForPackaging(packFolder)");
|
||||||
assertTrue("modded packager must validate before mutating package state",
|
assertTrue("modded packager must validate before mutating package state",
|
||||||
moddedValidation >= 0 && moddedValidation < modded.indexOf("IO.delete(folder)"));
|
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"));
|
"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
|
@Test
|
||||||
public void acceptsUpperDimensionWithoutStandaloneBoundaryWhenParentIsCovered() throws Exception {
|
public void acceptsUpperDimensionWithoutStandaloneBoundaryWhenParentIsCovered() throws Exception {
|
||||||
File pack = createPack("upper-parent-only-boundary", """
|
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")));
|
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
|
@Test
|
||||||
public void existingTargetRemainsUnchanged() throws Exception {
|
public void existingTargetRemainsUnchanged() throws Exception {
|
||||||
Path workspace = temporaryFolder.newFolder("packs").toPath();
|
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)");
|
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) {
|
private static void assertBefore(String source, String first, String second) {
|
||||||
int firstIndex = source.indexOf(first);
|
int firstIndex = source.indexOf(first);
|
||||||
int secondIndex = source.indexOf(second);
|
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.mantle.runtime.Mantle;
|
||||||
import art.arcane.volmlib.util.math.RNG;
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
import art.arcane.volmlib.util.matter.Matter;
|
import art.arcane.volmlib.util.matter.Matter;
|
||||||
|
import art.arcane.volmlib.util.matter.MatterBiomeInject;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Test;
|
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.anyString;
|
||||||
import static org.mockito.ArgumentMatchers.same;
|
import static org.mockito.ArgumentMatchers.same;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class IrisBiomeActuatorCoordinateTest {
|
public class IrisBiomeActuatorCoordinateTest {
|
||||||
@@ -69,6 +72,19 @@ public class IrisBiomeActuatorCoordinateTest {
|
|||||||
assertEquals(List.of("100,-200", "100,-199", "101,-200", "101,-199"), samples);
|
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")
|
@SuppressWarnings("unchecked")
|
||||||
private Engine engine() {
|
private Engine engine() {
|
||||||
Mantle<Matter> mantle = mock(Mantle.class);
|
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.IrisData;
|
||||||
import art.arcane.iris.core.loader.ResourceLoader;
|
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.IrisComplex;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
@@ -22,6 +24,7 @@ import org.junit.Test;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
@@ -301,6 +304,20 @@ public class HintedLocatorTest {
|
|||||||
assertEquals(1, plan.getStrideChunks());
|
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
|
@Test
|
||||||
public void regionPlanMatchesOnlyTargetRegionCells() {
|
public void regionPlanMatchesOnlyTargetRegionCells() {
|
||||||
IrisRegion regionA = region("reg_a");
|
IrisRegion regionA = region("reg_a");
|
||||||
@@ -326,6 +343,24 @@ public class HintedLocatorTest {
|
|||||||
assertFalse(plan.isPossible());
|
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
|
@Test
|
||||||
public void objectPlanPrunesToHostBiomes() {
|
public void objectPlanPrunesToHostBiomes() {
|
||||||
IrisRegion regionA = region("reg_a");
|
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;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class IrisDimensionReachableBiomesTest {
|
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
|
@Test
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public void includesExactRecursiveGenerationClosure() {
|
public void includesExactRecursiveGenerationClosure() {
|
||||||
|
|||||||
@@ -20,4 +20,20 @@ public class IrisDimensionTypeKeyTest {
|
|||||||
|
|
||||||
assertEquals("worlds_my_pack", dimension.getDimensionTypeKey());
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loaded 100 of 119 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user