mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 13:30:08 +00:00
Fix seeded entity spawning
- Derive initial chunk and marker populations from stable location seeds. - Persist initial spawn completion after region-owned application. - Seed entity loot and attribute modifier identities. - Cover spawning, reload completion, and registry fixtures with regression tests.
This commit is contained in:
1 parent
12e8bb5a09
commit
be0d3b0156
33 files changed
+1932
-407
No files matched your search
@@ -20,7 +20,6 @@ import org.mockito.MockedStatic;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -35,18 +34,7 @@ public class LinkedTerrainChunkTest {
|
||||
@BeforeClass
|
||||
public static void initializeBiomes() throws Exception {
|
||||
BukkitTestServer.install();
|
||||
AtomicReference<Object> registered = new AtomicReference<>();
|
||||
RegistryAccess access = mock(RegistryAccess.class, invocation -> {
|
||||
if (invocation.getMethod().getName().equals("getRegistry")) {
|
||||
if (registered.get() == null) {
|
||||
registered.set(Proxy.newProxyInstance(Registry.class.getClassLoader(), new Class<?>[]{Registry.class},
|
||||
(proxy, method, arguments) -> method.getName().equals("getOrThrow")
|
||||
? biome((Key) arguments[0]) : null));
|
||||
}
|
||||
return registered.get();
|
||||
}
|
||||
return RETURNS_DEFAULTS.answer(invocation);
|
||||
});
|
||||
RegistryAccess access = BukkitTestServer.registryAccess();
|
||||
InternalAPIBridge bridge = mock(InternalAPIBridge.class, invocation ->
|
||||
invocation.getMethod().getName().equals("constructLegacyCustomBiome")
|
||||
? biome(Key.key("minecraft:custom")) : RETURNS_DEFAULTS.answer(invocation));
|
||||
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.plugin.Chunks;
|
||||
import art.arcane.iris.generation.context.IrisContext;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import org.bukkit.GameRules;
|
||||
import org.bukkit.World;
|
||||
import org.junit.Test;
|
||||
import org.junit.BeforeClass;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class WorldChunkMaintenanceInitialSpawnTest {
|
||||
@BeforeClass
|
||||
public static void initializeGameRules() throws Exception {
|
||||
WorldEntitySpawnerAdmissionTest.initializeGameRules();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completionIsWrittenAfterOwningRegionWorkAndDuplicateRequestsAreSkipped() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
AtomicInteger attempts = new AtomicInteger();
|
||||
fixture.start(attempts::incrementAndGet);
|
||||
fixture.start(attempts::incrementAndGet);
|
||||
assertEquals(1, fixture.async.size());
|
||||
fixture.async.remove().run();
|
||||
verify(fixture.world, never()).isChunkLoaded(2, -1);
|
||||
verify(fixture.mantle, never()).flag(2, -1, MantleFlag.INITIAL_SPAWNED_MARKER, true);
|
||||
|
||||
fixture.regions.remove().run();
|
||||
|
||||
assertEquals(1, attempts.get());
|
||||
assertTrue(fixture.maintenance.isInitialSpawnComplete(2, -1));
|
||||
verify(fixture.mantle, never()).flag(2, -1, MantleFlag.INITIAL_SPAWNED_MARKER, true);
|
||||
fixture.start(attempts::incrementAndGet);
|
||||
assertTrue(fixture.regions.isEmpty());
|
||||
fixture.async.remove().run();
|
||||
verify(fixture.mantle).flag(2, -1, MantleFlag.INITIAL_SPAWNED_MARKER, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unloadAndDisabledMobSpawningRemainEligibleForRetry() {
|
||||
for (boolean unloaded : new boolean[]{true, false}) {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
AtomicInteger attempts = new AtomicInteger();
|
||||
when(fixture.world.isChunkLoaded(2, -1)).thenReturn(!unloaded);
|
||||
when(fixture.world.getGameRuleValue(GameRules.SPAWN_MOBS)).thenReturn(unloaded);
|
||||
fixture.start(attempts::incrementAndGet);
|
||||
fixture.async.remove().run();
|
||||
fixture.regions.remove().run();
|
||||
assertFalse(fixture.maintenance.isInitialSpawnComplete(2, -1));
|
||||
assertEquals(0, attempts.get());
|
||||
verify(fixture.mantle, never()).flag(2, -1, MantleFlag.INITIAL_SPAWNED_MARKER, true);
|
||||
|
||||
when(fixture.world.isChunkLoaded(2, -1)).thenReturn(true);
|
||||
when(fixture.world.getGameRuleValue(GameRules.SPAWN_MOBS)).thenReturn(true);
|
||||
fixture.start(attempts::incrementAndGet);
|
||||
fixture.async.remove().run();
|
||||
fixture.regions.remove().run();
|
||||
assertEquals(1, attempts.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void persistedCompletionAndRegionRejectionDoNotRunInitialWork() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
AtomicInteger attempts = new AtomicInteger();
|
||||
when(fixture.mantle.hasFlag(2, -1, MantleFlag.INITIAL_SPAWNED_MARKER)).thenReturn(true);
|
||||
fixture.start(attempts::incrementAndGet);
|
||||
fixture.async.remove().run();
|
||||
assertTrue(fixture.maintenance.isInitialSpawnComplete(2, -1));
|
||||
assertTrue(fixture.regions.isEmpty());
|
||||
assertEquals(0, attempts.get());
|
||||
}
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
AtomicInteger attempts = new AtomicInteger();
|
||||
fixture.acceptRegions = false;
|
||||
fixture.start(attempts::incrementAndGet);
|
||||
fixture.async.remove().run();
|
||||
fixture.start(attempts::incrementAndGet);
|
||||
assertEquals(1, fixture.async.size());
|
||||
assertFalse(fixture.maintenance.isInitialSpawnComplete(2, -1));
|
||||
assertEquals(0, attempts.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shutdownDrainWaitsForTheReservedCompletionWriteAfterInitialEntitiesWereEmitted() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
AtomicInteger spawned = new AtomicInteger();
|
||||
fixture.start(spawned::incrementAndGet);
|
||||
fixture.async.remove().run();
|
||||
try (GenerationSessionLease owner = fixture.sessions.acquire("initial_spawn_owner");
|
||||
IrisContext.Scope ignored = IrisContext.open(fixture.engine, owner.sessionId(), null)) {
|
||||
fixture.regions.remove().run();
|
||||
}
|
||||
assertEquals(1, spawned.get());
|
||||
assertEquals(1, fixture.sessions.activeLeases());
|
||||
when(fixture.engine.isClosing()).thenReturn(true);
|
||||
ExecutorService closer = Executors.newSingleThreadExecutor();
|
||||
try {
|
||||
CountDownLatch sealing = new CountDownLatch(1);
|
||||
Future<?> drained = closer.submit(() -> {
|
||||
sealing.countDown();
|
||||
fixture.sessions.sealAndAwait("shutdown", 2_000L, true);
|
||||
return null;
|
||||
});
|
||||
assertTrue(sealing.await(1L, TimeUnit.SECONDS));
|
||||
assertFalse(drained.isDone());
|
||||
fixture.async.remove().run();
|
||||
drained.get(1L, TimeUnit.SECONDS);
|
||||
|
||||
verify(fixture.mantle).flag(2, -1, MantleFlag.INITIAL_SPAWNED_MARKER, true);
|
||||
assertEquals(0, fixture.sessions.activeLeases());
|
||||
} finally {
|
||||
closer.shutdownNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static final class Fixture implements AutoCloseable {
|
||||
private final Engine engine = mock(Engine.class);
|
||||
private final GenerationSessionManager sessions = new GenerationSessionManager();
|
||||
private final IrisWorldManager manager = mock(IrisWorldManager.class);
|
||||
private final World world = mock(World.class);
|
||||
private final Mantle<Matter> mantle = mock(Mantle.class);
|
||||
private final WorldChunkMaintenance maintenance = new WorldChunkMaintenance(manager);
|
||||
private final Queue<Runnable> async = new ArrayDeque<>();
|
||||
private final Queue<Runnable> regions = new ArrayDeque<>();
|
||||
private final MockedStatic<J> scheduler;
|
||||
private final MockedStatic<Chunks> chunks;
|
||||
private boolean acceptRegions = true;
|
||||
|
||||
private Fixture() {
|
||||
scheduler = mockStatic(J.class);
|
||||
try {
|
||||
chunks = mockStatic(Chunks.class);
|
||||
} catch (RuntimeException | Error failure) {
|
||||
scheduler.close();
|
||||
throw failure;
|
||||
}
|
||||
try {
|
||||
configure();
|
||||
} catch (RuntimeException | Error failure) {
|
||||
close();
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
private void configure() {
|
||||
when(manager.getEngine()).thenReturn(engine);
|
||||
when(engine.getGenerationSessions()).thenReturn(sessions);
|
||||
when(manager.getMantle()).thenReturn(mantle);
|
||||
when(manager.managedTask(anyString(), any(Runnable.class), any(Runnable.class)))
|
||||
.thenAnswer(invocation -> invocation.getArgument(1));
|
||||
when(world.isChunkLoaded(2, -1)).thenReturn(true);
|
||||
when(world.getGameRuleValue(GameRules.SPAWN_MOBS)).thenReturn(true);
|
||||
chunks.when(() -> Chunks.isSafe(world, 2, -1)).thenReturn(true);
|
||||
scheduler.when(() -> J.a(any(Runnable.class))).thenAnswer(invocation -> {
|
||||
async.add(invocation.getArgument(0));
|
||||
return null;
|
||||
});
|
||||
scheduler.when(() -> J.runRegion(eq(world), eq(2), eq(-1), any(Runnable.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
if (acceptRegions) {
|
||||
regions.add(invocation.getArgument(3));
|
||||
}
|
||||
return acceptRegions;
|
||||
});
|
||||
}
|
||||
|
||||
private void start(Runnable work) {
|
||||
maintenance.runInitialSpawn(world, 2, -1, work);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
while (!async.isEmpty()) {
|
||||
async.remove().run();
|
||||
}
|
||||
chunks.close();
|
||||
scheduler.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-18
@@ -4,20 +4,15 @@ import art.arcane.iris.testsupport.BukkitTestServer;
|
||||
import io.papermc.paper.InternalAPIBridge;
|
||||
import io.papermc.paper.registry.RegistryAccess;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.GameRule;
|
||||
import org.bukkit.GameRules;
|
||||
import org.bukkit.Registry;
|
||||
import org.bukkit.World;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.RETURNS_DEFAULTS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -26,19 +21,7 @@ public class WorldEntitySpawnerAdmissionTest {
|
||||
@BeforeClass
|
||||
public static void initializeGameRules() throws Exception {
|
||||
BukkitTestServer.install();
|
||||
AtomicReference<Object> registered = new AtomicReference<>();
|
||||
RegistryAccess access = mock(RegistryAccess.class, invocation -> {
|
||||
if (invocation.getMethod().getName().equals("getRegistry")) {
|
||||
if (registered.get() == null) {
|
||||
registered.set(Proxy.newProxyInstance(Registry.class.getClassLoader(),
|
||||
new Class<?>[]{Registry.class},
|
||||
(proxy, method, arguments) -> method.getName().equals("getOrThrow")
|
||||
? mock(GameRule.class) : null));
|
||||
}
|
||||
return registered.get();
|
||||
}
|
||||
return RETURNS_DEFAULTS.answer(invocation);
|
||||
});
|
||||
RegistryAccess access = BukkitTestServer.registryAccess();
|
||||
InternalAPIBridge bridge = mock(InternalAPIBridge.class);
|
||||
try (MockedStatic<RegistryAccess> registries = mockStatic(RegistryAccess.class);
|
||||
MockedStatic<InternalAPIBridge> internals = mockStatic(InternalAPIBridge.class)) {
|
||||
|
||||
+8
-7
@@ -7,6 +7,7 @@ import art.arcane.iris.world.history.SavedBiomeUnavailableException;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.world.entity.IrisEntitySpawn;
|
||||
import art.arcane.iris.world.entity.IrisEntitySpawn.SpawnContext;
|
||||
import art.arcane.iris.pack.value.IrisPosition;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.world.entity.IrisSpawner;
|
||||
@@ -54,9 +55,9 @@ public class WorldEntitySpawnerBiomeTest {
|
||||
configured.when(IrisSettings::get).thenReturn(settings);
|
||||
WorldEntitySpawner spawner = new WorldEntitySpawner(manager);
|
||||
spawner.prepareInitialSpawn(chunk, List.of());
|
||||
verify(maintenance, never()).raiseInitialSpawnMarkerFlag(any(), anyInt(), anyInt(), any());
|
||||
verify(maintenance, never()).runInitialSpawn(any(), anyInt(), anyInt(), any());
|
||||
spawner.prepareInitialSpawn(chunk, List.of());
|
||||
verify(maintenance).raiseInitialSpawnMarkerFlag(eq(world), eq(2), eq(-1), any());
|
||||
verify(maintenance).runInitialSpawn(eq(world), eq(2), eq(-1), any());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,11 +77,10 @@ public class WorldEntitySpawnerBiomeTest {
|
||||
when(definition.getInitialSpawns()).thenReturn(new KList<>(entity));
|
||||
when(definition.canSpawn(engine, 0, 0)).thenReturn(true);
|
||||
when(entity.getRarity()).thenReturn(1);
|
||||
when(entity.getReferenceSpawner()).thenReturn(definition);
|
||||
when(entity.spawn(eq(engine), eq(position), any(RNG.class))).thenReturn(1);
|
||||
when(entity.spawn(eq(engine), eq(position), any(RNG.class), any(SpawnContext.class))).thenReturn(1);
|
||||
KSet<IrisSpawner> definitions = new KSet<>();
|
||||
definitions.add(definition);
|
||||
PreparedMarkerSpawn marker = new PreparedMarkerSpawn(position, definitions, environment);
|
||||
PreparedMarkerSpawn marker = new PreparedMarkerSpawn(position, definitions, environment, null);
|
||||
IrisSettings settings = new IrisSettings();
|
||||
settings.getWorld().setAmbientEntitySpawningSystem(false);
|
||||
|
||||
@@ -88,11 +88,11 @@ public class WorldEntitySpawnerBiomeTest {
|
||||
configured.when(IrisSettings::get).thenReturn(settings);
|
||||
new WorldEntitySpawner(manager).prepareInitialSpawn(chunk, List.of(marker));
|
||||
ArgumentCaptor<Runnable> callback = ArgumentCaptor.forClass(Runnable.class);
|
||||
verify(manager.chunkMaintenance).raiseInitialSpawnMarkerFlag(eq(world), eq(0), eq(0), callback.capture());
|
||||
verify(manager.chunkMaintenance).runInitialSpawn(eq(world), eq(0), eq(0), callback.capture());
|
||||
doThrow(new SavedBiomeUnavailableException("Evicted", true)).when(engine)
|
||||
.getBiomeEnvironment(anyInt(), anyInt(), anyInt());
|
||||
callback.getValue().run();
|
||||
verify(entity).spawn(eq(engine), eq(position), any(RNG.class));
|
||||
verify(entity).spawn(eq(engine), eq(position), any(RNG.class), any(SpawnContext.class));
|
||||
verify(engine).openBiomeEnvironmentScope(environment);
|
||||
verify(scope).close();
|
||||
verify(engine, never()).getBiomeEnvironment(anyInt(), anyInt(), anyInt());
|
||||
@@ -102,6 +102,7 @@ public class WorldEntitySpawnerBiomeTest {
|
||||
private static IrisWorldManager manager(Engine engine) throws Exception {
|
||||
IrisWorldManager manager = mock(IrisWorldManager.class);
|
||||
when(manager.getEngine()).thenReturn(engine);
|
||||
when(engine.getSeedManager()).thenReturn(mock(SeedManager.class));
|
||||
Field field = IrisWorldManager.class.getDeclaredField("chunkMaintenance");
|
||||
field.setAccessible(true);
|
||||
field.set(manager, mock(WorldChunkMaintenance.class));
|
||||
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.pack.loading.ResourceLoader;
|
||||
import art.arcane.iris.world.entity.IrisEntity;
|
||||
import art.arcane.iris.world.entity.IrisEntitySpawn;
|
||||
import art.arcane.iris.world.entity.IrisEntitySpawn.SpawnContext;
|
||||
import art.arcane.iris.world.entity.IrisSpawner;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.GameRules;
|
||||
import org.bukkit.World;
|
||||
import org.junit.Test;
|
||||
import org.junit.BeforeClass;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.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.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class WorldEntitySpawnerInitialSeedTest {
|
||||
@BeforeClass
|
||||
public static void initializeGameRules() throws Exception {
|
||||
WorldEntitySpawnerAdmissionTest.initializeGameRules();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initialPopulationUsesAuthoredCapacityWithoutLiveGatesOrPopulationReads() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.spawn(2, -3);
|
||||
|
||||
assertEquals(1, fixture.observed.size());
|
||||
Observation observation = fixture.observed.get("2,-3");
|
||||
assertEquals(4, observation.capacity());
|
||||
assertTrue(observation.initial());
|
||||
verify(fixture.definition, never()).canSpawn(any(Engine.class), anyInt(), anyInt());
|
||||
verify(fixture.definition, never()).spawn(any(Engine.class), anyInt(), anyInt());
|
||||
verify(fixture.lastChunk, never()).getEntities();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chunkBatchSeedsAreIndependentOfVisitOrderAndUnrelatedRandomActivity() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
for (int coordinate = -16; coordinate < 16; coordinate++) {
|
||||
fixture.spawn(coordinate, -coordinate - 1);
|
||||
}
|
||||
Map<String, Observation> forward = new HashMap<>(fixture.observed);
|
||||
fixture.observed.clear();
|
||||
for (int coordinate = 15; coordinate >= -16; coordinate--) {
|
||||
for (int draw = 0; draw < 19; draw++) {
|
||||
RNG.r.nextLong();
|
||||
}
|
||||
fixture.spawn(coordinate, -coordinate - 1);
|
||||
}
|
||||
assertEquals(forward, fixture.observed);
|
||||
assertEquals(32, forward.values().stream().map(Observation::seed).distinct().count());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initialSpawnerIgnoresCooldownButExcludedContentIsNeverAdmitted() {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisSpawner definition = mock(IrisSpawner.class);
|
||||
when(definition.canSpawn(engine, 1, 2)).thenReturn(false);
|
||||
assertTrue(WorldEntitySpawner.spawnerAllowed(definition, engine, 1, 2, true));
|
||||
assertFalse(WorldEntitySpawner.spawnerAllowed(definition, engine, 1, 2, false));
|
||||
when(definition.isCompatExcluded()).thenReturn(true);
|
||||
assertFalse(WorldEntitySpawner.spawnerAllowed(definition, engine, 1, 2, true));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static final class Fixture {
|
||||
private final Engine engine = mock(Engine.class);
|
||||
private final IrisWorldManager manager = mock(IrisWorldManager.class);
|
||||
private final World world = mock(World.class);
|
||||
private final IrisSpawner definition = mock(IrisSpawner.class);
|
||||
private final IrisEntitySpawn entry = mock(IrisEntitySpawn.class);
|
||||
private final Map<String, Observation> observed = new HashMap<>();
|
||||
private final BiomeEnvironment environment;
|
||||
private final Method spawn;
|
||||
private final WorldEntitySpawner spawner;
|
||||
private Chunk lastChunk;
|
||||
|
||||
private Fixture() throws Exception {
|
||||
SeedManager seeds = mock(SeedManager.class);
|
||||
when(seeds.getEntity()).thenReturn(918273645L);
|
||||
when(engine.getSeedManager()).thenReturn(seeds);
|
||||
when(manager.getEngine()).thenReturn(engine);
|
||||
IrisData data = mock(IrisData.class);
|
||||
ResourceLoader<IrisSpawner> loader = mock(ResourceLoader.class);
|
||||
when(data.getSpawnerLoader()).thenReturn(loader);
|
||||
KList<String> authored = new KList<>("qa/initial");
|
||||
when(loader.loadAll(any(KList.class))).thenReturn(new KList<>());
|
||||
when(loader.loadAll(authored)).thenReturn(new KList<>(definition));
|
||||
IrisBiome biome = mock(IrisBiome.class);
|
||||
IrisRegion region = mock(IrisRegion.class);
|
||||
IrisDimension dimension = mock(IrisDimension.class);
|
||||
when(biome.getEntitySpawners()).thenReturn(authored);
|
||||
when(region.getEntitySpawners()).thenReturn(new KList<>());
|
||||
when(dimension.getEntitySpawners()).thenReturn(new KList<>());
|
||||
when(definition.getInitialSpawns()).thenReturn(new KList<>(entry));
|
||||
when(definition.getMaxEntitiesPerChunk()).thenReturn(4);
|
||||
when(entry.getRarity()).thenReturn(1);
|
||||
when(entry.getRealEntity(engine)).thenReturn(mock(IrisEntity.class));
|
||||
when(world.getGameRuleValue(GameRules.SPAWN_MOBS)).thenReturn(true);
|
||||
when(entry.spawn(eq(engine), any(Chunk.class), any(RNG.class), anyInt(), any(SpawnContext.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
Chunk chunk = invocation.getArgument(1);
|
||||
RNG rng = invocation.getArgument(2);
|
||||
int capacity = invocation.getArgument(3);
|
||||
SpawnContext context = invocation.getArgument(4);
|
||||
observed.put(chunk.getX() + "," + chunk.getZ(), new Observation(rng.getSeed(), capacity, context.initial()));
|
||||
return 4;
|
||||
});
|
||||
environment = new BiomeEnvironment(1L, biome, region, dimension, data);
|
||||
spawner = new WorldEntitySpawner(manager);
|
||||
spawn = WorldEntitySpawner.class.getDeclaredMethod("spawnAmbient", Chunk.class, boolean.class, BiomeEnvironment.class);
|
||||
spawn.setAccessible(true);
|
||||
}
|
||||
|
||||
private void spawn(int x, int z) throws Exception {
|
||||
lastChunk = mock(Chunk.class);
|
||||
when(lastChunk.getWorld()).thenReturn(world);
|
||||
when(lastChunk.getX()).thenReturn(x);
|
||||
when(lastChunk.getZ()).thenReturn(z);
|
||||
spawn.invoke(spawner, lastChunk, true, environment);
|
||||
}
|
||||
}
|
||||
|
||||
private record Observation(long seed, int capacity, boolean initial) {
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.runtime.MarkerSpawnScanner.PreparedMarkerSpawn;
|
||||
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.value.IrisPosition;
|
||||
import art.arcane.iris.world.entity.IrisEntitySpawn;
|
||||
import art.arcane.iris.world.entity.IrisEntitySpawn.SpawnContext;
|
||||
import art.arcane.iris.world.entity.IrisMarker;
|
||||
import art.arcane.iris.world.entity.IrisSpawner;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
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.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class WorldEntitySpawnerMarkerSeedTest {
|
||||
@Test
|
||||
public void initialMarkersIgnoreHashSetInsertionOrderAndOtherChunksRandomDraws() throws Exception {
|
||||
Engine engine = mock(Engine.class);
|
||||
SeedManager seeds = mock(SeedManager.class);
|
||||
when(engine.getSeedManager()).thenReturn(seeds);
|
||||
when(seeds.getEntity()).thenReturn(145398276L);
|
||||
IrisWorldManager manager = mock(IrisWorldManager.class);
|
||||
when(manager.getEngine()).thenReturn(engine);
|
||||
BiomeEnvironment environment = new BiomeEnvironment(1L, mock(IrisBiome.class), mock(IrisRegion.class),
|
||||
mock(IrisDimension.class), mock(IrisData.class));
|
||||
when(engine.openBiomeEnvironmentScope(environment)).thenReturn(mock(BiomeEnvironment.Scope.class));
|
||||
Map<String, Observation> observed = new HashMap<>();
|
||||
IrisMarker marker = new IrisMarker();
|
||||
IrisSpawner first = definition("qa/a", engine, marker, observed);
|
||||
IrisSpawner second = definition("qa/b", engine, marker, observed);
|
||||
KSet<IrisSpawner> forward = new KSet<>();
|
||||
forward.add(first);
|
||||
forward.add(second);
|
||||
KSet<IrisSpawner> reversed = new KSet<>();
|
||||
reversed.add(second);
|
||||
reversed.add(first);
|
||||
Method spawn = WorldEntitySpawner.class.getDeclaredMethod("spawnPreparedMarker", PreparedMarkerSpawn.class, boolean.class);
|
||||
spawn.setAccessible(true);
|
||||
WorldEntitySpawner spawner = new WorldEntitySpawner(manager);
|
||||
for (int x = -32; x < 32; x++) {
|
||||
IrisPosition position = new IrisPosition(x * 16 + 1, -49, -x * 16 + 3);
|
||||
spawn.invoke(spawner, new PreparedMarkerSpawn(position, forward, environment, marker), true);
|
||||
}
|
||||
Map<String, Observation> expected = new HashMap<>(observed);
|
||||
observed.clear();
|
||||
for (int x = 31; x >= -32; x--) {
|
||||
RNG.r.nextLong();
|
||||
IrisPosition position = new IrisPosition(x * 16 + 1, -49, -x * 16 + 3);
|
||||
spawn.invoke(spawner, new PreparedMarkerSpawn(position, reversed, environment, marker), true);
|
||||
}
|
||||
assertEquals(expected, observed);
|
||||
assertEquals(64, observed.size());
|
||||
assertTrue(observed.values().stream().allMatch(Observation::initial));
|
||||
verify(first, never()).canSpawn(any(Engine.class), anyInt(), anyInt());
|
||||
verify(second, never()).canSpawn(any(Engine.class), anyInt(), anyInt());
|
||||
verify(first, never()).spawn(any(Engine.class), anyInt(), anyInt());
|
||||
verify(second, never()).spawn(any(Engine.class), anyInt(), anyInt());
|
||||
}
|
||||
|
||||
private static IrisSpawner definition(String key, Engine engine, IrisMarker marker, Map<String, Observation> observed) {
|
||||
IrisSpawner spawner = mock(IrisSpawner.class);
|
||||
IrisEntitySpawn entry = mock(IrisEntitySpawn.class);
|
||||
when(spawner.getLoadKey()).thenReturn(key);
|
||||
when(spawner.getInitialSpawns()).thenReturn(new KList<>(entry));
|
||||
when(entry.getRarity()).thenReturn(1);
|
||||
when(entry.spawn(eq(engine), any(IrisPosition.class), any(RNG.class), any(SpawnContext.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
IrisPosition position = invocation.getArgument(1);
|
||||
RNG rng = invocation.getArgument(2);
|
||||
SpawnContext context = invocation.getArgument(3);
|
||||
assertEquals(marker, context.marker());
|
||||
observed.put(position.getX() + "," + position.getY() + "," + position.getZ(),
|
||||
new Observation(context.spawner().getLoadKey(), rng.getSeed(), context.initial()));
|
||||
return 1;
|
||||
});
|
||||
return spawner;
|
||||
}
|
||||
|
||||
private record Observation(String definition, long seed, boolean initial) {
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -35,15 +35,15 @@ public class WorldEntitySpawnerPopulationTest {
|
||||
IrisEntity definition = mock(IrisEntity.class);
|
||||
when(definition.spawnCategory()).thenReturn("monster");
|
||||
when(entry.getRealEntity(engine)).thenReturn(definition);
|
||||
when(entry.getReferenceSpawner()).thenReturn(new IrisSpawner().setMaxEntitiesPerChunk(2));
|
||||
IrisSpawner spawner = new IrisSpawner().setMaxEntitiesPerChunk(2);
|
||||
try (MockedStatic<BukkitEntityType> types = mockStatic(BukkitEntityType.class)) {
|
||||
types.when(() -> BukkitEntityType.of(EntityType.FROG)).thenReturn(creatureType);
|
||||
types.when(() -> BukkitEntityType.of(EntityType.ZOMBIE)).thenReturn(monsterType);
|
||||
WorldEntitySpawner.ChunkCounter counter = new WorldEntitySpawner.ChunkCounter(
|
||||
new Entity[]{frog, frog, frog, zombie, player});
|
||||
assertEquals(1, counter.remainingCapacity(entry, engine));
|
||||
assertEquals(1, counter.remainingCapacity(entry, spawner, engine));
|
||||
when(definition.spawnCategory()).thenReturn("creature");
|
||||
assertEquals(0, counter.remainingCapacity(entry, engine));
|
||||
assertEquals(0, counter.remainingCapacity(entry, spawner, engine));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.studio.generation;
|
||||
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.testsupport.BukkitTestServer;
|
||||
import art.arcane.iris.pack.loading.ResourceLoader;
|
||||
import art.arcane.iris.studio.object.ObjectStudioLayout;
|
||||
import art.arcane.iris.studio.object.ObjectStudioSaveService;
|
||||
@@ -38,7 +39,6 @@ import java.nio.file.Files;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
@@ -59,19 +59,9 @@ public class ObjectStudioGeometryTest {
|
||||
public final TemporaryFolder temporary = new TemporaryFolder();
|
||||
|
||||
@BeforeClass
|
||||
@SuppressWarnings("unchecked")
|
||||
public static void initializeBukkit() throws Exception {
|
||||
PreObjectMatterTest.setUpBukkit();
|
||||
AtomicReference<Registry<Biome>> registered = new AtomicReference<>();
|
||||
RegistryAccess access = mock(RegistryAccess.class, invocation -> {
|
||||
if (invocation.getMethod().getName().equals("getRegistry")) {
|
||||
if (registered.get() == null) {
|
||||
registered.set(biomeRegistry());
|
||||
}
|
||||
return registered.get();
|
||||
}
|
||||
return RETURNS_DEFAULTS.answer(invocation);
|
||||
});
|
||||
RegistryAccess access = BukkitTestServer.registryAccess();
|
||||
InternalAPIBridge bridge = mock(InternalAPIBridge.class, invocation -> {
|
||||
if (invocation.getMethod().getName().equals("constructLegacyCustomBiome")) {
|
||||
return biome(Key.key("minecraft:custom"));
|
||||
@@ -89,20 +79,6 @@ public class ObjectStudioGeometryTest {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Registry<Biome> biomeRegistry() {
|
||||
return (Registry<Biome>) Proxy.newProxyInstance(Registry.class.getClassLoader(),
|
||||
new Class<?>[]{Registry.class},
|
||||
(proxy, method, arguments) -> switch (method.getName()) {
|
||||
case "getOrThrow" -> biome((Key) arguments[0]);
|
||||
case "hashCode" -> System.identityHashCode(proxy);
|
||||
case "equals" -> proxy == arguments[0];
|
||||
case "toString" -> "iris-object-studio-biome-registry";
|
||||
case "size" -> 0;
|
||||
case "hasTag" -> false;
|
||||
default -> null;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jungleClutterSignedBoundsStayInsideTheEditableCellAboveTheFloor() throws Exception {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package art.arcane.iris.world.entity;
|
||||
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
|
||||
public class EntitySpawnSeedTest {
|
||||
@Test
|
||||
public void coordinateAndAttemptSeedsRemainPinned() {
|
||||
assertEquals(-4130335506775599505L, EntitySpawnSeed.chunk(0L, 0, 0).getSeed());
|
||||
assertEquals(8337887711347547470L, EntitySpawnSeed.chunk(1337L, -2, 3).getSeed());
|
||||
assertEquals(7508281853961706446L,
|
||||
EntitySpawnSeed.chunk(-1L, Integer.MAX_VALUE, Integer.MIN_VALUE).getSeed());
|
||||
assertEquals(-2305592493329120404L, EntitySpawnSeed.marker(0L, 0, 0, 0).getSeed());
|
||||
assertEquals(8373759002518926811L, EntitySpawnSeed.marker(1337L, -32, -64, 48).getSeed());
|
||||
assertEquals(-8713573891296014035L,
|
||||
EntitySpawnSeed.marker(-1L, Integer.MAX_VALUE, Integer.MIN_VALUE, 1).getSeed());
|
||||
assertEquals(-6302458730787398690L, EntitySpawnSeed.entity(1337L, -1).getSeed());
|
||||
assertEquals(858402071020777840L, EntitySpawnSeed.entity(1337L, 0).getSeed());
|
||||
assertEquals(-632476273033191086L, EntitySpawnSeed.entity(1337L, 1).getSeed());
|
||||
assertEquals(4034378215462583149L, EntitySpawnSeed.entity(1337L, 17).getSeed());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void consumptionAndReverseOrderDoNotChangeSiblingAttempts() {
|
||||
long batchSeed = EntitySpawnSeed.chunk(1337L, -2, 3).getSeed();
|
||||
List<Long> forward = new ArrayList<>();
|
||||
for (int ordinal = 0; ordinal < 16; ordinal++) {
|
||||
forward.add(EntitySpawnSeed.entity(batchSeed, ordinal).nextLong());
|
||||
}
|
||||
for (int ordinal = 15; ordinal >= 0; ordinal--) {
|
||||
RNG sibling = EntitySpawnSeed.entity(batchSeed, ordinal - 1);
|
||||
for (int draw = 0; draw < 100; draw++) {
|
||||
sibling.nextLong();
|
||||
}
|
||||
assertEquals(forward.get(ordinal).longValue(), EntitySpawnSeed.entity(batchSeed, ordinal).nextLong());
|
||||
}
|
||||
assertNotEquals(EntitySpawnSeed.marker(1337L, -32, -64, 48).getSeed(),
|
||||
EntitySpawnSeed.marker(1337L, -32, -63, 48).getSeed());
|
||||
assertNotEquals(EntitySpawnSeed.chunk(1337L, -2, 3).getSeed(),
|
||||
EntitySpawnSeed.chunk(1338L, -2, 3).getSeed());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentCoordinateEvaluationMatchesSerialEvaluation() {
|
||||
List<CompletableFuture<Long>> results = new ArrayList<>();
|
||||
for (int ordinal = 0; ordinal < 64; ordinal++) {
|
||||
int index = ordinal;
|
||||
results.add(CompletableFuture.supplyAsync(() -> EntitySpawnSeed.marker(
|
||||
42L, index - 32, -64 + index, -index).nextLong()));
|
||||
}
|
||||
for (int ordinal = 63; ordinal >= 0; ordinal--) {
|
||||
assertEquals(EntitySpawnSeed.marker(42L, ordinal - 32, -64 + ordinal, -ordinal).nextLong(),
|
||||
results.get(ordinal).join().longValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package art.arcane.iris.world.entity;
|
||||
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.bukkit.attribute.AttributeModifier;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
|
||||
public class IrisAttributeModifierSeedTest {
|
||||
@Test
|
||||
public void repeatedSeedProducesIdenticalModifierKeyAmountAndOperation() {
|
||||
IrisAttributeModifier definition = new IrisAttributeModifier().setName("Custom Bonus")
|
||||
.setMinAmount(-3).setMaxAmount(7).setOperation("ADD_SCALAR");
|
||||
AttributeModifier first = definition.createModifier(new RNG(1337L));
|
||||
AttributeModifier repeated = definition.createModifier(new RNG(1337L));
|
||||
assertEquals(first.getKey(), repeated.getKey());
|
||||
assertEquals("minecraft:custom_bonus_a8f1288a2c8a93b8_b072351edfd5484c", first.getKey().toString());
|
||||
assertEquals(first.getAmount(), repeated.getAmount(), 0D);
|
||||
assertEquals(5.832726771624211D, first.getAmount(), 0D);
|
||||
assertEquals(AttributeModifier.Operation.ADD_SCALAR, first.getOperation());
|
||||
assertTrue(first.getKey().getKey().startsWith("custom_bonus_"));
|
||||
assertTrue(first.getAmount() >= -3D && first.getAmount() <= 7D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sameNamedModifiersRemainDistinctWithinOneSeededSequence() {
|
||||
IrisAttributeModifier definition = new IrisAttributeModifier().setName("Bonus");
|
||||
RNG rng = new RNG(1337L);
|
||||
AttributeModifier first = definition.createModifier(rng);
|
||||
AttributeModifier second = definition.createModifier(rng);
|
||||
assertNotEquals(first.getKey(), second.getKey());
|
||||
assertNotEquals(first.getKey(), definition.createModifier(new RNG(1338L)).getKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void zeroChanceNeverAppliesAModifier() {
|
||||
ItemMeta metadata = mock(ItemMeta.class);
|
||||
IrisAttributeModifier definition = new IrisAttributeModifier().setChance(0D);
|
||||
definition.apply(new RNG(1337L), metadata);
|
||||
verifyNoInteractions(metadata);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package art.arcane.iris.world.entity;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.loot.LootTable;
|
||||
import org.bukkit.loot.Lootable;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
public class IrisEntityLootSeedTest {
|
||||
@Test
|
||||
public void lootKeyDependsOnSpawnSeedRatherThanEntityHashOrWorldIdentity() {
|
||||
IrisEntity first = new IrisEntity().setCustomName("First");
|
||||
IrisEntity second = new IrisEntity().setCustomName("Second");
|
||||
assertNotEquals(first.hashCode(), second.hashCode());
|
||||
LootTable firstTable = bind(first, new RNG(1337L), mock(World.class));
|
||||
LootTable secondTable = bind(second, new RNG(1337L), mock(World.class));
|
||||
assertEquals(firstTable.getKey(), secondTable.getKey());
|
||||
assertEquals("iris:loot-22371b531cd5acd3", firstTable.getKey().toString());
|
||||
assertNotEquals(firstTable.getKey(), bind(first, new RNG(1338L), mock(World.class)).getKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void priorCustomizationConsumptionDoesNotChangeTheLootIdentity() {
|
||||
IrisEntity definition = new IrisEntity();
|
||||
RNG consumed = new RNG(1337L);
|
||||
for (int draw = 0; draw < 100; draw++) {
|
||||
consumed.nextLong();
|
||||
}
|
||||
assertEquals(bind(definition, new RNG(1337L), mock(World.class)).getKey(),
|
||||
bind(definition, consumed, mock(World.class)).getKey());
|
||||
}
|
||||
|
||||
private static LootTable bind(IrisEntity definition, RNG rng, World world) {
|
||||
Engine engine = mock(Engine.class);
|
||||
Lootable lootable = mock(Lootable.class);
|
||||
IrisEntity.BukkitOps.bindLoot(definition, engine, lootable, new Location(world, -31.5, -63.5, 48.5), rng);
|
||||
ArgumentCaptor<LootTable> table = ArgumentCaptor.forClass(LootTable.class);
|
||||
verify(lootable).setLootTable(table.capture());
|
||||
return table.getValue();
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package art.arcane.iris.world.entity;
|
||||
|
||||
import art.arcane.iris.generation.decoration.IrisSurface;
|
||||
import art.arcane.iris.pack.value.IrisPosition;
|
||||
import art.arcane.iris.pack.value.IrisRange;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.SeedManager;
|
||||
@@ -29,12 +30,14 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
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.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.spy;
|
||||
@@ -381,9 +384,8 @@ public class IrisEntitySpawnTest {
|
||||
IrisPosition position = invocation.getArgument(0);
|
||||
return new Location(fixture.world, position.getX(), position.getY(), position.getZ());
|
||||
});
|
||||
fixture.entry.setReferenceSpawner(new IrisSpawner());
|
||||
assertEquals(0, fixture.entry.spawn(fixture.engine, new IrisPosition(0, 78, 0), new RNG(1L)));
|
||||
assertEquals(3, fixture.entry.spawn(fixture.engine, new IrisPosition(0, 77, 0), new RNG(1L)));
|
||||
assertEquals(0, fixture.entry.spawn(fixture.engine, new IrisPosition(0, 78, 0), new RNG(1L), new IrisEntitySpawn.SpawnContext(new IrisSpawner(), null, false)));
|
||||
assertEquals(3, fixture.entry.spawn(fixture.engine, new IrisPosition(0, 77, 0), new RNG(1L), new IrisEntitySpawn.SpawnContext(new IrisSpawner(), null, false)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,6 +401,96 @@ public class IrisEntitySpawnTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedFirstSiblingDoesNotChangeLaterPositionsOrCustomizationSeeds() {
|
||||
try (SpawnFixture fixture = new SpawnFixture(new SpawnOptions(IrisSurface.LAND, Material.WATER, 78, 78))) {
|
||||
assertEquals(3, fixture.spawn(IrisSpawnGroup.NORMAL));
|
||||
List<Location> expectedPositions = new ArrayList<>(fixture.positions.subList(1, 3));
|
||||
List<Long> expectedSeeds = new ArrayList<>(fixture.customizationSeeds.subList(1, 3));
|
||||
Location rejected = fixture.positions.getFirst();
|
||||
Block obstruction = mock(Block.class);
|
||||
when(obstruction.getType()).thenReturn(Material.STONE);
|
||||
when(obstruction.isSolid()).thenReturn(true);
|
||||
when(fixture.world.getBlockAt(rejected.getBlockX(), rejected.getBlockY(), rejected.getBlockZ()))
|
||||
.thenReturn(obstruction);
|
||||
fixture.positions.clear();
|
||||
fixture.customizationSeeds.clear();
|
||||
assertEquals(2, fixture.spawn(IrisSpawnGroup.NORMAL));
|
||||
assertEquals(expectedPositions, fixture.positions);
|
||||
assertEquals(expectedSeeds, fixture.customizationSeeds);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initialCavePopulationDoesNotDependOnMarkerStorageOrder() {
|
||||
try (SpawnFixture fixture = new SpawnFixture(new SpawnOptions(IrisSurface.LAND, Material.WATER, 78, 78));
|
||||
MockedStatic<J> scheduler = mockStatic(J.class)) {
|
||||
scheduler.when(J::isFolia).thenReturn(false);
|
||||
EngineMantle mantle = mock(EngineMantle.class);
|
||||
when(fixture.engine.getMantle()).thenReturn(mantle);
|
||||
IrisPosition first = new IrisPosition(7, 79, 9);
|
||||
IrisPosition second = new IrisPosition(3, 79, 5);
|
||||
when(mantle.findMarkers(0, 0, MarkerMatter.CAVE_FLOOR))
|
||||
.thenReturn(new KList<>(first, second), new KList<>(second, first));
|
||||
fixture.platform.when(() -> BukkitPlatform.toLocation(any(IrisPosition.class), eq(fixture.world)))
|
||||
.thenAnswer(invocation -> {
|
||||
IrisPosition position = invocation.getArgument(0);
|
||||
return new Location(fixture.world, position.getX(), position.getY(), position.getZ());
|
||||
});
|
||||
IrisEntitySpawn.SpawnContext context = new IrisEntitySpawn.SpawnContext(
|
||||
new IrisSpawner().setGroup(IrisSpawnGroup.CAVE), null, true);
|
||||
assertEquals(3, fixture.entry.spawn(fixture.engine, fixture.chunk, new RNG(1337L), 3, context));
|
||||
List<Location> expected = new ArrayList<>(fixture.positions);
|
||||
List<Long> expectedSeeds = new ArrayList<>(fixture.customizationSeeds);
|
||||
fixture.positions.clear();
|
||||
fixture.customizationSeeds.clear();
|
||||
assertEquals(3, fixture.entry.spawn(fixture.engine, fixture.chunk, new RNG(1337L), 3, context));
|
||||
assertEquals(expected, fixture.positions);
|
||||
assertEquals(expectedSeeds, fixture.customizationSeeds);
|
||||
verify(mantle, times(2)).findMarkers(0, 0, MarkerMatter.CAVE_FLOOR);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initialPopulationIgnoresLiveLightButRecurringPopulationHonorsIt() {
|
||||
try (SpawnFixture fixture = new SpawnFixture(new SpawnOptions(IrisSurface.LAND, Material.WATER, 78, 78))) {
|
||||
fixture.lightLevel = 15;
|
||||
IrisSpawner spawner = new IrisSpawner().setAllowedLightLevels(new IrisRange(0, 0));
|
||||
assertEquals(0, fixture.entry.spawn(fixture.engine, fixture.chunk, new RNG(1337L), 3,
|
||||
new IrisEntitySpawn.SpawnContext(spawner, null, false)));
|
||||
assertEquals(3, fixture.entry.spawn(fixture.engine, fixture.chunk, new RNG(1337L), 3,
|
||||
new IrisEntitySpawn.SpawnContext(spawner, null, true)));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void markerExhaustionUsesOnlyTheProvidedSeed() {
|
||||
IrisMarker marker = new IrisMarker().setExhaustionChance(0.5D);
|
||||
for (long seed = 0; seed < 32; seed++) {
|
||||
assertEquals(marker.shouldExhaust(new RNG(seed)), marker.shouldExhaust(new RNG(seed)));
|
||||
}
|
||||
assertTrue(marker.setExhaustionChance(1D).shouldExhaust(new RNG(1337L)));
|
||||
assertFalse(marker.setExhaustionChance(-1D).shouldExhaust(new RNG(1337L)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void directEntitySeedDoesNotDependOnWorldObjectIdentity() {
|
||||
Engine engine = mock(Engine.class);
|
||||
SeedManager seeds = mock(SeedManager.class);
|
||||
when(engine.getSeedManager()).thenReturn(seeds);
|
||||
when(seeds.getEntity()).thenReturn(1337L);
|
||||
IrisEntity entity = spy(new IrisEntity());
|
||||
List<Long> captured = new ArrayList<>();
|
||||
doAnswer(invocation -> {
|
||||
RNG rng = invocation.getArgument(2);
|
||||
captured.add(rng.getSeed());
|
||||
return null;
|
||||
}).when(entity).spawn(eq(engine), any(Location.class), any(RNG.class));
|
||||
entity.spawn(engine, new Location(mock(World.class), -31.5, -63.5, 48.5));
|
||||
entity.spawn(engine, new Location(mock(World.class), -31.5, -63.5, 48.5));
|
||||
assertEquals(List.of(8373759002518926811L, 8373759002518926811L), captured);
|
||||
}
|
||||
|
||||
private record SpawnOptions(IrisSurface surface, Material fluid, int floor, int top) {
|
||||
}
|
||||
|
||||
@@ -408,6 +500,8 @@ public class IrisEntitySpawnTest {
|
||||
private final Chunk chunk = mock(Chunk.class);
|
||||
private final IrisEntitySpawn entry = spy(new IrisEntitySpawn());
|
||||
private final List<Location> positions = new ArrayList<>();
|
||||
private final List<Long> customizationSeeds = new ArrayList<>();
|
||||
private int lightLevel;
|
||||
private final MockedStatic<BukkitPlatform> platform = mockStatic(BukkitPlatform.class);
|
||||
private Material airMaterial = Material.AIR;
|
||||
private int remainingCapacity = Integer.MAX_VALUE;
|
||||
@@ -430,6 +524,7 @@ public class IrisEntitySpawnTest {
|
||||
Block block = mock(Block.class);
|
||||
when(block.getType()).thenReturn(y <= floor ? Material.STONE : y <= top ? fluid : airMaterial);
|
||||
when(block.isSolid()).thenReturn(y <= floor);
|
||||
when(block.getLightLevel()).thenReturn((byte) lightLevel);
|
||||
return block;
|
||||
});
|
||||
when(world.getBlockAt(any(Location.class))).thenAnswer(invocation -> {
|
||||
@@ -439,6 +534,8 @@ public class IrisEntitySpawnTest {
|
||||
when(definition.spawn(eq(engine), any(Location.class), any(RNG.class))).thenAnswer(invocation -> {
|
||||
Location location = invocation.getArgument(1);
|
||||
positions.add(location.clone());
|
||||
RNG customization = invocation.getArgument(2);
|
||||
customizationSeeds.add(customization.getSeed());
|
||||
Entity spawned = mock(Entity.class);
|
||||
when(spawned.getLocation()).thenReturn(location.clone());
|
||||
when(spawned.getType()).thenReturn(EntityType.TADPOLE);
|
||||
@@ -451,8 +548,8 @@ public class IrisEntitySpawnTest {
|
||||
}
|
||||
|
||||
private int spawn(IrisSpawnGroup group) {
|
||||
entry.setReferenceSpawner(new IrisSpawner().setGroup(group));
|
||||
return entry.spawn(engine, chunk, new RNG(1337L), remainingCapacity);
|
||||
return entry.spawn(engine, chunk, new RNG(1337L), remainingCapacity,
|
||||
new IrisEntitySpawn.SpawnContext(new IrisSpawner().setGroup(group), null, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in new issue
Block a user