mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 06:20:02 +00:00
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:
1 parent
4ef6fc7cc6
commit
a765e76aa3
31 files changed
+1712
-87
No files matched your search
@@ -197,7 +197,7 @@ public class IrisSettings {
|
||||
@Data
|
||||
public static class IrisSettingsWorld {
|
||||
public boolean postLoadBlockUpdates = true;
|
||||
public boolean forcePersistEntities = true;
|
||||
public boolean forcePersistEntities = false;
|
||||
public boolean ambientEntitySpawningSystem = true;
|
||||
public long asyncTickIntervalMS = 700;
|
||||
public double targetSpawnEntitiesPerChunk = 0.95;
|
||||
|
||||
@@ -28,6 +28,8 @@ import art.arcane.iris.world.entity.IrisEntitySpawn;
|
||||
import art.arcane.iris.pack.value.IrisPosition;
|
||||
import art.arcane.iris.world.entity.IrisSpawner;
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.platform.bukkit.BukkitEntityType;
|
||||
import art.arcane.iris.world.entity.IrisEntity;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.platform.bukkit.plugin.Chunks;
|
||||
import art.arcane.iris.world.task.J;
|
||||
@@ -37,13 +39,16 @@ import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.math.PowerOfTwoCoordinates;
|
||||
import art.arcane.volmlib.util.math.Position2;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import lombok.Data;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.GameRules;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import art.arcane.iris.generation.runtime.MarkerSpawnScanner.PreparedMarkerSpawn;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -381,6 +386,9 @@ final class WorldEntitySpawner {
|
||||
}
|
||||
|
||||
private void spawnAmbient(Chunk c, boolean initial, BiomeEnvironment environment) {
|
||||
if (!ambientAllowed(c, initial)) {
|
||||
return;
|
||||
}
|
||||
//@builder
|
||||
// Excluded spawners cannot spawn anything on this Minecraft version, so they never enter the selection pool.
|
||||
Predicate<IrisSpawner> filter = i -> !i.isCompatExcluded() && i.canSpawn(manager.getEngine(), c.getX(), c.getZ());
|
||||
@@ -404,20 +412,25 @@ final class WorldEntitySpawner {
|
||||
.shuffleCopy(RNG.r)
|
||||
.stream()
|
||||
.filter(filter)))
|
||||
.filter(counter)
|
||||
.flatMap((i) -> stream(i, initial))
|
||||
.filter(entry -> counter.remainingCapacity(entry, manager.getEngine()) > 0)
|
||||
.collect(Collectors.toList()))
|
||||
.getRandom();
|
||||
//@done
|
||||
if (v == null || v.getReferenceSpawner() == null)
|
||||
return;
|
||||
|
||||
spawn(c, v);
|
||||
spawn(c, v, counter.remainingCapacity(v, manager.getEngine()));
|
||||
}
|
||||
|
||||
private void spawn(Chunk c, IrisEntitySpawn i) {
|
||||
static boolean ambientAllowed(Chunk chunk, boolean initial) {
|
||||
return Boolean.TRUE.equals(chunk.getWorld().getGameRuleValue(GameRules.SPAWN_MOBS))
|
||||
&& (initial || chunk.getLoadLevel() == Chunk.LoadLevel.ENTITY_TICKING);
|
||||
}
|
||||
|
||||
private void spawn(Chunk c, IrisEntitySpawn i, int remainingCapacity) {
|
||||
IrisSpawner ref = i.getReferenceSpawner();
|
||||
int s = i.spawn(manager.getEngine(), c, RNG.r);
|
||||
int s = i.spawn(manager.getEngine(), c, RNG.r, remainingCapacity);
|
||||
actuallySpawned.addAndGet(s);
|
||||
if (s > 0) {
|
||||
ref.spawn(manager.getEngine(), c.getX(), c.getZ());
|
||||
@@ -469,26 +482,20 @@ final class WorldEntitySpawner {
|
||||
return Rarity.expandWeighted(types);
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class ChunkCounter implements Predicate<IrisSpawner> {
|
||||
private final Entity[] entities;
|
||||
private transient int index = 0;
|
||||
private transient int count = 0;
|
||||
static final class ChunkCounter {
|
||||
private final Map<String, Integer> counts = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public boolean test(IrisSpawner spawner) {
|
||||
int max = spawner.getMaxEntitiesPerChunk();
|
||||
if (max <= count)
|
||||
return false;
|
||||
|
||||
while (index < entities.length) {
|
||||
if (entities[index++] instanceof LivingEntity) {
|
||||
if (++count >= max)
|
||||
return false;
|
||||
ChunkCounter(Entity[] entities) {
|
||||
for (Entity entity : entities) {
|
||||
if (entity instanceof LivingEntity && !(entity instanceof Player)) {
|
||||
counts.merge(BukkitEntityType.of(entity.getType()).spawnCategory(), 1, Integer::sum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
int remainingCapacity(IrisEntitySpawn entry, Engine engine) {
|
||||
IrisEntity entity = entry.getRealEntity(engine);
|
||||
return entry.getReferenceSpawner().remainingCapacity(entity, counts);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,10 @@
|
||||
package art.arcane.iris.world.entity;
|
||||
|
||||
import art.arcane.volmlib.nativelib.entity.NativeEntityOptions;
|
||||
import art.arcane.volmlib.nativelib.entity.NativeEntityType;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
|
||||
import art.arcane.iris.command.IrisCommand;
|
||||
import art.arcane.iris.generation.decoration.IrisSurface;
|
||||
@@ -130,8 +134,8 @@ public class IrisEntity extends IrisRegistrant implements NativeEntityOptions {
|
||||
@Description("Should this entity be allowed to pickup items")
|
||||
private boolean pickupItems = false;
|
||||
|
||||
@Description("Should this entity be removed when far away")
|
||||
private boolean removable = false;
|
||||
@Description("Allow normal distance despawning. keepEntity and world.forcePersistEntities override this setting.")
|
||||
private boolean removable = true;
|
||||
|
||||
@Description("Entity helmet equipment")
|
||||
private IrisLoot helmet = null;
|
||||
@@ -197,6 +201,25 @@ public class IrisEntity extends IrisRegistrant implements NativeEntityOptions {
|
||||
@Description("Run raw commands when this entity is spawned. Use {x}, {y}, and {z} for location. /summon pig {x} {y} {z}")
|
||||
private KList<IrisCommand> rawCommands = new KList<>();
|
||||
|
||||
public String spawnCategory() {
|
||||
IrisPlatform platform = IrisPlatforms.getOrNull();
|
||||
if (platform == null || type == null || type.isBlank()) {
|
||||
return "misc";
|
||||
}
|
||||
PlatformRegistries registries = platform.registries();
|
||||
NativeEntityType resolved = registries == null ? null : registries.entity(type.trim().toLowerCase(Locale.ROOT));
|
||||
String category = resolved == null ? null : resolved.spawnCategory();
|
||||
return category == null || category.isBlank() ? "misc" : category;
|
||||
}
|
||||
|
||||
void applyPersistence(Entity entity, boolean forcePersistence) {
|
||||
boolean persistent = keepEntity || forcePersistence;
|
||||
entity.setPersistent(true);
|
||||
if (entity instanceof LivingEntity living) {
|
||||
living.setRemoveWhenFarAway(removable && !persistent);
|
||||
}
|
||||
}
|
||||
|
||||
public EntityType getBukkitType() {
|
||||
if (type == null || type.isBlank()) {
|
||||
return EntityType.UNKNOWN;
|
||||
@@ -292,7 +315,7 @@ public class IrisEntity extends IrisRegistrant implements NativeEntityOptions {
|
||||
e.setGravity(isGravity());
|
||||
e.setInvulnerable(isInvulnerable());
|
||||
e.setSilent(isSilent());
|
||||
e.setPersistent(isKeepEntity() || IrisSettings.get().getWorld().isForcePersistEntities());
|
||||
applyPersistence(e, IrisSettings.get().getWorld().isForcePersistEntities());
|
||||
|
||||
int gg = 0;
|
||||
for (IrisEntity i : passengers) {
|
||||
@@ -333,8 +356,6 @@ public class IrisEntity extends IrisRegistrant implements NativeEntityOptions {
|
||||
l.setLeashHolder(getLeashHolder().spawn(gen, at, rng.nextParallelRNG(234548)));
|
||||
}
|
||||
|
||||
l.setRemoveWhenFarAway(isRemovable());
|
||||
|
||||
if (getHelmet() != null && LootResolver.oneIn(rng, getHelmet().getRarity())) {
|
||||
l.getEquipment().setHelmet(getHelmet().get(gen.isStudio(), rng));
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ import org.bukkit.HeightMap;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Entity;
|
||||
|
||||
@Snippet("entity-spawn")
|
||||
@@ -60,6 +61,8 @@ import org.bukkit.entity.Entity;
|
||||
@Description("Represents an entity spawn during initial chunk generation")
|
||||
@Data
|
||||
public class IrisEntitySpawn implements Rarity {
|
||||
private static final int CAVE_COLUMN_ATTEMPTS = 8;
|
||||
private static final int CAVE_VERTICAL_ATTEMPTS = 128;
|
||||
private final transient AtomicCache<RNG> rng = new AtomicCache<>();
|
||||
private final transient AtomicCache<IrisEntity> ent = new AtomicCache<>();
|
||||
@RegistryListResource(IrisEntity.class)
|
||||
@@ -78,25 +81,32 @@ public class IrisEntitySpawn implements Rarity {
|
||||
private transient IrisSpawner referenceSpawner;
|
||||
private transient IrisMarker referenceMarker;
|
||||
|
||||
public int spawn(Engine gen, Chunk c, RNG rng) {
|
||||
public int spawn(Engine gen, Chunk c, RNG rng, int remainingCapacity) {
|
||||
if (remainingCapacity <= 0) {
|
||||
return 0;
|
||||
}
|
||||
IrisEntity definition = getRealEntity(gen);
|
||||
if (definition == null) {
|
||||
return 0;
|
||||
}
|
||||
int spawns = LootResolver.inclusive(rng, minSpawns, maxSpawns);
|
||||
int spawns = Math.min(remainingCapacity, LootResolver.inclusive(rng, minSpawns, maxSpawns));
|
||||
int s = 0;
|
||||
|
||||
if (spawns > 0) {
|
||||
for (int id = 0; id < spawns; id++) {
|
||||
int x = (c.getX() * 16) + rng.i(16);
|
||||
int z = (c.getZ() * 16) + rng.i(16);
|
||||
World world = c.getWorld();
|
||||
int h = world.getHighestBlockYAt(x, z, HeightMap.OCEAN_FLOOR);
|
||||
int hf = world.getHighestBlockYAt(x, z, HeightMap.WORLD_SURFACE);
|
||||
IrisSpawnGroup group = getReferenceSpawner().getGroup();
|
||||
Integer y = selectSurfaceSpawnY(group, definition.getSurface(), h, hf, rng);
|
||||
Location l = group == IrisSpawnGroup.CAVE ? findCaveSpawnLocation(gen, c, rng)
|
||||
: y == null ? null : new Location(world, x, y, z);
|
||||
Location l;
|
||||
if (group == IrisSpawnGroup.CAVE) {
|
||||
l = findCaveSpawnLocation(gen, c, rng, definition.getSurface());
|
||||
} else {
|
||||
int x = (c.getX() << 4) + rng.i(16);
|
||||
int z = (c.getZ() << 4) + rng.i(16);
|
||||
World world = c.getWorld();
|
||||
int h = world.getHighestBlockYAt(x, z, HeightMap.OCEAN_FLOOR);
|
||||
int hf = world.getHighestBlockYAt(x, z, HeightMap.WORLD_SURFACE);
|
||||
Integer y = selectSurfaceSpawnY(group, definition.getSurface(), h, hf, rng);
|
||||
l = y == null ? null : new Location(world, x, y, z);
|
||||
}
|
||||
|
||||
if (l != null) {
|
||||
if (referenceSpawner.getAllowedLightLevels().getMin() > 0 || referenceSpawner.getAllowedLightLevels().getMax() < 15) {
|
||||
@@ -193,16 +203,50 @@ public class IrisEntitySpawn implements Rarity {
|
||||
return null;
|
||||
}
|
||||
|
||||
static Location findCaveSpawnLocation(Engine engine, Chunk chunk, RNG rng) {
|
||||
static Location findCaveSpawnLocation(Engine engine, Chunk chunk, RNG rng, IrisSurface surface) {
|
||||
if (J.isFolia()) {
|
||||
return null;
|
||||
if (!J.isOwnedByCurrentRegion(chunk.getWorld(), chunk.getX(), chunk.getZ())) {
|
||||
return null;
|
||||
}
|
||||
return findLiveCaveSpawnLocation(chunk, rng, surface);
|
||||
}
|
||||
KList<IrisPosition> markers = engine.getMantle().findMarkers(chunk.getX(), chunk.getZ(), MarkerMatter.CAVE_FLOOR);
|
||||
return selectCaveSpawnLocation(markers, chunk.getWorld(), rng);
|
||||
}
|
||||
|
||||
static Location findLiveCaveSpawnLocation(Chunk chunk, RNG rng, IrisSurface surface) {
|
||||
boolean fluid = surface.isFluid();
|
||||
World world = chunk.getWorld();
|
||||
int minimumY = world.getMinHeight() + 1;
|
||||
for (int attempt = 0; attempt < CAVE_COLUMN_ATTEMPTS; attempt++) {
|
||||
int localX = rng.i(1, 15);
|
||||
int localZ = rng.i(1, 15);
|
||||
int worldX = (chunk.getX() << 4) + localX;
|
||||
int worldZ = (chunk.getZ() << 4) + localZ;
|
||||
int maximumY = Math.min(world.getMaxHeight() - 3,
|
||||
world.getHighestBlockYAt(worldX, worldZ, HeightMap.OCEAN_FLOOR) - 1);
|
||||
if (maximumY < minimumY) {
|
||||
continue;
|
||||
}
|
||||
int span = maximumY - minimumY + 1;
|
||||
int startY = LootResolver.inclusive(rng, minimumY, maximumY);
|
||||
int samples = Math.min(span, CAVE_VERTICAL_ATTEMPTS);
|
||||
for (int sample = 0; sample < samples; sample++) {
|
||||
int y = minimumY + Math.floorMod(startY - minimumY - sample, span);
|
||||
Block block = chunk.getBlock(localX, y, localZ);
|
||||
if (fluid ? surface.matches(block) : isAir(block.getType())
|
||||
&& chunk.getBlock(localX, y - 1, localZ).isSolid()
|
||||
&& isAir(chunk.getBlock(localX, y + 1, localZ).getType())) {
|
||||
return new Location(world, worldX, y, worldZ);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Location selectCaveSpawnLocation(KList<IrisPosition> markers, World world, RNG rng) {
|
||||
return markers.convert((marker) -> BukkitPlatform.toLocation(marker, world).add(0, 1, 0)).getRandom(rng);
|
||||
IrisPosition marker = markers.getRandom(rng);
|
||||
return marker == null ? null : BukkitPlatform.toLocation(marker, world);
|
||||
}
|
||||
|
||||
private Entity spawn100(Engine g, Location at) {
|
||||
@@ -217,6 +261,9 @@ public class IrisEntitySpawn implements Rarity {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (J.isFolia() && !J.isOwnedByCurrentRegion(at.getWorld(), at.getBlockX() >> 4, at.getBlockZ() >> 4)) {
|
||||
return null;
|
||||
}
|
||||
IrisSurface surface = irisEntity.getSurface();
|
||||
boolean checkPosition = !ignoreSurfaces || surface.isFluid();
|
||||
if (checkPosition && !surface.matches(at.clone().subtract(0, surface.isFluid() ? 0 : 1, 0).getBlock())) {
|
||||
@@ -231,7 +278,7 @@ public class IrisEntitySpawn implements Rarity {
|
||||
}
|
||||
}
|
||||
|
||||
Entity e = irisEntity.spawn(g, at.clone().add(0.5, 0.5, 0.5), rng.aquire(() -> new RNG(g.getSeedManager().getEntity())));
|
||||
Entity e = irisEntity.spawn(g, at.clone().add(0.5, surface.isFluid() ? 0.5 : 0, 0.5), rng.aquire(() -> new RNG(g.getSeedManager().getEntity())));
|
||||
if (e != null) {
|
||||
IrisLogging.debug("Spawned " + C.DARK_AQUA + "Entity<" + getEntity() + "> " + C.GREEN + e.getType() + C.LIGHT_PURPLE + " @ " + C.GRAY + e.getLocation().getX() + ", " + e.getLocation().getY() + ", " + e.getLocation().getZ());
|
||||
}
|
||||
@@ -244,26 +291,38 @@ public class IrisEntitySpawn implements Rarity {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isAir(Material material) {
|
||||
return material == Material.AIR || material == Material.CAVE_AIR || material == Material.VOID_AIR;
|
||||
}
|
||||
|
||||
private boolean isAreaClearForSpawn(Location center, Vector3d boundingBox, IrisSurface surface) {
|
||||
World world = center.getWorld();
|
||||
boolean fluid = surface.isFluid();
|
||||
int startX = fluid ? (int) Math.floor(center.getX() + 0.5 - boundingBox.x / 2)
|
||||
: center.getBlockX() - (int) (boundingBox.x / 2);
|
||||
int endX = fluid ? (int) Math.floor(Math.nextDown(center.getX() + 0.5 + boundingBox.x / 2))
|
||||
: center.getBlockX() + (int) (boundingBox.x / 2);
|
||||
int startY = fluid ? (int) Math.floor(center.getY() + 0.5) : center.getBlockY();
|
||||
int endY = fluid ? (int) Math.floor(Math.nextDown(center.getY() + 0.5 + boundingBox.y))
|
||||
: center.getBlockY() + (int) boundingBox.y;
|
||||
int startZ = fluid ? (int) Math.floor(center.getZ() + 0.5 - boundingBox.z / 2)
|
||||
: center.getBlockZ() - (int) (boundingBox.z / 2);
|
||||
int endZ = fluid ? (int) Math.floor(Math.nextDown(center.getZ() + 0.5 + boundingBox.z / 2))
|
||||
: center.getBlockZ() + (int) (boundingBox.z / 2);
|
||||
int startX = (int) Math.floor(center.getX() + 0.5 - boundingBox.x / 2);
|
||||
int endX = (int) Math.floor(Math.nextDown(center.getX() + 0.5 + boundingBox.x / 2));
|
||||
double baseY = center.getY() + (fluid ? 0.5 : 0);
|
||||
int startY = (int) Math.floor(baseY);
|
||||
int endY = (int) Math.floor(Math.nextDown(baseY + boundingBox.y));
|
||||
int startZ = (int) Math.floor(center.getZ() + 0.5 - boundingBox.z / 2);
|
||||
int endZ = (int) Math.floor(Math.nextDown(center.getZ() + 0.5 + boundingBox.z / 2));
|
||||
|
||||
if (startY < world.getMinHeight() || endY >= world.getMaxHeight()) {
|
||||
return false;
|
||||
}
|
||||
if (J.isFolia()) {
|
||||
for (int chunkX = startX >> 4; chunkX <= endX >> 4; chunkX++) {
|
||||
for (int chunkZ = startZ >> 4; chunkZ <= endZ >> 4; chunkZ++) {
|
||||
if (!J.isOwnedByCurrentRegion(world, chunkX, chunkZ)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int x = startX; x <= endX; x++) {
|
||||
for (int y = startY; y <= endY; y++) {
|
||||
for (int z = startZ; z <= endZ; z++) {
|
||||
if (fluid ? !surface.matches(world.getBlockAt(x, y, z))
|
||||
: world.getBlockAt(x, y, z).getType() != Material.AIR) {
|
||||
: !isAir(world.getBlockAt(x, y, z).getType())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Accessors(chain = true)
|
||||
@@ -62,7 +63,7 @@ public class IrisSpawner extends IrisRegistrant {
|
||||
@Description("The entity spawns to add initially. EXECUTES PER CHUNK!")
|
||||
private KList<IrisEntitySpawn> initialSpawns = new KList<>();
|
||||
|
||||
@Description("This spawner will not spawn in a given chunk if that chunk has more than the defined amount of living entities.")
|
||||
@Description("Maximum ambient entities of the selected entity's native spawn category in one chunk. Spawn groups are limited to the remaining capacity.")
|
||||
private int maxEntitiesPerChunk = 1;
|
||||
|
||||
@Description("The block of 24 hour time to contain this spawn in.")
|
||||
@@ -83,6 +84,21 @@ public class IrisSpawner extends IrisRegistrant {
|
||||
@Description("Where should these spawns be placed")
|
||||
private IrisSpawnGroup group = IrisSpawnGroup.NORMAL;
|
||||
|
||||
public int remainingCapacity(IrisEntity entity, Map<String, Integer> counts) {
|
||||
if (entity == null) {
|
||||
return 0;
|
||||
}
|
||||
String category = entity.spawnCategory();
|
||||
int count = counts.getOrDefault(category, 0);
|
||||
if ("misc".equals(category)) {
|
||||
count = 0;
|
||||
for (int population : counts.values()) {
|
||||
count += population;
|
||||
}
|
||||
}
|
||||
return Math.max(0, maxEntitiesPerChunk - count);
|
||||
}
|
||||
|
||||
public boolean isValid(IrisBiome biome) {
|
||||
return switch (group) {
|
||||
case NORMAL -> switch (biome.getInferredType()) {
|
||||
|
||||
Reference in new issue
Block a user