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:
Brian Neumann-Fopiano committed 2026-10-04 06:14:46 -04:00
1 parent 12e8bb5a09
commit be0d3b0156
33 files changed
+1932 -407

No files matched your search

@@ -6,6 +6,11 @@ import art.arcane.iris.generation.runtime.BiomeEnvironment;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.world.IrisCreator;
import art.arcane.iris.world.entity.IrisSpawner;
import art.arcane.iris.world.entity.IrisMarker;
import art.arcane.iris.world.entity.EntitySpawnSeed;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.iris.pack.value.IrisPosition;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.world.entity.IrisEntitySpawn;
import art.arcane.iris.world.entity.IrisEntity;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
@@ -28,6 +33,7 @@ import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
@@ -38,9 +44,13 @@ import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
@@ -83,7 +93,7 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
if (arguments.length != 1) {
notifySender(sender, "Use /irisspawnqa setup|frogs|zombies|stress|check|surface|cave");
notifySender(sender, "Use /irisspawnqa setup|frogs|zombies|stress|check|seeded|completion|surface|cave");
return true;
}
try {
@@ -93,6 +103,8 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
case "zombies" -> runRegion(sender, () -> spawnZombies(sender));
case "stress" -> runRegion(sender, () -> stress(sender));
case "check" -> runRegion(sender, () -> check(sender));
case "seeded" -> seeded(sender);
case "completion" -> completion(sender);
case "surface" -> view(sender, 67D);
case "cave" -> view(sender, -46D);
default -> throw new IllegalArgumentException("Unknown spawning operation");
@@ -147,6 +159,9 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
private void configureWorld(CommandSender sender, World world) {
try {
world.setDifficulty(Difficulty.NORMAL);
world.setTime(6000L);
world.setStorm(false);
world.setThundering(false);
world.setGameRule(GameRules.SPAWN_MOBS, true);
List<CompletableFuture<Void>> chunks = new ArrayList<>();
for (int chunkX = -1; chunkX <= 1; chunkX++) {
@@ -176,6 +191,11 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
Bukkit.getRegionScheduler().execute(this, world, chunkX, chunkZ, () -> {
try {
chunk.addPluginChunkTicket(this);
for (Entity entity : chunk.getEntities()) {
if (entity instanceof LivingEntity && !(entity instanceof Player)) {
entity.remove();
}
}
retained.complete(null);
} catch (Throwable error) {
retained.completeExceptionally(error);
@@ -244,12 +264,218 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
}
}
private void completion(CommandSender sender) {
World world = requireWorld();
Bukkit.getGlobalRegionScheduler().execute(this, () -> {
world.setGameRule(GameRules.SPAWN_MOBS, false);
runRegion(sender, () -> beginCompletion(sender, world));
});
}
private void beginCompletion(CommandSender sender, World world) {
try {
Engine engine = IrisToolbelt.access(world).getEngine();
Object manager = engine.getWorldManager();
Field field = manager.getClass().getDeclaredField("chunkMaintenance");
field.setAccessible(true);
Object maintenance = field.get(manager);
Method run = maintenance.getClass().getDeclaredMethod("runInitialSpawn", World.class, int.class, int.class, Runnable.class);
run.setAccessible(true);
Field pending = maintenance.getClass().getDeclaredField("markerFlagQueue");
pending.setAccessible(true);
AtomicInteger callbacks = new AtomicInteger();
Runnable callback = () -> {
require(Bukkit.isOwnedByCurrentRegion(world, 0, 0), "Initial callback ran outside its owning region");
callbacks.incrementAndGet();
Chunk chunk = world.getChunkAt(0, 0);
invokeAmbient(runtimeSpawn(chunk, "seeded"), chunk, true);
};
run.invoke(maintenance, world, 0, 0, callback);
awaitSkippedInitial(sender, world, new CompletionRun(engine, maintenance, run, pending, callback, callbacks), 0);
} catch (ReflectiveOperationException failure) {
throw new IllegalStateException("Could not start initial completion test", failure);
}
}
private void awaitSkippedInitial(CommandSender sender, World world, CompletionRun state, int attempt) {
Bukkit.getRegionScheduler().runDelayed(this, world, 0, 0, task -> {
try {
require(state.callbacks().get() == 0, "Disabled mob spawning consumed the initial pass");
if (!((Set<?>) state.pending().get(state.maintenance())).isEmpty()) {
require(attempt < 100, "Skipped initial pass did not release its pending state");
awaitSkippedInitial(sender, world, state, attempt + 1);
return;
}
Bukkit.getGlobalRegionScheduler().execute(this, () -> {
world.setGameRule(GameRules.SPAWN_MOBS, true);
runRegion(sender, () -> retryInitialCompletion(sender, world, state));
});
} catch (Throwable failure) {
fail(sender, failure);
}
}, 2L);
}
private void retryInitialCompletion(CommandSender sender, World world, CompletionRun state) {
try {
state.run().invoke(state.maintenance(), world, 0, 0, state.callback());
state.run().invoke(state.maintenance(), world, 0, 0, state.callback());
awaitInitialCompletion(sender, world, state, 0);
} catch (ReflectiveOperationException failure) {
throw new IllegalStateException("Could not retry initial spawning", failure);
}
}
private void awaitInitialCompletion(CommandSender sender, World world, CompletionRun state, int attempt) {
Bukkit.getRegionScheduler().runDelayed(this, world, 0, 0, task -> {
try {
if (state.callbacks().get() == 0 || !((Set<?>) state.pending().get(state.maintenance())).isEmpty()) {
require(attempt < 100, "Initial spawning did not complete after retry");
awaitInitialCompletion(sender, world, state, attempt + 1);
return;
}
require(state.callbacks().get() == 1, "Concurrent initial requests spawned duplicate populations");
state.run().invoke(state.maintenance(), world, 0, 0, state.callback());
Bukkit.getAsyncScheduler().runNow(this, ignored -> {
try {
require(state.engine().getMantle().getMantle().hasFlag(0, 0, MantleFlag.INITIAL_SPAWNED_MARKER), "Initial completion was not persisted");
require(state.callbacks().get() == 1, "Completed initial pass ran again");
evidence(sender, "SPAWN_QA_COMPLETION retry=true callbacks=1 persisted=true owned=true");
} catch (Throwable failure) {
fail(sender, failure);
}
});
} catch (Throwable failure) {
fail(sender, failure);
}
}, 2L);
}
private void seeded(CommandSender sender) {
requireWorld();
List<List<String>> baseline = new ArrayList<>();
sampleInitial(0, false).thenCompose(first -> {
baseline.add(first);
return sampleInitial(1, false);
}).thenCompose(second -> {
baseline.add(second);
return sampleInitial(1, false);
}).thenCompose(repeated -> {
require(baseline.get(1).equals(repeated), "Second chunk changed after reversed generation order");
return sampleInitial(0, false);
}).thenCompose(repeated -> {
require(baseline.get(0).equals(repeated), "First chunk changed after reversed generation order");
return sampleInitial(0, true);
}).thenCompose(marker -> {
baseline.add(marker);
return sampleInitial(1, true);
}).thenCompose(marker -> {
baseline.add(marker);
return sampleInitial(1, true);
}).thenCompose(repeated -> {
require(baseline.get(3).equals(repeated), "Second marker changed after reversed generation order");
return sampleInitial(0, true);
}).whenComplete((repeated, failure) -> {
if (failure != null) {
fail(sender, failure);
return;
}
try {
require(baseline.get(2).equals(repeated), "First marker changed after reversed generation order");
evidence(sender, "SPAWN_QA_SEEDED chunks=2 markers=2 repeats=2 equipment=true attributes=true");
} catch (Throwable error) {
fail(sender, error);
}
});
}
private CompletableFuture<List<String>> sampleInitial(int chunkX, boolean marker) {
CompletableFuture<List<String>> result = new CompletableFuture<>();
World world = requireWorld();
Bukkit.getRegionScheduler().execute(this, world, chunkX, 0, () -> {
try {
Chunk chunk = world.getChunkAt(chunkX, 0);
for (Entity entity : chunk.getEntities()) {
if (entity instanceof LivingEntity && !(entity instanceof Player)) {
entity.remove();
}
}
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
chunk.getBlock(x, 64, z).setType(Material.GRASS_BLOCK, false);
for (int y = 65; y <= 68; y++) {
chunk.getBlock(x, y, z).setType(Material.AIR, false);
}
}
}
RuntimeSpawn spawn = runtimeSpawn(chunk, "seeded");
IrisSpawner definition = spawn.environment().data().getSpawnerLoader().load("qa/seeded");
require(!definition.canSpawn(spawn.engine(), chunkX, 0), "Ongoing weather/time gate unexpectedly allowed spawning");
for (int i = 0; i < 100 + chunkX; i++) {
RNG.r.nextLong();
}
ambientInvocation.set(true);
try {
for (int i = 0; i < 5; i++) {
LivingEntity witness = (LivingEntity) world.spawnEntity(new Location(world, (chunkX << 4) + 8.5, 65, 8.5), EntityType.ZOMBIE);
witness.setCustomName("Population witness");
witness.setAI(false);
witness.setGravity(false);
}
} finally {
ambientInvocation.remove();
}
if (marker) {
invokeInitialMarker(spawn, definition, new IrisPosition((chunkX << 4) + 5, 64, 5));
} else {
invokeAmbient(spawn, chunk, true);
}
List<String> snapshot = new ArrayList<>();
for (Entity entity : chunk.getEntities()) {
String name = entity.getCustomName();
if (!(entity instanceof LivingEntity living) || name == null || !name.startsWith("Seeded ")) {
continue;
}
ItemStack hand = living.getEquipment().getItemInMainHand();
ItemStack helmet = living.getEquipment().getHelmet();
require(hand.hasItemMeta() && hand.getItemMeta().hasAttributeModifiers(), "Spawned gear has no authored attributes");
require(helmet != null && helmet.getType() == Material.IRON_HELMET, "Spawned helmet was not authored equipment");
Location at = entity.getLocation();
snapshot.add(entity.getType() + ":" + at.getX() + ":" + at.getY() + ":" + at.getZ()
+ ":" + HexFormat.of().formatHex(hand.serializeAsBytes())
+ ":" + HexFormat.of().formatHex(helmet.serializeAsBytes()));
}
require(snapshot.size() >= 2 && snapshot.size() <= 4, "Initial population was blocked by runtime gates or cap: " + snapshot.size());
Collections.sort(snapshot);
result.complete(List.copyOf(snapshot));
} catch (Throwable failure) {
result.completeExceptionally(failure);
}
});
return result;
}
private void invokeInitialMarker(RuntimeSpawn spawn, IrisSpawner definition, IrisPosition position) {
ambientInvocation.set(true);
try (BiomeEnvironment.Scope ignored = spawn.engine().openBiomeEnvironmentScope(spawn.environment())) {
Method method = spawn.spawner().getClass().getDeclaredMethod("spawnMarker", IrisPosition.class, IrisSpawner.class, IrisMarker.class, RNG.class, boolean.class);
method.setAccessible(true);
method.invoke(spawn.spawner(), position, definition, null, EntitySpawnSeed.marker(spawn.engine().getSeedManager().getEntity(), position.getX(), position.getY(), position.getZ()), true);
} catch (InvocationTargetException failure) {
throw new IllegalStateException("Initial marker spawning failed", failure.getCause());
} catch (ReflectiveOperationException failure) {
throw new IllegalStateException("Could not invoke initial marker spawning", failure);
} finally {
ambientInvocation.remove();
}
}
private void spawnFrogs(CommandSender sender) {
Chunk chunk = requireWorld().getChunkAt(0, 0);
RuntimeSpawn spawn = runtimeSpawn(chunk, "frogs");
getLogger().info(spawnDiagnostics(spawn, chunk));
for (int attempt = 0; attempt < 12; attempt++) {
invokeAmbient(spawn, chunk);
invokeAmbient(spawn, chunk, false);
require(count(chunk, EntityType.FROG) <= FROG_CAPACITY, "Frog batch exceeded its category capacity");
}
require(count(chunk, EntityType.FROG) == FROG_CAPACITY,
@@ -266,7 +492,7 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
require(count(chunk, EntityType.ZOMBIE) == 0, "Cave phase already ran");
RuntimeSpawn spawn = runtimeSpawn(chunk, "cave-zombies");
getLogger().info(spawnDiagnostics(spawn, chunk));
invokeAmbient(spawn, chunk);
invokeAmbient(spawn, chunk, false);
firstZombieBatch = count(chunk, EntityType.ZOMBIE);
require(firstZombieBatch == ZOMBIE_CAPACITY,
"Authored seven-zombie batch was not clamped to capacity two: " + spawnDiagnostics(spawn, chunk));
@@ -280,7 +506,7 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
Chunk chunk = requireWorld().getChunkAt(0, 0);
RuntimeSpawn spawn = runtimeSpawn(chunk, "cave-zombies");
for (int attempt = 0; attempt < 50; attempt++) {
invokeAmbient(spawn, chunk);
invokeAmbient(spawn, chunk, false);
require(count(chunk, EntityType.ZOMBIE) == ZOMBIE_CAPACITY, "Saturated zombie category exceeded capacity");
}
evidence(sender, "SPAWN_QA_STRESS attempts=50 zombies=" + count(chunk, EntityType.ZOMBIE));
@@ -371,10 +597,10 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
return diagnostics.toString();
}
private void invokeAmbient(RuntimeSpawn spawn, Chunk chunk) {
private void invokeAmbient(RuntimeSpawn spawn, Chunk chunk, boolean initial) {
ambientInvocation.set(true);
try (BiomeEnvironment.Scope ignored = spawn.engine().openBiomeEnvironmentScope(spawn.environment())) {
spawn.method().invoke(spawn.spawner(), chunk, false, spawn.environment());
spawn.method().invoke(spawn.spawner(), chunk, initial, spawn.environment());
} catch (InvocationTargetException failure) {
throw new IllegalStateException("Iris ambient spawning failed", failure.getCause());
} catch (ReflectiveOperationException failure) {
@@ -467,6 +693,9 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
}
}
private record CompletionRun(Engine engine, Object maintenance, Method run, Field pending, Runnable callback, AtomicInteger callbacks) {
}
private record RuntimeSpawn(Engine engine, Object spawner, Method method, BiomeEnvironment environment) {
}
}
@@ -0,0 +1,26 @@
{
"name": "Seeded population",
"derivative": "minecraft:plains",
"vanillaDerivative": "minecraft:plains",
"layers": [
{
"minHeight": 1,
"maxHeight": 1,
"palette": [
{
"block": "minecraft:grass_block"
}
]
}
],
"generators": [
{
"generator": "flat",
"min": 64,
"max": 64
}
],
"entitySpawners": [
"qa/seeded"
]
}
@@ -0,0 +1,29 @@
{
"type": "minecraft:husk",
"reason": "NATURAL",
"surface": "LAND",
"ai": false,
"aware": false,
"gravity": false,
"customName": "Seeded husk",
"invulnerable": true,
"helmet": {
"type": "IRON_HELMET",
"minDurability": 0.1,
"maxDurability": 0.9
},
"mainHand": {
"type": "IRON_SWORD",
"minDurability": 0.1,
"maxDurability": 0.9,
"attributes": [
{
"name": "seeded_damage",
"attribute": "minecraft:attack_damage",
"minAmount": 1.25,
"maxAmount": 6.75,
"chance": 1
}
]
}
}
@@ -0,0 +1,29 @@
{
"type": "minecraft:zombie",
"reason": "NATURAL",
"surface": "LAND",
"ai": false,
"aware": false,
"gravity": false,
"customName": "Seeded zombie",
"invulnerable": true,
"helmet": {
"type": "IRON_HELMET",
"minDurability": 0.1,
"maxDurability": 0.9
},
"mainHand": {
"type": "IRON_SWORD",
"minDurability": 0.1,
"maxDurability": 0.9,
"attributes": [
{
"name": "seeded_damage",
"attribute": "minecraft:attack_damage",
"minAmount": 1.25,
"maxAmount": 6.75,
"chance": 1
}
]
}
}
@@ -0,0 +1,33 @@
{
"group": "NORMAL",
"maxEntitiesPerChunk": 4,
"allowedLightLevels": {
"min": 0,
"max": 0
},
"weather": "DOWNFALL",
"timeBlock": {
"startHour": 20,
"endHour": 3
},
"maximumRate": {
"amount": 1,
"per": {
"days": 1
}
},
"initialSpawns": [
{
"entity": "qa/seeded-zombie",
"rarity": 1,
"minSpawns": 2,
"maxSpawns": 4
},
{
"entity": "qa/seeded-husk",
"rarity": 2,
"minSpawns": 2,
"maxSpawns": 4
}
]
}
@@ -8,7 +8,8 @@ export default {
context.expect(response.includes(expected) && !response.includes('SPAWN_QA_FAILED'), response)
return response
}
const entities = name => Object.values(context.bot.entities).filter(entity => entity.name === name)
const entities = name => Object.values(context.bot.entities).filter(entity => entity.name === name
&& entity.position.x >= 0 && entity.position.x < 16 && entity.position.z >= 0 && entity.position.z < 16)
await context.step('prepare the synthetic Iris world and dark cave', async () => {
await command('setup', 'SPAWN_QA_SETUP world=iris:spawn_qa seed=78264193', 180000)
await wait(() => Math.abs(context.bot.entity.position.y - 67) < 1, 'Fixture world teleport')
@@ -0,0 +1,24 @@
export default {
name: 'iris-seeded-entities',
description: 'Replay initial chunk and marker populations in reversed order, comparing positions and complete equipment data.',
async run(context) {
const command = async (operation, expected, timeoutMs) => {
const response = await context.command(`/irisspawnqa ${operation}`, /SPAWN_QA_[A-Z]+(?:\s|$)/, timeoutMs)
context.expect(response.includes(expected) && !response.includes('SPAWN_QA_FAILED'), response)
}
await context.step('prepare a controlled Iris world', async () => {
await command('setup', 'SPAWN_QA_SETUP', 180000)
})
await context.step('replay initial mobs despite unrelated randomness and live spawn restrictions', async () => {
await command('seeded', 'SPAWN_QA_SEEDED chunks=2 markers=2 repeats=2 equipment=true attributes=true', 60000)
await context.waitUntil(() => Object.values(context.bot.entities).some(entity => entity.name === 'zombie' || entity.name === 'husk'), {
label: 'Generated mobs reached the client', timeoutMs: 10000, intervalMs: 100
})
context.report.seededEntities = { chunks: 2, markers: 2, repeats: 2, positions: true, equipmentBytes: true, attributeIds: true }
})
await context.step('retry a skipped initial pass and persist completion once', async () => {
await command('completion', 'SPAWN_QA_COMPLETION retry=true callbacks=1 persisted=true owned=true', 30000)
context.report.seededEntities.completionRetry = true
})
}
}
@@ -44,6 +44,8 @@ import art.arcane.iris.pack.value.IrisPosition;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.iris.world.entity.IrisSpawnGroup;
import art.arcane.iris.world.entity.IrisSpawner;
import art.arcane.iris.world.entity.EntitySpawnSeed;
import art.arcane.iris.world.entity.IrisEntitySpawn.SpawnContext;
import art.arcane.iris.generation.decoration.IrisSurface;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
@@ -61,6 +63,7 @@ import java.util.List;
import java.util.ArrayList;
import java.util.Optional;
import java.util.Set;
import java.util.Comparator;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
@@ -235,6 +238,10 @@ public final class ModdedWorldManager implements EngineWorldManager {
}
boolean initialSpawnChunk(NativeWorld level, int chunkX, int chunkZ) {
if (!isEntitySpawningEnabledForCurrentWorld()
|| !NativeSpawnQueries.ambientAllowed(level, chunkX, chunkZ, true)) {
return false;
}
if (!ModdedEntitySpawner.chunksSafe(entityRuntime(level), chunkX, chunkZ)) {
return false;
}
@@ -249,54 +256,25 @@ public final class ModdedWorldManager implements EngineWorldManager {
MantleChunk<Matter> chunk = mantle.useChunk(chunkX, chunkZ);
try {
List<PreparedMarkerSpawn> markers = markerSystemEnabled()
? prepareMarkerSpawns(level, chunkX, chunkZ, chunk) : List.of();
? prepareMarkerSpawns(level, chunkX, chunkZ, chunk, true) : List.of();
Optional<BiomeEnvironment> environment = ambientSystemEnabled()
? resolveSurfaceEnvironment(chunkX, chunkZ) : Optional.empty();
chunk.raiseFlagUnchecked(INITIAL_SPAWN_COMPLETION_FLAG, () -> {
spawnPreparedMarkers(level, markers, true);
if (environment.isPresent()) {
scheduleInitialFollowUp(level, chunkX, chunkZ, environment.get());
if (ambientSystemEnabled() && engine.getComplex() == null) {
return false;
}
spawnPreparedMarkers(level, markers, true);
if (environment.isPresent()) {
try (BiomeEnvironment.Scope ignored = engine.openBiomeEnvironmentScope(environment.get())) {
spawnAmbient(level, chunkX, chunkZ, true, environment.get());
}
});
}
chunk.raiseFlagUnchecked(INITIAL_SPAWN_COMPLETION_FLAG, () -> {});
} finally {
chunk.release();
}
return true;
}
private void scheduleInitialFollowUp(NativeWorld level, int chunkX, int chunkZ, BiomeEnvironment environment) {
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
if (scheduler == null) {
IrisLogging.error("Iris could not schedule the initial entity-spawn follow-up because the modded scheduler is unavailable.");
return;
}
scheduler.laterGlobal(
() -> EngineLifecycleTasks.run(
engine,
"modded_world_manager_initial_spawn_followup",
() -> runInitialFollowUp(level, chunkX, chunkZ, environment)),
RNG.r.i(5, 200));
}
private void runInitialFollowUp(NativeWorld level, int chunkX, int chunkZ, BiomeEnvironment environment) {
Mantle<Matter> mantle = engine.getMantle().getMantle();
if (closed || engine.isClosed() || mantle.isClosed() || !isEntitySpawningEnabledForCurrentWorld()) {
return;
}
if (!level.isChunkLoaded(chunkX, chunkZ) || !mantle.isChunkLoaded(chunkX, chunkZ)) {
return;
}
if (engine.getComplex() == null) {
return;
}
if (ambientSystemEnabled()) {
try (BiomeEnvironment.Scope ignored = engine.openBiomeEnvironmentScope(environment)) {
spawnAmbient(level, chunkX, chunkZ, true, environment);
}
}
}
private void warmupMantleChunkAsync(long key, int chunkX, int chunkZ) {
if (closed || !mantleWarmups.add(key)) {
return;
@@ -380,11 +358,14 @@ public final class ModdedWorldManager implements EngineWorldManager {
if (!mantle.isChunkLoaded(chunkX, chunkZ)) {
return;
}
if (!mantle.hasFlag(chunkX, chunkZ, INITIAL_SPAWN_COMPLETION_FLAG)) {
return;
}
MantleChunk<Matter> chunk = mantle.useChunk(chunkX, chunkZ);
try {
if (markerSystemEnabled()) {
spawnPreparedMarkers(level, prepareMarkerSpawns(level, chunkX, chunkZ, chunk), false);
spawnPreparedMarkers(level, prepareMarkerSpawns(level, chunkX, chunkZ, chunk, false), false);
}
if (ambientSystemEnabled()) {
spawnAmbient(level, chunkX, chunkZ, false);
@@ -394,7 +375,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
}
}
private List<PreparedMarkerSpawn> prepareMarkerSpawns(NativeWorld level, int chunkX, int chunkZ, MantleChunk<Matter> chunk) {
private List<PreparedMarkerSpawn> prepareMarkerSpawns(NativeWorld level, int chunkX, int chunkZ, MantleChunk<Matter> chunk, boolean initial) {
int minHeight = engine.getWorld().minHeight();
KList<IrisPosition> obstructed = new KList<>();
List<PreparedMarkerSpawn> prepared = new ArrayList<>();
@@ -429,29 +410,41 @@ public final class ModdedWorldManager implements EngineWorldManager {
if (spawners.isEmpty()) {
return;
}
IrisSpawner chosen = spawners.getRandom();
if (initial) {
spawners.sort(Comparator.comparing(IrisSpawner::getLoadKey, Comparator.nullsFirst(Comparator.naturalOrder())));
}
RNG markerRng = initial ? EntitySpawnSeed.marker(engine.getSeedManager().getEntity(), worldX, worldY, worldZ)
: new RNG(RNG.r.nextLong());
IrisSpawner chosen = spawners.getRandom(markerRng);
if (chosen == null) {
return;
}
prepared.add(new PreparedMarkerSpawn(new IrisPosition(worldX, worldY, worldZ), chosen, environment));
prepared.add(new PreparedMarkerSpawn(new IrisPosition(worldX, worldY, worldZ), chosen, resolved, markerRng, environment));
});
Mantle<Matter> mantle = engine.getMantle().getMantle();
for (IrisPosition position : obstructed) {
mantle.remove(position.getX(), position.getY(), position.getZ(), MatterMarker.class);
}
if (initial) {
prepared.sort(Comparator.comparingInt((PreparedMarkerSpawn marker) -> marker.position().getX())
.thenComparingInt(marker -> marker.position().getY())
.thenComparingInt(marker -> marker.position().getZ()));
}
return List.copyOf(prepared);
}
private KList<IrisSpawner> resolveMarkerSpawners(IrisMarker marker, BiomeEnvironment environment) {
KList<IrisSpawner> spawners = new KList<>();
Set<IrisSpawner> seen = new HashSet<>();
for (String key : marker.getSpawners()) {
IrisSpawner spawner = environment.data().getSpawnerLoader().load(key);
if (spawner == null) {
IrisLogging.error("Cannot load spawner: " + key + " for marker on " + engine.getName());
continue;
}
spawner.setReferenceMarker(marker);
spawners.add(spawner);
if (!spawner.isCompatExcluded() && seen.add(spawner)) {
spawners.add(spawner);
}
}
return spawners;
}
@@ -459,32 +452,29 @@ public final class ModdedWorldManager implements EngineWorldManager {
private void spawnPreparedMarkers(NativeWorld level, List<PreparedMarkerSpawn> markers, boolean initial) {
for (PreparedMarkerSpawn marker : markers) {
try (BiomeEnvironment.Scope ignored = engine.openBiomeEnvironmentScope(marker.environment())) {
spawnFromSpawner(level, marker.position(), marker.spawner(), initial);
spawnFromSpawner(level, marker.position(), marker.rng(), new SpawnContext(marker.spawner(), marker.marker(), initial));
}
}
}
private void spawnFromSpawner(NativeWorld level, IrisPosition position, IrisSpawner spawner, boolean initial) {
KList<IrisEntitySpawn> spawns = initial ? spawner.getInitialSpawns() : spawner.getSpawns();
private void spawnFromSpawner(NativeWorld level, IrisPosition position, RNG rng, SpawnContext context) {
IrisSpawner spawner = context.spawner();
KList<IrisEntitySpawn> spawns = context.initial() ? spawner.getInitialSpawns() : spawner.getSpawns();
if (spawns.isEmpty()) {
return;
}
for (IrisEntitySpawn entry : spawns) {
entry.setReferenceSpawner(spawner);
entry.setReferenceMarker(spawner.getReferenceMarker());
}
IrisEntitySpawn chosen = rarityPick(spawns);
IrisEntitySpawn chosen = rarityPick(spawns, rng);
if (chosen == null) {
return;
}
int chunkX = position.getX() >> 4;
int chunkZ = position.getZ() >> 4;
if (!canSpawn(spawner, chunkX, chunkZ)) {
if (!canSpawn(spawner, chunkX, chunkZ, context.initial())) {
return;
}
int spawned = spawnEntryAt(level, chosen, spawner, position);
if (spawned > 0) {
int spawned = spawnEntryAt(level, chosen, position, rng, context);
if (spawned > 0 && !context.initial()) {
spawner.spawn(engine, chunkX, chunkZ);
}
}
@@ -521,9 +511,11 @@ public final class ModdedWorldManager implements EngineWorldManager {
}
IrisBiome biome = environment.biome();
Map<String, Integer> chunkMobs = NativeSpawnQueries.livingEntityCategories(level, chunkX, chunkZ);
Map<String, Integer> chunkMobs = initial ? Map.of() : NativeSpawnQueries.livingEntityCategories(level, chunkX, chunkZ);
RNG rng = initial ? EntitySpawnSeed.chunk(engine.getSeedManager().getEntity(), chunkX, chunkZ)
: new RNG(RNG.r.nextLong());
KList<IrisEntitySpawn> pool = new KList<>();
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);
@@ -531,22 +523,22 @@ public final class ModdedWorldManager implements EngineWorldManager {
return;
}
IrisEntitySpawn chosen = rarityPick(pool);
if (chosen == null || chosen.getReferenceSpawner() == null) {
SpawnCandidate chosen = rarityPick(pool, rng);
if (chosen == null) {
return;
}
IrisSpawner spawner = chosen.getReferenceSpawner();
if (!canSpawn(spawner, chunkX, chunkZ)) {
IrisSpawner spawner = chosen.spawner();
if (!canSpawn(spawner, chunkX, chunkZ, initial)) {
return;
}
int spawned = spawnEntry(level, chosen, spawner, chunkX, chunkZ,
remainingCapacity(chosen, spawner, chunkMobs));
if (spawned > 0) {
int spawned = spawnEntry(level, chosen.entry(), chunkX, chunkZ,
remainingCapacity(chosen.entry(), spawner, chunkMobs), rng, new SpawnContext(spawner, null, initial));
if (spawned > 0 && !initial) {
spawner.spawn(engine, chunkX, chunkZ);
}
}
private void collectSpawns(KList<IrisEntitySpawn> pool, KList<IrisSpawner> spawners, IrisBiome biomeFilter, int chunkX, int chunkZ, Map<String, Integer> chunkMobs, boolean initial) {
private void collectSpawns(KList<SpawnCandidate> pool, KList<IrisSpawner> spawners, IrisBiome biomeFilter, int chunkX, int chunkZ, Map<String, Integer> chunkMobs, boolean initial) {
for (IrisSpawner spawner : spawners) {
if (spawner == null) {
continue;
@@ -554,15 +546,13 @@ public final class ModdedWorldManager implements EngineWorldManager {
if (biomeFilter != null && !spawner.isValid(biomeFilter)) {
continue;
}
if (!canSpawn(spawner, chunkX, chunkZ)) {
if (!canSpawn(spawner, chunkX, chunkZ, initial)) {
continue;
}
KList<IrisEntitySpawn> spawns = initial ? spawner.getInitialSpawns() : spawner.getSpawns();
for (IrisEntitySpawn entry : spawns) {
entry.setReferenceSpawner(spawner);
entry.setReferenceMarker(spawner.getReferenceMarker());
if (remainingCapacity(entry, spawner, chunkMobs) > 0) {
pool.add(entry);
pool.add(new SpawnCandidate(entry, spawner));
}
}
}
@@ -573,7 +563,8 @@ public final class ModdedWorldManager implements EngineWorldManager {
return spawner.remainingCapacity(entity, counts);
}
private int spawnEntry(NativeWorld level, IrisEntitySpawn entry, IrisSpawner spawner, int chunkX, int chunkZ, int remainingCapacity) {
private int spawnEntry(NativeWorld level, IrisEntitySpawn entry, int chunkX, int chunkZ,
int remainingCapacity, RNG rng, SpawnContext context) {
IrisEntity irisEntity = entry.getRealEntity(engine);
if (irisEntity == null) {
return 0;
@@ -581,18 +572,24 @@ public final class ModdedWorldManager implements EngineWorldManager {
int min = entry.getMinSpawns();
int max = entry.getMaxSpawns();
int count = Math.min(Math.max(0, remainingCapacity), LootResolver.inclusive(RNG.r, min, max));
int count = Math.min(Math.max(0, remainingCapacity), LootResolver.inclusive(rng, min, max));
if (count <= 0) {
return 0;
}
RNG entityRng = entry.getRng().aquire(() -> new RNG(engine.getSeedManager().getEntity()));
IrisSpawner spawner = context.spawner();
IrisSpawnGroup group = spawner.getGroup();
KList<IrisPosition> caveFloors = group == IrisSpawnGroup.CAVE
? engine.getMantle().findMarkers(chunkX, chunkZ, MarkerMatter.CAVE_FLOOR)
: new KList<>();
if (context.initial()) {
caveFloors.sort(Comparator.comparingInt(IrisPosition::getX)
.thenComparingInt(IrisPosition::getY).thenComparingInt(IrisPosition::getZ));
}
int spawned = 0;
for (int i = 0; i < count; i++) {
RNG entityRng = EntitySpawnSeed.entity(rng.getSeed(), i);
RNG positionRng = entityRng.nextParallelRNG(0x632BE59BD9B4E019L);
int worldX;
int worldY;
int worldZ;
@@ -600,16 +597,16 @@ public final class ModdedWorldManager implements EngineWorldManager {
if (caveFloors.isEmpty()) {
continue;
}
IrisPosition caveFloor = caveFloors.getRandom(RNG.r);
IrisPosition caveFloor = caveFloors.getRandom(positionRng);
worldX = caveFloor.getX();
worldY = caveFloor.getY();
worldZ = caveFloor.getZ();
} else {
worldX = (chunkX << 4) + RNG.r.i(16);
worldZ = (chunkZ << 4) + RNG.r.i(16);
worldX = (chunkX << 4) + positionRng.i(16);
worldZ = (chunkZ << 4) + positionRng.i(16);
int surfaceY = NativeSpawnQueries.surfaceHeight(level, worldX, worldZ, false);
int solidY = NativeSpawnQueries.surfaceHeight(level, worldX, worldZ, true);
Integer selectedY = IrisEntitySpawn.selectSurfaceSpawnY(group, irisEntity.getSurface(), solidY, surfaceY, RNG.r);
Integer selectedY = IrisEntitySpawn.selectSurfaceSpawnY(group, irisEntity.getSurface(), solidY, surfaceY, positionRng);
if (selectedY == null) {
continue;
}
@@ -619,7 +616,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
continue;
}
// Rarity is applied exactly once, as pool weighting in rarityPick - never re-rolled per position (Bukkit parity).
if (!lightAllowed(spawner, level, worldX, worldY, worldZ)) {
if (!context.initial() && !lightAllowed(spawner, level, worldX, worldY, worldZ)) {
continue;
}
if (!surfaceMatches(irisEntity.getSurface(), level, worldX, worldY, worldZ)) {
@@ -635,7 +632,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
return spawned;
}
private int spawnEntryAt(NativeWorld level, IrisEntitySpawn entry, IrisSpawner spawner, IrisPosition position) {
private int spawnEntryAt(NativeWorld level, IrisEntitySpawn entry, IrisPosition position, RNG rng, SpawnContext context) {
IrisEntity irisEntity = entry.getRealEntity(engine);
if (irisEntity == null) {
return 0;
@@ -643,21 +640,21 @@ public final class ModdedWorldManager implements EngineWorldManager {
int min = entry.getMinSpawns();
int max = entry.getMaxSpawns();
int count = LootResolver.inclusive(RNG.r, min, max);
int count = LootResolver.inclusive(rng, min, max);
if (count <= 0) {
return 0;
}
exhaustMarker(spawner, position);
exhaustMarker(context.marker(), position, EntitySpawnSeed.entity(rng.getSeed(), -1));
RNG entityRng = entry.getRng().aquire(() -> new RNG(engine.getSeedManager().getEntity()));
int worldX = position.getX();
int worldY = position.getY() + 1;
int worldZ = position.getZ();
int spawned = 0;
for (int i = 0; i < count; i++) {
RNG entityRng = EntitySpawnSeed.entity(rng.getSeed(), i);
// Rarity is applied exactly once, as pool weighting in rarityPick - never re-rolled per position (Bukkit parity).
if (!lightAllowed(spawner, level, worldX, worldY, worldZ)) {
if (!context.initial() && !lightAllowed(context.spawner(), level, worldX, worldY, worldZ)) {
continue;
}
if (irisEntity.getSurface().isFluid()
@@ -672,16 +669,15 @@ public final class ModdedWorldManager implements EngineWorldManager {
return spawned;
}
private void exhaustMarker(IrisSpawner spawner, IrisPosition position) {
IrisMarker marker = spawner.getReferenceMarker();
if (marker == null || !marker.shouldExhaust()) {
private void exhaustMarker(IrisMarker marker, IrisPosition position, RNG rng) {
if (marker == null || !marker.shouldExhaust(rng)) {
return;
}
engine.getMantle().getMantle().remove(position.getX(), position.getY() - engine.getWorld().minHeight(), position.getZ(), MatterMarker.class);
}
private boolean canSpawn(IrisSpawner spawner, int chunkX, int chunkZ) {
return spawner.canSpawn(engine, chunkX, chunkZ);
private boolean canSpawn(IrisSpawner spawner, int chunkX, int chunkZ, boolean initial) {
return !spawner.isCompatExcluded() && (initial || spawner.canSpawn(engine, chunkX, chunkZ));
}
private boolean lightAllowed(IrisSpawner spawner, NativeWorld level, int worldX, int worldY, int worldZ) {
@@ -796,9 +792,9 @@ public final class ModdedWorldManager implements EngineWorldManager {
return !studio || studioSetting;
}
private IrisEntitySpawn rarityPick(KList<IrisEntitySpawn> entries) {
KList<IrisEntitySpawn> weighted = Rarity.expandWeighted(entries);
return weighted.isEmpty() ? entries.getRandom() : weighted.getRandom();
private static <T extends Rarity> T rarityPick(KList<T> entries, RNG rng) {
KList<T> weighted = Rarity.expandWeighted(entries);
return weighted.isEmpty() ? entries.getRandom(rng) : weighted.getRandom(rng);
}
private static long pack(int x, int z) {
@@ -893,7 +889,14 @@ public final class ModdedWorldManager implements EngineWorldManager {
return failure;
}
private record PreparedMarkerSpawn(IrisPosition position, IrisSpawner spawner, BiomeEnvironment environment) {
private record PreparedMarkerSpawn(IrisPosition position, IrisSpawner spawner, IrisMarker marker, RNG rng, BiomeEnvironment environment) {
}
private record SpawnCandidate(IrisEntitySpawn entry, IrisSpawner spawner) implements Rarity {
@Override
public int getRarity() {
return entry.getRarity();
}
}
private NativeEntityRuntime entityRuntime(NativeWorld level) {
@@ -0,0 +1,317 @@
package art.arcane.iris.modded;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.mantle.EngineMantle;
import art.arcane.iris.generation.runtime.BiomeEnvironment;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.SeedManager;
import art.arcane.iris.generation.terrain.InferredType;
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.validation.CompatStatus;
import art.arcane.iris.pack.value.IrisRange;
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.IrisMarker;
import art.arcane.iris.world.entity.EntitySpawnSeed;
import art.arcane.iris.structure.placement.LootResolver;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeEntityRuntime;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeSpawnQueries;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeSpawnedEntity;
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.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.math.Rarity;
import art.arcane.volmlib.util.function.Consumer4;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterMarker;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ServerLevel;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.lang.reflect.Method;
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.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ModdedInitialEntitySpawnTest {
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void initialBatchesIgnoreChunkOrderRuntimeRandomPopulationLightAndCooldowns() {
Map<Long, List<Spawned>> first;
try (Fixture fixture = new Fixture()) {
assertTrue(fixture.manager.initialSpawnChunk(fixture.world, -2, 3));
RNG.r.nextLong();
assertTrue(fixture.manager.initialSpawnChunk(fixture.world, 4, -5));
first = fixture.byChunk();
fixture.verifyInitialPolicy();
}
try (Fixture fixture = new Fixture()) {
for (int i = 0; i < 64; i++) {
RNG.r.nextLong();
}
assertTrue(fixture.manager.initialSpawnChunk(fixture.world, 4, -5));
assertTrue(fixture.manager.initialSpawnChunk(fixture.world, -2, 3));
assertEquals(first, fixture.byChunk());
assertEquals(2, fixture.completed.size());
for (List<Spawned> batch : fixture.byChunk().values()) {
assertTrue(batch.size() >= 2 && batch.size() <= 3);
}
fixture.verifyInitialPolicy();
}
}
@Test
public void initialCompletionFollowsOwnedSpawnPassAndDoesNotRepeatIt() {
try (Fixture fixture = new Fixture()) {
assertTrue(fixture.manager.initialSpawnChunk(fixture.world, -2, 3));
int count = fixture.spawned.size();
assertTrue(count > 0);
assertEquals("complete", fixture.trace.getLast());
assertTrue(fixture.trace.subList(0, fixture.trace.size() - 1).stream().allMatch("spawn"::equals));
assertTrue(fixture.manager.initialSpawnChunk(fixture.world, -2, 3));
assertEquals(count, fixture.spawned.size());
assertEquals(1, fixture.completed.size());
}
}
@Test
public void unsafeChunkAndDisabledMobSpawningLeaveCompletionForRetry() {
try (Fixture fixture = new Fixture()) {
fixture.queries.when(() -> NativeSpawnQueries.ambientAllowed(fixture.world, -2, 3, true)).thenReturn(false);
assertFalse(fixture.manager.initialSpawnChunk(fixture.world, -2, 3));
assertTrue(fixture.completed.isEmpty());
fixture.queries.when(() -> NativeSpawnQueries.ambientAllowed(fixture.world, -2, 3, true)).thenReturn(true);
fixture.entities.when(() -> ModdedEntitySpawner.chunksSafe(any(NativeEntityRuntime.class), eq(-2), eq(3)))
.thenReturn(false);
assertFalse(fixture.manager.initialSpawnChunk(fixture.world, -2, 3));
assertTrue(fixture.completed.isEmpty());
assertTrue(fixture.spawned.isEmpty());
fixture.entities.when(() -> ModdedEntitySpawner.chunksSafe(any(NativeEntityRuntime.class), eq(-2), eq(3)))
.thenReturn(true);
assertTrue(fixture.manager.initialSpawnChunk(fixture.world, -2, 3));
assertTrue(fixture.completed.contains(key(-2, 3)));
}
}
@Test
@SuppressWarnings("unchecked")
public void initialMarkerSelectionUsesOneStreamAndExcludesDuplicateAndUnsupportedSpawners() throws ReflectiveOperationException {
try (Fixture fixture = new Fixture()) {
IrisSpawner first = fixture.definitions.get(0);
IrisSpawner second = fixture.definitions.get(1);
first.setLoadKey("a");
second.setLoadKey("b");
IrisSpawner excluded = new IrisSpawner();
excluded.setLoadKey("excluded");
excluded.setCompat(CompatStatus.excludedBy(List.of()));
IrisMarker marker = new IrisMarker().setSpawners(new KList<>("b", "excluded", "a", "b"))
.setEmptyAbove(false).setExhaustionChance(0.5);
BiomeEnvironment environment = fixture.engine.getSurfaceBiomeEnvironment(0, 0);
ResourceLoader<IrisSpawner> spawners = environment.data().getSpawnerLoader();
when(spawners.load("a")).thenReturn(first);
when(spawners.load("b")).thenReturn(second);
when(spawners.load("excluded")).thenReturn(excluded);
ResourceLoader<IrisMarker> markers = mock(ResourceLoader.class);
when(environment.data().getMarkerLoader()).thenReturn(markers);
when(markers.load("test")).thenReturn(marker);
when(fixture.engine.getWorld().minHeight()).thenReturn(-64);
when(fixture.engine.getBiomeEnvironment(-25, 15, 57)).thenReturn(environment);
MantleChunk<Matter> chunk = mock(MantleChunk.class);
doAnswer(invocation -> {
Consumer4<Integer, Integer, Integer, MatterMarker> callback = invocation.getArgument(1);
callback.accept(7, 15, 9, new MatterMarker("test"));
return null;
}).when(chunk).iterate(eq(MatterMarker.class), any());
Method resolve = ModdedWorldManager.class.getDeclaredMethod("resolveMarkerSpawners", IrisMarker.class, BiomeEnvironment.class);
resolve.setAccessible(true);
KList<IrisSpawner> resolved = (KList<IrisSpawner>) resolve.invoke(fixture.manager, marker, environment);
assertEquals(List.of(second, first), resolved);
Method prepare = ModdedWorldManager.class.getDeclaredMethod("prepareMarkerSpawns", NativeWorld.class,
int.class, int.class, MantleChunk.class, boolean.class);
prepare.setAccessible(true);
Object prepared = prepare.invoke(fixture.manager, fixture.world, -2, 3, chunk, true);
Method spawn = ModdedWorldManager.class.getDeclaredMethod("spawnPreparedMarkers", NativeWorld.class, List.class, boolean.class);
spawn.setAccessible(true);
spawn.invoke(fixture.manager, fixture.world, prepared, true);
RNG rng = EntitySpawnSeed.marker(7721L, -25, -49, 57);
IrisSpawner selected = new KList<>(first, second).getRandom(rng);
IrisEntitySpawn entry = Rarity.expandWeighted(selected.getInitialSpawns()).getRandom(rng);
int count = LootResolver.inclusive(rng, entry.getMinSpawns(), entry.getMaxSpawns());
List<Spawned> expected = new ArrayList<>();
for (int ordinal = 0; ordinal < count; ordinal++) {
expected.add(new Spawned(entry.getRealEntity(fixture.engine).getType(), -25, -48, 57,
EntitySpawnSeed.entity(rng.getSeed(), ordinal).nextLong()));
}
assertEquals(expected, fixture.spawned);
Mantle<Matter> mantle = fixture.engine.getMantle().getMantle();
if (marker.shouldExhaust(EntitySpawnSeed.entity(rng.getSeed(), -1))) {
verify(mantle).remove(-25, 15, 57, MatterMarker.class);
} else {
verify(mantle, never()).remove(anyInt(), anyInt(), anyInt(), eq(MatterMarker.class));
}
}
}
private static long key(int x, int z) {
return (long) x & 0xFFFFFFFFL | (long) z << 32;
}
private record Spawned(String type, int x, int y, int z, long customization) {
}
private static final class Fixture implements AutoCloseable {
private final Engine engine = mock(Engine.class);
private final NativeWorld world = mock(NativeWorld.class);
private final ModdedWorldManager manager = new ModdedWorldManager(engine);
private final MockedStatic<IrisSettings> configured = mockStatic(IrisSettings.class);
private final MockedStatic<NativeSpawnQueries> queries = mockStatic(NativeSpawnQueries.class);
private final MockedStatic<ModdedEntitySpawner> entities = mockStatic(ModdedEntitySpawner.class);
private final List<Spawned> spawned = new ArrayList<>();
private final List<String> trace = new ArrayList<>();
private final Set<Long> completed = new HashSet<>();
private final List<IrisSpawner> definitions = new ArrayList<>();
@SuppressWarnings("unchecked")
private Fixture() {
IrisSettings settings = new IrisSettings();
settings.getWorld().setAmbientEntitySpawningSystem(true);
settings.getWorld().setMarkerEntitySpawningSystem(false);
configured.when(IrisSettings::get).thenReturn(settings);
EngineMantle engineMantle = mock(EngineMantle.class);
Mantle<Matter> mantle = mock(Mantle.class);
IrisWorld irisWorld = mock(IrisWorld.class);
SeedManager seeds = mock(SeedManager.class);
when(seeds.getEntity()).thenReturn(7721L);
when(engine.getSeedManager()).thenReturn(seeds);
when(engine.getWorld()).thenReturn(irisWorld);
when(engine.getComplex()).thenReturn(mock(IrisComplex.class));
when(engine.getMantle()).thenReturn(engineMantle);
when(engineMantle.getMantle()).thenReturn(mantle);
when(world.nativeHandle()).thenReturn(mock(ServerLevel.class));
when(world.minHeight()).thenReturn(-64);
when(world.maxHeight()).thenReturn(320);
NativeBlockState support = mock(NativeBlockState.class);
when(world.getBlock(anyInt(), anyInt(), anyInt())).thenReturn(support);
queries.when(() -> NativeSpawnQueries.solid(support)).thenReturn(true);
queries.when(() -> NativeSpawnQueries.ambientAllowed(eq(world), anyInt(), anyInt(), anyBoolean())).thenReturn(true);
queries.when(() -> NativeSpawnQueries.surfaceHeight(eq(world), anyInt(), anyInt(), anyBoolean())).thenReturn(50);
queries.when(() -> NativeSpawnQueries.livingEntityCategories(eq(world), anyInt(), anyInt()))
.thenReturn(Map.of("creature", 1000));
queries.when(() -> NativeSpawnQueries.light(eq(world), anyInt(), anyInt(), anyInt())).thenReturn(15);
entities.when(() -> ModdedEntitySpawner.chunksSafe(any(NativeEntityRuntime.class), anyInt(), anyInt())).thenReturn(true);
entities.when(() -> ModdedEntitySpawner.isAreaClearForSpawn(any(NativeEntityRuntime.class),
any(IrisEntity.class), anyInt(), anyInt(), anyInt())).thenReturn(true);
entities.when(() -> ModdedEntitySpawner.spawn(eq(engine), any(IrisEntity.class), any(NativeEntityRuntime.class),
anyInt(), anyInt(), anyInt(), any(RNG.class))).thenAnswer(invocation -> {
IrisEntity entity = invocation.getArgument(1);
RNG rng = invocation.getArgument(6);
spawned.add(new Spawned(entity.getType(), invocation.getArgument(3), invocation.getArgument(4),
invocation.getArgument(5), rng.nextLong()));
trace.add("spawn");
return mock(NativeSpawnedEntity.class);
});
when(mantle.isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
when(mantle.hasFlag(anyInt(), anyInt(), eq(ModdedWorldManager.INITIAL_SPAWN_COMPLETION_FLAG)))
.thenAnswer(invocation -> completed.contains(key(invocation.getArgument(0), invocation.getArgument(1))));
when(mantle.useChunk(anyInt(), anyInt())).thenAnswer(invocation -> {
long key = key(invocation.getArgument(0), invocation.getArgument(1));
MantleChunk<Matter> chunk = mock(MantleChunk.class);
doAnswer(flag -> {
completed.add(key);
trace.add("complete");
return null;
}).when(chunk).raiseFlagUnchecked(eq(ModdedWorldManager.INITIAL_SPAWN_COMPLETION_FLAG), any());
return chunk;
});
IrisData data = mock(IrisData.class);
ResourceLoader<IrisSpawner> loader = mock(ResourceLoader.class);
when(data.getSpawnerLoader()).thenReturn(loader);
IrisBiome biome = mock(IrisBiome.class);
when(biome.getInferredType()).thenReturn(InferredType.LAND);
when(biome.getEntitySpawners()).thenReturn(new KList<>());
IrisRegion region = mock(IrisRegion.class);
when(region.getEntitySpawners()).thenReturn(new KList<>());
IrisDimension dimension = mock(IrisDimension.class);
when(dimension.getEntitySpawners()).thenReturn(new KList<>("land"));
for (String type : List.of("minecraft:cow", "minecraft:pig")) {
IrisEntity entity = new IrisEntity().setType(type);
IrisEntitySpawn entry = spy(new IrisEntitySpawn().setMinSpawns(2).setMaxSpawns(5));
doReturn(entity).when(entry).getRealEntity(engine);
IrisSpawner spawner = spy(new IrisSpawner().setInitialSpawns(new KList<>(entry))
.setMaxEntitiesPerChunk(3).setAllowedLightLevels(new IrisRange(0, 0)));
doReturn(false).when(spawner).canSpawn(eq(engine), anyInt(), anyInt());
definitions.add(spawner);
}
when(loader.loadAll(any(KList.class))).thenAnswer(invocation -> {
KList<String> names = invocation.getArgument(0);
return names.contains("land") ? new KList<>(definitions) : new KList<>();
});
BiomeEnvironment environment = new BiomeEnvironment(1L, biome, region, dimension, data);
when(engine.getSurfaceBiomeEnvironment(anyInt(), anyInt())).thenReturn(environment);
when(engine.openBiomeEnvironmentScope(environment)).thenReturn(mock(BiomeEnvironment.Scope.class));
}
private Map<Long, List<Spawned>> byChunk() {
Map<Long, List<Spawned>> output = new HashMap<>();
for (Spawned spawn : spawned) {
output.computeIfAbsent(key(spawn.x() >> 4, spawn.z() >> 4), ignored -> new ArrayList<>()).add(spawn);
}
return output;
}
private void verifyInitialPolicy() {
queries.verify(() -> NativeSpawnQueries.livingEntityCategories(eq(world), anyInt(), anyInt()), never());
queries.verify(() -> NativeSpawnQueries.light(eq(world), anyInt(), anyInt(), anyInt()), never());
for (IrisSpawner spawner : definitions) {
verify(spawner, never()).canSpawn(eq(engine), anyInt(), anyInt());
verify(spawner, never()).spawn(eq(engine), anyInt(), anyInt());
}
}
@Override
public void close() {
manager.close();
entities.close();
queries.close();
configured.close();
}
}
}
@@ -5,11 +5,17 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeEntityRuntime;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeSpawnQueries;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.loading.ResourceLoader;
import art.arcane.iris.generation.runtime.BiomeEnvironment;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.SeedManager;
import art.arcane.iris.world.entity.IrisSpawner;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
import art.arcane.iris.generation.mantle.EngineMantle;
import art.arcane.iris.generation.biome.IrisBiome;
@@ -72,6 +78,15 @@ public class ModdedSavedBiomeConsumersTest {
when(mantle.isChunkLoaded(2, -1)).thenReturn(true);
when(mantle.useChunk(2, -1)).thenReturn(chunk);
BiomeEnvironment environment = environment();
ResourceLoader<IrisSpawner> spawners = mock(ResourceLoader.class);
when(environment.data().getSpawnerLoader()).thenReturn(spawners);
when(spawners.loadAll(any(KList.class))).thenReturn(new KList<>());
when(environment.dimension().getEntitySpawners()).thenReturn(new KList<>());
when(environment.region().getEntitySpawners()).thenReturn(new KList<>());
when(environment.biome().getEntitySpawners()).thenReturn(new KList<>());
when(engine.getSeedManager()).thenReturn(mock(SeedManager.class));
when(engine.getComplex()).thenReturn(mock(IrisComplex.class));
when(engine.openBiomeEnvironmentScope(environment)).thenReturn(mock(BiomeEnvironment.Scope.class));
when(engine.getSurfaceBiomeEnvironment(40, -8))
.thenThrow(new SavedBiomeUnavailableException("Loading", true))
.thenReturn(environment);
@@ -81,8 +96,10 @@ public class ModdedSavedBiomeConsumersTest {
ModdedWorldManager manager = new ModdedWorldManager(engine);
try (MockedStatic<IrisSettings> configured = mockStatic(IrisSettings.class);
MockedStatic<ModdedEntitySpawner> spawns = mockStatic(ModdedEntitySpawner.class)) {
MockedStatic<ModdedEntitySpawner> spawns = mockStatic(ModdedEntitySpawner.class);
MockedStatic<NativeSpawnQueries> queries = mockStatic(NativeSpawnQueries.class)) {
configured.when(IrisSettings::get).thenReturn(settings);
queries.when(() -> NativeSpawnQueries.ambientAllowed(any(NativeWorld.class), eq(2), eq(-1), eq(true))).thenReturn(true);
spawns.when(() -> ModdedEntitySpawner.chunksSafe(any(NativeEntityRuntime.class), eq(2), eq(-1))).thenReturn(true);
assertThrows(SavedBiomeUnavailableException.class, () -> manager.initialSpawnChunk(new ModdedPlatformWorld(level), 2, -1));
verify(chunk, never()).raiseFlagUnchecked(eq(ModdedWorldManager.INITIAL_SPAWN_COMPLETION_FLAG), any());
@@ -14,6 +14,7 @@ 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.iris.world.entity.IrisEntitySpawn.SpawnContext;
import art.arcane.iris.generation.decoration.IrisSurface;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.collection.KList;
@@ -196,7 +197,7 @@ public class ModdedWaterEntitySpawnTest {
doReturn(entity).when(entry).getRealEntity(engine);
IrisSpawner spawner = new IrisSpawner().setGroup(IrisSpawnGroup.CAVE);
Method spawn = ModdedWorldManager.class.getDeclaredMethod("spawnEntry", NativeWorld.class,
IrisEntitySpawn.class, IrisSpawner.class, int.class, int.class, int.class);
IrisEntitySpawn.class, int.class, int.class, int.class, RNG.class, SpawnContext.class);
spawn.setAccessible(true);
ModdedWorldManager manager = new ModdedWorldManager(engine);
try (MockedStatic<ModdedEntitySpawner> entities = mockStatic(ModdedEntitySpawner.class)) {
@@ -205,7 +206,7 @@ public class ModdedWaterEntitySpawnTest {
entities.when(() -> ModdedEntitySpawner.spawn(eq(engine), eq(entity), any(NativeEntityRuntime.class),
eq(-25), eq(-49), eq(57), any(RNG.class))).thenReturn(mock(NativeSpawnedEntity.class));
assertEquals(1, spawn.invoke(manager, world, entry, spawner, -2, 3, 1));
assertEquals(1, spawn.invoke(manager, world, entry, -2, 3, 1, new RNG(1L), new SpawnContext(spawner, null, false)));
entities.verify(() -> ModdedEntitySpawner.spawn(eq(engine), eq(entity), any(NativeEntityRuntime.class),
eq(-25), eq(-49), eq(57), any(RNG.class)));