Restore natural mob spawning

- Restore cave spawning and correct floor placement across platforms.
- Enforce category population limits and clamp ambient spawn batches.
- Honor mob-spawning rules and entity-ticking chunk admission.
- Separate chunk saving from configurable distance-despawn retention.
- Add spawning regressions and Folia gameplay acceptance coverage.
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-02 16:55:52 -04:00
1 parent 4ef6fc7cc6
commit a765e76aa3
31 files changed
+1712 -87

No files matched your search

@@ -61,11 +61,12 @@ public final class ModdedEntitySpawner {
}
double x = blockX + 0.5;
double y = blockY + 0.5;
double verticalOffset = irisEntity.getSurface().isFluid() ? 0.5 : 0;
double y = blockY + verticalOffset;
double z = blockZ + 0.5;
boolean riseEffectActive = irisEntity.isSpawnEffectRiseOutOfGround() && level.hasPlayersNearby(new NativeEntityRuntime.Position(x, y, z), PLAYER_EFFECT_RADIUS);
int spawnBlockY = riseEffectActive ? blockY - 5 : blockY;
double spawnY = spawnBlockY + 0.5;
double spawnY = spawnBlockY + verticalOffset;
NativeSpawnedEntity created = create(irisEntity, level, x, spawnY, z);
if (created == null) {
@@ -98,8 +99,7 @@ public final class ModdedEntitySpawner {
String customName = irisEntity.getCustomName();
entity.applyBase(irisEntity, customName == null ? null : colorize(customName));
boolean persistent = irisEntity.isKeepEntity() || forcePersist();
applyPersistence(entity, persistent);
applyPersistence(irisEntity, entity, forcePersist());
applyPassengers(engine, irisEntity, entity, level, blockX, blockY, blockZ, rng);
@@ -273,8 +273,8 @@ public final class ModdedEntitySpawner {
return IrisSettings.get().getWorld().isForcePersistEntities();
}
private static void applyPersistence(NativeSpawnedEntity entity, boolean persistent) {
if (!entity.persistence(persistent)) {
static void applyPersistence(IrisEntity options, NativeSpawnedEntity entity, boolean forcePersistence) {
if (!entity.persistence(options.isKeepEntity() || forcePersistence)) {
String type = entity.typeKey();
if (WARNED_PERSISTENCE_TYPES.add(type)) {
IrisLogging.warn("Iris entity: vanilla cannot persist non-serializable entity type '" + type + "'");
@@ -55,6 +55,7 @@ import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterMarker;
import art.arcane.volmlib.util.matter.slices.MarkerMatter;
import java.util.Map;
import java.util.HashSet;
import java.util.List;
import java.util.ArrayList;
@@ -511,13 +512,16 @@ public final class ModdedWorldManager implements EngineWorldManager {
}
private void spawnAmbient(NativeWorld level, int chunkX, int chunkZ, boolean initial, BiomeEnvironment environment) {
if (!NativeSpawnQueries.ambientAllowed(level, chunkX, chunkZ, initial)) {
return;
}
IrisComplex complex = engine.getComplex();
if (complex == null) {
return;
}
IrisBiome biome = environment.biome();
int chunkMobs = countChunkLivingEntities(level, chunkX, chunkZ);
Map<String, Integer> chunkMobs = NativeSpawnQueries.livingEntityCategories(level, chunkX, chunkZ);
KList<IrisEntitySpawn> pool = new KList<>();
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.dimension().getEntitySpawners()), biome, chunkX, chunkZ, chunkMobs, initial);
@@ -535,20 +539,18 @@ public final class ModdedWorldManager implements EngineWorldManager {
if (!canSpawn(spawner, chunkX, chunkZ)) {
return;
}
int spawned = spawnEntry(level, chosen, spawner, chunkX, chunkZ);
int spawned = spawnEntry(level, chosen, spawner, chunkX, chunkZ,
remainingCapacity(chosen, spawner, chunkMobs));
if (spawned > 0) {
spawner.spawn(engine, chunkX, chunkZ);
}
}
private void collectSpawns(KList<IrisEntitySpawn> pool, KList<IrisSpawner> spawners, IrisBiome biomeFilter, int chunkX, int chunkZ, int chunkMobs, boolean initial) {
private void collectSpawns(KList<IrisEntitySpawn> pool, KList<IrisSpawner> spawners, IrisBiome biomeFilter, int chunkX, int chunkZ, Map<String, Integer> chunkMobs, boolean initial) {
for (IrisSpawner spawner : spawners) {
if (spawner == null) {
continue;
}
if (spawner.getMaxEntitiesPerChunk() <= chunkMobs) {
continue;
}
if (biomeFilter != null && !spawner.isValid(biomeFilter)) {
continue;
}
@@ -559,12 +561,19 @@ public final class ModdedWorldManager implements EngineWorldManager {
for (IrisEntitySpawn entry : spawns) {
entry.setReferenceSpawner(spawner);
entry.setReferenceMarker(spawner.getReferenceMarker());
pool.add(entry);
if (remainingCapacity(entry, spawner, chunkMobs) > 0) {
pool.add(entry);
}
}
}
}
private int spawnEntry(NativeWorld level, IrisEntitySpawn entry, IrisSpawner spawner, int chunkX, int chunkZ) {
private int remainingCapacity(IrisEntitySpawn entry, IrisSpawner spawner, Map<String, Integer> counts) {
IrisEntity entity = entry.getRealEntity(engine);
return spawner.remainingCapacity(entity, counts);
}
private int spawnEntry(NativeWorld level, IrisEntitySpawn entry, IrisSpawner spawner, int chunkX, int chunkZ, int remainingCapacity) {
IrisEntity irisEntity = entry.getRealEntity(engine);
if (irisEntity == null) {
return 0;
@@ -572,7 +581,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
int min = entry.getMinSpawns();
int max = entry.getMaxSpawns();
int count = LootResolver.inclusive(RNG.r, min, max);
int count = Math.min(Math.max(0, remainingCapacity), LootResolver.inclusive(RNG.r, min, max));
if (count <= 0) {
return 0;
}
@@ -593,7 +602,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
}
IrisPosition caveFloor = caveFloors.getRandom(RNG.r);
worldX = caveFloor.getX();
worldY = caveFloor.getY() + 1;
worldY = caveFloor.getY();
worldZ = caveFloor.getZ();
} else {
worldX = (chunkX << 4) + RNG.r.i(16);
@@ -708,10 +717,6 @@ public final class ModdedWorldManager implements EngineWorldManager {
|| NativeSpawnQueries.solid(level.getBlock(worldX, worldY + 2, worldZ));
}
private int countChunkLivingEntities(NativeWorld level, int chunkX, int chunkZ) {
return NativeSpawnQueries.livingEntities(level, chunkX, chunkZ);
}
/**
* ServerChunkCache.tickChunks rebuilds NaturalSpawner.SpawnState every tick from every entity in the
* level, so read that instead of walking all entities again on an Iris timer. The count is the natural