Add subterrain features

- Generate authored underground rooms, materials, fluids, and formations.
- Preserve underground biome ownership across generation and saved terrain.
- Add subterrain searches and regression coverage.
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-04 09:59:57 -04:00
1 parent be0d3b0156
commit 5858eb366d
76 files changed
+4187 -201

No files matched your search

@@ -325,7 +325,7 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
private H getNoiseBiome(Engine engine, int quartX, int quartY, int quartZ,
S sampler) {
if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) {
if (isGuaranteedSurfaceBiome(engine, quartY)) {
return getSurfaceStructureBiome(engine, quartX, quartZ, sampler);
}
boolean cacheable = isBiomeCacheable(
@@ -512,7 +512,7 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
}
boolean monumentQuery = isMonumentSurfaceBiomeQuery(
y, radius, engine.getMinHeight(), engine.getDimension().getFluidHeight());
surfaceQuery = monumentQuery || isGuaranteedSurfaceBiome(minQuartY, engine.getMinHeight());
surfaceQuery = monumentQuery || isGuaranteedSurfaceBiome(engine, minQuartY);
}
if (!surfaceQuery) {
return query.fallback().get();
@@ -701,6 +701,11 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
int blockZ = quartZ << 2;
int internalY = blockY - engine.getMinHeight();
int caveSwitchY = Math.max(-8 - engine.getMinHeight(), 40);
IrisBiome featureBiome = engine.getSubterrainBiome(blockX, internalY, blockZ);
if (featureBiome != null) {
return createBiomeResolution(engine, featureBiome, true, engine.getDimension(), engine.getData(),
blockX, blockY, blockZ);
}
DimensionStackLayout.Layer stackLayer = includeDimensionStack
? stackLayout == null
? resolveDimensionStackLayer(engine, blockX, internalY, blockZ)
@@ -846,7 +851,13 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
return configuredBiomeKeys;
}
static boolean isGuaranteedSurfaceBiome(int quartY, int minHeight) {
static boolean isGuaranteedSurfaceBiome(Engine engine, int quartY) {
IrisDimension dimension = engine.getDimension();
if (dimension != null && dimension.allowsSubterrainFeatures()
&& !dimension.getSubterrainFeatures().isEmpty()) {
return false;
}
int minHeight = engine.getMinHeight();
int internalY = (quartY << 2) - minHeight;
int caveSwitchY = Math.max(-8 - minHeight, 40);
return internalY > caveSwitchY;
@@ -58,6 +58,7 @@ import art.arcane.volmlib.util.matter.MatterMarker;
import art.arcane.volmlib.util.matter.slices.MarkerMatter;
import java.util.Map;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ArrayList;
@@ -511,14 +512,18 @@ public final class ModdedWorldManager implements EngineWorldManager {
}
IrisBiome biome = environment.biome();
Map<String, Integer> chunkMobs = initial ? Map.of() : NativeSpawnQueries.livingEntityCategories(level, chunkX, chunkZ);
Map<String, Integer> chunkMobs = new HashMap<>(initial ? Map.of()
: NativeSpawnQueries.livingEntityCategories(level, chunkX, chunkZ));
RNG rng = initial ? EntitySpawnSeed.chunk(engine.getSeedManager().getEntity(), chunkX, chunkZ)
: new RNG(RNG.r.nextLong());
KList<SpawnCandidate> pool = new KList<>();
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.dimension().getEntitySpawners()), biome, chunkX, chunkZ, chunkMobs, initial);
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.region().getEntitySpawners()), null, chunkX, chunkZ, chunkMobs, initial);
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(biome.getEntitySpawners()), null, chunkX, chunkZ, chunkMobs, initial);
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.dimension().getEntitySpawners()), biome, chunkX, chunkZ, chunkMobs, initial, false);
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.region().getEntitySpawners()), null, chunkX, chunkZ, chunkMobs, initial, false);
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(biome.getEntitySpawners()), null, chunkX, chunkZ, chunkMobs, initial, false);
if (environment.dimension().hasUndergroundSpawners(engine)) {
spawnUnderground(level, chunkX, chunkZ, initial, chunkMobs, rng.nextParallelRNG(0x5A17D3B48269C0EFL));
}
if (pool.isEmpty()) {
return;
}
@@ -533,14 +538,111 @@ public final class ModdedWorldManager implements EngineWorldManager {
}
int spawned = spawnEntry(level, chosen.entry(), chunkX, chunkZ,
remainingCapacity(chosen.entry(), spawner, chunkMobs), rng, new SpawnContext(spawner, null, initial));
recordSpawned(chosen.entry(), chunkMobs, spawned);
if (spawned > 0 && !initial) {
spawner.spawn(engine, chunkX, chunkZ);
}
}
private void collectSpawns(KList<SpawnCandidate> pool, KList<IrisSpawner> spawners, IrisBiome biomeFilter, int chunkX, int chunkZ, Map<String, Integer> chunkMobs, boolean initial) {
private void spawnUnderground(NativeWorld level, int chunkX, int chunkZ, boolean initial,
Map<String, Integer> chunkMobs, RNG random) {
for (IrisSurface surface : List.of(IrisSurface.LAND, IrisSurface.WATER, IrisSurface.LAVA)) {
IrisPosition candidate = findUndergroundCandidate(level, chunkX, chunkZ,
random.nextParallelRNG(surface.ordinal()), surface);
if (candidate == null) {
continue;
}
BiomeEnvironment environment;
try {
environment = engine.getBiomeOrMantleEnvironment(candidate.getX(),
candidate.getY() - engine.getWorld().minHeight(), candidate.getZ());
} catch (SavedBiomeUnavailableException unavailable) {
continue;
}
try (BiomeEnvironment.Scope ignored = engine.openBiomeEnvironmentScope(environment)) {
KList<SpawnCandidate> pool = new KList<>();
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.dimension().getEntitySpawners()),
environment.biome(), chunkX, chunkZ, chunkMobs, initial, true);
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.region().getEntitySpawners()),
null, chunkX, chunkZ, chunkMobs, initial, true);
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.biome().getEntitySpawners()),
null, chunkX, chunkZ, chunkMobs, initial, true);
pool.removeIf(selection -> !matchesUndergroundSurface(selection.entry(), surface));
SpawnCandidate selected = rarityPick(pool, random);
if (selected == null) {
continue;
}
int capacity = remainingCapacity(selected.entry(), selected.spawner(), chunkMobs);
int spawned = spawnUndergroundEntry(level, candidate, selected, capacity, random, initial);
recordSpawned(selected.entry(), chunkMobs, spawned);
if (spawned > 0 && !initial) {
selected.spawner().spawn(engine, chunkX, chunkZ);
}
return;
}
}
}
private boolean matchesUndergroundSurface(IrisEntitySpawn entry, IrisSurface surface) {
IrisEntity entity = entry.getRealEntity(engine);
return entity != null && entity.getSurface().isFluid() == surface.isFluid()
&& (!surface.isFluid() || entity.getSurface() == surface);
}
static IrisPosition findUndergroundCandidate(NativeWorld level, int chunkX, int chunkZ, RNG random, IrisSurface surface) {
int minimumY = level.minHeight() + 1;
for (int attempt = 0; attempt < 8; attempt++) {
int x = (chunkX << 4) + random.i(1, 15);
int z = (chunkZ << 4) + random.i(1, 15);
int maximumY = Math.min(level.maxHeight() - 3, NativeSpawnQueries.surfaceHeight(level, x, z, true) - 1);
if (maximumY < minimumY) {
continue;
}
int span = maximumY - minimumY + 1;
int startY = LootResolver.inclusive(random, minimumY, maximumY);
for (int sample = 0; sample < Math.min(span, 128); sample++) {
int y = minimumY + Math.floorMod(startY - minimumY - sample, span);
NativeBlockState state = level.getBlock(x, y, z);
boolean acceptable = surface.isFluid() ? matchesSurface(surface, state)
: state != null && state.isAir() && NativeSpawnQueries.solid(level.getBlock(x, y - 1, z))
&& level.getBlock(x, y + 1, z).isAir();
if (acceptable) {
return new IrisPosition(x, y, z);
}
}
}
return null;
}
private int spawnUndergroundEntry(NativeWorld level, IrisPosition candidate, SpawnCandidate selection,
int capacity, RNG random, boolean initial) {
IrisEntity entity = selection.entry().getRealEntity(engine);
if (entity == null) {
return 0;
}
int x = candidate.getX();
int y = candidate.getY();
int z = candidate.getZ();
int count = Math.min(Math.max(0, capacity), LootResolver.inclusive(random,
selection.entry().getMinSpawns(), selection.entry().getMaxSpawns()));
int spawned = 0;
for (int ordinal = 0; ordinal < count; ordinal++) {
if (!initial && !lightAllowed(selection.spawner(), level, x, y, z)
|| !surfaceMatches(entity.getSurface(), level, x, y, z)
|| !ModdedEntitySpawner.isAreaClearForSpawn(entityRuntime(level), entity, x, y, z)) {
continue;
}
if (ModdedEntitySpawner.spawn(engine, entity, entityRuntime(level), x, y, z,
EntitySpawnSeed.entity(random.getSeed(), ordinal)) != null) {
spawned++;
}
}
return spawned;
}
private void collectSpawns(KList<SpawnCandidate> pool, KList<IrisSpawner> spawners, IrisBiome biomeFilter, int chunkX, int chunkZ, Map<String, Integer> chunkMobs, boolean initial, boolean underground) {
for (IrisSpawner spawner : spawners) {
if (spawner == null) {
if (spawner == null || (spawner.getGroup() == IrisSpawnGroup.CAVE) != underground) {
continue;
}
if (biomeFilter != null && !spawner.isValid(biomeFilter)) {
@@ -558,6 +660,13 @@ public final class ModdedWorldManager implements EngineWorldManager {
}
}
private void recordSpawned(IrisEntitySpawn entry, Map<String, Integer> counts, int spawned) {
IrisEntity entity = entry.getRealEntity(engine);
if (spawned > 0 && entity != null) {
counts.merge(entity.spawnCategory(), spawned, Integer::sum);
}
}
private int remainingCapacity(IrisEntitySpawn entry, IrisSpawner spawner, Map<String, Integer> counts) {
IrisEntity entity = entry.getRealEntity(engine);
return spawner.remainingCapacity(entity, counts);
@@ -38,12 +38,18 @@ public class IrisModdedStructureParityTest {
Bootstrap.bootStrap();
}
private static Engine surfaceEngine(int minHeight) {
Engine engine = mock(Engine.class);
when(engine.getMinHeight()).thenReturn(minHeight);
return engine;
}
@Test
public void surfaceBiomeFastPathBeginsAboveTheCaveSwitch() {
assertFalse(ModdedBiomePolicy.isGuaranteedSurfaceBiome(-2, -256));
assertTrue(ModdedBiomePolicy.isGuaranteedSurfaceBiome(-1, -256));
assertFalse(ModdedBiomePolicy.isGuaranteedSurfaceBiome(10, 0));
assertTrue(ModdedBiomePolicy.isGuaranteedSurfaceBiome(11, 0));
assertFalse(ModdedBiomePolicy.isGuaranteedSurfaceBiome(surfaceEngine(-256), -2));
assertTrue(ModdedBiomePolicy.isGuaranteedSurfaceBiome(surfaceEngine(-256), -1));
assertFalse(ModdedBiomePolicy.isGuaranteedSurfaceBiome(surfaceEngine(0), 10));
assertTrue(ModdedBiomePolicy.isGuaranteedSurfaceBiome(surfaceEngine(0), 11));
}
@Test
@@ -2,6 +2,11 @@ package art.arcane.iris.modded;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.DimensionStackLayout;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.subterrain.IrisSubterrainFeature;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.generation.runtime.GenerationSessionException;
import art.arcane.iris.generation.runtime.GenerationSessionLease;
import art.arcane.iris.generation.runtime.IrisComplex;
@@ -11,20 +16,54 @@ import art.arcane.volmlib.util.stream.interpolation.Interpolated;
import org.junit.Test;
import java.util.ArrayList;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public class ModdedBiomePolicyTest {
@Test
public void authoredBiomeWinsAboveSurfaceGateAndBeforeStackOwnership() throws Exception {
Engine engine = mock(Engine.class);
IrisBiome feature = mock(IrisBiome.class);
IrisDimension host = new IrisDimension().setSubterrainFeatures(new KList<>(
new IrisSubterrainFeature().setId("room")));
IrisData data = mock(IrisData.class);
when(data.getDataFolder()).thenReturn(new File("synthetic-pack"));
when(engine.getData()).thenReturn(data);
when(engine.getDimension()).thenReturn(host);
when(engine.getComplex()).thenReturn(mock(IrisComplex.class));
when(engine.getMinHeight()).thenReturn(-64);
when(engine.getSubterrainBiome(12, 224, -20)).thenReturn(feature);
assertFalse(ModdedBiomePolicy.isGuaranteedSurfaceBiome(engine, 40));
ModdedBiomePolicy<BiomeToken, Object> policy = boundTo(engine);
DimensionStackLayout stack = mock(DimensionStackLayout.class);
Method method = ModdedBiomePolicy.class.getDeclaredMethod("resolveBiomeResolution", Engine.class,
int.class, int.class, int.class, boolean.class, DimensionStackLayout.class);
method.setAccessible(true);
Object resolution = method.invoke(policy, engine, 3, 40, -5, true, stack);
Field biome = resolution.getClass().getDeclaredField("irisBiome");
biome.setAccessible(true);
assertSame(feature, biome.get(resolution));
Field dimension = resolution.getClass().getDeclaredField("dimension");
dimension.setAccessible(true);
assertSame(host, dimension.get(resolution));
verifyNoInteractions(stack);
}
@Test
public void registryOrderAndHolderIdentitySurviveThePolicyBoundaryAndRefreshOnRepoint() {
BiomeToken ocean = new BiomeToken("minecraft:ocean");
@@ -18,6 +18,7 @@ import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.world.entity.IrisEntity;
import art.arcane.iris.world.entity.IrisEntitySpawn;
import art.arcane.iris.world.entity.IrisSpawner;
import art.arcane.iris.world.entity.IrisSpawnGroup;
import art.arcane.iris.world.entity.IrisMarker;
import art.arcane.iris.world.entity.EntitySpawnSeed;
import art.arcane.iris.structure.placement.LootResolver;
@@ -97,6 +98,33 @@ public class ModdedInitialEntitySpawnTest {
}
}
@Test
public void undergroundAndSurfacePassesShareTheSuccessfulPopulationBudget() {
try (Fixture fixture = new Fixture()) {
BiomeEnvironment environment = fixture.engine.getSurfaceBiomeEnvironment(0, 0);
when(environment.dimension().hasUndergroundSpawners(fixture.engine)).thenReturn(true);
when(fixture.engine.getBiomeOrMantleEnvironment(anyInt(), anyInt(), anyInt())).thenReturn(environment);
NativeBlockState stone = mock(NativeBlockState.class);
NativeBlockState air = mock(NativeBlockState.class);
when(air.isAir()).thenReturn(true);
when(fixture.world.getBlock(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
int y = invocation.getArgument(1);
return y <= -21 || y == 50 ? stone : air;
});
fixture.queries.when(() -> NativeSpawnQueries.solid(stone)).thenReturn(true);
IrisEntity entity = new IrisEntity().setType("minecraft:cow");
IrisEntitySpawn entry = spy(new IrisEntitySpawn().setMinSpawns(2).setMaxSpawns(2));
doReturn(entity).when(entry).getRealEntity(fixture.engine);
fixture.definitions.add(new IrisSpawner().setGroup(IrisSpawnGroup.CAVE)
.setInitialSpawns(new KList<>(entry)).setMaxEntitiesPerChunk(3));
assertTrue(fixture.manager.initialSpawnChunk(fixture.world, -2, 3));
assertEquals(3, fixture.spawned.size());
assertEquals(2, fixture.spawned.stream().filter(spawn -> spawn.y() == -20).count());
assertEquals(1, fixture.spawned.stream().filter(spawn -> spawn.y() == 51).count());
fixture.queries.verify(() -> NativeSpawnQueries.livingEntityCategories(eq(fixture.world), anyInt(), anyInt()), never());
}
}
@Test
public void initialCompletionFollowsOwnedSpawnPassAndDoesNotRepeatIt() {
try (Fixture fixture = new Fixture()) {
@@ -0,0 +1,114 @@
package art.arcane.iris.modded;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.decoration.IrisSurface;
import art.arcane.iris.generation.runtime.BiomeEnvironment;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.loading.ResourceLoader;
import art.arcane.iris.pack.value.IrisPosition;
import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.world.entity.IrisEntity;
import art.arcane.iris.world.entity.IrisEntitySpawn;
import art.arcane.iris.world.entity.IrisSpawnGroup;
import art.arcane.iris.world.entity.IrisSpawner;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeEntityRuntime;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeSpawnQueries;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ModdedUndergroundSpawnOwnershipTest {
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void candidateSamplingAndPoolSelectionUseAbsoluteUndergroundOwner() throws Exception {
NativeWorld world = mock(NativeWorld.class);
when(world.minHeight()).thenReturn(-64);
when(world.maxHeight()).thenReturn(320);
NativeBlockState stone = mock(NativeBlockState.class);
NativeBlockState air = mock(NativeBlockState.class);
when(air.isAir()).thenReturn(true);
when(world.getBlock(anyInt(), anyInt(), anyInt())).thenAnswer(call ->
(int) call.getArgument(1) <= -21 ? stone : air);
Engine engine = mock(Engine.class);
when(engine.getWorld()).thenReturn(IrisWorld.builder().minHeight(-64).maxHeight(320).build());
IrisBiome cave = new IrisBiome().setEntitySpawners(new KList<>("residents"));
IrisDimension dimension = new IrisDimension();
IrisRegion region = new IrisRegion();
IrisData data = mock(IrisData.class);
@SuppressWarnings("unchecked")
ResourceLoader<IrisSpawner> loader = mock(ResourceLoader.class);
when(data.getSpawnerLoader()).thenReturn(loader);
when(loader.loadAll(dimension.getEntitySpawners())).thenReturn(new KList<>());
when(loader.loadAll(region.getEntitySpawners())).thenReturn(new KList<>());
IrisEntitySpawn entry = mock(IrisEntitySpawn.class);
IrisEntity entity = mock(IrisEntity.class);
when(entry.getRealEntity(engine)).thenReturn(entity);
when(entry.getRarity()).thenReturn(1);
when(entry.getMinSpawns()).thenReturn(1);
when(entry.getMaxSpawns()).thenReturn(1);
when(entity.getSurface()).thenReturn(IrisSurface.LAND);
when(entity.spawnCategory()).thenReturn("creature");
IrisSpawner spawner = new IrisSpawner().setGroup(IrisSpawnGroup.CAVE).setInitialSpawns(new KList<>(entry));
when(loader.loadAll(cave.getEntitySpawners())).thenReturn(new KList<>(spawner));
BiomeEnvironment environment = new BiomeEnvironment(9L, cave, region, dimension, data);
when(engine.getBiomeOrMantleEnvironment(anyInt(), eq(44), anyInt())).thenReturn(environment);
BiomeEnvironment.Scope scope = mock(BiomeEnvironment.Scope.class);
when(engine.openBiomeEnvironmentScope(environment)).thenReturn(scope);
NativeEntityRuntime runtime = mock(NativeEntityRuntime.class);
when(runtime.world()).thenReturn(world);
ModdedWorldManager manager = new ModdedWorldManager(engine);
try (MockedStatic<NativeSpawnQueries> queries = mockStatic(NativeSpawnQueries.class);
MockedStatic<ModdedEntitySpawner> entities = mockStatic(ModdedEntitySpawner.class)) {
queries.when(() -> NativeSpawnQueries.surfaceHeight(eq(world), anyInt(), anyInt(), eq(true))).thenReturn(64);
queries.when(() -> NativeSpawnQueries.solid(stone)).thenReturn(true);
IrisPosition first = ModdedWorldManager.findUndergroundCandidate(world, 2, -1, new RNG(88771L), IrisSurface.LAND);
IrisPosition second = ModdedWorldManager.findUndergroundCandidate(world, 2, -1, new RNG(88771L), IrisSurface.LAND);
assertNotNull(first);
assertEquals(first, second);
assertEquals(-20, first.getY());
assertTrue(first.getX() >= 33 && first.getX() <= 47 && first.getZ() >= -15 && first.getZ() <= -1);
Field field = ModdedWorldManager.class.getDeclaredField("entityRuntime");
field.setAccessible(true);
field.set(manager, runtime);
entities.when(() -> ModdedEntitySpawner.isAreaClearForSpawn(eq(runtime), eq(entity), anyInt(), eq(-20), anyInt()))
.thenReturn(true);
Method spawn = ModdedWorldManager.class.getDeclaredMethod("spawnUnderground", NativeWorld.class,
int.class, int.class, boolean.class, Map.class, RNG.class);
spawn.setAccessible(true);
spawn.invoke(manager, world, 2, -1, true, Map.of(), new RNG(88771L));
entities.verify(() -> ModdedEntitySpawner.spawn(eq(engine), eq(entity), eq(runtime), anyInt(), eq(-20), anyInt(), any(RNG.class)));
} finally {
manager.close();
}
verify(scope).close();
}
}