mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +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
@@ -67,6 +67,7 @@ final class MarkerSpawnScanner {
|
||||
J.a(manager.managedTask("bukkit_world_manager_marker_scan", () -> {
|
||||
try {
|
||||
if (initialOnly && manager.getMantle().hasFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED_MARKER)) {
|
||||
manager.chunkMaintenance.recordInitialSpawnCompletion(chunkX, chunkZ);
|
||||
markerScanQueue.remove(key);
|
||||
return;
|
||||
}
|
||||
@@ -91,7 +92,7 @@ final class MarkerSpawnScanner {
|
||||
continue;
|
||||
}
|
||||
prepared.add(new PreparedMarkerSpawn(new IrisPosition(position.getX(),
|
||||
position.getY() + minimumY, position.getZ()), data.spawners, data.environment));
|
||||
position.getY() + minimumY, position.getZ()), data.spawners, data.environment, data.marker));
|
||||
}
|
||||
consumer.accept(List.copyOf(prepared));
|
||||
} finally {
|
||||
@@ -137,7 +138,7 @@ final class MarkerSpawnScanner {
|
||||
}
|
||||
|
||||
IrisPosition position = new IrisPosition((chunkX << 4) + x, y, (chunkZ << 4) + z);
|
||||
MarkerSpawnData data = markerData.computeIfAbsent(position, k -> new MarkerSpawnData(environment));
|
||||
MarkerSpawnData data = markerData.computeIfAbsent(position, k -> new MarkerSpawnData(environment, mark));
|
||||
data.requiresEmptyAbove = data.requiresEmptyAbove || mark.isEmptyAbove();
|
||||
|
||||
for (String i : mark.getSpawners()) {
|
||||
@@ -149,7 +150,6 @@ final class MarkerSpawnScanner {
|
||||
if (spawner.isCompatExcluded()) {
|
||||
continue;
|
||||
}
|
||||
spawner.setReferenceMarker(mark);
|
||||
data.spawners.add(spawner);
|
||||
}
|
||||
});
|
||||
@@ -184,16 +184,18 @@ final class MarkerSpawnScanner {
|
||||
}));
|
||||
}
|
||||
|
||||
record PreparedMarkerSpawn(IrisPosition position, KSet<IrisSpawner> spawners, BiomeEnvironment environment) {
|
||||
record PreparedMarkerSpawn(IrisPosition position, KSet<IrisSpawner> spawners, BiomeEnvironment environment, IrisMarker marker) {
|
||||
}
|
||||
|
||||
private static final class MarkerSpawnData {
|
||||
private final KSet<IrisSpawner> spawners = new KSet<>();
|
||||
private final BiomeEnvironment environment;
|
||||
private final IrisMarker marker;
|
||||
private boolean requiresEmptyAbove;
|
||||
|
||||
private MarkerSpawnData(BiomeEnvironment environment) {
|
||||
private MarkerSpawnData(BiomeEnvironment environment, IrisMarker marker) {
|
||||
this.environment = environment;
|
||||
this.marker = marker;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.generation.cache.Cache;
|
||||
import art.arcane.iris.generation.context.IrisContext;
|
||||
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter.SavedChunkMantle;
|
||||
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
|
||||
@@ -35,6 +36,7 @@ import art.arcane.volmlib.util.math.PowerOfTwoCoordinates;
|
||||
import art.arcane.volmlib.util.math.Position2;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.GameRules;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
@@ -44,6 +46,7 @@ import java.util.Comparator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
@@ -59,10 +62,12 @@ final class WorldChunkMaintenance {
|
||||
private static final int MAX_FORCED_CHUNK_UPDATES = 128;
|
||||
private static final int MAX_PENDING_MATERIALIZATIONS = 128;
|
||||
private static final int MAX_COMPLETED_MATERIALIZATIONS = 8192;
|
||||
private static final int MAX_COMPLETED_INITIAL_SPAWNS = 8192;
|
||||
|
||||
private final IrisWorldManager manager;
|
||||
private final Set<Long> mantleWarmupQueue = ConcurrentHashMap.newKeySet();
|
||||
private final Set<Long> markerFlagQueue = ConcurrentHashMap.newKeySet();
|
||||
private final Set<Long> completedInitialSpawns = Collections.synchronizedSet(new LinkedHashSet<>());
|
||||
private final Set<Long> discoveredFlagQueue = ConcurrentHashMap.newKeySet();
|
||||
private final Set<Long> chunkUpdateQueue = ConcurrentHashMap.newKeySet();
|
||||
private final Map<Long, RoutedMaterialization> materializations = new ConcurrentHashMap<>();
|
||||
@@ -85,6 +90,7 @@ final class WorldChunkMaintenance {
|
||||
}
|
||||
}
|
||||
completedMaterializations.clear();
|
||||
completedInitialSpawns.clear();
|
||||
}
|
||||
|
||||
void invalidateMaterialization(int chunkX, int chunkZ) {
|
||||
@@ -375,46 +381,107 @@ final class WorldChunkMaintenance {
|
||||
}
|
||||
}
|
||||
|
||||
void raiseInitialSpawnMarkerFlag(World world, int chunkX, int chunkZ, Runnable onFirstRaise) {
|
||||
if (world == null || onFirstRaise == null) {
|
||||
boolean isInitialSpawnComplete(int chunkX, int chunkZ) {
|
||||
return completedInitialSpawns.contains(Cache.key(chunkX, chunkZ));
|
||||
}
|
||||
|
||||
void recordInitialSpawnCompletion(int chunkX, int chunkZ) {
|
||||
rememberInitialSpawn(Cache.key(chunkX, chunkZ));
|
||||
}
|
||||
|
||||
void runInitialSpawn(World world, int chunkX, int chunkZ, Runnable initialSpawn) {
|
||||
if (world == null || initialSpawn == null || closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!J.isFolia()) {
|
||||
manager.getMantle().raiseFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED_MARKER, onFirstRaise);
|
||||
return;
|
||||
}
|
||||
|
||||
long key = Cache.key(chunkX, chunkZ);
|
||||
if (!markerFlagQueue.add(key)) {
|
||||
if (completedInitialSpawns.contains(key) || !markerFlagQueue.add(key)) {
|
||||
return;
|
||||
}
|
||||
J.a(manager.managedTask("bukkit_world_manager_initial_spawn_prepare",
|
||||
() -> prepareInitialSpawnFlag(world, chunkX, chunkZ, key, initialSpawn),
|
||||
() -> markerFlagQueue.remove(key)));
|
||||
}
|
||||
|
||||
J.a(manager.managedTask("bukkit_world_manager_spawn_marker_flag", () -> {
|
||||
boolean raised = false;
|
||||
try {
|
||||
Mantle<Matter> mantle = manager.getMantle();
|
||||
if (!mantle.hasFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED_MARKER)) {
|
||||
mantle.flag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED_MARKER, true);
|
||||
raised = true;
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
} finally {
|
||||
private void prepareInitialSpawnFlag(World world, int chunkX, int chunkZ, long key, Runnable initialSpawn) {
|
||||
try {
|
||||
if (manager.getMantle().hasFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED_MARKER)) {
|
||||
rememberInitialSpawn(key);
|
||||
markerFlagQueue.remove(key);
|
||||
}
|
||||
|
||||
if (!raised) {
|
||||
return;
|
||||
}
|
||||
boolean accepted = J.runRegion(world, chunkX, chunkZ,
|
||||
manager.managedTask("bukkit_world_manager_initial_spawn_apply",
|
||||
() -> applyInitialSpawn(world, chunkX, chunkZ, key, initialSpawn),
|
||||
() -> markerFlagQueue.remove(key)));
|
||||
if (!accepted) {
|
||||
markerFlagQueue.remove(key);
|
||||
}
|
||||
} catch (Throwable failure) {
|
||||
markerFlagQueue.remove(key);
|
||||
IrisLogging.reportError("Failed to prepare initial Iris entities in chunk " + chunkX + "," + chunkZ + ".", failure);
|
||||
}
|
||||
}
|
||||
|
||||
J.runRegion(world, chunkX, chunkZ, manager.managedTask("bukkit_world_manager_spawn_marker_callback", () -> {
|
||||
if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
|
||||
return;
|
||||
}
|
||||
onFirstRaise.run();
|
||||
}));
|
||||
}, () -> markerFlagQueue.remove(key)));
|
||||
private void applyInitialSpawn(World world, int chunkX, int chunkZ, long key, Runnable initialSpawn) {
|
||||
GenerationSessionLease completionLease = null;
|
||||
try {
|
||||
if (closed || !world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)
|
||||
|| !Boolean.TRUE.equals(world.getGameRuleValue(GameRules.SPAWN_MOBS))) {
|
||||
markerFlagQueue.remove(key);
|
||||
return;
|
||||
}
|
||||
Optional<GenerationSessionLease> admitted = initialCompletionLease();
|
||||
if (admitted.isEmpty()) {
|
||||
markerFlagQueue.remove(key);
|
||||
return;
|
||||
}
|
||||
completionLease = admitted.get();
|
||||
initialSpawn.run();
|
||||
rememberInitialSpawn(key);
|
||||
completionLease.detachThread();
|
||||
GenerationSessionLease transferred = completionLease;
|
||||
J.a(() -> persistInitialSpawnFlag(chunkX, chunkZ, key, transferred));
|
||||
completionLease = null;
|
||||
} catch (Throwable failure) {
|
||||
markerFlagQueue.remove(key);
|
||||
IrisLogging.reportError("Failed to apply initial Iris entities in chunk " + chunkX + "," + chunkZ + ".", failure);
|
||||
} finally {
|
||||
if (completionLease != null) {
|
||||
completionLease.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<GenerationSessionLease> initialCompletionLease() throws GenerationSessionException {
|
||||
Engine engine = manager.getEngine();
|
||||
GenerationSessionManager sessions = engine.getGenerationSessions();
|
||||
if (sessions == null) {
|
||||
return Optional.of(GenerationSessionLease.noop());
|
||||
}
|
||||
IrisContext context = IrisContext.get();
|
||||
if (context != null && context.getEngine() == engine && context.getGenerationSessionId() != 0L) {
|
||||
return Optional.of(sessions.continueSession("bukkit_world_manager_initial_spawn_complete", context.getGenerationSessionId()));
|
||||
}
|
||||
return sessions.tryAcquireForEngine(engine, "bukkit_world_manager_initial_spawn_complete");
|
||||
}
|
||||
|
||||
private void persistInitialSpawnFlag(int chunkX, int chunkZ, long key, GenerationSessionLease lease) {
|
||||
try (lease; IrisContext.Scope ignored = IrisContext.open(manager.getEngine(), lease.sessionId(), null)) {
|
||||
manager.getMantle().flag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED_MARKER, true);
|
||||
} catch (Throwable failure) {
|
||||
IrisLogging.reportError("Failed to save initial Iris entity completion in chunk " + chunkX + "," + chunkZ + ".", failure);
|
||||
} finally {
|
||||
markerFlagQueue.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
private void rememberInitialSpawn(long key) {
|
||||
synchronized (completedInitialSpawns) {
|
||||
completedInitialSpawns.add(key);
|
||||
if (completedInitialSpawns.size() > MAX_COMPLETED_INITIAL_SPAWNS) {
|
||||
completedInitialSpawns.remove(completedInitialSpawns.iterator().next());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void warmupMantleChunkAsync(int chunkX, int chunkZ) {
|
||||
|
||||
@@ -25,6 +25,9 @@ import art.arcane.volmlib.util.math.Rarity;
|
||||
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.world.entity.IrisEntitySpawn;
|
||||
import art.arcane.iris.world.entity.EntitySpawnSeed;
|
||||
import art.arcane.iris.world.entity.IrisMarker;
|
||||
import art.arcane.iris.world.entity.IrisEntitySpawn.SpawnContext;
|
||||
import art.arcane.iris.pack.value.IrisPosition;
|
||||
import art.arcane.iris.world.entity.IrisSpawner;
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
@@ -47,6 +50,9 @@ import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Objects;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -57,10 +63,7 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Ambient and marker entity spawning for a Bukkit Iris world. Every count and spawn hops onto the
|
||||
@@ -308,12 +311,15 @@ final class WorldEntitySpawner {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!manager.chunkMaintenance.isInitialSpawnComplete(c.getX(), c.getZ())) {
|
||||
spawnInitially(c);
|
||||
return;
|
||||
}
|
||||
if (IrisSettings.get().getWorld().isMarkerEntitySpawningSystem()) {
|
||||
manager.markerScanner.scanMarkerSpawners(c, false, markers -> {
|
||||
for (PreparedMarkerSpawn marker : markers) {
|
||||
spawnPreparedMarker(marker, false);
|
||||
}
|
||||
prepareInitialSpawn(c, markers);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -333,7 +339,9 @@ final class WorldEntitySpawner {
|
||||
}
|
||||
|
||||
void spawnInitially(Chunk chunk) {
|
||||
if (manager.getEngine().isClosed() || !isEntitySpawningEnabledForCurrentWorld()) {
|
||||
if (manager.getEngine().isClosed() || !isEntitySpawningEnabledForCurrentWorld()
|
||||
|| manager.chunkMaintenance.isInitialSpawnComplete(chunk.getX(), chunk.getZ())
|
||||
|| !Boolean.TRUE.equals(chunk.getWorld().getGameRuleValue(GameRules.SPAWN_MOBS))) {
|
||||
return;
|
||||
}
|
||||
if (IrisSettings.get().getWorld().isMarkerEntitySpawningSystem()) {
|
||||
@@ -356,71 +364,94 @@ final class WorldEntitySpawner {
|
||||
}
|
||||
}
|
||||
Optional<BiomeEnvironment> preparedEnvironment = environment;
|
||||
manager.chunkMaintenance.raiseInitialSpawnMarkerFlag(chunk.getWorld(), chunk.getX(), chunk.getZ(), () -> {
|
||||
for (PreparedMarkerSpawn marker : markers) {
|
||||
List<PreparedMarkerSpawn> ordered = new ArrayList<>(markers);
|
||||
ordered.sort(Comparator.comparingInt((PreparedMarkerSpawn marker) -> marker.position().getX())
|
||||
.thenComparingInt(marker -> marker.position().getY())
|
||||
.thenComparingInt(marker -> marker.position().getZ()));
|
||||
manager.chunkMaintenance.runInitialSpawn(chunk.getWorld(), chunk.getX(), chunk.getZ(), () -> {
|
||||
for (PreparedMarkerSpawn marker : ordered) {
|
||||
spawnPreparedMarker(marker, true);
|
||||
}
|
||||
if (preparedEnvironment.isPresent()) {
|
||||
J.runRegion(chunk.getWorld(), chunk.getX(), chunk.getZ(),
|
||||
manager.managedTask("bukkit_world_manager_initial_spawn_followup", () -> {
|
||||
if (!chunk.getWorld().isChunkLoaded(chunk.getX(), chunk.getZ())) {
|
||||
return;
|
||||
}
|
||||
BiomeEnvironment selected = preparedEnvironment.get();
|
||||
try (BiomeEnvironment.Scope ignored = manager.getEngine().openBiomeEnvironmentScope(selected)) {
|
||||
spawnAmbient(chunk, true, selected);
|
||||
}
|
||||
}), RNG.r.i(5, 200));
|
||||
BiomeEnvironment selected = preparedEnvironment.get();
|
||||
try (BiomeEnvironment.Scope ignored = manager.getEngine().openBiomeEnvironmentScope(selected)) {
|
||||
spawnAmbient(chunk, true, selected);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void spawnPreparedMarker(PreparedMarkerSpawn marker, boolean initial) {
|
||||
IrisSpawner spawner = new KList<>(marker.spawners()).getRandom();
|
||||
if (spawner == null || manager.getEngine().isClosed()) {
|
||||
if (manager.getEngine().isClosed()) {
|
||||
return;
|
||||
}
|
||||
RNG random = initial ? EntitySpawnSeed.marker(manager.getEngine().getSeedManager().getEntity(),
|
||||
marker.position().getX(), marker.position().getY(), marker.position().getZ()) : new RNG(RNG.r.nextLong());
|
||||
KList<IrisSpawner> spawners = new KList<>(marker.spawners());
|
||||
if (initial) {
|
||||
spawners.sort(Comparator.comparing(spawner -> Objects.toString(spawner.getLoadKey(), "")));
|
||||
}
|
||||
IrisSpawner spawner = spawners.getRandom(random);
|
||||
if (spawner == null) {
|
||||
return;
|
||||
}
|
||||
try (BiomeEnvironment.Scope ignored = manager.getEngine().openBiomeEnvironmentScope(marker.environment())) {
|
||||
spawnMarker(marker.position(), spawner, initial);
|
||||
spawnMarker(marker.position(), spawner, marker.marker(), random, initial);
|
||||
}
|
||||
}
|
||||
|
||||
private void spawnAmbient(Chunk c, boolean initial, BiomeEnvironment environment) {
|
||||
if (!ambientAllowed(c, initial)) {
|
||||
private void spawnAmbient(Chunk chunk, boolean initial, BiomeEnvironment environment) {
|
||||
if (!ambientAllowed(chunk, 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());
|
||||
ChunkCounter counter = new ChunkCounter(c.getEntities());
|
||||
|
||||
IrisBiome biome = environment.biome();
|
||||
IrisEntitySpawn v = spawnRandomly(Stream.concat(environment.data().getSpawnerLoader()
|
||||
.loadAll(environment.dimension().getEntitySpawners())
|
||||
.shuffleCopy(RNG.r)
|
||||
.stream()
|
||||
.filter(filter)
|
||||
.filter((i) -> i.isValid(biome)),
|
||||
Stream.concat(environment.data()
|
||||
.getSpawnerLoader()
|
||||
.loadAll(environment.region().getEntitySpawners())
|
||||
.shuffleCopy(RNG.r)
|
||||
.stream()
|
||||
.filter(filter),
|
||||
environment.data().getSpawnerLoader()
|
||||
.loadAll(environment.biome().getEntitySpawners())
|
||||
.shuffleCopy(RNG.r)
|
||||
.stream()
|
||||
.filter(filter)))
|
||||
.flatMap((i) -> stream(i, initial))
|
||||
.filter(entry -> counter.remainingCapacity(entry, manager.getEngine()) > 0)
|
||||
.collect(Collectors.toList()))
|
||||
.getRandom();
|
||||
//@done
|
||||
if (v == null || v.getReferenceSpawner() == null)
|
||||
ChunkCounter counter = initial ? null : new ChunkCounter(chunk.getEntities());
|
||||
List<SpawnSelection> pool = new ArrayList<>();
|
||||
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.dimension().getEntitySpawners()),
|
||||
environment.biome(), chunk, initial, counter);
|
||||
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.region().getEntitySpawners()),
|
||||
null, chunk, initial, counter);
|
||||
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.biome().getEntitySpawners()),
|
||||
null, chunk, initial, counter);
|
||||
RNG random = initial ? EntitySpawnSeed.chunk(manager.getEngine().getSeedManager().getEntity(),
|
||||
chunk.getX(), chunk.getZ()) : new RNG(RNG.r.nextLong());
|
||||
SpawnSelection selected = selectSpawn(pool, random);
|
||||
if (selected == null) {
|
||||
return;
|
||||
}
|
||||
int capacity = initial ? Math.max(0, selected.spawner().getMaxEntitiesPerChunk())
|
||||
: counter.remainingCapacity(selected.entry(), selected.spawner(), manager.getEngine());
|
||||
int spawned = selected.entry().spawn(manager.getEngine(), chunk, random, capacity,
|
||||
new SpawnContext(selected.spawner(), null, initial));
|
||||
actuallySpawned.addAndGet(spawned);
|
||||
if (spawned > 0 && !initial) {
|
||||
selected.spawner().spawn(manager.getEngine(), chunk.getX(), chunk.getZ());
|
||||
}
|
||||
}
|
||||
|
||||
spawn(c, v, counter.remainingCapacity(v, manager.getEngine()));
|
||||
private void collectSpawns(List<SpawnSelection> pool, KList<IrisSpawner> spawners, IrisBiome biome,
|
||||
Chunk chunk, boolean initial, ChunkCounter counter) {
|
||||
for (IrisSpawner spawner : spawners) {
|
||||
if (!spawnerAllowed(spawner, manager.getEngine(), chunk.getX(), chunk.getZ(), initial)
|
||||
|| biome != null && !spawner.isValid(biome)) {
|
||||
continue;
|
||||
}
|
||||
for (IrisEntitySpawn entry : initial ? spawner.getInitialSpawns() : spawner.getSpawns()) {
|
||||
IrisEntity entity = entry.getRealEntity(manager.getEngine());
|
||||
int capacity = initial ? Math.max(0, spawner.getMaxEntitiesPerChunk())
|
||||
: counter.remainingCapacity(entry, spawner, manager.getEngine());
|
||||
if (entity != null && capacity > 0) {
|
||||
pool.add(new SpawnSelection(entry, spawner));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static boolean spawnerAllowed(IrisSpawner spawner, Engine engine, int chunkX, int chunkZ, boolean initial) {
|
||||
return !spawner.isCompatExcluded() && (initial || spawner.canSpawn(engine, chunkX, chunkZ));
|
||||
}
|
||||
|
||||
static SpawnSelection selectSpawn(List<SpawnSelection> pool, RNG random) {
|
||||
return Rarity.expandWeighted(pool).getRandom(random);
|
||||
}
|
||||
|
||||
static boolean ambientAllowed(Chunk chunk, boolean initial) {
|
||||
@@ -428,46 +459,22 @@ final class WorldEntitySpawner {
|
||||
&& (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, remainingCapacity);
|
||||
actuallySpawned.addAndGet(s);
|
||||
if (s > 0) {
|
||||
ref.spawn(manager.getEngine(), c.getX(), c.getZ());
|
||||
}
|
||||
}
|
||||
|
||||
private void spawn(IrisPosition pos, IrisEntitySpawn i) {
|
||||
IrisSpawner ref = i.getReferenceSpawner();
|
||||
if (!ref.canSpawn(manager.getEngine(), PowerOfTwoCoordinates.blockToChunkFloor(pos.getX()), PowerOfTwoCoordinates.blockToChunkFloor(pos.getZ())))
|
||||
return;
|
||||
|
||||
int s = i.spawn(manager.getEngine(), pos, RNG.r);
|
||||
actuallySpawned.addAndGet(s);
|
||||
if (s > 0) {
|
||||
ref.spawn(manager.getEngine(), PowerOfTwoCoordinates.blockToChunkFloor(pos.getX()), PowerOfTwoCoordinates.blockToChunkFloor(pos.getZ()));
|
||||
}
|
||||
}
|
||||
|
||||
private void spawnMarker(IrisPosition block, IrisSpawner spawner, boolean initial) {
|
||||
KList<IrisEntitySpawn> s = initial ? spawner.getInitialSpawns() : spawner.getSpawns();
|
||||
if (s.isEmpty()) {
|
||||
private void spawnMarker(IrisPosition position, IrisSpawner spawner, IrisMarker marker, RNG random, boolean initial) {
|
||||
int chunkX = PowerOfTwoCoordinates.blockToChunkFloor(position.getX());
|
||||
int chunkZ = PowerOfTwoCoordinates.blockToChunkFloor(position.getZ());
|
||||
if (!spawnerAllowed(spawner, manager.getEngine(), chunkX, chunkZ, initial)) {
|
||||
return;
|
||||
}
|
||||
|
||||
IrisEntitySpawn ss = spawnRandomly(s).getRandom();
|
||||
ss.setReferenceSpawner(spawner);
|
||||
ss.setReferenceMarker(spawner.getReferenceMarker());
|
||||
spawn(block, ss);
|
||||
}
|
||||
|
||||
private Stream<IrisEntitySpawn> stream(IrisSpawner s, boolean initial) {
|
||||
for (IrisEntitySpawn i : initial ? s.getInitialSpawns() : s.getSpawns()) {
|
||||
i.setReferenceSpawner(s);
|
||||
i.setReferenceMarker(s.getReferenceMarker());
|
||||
KList<IrisEntitySpawn> entries = initial ? spawner.getInitialSpawns() : spawner.getSpawns();
|
||||
IrisEntitySpawn selected = Rarity.expandWeighted(entries).getRandom(random);
|
||||
if (selected == null) {
|
||||
return;
|
||||
}
|
||||
int spawned = selected.spawn(manager.getEngine(), position, random, new SpawnContext(spawner, marker, initial));
|
||||
actuallySpawned.addAndGet(spawned);
|
||||
if (spawned > 0 && !initial) {
|
||||
spawner.spawn(manager.getEngine(), chunkX, chunkZ);
|
||||
}
|
||||
|
||||
return (initial ? s.getInitialSpawns() : s.getSpawns()).stream();
|
||||
}
|
||||
|
||||
boolean isEntitySpawningEnabledForCurrentWorld() {
|
||||
@@ -478,8 +485,11 @@ final class WorldEntitySpawner {
|
||||
return IrisSettings.get().getStudio().isEntitySpawning();
|
||||
}
|
||||
|
||||
private KList<IrisEntitySpawn> spawnRandomly(List<IrisEntitySpawn> types) {
|
||||
return Rarity.expandWeighted(types);
|
||||
record SpawnSelection(IrisEntitySpawn entry, IrisSpawner spawner) implements Rarity {
|
||||
@Override
|
||||
public int getRarity() {
|
||||
return entry.getRarity();
|
||||
}
|
||||
}
|
||||
|
||||
static final class ChunkCounter {
|
||||
@@ -493,9 +503,9 @@ final class WorldEntitySpawner {
|
||||
}
|
||||
}
|
||||
|
||||
int remainingCapacity(IrisEntitySpawn entry, Engine engine) {
|
||||
int remainingCapacity(IrisEntitySpawn entry, IrisSpawner spawner, Engine engine) {
|
||||
IrisEntity entity = entry.getRealEntity(engine);
|
||||
return entry.getReferenceSpawner().remainingCapacity(entity, counts);
|
||||
return spawner.remainingCapacity(entity, counts);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package art.arcane.iris.world.entity;
|
||||
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
|
||||
public final class EntitySpawnSeed {
|
||||
private static final long CHUNK_SALT = 0xA0761D6478BD642FL;
|
||||
private static final long MARKER_SALT = 0xE7037ED1A0B428DBL;
|
||||
private static final long ENTITY_SALT = 0x8EBC6AF09C88C6E3L;
|
||||
|
||||
private EntitySpawnSeed() {
|
||||
}
|
||||
|
||||
public static RNG chunk(long entitySeed, int x, int z) {
|
||||
long seed = mix(entitySeed ^ CHUNK_SALT);
|
||||
seed = mix(seed ^ (long) x * 0xD6E8FEB86659FD93L);
|
||||
return new RNG(mix(seed ^ (long) z * 0x8CB92BA72F3D8DD7L));
|
||||
}
|
||||
|
||||
public static RNG marker(long entitySeed, int x, int y, int z) {
|
||||
long seed = mix(entitySeed ^ MARKER_SALT);
|
||||
seed = mix(seed ^ (long) x * 0xD6E8FEB86659FD93L);
|
||||
seed = mix(seed ^ (long) y * 0xA5A3564E27F886A7L);
|
||||
return new RNG(mix(seed ^ (long) z * 0x8CB92BA72F3D8DD7L));
|
||||
}
|
||||
|
||||
public static RNG entity(long batchSeed, int ordinal) {
|
||||
return new RNG(mix(batchSeed ^ ENTITY_SALT ^ (long) ordinal * 0x9E3779B97F4A7C15L));
|
||||
}
|
||||
|
||||
private static long mix(long seed) {
|
||||
seed = (seed ^ seed >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
seed = (seed ^ seed >>> 27) * 0x94D049BB133111EBL;
|
||||
return seed ^ seed >>> 31;
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,6 @@ import org.bukkit.inventory.meta.ItemMeta;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Snippet("attribute-modifier")
|
||||
@@ -134,12 +133,12 @@ public class IrisAttributeModifier {
|
||||
}
|
||||
}
|
||||
|
||||
private AttributeModifier createModifier(RNG rng) {
|
||||
NamespacedKey key = NamespacedKey.minecraft(generateModifierKey());
|
||||
AttributeModifier createModifier(RNG rng) {
|
||||
NamespacedKey key = NamespacedKey.minecraft(generateModifierKey(rng));
|
||||
return new AttributeModifier(key, getAmount(rng), resolveOperation(), EquipmentSlotGroup.ANY);
|
||||
}
|
||||
|
||||
private String generateModifierKey() {
|
||||
private String generateModifierKey(RNG rng) {
|
||||
String source = getName() == null ? "modifier" : getName();
|
||||
String normalized = source.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9/._-]", "_");
|
||||
if (normalized.isBlank()) {
|
||||
@@ -148,7 +147,8 @@ public class IrisAttributeModifier {
|
||||
if (normalized.length() > 32) {
|
||||
normalized = normalized.substring(0, 32);
|
||||
}
|
||||
String random = UUID.randomUUID().toString().replace("-", "");
|
||||
String random = Long.toUnsignedString(rng.nextLong(), 16)
|
||||
+ "_" + Long.toUnsignedString(rng.nextLong(), 16);
|
||||
return normalized + "_" + random;
|
||||
}
|
||||
}
|
||||
@@ -268,7 +268,8 @@ public class IrisEntity extends IrisRegistrant implements NativeEntityOptions {
|
||||
}
|
||||
|
||||
public Entity spawn(Engine gen, Location at) {
|
||||
return spawn(gen, at, new RNG(at.hashCode()));
|
||||
return spawn(gen, at, EntitySpawnSeed.marker(gen.getSeedManager().getEntity(),
|
||||
at.getBlockX(), at.getBlockY(), at.getBlockZ()));
|
||||
}
|
||||
|
||||
public Entity spawn(Engine gen, Location at, RNG rng) {
|
||||
@@ -531,17 +532,19 @@ public class IrisEntity extends IrisRegistrant implements NativeEntityOptions {
|
||||
return specialType != null && !specialType.equals("");
|
||||
}
|
||||
|
||||
private static final class BukkitOps {
|
||||
static final class BukkitOps {
|
||||
private static void persistVillager(Villager villager) {
|
||||
villager.setPersistent(true);
|
||||
}
|
||||
|
||||
private static void bindLoot(IrisEntity entity, Engine gen, Lootable l, Location finalAt, RNG rng) {
|
||||
static void bindLoot(IrisEntity entity, Engine gen, Lootable l, Location finalAt, RNG rng) {
|
||||
IrisData definitions = gen.getData();
|
||||
long lootSeed = EntitySpawnSeed.entity(rng.getSeed(), -2).getSeed();
|
||||
NamespacedKey lootKey = new NamespacedKey("iris", "loot-" + Long.toUnsignedString(lootSeed, 16));
|
||||
l.setLootTable(new LootTable() {
|
||||
@Override
|
||||
public NamespacedKey getKey() {
|
||||
return new NamespacedKey("iris", "loot-" + entity.hashCode());
|
||||
return lootKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -54,6 +54,9 @@ import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Entity;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Comparator;
|
||||
|
||||
@Snippet("entity-spawn")
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@@ -63,7 +66,6 @@ import org.bukkit.entity.Entity;
|
||||
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)
|
||||
@Required
|
||||
@@ -78,10 +80,8 @@ public class IrisEntitySpawn implements Rarity {
|
||||
@MinNumber(1)
|
||||
@Description("The max of this entity to spawn")
|
||||
private int maxSpawns = 1;
|
||||
private transient IrisSpawner referenceSpawner;
|
||||
private transient IrisMarker referenceMarker;
|
||||
|
||||
public int spawn(Engine gen, Chunk c, RNG rng, int remainingCapacity) {
|
||||
public int spawn(Engine gen, Chunk chunk, RNG rng, int remainingCapacity, SpawnContext context) {
|
||||
if (remainingCapacity <= 0) {
|
||||
return 0;
|
||||
}
|
||||
@@ -89,42 +89,39 @@ public class IrisEntitySpawn implements Rarity {
|
||||
if (definition == null) {
|
||||
return 0;
|
||||
}
|
||||
int spawns = Math.min(remainingCapacity, LootResolver.inclusive(rng, minSpawns, maxSpawns));
|
||||
int s = 0;
|
||||
|
||||
if (spawns > 0) {
|
||||
for (int id = 0; id < spawns; id++) {
|
||||
IrisSpawnGroup group = getReferenceSpawner().getGroup();
|
||||
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) {
|
||||
if (referenceSpawner.getAllowedLightLevels().contains(l.getBlock().getLightLevel())) {
|
||||
if (spawn100(gen, l) != null) {
|
||||
s++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (spawn100(gen, l) != null) {
|
||||
s++;
|
||||
}
|
||||
}
|
||||
}
|
||||
long batchSeed = rng.getSeed();
|
||||
int count = Math.min(remainingCapacity, LootResolver.inclusive(rng, minSpawns, maxSpawns));
|
||||
IrisSpawnGroup group = context.spawner().getGroup();
|
||||
KList<IrisPosition> caveMarkers = null;
|
||||
if (context.initial() && group == IrisSpawnGroup.CAVE && !J.isFolia()) {
|
||||
caveMarkers = gen.getMantle().findMarkers(chunk.getX(), chunk.getZ(), MarkerMatter.CAVE_FLOOR);
|
||||
caveMarkers.sort(Comparator.comparingInt(IrisPosition::getX)
|
||||
.thenComparingInt(IrisPosition::getY).thenComparingInt(IrisPosition::getZ));
|
||||
}
|
||||
int spawned = 0;
|
||||
for (int ordinal = 0; ordinal < count; ordinal++) {
|
||||
RNG attemptRng = EntitySpawnSeed.entity(batchSeed, ordinal);
|
||||
RNG positionRng = attemptRng.nextParallelRNG(0x632BE59BD9B4E019L);
|
||||
Location location;
|
||||
if (group == IrisSpawnGroup.CAVE) {
|
||||
location = caveMarkers == null
|
||||
? findCaveSpawnLocation(gen, chunk, positionRng, definition.getSurface())
|
||||
: selectCaveSpawnLocation(caveMarkers, chunk.getWorld(), positionRng);
|
||||
} else {
|
||||
int x = (chunk.getX() << 4) + positionRng.i(16);
|
||||
int z = (chunk.getZ() << 4) + positionRng.i(16);
|
||||
World world = chunk.getWorld();
|
||||
int floor = world.getHighestBlockYAt(x, z, HeightMap.OCEAN_FLOOR);
|
||||
int top = world.getHighestBlockYAt(x, z, HeightMap.WORLD_SURFACE);
|
||||
Integer y = selectSurfaceSpawnY(group, definition.getSurface(), floor, top, positionRng);
|
||||
location = y == null ? null : new Location(world, x, y, z);
|
||||
}
|
||||
if (location != null && lightAllowed(location, context)
|
||||
&& spawn100(gen, location, attemptRng, false) != null) {
|
||||
spawned++;
|
||||
}
|
||||
}
|
||||
|
||||
return s;
|
||||
return spawned;
|
||||
}
|
||||
|
||||
public static Integer selectSurfaceSpawnY(IrisSpawnGroup group, IrisSurface surface,
|
||||
@@ -138,43 +135,41 @@ public class IrisEntitySpawn implements Rarity {
|
||||
return top > floor ? LootResolver.inclusive(rng, floor + 1, top) : null;
|
||||
}
|
||||
|
||||
public int spawn(Engine gen, IrisPosition c, RNG rng) {
|
||||
int spawns = LootResolver.inclusive(rng, minSpawns, maxSpawns);
|
||||
int s = 0;
|
||||
|
||||
if (!BukkitWorldBinding.tryBind(gen.getWorld())) {
|
||||
public int spawn(Engine gen, IrisPosition position, RNG rng, SpawnContext context) {
|
||||
long batchSeed = rng.getSeed();
|
||||
int count = LootResolver.inclusive(rng, minSpawns, maxSpawns);
|
||||
if (count <= 0 || !BukkitWorldBinding.tryBind(gen.getWorld())) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
World world = BukkitWorldBinding.world(gen.getWorld());
|
||||
if (spawns > 0) {
|
||||
|
||||
if (referenceMarker != null && referenceMarker.shouldExhaust()) {
|
||||
if (J.isFolia()) {
|
||||
J.a(() -> gen.getMantle().getMantle().remove(c.getX(), c.getY() - gen.getWorld().minHeight(), c.getZ(), MatterMarker.class));
|
||||
} else {
|
||||
gen.getMantle().getMantle().remove(c.getX(), c.getY() - gen.getWorld().minHeight(), c.getZ(), MatterMarker.class);
|
||||
}
|
||||
}
|
||||
|
||||
for (int id = 0; id < spawns; id++) {
|
||||
Location l = BukkitPlatform.toLocation(c, world).add(0, 1, 0);
|
||||
|
||||
if (referenceSpawner.getAllowedLightLevels().getMin() > 0 || referenceSpawner.getAllowedLightLevels().getMax() < 15) {
|
||||
if (referenceSpawner.getAllowedLightLevels().contains(l.getBlock().getLightLevel())) {
|
||||
if (spawn100(gen, l, true) != null) {
|
||||
s++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (spawn100(gen, l, true) != null) {
|
||||
s++;
|
||||
}
|
||||
}
|
||||
if (context.marker() != null && context.marker().shouldExhaust(EntitySpawnSeed.entity(batchSeed, -1))) {
|
||||
if (J.isFolia()) {
|
||||
J.a(() -> gen.getMantle().getMantle().remove(position.getX(),
|
||||
position.getY() - gen.getWorld().minHeight(), position.getZ(), MatterMarker.class));
|
||||
} else {
|
||||
gen.getMantle().getMantle().remove(position.getX(),
|
||||
position.getY() - gen.getWorld().minHeight(), position.getZ(), MatterMarker.class);
|
||||
}
|
||||
}
|
||||
int spawned = 0;
|
||||
for (int ordinal = 0; ordinal < count; ordinal++) {
|
||||
Location location = BukkitPlatform.toLocation(position, world).add(0, 1, 0);
|
||||
if (lightAllowed(location, context)
|
||||
&& spawn100(gen, location, EntitySpawnSeed.entity(batchSeed, ordinal), true) != null) {
|
||||
spawned++;
|
||||
}
|
||||
}
|
||||
return spawned;
|
||||
}
|
||||
|
||||
return s;
|
||||
private static boolean lightAllowed(Location location, SpawnContext context) {
|
||||
if (context.initial()) {
|
||||
return true;
|
||||
}
|
||||
IrisSpawner spawner = context.spawner();
|
||||
return spawner.getAllowedLightLevels().getMin() <= 0
|
||||
&& spawner.getAllowedLightLevels().getMax() >= 15
|
||||
|| spawner.getAllowedLightLevels().contains(location.getBlock().getLightLevel());
|
||||
}
|
||||
|
||||
public IrisEntity getRealEntity(Engine g) {
|
||||
@@ -191,16 +186,13 @@ public class IrisEntitySpawn implements Rarity {
|
||||
return entity == null || entity.isCompatExcluded() ? null : entity;
|
||||
}
|
||||
|
||||
public Entity spawn(Engine g, Location at) {
|
||||
if (getRealEntity(g) == null) {
|
||||
public Entity spawn(Engine engine, Location at) {
|
||||
if (getRealEntity(engine) == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (LootResolver.oneIn(rng.aquire(() -> new RNG(g.getSeedManager().getEntity())), getRarity())) {
|
||||
return spawn100(g, at);
|
||||
}
|
||||
|
||||
return null;
|
||||
RNG rng = EntitySpawnSeed.marker(engine.getSeedManager().getEntity(),
|
||||
at.getBlockX(), at.getBlockY(), at.getBlockZ());
|
||||
return LootResolver.oneIn(rng, getRarity()) ? spawn100(engine, at, rng, false) : null;
|
||||
}
|
||||
|
||||
static Location findCaveSpawnLocation(Engine engine, Chunk chunk, RNG rng, IrisSurface surface) {
|
||||
@@ -249,11 +241,7 @@ public class IrisEntitySpawn implements Rarity {
|
||||
return marker == null ? null : BukkitPlatform.toLocation(marker, world);
|
||||
}
|
||||
|
||||
private Entity spawn100(Engine g, Location at) {
|
||||
return spawn100(g, at, false);
|
||||
}
|
||||
|
||||
private Entity spawn100(Engine g, Location at, boolean ignoreSurfaces) {
|
||||
private Entity spawn100(Engine g, Location at, RNG rng, boolean ignoreSurfaces) {
|
||||
try {
|
||||
IrisEntity irisEntity = getRealEntity(g);
|
||||
if (irisEntity == null) { // No entity
|
||||
@@ -278,7 +266,7 @@ public class IrisEntitySpawn implements Rarity {
|
||||
}
|
||||
}
|
||||
|
||||
Entity e = irisEntity.spawn(g, at.clone().add(0.5, surface.isFluid() ? 0.5 : 0, 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);
|
||||
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());
|
||||
}
|
||||
@@ -291,6 +279,12 @@ public class IrisEntitySpawn implements Rarity {
|
||||
}
|
||||
}
|
||||
|
||||
public record SpawnContext(IrisSpawner spawner, IrisMarker marker, boolean initial) {
|
||||
public SpawnContext {
|
||||
Objects.requireNonNull(spawner, "spawner");
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isAir(Material material) {
|
||||
return material == Material.AIR || material == Material.CAVE_AIR || material == Material.VOID_AIR;
|
||||
}
|
||||
|
||||
@@ -51,8 +51,8 @@ public class IrisMarker extends IrisRegistrant {
|
||||
@Description("If this marker is used, what is the chance it removes itself. For example 25% (0.25) would mean that on average 4 uses will remove a specific marker. Set this below 0 (-1) to never exhaust & set this to 1 or higher to always exhaust on first use.")
|
||||
private double exhaustionChance = 0;
|
||||
|
||||
public boolean shouldExhaust() {
|
||||
return exhaustionChance > RNG.r.nextDouble();
|
||||
public boolean shouldExhaust(RNG rng) {
|
||||
return exhaustionChance > rng.nextDouble();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -49,36 +49,33 @@ import java.util.Map;
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Description("Represents an entity spawn during initial chunk generation")
|
||||
@Description("Configures initial inhabitants and ongoing entity spawning")
|
||||
@Data
|
||||
public class IrisSpawner extends IrisRegistrant {
|
||||
|
||||
private transient IrisMarker referenceMarker;
|
||||
|
||||
@ArrayType(min = 1, type = IrisEntitySpawn.class)
|
||||
@Description("The entity spawns to add")
|
||||
private KList<IrisEntitySpawn> spawns = new KList<>();
|
||||
|
||||
@ArrayType(min = 1, type = IrisEntitySpawn.class)
|
||||
@Description("The entity spawns to add initially. EXECUTES PER CHUNK!")
|
||||
@Description("The seeded entity pool used once per chunk or marker for initial inhabitants. Live time, weather, light, population, and cooldowns do not restrict this pool.")
|
||||
private KList<IrisEntitySpawn> initialSpawns = new KList<>();
|
||||
|
||||
@Description("Maximum ambient entities of the selected entity's native spawn category in one chunk. Spawn groups are limited to the remaining capacity.")
|
||||
@Description("Maximum ambient entities of the selected entity's native spawn category in one chunk. Ongoing groups use remaining capacity; initial groups use this limit independently of existing mobs.")
|
||||
private int maxEntitiesPerChunk = 1;
|
||||
|
||||
@Description("The block of 24 hour time to contain this spawn in.")
|
||||
@Description("The 24-hour time window for ongoing spawns.")
|
||||
private IrisTimeBlock timeBlock = new IrisTimeBlock();
|
||||
|
||||
@Description("The weather condition required for this spawner to fire.")
|
||||
@Description("The weather condition required for ongoing spawns.")
|
||||
private IrisWeather weather = IrisWeather.ANY;
|
||||
|
||||
@Description("The maximum rate this spawner can fire")
|
||||
@Description("The maximum ongoing spawn rate")
|
||||
private IrisRate maximumRate = new IrisRate();
|
||||
|
||||
@Description("The maximum rate this spawner can fire on a specific chunk")
|
||||
@Description("The maximum ongoing spawn rate in a specific chunk")
|
||||
private IrisRate maximumRatePerChunk = new IrisRate();
|
||||
|
||||
@Description("The light levels this spawn is allowed to run in (0-15 inclusive)")
|
||||
@Description("The light levels allowed for ongoing spawns (0-15 inclusive). Initial inhabitants ignore light.")
|
||||
private IrisRange allowedLightLevels = new IrisRange(0, 15);
|
||||
|
||||
@Description("Where should these spawns be placed")
|
||||
|
||||
Reference in new issue
Block a user