mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-11 19:14:11 +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:
+235
-6
@@ -6,6 +6,11 @@ import art.arcane.iris.generation.runtime.BiomeEnvironment;
|
|||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.iris.world.IrisCreator;
|
import art.arcane.iris.world.IrisCreator;
|
||||||
import art.arcane.iris.world.entity.IrisSpawner;
|
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.IrisEntitySpawn;
|
||||||
import art.arcane.iris.world.entity.IrisEntity;
|
import art.arcane.iris.world.entity.IrisEntity;
|
||||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
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.EntityType;
|
||||||
import org.bukkit.entity.LivingEntity;
|
import org.bukkit.entity.LivingEntity;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
import org.bukkit.event.EventHandler;
|
import org.bukkit.event.EventHandler;
|
||||||
import org.bukkit.event.EventPriority;
|
import org.bukkit.event.EventPriority;
|
||||||
import org.bukkit.event.Listener;
|
import org.bukkit.event.Listener;
|
||||||
@@ -38,9 +44,13 @@ import java.lang.reflect.Field;
|
|||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HexFormat;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
@@ -83,7 +93,7 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
|
|||||||
@Override
|
@Override
|
||||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
|
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
|
||||||
if (arguments.length != 1) {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -93,6 +103,8 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
|
|||||||
case "zombies" -> runRegion(sender, () -> spawnZombies(sender));
|
case "zombies" -> runRegion(sender, () -> spawnZombies(sender));
|
||||||
case "stress" -> runRegion(sender, () -> stress(sender));
|
case "stress" -> runRegion(sender, () -> stress(sender));
|
||||||
case "check" -> runRegion(sender, () -> check(sender));
|
case "check" -> runRegion(sender, () -> check(sender));
|
||||||
|
case "seeded" -> seeded(sender);
|
||||||
|
case "completion" -> completion(sender);
|
||||||
case "surface" -> view(sender, 67D);
|
case "surface" -> view(sender, 67D);
|
||||||
case "cave" -> view(sender, -46D);
|
case "cave" -> view(sender, -46D);
|
||||||
default -> throw new IllegalArgumentException("Unknown spawning operation");
|
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) {
|
private void configureWorld(CommandSender sender, World world) {
|
||||||
try {
|
try {
|
||||||
world.setDifficulty(Difficulty.NORMAL);
|
world.setDifficulty(Difficulty.NORMAL);
|
||||||
|
world.setTime(6000L);
|
||||||
|
world.setStorm(false);
|
||||||
|
world.setThundering(false);
|
||||||
world.setGameRule(GameRules.SPAWN_MOBS, true);
|
world.setGameRule(GameRules.SPAWN_MOBS, true);
|
||||||
List<CompletableFuture<Void>> chunks = new ArrayList<>();
|
List<CompletableFuture<Void>> chunks = new ArrayList<>();
|
||||||
for (int chunkX = -1; chunkX <= 1; chunkX++) {
|
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, () -> {
|
Bukkit.getRegionScheduler().execute(this, world, chunkX, chunkZ, () -> {
|
||||||
try {
|
try {
|
||||||
chunk.addPluginChunkTicket(this);
|
chunk.addPluginChunkTicket(this);
|
||||||
|
for (Entity entity : chunk.getEntities()) {
|
||||||
|
if (entity instanceof LivingEntity && !(entity instanceof Player)) {
|
||||||
|
entity.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
retained.complete(null);
|
retained.complete(null);
|
||||||
} catch (Throwable error) {
|
} catch (Throwable error) {
|
||||||
retained.completeExceptionally(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) {
|
private void spawnFrogs(CommandSender sender) {
|
||||||
Chunk chunk = requireWorld().getChunkAt(0, 0);
|
Chunk chunk = requireWorld().getChunkAt(0, 0);
|
||||||
RuntimeSpawn spawn = runtimeSpawn(chunk, "frogs");
|
RuntimeSpawn spawn = runtimeSpawn(chunk, "frogs");
|
||||||
getLogger().info(spawnDiagnostics(spawn, chunk));
|
getLogger().info(spawnDiagnostics(spawn, chunk));
|
||||||
for (int attempt = 0; attempt < 12; attempt++) {
|
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, "Frog batch exceeded its category capacity");
|
||||||
}
|
}
|
||||||
require(count(chunk, EntityType.FROG) == FROG_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");
|
require(count(chunk, EntityType.ZOMBIE) == 0, "Cave phase already ran");
|
||||||
RuntimeSpawn spawn = runtimeSpawn(chunk, "cave-zombies");
|
RuntimeSpawn spawn = runtimeSpawn(chunk, "cave-zombies");
|
||||||
getLogger().info(spawnDiagnostics(spawn, chunk));
|
getLogger().info(spawnDiagnostics(spawn, chunk));
|
||||||
invokeAmbient(spawn, chunk);
|
invokeAmbient(spawn, chunk, false);
|
||||||
firstZombieBatch = count(chunk, EntityType.ZOMBIE);
|
firstZombieBatch = count(chunk, EntityType.ZOMBIE);
|
||||||
require(firstZombieBatch == ZOMBIE_CAPACITY,
|
require(firstZombieBatch == ZOMBIE_CAPACITY,
|
||||||
"Authored seven-zombie batch was not clamped to capacity two: " + spawnDiagnostics(spawn, chunk));
|
"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);
|
Chunk chunk = requireWorld().getChunkAt(0, 0);
|
||||||
RuntimeSpawn spawn = runtimeSpawn(chunk, "cave-zombies");
|
RuntimeSpawn spawn = runtimeSpawn(chunk, "cave-zombies");
|
||||||
for (int attempt = 0; attempt < 50; attempt++) {
|
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");
|
require(count(chunk, EntityType.ZOMBIE) == ZOMBIE_CAPACITY, "Saturated zombie category exceeded capacity");
|
||||||
}
|
}
|
||||||
evidence(sender, "SPAWN_QA_STRESS attempts=50 zombies=" + count(chunk, EntityType.ZOMBIE));
|
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();
|
return diagnostics.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void invokeAmbient(RuntimeSpawn spawn, Chunk chunk) {
|
private void invokeAmbient(RuntimeSpawn spawn, Chunk chunk, boolean initial) {
|
||||||
ambientInvocation.set(true);
|
ambientInvocation.set(true);
|
||||||
try (BiomeEnvironment.Scope ignored = spawn.engine().openBiomeEnvironmentScope(spawn.environment())) {
|
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) {
|
} catch (InvocationTargetException failure) {
|
||||||
throw new IllegalStateException("Iris ambient spawning failed", failure.getCause());
|
throw new IllegalStateException("Iris ambient spawning failed", failure.getCause());
|
||||||
} catch (ReflectiveOperationException failure) {
|
} 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) {
|
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"
|
||||||
|
]
|
||||||
|
}
|
||||||
+29
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
@@ -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)
|
context.expect(response.includes(expected) && !response.includes('SPAWN_QA_FAILED'), response)
|
||||||
return 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 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 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')
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+92
-89
@@ -44,6 +44,8 @@ import art.arcane.iris.pack.value.IrisPosition;
|
|||||||
import art.arcane.iris.pack.value.IrisRange;
|
import art.arcane.iris.pack.value.IrisRange;
|
||||||
import art.arcane.iris.world.entity.IrisSpawnGroup;
|
import art.arcane.iris.world.entity.IrisSpawnGroup;
|
||||||
import art.arcane.iris.world.entity.IrisSpawner;
|
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.generation.decoration.IrisSurface;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
@@ -61,6 +63,7 @@ import java.util.List;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.concurrent.ArrayBlockingQueue;
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
import java.util.concurrent.BlockingQueue;
|
import java.util.concurrent.BlockingQueue;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
@@ -235,6 +238,10 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
boolean initialSpawnChunk(NativeWorld level, int chunkX, int chunkZ) {
|
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)) {
|
if (!ModdedEntitySpawner.chunksSafe(entityRuntime(level), chunkX, chunkZ)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -249,54 +256,25 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
MantleChunk<Matter> chunk = mantle.useChunk(chunkX, chunkZ);
|
MantleChunk<Matter> chunk = mantle.useChunk(chunkX, chunkZ);
|
||||||
try {
|
try {
|
||||||
List<PreparedMarkerSpawn> markers = markerSystemEnabled()
|
List<PreparedMarkerSpawn> markers = markerSystemEnabled()
|
||||||
? prepareMarkerSpawns(level, chunkX, chunkZ, chunk) : List.of();
|
? prepareMarkerSpawns(level, chunkX, chunkZ, chunk, true) : List.of();
|
||||||
Optional<BiomeEnvironment> environment = ambientSystemEnabled()
|
Optional<BiomeEnvironment> environment = ambientSystemEnabled()
|
||||||
? resolveSurfaceEnvironment(chunkX, chunkZ) : Optional.empty();
|
? resolveSurfaceEnvironment(chunkX, chunkZ) : Optional.empty();
|
||||||
chunk.raiseFlagUnchecked(INITIAL_SPAWN_COMPLETION_FLAG, () -> {
|
if (ambientSystemEnabled() && engine.getComplex() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
spawnPreparedMarkers(level, markers, true);
|
spawnPreparedMarkers(level, markers, true);
|
||||||
if (environment.isPresent()) {
|
if (environment.isPresent()) {
|
||||||
scheduleInitialFollowUp(level, chunkX, chunkZ, environment.get());
|
try (BiomeEnvironment.Scope ignored = engine.openBiomeEnvironmentScope(environment.get())) {
|
||||||
|
spawnAmbient(level, chunkX, chunkZ, true, environment.get());
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
chunk.raiseFlagUnchecked(INITIAL_SPAWN_COMPLETION_FLAG, () -> {});
|
||||||
} finally {
|
} finally {
|
||||||
chunk.release();
|
chunk.release();
|
||||||
}
|
}
|
||||||
return true;
|
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) {
|
private void warmupMantleChunkAsync(long key, int chunkX, int chunkZ) {
|
||||||
if (closed || !mantleWarmups.add(key)) {
|
if (closed || !mantleWarmups.add(key)) {
|
||||||
return;
|
return;
|
||||||
@@ -380,11 +358,14 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
if (!mantle.isChunkLoaded(chunkX, chunkZ)) {
|
if (!mantle.isChunkLoaded(chunkX, chunkZ)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!mantle.hasFlag(chunkX, chunkZ, INITIAL_SPAWN_COMPLETION_FLAG)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
MantleChunk<Matter> chunk = mantle.useChunk(chunkX, chunkZ);
|
MantleChunk<Matter> chunk = mantle.useChunk(chunkX, chunkZ);
|
||||||
try {
|
try {
|
||||||
if (markerSystemEnabled()) {
|
if (markerSystemEnabled()) {
|
||||||
spawnPreparedMarkers(level, prepareMarkerSpawns(level, chunkX, chunkZ, chunk), false);
|
spawnPreparedMarkers(level, prepareMarkerSpawns(level, chunkX, chunkZ, chunk, false), false);
|
||||||
}
|
}
|
||||||
if (ambientSystemEnabled()) {
|
if (ambientSystemEnabled()) {
|
||||||
spawnAmbient(level, chunkX, chunkZ, false);
|
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();
|
int minHeight = engine.getWorld().minHeight();
|
||||||
KList<IrisPosition> obstructed = new KList<>();
|
KList<IrisPosition> obstructed = new KList<>();
|
||||||
List<PreparedMarkerSpawn> prepared = new ArrayList<>();
|
List<PreparedMarkerSpawn> prepared = new ArrayList<>();
|
||||||
@@ -429,62 +410,71 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
if (spawners.isEmpty()) {
|
if (spawners.isEmpty()) {
|
||||||
return;
|
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) {
|
if (chosen == null) {
|
||||||
return;
|
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();
|
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||||
for (IrisPosition position : obstructed) {
|
for (IrisPosition position : obstructed) {
|
||||||
mantle.remove(position.getX(), position.getY(), position.getZ(), MatterMarker.class);
|
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);
|
return List.copyOf(prepared);
|
||||||
}
|
}
|
||||||
|
|
||||||
private KList<IrisSpawner> resolveMarkerSpawners(IrisMarker marker, BiomeEnvironment environment) {
|
private KList<IrisSpawner> resolveMarkerSpawners(IrisMarker marker, BiomeEnvironment environment) {
|
||||||
KList<IrisSpawner> spawners = new KList<>();
|
KList<IrisSpawner> spawners = new KList<>();
|
||||||
|
Set<IrisSpawner> seen = new HashSet<>();
|
||||||
for (String key : marker.getSpawners()) {
|
for (String key : marker.getSpawners()) {
|
||||||
IrisSpawner spawner = environment.data().getSpawnerLoader().load(key);
|
IrisSpawner spawner = environment.data().getSpawnerLoader().load(key);
|
||||||
if (spawner == null) {
|
if (spawner == null) {
|
||||||
IrisLogging.error("Cannot load spawner: " + key + " for marker on " + engine.getName());
|
IrisLogging.error("Cannot load spawner: " + key + " for marker on " + engine.getName());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
spawner.setReferenceMarker(marker);
|
if (!spawner.isCompatExcluded() && seen.add(spawner)) {
|
||||||
spawners.add(spawner);
|
spawners.add(spawner);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return spawners;
|
return spawners;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void spawnPreparedMarkers(NativeWorld level, List<PreparedMarkerSpawn> markers, boolean initial) {
|
private void spawnPreparedMarkers(NativeWorld level, List<PreparedMarkerSpawn> markers, boolean initial) {
|
||||||
for (PreparedMarkerSpawn marker : markers) {
|
for (PreparedMarkerSpawn marker : markers) {
|
||||||
try (BiomeEnvironment.Scope ignored = engine.openBiomeEnvironmentScope(marker.environment())) {
|
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) {
|
private void spawnFromSpawner(NativeWorld level, IrisPosition position, RNG rng, SpawnContext context) {
|
||||||
KList<IrisEntitySpawn> spawns = initial ? spawner.getInitialSpawns() : spawner.getSpawns();
|
IrisSpawner spawner = context.spawner();
|
||||||
|
KList<IrisEntitySpawn> spawns = context.initial() ? spawner.getInitialSpawns() : spawner.getSpawns();
|
||||||
if (spawns.isEmpty()) {
|
if (spawns.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (IrisEntitySpawn entry : spawns) {
|
IrisEntitySpawn chosen = rarityPick(spawns, rng);
|
||||||
entry.setReferenceSpawner(spawner);
|
|
||||||
entry.setReferenceMarker(spawner.getReferenceMarker());
|
|
||||||
}
|
|
||||||
IrisEntitySpawn chosen = rarityPick(spawns);
|
|
||||||
if (chosen == null) {
|
if (chosen == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int chunkX = position.getX() >> 4;
|
int chunkX = position.getX() >> 4;
|
||||||
int chunkZ = position.getZ() >> 4;
|
int chunkZ = position.getZ() >> 4;
|
||||||
if (!canSpawn(spawner, chunkX, chunkZ)) {
|
if (!canSpawn(spawner, chunkX, chunkZ, context.initial())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int spawned = spawnEntryAt(level, chosen, spawner, position);
|
int spawned = spawnEntryAt(level, chosen, position, rng, context);
|
||||||
if (spawned > 0) {
|
if (spawned > 0 && !context.initial()) {
|
||||||
spawner.spawn(engine, chunkX, chunkZ);
|
spawner.spawn(engine, chunkX, chunkZ);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -521,9 +511,11 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
IrisBiome biome = environment.biome();
|
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.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(environment.region().getEntitySpawners()), null, chunkX, chunkZ, chunkMobs, initial);
|
||||||
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(biome.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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
IrisEntitySpawn chosen = rarityPick(pool);
|
SpawnCandidate chosen = rarityPick(pool, rng);
|
||||||
if (chosen == null || chosen.getReferenceSpawner() == null) {
|
if (chosen == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
IrisSpawner spawner = chosen.getReferenceSpawner();
|
IrisSpawner spawner = chosen.spawner();
|
||||||
if (!canSpawn(spawner, chunkX, chunkZ)) {
|
if (!canSpawn(spawner, chunkX, chunkZ, initial)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int spawned = spawnEntry(level, chosen, spawner, chunkX, chunkZ,
|
int spawned = spawnEntry(level, chosen.entry(), chunkX, chunkZ,
|
||||||
remainingCapacity(chosen, spawner, chunkMobs));
|
remainingCapacity(chosen.entry(), spawner, chunkMobs), rng, new SpawnContext(spawner, null, initial));
|
||||||
if (spawned > 0) {
|
if (spawned > 0 && !initial) {
|
||||||
spawner.spawn(engine, chunkX, chunkZ);
|
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) {
|
for (IrisSpawner spawner : spawners) {
|
||||||
if (spawner == null) {
|
if (spawner == null) {
|
||||||
continue;
|
continue;
|
||||||
@@ -554,15 +546,13 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
if (biomeFilter != null && !spawner.isValid(biomeFilter)) {
|
if (biomeFilter != null && !spawner.isValid(biomeFilter)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!canSpawn(spawner, chunkX, chunkZ)) {
|
if (!canSpawn(spawner, chunkX, chunkZ, initial)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
KList<IrisEntitySpawn> spawns = initial ? spawner.getInitialSpawns() : spawner.getSpawns();
|
KList<IrisEntitySpawn> spawns = initial ? spawner.getInitialSpawns() : spawner.getSpawns();
|
||||||
for (IrisEntitySpawn entry : spawns) {
|
for (IrisEntitySpawn entry : spawns) {
|
||||||
entry.setReferenceSpawner(spawner);
|
|
||||||
entry.setReferenceMarker(spawner.getReferenceMarker());
|
|
||||||
if (remainingCapacity(entry, spawner, chunkMobs) > 0) {
|
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);
|
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);
|
IrisEntity irisEntity = entry.getRealEntity(engine);
|
||||||
if (irisEntity == null) {
|
if (irisEntity == null) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -581,18 +572,24 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
|
|
||||||
int min = entry.getMinSpawns();
|
int min = entry.getMinSpawns();
|
||||||
int max = entry.getMaxSpawns();
|
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) {
|
if (count <= 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
RNG entityRng = entry.getRng().aquire(() -> new RNG(engine.getSeedManager().getEntity()));
|
IrisSpawner spawner = context.spawner();
|
||||||
IrisSpawnGroup group = spawner.getGroup();
|
IrisSpawnGroup group = spawner.getGroup();
|
||||||
KList<IrisPosition> caveFloors = group == IrisSpawnGroup.CAVE
|
KList<IrisPosition> caveFloors = group == IrisSpawnGroup.CAVE
|
||||||
? engine.getMantle().findMarkers(chunkX, chunkZ, MarkerMatter.CAVE_FLOOR)
|
? engine.getMantle().findMarkers(chunkX, chunkZ, MarkerMatter.CAVE_FLOOR)
|
||||||
: new KList<>();
|
: new KList<>();
|
||||||
|
if (context.initial()) {
|
||||||
|
caveFloors.sort(Comparator.comparingInt(IrisPosition::getX)
|
||||||
|
.thenComparingInt(IrisPosition::getY).thenComparingInt(IrisPosition::getZ));
|
||||||
|
}
|
||||||
int spawned = 0;
|
int spawned = 0;
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
|
RNG entityRng = EntitySpawnSeed.entity(rng.getSeed(), i);
|
||||||
|
RNG positionRng = entityRng.nextParallelRNG(0x632BE59BD9B4E019L);
|
||||||
int worldX;
|
int worldX;
|
||||||
int worldY;
|
int worldY;
|
||||||
int worldZ;
|
int worldZ;
|
||||||
@@ -600,16 +597,16 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
if (caveFloors.isEmpty()) {
|
if (caveFloors.isEmpty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
IrisPosition caveFloor = caveFloors.getRandom(RNG.r);
|
IrisPosition caveFloor = caveFloors.getRandom(positionRng);
|
||||||
worldX = caveFloor.getX();
|
worldX = caveFloor.getX();
|
||||||
worldY = caveFloor.getY();
|
worldY = caveFloor.getY();
|
||||||
worldZ = caveFloor.getZ();
|
worldZ = caveFloor.getZ();
|
||||||
} else {
|
} else {
|
||||||
worldX = (chunkX << 4) + RNG.r.i(16);
|
worldX = (chunkX << 4) + positionRng.i(16);
|
||||||
worldZ = (chunkZ << 4) + RNG.r.i(16);
|
worldZ = (chunkZ << 4) + positionRng.i(16);
|
||||||
int surfaceY = NativeSpawnQueries.surfaceHeight(level, worldX, worldZ, false);
|
int surfaceY = NativeSpawnQueries.surfaceHeight(level, worldX, worldZ, false);
|
||||||
int solidY = NativeSpawnQueries.surfaceHeight(level, worldX, worldZ, true);
|
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) {
|
if (selectedY == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -619,7 +616,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Rarity is applied exactly once, as pool weighting in rarityPick - never re-rolled per position (Bukkit parity).
|
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
if (!surfaceMatches(irisEntity.getSurface(), level, worldX, worldY, worldZ)) {
|
if (!surfaceMatches(irisEntity.getSurface(), level, worldX, worldY, worldZ)) {
|
||||||
@@ -635,7 +632,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
return spawned;
|
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);
|
IrisEntity irisEntity = entry.getRealEntity(engine);
|
||||||
if (irisEntity == null) {
|
if (irisEntity == null) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -643,21 +640,21 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
|
|
||||||
int min = entry.getMinSpawns();
|
int min = entry.getMinSpawns();
|
||||||
int max = entry.getMaxSpawns();
|
int max = entry.getMaxSpawns();
|
||||||
int count = LootResolver.inclusive(RNG.r, min, max);
|
int count = LootResolver.inclusive(rng, min, max);
|
||||||
if (count <= 0) {
|
if (count <= 0) {
|
||||||
return 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 worldX = position.getX();
|
||||||
int worldY = position.getY() + 1;
|
int worldY = position.getY() + 1;
|
||||||
int worldZ = position.getZ();
|
int worldZ = position.getZ();
|
||||||
int spawned = 0;
|
int spawned = 0;
|
||||||
for (int i = 0; i < count; i++) {
|
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).
|
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
if (irisEntity.getSurface().isFluid()
|
if (irisEntity.getSurface().isFluid()
|
||||||
@@ -672,16 +669,15 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
return spawned;
|
return spawned;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void exhaustMarker(IrisSpawner spawner, IrisPosition position) {
|
private void exhaustMarker(IrisMarker marker, IrisPosition position, RNG rng) {
|
||||||
IrisMarker marker = spawner.getReferenceMarker();
|
if (marker == null || !marker.shouldExhaust(rng)) {
|
||||||
if (marker == null || !marker.shouldExhaust()) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
engine.getMantle().getMantle().remove(position.getX(), position.getY() - engine.getWorld().minHeight(), position.getZ(), MatterMarker.class);
|
engine.getMantle().getMantle().remove(position.getX(), position.getY() - engine.getWorld().minHeight(), position.getZ(), MatterMarker.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean canSpawn(IrisSpawner spawner, int chunkX, int chunkZ) {
|
private boolean canSpawn(IrisSpawner spawner, int chunkX, int chunkZ, boolean initial) {
|
||||||
return spawner.canSpawn(engine, chunkX, chunkZ);
|
return !spawner.isCompatExcluded() && (initial || spawner.canSpawn(engine, chunkX, chunkZ));
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean lightAllowed(IrisSpawner spawner, NativeWorld level, int worldX, int worldY, int worldZ) {
|
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;
|
return !studio || studioSetting;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IrisEntitySpawn rarityPick(KList<IrisEntitySpawn> entries) {
|
private static <T extends Rarity> T rarityPick(KList<T> entries, RNG rng) {
|
||||||
KList<IrisEntitySpawn> weighted = Rarity.expandWeighted(entries);
|
KList<T> weighted = Rarity.expandWeighted(entries);
|
||||||
return weighted.isEmpty() ? entries.getRandom() : weighted.getRandom();
|
return weighted.isEmpty() ? entries.getRandom(rng) : weighted.getRandom(rng);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long pack(int x, int z) {
|
private static long pack(int x, int z) {
|
||||||
@@ -893,7 +889,14 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
return failure;
|
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) {
|
private NativeEntityRuntime entityRuntime(NativeWorld level) {
|
||||||
|
|||||||
+317
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
-1
@@ -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.terrain.NativeWorld;
|
||||||
|
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeEntityRuntime;
|
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.configuration.IrisSettings;
|
||||||
import art.arcane.iris.pack.loading.IrisData;
|
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.BiomeEnvironment;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
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.world.history.SavedBiomeUnavailableException;
|
||||||
import art.arcane.iris.generation.mantle.EngineMantle;
|
import art.arcane.iris.generation.mantle.EngineMantle;
|
||||||
import art.arcane.iris.generation.biome.IrisBiome;
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
@@ -72,6 +78,15 @@ public class ModdedSavedBiomeConsumersTest {
|
|||||||
when(mantle.isChunkLoaded(2, -1)).thenReturn(true);
|
when(mantle.isChunkLoaded(2, -1)).thenReturn(true);
|
||||||
when(mantle.useChunk(2, -1)).thenReturn(chunk);
|
when(mantle.useChunk(2, -1)).thenReturn(chunk);
|
||||||
BiomeEnvironment environment = environment();
|
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))
|
when(engine.getSurfaceBiomeEnvironment(40, -8))
|
||||||
.thenThrow(new SavedBiomeUnavailableException("Loading", true))
|
.thenThrow(new SavedBiomeUnavailableException("Loading", true))
|
||||||
.thenReturn(environment);
|
.thenReturn(environment);
|
||||||
@@ -81,8 +96,10 @@ public class ModdedSavedBiomeConsumersTest {
|
|||||||
ModdedWorldManager manager = new ModdedWorldManager(engine);
|
ModdedWorldManager manager = new ModdedWorldManager(engine);
|
||||||
|
|
||||||
try (MockedStatic<IrisSettings> configured = mockStatic(IrisSettings.class);
|
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);
|
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);
|
spawns.when(() -> ModdedEntitySpawner.chunksSafe(any(NativeEntityRuntime.class), eq(2), eq(-1))).thenReturn(true);
|
||||||
assertThrows(SavedBiomeUnavailableException.class, () -> manager.initialSpawnChunk(new ModdedPlatformWorld(level), 2, -1));
|
assertThrows(SavedBiomeUnavailableException.class, () -> manager.initialSpawnChunk(new ModdedPlatformWorld(level), 2, -1));
|
||||||
verify(chunk, never()).raiseFlagUnchecked(eq(ModdedWorldManager.INITIAL_SPAWN_COMPLETION_FLAG), any());
|
verify(chunk, never()).raiseFlagUnchecked(eq(ModdedWorldManager.INITIAL_SPAWN_COMPLETION_FLAG), any());
|
||||||
|
|||||||
+3
-2
@@ -14,6 +14,7 @@ import art.arcane.iris.world.entity.IrisEntity;
|
|||||||
import art.arcane.iris.world.entity.IrisEntitySpawn;
|
import art.arcane.iris.world.entity.IrisEntitySpawn;
|
||||||
import art.arcane.iris.world.entity.IrisSpawnGroup;
|
import art.arcane.iris.world.entity.IrisSpawnGroup;
|
||||||
import art.arcane.iris.world.entity.IrisSpawner;
|
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.iris.generation.decoration.IrisSurface;
|
||||||
import art.arcane.volmlib.util.math.RNG;
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
@@ -196,7 +197,7 @@ public class ModdedWaterEntitySpawnTest {
|
|||||||
doReturn(entity).when(entry).getRealEntity(engine);
|
doReturn(entity).when(entry).getRealEntity(engine);
|
||||||
IrisSpawner spawner = new IrisSpawner().setGroup(IrisSpawnGroup.CAVE);
|
IrisSpawner spawner = new IrisSpawner().setGroup(IrisSpawnGroup.CAVE);
|
||||||
Method spawn = ModdedWorldManager.class.getDeclaredMethod("spawnEntry", NativeWorld.class,
|
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);
|
spawn.setAccessible(true);
|
||||||
ModdedWorldManager manager = new ModdedWorldManager(engine);
|
ModdedWorldManager manager = new ModdedWorldManager(engine);
|
||||||
try (MockedStatic<ModdedEntitySpawner> entities = mockStatic(ModdedEntitySpawner.class)) {
|
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),
|
entities.when(() -> ModdedEntitySpawner.spawn(eq(engine), eq(entity), any(NativeEntityRuntime.class),
|
||||||
eq(-25), eq(-49), eq(57), any(RNG.class))).thenReturn(mock(NativeSpawnedEntity.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),
|
entities.verify(() -> ModdedEntitySpawner.spawn(eq(engine), eq(entity), any(NativeEntityRuntime.class),
|
||||||
eq(-25), eq(-49), eq(57), any(RNG.class)));
|
eq(-25), eq(-49), eq(57), any(RNG.class)));
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ final class MarkerSpawnScanner {
|
|||||||
J.a(manager.managedTask("bukkit_world_manager_marker_scan", () -> {
|
J.a(manager.managedTask("bukkit_world_manager_marker_scan", () -> {
|
||||||
try {
|
try {
|
||||||
if (initialOnly && manager.getMantle().hasFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED_MARKER)) {
|
if (initialOnly && manager.getMantle().hasFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED_MARKER)) {
|
||||||
|
manager.chunkMaintenance.recordInitialSpawnCompletion(chunkX, chunkZ);
|
||||||
markerScanQueue.remove(key);
|
markerScanQueue.remove(key);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -91,7 +92,7 @@ final class MarkerSpawnScanner {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
prepared.add(new PreparedMarkerSpawn(new IrisPosition(position.getX(),
|
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));
|
consumer.accept(List.copyOf(prepared));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -137,7 +138,7 @@ final class MarkerSpawnScanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
IrisPosition position = new IrisPosition((chunkX << 4) + x, y, (chunkZ << 4) + z);
|
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();
|
data.requiresEmptyAbove = data.requiresEmptyAbove || mark.isEmptyAbove();
|
||||||
|
|
||||||
for (String i : mark.getSpawners()) {
|
for (String i : mark.getSpawners()) {
|
||||||
@@ -149,7 +150,6 @@ final class MarkerSpawnScanner {
|
|||||||
if (spawner.isCompatExcluded()) {
|
if (spawner.isCompatExcluded()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
spawner.setReferenceMarker(mark);
|
|
||||||
data.spawners.add(spawner);
|
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 static final class MarkerSpawnData {
|
||||||
private final KSet<IrisSpawner> spawners = new KSet<>();
|
private final KSet<IrisSpawner> spawners = new KSet<>();
|
||||||
private final BiomeEnvironment environment;
|
private final BiomeEnvironment environment;
|
||||||
|
private final IrisMarker marker;
|
||||||
private boolean requiresEmptyAbove;
|
private boolean requiresEmptyAbove;
|
||||||
|
|
||||||
private MarkerSpawnData(BiomeEnvironment environment) {
|
private MarkerSpawnData(BiomeEnvironment environment, IrisMarker marker) {
|
||||||
this.environment = environment;
|
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.configuration.IrisSettings;
|
||||||
import art.arcane.iris.generation.cache.Cache;
|
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;
|
||||||
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter.SavedChunkMantle;
|
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter.SavedChunkMantle;
|
||||||
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
|
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.math.Position2;
|
||||||
import art.arcane.volmlib.util.matter.Matter;
|
import art.arcane.volmlib.util.matter.Matter;
|
||||||
import org.bukkit.Chunk;
|
import org.bukkit.Chunk;
|
||||||
|
import org.bukkit.GameRules;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
@@ -44,6 +46,7 @@ import java.util.Comparator;
|
|||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ForkJoinPool;
|
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_FORCED_CHUNK_UPDATES = 128;
|
||||||
private static final int MAX_PENDING_MATERIALIZATIONS = 128;
|
private static final int MAX_PENDING_MATERIALIZATIONS = 128;
|
||||||
private static final int MAX_COMPLETED_MATERIALIZATIONS = 8192;
|
private static final int MAX_COMPLETED_MATERIALIZATIONS = 8192;
|
||||||
|
private static final int MAX_COMPLETED_INITIAL_SPAWNS = 8192;
|
||||||
|
|
||||||
private final IrisWorldManager manager;
|
private final IrisWorldManager manager;
|
||||||
private final Set<Long> mantleWarmupQueue = ConcurrentHashMap.newKeySet();
|
private final Set<Long> mantleWarmupQueue = ConcurrentHashMap.newKeySet();
|
||||||
private final Set<Long> markerFlagQueue = 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> discoveredFlagQueue = ConcurrentHashMap.newKeySet();
|
||||||
private final Set<Long> chunkUpdateQueue = ConcurrentHashMap.newKeySet();
|
private final Set<Long> chunkUpdateQueue = ConcurrentHashMap.newKeySet();
|
||||||
private final Map<Long, RoutedMaterialization> materializations = new ConcurrentHashMap<>();
|
private final Map<Long, RoutedMaterialization> materializations = new ConcurrentHashMap<>();
|
||||||
@@ -85,6 +90,7 @@ final class WorldChunkMaintenance {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
completedMaterializations.clear();
|
completedMaterializations.clear();
|
||||||
|
completedInitialSpawns.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void invalidateMaterialization(int chunkX, int chunkZ) {
|
void invalidateMaterialization(int chunkX, int chunkZ) {
|
||||||
@@ -375,46 +381,107 @@ final class WorldChunkMaintenance {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void raiseInitialSpawnMarkerFlag(World world, int chunkX, int chunkZ, Runnable onFirstRaise) {
|
boolean isInitialSpawnComplete(int chunkX, int chunkZ) {
|
||||||
if (world == null || onFirstRaise == null) {
|
return completedInitialSpawns.contains(Cache.key(chunkX, chunkZ));
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!J.isFolia()) {
|
void recordInitialSpawnCompletion(int chunkX, int chunkZ) {
|
||||||
manager.getMantle().raiseFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED_MARKER, onFirstRaise);
|
rememberInitialSpawn(Cache.key(chunkX, chunkZ));
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void runInitialSpawn(World world, int chunkX, int chunkZ, Runnable initialSpawn) {
|
||||||
|
if (world == null || initialSpawn == null || closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
long key = Cache.key(chunkX, chunkZ);
|
long key = Cache.key(chunkX, chunkZ);
|
||||||
if (!markerFlagQueue.add(key)) {
|
if (completedInitialSpawns.contains(key) || !markerFlagQueue.add(key)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
J.a(manager.managedTask("bukkit_world_manager_initial_spawn_prepare",
|
||||||
J.a(manager.managedTask("bukkit_world_manager_spawn_marker_flag", () -> {
|
() -> prepareInitialSpawnFlag(world, chunkX, chunkZ, key, initialSpawn),
|
||||||
boolean raised = false;
|
() -> markerFlagQueue.remove(key)));
|
||||||
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);
|
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);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
} finally {
|
||||||
markerFlagQueue.remove(key);
|
markerFlagQueue.remove(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!raised) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
J.runRegion(world, chunkX, chunkZ, manager.managedTask("bukkit_world_manager_spawn_marker_callback", () -> {
|
private void rememberInitialSpawn(long key) {
|
||||||
if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
|
synchronized (completedInitialSpawns) {
|
||||||
return;
|
completedInitialSpawns.add(key);
|
||||||
|
if (completedInitialSpawns.size() > MAX_COMPLETED_INITIAL_SPAWNS) {
|
||||||
|
completedInitialSpawns.remove(completedInitialSpawns.iterator().next());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onFirstRaise.run();
|
|
||||||
}));
|
|
||||||
}, () -> markerFlagQueue.remove(key)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void warmupMantleChunkAsync(int chunkX, int chunkZ) {
|
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.world.history.SavedBiomeUnavailableException;
|
||||||
import art.arcane.iris.generation.biome.IrisBiome;
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
import art.arcane.iris.world.entity.IrisEntitySpawn;
|
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.pack.value.IrisPosition;
|
||||||
import art.arcane.iris.world.entity.IrisSpawner;
|
import art.arcane.iris.world.entity.IrisSpawner;
|
||||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||||
@@ -47,6 +50,9 @@ import org.bukkit.entity.LivingEntity;
|
|||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -57,10 +63,7 @@ import java.util.concurrent.TimeUnit;
|
|||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.function.Predicate;
|
|
||||||
import java.util.function.Supplier;
|
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
|
* 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!manager.chunkMaintenance.isInitialSpawnComplete(c.getX(), c.getZ())) {
|
||||||
|
spawnInitially(c);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (IrisSettings.get().getWorld().isMarkerEntitySpawningSystem()) {
|
if (IrisSettings.get().getWorld().isMarkerEntitySpawningSystem()) {
|
||||||
manager.markerScanner.scanMarkerSpawners(c, false, markers -> {
|
manager.markerScanner.scanMarkerSpawners(c, false, markers -> {
|
||||||
for (PreparedMarkerSpawn marker : markers) {
|
for (PreparedMarkerSpawn marker : markers) {
|
||||||
spawnPreparedMarker(marker, false);
|
spawnPreparedMarker(marker, false);
|
||||||
}
|
}
|
||||||
prepareInitialSpawn(c, markers);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,7 +339,9 @@ final class WorldEntitySpawner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void spawnInitially(Chunk chunk) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (IrisSettings.get().getWorld().isMarkerEntitySpawningSystem()) {
|
if (IrisSettings.get().getWorld().isMarkerEntitySpawningSystem()) {
|
||||||
@@ -356,71 +364,94 @@ final class WorldEntitySpawner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Optional<BiomeEnvironment> preparedEnvironment = environment;
|
Optional<BiomeEnvironment> preparedEnvironment = environment;
|
||||||
manager.chunkMaintenance.raiseInitialSpawnMarkerFlag(chunk.getWorld(), chunk.getX(), chunk.getZ(), () -> {
|
List<PreparedMarkerSpawn> ordered = new ArrayList<>(markers);
|
||||||
for (PreparedMarkerSpawn marker : 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);
|
spawnPreparedMarker(marker, true);
|
||||||
}
|
}
|
||||||
if (preparedEnvironment.isPresent()) {
|
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();
|
BiomeEnvironment selected = preparedEnvironment.get();
|
||||||
try (BiomeEnvironment.Scope ignored = manager.getEngine().openBiomeEnvironmentScope(selected)) {
|
try (BiomeEnvironment.Scope ignored = manager.getEngine().openBiomeEnvironmentScope(selected)) {
|
||||||
spawnAmbient(chunk, true, selected);
|
spawnAmbient(chunk, true, selected);
|
||||||
}
|
}
|
||||||
}), RNG.r.i(5, 200));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void spawnPreparedMarker(PreparedMarkerSpawn marker, boolean initial) {
|
private void spawnPreparedMarker(PreparedMarkerSpawn marker, boolean initial) {
|
||||||
IrisSpawner spawner = new KList<>(marker.spawners()).getRandom();
|
if (manager.getEngine().isClosed()) {
|
||||||
if (spawner == null || 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;
|
return;
|
||||||
}
|
}
|
||||||
try (BiomeEnvironment.Scope ignored = manager.getEngine().openBiomeEnvironmentScope(marker.environment())) {
|
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) {
|
private void spawnAmbient(Chunk chunk, boolean initial, BiomeEnvironment environment) {
|
||||||
if (!ambientAllowed(c, initial)) {
|
if (!ambientAllowed(chunk, initial)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
//@builder
|
ChunkCounter counter = initial ? null : new ChunkCounter(chunk.getEntities());
|
||||||
// Excluded spawners cannot spawn anything on this Minecraft version, so they never enter the selection pool.
|
List<SpawnSelection> pool = new ArrayList<>();
|
||||||
Predicate<IrisSpawner> filter = i -> !i.isCompatExcluded() && i.canSpawn(manager.getEngine(), c.getX(), c.getZ());
|
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.dimension().getEntitySpawners()),
|
||||||
ChunkCounter counter = new ChunkCounter(c.getEntities());
|
environment.biome(), chunk, initial, counter);
|
||||||
|
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.region().getEntitySpawners()),
|
||||||
IrisBiome biome = environment.biome();
|
null, chunk, initial, counter);
|
||||||
IrisEntitySpawn v = spawnRandomly(Stream.concat(environment.data().getSpawnerLoader()
|
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.biome().getEntitySpawners()),
|
||||||
.loadAll(environment.dimension().getEntitySpawners())
|
null, chunk, initial, counter);
|
||||||
.shuffleCopy(RNG.r)
|
RNG random = initial ? EntitySpawnSeed.chunk(manager.getEngine().getSeedManager().getEntity(),
|
||||||
.stream()
|
chunk.getX(), chunk.getZ()) : new RNG(RNG.r.nextLong());
|
||||||
.filter(filter)
|
SpawnSelection selected = selectSpawn(pool, random);
|
||||||
.filter((i) -> i.isValid(biome)),
|
if (selected == null) {
|
||||||
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)
|
|
||||||
return;
|
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) {
|
static boolean ambientAllowed(Chunk chunk, boolean initial) {
|
||||||
@@ -428,46 +459,22 @@ final class WorldEntitySpawner {
|
|||||||
&& (initial || chunk.getLoadLevel() == Chunk.LoadLevel.ENTITY_TICKING);
|
&& (initial || chunk.getLoadLevel() == Chunk.LoadLevel.ENTITY_TICKING);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void spawn(Chunk c, IrisEntitySpawn i, int remainingCapacity) {
|
private void spawnMarker(IrisPosition position, IrisSpawner spawner, IrisMarker marker, RNG random, boolean initial) {
|
||||||
IrisSpawner ref = i.getReferenceSpawner();
|
int chunkX = PowerOfTwoCoordinates.blockToChunkFloor(position.getX());
|
||||||
int s = i.spawn(manager.getEngine(), c, RNG.r, remainingCapacity);
|
int chunkZ = PowerOfTwoCoordinates.blockToChunkFloor(position.getZ());
|
||||||
actuallySpawned.addAndGet(s);
|
if (!spawnerAllowed(spawner, manager.getEngine(), chunkX, chunkZ, initial)) {
|
||||||
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()) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
KList<IrisEntitySpawn> entries = initial ? spawner.getInitialSpawns() : spawner.getSpawns();
|
||||||
IrisEntitySpawn ss = spawnRandomly(s).getRandom();
|
IrisEntitySpawn selected = Rarity.expandWeighted(entries).getRandom(random);
|
||||||
ss.setReferenceSpawner(spawner);
|
if (selected == null) {
|
||||||
ss.setReferenceMarker(spawner.getReferenceMarker());
|
return;
|
||||||
spawn(block, ss);
|
|
||||||
}
|
}
|
||||||
|
int spawned = selected.spawn(manager.getEngine(), position, random, new SpawnContext(spawner, marker, initial));
|
||||||
private Stream<IrisEntitySpawn> stream(IrisSpawner s, boolean initial) {
|
actuallySpawned.addAndGet(spawned);
|
||||||
for (IrisEntitySpawn i : initial ? s.getInitialSpawns() : s.getSpawns()) {
|
if (spawned > 0 && !initial) {
|
||||||
i.setReferenceSpawner(s);
|
spawner.spawn(manager.getEngine(), chunkX, chunkZ);
|
||||||
i.setReferenceMarker(s.getReferenceMarker());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (initial ? s.getInitialSpawns() : s.getSpawns()).stream();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isEntitySpawningEnabledForCurrentWorld() {
|
boolean isEntitySpawningEnabledForCurrentWorld() {
|
||||||
@@ -478,8 +485,11 @@ final class WorldEntitySpawner {
|
|||||||
return IrisSettings.get().getStudio().isEntitySpawning();
|
return IrisSettings.get().getStudio().isEntitySpawning();
|
||||||
}
|
}
|
||||||
|
|
||||||
private KList<IrisEntitySpawn> spawnRandomly(List<IrisEntitySpawn> types) {
|
record SpawnSelection(IrisEntitySpawn entry, IrisSpawner spawner) implements Rarity {
|
||||||
return Rarity.expandWeighted(types);
|
@Override
|
||||||
|
public int getRarity() {
|
||||||
|
return entry.getRarity();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static final class ChunkCounter {
|
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);
|
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.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
@Snippet("attribute-modifier")
|
@Snippet("attribute-modifier")
|
||||||
@@ -134,12 +133,12 @@ public class IrisAttributeModifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private AttributeModifier createModifier(RNG rng) {
|
AttributeModifier createModifier(RNG rng) {
|
||||||
NamespacedKey key = NamespacedKey.minecraft(generateModifierKey());
|
NamespacedKey key = NamespacedKey.minecraft(generateModifierKey(rng));
|
||||||
return new AttributeModifier(key, getAmount(rng), resolveOperation(), EquipmentSlotGroup.ANY);
|
return new AttributeModifier(key, getAmount(rng), resolveOperation(), EquipmentSlotGroup.ANY);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String generateModifierKey() {
|
private String generateModifierKey(RNG rng) {
|
||||||
String source = getName() == null ? "modifier" : getName();
|
String source = getName() == null ? "modifier" : getName();
|
||||||
String normalized = source.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9/._-]", "_");
|
String normalized = source.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9/._-]", "_");
|
||||||
if (normalized.isBlank()) {
|
if (normalized.isBlank()) {
|
||||||
@@ -148,7 +147,8 @@ public class IrisAttributeModifier {
|
|||||||
if (normalized.length() > 32) {
|
if (normalized.length() > 32) {
|
||||||
normalized = normalized.substring(0, 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;
|
return normalized + "_" + random;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,7 +268,8 @@ public class IrisEntity extends IrisRegistrant implements NativeEntityOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Entity spawn(Engine gen, Location at) {
|
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) {
|
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("");
|
return specialType != null && !specialType.equals("");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class BukkitOps {
|
static final class BukkitOps {
|
||||||
private static void persistVillager(Villager villager) {
|
private static void persistVillager(Villager villager) {
|
||||||
villager.setPersistent(true);
|
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();
|
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() {
|
l.setLootTable(new LootTable() {
|
||||||
@Override
|
@Override
|
||||||
public NamespacedKey getKey() {
|
public NamespacedKey getKey() {
|
||||||
return new NamespacedKey("iris", "loot-" + entity.hashCode());
|
return lootKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ import org.bukkit.World;
|
|||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
import org.bukkit.entity.Entity;
|
import org.bukkit.entity.Entity;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Comparator;
|
||||||
|
|
||||||
@Snippet("entity-spawn")
|
@Snippet("entity-spawn")
|
||||||
@Accessors(chain = true)
|
@Accessors(chain = true)
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@@ -63,7 +66,6 @@ import org.bukkit.entity.Entity;
|
|||||||
public class IrisEntitySpawn implements Rarity {
|
public class IrisEntitySpawn implements Rarity {
|
||||||
private static final int CAVE_COLUMN_ATTEMPTS = 8;
|
private static final int CAVE_COLUMN_ATTEMPTS = 8;
|
||||||
private static final int CAVE_VERTICAL_ATTEMPTS = 128;
|
private static final int CAVE_VERTICAL_ATTEMPTS = 128;
|
||||||
private final transient AtomicCache<RNG> rng = new AtomicCache<>();
|
|
||||||
private final transient AtomicCache<IrisEntity> ent = new AtomicCache<>();
|
private final transient AtomicCache<IrisEntity> ent = new AtomicCache<>();
|
||||||
@RegistryListResource(IrisEntity.class)
|
@RegistryListResource(IrisEntity.class)
|
||||||
@Required
|
@Required
|
||||||
@@ -78,10 +80,8 @@ public class IrisEntitySpawn implements Rarity {
|
|||||||
@MinNumber(1)
|
@MinNumber(1)
|
||||||
@Description("The max of this entity to spawn")
|
@Description("The max of this entity to spawn")
|
||||||
private int maxSpawns = 1;
|
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) {
|
if (remainingCapacity <= 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -89,42 +89,39 @@ public class IrisEntitySpawn implements Rarity {
|
|||||||
if (definition == null) {
|
if (definition == null) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
int spawns = Math.min(remainingCapacity, LootResolver.inclusive(rng, minSpawns, maxSpawns));
|
long batchSeed = rng.getSeed();
|
||||||
int s = 0;
|
int count = Math.min(remainingCapacity, LootResolver.inclusive(rng, minSpawns, maxSpawns));
|
||||||
|
IrisSpawnGroup group = context.spawner().getGroup();
|
||||||
if (spawns > 0) {
|
KList<IrisPosition> caveMarkers = null;
|
||||||
for (int id = 0; id < spawns; id++) {
|
if (context.initial() && group == IrisSpawnGroup.CAVE && !J.isFolia()) {
|
||||||
IrisSpawnGroup group = getReferenceSpawner().getGroup();
|
caveMarkers = gen.getMantle().findMarkers(chunk.getX(), chunk.getZ(), MarkerMatter.CAVE_FLOOR);
|
||||||
Location l;
|
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) {
|
if (group == IrisSpawnGroup.CAVE) {
|
||||||
l = findCaveSpawnLocation(gen, c, rng, definition.getSurface());
|
location = caveMarkers == null
|
||||||
|
? findCaveSpawnLocation(gen, chunk, positionRng, definition.getSurface())
|
||||||
|
: selectCaveSpawnLocation(caveMarkers, chunk.getWorld(), positionRng);
|
||||||
} else {
|
} else {
|
||||||
int x = (c.getX() << 4) + rng.i(16);
|
int x = (chunk.getX() << 4) + positionRng.i(16);
|
||||||
int z = (c.getZ() << 4) + rng.i(16);
|
int z = (chunk.getZ() << 4) + positionRng.i(16);
|
||||||
World world = c.getWorld();
|
World world = chunk.getWorld();
|
||||||
int h = world.getHighestBlockYAt(x, z, HeightMap.OCEAN_FLOOR);
|
int floor = world.getHighestBlockYAt(x, z, HeightMap.OCEAN_FLOOR);
|
||||||
int hf = world.getHighestBlockYAt(x, z, HeightMap.WORLD_SURFACE);
|
int top = world.getHighestBlockYAt(x, z, HeightMap.WORLD_SURFACE);
|
||||||
Integer y = selectSurfaceSpawnY(group, definition.getSurface(), h, hf, rng);
|
Integer y = selectSurfaceSpawnY(group, definition.getSurface(), floor, top, positionRng);
|
||||||
l = y == null ? null : new Location(world, x, y, z);
|
location = y == null ? null : new Location(world, x, y, z);
|
||||||
}
|
}
|
||||||
|
if (location != null && lightAllowed(location, context)
|
||||||
if (l != null) {
|
&& spawn100(gen, location, attemptRng, false) != null) {
|
||||||
if (referenceSpawner.getAllowedLightLevels().getMin() > 0 || referenceSpawner.getAllowedLightLevels().getMax() < 15) {
|
spawned++;
|
||||||
if (referenceSpawner.getAllowedLightLevels().contains(l.getBlock().getLightLevel())) {
|
|
||||||
if (spawn100(gen, l) != null) {
|
|
||||||
s++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
return spawned;
|
||||||
if (spawn100(gen, l) != null) {
|
|
||||||
s++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Integer selectSurfaceSpawnY(IrisSpawnGroup group, IrisSurface surface,
|
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;
|
return top > floor ? LootResolver.inclusive(rng, floor + 1, top) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int spawn(Engine gen, IrisPosition c, RNG rng) {
|
public int spawn(Engine gen, IrisPosition position, RNG rng, SpawnContext context) {
|
||||||
int spawns = LootResolver.inclusive(rng, minSpawns, maxSpawns);
|
long batchSeed = rng.getSeed();
|
||||||
int s = 0;
|
int count = LootResolver.inclusive(rng, minSpawns, maxSpawns);
|
||||||
|
if (count <= 0 || !BukkitWorldBinding.tryBind(gen.getWorld())) {
|
||||||
if (!BukkitWorldBinding.tryBind(gen.getWorld())) {
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
World world = BukkitWorldBinding.world(gen.getWorld());
|
World world = BukkitWorldBinding.world(gen.getWorld());
|
||||||
if (spawns > 0) {
|
if (context.marker() != null && context.marker().shouldExhaust(EntitySpawnSeed.entity(batchSeed, -1))) {
|
||||||
|
|
||||||
if (referenceMarker != null && referenceMarker.shouldExhaust()) {
|
|
||||||
if (J.isFolia()) {
|
if (J.isFolia()) {
|
||||||
J.a(() -> gen.getMantle().getMantle().remove(c.getX(), c.getY() - gen.getWorld().minHeight(), c.getZ(), MatterMarker.class));
|
J.a(() -> gen.getMantle().getMantle().remove(position.getX(),
|
||||||
|
position.getY() - gen.getWorld().minHeight(), position.getZ(), MatterMarker.class));
|
||||||
} else {
|
} else {
|
||||||
gen.getMantle().getMantle().remove(c.getX(), c.getY() - gen.getWorld().minHeight(), c.getZ(), MatterMarker.class);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
for (int id = 0; id < spawns; id++) {
|
private static boolean lightAllowed(Location location, SpawnContext context) {
|
||||||
Location l = BukkitPlatform.toLocation(c, world).add(0, 1, 0);
|
if (context.initial()) {
|
||||||
|
return true;
|
||||||
if (referenceSpawner.getAllowedLightLevels().getMin() > 0 || referenceSpawner.getAllowedLightLevels().getMax() < 15) {
|
|
||||||
if (referenceSpawner.getAllowedLightLevels().contains(l.getBlock().getLightLevel())) {
|
|
||||||
if (spawn100(gen, l, true) != null) {
|
|
||||||
s++;
|
|
||||||
}
|
}
|
||||||
}
|
IrisSpawner spawner = context.spawner();
|
||||||
} else {
|
return spawner.getAllowedLightLevels().getMin() <= 0
|
||||||
if (spawn100(gen, l, true) != null) {
|
&& spawner.getAllowedLightLevels().getMax() >= 15
|
||||||
s++;
|
|| spawner.getAllowedLightLevels().contains(location.getBlock().getLightLevel());
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IrisEntity getRealEntity(Engine g) {
|
public IrisEntity getRealEntity(Engine g) {
|
||||||
@@ -191,16 +186,13 @@ public class IrisEntitySpawn implements Rarity {
|
|||||||
return entity == null || entity.isCompatExcluded() ? null : entity;
|
return entity == null || entity.isCompatExcluded() ? null : entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Entity spawn(Engine g, Location at) {
|
public Entity spawn(Engine engine, Location at) {
|
||||||
if (getRealEntity(g) == null) {
|
if (getRealEntity(engine) == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
RNG rng = EntitySpawnSeed.marker(engine.getSeedManager().getEntity(),
|
||||||
if (LootResolver.oneIn(rng.aquire(() -> new RNG(g.getSeedManager().getEntity())), getRarity())) {
|
at.getBlockX(), at.getBlockY(), at.getBlockZ());
|
||||||
return spawn100(g, at);
|
return LootResolver.oneIn(rng, getRarity()) ? spawn100(engine, at, rng, false) : null;
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Location findCaveSpawnLocation(Engine engine, Chunk chunk, RNG rng, IrisSurface surface) {
|
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);
|
return marker == null ? null : BukkitPlatform.toLocation(marker, world);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Entity spawn100(Engine g, Location at) {
|
private Entity spawn100(Engine g, Location at, RNG rng, boolean ignoreSurfaces) {
|
||||||
return spawn100(g, at, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Entity spawn100(Engine g, Location at, boolean ignoreSurfaces) {
|
|
||||||
try {
|
try {
|
||||||
IrisEntity irisEntity = getRealEntity(g);
|
IrisEntity irisEntity = getRealEntity(g);
|
||||||
if (irisEntity == null) { // No entity
|
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) {
|
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());
|
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) {
|
private static boolean isAir(Material material) {
|
||||||
return material == Material.AIR || material == Material.CAVE_AIR || material == Material.VOID_AIR;
|
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.")
|
@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;
|
private double exhaustionChance = 0;
|
||||||
|
|
||||||
public boolean shouldExhaust() {
|
public boolean shouldExhaust(RNG rng) {
|
||||||
return exhaustionChance > RNG.r.nextDouble();
|
return exhaustionChance > rng.nextDouble();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -49,36 +49,33 @@ import java.util.Map;
|
|||||||
@Accessors(chain = true)
|
@Accessors(chain = true)
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Description("Represents an entity spawn during initial chunk generation")
|
@Description("Configures initial inhabitants and ongoing entity spawning")
|
||||||
@Data
|
@Data
|
||||||
public class IrisSpawner extends IrisRegistrant {
|
public class IrisSpawner extends IrisRegistrant {
|
||||||
|
|
||||||
private transient IrisMarker referenceMarker;
|
|
||||||
|
|
||||||
@ArrayType(min = 1, type = IrisEntitySpawn.class)
|
@ArrayType(min = 1, type = IrisEntitySpawn.class)
|
||||||
@Description("The entity spawns to add")
|
@Description("The entity spawns to add")
|
||||||
private KList<IrisEntitySpawn> spawns = new KList<>();
|
private KList<IrisEntitySpawn> spawns = new KList<>();
|
||||||
|
|
||||||
@ArrayType(min = 1, type = IrisEntitySpawn.class)
|
@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<>();
|
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;
|
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();
|
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;
|
private IrisWeather weather = IrisWeather.ANY;
|
||||||
|
|
||||||
@Description("The maximum rate this spawner can fire")
|
@Description("The maximum ongoing spawn rate")
|
||||||
private IrisRate maximumRate = new IrisRate();
|
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();
|
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);
|
private IrisRange allowedLightLevels = new IrisRange(0, 15);
|
||||||
|
|
||||||
@Description("Where should these spawns be placed")
|
@Description("Where should these spawns be placed")
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import org.mockito.MockedStatic;
|
|||||||
import java.lang.reflect.Proxy;
|
import java.lang.reflect.Proxy;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertArrayEquals;
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
@@ -35,18 +34,7 @@ public class LinkedTerrainChunkTest {
|
|||||||
@BeforeClass
|
@BeforeClass
|
||||||
public static void initializeBiomes() throws Exception {
|
public static void initializeBiomes() throws Exception {
|
||||||
BukkitTestServer.install();
|
BukkitTestServer.install();
|
||||||
AtomicReference<Object> registered = new AtomicReference<>();
|
RegistryAccess access = BukkitTestServer.registryAccess();
|
||||||
RegistryAccess access = mock(RegistryAccess.class, invocation -> {
|
|
||||||
if (invocation.getMethod().getName().equals("getRegistry")) {
|
|
||||||
if (registered.get() == null) {
|
|
||||||
registered.set(Proxy.newProxyInstance(Registry.class.getClassLoader(), new Class<?>[]{Registry.class},
|
|
||||||
(proxy, method, arguments) -> method.getName().equals("getOrThrow")
|
|
||||||
? biome((Key) arguments[0]) : null));
|
|
||||||
}
|
|
||||||
return registered.get();
|
|
||||||
}
|
|
||||||
return RETURNS_DEFAULTS.answer(invocation);
|
|
||||||
});
|
|
||||||
InternalAPIBridge bridge = mock(InternalAPIBridge.class, invocation ->
|
InternalAPIBridge bridge = mock(InternalAPIBridge.class, invocation ->
|
||||||
invocation.getMethod().getName().equals("constructLegacyCustomBiome")
|
invocation.getMethod().getName().equals("constructLegacyCustomBiome")
|
||||||
? biome(Key.key("minecraft:custom")) : RETURNS_DEFAULTS.answer(invocation));
|
? biome(Key.key("minecraft:custom")) : RETURNS_DEFAULTS.answer(invocation));
|
||||||
|
|||||||
+211
@@ -0,0 +1,211 @@
|
|||||||
|
package art.arcane.iris.generation.runtime;
|
||||||
|
|
||||||
|
import art.arcane.iris.platform.bukkit.plugin.Chunks;
|
||||||
|
import art.arcane.iris.generation.context.IrisContext;
|
||||||
|
import art.arcane.iris.world.task.J;
|
||||||
|
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||||
|
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||||
|
import art.arcane.volmlib.util.matter.Matter;
|
||||||
|
import org.bukkit.GameRules;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class WorldChunkMaintenanceInitialSpawnTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void initializeGameRules() throws Exception {
|
||||||
|
WorldEntitySpawnerAdmissionTest.initializeGameRules();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void completionIsWrittenAfterOwningRegionWorkAndDuplicateRequestsAreSkipped() {
|
||||||
|
try (Fixture fixture = new Fixture()) {
|
||||||
|
AtomicInteger attempts = new AtomicInteger();
|
||||||
|
fixture.start(attempts::incrementAndGet);
|
||||||
|
fixture.start(attempts::incrementAndGet);
|
||||||
|
assertEquals(1, fixture.async.size());
|
||||||
|
fixture.async.remove().run();
|
||||||
|
verify(fixture.world, never()).isChunkLoaded(2, -1);
|
||||||
|
verify(fixture.mantle, never()).flag(2, -1, MantleFlag.INITIAL_SPAWNED_MARKER, true);
|
||||||
|
|
||||||
|
fixture.regions.remove().run();
|
||||||
|
|
||||||
|
assertEquals(1, attempts.get());
|
||||||
|
assertTrue(fixture.maintenance.isInitialSpawnComplete(2, -1));
|
||||||
|
verify(fixture.mantle, never()).flag(2, -1, MantleFlag.INITIAL_SPAWNED_MARKER, true);
|
||||||
|
fixture.start(attempts::incrementAndGet);
|
||||||
|
assertTrue(fixture.regions.isEmpty());
|
||||||
|
fixture.async.remove().run();
|
||||||
|
verify(fixture.mantle).flag(2, -1, MantleFlag.INITIAL_SPAWNED_MARKER, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unloadAndDisabledMobSpawningRemainEligibleForRetry() {
|
||||||
|
for (boolean unloaded : new boolean[]{true, false}) {
|
||||||
|
try (Fixture fixture = new Fixture()) {
|
||||||
|
AtomicInteger attempts = new AtomicInteger();
|
||||||
|
when(fixture.world.isChunkLoaded(2, -1)).thenReturn(!unloaded);
|
||||||
|
when(fixture.world.getGameRuleValue(GameRules.SPAWN_MOBS)).thenReturn(unloaded);
|
||||||
|
fixture.start(attempts::incrementAndGet);
|
||||||
|
fixture.async.remove().run();
|
||||||
|
fixture.regions.remove().run();
|
||||||
|
assertFalse(fixture.maintenance.isInitialSpawnComplete(2, -1));
|
||||||
|
assertEquals(0, attempts.get());
|
||||||
|
verify(fixture.mantle, never()).flag(2, -1, MantleFlag.INITIAL_SPAWNED_MARKER, true);
|
||||||
|
|
||||||
|
when(fixture.world.isChunkLoaded(2, -1)).thenReturn(true);
|
||||||
|
when(fixture.world.getGameRuleValue(GameRules.SPAWN_MOBS)).thenReturn(true);
|
||||||
|
fixture.start(attempts::incrementAndGet);
|
||||||
|
fixture.async.remove().run();
|
||||||
|
fixture.regions.remove().run();
|
||||||
|
assertEquals(1, attempts.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void persistedCompletionAndRegionRejectionDoNotRunInitialWork() {
|
||||||
|
try (Fixture fixture = new Fixture()) {
|
||||||
|
AtomicInteger attempts = new AtomicInteger();
|
||||||
|
when(fixture.mantle.hasFlag(2, -1, MantleFlag.INITIAL_SPAWNED_MARKER)).thenReturn(true);
|
||||||
|
fixture.start(attempts::incrementAndGet);
|
||||||
|
fixture.async.remove().run();
|
||||||
|
assertTrue(fixture.maintenance.isInitialSpawnComplete(2, -1));
|
||||||
|
assertTrue(fixture.regions.isEmpty());
|
||||||
|
assertEquals(0, attempts.get());
|
||||||
|
}
|
||||||
|
try (Fixture fixture = new Fixture()) {
|
||||||
|
AtomicInteger attempts = new AtomicInteger();
|
||||||
|
fixture.acceptRegions = false;
|
||||||
|
fixture.start(attempts::incrementAndGet);
|
||||||
|
fixture.async.remove().run();
|
||||||
|
fixture.start(attempts::incrementAndGet);
|
||||||
|
assertEquals(1, fixture.async.size());
|
||||||
|
assertFalse(fixture.maintenance.isInitialSpawnComplete(2, -1));
|
||||||
|
assertEquals(0, attempts.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shutdownDrainWaitsForTheReservedCompletionWriteAfterInitialEntitiesWereEmitted() throws Exception {
|
||||||
|
try (Fixture fixture = new Fixture()) {
|
||||||
|
AtomicInteger spawned = new AtomicInteger();
|
||||||
|
fixture.start(spawned::incrementAndGet);
|
||||||
|
fixture.async.remove().run();
|
||||||
|
try (GenerationSessionLease owner = fixture.sessions.acquire("initial_spawn_owner");
|
||||||
|
IrisContext.Scope ignored = IrisContext.open(fixture.engine, owner.sessionId(), null)) {
|
||||||
|
fixture.regions.remove().run();
|
||||||
|
}
|
||||||
|
assertEquals(1, spawned.get());
|
||||||
|
assertEquals(1, fixture.sessions.activeLeases());
|
||||||
|
when(fixture.engine.isClosing()).thenReturn(true);
|
||||||
|
ExecutorService closer = Executors.newSingleThreadExecutor();
|
||||||
|
try {
|
||||||
|
CountDownLatch sealing = new CountDownLatch(1);
|
||||||
|
Future<?> drained = closer.submit(() -> {
|
||||||
|
sealing.countDown();
|
||||||
|
fixture.sessions.sealAndAwait("shutdown", 2_000L, true);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
assertTrue(sealing.await(1L, TimeUnit.SECONDS));
|
||||||
|
assertFalse(drained.isDone());
|
||||||
|
fixture.async.remove().run();
|
||||||
|
drained.get(1L, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
verify(fixture.mantle).flag(2, -1, MantleFlag.INITIAL_SPAWNED_MARKER, true);
|
||||||
|
assertEquals(0, fixture.sessions.activeLeases());
|
||||||
|
} finally {
|
||||||
|
closer.shutdownNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static final class Fixture implements AutoCloseable {
|
||||||
|
private final Engine engine = mock(Engine.class);
|
||||||
|
private final GenerationSessionManager sessions = new GenerationSessionManager();
|
||||||
|
private final IrisWorldManager manager = mock(IrisWorldManager.class);
|
||||||
|
private final World world = mock(World.class);
|
||||||
|
private final Mantle<Matter> mantle = mock(Mantle.class);
|
||||||
|
private final WorldChunkMaintenance maintenance = new WorldChunkMaintenance(manager);
|
||||||
|
private final Queue<Runnable> async = new ArrayDeque<>();
|
||||||
|
private final Queue<Runnable> regions = new ArrayDeque<>();
|
||||||
|
private final MockedStatic<J> scheduler;
|
||||||
|
private final MockedStatic<Chunks> chunks;
|
||||||
|
private boolean acceptRegions = true;
|
||||||
|
|
||||||
|
private Fixture() {
|
||||||
|
scheduler = mockStatic(J.class);
|
||||||
|
try {
|
||||||
|
chunks = mockStatic(Chunks.class);
|
||||||
|
} catch (RuntimeException | Error failure) {
|
||||||
|
scheduler.close();
|
||||||
|
throw failure;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
configure();
|
||||||
|
} catch (RuntimeException | Error failure) {
|
||||||
|
close();
|
||||||
|
throw failure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void configure() {
|
||||||
|
when(manager.getEngine()).thenReturn(engine);
|
||||||
|
when(engine.getGenerationSessions()).thenReturn(sessions);
|
||||||
|
when(manager.getMantle()).thenReturn(mantle);
|
||||||
|
when(manager.managedTask(anyString(), any(Runnable.class), any(Runnable.class)))
|
||||||
|
.thenAnswer(invocation -> invocation.getArgument(1));
|
||||||
|
when(world.isChunkLoaded(2, -1)).thenReturn(true);
|
||||||
|
when(world.getGameRuleValue(GameRules.SPAWN_MOBS)).thenReturn(true);
|
||||||
|
chunks.when(() -> Chunks.isSafe(world, 2, -1)).thenReturn(true);
|
||||||
|
scheduler.when(() -> J.a(any(Runnable.class))).thenAnswer(invocation -> {
|
||||||
|
async.add(invocation.getArgument(0));
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
scheduler.when(() -> J.runRegion(eq(world), eq(2), eq(-1), any(Runnable.class)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
if (acceptRegions) {
|
||||||
|
regions.add(invocation.getArgument(3));
|
||||||
|
}
|
||||||
|
return acceptRegions;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void start(Runnable work) {
|
||||||
|
maintenance.runInitialSpawn(world, 2, -1, work);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
while (!async.isEmpty()) {
|
||||||
|
async.remove().run();
|
||||||
|
}
|
||||||
|
chunks.close();
|
||||||
|
scheduler.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-18
@@ -4,20 +4,15 @@ import art.arcane.iris.testsupport.BukkitTestServer;
|
|||||||
import io.papermc.paper.InternalAPIBridge;
|
import io.papermc.paper.InternalAPIBridge;
|
||||||
import io.papermc.paper.registry.RegistryAccess;
|
import io.papermc.paper.registry.RegistryAccess;
|
||||||
import org.bukkit.Chunk;
|
import org.bukkit.Chunk;
|
||||||
import org.bukkit.GameRule;
|
|
||||||
import org.bukkit.GameRules;
|
import org.bukkit.GameRules;
|
||||||
import org.bukkit.Registry;
|
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.mockito.MockedStatic;
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
import java.lang.reflect.Proxy;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.Mockito.RETURNS_DEFAULTS;
|
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.mockStatic;
|
import static org.mockito.Mockito.mockStatic;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
@@ -26,19 +21,7 @@ public class WorldEntitySpawnerAdmissionTest {
|
|||||||
@BeforeClass
|
@BeforeClass
|
||||||
public static void initializeGameRules() throws Exception {
|
public static void initializeGameRules() throws Exception {
|
||||||
BukkitTestServer.install();
|
BukkitTestServer.install();
|
||||||
AtomicReference<Object> registered = new AtomicReference<>();
|
RegistryAccess access = BukkitTestServer.registryAccess();
|
||||||
RegistryAccess access = mock(RegistryAccess.class, invocation -> {
|
|
||||||
if (invocation.getMethod().getName().equals("getRegistry")) {
|
|
||||||
if (registered.get() == null) {
|
|
||||||
registered.set(Proxy.newProxyInstance(Registry.class.getClassLoader(),
|
|
||||||
new Class<?>[]{Registry.class},
|
|
||||||
(proxy, method, arguments) -> method.getName().equals("getOrThrow")
|
|
||||||
? mock(GameRule.class) : null));
|
|
||||||
}
|
|
||||||
return registered.get();
|
|
||||||
}
|
|
||||||
return RETURNS_DEFAULTS.answer(invocation);
|
|
||||||
});
|
|
||||||
InternalAPIBridge bridge = mock(InternalAPIBridge.class);
|
InternalAPIBridge bridge = mock(InternalAPIBridge.class);
|
||||||
try (MockedStatic<RegistryAccess> registries = mockStatic(RegistryAccess.class);
|
try (MockedStatic<RegistryAccess> registries = mockStatic(RegistryAccess.class);
|
||||||
MockedStatic<InternalAPIBridge> internals = mockStatic(InternalAPIBridge.class)) {
|
MockedStatic<InternalAPIBridge> internals = mockStatic(InternalAPIBridge.class)) {
|
||||||
|
|||||||
+8
-7
@@ -7,6 +7,7 @@ import art.arcane.iris.world.history.SavedBiomeUnavailableException;
|
|||||||
import art.arcane.iris.generation.biome.IrisBiome;
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
import art.arcane.iris.world.entity.IrisEntitySpawn;
|
import art.arcane.iris.world.entity.IrisEntitySpawn;
|
||||||
|
import art.arcane.iris.world.entity.IrisEntitySpawn.SpawnContext;
|
||||||
import art.arcane.iris.pack.value.IrisPosition;
|
import art.arcane.iris.pack.value.IrisPosition;
|
||||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||||
import art.arcane.iris.world.entity.IrisSpawner;
|
import art.arcane.iris.world.entity.IrisSpawner;
|
||||||
@@ -54,9 +55,9 @@ public class WorldEntitySpawnerBiomeTest {
|
|||||||
configured.when(IrisSettings::get).thenReturn(settings);
|
configured.when(IrisSettings::get).thenReturn(settings);
|
||||||
WorldEntitySpawner spawner = new WorldEntitySpawner(manager);
|
WorldEntitySpawner spawner = new WorldEntitySpawner(manager);
|
||||||
spawner.prepareInitialSpawn(chunk, List.of());
|
spawner.prepareInitialSpawn(chunk, List.of());
|
||||||
verify(maintenance, never()).raiseInitialSpawnMarkerFlag(any(), anyInt(), anyInt(), any());
|
verify(maintenance, never()).runInitialSpawn(any(), anyInt(), anyInt(), any());
|
||||||
spawner.prepareInitialSpawn(chunk, List.of());
|
spawner.prepareInitialSpawn(chunk, List.of());
|
||||||
verify(maintenance).raiseInitialSpawnMarkerFlag(eq(world), eq(2), eq(-1), any());
|
verify(maintenance).runInitialSpawn(eq(world), eq(2), eq(-1), any());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,11 +77,10 @@ public class WorldEntitySpawnerBiomeTest {
|
|||||||
when(definition.getInitialSpawns()).thenReturn(new KList<>(entity));
|
when(definition.getInitialSpawns()).thenReturn(new KList<>(entity));
|
||||||
when(definition.canSpawn(engine, 0, 0)).thenReturn(true);
|
when(definition.canSpawn(engine, 0, 0)).thenReturn(true);
|
||||||
when(entity.getRarity()).thenReturn(1);
|
when(entity.getRarity()).thenReturn(1);
|
||||||
when(entity.getReferenceSpawner()).thenReturn(definition);
|
when(entity.spawn(eq(engine), eq(position), any(RNG.class), any(SpawnContext.class))).thenReturn(1);
|
||||||
when(entity.spawn(eq(engine), eq(position), any(RNG.class))).thenReturn(1);
|
|
||||||
KSet<IrisSpawner> definitions = new KSet<>();
|
KSet<IrisSpawner> definitions = new KSet<>();
|
||||||
definitions.add(definition);
|
definitions.add(definition);
|
||||||
PreparedMarkerSpawn marker = new PreparedMarkerSpawn(position, definitions, environment);
|
PreparedMarkerSpawn marker = new PreparedMarkerSpawn(position, definitions, environment, null);
|
||||||
IrisSettings settings = new IrisSettings();
|
IrisSettings settings = new IrisSettings();
|
||||||
settings.getWorld().setAmbientEntitySpawningSystem(false);
|
settings.getWorld().setAmbientEntitySpawningSystem(false);
|
||||||
|
|
||||||
@@ -88,11 +88,11 @@ public class WorldEntitySpawnerBiomeTest {
|
|||||||
configured.when(IrisSettings::get).thenReturn(settings);
|
configured.when(IrisSettings::get).thenReturn(settings);
|
||||||
new WorldEntitySpawner(manager).prepareInitialSpawn(chunk, List.of(marker));
|
new WorldEntitySpawner(manager).prepareInitialSpawn(chunk, List.of(marker));
|
||||||
ArgumentCaptor<Runnable> callback = ArgumentCaptor.forClass(Runnable.class);
|
ArgumentCaptor<Runnable> callback = ArgumentCaptor.forClass(Runnable.class);
|
||||||
verify(manager.chunkMaintenance).raiseInitialSpawnMarkerFlag(eq(world), eq(0), eq(0), callback.capture());
|
verify(manager.chunkMaintenance).runInitialSpawn(eq(world), eq(0), eq(0), callback.capture());
|
||||||
doThrow(new SavedBiomeUnavailableException("Evicted", true)).when(engine)
|
doThrow(new SavedBiomeUnavailableException("Evicted", true)).when(engine)
|
||||||
.getBiomeEnvironment(anyInt(), anyInt(), anyInt());
|
.getBiomeEnvironment(anyInt(), anyInt(), anyInt());
|
||||||
callback.getValue().run();
|
callback.getValue().run();
|
||||||
verify(entity).spawn(eq(engine), eq(position), any(RNG.class));
|
verify(entity).spawn(eq(engine), eq(position), any(RNG.class), any(SpawnContext.class));
|
||||||
verify(engine).openBiomeEnvironmentScope(environment);
|
verify(engine).openBiomeEnvironmentScope(environment);
|
||||||
verify(scope).close();
|
verify(scope).close();
|
||||||
verify(engine, never()).getBiomeEnvironment(anyInt(), anyInt(), anyInt());
|
verify(engine, never()).getBiomeEnvironment(anyInt(), anyInt(), anyInt());
|
||||||
@@ -102,6 +102,7 @@ public class WorldEntitySpawnerBiomeTest {
|
|||||||
private static IrisWorldManager manager(Engine engine) throws Exception {
|
private static IrisWorldManager manager(Engine engine) throws Exception {
|
||||||
IrisWorldManager manager = mock(IrisWorldManager.class);
|
IrisWorldManager manager = mock(IrisWorldManager.class);
|
||||||
when(manager.getEngine()).thenReturn(engine);
|
when(manager.getEngine()).thenReturn(engine);
|
||||||
|
when(engine.getSeedManager()).thenReturn(mock(SeedManager.class));
|
||||||
Field field = IrisWorldManager.class.getDeclaredField("chunkMaintenance");
|
Field field = IrisWorldManager.class.getDeclaredField("chunkMaintenance");
|
||||||
field.setAccessible(true);
|
field.setAccessible(true);
|
||||||
field.set(manager, mock(WorldChunkMaintenance.class));
|
field.set(manager, mock(WorldChunkMaintenance.class));
|
||||||
|
|||||||
+145
@@ -0,0 +1,145 @@
|
|||||||
|
package art.arcane.iris.generation.runtime;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||||
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
|
import art.arcane.iris.pack.loading.ResourceLoader;
|
||||||
|
import art.arcane.iris.world.entity.IrisEntity;
|
||||||
|
import art.arcane.iris.world.entity.IrisEntitySpawn;
|
||||||
|
import art.arcane.iris.world.entity.IrisEntitySpawn.SpawnContext;
|
||||||
|
import art.arcane.iris.world.entity.IrisSpawner;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
|
import org.bukkit.Chunk;
|
||||||
|
import org.bukkit.GameRules;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class WorldEntitySpawnerInitialSeedTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void initializeGameRules() throws Exception {
|
||||||
|
WorldEntitySpawnerAdmissionTest.initializeGameRules();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void initialPopulationUsesAuthoredCapacityWithoutLiveGatesOrPopulationReads() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
fixture.spawn(2, -3);
|
||||||
|
|
||||||
|
assertEquals(1, fixture.observed.size());
|
||||||
|
Observation observation = fixture.observed.get("2,-3");
|
||||||
|
assertEquals(4, observation.capacity());
|
||||||
|
assertTrue(observation.initial());
|
||||||
|
verify(fixture.definition, never()).canSpawn(any(Engine.class), anyInt(), anyInt());
|
||||||
|
verify(fixture.definition, never()).spawn(any(Engine.class), anyInt(), anyInt());
|
||||||
|
verify(fixture.lastChunk, never()).getEntities();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void chunkBatchSeedsAreIndependentOfVisitOrderAndUnrelatedRandomActivity() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
for (int coordinate = -16; coordinate < 16; coordinate++) {
|
||||||
|
fixture.spawn(coordinate, -coordinate - 1);
|
||||||
|
}
|
||||||
|
Map<String, Observation> forward = new HashMap<>(fixture.observed);
|
||||||
|
fixture.observed.clear();
|
||||||
|
for (int coordinate = 15; coordinate >= -16; coordinate--) {
|
||||||
|
for (int draw = 0; draw < 19; draw++) {
|
||||||
|
RNG.r.nextLong();
|
||||||
|
}
|
||||||
|
fixture.spawn(coordinate, -coordinate - 1);
|
||||||
|
}
|
||||||
|
assertEquals(forward, fixture.observed);
|
||||||
|
assertEquals(32, forward.values().stream().map(Observation::seed).distinct().count());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void initialSpawnerIgnoresCooldownButExcludedContentIsNeverAdmitted() {
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
IrisSpawner definition = mock(IrisSpawner.class);
|
||||||
|
when(definition.canSpawn(engine, 1, 2)).thenReturn(false);
|
||||||
|
assertTrue(WorldEntitySpawner.spawnerAllowed(definition, engine, 1, 2, true));
|
||||||
|
assertFalse(WorldEntitySpawner.spawnerAllowed(definition, engine, 1, 2, false));
|
||||||
|
when(definition.isCompatExcluded()).thenReturn(true);
|
||||||
|
assertFalse(WorldEntitySpawner.spawnerAllowed(definition, engine, 1, 2, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static final class Fixture {
|
||||||
|
private final Engine engine = mock(Engine.class);
|
||||||
|
private final IrisWorldManager manager = mock(IrisWorldManager.class);
|
||||||
|
private final World world = mock(World.class);
|
||||||
|
private final IrisSpawner definition = mock(IrisSpawner.class);
|
||||||
|
private final IrisEntitySpawn entry = mock(IrisEntitySpawn.class);
|
||||||
|
private final Map<String, Observation> observed = new HashMap<>();
|
||||||
|
private final BiomeEnvironment environment;
|
||||||
|
private final Method spawn;
|
||||||
|
private final WorldEntitySpawner spawner;
|
||||||
|
private Chunk lastChunk;
|
||||||
|
|
||||||
|
private Fixture() throws Exception {
|
||||||
|
SeedManager seeds = mock(SeedManager.class);
|
||||||
|
when(seeds.getEntity()).thenReturn(918273645L);
|
||||||
|
when(engine.getSeedManager()).thenReturn(seeds);
|
||||||
|
when(manager.getEngine()).thenReturn(engine);
|
||||||
|
IrisData data = mock(IrisData.class);
|
||||||
|
ResourceLoader<IrisSpawner> loader = mock(ResourceLoader.class);
|
||||||
|
when(data.getSpawnerLoader()).thenReturn(loader);
|
||||||
|
KList<String> authored = new KList<>("qa/initial");
|
||||||
|
when(loader.loadAll(any(KList.class))).thenReturn(new KList<>());
|
||||||
|
when(loader.loadAll(authored)).thenReturn(new KList<>(definition));
|
||||||
|
IrisBiome biome = mock(IrisBiome.class);
|
||||||
|
IrisRegion region = mock(IrisRegion.class);
|
||||||
|
IrisDimension dimension = mock(IrisDimension.class);
|
||||||
|
when(biome.getEntitySpawners()).thenReturn(authored);
|
||||||
|
when(region.getEntitySpawners()).thenReturn(new KList<>());
|
||||||
|
when(dimension.getEntitySpawners()).thenReturn(new KList<>());
|
||||||
|
when(definition.getInitialSpawns()).thenReturn(new KList<>(entry));
|
||||||
|
when(definition.getMaxEntitiesPerChunk()).thenReturn(4);
|
||||||
|
when(entry.getRarity()).thenReturn(1);
|
||||||
|
when(entry.getRealEntity(engine)).thenReturn(mock(IrisEntity.class));
|
||||||
|
when(world.getGameRuleValue(GameRules.SPAWN_MOBS)).thenReturn(true);
|
||||||
|
when(entry.spawn(eq(engine), any(Chunk.class), any(RNG.class), anyInt(), any(SpawnContext.class)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
Chunk chunk = invocation.getArgument(1);
|
||||||
|
RNG rng = invocation.getArgument(2);
|
||||||
|
int capacity = invocation.getArgument(3);
|
||||||
|
SpawnContext context = invocation.getArgument(4);
|
||||||
|
observed.put(chunk.getX() + "," + chunk.getZ(), new Observation(rng.getSeed(), capacity, context.initial()));
|
||||||
|
return 4;
|
||||||
|
});
|
||||||
|
environment = new BiomeEnvironment(1L, biome, region, dimension, data);
|
||||||
|
spawner = new WorldEntitySpawner(manager);
|
||||||
|
spawn = WorldEntitySpawner.class.getDeclaredMethod("spawnAmbient", Chunk.class, boolean.class, BiomeEnvironment.class);
|
||||||
|
spawn.setAccessible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void spawn(int x, int z) throws Exception {
|
||||||
|
lastChunk = mock(Chunk.class);
|
||||||
|
when(lastChunk.getWorld()).thenReturn(world);
|
||||||
|
when(lastChunk.getX()).thenReturn(x);
|
||||||
|
when(lastChunk.getZ()).thenReturn(z);
|
||||||
|
spawn.invoke(spawner, lastChunk, true, environment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Observation(long seed, int capacity, boolean initial) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
package art.arcane.iris.generation.runtime;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
|
import art.arcane.iris.generation.runtime.MarkerSpawnScanner.PreparedMarkerSpawn;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||||
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
|
import art.arcane.iris.pack.value.IrisPosition;
|
||||||
|
import art.arcane.iris.world.entity.IrisEntitySpawn;
|
||||||
|
import art.arcane.iris.world.entity.IrisEntitySpawn.SpawnContext;
|
||||||
|
import art.arcane.iris.world.entity.IrisMarker;
|
||||||
|
import art.arcane.iris.world.entity.IrisSpawner;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import art.arcane.volmlib.util.collection.KSet;
|
||||||
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class WorldEntitySpawnerMarkerSeedTest {
|
||||||
|
@Test
|
||||||
|
public void initialMarkersIgnoreHashSetInsertionOrderAndOtherChunksRandomDraws() throws Exception {
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
SeedManager seeds = mock(SeedManager.class);
|
||||||
|
when(engine.getSeedManager()).thenReturn(seeds);
|
||||||
|
when(seeds.getEntity()).thenReturn(145398276L);
|
||||||
|
IrisWorldManager manager = mock(IrisWorldManager.class);
|
||||||
|
when(manager.getEngine()).thenReturn(engine);
|
||||||
|
BiomeEnvironment environment = new BiomeEnvironment(1L, mock(IrisBiome.class), mock(IrisRegion.class),
|
||||||
|
mock(IrisDimension.class), mock(IrisData.class));
|
||||||
|
when(engine.openBiomeEnvironmentScope(environment)).thenReturn(mock(BiomeEnvironment.Scope.class));
|
||||||
|
Map<String, Observation> observed = new HashMap<>();
|
||||||
|
IrisMarker marker = new IrisMarker();
|
||||||
|
IrisSpawner first = definition("qa/a", engine, marker, observed);
|
||||||
|
IrisSpawner second = definition("qa/b", engine, marker, observed);
|
||||||
|
KSet<IrisSpawner> forward = new KSet<>();
|
||||||
|
forward.add(first);
|
||||||
|
forward.add(second);
|
||||||
|
KSet<IrisSpawner> reversed = new KSet<>();
|
||||||
|
reversed.add(second);
|
||||||
|
reversed.add(first);
|
||||||
|
Method spawn = WorldEntitySpawner.class.getDeclaredMethod("spawnPreparedMarker", PreparedMarkerSpawn.class, boolean.class);
|
||||||
|
spawn.setAccessible(true);
|
||||||
|
WorldEntitySpawner spawner = new WorldEntitySpawner(manager);
|
||||||
|
for (int x = -32; x < 32; x++) {
|
||||||
|
IrisPosition position = new IrisPosition(x * 16 + 1, -49, -x * 16 + 3);
|
||||||
|
spawn.invoke(spawner, new PreparedMarkerSpawn(position, forward, environment, marker), true);
|
||||||
|
}
|
||||||
|
Map<String, Observation> expected = new HashMap<>(observed);
|
||||||
|
observed.clear();
|
||||||
|
for (int x = 31; x >= -32; x--) {
|
||||||
|
RNG.r.nextLong();
|
||||||
|
IrisPosition position = new IrisPosition(x * 16 + 1, -49, -x * 16 + 3);
|
||||||
|
spawn.invoke(spawner, new PreparedMarkerSpawn(position, reversed, environment, marker), true);
|
||||||
|
}
|
||||||
|
assertEquals(expected, observed);
|
||||||
|
assertEquals(64, observed.size());
|
||||||
|
assertTrue(observed.values().stream().allMatch(Observation::initial));
|
||||||
|
verify(first, never()).canSpawn(any(Engine.class), anyInt(), anyInt());
|
||||||
|
verify(second, never()).canSpawn(any(Engine.class), anyInt(), anyInt());
|
||||||
|
verify(first, never()).spawn(any(Engine.class), anyInt(), anyInt());
|
||||||
|
verify(second, never()).spawn(any(Engine.class), anyInt(), anyInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisSpawner definition(String key, Engine engine, IrisMarker marker, Map<String, Observation> observed) {
|
||||||
|
IrisSpawner spawner = mock(IrisSpawner.class);
|
||||||
|
IrisEntitySpawn entry = mock(IrisEntitySpawn.class);
|
||||||
|
when(spawner.getLoadKey()).thenReturn(key);
|
||||||
|
when(spawner.getInitialSpawns()).thenReturn(new KList<>(entry));
|
||||||
|
when(entry.getRarity()).thenReturn(1);
|
||||||
|
when(entry.spawn(eq(engine), any(IrisPosition.class), any(RNG.class), any(SpawnContext.class)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
IrisPosition position = invocation.getArgument(1);
|
||||||
|
RNG rng = invocation.getArgument(2);
|
||||||
|
SpawnContext context = invocation.getArgument(3);
|
||||||
|
assertEquals(marker, context.marker());
|
||||||
|
observed.put(position.getX() + "," + position.getY() + "," + position.getZ(),
|
||||||
|
new Observation(context.spawner().getLoadKey(), rng.getSeed(), context.initial()));
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
return spawner;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Observation(String definition, long seed, boolean initial) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-3
@@ -35,15 +35,15 @@ public class WorldEntitySpawnerPopulationTest {
|
|||||||
IrisEntity definition = mock(IrisEntity.class);
|
IrisEntity definition = mock(IrisEntity.class);
|
||||||
when(definition.spawnCategory()).thenReturn("monster");
|
when(definition.spawnCategory()).thenReturn("monster");
|
||||||
when(entry.getRealEntity(engine)).thenReturn(definition);
|
when(entry.getRealEntity(engine)).thenReturn(definition);
|
||||||
when(entry.getReferenceSpawner()).thenReturn(new IrisSpawner().setMaxEntitiesPerChunk(2));
|
IrisSpawner spawner = new IrisSpawner().setMaxEntitiesPerChunk(2);
|
||||||
try (MockedStatic<BukkitEntityType> types = mockStatic(BukkitEntityType.class)) {
|
try (MockedStatic<BukkitEntityType> types = mockStatic(BukkitEntityType.class)) {
|
||||||
types.when(() -> BukkitEntityType.of(EntityType.FROG)).thenReturn(creatureType);
|
types.when(() -> BukkitEntityType.of(EntityType.FROG)).thenReturn(creatureType);
|
||||||
types.when(() -> BukkitEntityType.of(EntityType.ZOMBIE)).thenReturn(monsterType);
|
types.when(() -> BukkitEntityType.of(EntityType.ZOMBIE)).thenReturn(monsterType);
|
||||||
WorldEntitySpawner.ChunkCounter counter = new WorldEntitySpawner.ChunkCounter(
|
WorldEntitySpawner.ChunkCounter counter = new WorldEntitySpawner.ChunkCounter(
|
||||||
new Entity[]{frog, frog, frog, zombie, player});
|
new Entity[]{frog, frog, frog, zombie, player});
|
||||||
assertEquals(1, counter.remainingCapacity(entry, engine));
|
assertEquals(1, counter.remainingCapacity(entry, spawner, engine));
|
||||||
when(definition.spawnCategory()).thenReturn("creature");
|
when(definition.spawnCategory()).thenReturn("creature");
|
||||||
assertEquals(0, counter.remainingCapacity(entry, engine));
|
assertEquals(0, counter.remainingCapacity(entry, spawner, engine));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package art.arcane.iris.studio.generation;
|
package art.arcane.iris.studio.generation;
|
||||||
|
|
||||||
import art.arcane.iris.pack.loading.IrisData;
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
|
import art.arcane.iris.testsupport.BukkitTestServer;
|
||||||
import art.arcane.iris.pack.loading.ResourceLoader;
|
import art.arcane.iris.pack.loading.ResourceLoader;
|
||||||
import art.arcane.iris.studio.object.ObjectStudioLayout;
|
import art.arcane.iris.studio.object.ObjectStudioLayout;
|
||||||
import art.arcane.iris.studio.object.ObjectStudioSaveService;
|
import art.arcane.iris.studio.object.ObjectStudioSaveService;
|
||||||
@@ -38,7 +39,6 @@ import java.nio.file.Files;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
@@ -59,19 +59,9 @@ public class ObjectStudioGeometryTest {
|
|||||||
public final TemporaryFolder temporary = new TemporaryFolder();
|
public final TemporaryFolder temporary = new TemporaryFolder();
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public static void initializeBukkit() throws Exception {
|
public static void initializeBukkit() throws Exception {
|
||||||
PreObjectMatterTest.setUpBukkit();
|
PreObjectMatterTest.setUpBukkit();
|
||||||
AtomicReference<Registry<Biome>> registered = new AtomicReference<>();
|
RegistryAccess access = BukkitTestServer.registryAccess();
|
||||||
RegistryAccess access = mock(RegistryAccess.class, invocation -> {
|
|
||||||
if (invocation.getMethod().getName().equals("getRegistry")) {
|
|
||||||
if (registered.get() == null) {
|
|
||||||
registered.set(biomeRegistry());
|
|
||||||
}
|
|
||||||
return registered.get();
|
|
||||||
}
|
|
||||||
return RETURNS_DEFAULTS.answer(invocation);
|
|
||||||
});
|
|
||||||
InternalAPIBridge bridge = mock(InternalAPIBridge.class, invocation -> {
|
InternalAPIBridge bridge = mock(InternalAPIBridge.class, invocation -> {
|
||||||
if (invocation.getMethod().getName().equals("constructLegacyCustomBiome")) {
|
if (invocation.getMethod().getName().equals("constructLegacyCustomBiome")) {
|
||||||
return biome(Key.key("minecraft:custom"));
|
return biome(Key.key("minecraft:custom"));
|
||||||
@@ -89,20 +79,6 @@ public class ObjectStudioGeometryTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private static Registry<Biome> biomeRegistry() {
|
|
||||||
return (Registry<Biome>) Proxy.newProxyInstance(Registry.class.getClassLoader(),
|
|
||||||
new Class<?>[]{Registry.class},
|
|
||||||
(proxy, method, arguments) -> switch (method.getName()) {
|
|
||||||
case "getOrThrow" -> biome((Key) arguments[0]);
|
|
||||||
case "hashCode" -> System.identityHashCode(proxy);
|
|
||||||
case "equals" -> proxy == arguments[0];
|
|
||||||
case "toString" -> "iris-object-studio-biome-registry";
|
|
||||||
case "size" -> 0;
|
|
||||||
case "hasTag" -> false;
|
|
||||||
default -> null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void jungleClutterSignedBoundsStayInsideTheEditableCellAboveTheFloor() throws Exception {
|
public void jungleClutterSignedBoundsStayInsideTheEditableCellAboveTheFloor() throws Exception {
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package art.arcane.iris.world.entity;
|
||||||
|
|
||||||
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
|
|
||||||
|
public class EntitySpawnSeedTest {
|
||||||
|
@Test
|
||||||
|
public void coordinateAndAttemptSeedsRemainPinned() {
|
||||||
|
assertEquals(-4130335506775599505L, EntitySpawnSeed.chunk(0L, 0, 0).getSeed());
|
||||||
|
assertEquals(8337887711347547470L, EntitySpawnSeed.chunk(1337L, -2, 3).getSeed());
|
||||||
|
assertEquals(7508281853961706446L,
|
||||||
|
EntitySpawnSeed.chunk(-1L, Integer.MAX_VALUE, Integer.MIN_VALUE).getSeed());
|
||||||
|
assertEquals(-2305592493329120404L, EntitySpawnSeed.marker(0L, 0, 0, 0).getSeed());
|
||||||
|
assertEquals(8373759002518926811L, EntitySpawnSeed.marker(1337L, -32, -64, 48).getSeed());
|
||||||
|
assertEquals(-8713573891296014035L,
|
||||||
|
EntitySpawnSeed.marker(-1L, Integer.MAX_VALUE, Integer.MIN_VALUE, 1).getSeed());
|
||||||
|
assertEquals(-6302458730787398690L, EntitySpawnSeed.entity(1337L, -1).getSeed());
|
||||||
|
assertEquals(858402071020777840L, EntitySpawnSeed.entity(1337L, 0).getSeed());
|
||||||
|
assertEquals(-632476273033191086L, EntitySpawnSeed.entity(1337L, 1).getSeed());
|
||||||
|
assertEquals(4034378215462583149L, EntitySpawnSeed.entity(1337L, 17).getSeed());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void consumptionAndReverseOrderDoNotChangeSiblingAttempts() {
|
||||||
|
long batchSeed = EntitySpawnSeed.chunk(1337L, -2, 3).getSeed();
|
||||||
|
List<Long> forward = new ArrayList<>();
|
||||||
|
for (int ordinal = 0; ordinal < 16; ordinal++) {
|
||||||
|
forward.add(EntitySpawnSeed.entity(batchSeed, ordinal).nextLong());
|
||||||
|
}
|
||||||
|
for (int ordinal = 15; ordinal >= 0; ordinal--) {
|
||||||
|
RNG sibling = EntitySpawnSeed.entity(batchSeed, ordinal - 1);
|
||||||
|
for (int draw = 0; draw < 100; draw++) {
|
||||||
|
sibling.nextLong();
|
||||||
|
}
|
||||||
|
assertEquals(forward.get(ordinal).longValue(), EntitySpawnSeed.entity(batchSeed, ordinal).nextLong());
|
||||||
|
}
|
||||||
|
assertNotEquals(EntitySpawnSeed.marker(1337L, -32, -64, 48).getSeed(),
|
||||||
|
EntitySpawnSeed.marker(1337L, -32, -63, 48).getSeed());
|
||||||
|
assertNotEquals(EntitySpawnSeed.chunk(1337L, -2, 3).getSeed(),
|
||||||
|
EntitySpawnSeed.chunk(1338L, -2, 3).getSeed());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void concurrentCoordinateEvaluationMatchesSerialEvaluation() {
|
||||||
|
List<CompletableFuture<Long>> results = new ArrayList<>();
|
||||||
|
for (int ordinal = 0; ordinal < 64; ordinal++) {
|
||||||
|
int index = ordinal;
|
||||||
|
results.add(CompletableFuture.supplyAsync(() -> EntitySpawnSeed.marker(
|
||||||
|
42L, index - 32, -64 + index, -index).nextLong()));
|
||||||
|
}
|
||||||
|
for (int ordinal = 63; ordinal >= 0; ordinal--) {
|
||||||
|
assertEquals(EntitySpawnSeed.marker(42L, ordinal - 32, -64 + ordinal, -ordinal).nextLong(),
|
||||||
|
results.get(ordinal).join().longValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package art.arcane.iris.world.entity;
|
||||||
|
|
||||||
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
|
import org.bukkit.attribute.AttributeModifier;
|
||||||
|
import org.bukkit.inventory.meta.ItemMeta;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
|
|
||||||
|
public class IrisAttributeModifierSeedTest {
|
||||||
|
@Test
|
||||||
|
public void repeatedSeedProducesIdenticalModifierKeyAmountAndOperation() {
|
||||||
|
IrisAttributeModifier definition = new IrisAttributeModifier().setName("Custom Bonus")
|
||||||
|
.setMinAmount(-3).setMaxAmount(7).setOperation("ADD_SCALAR");
|
||||||
|
AttributeModifier first = definition.createModifier(new RNG(1337L));
|
||||||
|
AttributeModifier repeated = definition.createModifier(new RNG(1337L));
|
||||||
|
assertEquals(first.getKey(), repeated.getKey());
|
||||||
|
assertEquals("minecraft:custom_bonus_a8f1288a2c8a93b8_b072351edfd5484c", first.getKey().toString());
|
||||||
|
assertEquals(first.getAmount(), repeated.getAmount(), 0D);
|
||||||
|
assertEquals(5.832726771624211D, first.getAmount(), 0D);
|
||||||
|
assertEquals(AttributeModifier.Operation.ADD_SCALAR, first.getOperation());
|
||||||
|
assertTrue(first.getKey().getKey().startsWith("custom_bonus_"));
|
||||||
|
assertTrue(first.getAmount() >= -3D && first.getAmount() <= 7D);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sameNamedModifiersRemainDistinctWithinOneSeededSequence() {
|
||||||
|
IrisAttributeModifier definition = new IrisAttributeModifier().setName("Bonus");
|
||||||
|
RNG rng = new RNG(1337L);
|
||||||
|
AttributeModifier first = definition.createModifier(rng);
|
||||||
|
AttributeModifier second = definition.createModifier(rng);
|
||||||
|
assertNotEquals(first.getKey(), second.getKey());
|
||||||
|
assertNotEquals(first.getKey(), definition.createModifier(new RNG(1338L)).getKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void zeroChanceNeverAppliesAModifier() {
|
||||||
|
ItemMeta metadata = mock(ItemMeta.class);
|
||||||
|
IrisAttributeModifier definition = new IrisAttributeModifier().setChance(0D);
|
||||||
|
definition.apply(new RNG(1337L), metadata);
|
||||||
|
verifyNoInteractions(metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package art.arcane.iris.world.entity;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.loot.LootTable;
|
||||||
|
import org.bukkit.loot.Lootable;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
public class IrisEntityLootSeedTest {
|
||||||
|
@Test
|
||||||
|
public void lootKeyDependsOnSpawnSeedRatherThanEntityHashOrWorldIdentity() {
|
||||||
|
IrisEntity first = new IrisEntity().setCustomName("First");
|
||||||
|
IrisEntity second = new IrisEntity().setCustomName("Second");
|
||||||
|
assertNotEquals(first.hashCode(), second.hashCode());
|
||||||
|
LootTable firstTable = bind(first, new RNG(1337L), mock(World.class));
|
||||||
|
LootTable secondTable = bind(second, new RNG(1337L), mock(World.class));
|
||||||
|
assertEquals(firstTable.getKey(), secondTable.getKey());
|
||||||
|
assertEquals("iris:loot-22371b531cd5acd3", firstTable.getKey().toString());
|
||||||
|
assertNotEquals(firstTable.getKey(), bind(first, new RNG(1338L), mock(World.class)).getKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void priorCustomizationConsumptionDoesNotChangeTheLootIdentity() {
|
||||||
|
IrisEntity definition = new IrisEntity();
|
||||||
|
RNG consumed = new RNG(1337L);
|
||||||
|
for (int draw = 0; draw < 100; draw++) {
|
||||||
|
consumed.nextLong();
|
||||||
|
}
|
||||||
|
assertEquals(bind(definition, new RNG(1337L), mock(World.class)).getKey(),
|
||||||
|
bind(definition, consumed, mock(World.class)).getKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LootTable bind(IrisEntity definition, RNG rng, World world) {
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
Lootable lootable = mock(Lootable.class);
|
||||||
|
IrisEntity.BukkitOps.bindLoot(definition, engine, lootable, new Location(world, -31.5, -63.5, 48.5), rng);
|
||||||
|
ArgumentCaptor<LootTable> table = ArgumentCaptor.forClass(LootTable.class);
|
||||||
|
verify(lootable).setLootTable(table.capture());
|
||||||
|
return table.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package art.arcane.iris.world.entity;
|
|||||||
|
|
||||||
import art.arcane.iris.generation.decoration.IrisSurface;
|
import art.arcane.iris.generation.decoration.IrisSurface;
|
||||||
import art.arcane.iris.pack.value.IrisPosition;
|
import art.arcane.iris.pack.value.IrisPosition;
|
||||||
|
import art.arcane.iris.pack.value.IrisRange;
|
||||||
|
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.iris.generation.runtime.SeedManager;
|
import art.arcane.iris.generation.runtime.SeedManager;
|
||||||
@@ -29,12 +30,14 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.doReturn;
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.doAnswer;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.mockStatic;
|
import static org.mockito.Mockito.mockStatic;
|
||||||
import static org.mockito.Mockito.spy;
|
import static org.mockito.Mockito.spy;
|
||||||
@@ -381,9 +384,8 @@ public class IrisEntitySpawnTest {
|
|||||||
IrisPosition position = invocation.getArgument(0);
|
IrisPosition position = invocation.getArgument(0);
|
||||||
return new Location(fixture.world, position.getX(), position.getY(), position.getZ());
|
return new Location(fixture.world, position.getX(), position.getY(), position.getZ());
|
||||||
});
|
});
|
||||||
fixture.entry.setReferenceSpawner(new IrisSpawner());
|
assertEquals(0, fixture.entry.spawn(fixture.engine, new IrisPosition(0, 78, 0), new RNG(1L), new IrisEntitySpawn.SpawnContext(new IrisSpawner(), null, false)));
|
||||||
assertEquals(0, fixture.entry.spawn(fixture.engine, new IrisPosition(0, 78, 0), new RNG(1L)));
|
assertEquals(3, fixture.entry.spawn(fixture.engine, new IrisPosition(0, 77, 0), new RNG(1L), new IrisEntitySpawn.SpawnContext(new IrisSpawner(), null, false)));
|
||||||
assertEquals(3, fixture.entry.spawn(fixture.engine, new IrisPosition(0, 77, 0), new RNG(1L)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,6 +401,96 @@ public class IrisEntitySpawnTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failedFirstSiblingDoesNotChangeLaterPositionsOrCustomizationSeeds() {
|
||||||
|
try (SpawnFixture fixture = new SpawnFixture(new SpawnOptions(IrisSurface.LAND, Material.WATER, 78, 78))) {
|
||||||
|
assertEquals(3, fixture.spawn(IrisSpawnGroup.NORMAL));
|
||||||
|
List<Location> expectedPositions = new ArrayList<>(fixture.positions.subList(1, 3));
|
||||||
|
List<Long> expectedSeeds = new ArrayList<>(fixture.customizationSeeds.subList(1, 3));
|
||||||
|
Location rejected = fixture.positions.getFirst();
|
||||||
|
Block obstruction = mock(Block.class);
|
||||||
|
when(obstruction.getType()).thenReturn(Material.STONE);
|
||||||
|
when(obstruction.isSolid()).thenReturn(true);
|
||||||
|
when(fixture.world.getBlockAt(rejected.getBlockX(), rejected.getBlockY(), rejected.getBlockZ()))
|
||||||
|
.thenReturn(obstruction);
|
||||||
|
fixture.positions.clear();
|
||||||
|
fixture.customizationSeeds.clear();
|
||||||
|
assertEquals(2, fixture.spawn(IrisSpawnGroup.NORMAL));
|
||||||
|
assertEquals(expectedPositions, fixture.positions);
|
||||||
|
assertEquals(expectedSeeds, fixture.customizationSeeds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void initialCavePopulationDoesNotDependOnMarkerStorageOrder() {
|
||||||
|
try (SpawnFixture fixture = new SpawnFixture(new SpawnOptions(IrisSurface.LAND, Material.WATER, 78, 78));
|
||||||
|
MockedStatic<J> scheduler = mockStatic(J.class)) {
|
||||||
|
scheduler.when(J::isFolia).thenReturn(false);
|
||||||
|
EngineMantle mantle = mock(EngineMantle.class);
|
||||||
|
when(fixture.engine.getMantle()).thenReturn(mantle);
|
||||||
|
IrisPosition first = new IrisPosition(7, 79, 9);
|
||||||
|
IrisPosition second = new IrisPosition(3, 79, 5);
|
||||||
|
when(mantle.findMarkers(0, 0, MarkerMatter.CAVE_FLOOR))
|
||||||
|
.thenReturn(new KList<>(first, second), new KList<>(second, first));
|
||||||
|
fixture.platform.when(() -> BukkitPlatform.toLocation(any(IrisPosition.class), eq(fixture.world)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
IrisPosition position = invocation.getArgument(0);
|
||||||
|
return new Location(fixture.world, position.getX(), position.getY(), position.getZ());
|
||||||
|
});
|
||||||
|
IrisEntitySpawn.SpawnContext context = new IrisEntitySpawn.SpawnContext(
|
||||||
|
new IrisSpawner().setGroup(IrisSpawnGroup.CAVE), null, true);
|
||||||
|
assertEquals(3, fixture.entry.spawn(fixture.engine, fixture.chunk, new RNG(1337L), 3, context));
|
||||||
|
List<Location> expected = new ArrayList<>(fixture.positions);
|
||||||
|
List<Long> expectedSeeds = new ArrayList<>(fixture.customizationSeeds);
|
||||||
|
fixture.positions.clear();
|
||||||
|
fixture.customizationSeeds.clear();
|
||||||
|
assertEquals(3, fixture.entry.spawn(fixture.engine, fixture.chunk, new RNG(1337L), 3, context));
|
||||||
|
assertEquals(expected, fixture.positions);
|
||||||
|
assertEquals(expectedSeeds, fixture.customizationSeeds);
|
||||||
|
verify(mantle, times(2)).findMarkers(0, 0, MarkerMatter.CAVE_FLOOR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void initialPopulationIgnoresLiveLightButRecurringPopulationHonorsIt() {
|
||||||
|
try (SpawnFixture fixture = new SpawnFixture(new SpawnOptions(IrisSurface.LAND, Material.WATER, 78, 78))) {
|
||||||
|
fixture.lightLevel = 15;
|
||||||
|
IrisSpawner spawner = new IrisSpawner().setAllowedLightLevels(new IrisRange(0, 0));
|
||||||
|
assertEquals(0, fixture.entry.spawn(fixture.engine, fixture.chunk, new RNG(1337L), 3,
|
||||||
|
new IrisEntitySpawn.SpawnContext(spawner, null, false)));
|
||||||
|
assertEquals(3, fixture.entry.spawn(fixture.engine, fixture.chunk, new RNG(1337L), 3,
|
||||||
|
new IrisEntitySpawn.SpawnContext(spawner, null, true)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void markerExhaustionUsesOnlyTheProvidedSeed() {
|
||||||
|
IrisMarker marker = new IrisMarker().setExhaustionChance(0.5D);
|
||||||
|
for (long seed = 0; seed < 32; seed++) {
|
||||||
|
assertEquals(marker.shouldExhaust(new RNG(seed)), marker.shouldExhaust(new RNG(seed)));
|
||||||
|
}
|
||||||
|
assertTrue(marker.setExhaustionChance(1D).shouldExhaust(new RNG(1337L)));
|
||||||
|
assertFalse(marker.setExhaustionChance(-1D).shouldExhaust(new RNG(1337L)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void directEntitySeedDoesNotDependOnWorldObjectIdentity() {
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
SeedManager seeds = mock(SeedManager.class);
|
||||||
|
when(engine.getSeedManager()).thenReturn(seeds);
|
||||||
|
when(seeds.getEntity()).thenReturn(1337L);
|
||||||
|
IrisEntity entity = spy(new IrisEntity());
|
||||||
|
List<Long> captured = new ArrayList<>();
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
RNG rng = invocation.getArgument(2);
|
||||||
|
captured.add(rng.getSeed());
|
||||||
|
return null;
|
||||||
|
}).when(entity).spawn(eq(engine), any(Location.class), any(RNG.class));
|
||||||
|
entity.spawn(engine, new Location(mock(World.class), -31.5, -63.5, 48.5));
|
||||||
|
entity.spawn(engine, new Location(mock(World.class), -31.5, -63.5, 48.5));
|
||||||
|
assertEquals(List.of(8373759002518926811L, 8373759002518926811L), captured);
|
||||||
|
}
|
||||||
|
|
||||||
private record SpawnOptions(IrisSurface surface, Material fluid, int floor, int top) {
|
private record SpawnOptions(IrisSurface surface, Material fluid, int floor, int top) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,6 +500,8 @@ public class IrisEntitySpawnTest {
|
|||||||
private final Chunk chunk = mock(Chunk.class);
|
private final Chunk chunk = mock(Chunk.class);
|
||||||
private final IrisEntitySpawn entry = spy(new IrisEntitySpawn());
|
private final IrisEntitySpawn entry = spy(new IrisEntitySpawn());
|
||||||
private final List<Location> positions = new ArrayList<>();
|
private final List<Location> positions = new ArrayList<>();
|
||||||
|
private final List<Long> customizationSeeds = new ArrayList<>();
|
||||||
|
private int lightLevel;
|
||||||
private final MockedStatic<BukkitPlatform> platform = mockStatic(BukkitPlatform.class);
|
private final MockedStatic<BukkitPlatform> platform = mockStatic(BukkitPlatform.class);
|
||||||
private Material airMaterial = Material.AIR;
|
private Material airMaterial = Material.AIR;
|
||||||
private int remainingCapacity = Integer.MAX_VALUE;
|
private int remainingCapacity = Integer.MAX_VALUE;
|
||||||
@@ -430,6 +524,7 @@ public class IrisEntitySpawnTest {
|
|||||||
Block block = mock(Block.class);
|
Block block = mock(Block.class);
|
||||||
when(block.getType()).thenReturn(y <= floor ? Material.STONE : y <= top ? fluid : airMaterial);
|
when(block.getType()).thenReturn(y <= floor ? Material.STONE : y <= top ? fluid : airMaterial);
|
||||||
when(block.isSolid()).thenReturn(y <= floor);
|
when(block.isSolid()).thenReturn(y <= floor);
|
||||||
|
when(block.getLightLevel()).thenReturn((byte) lightLevel);
|
||||||
return block;
|
return block;
|
||||||
});
|
});
|
||||||
when(world.getBlockAt(any(Location.class))).thenAnswer(invocation -> {
|
when(world.getBlockAt(any(Location.class))).thenAnswer(invocation -> {
|
||||||
@@ -439,6 +534,8 @@ public class IrisEntitySpawnTest {
|
|||||||
when(definition.spawn(eq(engine), any(Location.class), any(RNG.class))).thenAnswer(invocation -> {
|
when(definition.spawn(eq(engine), any(Location.class), any(RNG.class))).thenAnswer(invocation -> {
|
||||||
Location location = invocation.getArgument(1);
|
Location location = invocation.getArgument(1);
|
||||||
positions.add(location.clone());
|
positions.add(location.clone());
|
||||||
|
RNG customization = invocation.getArgument(2);
|
||||||
|
customizationSeeds.add(customization.getSeed());
|
||||||
Entity spawned = mock(Entity.class);
|
Entity spawned = mock(Entity.class);
|
||||||
when(spawned.getLocation()).thenReturn(location.clone());
|
when(spawned.getLocation()).thenReturn(location.clone());
|
||||||
when(spawned.getType()).thenReturn(EntityType.TADPOLE);
|
when(spawned.getType()).thenReturn(EntityType.TADPOLE);
|
||||||
@@ -451,8 +548,8 @@ public class IrisEntitySpawnTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private int spawn(IrisSpawnGroup group) {
|
private int spawn(IrisSpawnGroup group) {
|
||||||
entry.setReferenceSpawner(new IrisSpawner().setGroup(group));
|
return entry.spawn(engine, chunk, new RNG(1337L), remainingCapacity,
|
||||||
return entry.spawn(engine, chunk, new RNG(1337L), remainingCapacity);
|
new IrisEntitySpawn.SpawnContext(new IrisSpawner().setGroup(group), null, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
package art.arcane.iris.testsupport;
|
package art.arcane.iris.testsupport;
|
||||||
|
|
||||||
|
|
||||||
|
import io.papermc.paper.registry.RegistryAccess;
|
||||||
|
import io.papermc.paper.registry.RegistryKey;
|
||||||
|
import net.kyori.adventure.key.Key;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.GameRule;
|
||||||
|
import org.bukkit.NamespacedKey;
|
||||||
|
import org.bukkit.Registry;
|
||||||
|
import org.bukkit.block.Biome;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.Server;
|
import org.bukkit.Server;
|
||||||
import org.bukkit.block.data.BlockData;
|
import org.bukkit.block.data.BlockData;
|
||||||
@@ -11,7 +18,10 @@ import org.junit.runner.Description;
|
|||||||
import org.junit.runners.model.Statement;
|
import org.junit.runners.model.Statement;
|
||||||
import org.mockito.invocation.InvocationOnMock;
|
import org.mockito.invocation.InvocationOnMock;
|
||||||
|
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
@@ -20,6 +30,7 @@ import static org.mockito.ArgumentMatchers.anyString;
|
|||||||
import static org.mockito.Mockito.doAnswer;
|
import static org.mockito.Mockito.doAnswer;
|
||||||
import static org.mockito.Mockito.doReturn;
|
import static org.mockito.Mockito.doReturn;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.RETURNS_DEFAULTS;
|
||||||
|
|
||||||
public final class BukkitTestServer {
|
public final class BukkitTestServer {
|
||||||
private static final String DEFAULT_NAME = "IrisTestServer";
|
private static final String DEFAULT_NAME = "IrisTestServer";
|
||||||
@@ -71,6 +82,45 @@ public final class BukkitTestServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static RegistryAccess registryAccess() {
|
||||||
|
Map<Object, Object> registries = new HashMap<>();
|
||||||
|
return mock(RegistryAccess.class, invocation -> {
|
||||||
|
if (!invocation.getMethod().getName().equals("getRegistry")) {
|
||||||
|
return RETURNS_DEFAULTS.answer(invocation);
|
||||||
|
}
|
||||||
|
Object key = invocation.getArgument(0);
|
||||||
|
return registries.computeIfAbsent(key, registryKey -> registryProxy(registryKey == RegistryKey.GAME_RULE
|
||||||
|
|| registryKey == GameRule.class));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object registryProxy(boolean gameRules) {
|
||||||
|
Map<Key, Object> values = new HashMap<>();
|
||||||
|
return Proxy.newProxyInstance(Registry.class.getClassLoader(), new Class<?>[]{Registry.class},
|
||||||
|
(proxy, method, arguments) -> switch (method.getName()) {
|
||||||
|
case "getOrThrow", "get" -> values.computeIfAbsent((Key) arguments[0],
|
||||||
|
key -> gameRules ? mock(GameRule.class) : biome(key));
|
||||||
|
case "hashCode" -> System.identityHashCode(proxy);
|
||||||
|
case "equals" -> proxy == arguments[0];
|
||||||
|
case "toString" -> gameRules ? "iris-test-game-rule-registry" : "iris-test-biome-registry";
|
||||||
|
case "size" -> values.size();
|
||||||
|
case "hasTag" -> false;
|
||||||
|
default -> null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Biome biome(Key key) {
|
||||||
|
NamespacedKey namespaced = new NamespacedKey(key.namespace(), key.value());
|
||||||
|
return (Biome) Proxy.newProxyInstance(Biome.class.getClassLoader(), new Class<?>[]{Biome.class},
|
||||||
|
(proxy, method, arguments) -> switch (method.getName()) {
|
||||||
|
case "getKey" -> namespaced;
|
||||||
|
case "hashCode" -> namespaced.hashCode();
|
||||||
|
case "equals" -> proxy == arguments[0];
|
||||||
|
case "toString", "name" -> key.value();
|
||||||
|
default -> null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public static ServerBranding branding() {
|
public static ServerBranding branding() {
|
||||||
return new ServerBranding();
|
return new ServerBranding();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user