mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +00:00
Lock-free scopes and caches
- History coordinate scopes read the router and routing flag lock-free - Subterrain planner shares bounded plan and chunk candidate caches - Cave blend roll uses a stateless hash identical to the parallel RNG - Custom biome resource keys are cached per dimension and biome identity
This commit is contained in:
1 parent
e2430500cf
commit
7282a46a26
10 files changed
+395
-81
No files matched your search
@@ -177,7 +177,7 @@ public class IrisEngine implements Engine {
|
|||||||
volatile GenerationHistoryRuntimeRouter generationHistoryRuntimeRouter;
|
volatile GenerationHistoryRuntimeRouter generationHistoryRuntimeRouter;
|
||||||
@Getter(AccessLevel.NONE)
|
@Getter(AccessLevel.NONE)
|
||||||
@Setter(AccessLevel.NONE)
|
@Setter(AccessLevel.NONE)
|
||||||
private boolean generationHistoryRoutingRequired;
|
private volatile boolean generationHistoryRoutingRequired;
|
||||||
@Getter(AccessLevel.NONE)
|
@Getter(AccessLevel.NONE)
|
||||||
@Setter(AccessLevel.NONE)
|
@Setter(AccessLevel.NONE)
|
||||||
volatile EngineTarget publishedTarget;
|
volatile EngineTarget publishedTarget;
|
||||||
@@ -1011,13 +1011,7 @@ public class IrisEngine implements Engine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Optional<GenerationHistoryRuntimeRouter> getGenerationHistoryRuntimeRouter() {
|
public Optional<GenerationHistoryRuntimeRouter> getGenerationHistoryRuntimeRouter() {
|
||||||
GenerationHistoryRuntimeRouter router = generationHistoryRuntimeRouter;
|
return Optional.ofNullable(generationHistoryRuntimeRouter);
|
||||||
if (router != null) {
|
|
||||||
return Optional.of(router);
|
|
||||||
}
|
|
||||||
synchronized (generationHistoryRuntimeRouterLock) {
|
|
||||||
return Optional.ofNullable(generationHistoryRuntimeRouter);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public GenerationHistoryRuntimeRouter.CoordinateScope openGenerationHistoryCoordinateScope(
|
public GenerationHistoryRuntimeRouter.CoordinateScope openGenerationHistoryCoordinateScope(
|
||||||
@@ -1028,13 +1022,14 @@ public class IrisEngine implements Engine {
|
|||||||
if (router != null) {
|
if (router != null) {
|
||||||
return router.openCoordinateScope(blockX, blockZ);
|
return router.openCoordinateScope(blockX, blockZ);
|
||||||
}
|
}
|
||||||
synchronized (generationHistoryRuntimeRouterLock) {
|
if (!generationHistoryRoutingRequired) {
|
||||||
router = generationHistoryRuntimeRouter;
|
return null;
|
||||||
if (router == null && generationHistoryRoutingRequired) {
|
|
||||||
throw new IllegalStateException("Iris generation-history runtime router is detached.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return router == null ? null : router.openCoordinateScope(blockX, blockZ);
|
router = generationHistoryRuntimeRouter;
|
||||||
|
if (router == null) {
|
||||||
|
throw new IllegalStateException("Iris generation-history runtime router is detached.");
|
||||||
|
}
|
||||||
|
return router.openCoordinateScope(blockX, blockZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasGenerationRuntimeScope() {
|
public boolean hasGenerationRuntimeScope() {
|
||||||
|
|||||||
@@ -79,15 +79,20 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
|||||||
private static final int CAVE_BIOME_BLEND_CENTER_WEIGHT = 4;
|
private static final int CAVE_BIOME_BLEND_CENTER_WEIGHT = 4;
|
||||||
private static final int CAVE_BIOME_BLEND_TOTAL_WEIGHT = 8;
|
private static final int CAVE_BIOME_BLEND_TOTAL_WEIGHT = 8;
|
||||||
private static final int SUBMERGED_FLOOR_SUBSTRATE_DEPTH = 8;
|
private static final int SUBMERGED_FLOOR_SUBSTRATE_DEPTH = 8;
|
||||||
|
private static final long RANDOM_MULTIPLIER = 0x5DEECE66DL;
|
||||||
|
private static final long RANDOM_ADDEND = 0xBL;
|
||||||
|
private static final long RANDOM_MASK = (1L << 48) - 1;
|
||||||
private static final MatterCavern BASIC_CAVERN = new MatterCavern(true, "", (byte) 0);
|
private static final MatterCavern BASIC_CAVERN = new MatterCavern(true, "", (byte) 0);
|
||||||
private final RNG rng;
|
private final RNG rng;
|
||||||
|
private final long caveBlendSeed;
|
||||||
private final NativeBlockState AIR = B.getState("CAVE_AIR");
|
private final NativeBlockState AIR = B.getState("CAVE_AIR");
|
||||||
private final NativeBlockState LAVA = B.getState("LAVA");
|
private final NativeBlockState LAVA = B.getState("LAVA");
|
||||||
private final IrisDecorantActuator decorant;
|
private final IrisDecorantActuator decorant;
|
||||||
|
|
||||||
public IrisCarveModifier(Engine engine) {
|
public IrisCarveModifier(Engine engine) {
|
||||||
super(engine, "Carve");
|
super(engine, "Carve");
|
||||||
rng = new RNG(getEngine().getSeedManager().getCarve());
|
caveBlendSeed = getEngine().getSeedManager().getCarve();
|
||||||
|
rng = new RNG(caveBlendSeed);
|
||||||
decorant = new IrisDecorantActuator(engine);
|
decorant = new IrisDecorantActuator(engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1233,7 +1238,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
int roll = Math.floorMod(rng.nextParallelRNG(BlockPosition.toLong(x, y, z)).nextInt(), CAVE_BIOME_BLEND_TOTAL_WEIGHT);
|
int roll = Math.floorMod(caveBlendRandom(caveBlendSeed, x, y, z), CAVE_BIOME_BLEND_TOTAL_WEIGHT);
|
||||||
if (roll < CAVE_BIOME_BLEND_CENTER_WEIGHT) {
|
if (roll < CAVE_BIOME_BLEND_CENTER_WEIGHT) {
|
||||||
return center;
|
return center;
|
||||||
}
|
}
|
||||||
@@ -1247,6 +1252,11 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
|||||||
return neighbor != null ? neighbor : center;
|
return neighbor != null ? neighbor : center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int caveBlendRandom(long seed, int x, int y, int z) {
|
||||||
|
long scrambled = ((seed + BlockPosition.toLong(x, y, z)) ^ RANDOM_MULTIPLIER) & RANDOM_MASK;
|
||||||
|
return (int) (((scrambled * RANDOM_MULTIPLIER + RANDOM_ADDEND) & RANDOM_MASK) >>> 16);
|
||||||
|
}
|
||||||
|
|
||||||
private IrisBiome sampleCaveBiome(Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, int x, int y, int z, CaveInputs resolverState) {
|
private IrisBiome sampleCaveBiome(Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, int x, int y, int z, CaveInputs resolverState) {
|
||||||
long key = BlockPosition.toLong(x, y, z);
|
long key = BlockPosition.toLong(x, y, z);
|
||||||
IrisBiome cachedBiome = caveBiomeCache.get(key);
|
IrisBiome cachedBiome = caveBiomeCache.get(key);
|
||||||
|
|||||||
@@ -6,14 +6,21 @@ import java.util.HashSet;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicReferenceArray;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
public final class SubterrainPlanner {
|
public final class SubterrainPlanner {
|
||||||
|
private static final int PLAN_CACHE_SLOTS = 4096;
|
||||||
|
private static final int TILE_CACHE_SLOTS = 1024;
|
||||||
|
private static final Comparator<SubterrainPlan> PLAN_ORDER =
|
||||||
|
Comparator.comparingInt(SubterrainPlan::priority).reversed().thenComparing(SubterrainPlan::id);
|
||||||
|
|
||||||
private final List<Definition> definitions;
|
private final List<Definition> definitions;
|
||||||
private final long seed;
|
private final long seed;
|
||||||
private final int minY;
|
private final int minY;
|
||||||
private final int maxYExclusive;
|
private final int maxYExclusive;
|
||||||
private final ThreadLocal<CandidateTile> sampleTile = new ThreadLocal<>();
|
private final AtomicReferenceArray<CachedPlan> planCache = new AtomicReferenceArray<>(PLAN_CACHE_SLOTS);
|
||||||
|
private final AtomicReferenceArray<CandidateTile> tileCache = new AtomicReferenceArray<>(TILE_CACHE_SLOTS);
|
||||||
private final Predicate<SubterrainBounds> allowedBounds;
|
private final Predicate<SubterrainBounds> allowedBounds;
|
||||||
private final RegionResolver regionResolver;
|
private final RegionResolver regionResolver;
|
||||||
|
|
||||||
@@ -70,8 +77,9 @@ public final class SubterrainPlanner {
|
|||||||
if (minX > maxX || minZ > maxZ) {
|
if (minX > maxX || minZ > maxZ) {
|
||||||
throw new IllegalArgumentException("Subterrain query bounds are inverted");
|
throw new IllegalArgumentException("Subterrain query bounds are inverted");
|
||||||
}
|
}
|
||||||
List<SubterrainPlan> result = new ArrayList<>();
|
List<SubterrainPlan> result = null;
|
||||||
for (Definition definition : definitions) {
|
for (int definitionIndex = 0; definitionIndex < definitions.size(); definitionIndex++) {
|
||||||
|
Definition definition = definitions.get(definitionIndex);
|
||||||
long firstX = Math.floorDiv(minX, definition.spacing()) - 1L;
|
long firstX = Math.floorDiv(minX, definition.spacing()) - 1L;
|
||||||
long lastX = Math.floorDiv(maxX, definition.spacing()) + 1L;
|
long lastX = Math.floorDiv(maxX, definition.spacing()) + 1L;
|
||||||
long firstZ = Math.floorDiv(minZ, definition.spacing()) - 1L;
|
long firstZ = Math.floorDiv(minZ, definition.spacing()) - 1L;
|
||||||
@@ -81,14 +89,20 @@ public final class SubterrainPlanner {
|
|||||||
}
|
}
|
||||||
for (long gridX = firstX; gridX <= lastX; gridX++) {
|
for (long gridX = firstX; gridX <= lastX; gridX++) {
|
||||||
for (long gridZ = firstZ; gridZ <= lastZ; gridZ++) {
|
for (long gridZ = firstZ; gridZ <= lastZ; gridZ++) {
|
||||||
SubterrainPlan plan = create(definition, gridX, gridZ);
|
SubterrainPlan plan = plan(definitionIndex, definition, gridX, gridZ);
|
||||||
if (plan != null && plan.bounds().intersects(minX, minZ, maxX, maxZ)) {
|
if (plan != null && plan.bounds().intersects(minX, minZ, maxX, maxZ)) {
|
||||||
|
if (result == null) {
|
||||||
|
result = new ArrayList<>();
|
||||||
|
}
|
||||||
result.add(plan);
|
result.add(plan);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.sort(Comparator.comparingInt(SubterrainPlan::priority).reversed().thenComparing(SubterrainPlan::id));
|
if (result == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
result.sort(PLAN_ORDER);
|
||||||
return List.copyOf(result);
|
return List.copyOf(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,20 +110,7 @@ public final class SubterrainPlanner {
|
|||||||
if (isEmpty() || y < minY || y >= maxYExclusive) {
|
if (isEmpty() || y < minY || y >= maxYExclusive) {
|
||||||
return SubterrainCell.OUTSIDE;
|
return SubterrainCell.OUTSIDE;
|
||||||
}
|
}
|
||||||
int chunkX = Math.floorDiv(x, 16);
|
return sample(candidates(Math.floorDiv(x, 16), Math.floorDiv(z, 16)), x, y, z);
|
||||||
int chunkZ = Math.floorDiv(z, 16);
|
|
||||||
CandidateTile tile = sampleTile.get();
|
|
||||||
if (tile == null || tile.chunkX() != chunkX || tile.chunkZ() != chunkZ) {
|
|
||||||
long baseX = (long) chunkX * 16;
|
|
||||||
long baseZ = (long) chunkZ * 16;
|
|
||||||
tile = new CandidateTile(chunkX, chunkZ, plansForBounds(
|
|
||||||
(int) Math.max(Integer.MIN_VALUE, baseX - 1),
|
|
||||||
(int) Math.max(Integer.MIN_VALUE, baseZ - 1),
|
|
||||||
(int) Math.min(Integer.MAX_VALUE, baseX + 16),
|
|
||||||
(int) Math.min(Integer.MAX_VALUE, baseZ + 16)));
|
|
||||||
sampleTile.set(tile);
|
|
||||||
}
|
|
||||||
return sample(tile.plans(), x, y, z);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public SubterrainCell sample(List<SubterrainPlan> candidates, int x, int y, int z) {
|
public SubterrainCell sample(List<SubterrainPlan> candidates, int x, int y, int z) {
|
||||||
@@ -127,10 +128,38 @@ public final class SubterrainPlanner {
|
|||||||
return selected;
|
return selected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<SubterrainPlan> candidates(int chunkX, int chunkZ) {
|
||||||
|
int slot = (int) mix(((long) chunkX << 32) ^ (chunkZ & 0xffffffffL)) & (TILE_CACHE_SLOTS - 1);
|
||||||
|
CandidateTile tile = tileCache.getAcquire(slot);
|
||||||
|
if (tile != null && tile.chunkX() == chunkX && tile.chunkZ() == chunkZ) {
|
||||||
|
return tile.plans();
|
||||||
|
}
|
||||||
|
long baseX = (long) chunkX * 16;
|
||||||
|
long baseZ = (long) chunkZ * 16;
|
||||||
|
List<SubterrainPlan> plans = plansForBounds(
|
||||||
|
(int) Math.max(Integer.MIN_VALUE, baseX - 1),
|
||||||
|
(int) Math.max(Integer.MIN_VALUE, baseZ - 1),
|
||||||
|
(int) Math.min(Integer.MAX_VALUE, baseX + 16),
|
||||||
|
(int) Math.min(Integer.MAX_VALUE, baseZ + 16));
|
||||||
|
tileCache.setRelease(slot, new CandidateTile(chunkX, chunkZ, plans));
|
||||||
|
return plans;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SubterrainPlan plan(int definitionIndex, Definition definition, long gridX, long gridZ) {
|
||||||
|
int slot = (int) mix(mix(gridX ^ definition.seedSalt()) ^ Long.rotateLeft(gridZ, 29)) & (PLAN_CACHE_SLOTS - 1);
|
||||||
|
CachedPlan cached = planCache.getAcquire(slot);
|
||||||
|
if (cached != null && cached.definition() == definitionIndex && cached.gridX() == gridX && cached.gridZ() == gridZ) {
|
||||||
|
return cached.plan();
|
||||||
|
}
|
||||||
|
SubterrainPlan plan = create(definition, gridX, gridZ);
|
||||||
|
planCache.setRelease(slot, new CachedPlan(definitionIndex, gridX, gridZ, plan));
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
private SubterrainCell raw(List<SubterrainPlan> candidates, int x, int y, int z) {
|
private SubterrainCell raw(List<SubterrainPlan> candidates, int x, int y, int z) {
|
||||||
SubterrainCell selected = SubterrainCell.OUTSIDE;
|
SubterrainCell selected = SubterrainCell.OUTSIDE;
|
||||||
for (SubterrainPlan plan : candidates) {
|
for (int index = 0; index < candidates.size(); index++) {
|
||||||
SubterrainCell cell = plan.sample(x, y, z);
|
SubterrainCell cell = candidates.get(index).sample(x, y, z);
|
||||||
if (cell.solid() && cell.room().reservedSolid()) {
|
if (cell.solid() && cell.room().reservedSolid()) {
|
||||||
return cell;
|
return cell;
|
||||||
}
|
}
|
||||||
@@ -212,6 +241,9 @@ public final class SubterrainPlanner {
|
|||||||
private record CandidateTile(int chunkX, int chunkZ, List<SubterrainPlan> plans) {
|
private record CandidateTile(int chunkX, int chunkZ, List<SubterrainPlan> plans) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record CachedPlan(int definition, long gridX, long gridZ, SubterrainPlan plan) {
|
||||||
|
}
|
||||||
|
|
||||||
record Definition(String id, IrisSubterrainFamily family, IrisSubterrainFluid fluid, String biome, int spacing, double probability,
|
record Definition(String id, IrisSubterrainFamily family, IrisSubterrainFluid fluid, String biome, int spacing, double probability,
|
||||||
int length, int radius, int height, double shapeWarp, int fluidDepth, int chimneyHeight, int terraceCount,
|
int length, int radius, int height, double shapeWarp, int fluidDepth, int chimneyHeight, int terraceCount,
|
||||||
int pillarSpacing, double formationFraction, String solid, int priority,
|
int pillarSpacing, double formationFraction, String solid, int priority,
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||||
|
private static final int CUSTOM_BIOME_KEY_CACHE_LIMIT = 4096;
|
||||||
private static final Map<File, IrisData> dataLoaders = new ConcurrentHashMap<>();
|
private static final Map<File, IrisData> dataLoaders = new ConcurrentHashMap<>();
|
||||||
// Loaders (cached or detached) that currently have registered engines; see hasActiveEngines.
|
// Loaders (cached or detached) that currently have registered engines; see hasActiveEngines.
|
||||||
// Identity-keyed on purpose: Lombok's @Data hashCode over this class's mutable loader state
|
// Identity-keyed on purpose: Lombok's @Data hashCode over this class's mutable loader state
|
||||||
@@ -159,6 +160,10 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
|||||||
@Setter(AccessLevel.NONE)
|
@Setter(AccessLevel.NONE)
|
||||||
@EqualsAndHashCode.Exclude
|
@EqualsAndHashCode.Exclude
|
||||||
private transient volatile GenerationRegistryContractFactory.CustomBiomeResourceResolver customBiomeResourceResolver;
|
private transient volatile GenerationRegistryContractFactory.CustomBiomeResourceResolver customBiomeResourceResolver;
|
||||||
|
@Getter(AccessLevel.NONE)
|
||||||
|
@Setter(AccessLevel.NONE)
|
||||||
|
@EqualsAndHashCode.Exclude
|
||||||
|
private transient volatile CustomBiomeKeyCache customBiomeKeyCache;
|
||||||
private transient volatile Engine soleEngine;
|
private transient volatile Engine soleEngine;
|
||||||
@Getter(AccessLevel.NONE)
|
@Getter(AccessLevel.NONE)
|
||||||
@Setter(AccessLevel.NONE)
|
@Setter(AccessLevel.NONE)
|
||||||
@@ -234,6 +239,32 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
|||||||
IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension");
|
IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension");
|
||||||
IrisBiomeCustom requiredBiome = Objects.requireNonNull(customBiome, "customBiome");
|
IrisBiomeCustom requiredBiome = Objects.requireNonNull(customBiome, "customBiome");
|
||||||
GenerationRegistryContractFactory.CustomBiomeResourceResolver resolver = customBiomeResourceResolver;
|
GenerationRegistryContractFactory.CustomBiomeResourceResolver resolver = customBiomeResourceResolver;
|
||||||
|
ContentGate gate = resolver == null ? getContentGate() : null;
|
||||||
|
Object owner = resolver == null ? gate : resolver;
|
||||||
|
CustomBiomeKeyCache cache = customBiomeKeyCache;
|
||||||
|
if (cache == null || cache.owner() != owner) {
|
||||||
|
cache = new CustomBiomeKeyCache(owner, new ConcurrentHashMap<>());
|
||||||
|
customBiomeKeyCache = cache;
|
||||||
|
}
|
||||||
|
CustomBiomeKey key = new CustomBiomeKey(requiredDimension.getLoadKey(), requiredBiome);
|
||||||
|
String cached = cache.keys().get(key);
|
||||||
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
String resolved = resolveCustomBiomeResourceKey(requiredDimension, requiredBiome, resolver, gate);
|
||||||
|
if (cache.keys().size() >= CUSTOM_BIOME_KEY_CACHE_LIMIT) {
|
||||||
|
cache.keys().clear();
|
||||||
|
}
|
||||||
|
cache.keys().put(key, resolved);
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveCustomBiomeResourceKey(
|
||||||
|
IrisDimension requiredDimension,
|
||||||
|
IrisBiomeCustom requiredBiome,
|
||||||
|
GenerationRegistryContractFactory.CustomBiomeResourceResolver resolver,
|
||||||
|
ContentGate gate
|
||||||
|
) {
|
||||||
if (resolver != null) {
|
if (resolver != null) {
|
||||||
try {
|
try {
|
||||||
return resolver.resolve(
|
return resolver.resolve(
|
||||||
@@ -248,7 +279,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
|||||||
return GenerationRegistryContractFactory.customBiomeResourceKey(
|
return GenerationRegistryContractFactory.customBiomeResourceKey(
|
||||||
requiredDimension.getLoadKey(),
|
requiredDimension.getLoadKey(),
|
||||||
requiredBiome,
|
requiredBiome,
|
||||||
getContentGate()
|
gate
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1035,4 +1066,29 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
|||||||
return immutable;
|
return immutable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record CustomBiomeKeyCache(Object owner, ConcurrentHashMap<CustomBiomeKey, String> keys) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class CustomBiomeKey {
|
||||||
|
private final String dimensionKey;
|
||||||
|
private final IrisBiomeCustom biome;
|
||||||
|
private final int hash;
|
||||||
|
|
||||||
|
private CustomBiomeKey(String dimensionKey, IrisBiomeCustom biome) {
|
||||||
|
this.dimensionKey = dimensionKey;
|
||||||
|
this.biome = biome;
|
||||||
|
hash = 31 * Objects.hashCode(dimensionKey) + System.identityHashCode(biome);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other) {
|
||||||
|
return other instanceof CustomBiomeKey key && key.biome == biome && Objects.equals(key.dimensionKey, dimensionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+15
-43
@@ -8,7 +8,6 @@ import art.arcane.iris.generation.runtime.GenerationRuntime.BiomeMaxes;
|
|||||||
import art.arcane.iris.world.history.SavedBiomeRuntime;
|
import art.arcane.iris.world.history.SavedBiomeRuntime;
|
||||||
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
|
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
|
||||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||||
import art.arcane.iris.testsupport.Await;
|
|
||||||
import art.arcane.iris.testsupport.PlatformBinding;
|
import art.arcane.iris.testsupport.PlatformBinding;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@@ -37,13 +36,11 @@ import java.io.IOException;
|
|||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.IdentityHashMap;
|
import java.util.IdentityHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CountDownLatch;
|
|
||||||
import java.util.concurrent.CopyOnWriteArraySet;
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.FutureTask;
|
import java.util.concurrent.FutureTask;
|
||||||
@@ -680,30 +677,19 @@ public class IrisEngineGenerationRuntimeScopeTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void missingRouterReadWaitsForAttachmentPublication() throws Exception {
|
public void missingRouterReadsDoNotWaitForTheAttachmentMonitor() throws Exception {
|
||||||
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
|
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
|
||||||
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
|
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
|
||||||
GenerationHistoryRuntimeRouter router = historyRouter(engine);
|
ExecutorService reader = Executors.newSingleThreadExecutor();
|
||||||
GenerationHistoryRuntimeRouter.CoordinateScope scope = mock(GenerationHistoryRuntimeRouter.CoordinateScope.class);
|
|
||||||
when(router.openCoordinateScope(-17, 32)).thenReturn(scope);
|
|
||||||
CountDownLatch started = new CountDownLatch(1);
|
|
||||||
FutureTask<GenerationHistoryRuntimeRouter.CoordinateScope> read = new FutureTask<>(() -> {
|
|
||||||
started.countDown();
|
|
||||||
return engine.openGenerationHistoryCoordinateScope(-17, 32);
|
|
||||||
});
|
|
||||||
Thread reader = new Thread(read, "Pending history attachment");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
synchronized (engine.generationHistoryRuntimeRouterLock) {
|
synchronized (engine.generationHistoryRuntimeRouterLock) {
|
||||||
reader.start();
|
assertNull(reader.submit(() -> engine.openGenerationHistoryCoordinateScope(-17, 32))
|
||||||
assertTrue(started.await(5, TimeUnit.SECONDS));
|
.get(5, TimeUnit.SECONDS));
|
||||||
awaitBlocked(reader);
|
assertTrue(reader.submit(engine::getGenerationHistoryRuntimeRouter).get(5, TimeUnit.SECONDS).isEmpty());
|
||||||
engine.attachGenerationHistoryRuntimeRouter(router);
|
|
||||||
}
|
}
|
||||||
assertSame(scope, read.get(5, TimeUnit.SECONDS));
|
|
||||||
} finally {
|
} finally {
|
||||||
reader.join(5_000L);
|
reader.shutdownNow();
|
||||||
assertTrue("Pending router read did not finish", !reader.isAlive());
|
assertTrue(reader.awaitTermination(5, TimeUnit.SECONDS));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -713,27 +699,19 @@ public class IrisEngineGenerationRuntimeScopeTest {
|
|||||||
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
|
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
|
||||||
GenerationHistoryRuntimeRouter router = historyRouter(engine);
|
GenerationHistoryRuntimeRouter router = historyRouter(engine);
|
||||||
engine.attachGenerationHistoryRuntimeRouter(router);
|
engine.attachGenerationHistoryRuntimeRouter(router);
|
||||||
CountDownLatch started = new CountDownLatch(1);
|
engine.detachGenerationHistoryRuntimeRouter(router);
|
||||||
FutureTask<GenerationHistoryRuntimeRouter.CoordinateScope> read = new FutureTask<>(() -> {
|
ExecutorService reader = Executors.newSingleThreadExecutor();
|
||||||
started.countDown();
|
|
||||||
return engine.openGenerationHistoryCoordinateScope(-17, 32);
|
|
||||||
});
|
|
||||||
Thread reader = new Thread(read, "Detached history read");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
synchronized (engine.generationHistoryRuntimeRouterLock) {
|
synchronized (engine.generationHistoryRuntimeRouterLock) {
|
||||||
engine.detachGenerationHistoryRuntimeRouter(router);
|
ExecutionException failure = assertThrows(ExecutionException.class, () -> reader.submit(
|
||||||
reader.start();
|
() -> engine.openGenerationHistoryCoordinateScope(-17, 32)).get(5, TimeUnit.SECONDS));
|
||||||
assertTrue(started.await(5, TimeUnit.SECONDS));
|
assertTrue(failure.getCause() instanceof IllegalStateException);
|
||||||
awaitBlocked(reader);
|
assertEquals("Iris generation-history runtime router is detached.", failure.getCause().getMessage());
|
||||||
}
|
}
|
||||||
ExecutionException failure = assertThrows(ExecutionException.class, () -> read.get(5, TimeUnit.SECONDS));
|
|
||||||
assertTrue(failure.getCause() instanceof IllegalStateException);
|
|
||||||
assertEquals("Iris generation-history runtime router is detached.", failure.getCause().getMessage());
|
|
||||||
assertTrue(engine.getGenerationHistoryRuntimeRouter().isEmpty());
|
assertTrue(engine.getGenerationHistoryRuntimeRouter().isEmpty());
|
||||||
} finally {
|
} finally {
|
||||||
reader.join(5_000L);
|
reader.shutdownNow();
|
||||||
assertTrue("Detached router read did not finish", !reader.isAlive());
|
assertTrue(reader.awaitTermination(5, TimeUnit.SECONDS));
|
||||||
}
|
}
|
||||||
verify(router, never()).openCoordinateScope(anyInt(), anyInt());
|
verify(router, never()).openCoordinateScope(anyInt(), anyInt());
|
||||||
}
|
}
|
||||||
@@ -909,12 +887,6 @@ public class IrisEngineGenerationRuntimeScopeTest {
|
|||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void awaitBlocked(Thread thread) {
|
|
||||||
Await.reached("the reader to block on the engine lifecycle lock", Duration.ofSeconds(5L),
|
|
||||||
() -> thread.getState() == Thread.State.BLOCKED || !thread.isAlive());
|
|
||||||
assertEquals(Thread.State.BLOCKED, thread.getState());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IrisEngine engine(
|
private static IrisEngine engine(
|
||||||
GenerationRuntime generationRuntime,
|
GenerationRuntime generationRuntime,
|
||||||
EngineEffects effects,
|
EngineEffects effects,
|
||||||
|
|||||||
+3
@@ -132,6 +132,9 @@ public class IrisCarveModifierBiomeBlendTest {
|
|||||||
Field field = IrisCarveModifier.class.getDeclaredField("rng");
|
Field field = IrisCarveModifier.class.getDeclaredField("rng");
|
||||||
field.setAccessible(true);
|
field.setAccessible(true);
|
||||||
field.set(modifier, rng);
|
field.set(modifier, rng);
|
||||||
|
Field seed = IrisCarveModifier.class.getDeclaredField("caveBlendSeed");
|
||||||
|
seed.setAccessible(true);
|
||||||
|
seed.setLong(modifier, SEED);
|
||||||
return modifier;
|
return modifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
@@ -56,6 +56,9 @@ public class IrisCarveModifierBoundarySupportTest {
|
|||||||
Field rng = IrisCarveModifier.class.getDeclaredField("rng");
|
Field rng = IrisCarveModifier.class.getDeclaredField("rng");
|
||||||
rng.setAccessible(true);
|
rng.setAccessible(true);
|
||||||
rng.set(modifier, new RNG(71L));
|
rng.set(modifier, new RNG(71L));
|
||||||
|
Field seed = IrisCarveModifier.class.getDeclaredField("caveBlendSeed");
|
||||||
|
seed.setAccessible(true);
|
||||||
|
seed.setLong(modifier, 71L);
|
||||||
|
|
||||||
IrisMatterSupport.ensureRegistered();
|
IrisMatterSupport.ensureRegistered();
|
||||||
Matter floorMatter = new IrisMatter(16, 16, 16);
|
Matter floorMatter = new IrisMatter(16, 16, 16);
|
||||||
|
|||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
package art.arcane.iris.generation.stage;
|
||||||
|
|
||||||
|
import art.arcane.volmlib.util.math.BlockPosition;
|
||||||
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.SplittableRandom;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
public class IrisCarveModifierCaveBlendRollTest {
|
||||||
|
private static final int SAMPLES = 1_000_000;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void statelessBlendRandomMatchesParallelRngForOneMillionPoints() {
|
||||||
|
SplittableRandom random = new SplittableRandom(0x5eed_cafe_f00dL);
|
||||||
|
long[] seeds = {0L, 1L, -1L, 1337L, Long.MIN_VALUE, Long.MAX_VALUE, 0x5DEECE66DL, random.nextLong()};
|
||||||
|
for (int sample = 0; sample < SAMPLES; sample++) {
|
||||||
|
long seed = sample < seeds.length * 64 ? seeds[sample % seeds.length] : random.nextLong();
|
||||||
|
int x = coordinate(random, sample);
|
||||||
|
int y = sample % 7 == 0 ? random.nextInt() : random.nextInt(-2048, 2048);
|
||||||
|
int z = coordinate(random, sample + 3);
|
||||||
|
assertEquals(seed + " @ " + x + "," + y + "," + z,
|
||||||
|
new RNG(seed).nextParallelRNG(BlockPosition.toLong(x, y, z)).nextInt(),
|
||||||
|
IrisCarveModifier.caveBlendRandom(seed, x, y, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void statelessBlendRandomMatchesParallelRngAtCoordinateExtremes() {
|
||||||
|
int[] edges = {Integer.MIN_VALUE, Integer.MIN_VALUE + 1, -33_554_433, -33_554_432, -1, 0, 1,
|
||||||
|
33_554_431, 33_554_432, Integer.MAX_VALUE - 1, Integer.MAX_VALUE};
|
||||||
|
long[] seeds = {0L, -1L, 1337L, Long.MIN_VALUE, Long.MAX_VALUE};
|
||||||
|
for (long seed : seeds) {
|
||||||
|
for (int x : edges) {
|
||||||
|
for (int y : edges) {
|
||||||
|
for (int z : edges) {
|
||||||
|
assertEquals(new RNG(seed).nextParallelRNG(BlockPosition.toLong(x, y, z)).nextInt(),
|
||||||
|
IrisCarveModifier.caveBlendRandom(seed, x, y, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int coordinate(SplittableRandom random, int sample) {
|
||||||
|
return switch (sample % 4) {
|
||||||
|
case 0 -> random.nextInt();
|
||||||
|
case 1 -> random.nextInt(-30_000_000, 30_000_000);
|
||||||
|
default -> random.nextInt(-65_536, 65_536);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package art.arcane.iris.generation.subterrain;
|
package art.arcane.iris.generation.subterrain;
|
||||||
|
|
||||||
import art.arcane.iris.pack.value.IrisRange;
|
import art.arcane.iris.pack.value.IrisRange;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -10,6 +11,11 @@ import java.util.Collections;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
@@ -441,6 +447,74 @@ public class SubterrainPlannerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void alternatingChunkSamplingResolvesEachCandidateCellOnce() {
|
||||||
|
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.CENOTE).setAllowedRegions(new KList<>("test-region"));
|
||||||
|
AtomicInteger regionQueries = new AtomicInteger();
|
||||||
|
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 7123, -64, 97,
|
||||||
|
(x, z) -> {
|
||||||
|
regionQueries.incrementAndGet();
|
||||||
|
return "test-region";
|
||||||
|
}));
|
||||||
|
for (int round = 0; round < 500; round++) {
|
||||||
|
planner.sample(7, 10, 7);
|
||||||
|
planner.sample(23, 10, 7);
|
||||||
|
planner.sample(7, 10, 23);
|
||||||
|
planner.sample(-9, 10, -9);
|
||||||
|
}
|
||||||
|
int firstRound = regionQueries.get();
|
||||||
|
assertTrue("candidate cells resolved " + firstRound + " times", firstRound <= 16);
|
||||||
|
for (int round = 0; round < 500; round++) {
|
||||||
|
planner.plansForBounds(-1, -1, 16, 16);
|
||||||
|
planner.plansForBounds(15, 15, 32, 32);
|
||||||
|
}
|
||||||
|
assertEquals(firstRound, regionQueries.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void concurrentCrossChunkSamplingMatchesAFreshPlanner() throws Exception {
|
||||||
|
List<IrisSubterrainFeature> features = List.of(compactFeature(IrisSubterrainFamily.CENOTE).setSpacing(96),
|
||||||
|
compactFeature(IrisSubterrainFamily.TRAVERTINE_TERRACES).setSpacing(160));
|
||||||
|
SubterrainPlanner shared = new SubterrainPlanner(new SubterrainPlanner.Options(features, 4242, -64, 97, (x, z) -> "test-region"));
|
||||||
|
SubterrainPlanner reference = new SubterrainPlanner(new SubterrainPlanner.Options(features, 4242, -64, 97, (x, z) -> "test-region"));
|
||||||
|
int threads = 8;
|
||||||
|
ExecutorService pool = Executors.newFixedThreadPool(threads);
|
||||||
|
try {
|
||||||
|
List<Future<Integer>> results = new ArrayList<>();
|
||||||
|
for (int thread = 0; thread < threads; thread++) {
|
||||||
|
int offset = thread;
|
||||||
|
results.add(pool.submit(() -> {
|
||||||
|
int occupied = 0;
|
||||||
|
for (int step = 0; step < 6000; step++) {
|
||||||
|
int x = Math.floorMod(step * 37 + offset * 101, 2400) - 1200;
|
||||||
|
int z = Math.floorMod(step * 53 + offset * 211, 2400) - 1200;
|
||||||
|
int y = Math.floorMod(step * 11 + offset, 150) - 60;
|
||||||
|
int chunkX = Math.floorDiv(x, 16) * 16;
|
||||||
|
int chunkZ = Math.floorDiv(z, 16) * 16;
|
||||||
|
SubterrainCell expected;
|
||||||
|
synchronized (reference) {
|
||||||
|
expected = reference.sample(reference.plansForBounds(chunkX - 1, chunkZ - 1, chunkX + 16, chunkZ + 16), x, y, z);
|
||||||
|
}
|
||||||
|
SubterrainCell actual = shared.sample(x, y, z);
|
||||||
|
assertEquals(expected, actual);
|
||||||
|
if (actual.occupied()) {
|
||||||
|
occupied++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return occupied;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
int occupied = 0;
|
||||||
|
for (Future<Integer> result : results) {
|
||||||
|
occupied += result.get(60, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
assertTrue(occupied > 0);
|
||||||
|
} finally {
|
||||||
|
pool.shutdownNow();
|
||||||
|
assertTrue(pool.awaitTermination(10, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void immutableSnapshotsAndSeedsControlPlans() {
|
public void immutableSnapshotsAndSeedsControlPlans() {
|
||||||
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.CENOTE);
|
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.CENOTE);
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package art.arcane.iris.pack.loading;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.biome.IrisBiomeCustom;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
|
import art.arcane.iris.testsupport.DurabilityMode;
|
||||||
|
import art.arcane.iris.world.history.GenerationRegistryContractFactory;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class IrisDataCustomBiomeKeyTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void repeatedConcurrentLookupsMatchTheContentAddressedKeyPerDimension() throws Exception {
|
||||||
|
Path pack = temporaryFolder.newFolder("custom-keys").toPath().resolve("iris/pack");
|
||||||
|
writePack(pack);
|
||||||
|
IrisData data = IrisData.openDatapackCompiler(pack.toFile());
|
||||||
|
ExecutorService pool = Executors.newFixedThreadPool(8);
|
||||||
|
try {
|
||||||
|
IrisDimension overworld = data.getDimensionLoader().load("overworld");
|
||||||
|
IrisDimension second = data.getDimensionLoader().load("second");
|
||||||
|
List<IrisBiomeCustom> customs = data.getBiomeLoader().load("test").getCustomDerivitives();
|
||||||
|
assertEquals(2, customs.size());
|
||||||
|
List<String> expected = new ArrayList<>();
|
||||||
|
for (IrisDimension dimension : List.of(overworld, second)) {
|
||||||
|
for (IrisBiomeCustom custom : customs) {
|
||||||
|
expected.add(GenerationRegistryContractFactory.customBiomeResourceKey(
|
||||||
|
dimension.getLoadKey(), custom, data.getContentGate()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertEquals(4, expected.stream().distinct().count());
|
||||||
|
List<Future<List<String>>> results = new ArrayList<>();
|
||||||
|
for (int thread = 0; thread < 8; thread++) {
|
||||||
|
results.add(pool.submit(() -> {
|
||||||
|
List<String> seen = new ArrayList<>();
|
||||||
|
for (int round = 0; round < 200; round++) {
|
||||||
|
seen.clear();
|
||||||
|
for (IrisDimension dimension : List.of(overworld, second)) {
|
||||||
|
for (IrisBiomeCustom custom : customs) {
|
||||||
|
seen.add(data.customBiomeResourceKey(dimension, custom));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(seen);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
for (Future<List<String>> result : results) {
|
||||||
|
assertEquals(expected, result.get(60, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
IrisBiomeCustom equalCopy = new IrisBiomeCustom().setId(customs.getFirst().getId());
|
||||||
|
assertEquals(GenerationRegistryContractFactory.customBiomeResourceKey(
|
||||||
|
overworld.getLoadKey(), equalCopy, data.getContentGate()),
|
||||||
|
data.customBiomeResourceKey(overworld, equalCopy));
|
||||||
|
assertNotEquals(data.customBiomeResourceKey(overworld, customs.getFirst()),
|
||||||
|
data.customBiomeResourceKey(second, customs.getFirst()));
|
||||||
|
} finally {
|
||||||
|
pool.shutdownNow();
|
||||||
|
assertTrue(pool.awaitTermination(10, TimeUnit.SECONDS));
|
||||||
|
data.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writePack(Path root) throws Exception {
|
||||||
|
Files.createDirectories(root.resolve("dimensions"));
|
||||||
|
Files.createDirectories(root.resolve("biomes"));
|
||||||
|
for (String dimension : List.of("overworld", "second")) {
|
||||||
|
Files.writeString(root.resolve("dimensions/" + dimension + ".json"), """
|
||||||
|
{
|
||||||
|
"name": "Test Dimension",
|
||||||
|
"environment": "NORMAL",
|
||||||
|
"logicalHeight": 256,
|
||||||
|
"dimensionHeight": {
|
||||||
|
"min": -64,
|
||||||
|
"max": 320
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""", StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
Files.writeString(root.resolve("biomes/test.json"), """
|
||||||
|
{
|
||||||
|
"name": "Test Biome",
|
||||||
|
"derivative": "minecraft:plains",
|
||||||
|
"vanillaDerivative": "minecraft:plains",
|
||||||
|
"customDerivitives": [
|
||||||
|
{
|
||||||
|
"id": "first_custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "second_custom",
|
||||||
|
"temperature": 1.5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""", StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user