mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-12 03:24:14 +00:00
Compare commits
53
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12e8bb5a09 | ||
|
|
a765e76aa3 | ||
|
|
4ef6fc7cc6 | ||
|
|
6b82ea99e2 | ||
|
|
813f3c7bcf | ||
|
|
318c1e100b | ||
|
|
357f14e900 | ||
|
|
99cc2383d6 | ||
|
|
76d71f8344 | ||
|
|
1544c34f44 | ||
|
|
00601678d9 | ||
|
|
700dc53559 | ||
|
|
0faae93049 | ||
|
|
92310ef839 | ||
|
|
5fa6d82b79 | ||
|
|
2078bece40 | ||
|
|
077b48f47f | ||
|
|
7981fa3ce5 | ||
|
|
5c4f7bb521 | ||
|
|
0d16f8e57f | ||
|
|
cf7ebcece1 | ||
|
|
8f8a3b2355 | ||
|
|
ffc409a4ac | ||
|
|
5b5c1975c6 | ||
|
|
25c04c0157 | ||
|
|
1e25c07eac | ||
|
|
ff74a6f811 | ||
|
|
9e07009f1d | ||
|
|
53172c38ad | ||
|
|
658c6b0fde | ||
|
|
b998ff535a | ||
|
|
ba4708f2bb | ||
|
|
bda770650b | ||
|
|
8bccb48c09 | ||
|
|
a9df19cae2 | ||
|
|
4ac49baaff | ||
|
|
38a7e0fdb5 | ||
|
|
7e73e8f599 | ||
|
|
788ac06713 | ||
|
|
622b40f786 | ||
|
|
72eff9b775 | ||
|
|
789644b93f | ||
|
|
f07dfb7a85 | ||
|
|
5bffa6eb04 | ||
|
|
0b6ac19fa7 | ||
|
|
b7e6259a05 | ||
|
|
a0aa62b8c6 | ||
|
|
d80fcedd91 | ||
|
|
41664fc605 | ||
|
|
74df63f717 | ||
|
|
f46041690b | ||
|
|
b8433340c9 | ||
|
|
593c3f732a |
+35
@@ -0,0 +1,35 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
final class GenerationRateWindow {
|
||||
static final long TIMING_IDLE_MILLIS = 10_000L;
|
||||
|
||||
private long lastSampleAtMs;
|
||||
private int lastGeneratedCount;
|
||||
private long lastGenerationAtMs;
|
||||
|
||||
double sampleChunksPerSecond(int generatedCount, long sampledAtMs) {
|
||||
int safeGeneratedCount = Math.max(0, generatedCount);
|
||||
long previousAt = lastSampleAtMs;
|
||||
int previousCount = lastGeneratedCount;
|
||||
lastSampleAtMs = sampledAtMs;
|
||||
lastGeneratedCount = safeGeneratedCount;
|
||||
if (previousAt <= 0L) {
|
||||
if (safeGeneratedCount > 0) {
|
||||
lastGenerationAtMs = sampledAtMs;
|
||||
}
|
||||
return 0D;
|
||||
}
|
||||
if (sampledAtMs <= previousAt) {
|
||||
return 0D;
|
||||
}
|
||||
int generatedDelta = Math.max(0, safeGeneratedCount - previousCount);
|
||||
if (generatedDelta > 0) {
|
||||
lastGenerationAtMs = sampledAtMs;
|
||||
}
|
||||
return generatedDelta * 1000D / (sampledAtMs - previousAt);
|
||||
}
|
||||
|
||||
boolean generationTimingsFresh(long sampledAtMs) {
|
||||
return lastGenerationAtMs > 0L && sampledAtMs - lastGenerationAtMs <= TIMING_IDLE_MILLIS;
|
||||
}
|
||||
}
|
||||
+8
-17
@@ -460,8 +460,12 @@ public final class IrisEngineSVC implements IrisService {
|
||||
continue;
|
||||
}
|
||||
|
||||
double chunksPerSecond = registered.sampleChunksPerSecond(engine.getGenerated(), now);
|
||||
worldSnapshots.add(EngineTelemetrySnapshot.capture(engine, chunksPerSecond, now));
|
||||
double chunksPerSecond = registered.generationRate.sampleChunksPerSecond(engine.getGenerated(), now);
|
||||
worldSnapshots.add(EngineTelemetrySnapshot.capture(
|
||||
engine,
|
||||
chunksPerSecond,
|
||||
registered.generationRate.generationTimingsFresh(now),
|
||||
now));
|
||||
registered.clearMetricsFailure();
|
||||
} catch (Throwable exception) {
|
||||
if (EngineMaintenance.isMantleClosed(exception)) {
|
||||
@@ -552,6 +556,7 @@ public final class IrisEngineSVC implements IrisService {
|
||||
progress.generated(),
|
||||
progress.totalChunks(),
|
||||
progress.chunksRemaining(),
|
||||
progress.chunksInFlight(),
|
||||
progress.chunksPerSecond(),
|
||||
progress.eta(),
|
||||
progress.elapsed(),
|
||||
@@ -612,8 +617,7 @@ public final class IrisEngineSVC implements IrisService {
|
||||
private final AtomicReference<CompletableFuture<Void>> activeMaintenance = new AtomicReference<>();
|
||||
private volatile ScheduledFuture<?> maintenance;
|
||||
private volatile boolean closed;
|
||||
private long lastGeneratedSampleAtMs;
|
||||
private int lastGeneratedCount;
|
||||
private final GenerationRateWindow generationRate = new GenerationRateWindow();
|
||||
|
||||
private Registered(Registration registration, ScheduledThreadPoolExecutor executor) {
|
||||
name = registration.name();
|
||||
@@ -696,19 +700,6 @@ public final class IrisEngineSVC implements IrisService {
|
||||
return activeMaintenance.get() != null;
|
||||
}
|
||||
|
||||
private double sampleChunksPerSecond(int generatedCount, long sampledAtMs) {
|
||||
int safeGeneratedCount = Math.max(0, generatedCount);
|
||||
long previousAt = lastGeneratedSampleAtMs;
|
||||
int previousCount = lastGeneratedCount;
|
||||
lastGeneratedSampleAtMs = sampledAtMs;
|
||||
lastGeneratedCount = safeGeneratedCount;
|
||||
if (previousAt <= 0L || sampledAtMs <= previousAt) {
|
||||
return 0D;
|
||||
}
|
||||
int generatedDelta = Math.max(0, safeGeneratedCount - previousCount);
|
||||
return generatedDelta * 1000D / (sampledAtMs - previousAt);
|
||||
}
|
||||
|
||||
private void close() {
|
||||
if (closed) {
|
||||
return;
|
||||
|
||||
+3
@@ -101,6 +101,7 @@ public record IrisTelemetrySnapshot(
|
||||
long generated,
|
||||
long total,
|
||||
long remaining,
|
||||
long inFlight,
|
||||
double chunksPerSecond,
|
||||
long etaMs,
|
||||
long elapsedMs,
|
||||
@@ -115,6 +116,7 @@ public record IrisTelemetrySnapshot(
|
||||
0L,
|
||||
0L,
|
||||
0L,
|
||||
0L,
|
||||
0D,
|
||||
0L,
|
||||
0L,
|
||||
@@ -130,6 +132,7 @@ public record IrisTelemetrySnapshot(
|
||||
generated = Math.max(0L, generated);
|
||||
total = Math.max(0L, total);
|
||||
remaining = Math.max(0L, remaining);
|
||||
inFlight = Math.max(0L, inFlight);
|
||||
chunksPerSecond = Double.isFinite(chunksPerSecond) ? Math.max(0D, chunksPerSecond) : 0D;
|
||||
etaMs = Math.max(0L, etaMs);
|
||||
elapsedMs = Math.max(0L, elapsedMs);
|
||||
|
||||
+3
-1
@@ -319,6 +319,7 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
|
||||
IntegrationMetricSchema.IRIS_PREGEN_GENERATED,
|
||||
IntegrationMetricSchema.IRIS_PREGEN_TOTAL,
|
||||
IntegrationMetricSchema.IRIS_PREGEN_QUEUE,
|
||||
IntegrationMetricSchema.IRIS_PREGEN_REMAINING,
|
||||
IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT,
|
||||
IntegrationMetricSchema.IRIS_PREGEN_ETA_MS,
|
||||
IntegrationMetricSchema.IRIS_PREGEN_ELAPSED_MS,
|
||||
@@ -332,7 +333,8 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
|
||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_PROGRESS, pregenerator.progressPercent(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_GENERATED, pregenerator.generated(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_TOTAL, pregenerator.total(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_QUEUE, pregenerator.remaining(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_QUEUE, pregenerator.inFlight(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_REMAINING, pregenerator.remaining(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT, pregenerator.chunksPerSecond(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_ETA_MS, pregenerator.etaMs(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_ELAPSED_MS, pregenerator.elapsedMs(), sampledAtMs);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
}
|
||||
|
||||
version = '1.0.0'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url = 'https://repo.papermc.io/repository/maven-public/' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly files(providers.gradleProperty('irisJar').get())
|
||||
compileOnly files(providers.gradleProperty('irisGsonJar').get())
|
||||
compileOnly 'io.papermc.paper:paper-api:26.1.2.build.74-stable'
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain.languageVersion = JavaLanguageVersion.of(25)
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java.srcDirs = ['src']
|
||||
resources.srcDirs = ['resources']
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.compilerArgs += '-parameters'
|
||||
}
|
||||
|
||||
tasks.named('jar') {
|
||||
archiveBaseName = 'IrisNaturalSpawningFixture'
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
name: IrisNaturalSpawningFixture
|
||||
version: 1.0.0
|
||||
main: art.arcane.iris.fixture.NaturalSpawningFixture
|
||||
api-version: '26.1.2'
|
||||
folia-supported: true
|
||||
depend: [Iris]
|
||||
commands:
|
||||
irisspawnqa:
|
||||
permission: irisspawnqa.run
|
||||
permissions:
|
||||
irisspawnqa.run:
|
||||
default: op
|
||||
@@ -0,0 +1 @@
|
||||
rootProject.name = 'iris-natural-spawning-fixture'
|
||||
+472
@@ -0,0 +1,472 @@
|
||||
package art.arcane.iris.fixture;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.runtime.BiomeEnvironment;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.world.IrisCreator;
|
||||
import art.arcane.iris.world.entity.IrisSpawner;
|
||||
import art.arcane.iris.world.entity.IrisEntitySpawn;
|
||||
import art.arcane.iris.world.entity.IrisEntity;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.platform.bukkit.BukkitEntityType;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Difficulty;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.HeightMap;
|
||||
import org.bukkit.GameRules;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.CreatureSpawnEvent;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class NaturalSpawningFixture extends JavaPlugin implements Listener {
|
||||
private static final NamespacedKey WORLD_KEY = new NamespacedKey("iris", "spawn_qa");
|
||||
private static final long WORLD_SEED = 78264193L;
|
||||
private static final int FROG_CAPACITY = 16;
|
||||
private static final int ZOMBIE_CAPACITY = 2;
|
||||
private static final int BIOME_READY_ATTEMPTS = 60;
|
||||
private static final long BIOME_RETRY_TICKS = 10L;
|
||||
|
||||
private boolean previousAmbient;
|
||||
private boolean previousMarkers;
|
||||
private boolean previousForcedPersistence;
|
||||
private volatile boolean ready;
|
||||
private int firstZombieBatch;
|
||||
private int ambientEvents;
|
||||
private int cancelledAmbientEvents;
|
||||
private final ThreadLocal<Boolean> ambientInvocation = ThreadLocal.withInitial(() -> false);
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
Bukkit.getPluginManager().registerEvents(this, this);
|
||||
previousAmbient = IrisSettings.get().getWorld().isAmbientEntitySpawningSystem();
|
||||
previousMarkers = IrisSettings.get().getWorld().isMarkerEntitySpawningSystem();
|
||||
previousForcedPersistence = IrisSettings.get().getWorld().isForcePersistEntities();
|
||||
IrisSettings.get().getWorld().setAmbientEntitySpawningSystem(false);
|
||||
IrisSettings.get().getWorld().setMarkerEntitySpawningSystem(false);
|
||||
IrisSettings.get().getWorld().setForcePersistEntities(false);
|
||||
getLogger().info("Natural spawning fixture ready; commands use the Iris ambient path on the owning region.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
IrisSettings.get().getWorld().setAmbientEntitySpawningSystem(previousAmbient);
|
||||
IrisSettings.get().getWorld().setMarkerEntitySpawningSystem(previousMarkers);
|
||||
IrisSettings.get().getWorld().setForcePersistEntities(previousForcedPersistence);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
|
||||
if (arguments.length != 1) {
|
||||
notifySender(sender, "Use /irisspawnqa setup|frogs|zombies|stress|check|surface|cave");
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
switch (arguments[0]) {
|
||||
case "setup" -> setup(sender);
|
||||
case "frogs" -> runRegion(sender, () -> spawnFrogs(sender));
|
||||
case "zombies" -> runRegion(sender, () -> spawnZombies(sender));
|
||||
case "stress" -> runRegion(sender, () -> stress(sender));
|
||||
case "check" -> runRegion(sender, () -> check(sender));
|
||||
case "surface" -> view(sender, 67D);
|
||||
case "cave" -> view(sender, -46D);
|
||||
default -> throw new IllegalArgumentException("Unknown spawning operation");
|
||||
}
|
||||
} catch (Throwable failure) {
|
||||
fail(sender, failure);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void isolateSpawns(CreatureSpawnEvent event) {
|
||||
if (!WORLD_KEY.equals(event.getLocation().getWorld().getKey())) {
|
||||
return;
|
||||
}
|
||||
if (!ambientInvocation.get()) {
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
ambientEvents++;
|
||||
if (event.isCancelled()) {
|
||||
cancelledAmbientEvents++;
|
||||
}
|
||||
}
|
||||
|
||||
private void setup(CommandSender sender) {
|
||||
if (!(sender instanceof Player)) {
|
||||
throw new IllegalArgumentException("Setup requires a player");
|
||||
}
|
||||
World world = Bukkit.getWorld(WORLD_KEY);
|
||||
if (world != null) {
|
||||
require(IrisToolbelt.isIrisWorld(world) && world.getSeed() == WORLD_SEED,
|
||||
"Existing fixture key is not the owned Iris world with the expected seed");
|
||||
ready = false;
|
||||
Bukkit.getGlobalRegionScheduler().execute(this, () -> configureWorld(sender, world));
|
||||
return;
|
||||
}
|
||||
ready = false;
|
||||
Bukkit.getAsyncScheduler().runNow(this, task -> createWorld(sender));
|
||||
}
|
||||
|
||||
private void createWorld(CommandSender sender) {
|
||||
try {
|
||||
World world = new IrisCreator().name(WORLD_KEY.toString()).dimension("spawn-qa")
|
||||
.seed(WORLD_SEED).create();
|
||||
Bukkit.getGlobalRegionScheduler().execute(this, () -> configureWorld(sender, world));
|
||||
} catch (Throwable failure) {
|
||||
fail(sender, failure);
|
||||
}
|
||||
}
|
||||
|
||||
private void configureWorld(CommandSender sender, World world) {
|
||||
try {
|
||||
world.setDifficulty(Difficulty.NORMAL);
|
||||
world.setGameRule(GameRules.SPAWN_MOBS, true);
|
||||
List<CompletableFuture<Void>> chunks = new ArrayList<>();
|
||||
for (int chunkX = -1; chunkX <= 1; chunkX++) {
|
||||
for (int chunkZ = -1; chunkZ <= 1; chunkZ++) {
|
||||
chunks.add(retainChunk(world, chunkX, chunkZ));
|
||||
}
|
||||
}
|
||||
CompletableFuture.allOf(chunks.toArray(CompletableFuture[]::new)).whenComplete((ignored, failure) -> {
|
||||
if (failure != null) {
|
||||
fail(sender, failure);
|
||||
} else {
|
||||
Bukkit.getRegionScheduler().execute(this, world, 0, 0, () -> prepareChunk(sender, world));
|
||||
}
|
||||
});
|
||||
} catch (Throwable failure) {
|
||||
fail(sender, failure);
|
||||
}
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> retainChunk(World world, int chunkX, int chunkZ) {
|
||||
CompletableFuture<Void> retained = new CompletableFuture<>();
|
||||
world.getChunkAtAsync(chunkX, chunkZ, true).whenComplete((chunk, failure) -> {
|
||||
if (failure != null) {
|
||||
retained.completeExceptionally(failure);
|
||||
return;
|
||||
}
|
||||
Bukkit.getRegionScheduler().execute(this, world, chunkX, chunkZ, () -> {
|
||||
try {
|
||||
chunk.addPluginChunkTicket(this);
|
||||
retained.complete(null);
|
||||
} catch (Throwable error) {
|
||||
retained.completeExceptionally(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
return retained;
|
||||
}
|
||||
|
||||
private void prepareChunk(CommandSender sender, World world) {
|
||||
try {
|
||||
require(WORLD_KEY.equals(world.getKey()) && IrisToolbelt.isIrisWorld(world)
|
||||
&& world.getSeed() == WORLD_SEED, "Fixture world ownership changed");
|
||||
Chunk chunk = world.getChunkAt(0, 0);
|
||||
int removed = 0;
|
||||
for (Entity entity : chunk.getEntities()) {
|
||||
if (entity instanceof LivingEntity && !(entity instanceof Player)) {
|
||||
entity.remove();
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
for (Entity entity : chunk.getEntities()) {
|
||||
require(!(entity instanceof LivingEntity) || entity instanceof Player || !entity.isValid(),
|
||||
"Fixture target chunk retained a living mob after cleanup: " + entity.getType());
|
||||
}
|
||||
getLogger().info("SPAWN_QA_CLEANUP removed=" + removed + " chunk=0,0");
|
||||
firstZombieBatch = 0;
|
||||
ambientEvents = 0;
|
||||
cancelledAmbientEvents = 0;
|
||||
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);
|
||||
}
|
||||
for (int y = -48; y <= -42; y++) {
|
||||
boolean shell = y == -48 || y == -42 || x == 0 || x == 15 || z == 0 || z == 15;
|
||||
chunk.getBlock(x, y, z).setType(shell ? Material.STONE : Material.CAVE_AIR, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!IrisToolbelt.isIrisWorld(world) || world.getSeed() != WORLD_SEED) {
|
||||
throw new IllegalStateException("Fixture is not the requested Iris world");
|
||||
}
|
||||
awaitBiomeEnvironment(sender, world, 0);
|
||||
} catch (Throwable failure) {
|
||||
fail(sender, failure);
|
||||
}
|
||||
}
|
||||
|
||||
private void awaitBiomeEnvironment(CommandSender sender, World world, int attempt) {
|
||||
try {
|
||||
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||
engine.getSurfaceBiomeEnvironment(8, 8);
|
||||
ready = true;
|
||||
teleport(sender, world, 67D, "SPAWN_QA_SETUP world=" + WORLD_KEY + " seed=" + WORLD_SEED);
|
||||
} catch (SavedBiomeUnavailableException unavailable) {
|
||||
if (!unavailable.isLoading() || attempt >= BIOME_READY_ATTEMPTS) {
|
||||
fail(sender, unavailable);
|
||||
return;
|
||||
}
|
||||
Bukkit.getRegionScheduler().runDelayed(this, world, 0, 0,
|
||||
task -> awaitBiomeEnvironment(sender, world, attempt + 1), BIOME_RETRY_TICKS);
|
||||
} catch (Throwable failure) {
|
||||
fail(sender, failure);
|
||||
}
|
||||
}
|
||||
|
||||
private void spawnFrogs(CommandSender sender) {
|
||||
Chunk chunk = requireWorld().getChunkAt(0, 0);
|
||||
RuntimeSpawn spawn = runtimeSpawn(chunk, "frogs");
|
||||
getLogger().info(spawnDiagnostics(spawn, chunk));
|
||||
for (int attempt = 0; attempt < 12; attempt++) {
|
||||
invokeAmbient(spawn, chunk);
|
||||
require(count(chunk, EntityType.FROG) <= FROG_CAPACITY, "Frog batch exceeded its category capacity");
|
||||
}
|
||||
require(count(chunk, EntityType.FROG) == FROG_CAPACITY,
|
||||
"Frog ambient category did not fill: " + spawnDiagnostics(spawn, chunk));
|
||||
require(count(chunk, EntityType.ZOMBIE) == 0, "Unexpected zombie before cave phase");
|
||||
verifyPersistence(chunk);
|
||||
evidence(sender, "SPAWN_QA_FROGS frogs=" + count(chunk, EntityType.FROG)
|
||||
+ " persistent=true removable=true ambient=true");
|
||||
}
|
||||
|
||||
private void spawnZombies(CommandSender sender) {
|
||||
Chunk chunk = requireWorld().getChunkAt(0, 0);
|
||||
require(count(chunk, EntityType.FROG) == FROG_CAPACITY, "Frogs must fill their category before cave spawning");
|
||||
require(count(chunk, EntityType.ZOMBIE) == 0, "Cave phase already ran");
|
||||
RuntimeSpawn spawn = runtimeSpawn(chunk, "cave-zombies");
|
||||
getLogger().info(spawnDiagnostics(spawn, chunk));
|
||||
invokeAmbient(spawn, chunk);
|
||||
firstZombieBatch = count(chunk, EntityType.ZOMBIE);
|
||||
require(firstZombieBatch == ZOMBIE_CAPACITY,
|
||||
"Authored seven-zombie batch was not clamped to capacity two: " + spawnDiagnostics(spawn, chunk));
|
||||
require(count(chunk, EntityType.FROG) == FROG_CAPACITY, "Cave spawning changed the passive population");
|
||||
verifyPersistence(chunk);
|
||||
evidence(sender, "SPAWN_QA_ZOMBIES frogs=" + count(chunk, EntityType.FROG)
|
||||
+ " zombies=" + firstZombieBatch + " firstBatch=" + firstZombieBatch + " ambient=true");
|
||||
}
|
||||
|
||||
private void stress(CommandSender sender) {
|
||||
Chunk chunk = requireWorld().getChunkAt(0, 0);
|
||||
RuntimeSpawn spawn = runtimeSpawn(chunk, "cave-zombies");
|
||||
for (int attempt = 0; attempt < 50; attempt++) {
|
||||
invokeAmbient(spawn, chunk);
|
||||
require(count(chunk, EntityType.ZOMBIE) == ZOMBIE_CAPACITY, "Saturated zombie category exceeded capacity");
|
||||
}
|
||||
evidence(sender, "SPAWN_QA_STRESS attempts=50 zombies=" + count(chunk, EntityType.ZOMBIE));
|
||||
}
|
||||
|
||||
private void check(CommandSender sender) {
|
||||
Chunk chunk = requireWorld().getChunkAt(0, 0);
|
||||
require(count(chunk, EntityType.FROG) == FROG_CAPACITY, "Passive population differs");
|
||||
require(count(chunk, EntityType.ZOMBIE) == ZOMBIE_CAPACITY, "Hostile population differs");
|
||||
require(firstZombieBatch == ZOMBIE_CAPACITY, "First hostile batch was not pinned");
|
||||
require(chunk.getBlock(8, -47, 8).getType() == Material.CAVE_AIR, "Cave is not real CAVE_AIR");
|
||||
require(chunk.getBlock(8, -47, 8).getLightLevel() == 0, "Cave is not dark");
|
||||
for (Entity entity : chunk.getEntities()) {
|
||||
if (entity.getType() == EntityType.ZOMBIE) {
|
||||
require(entity.getLocation().getY() < -42, "Hostile spawned outside the cave");
|
||||
}
|
||||
}
|
||||
verifyPersistence(chunk);
|
||||
evidence(sender, "SPAWN_QA_RESULT frogs=16 zombies=2 firstBatch=2 persistent=true removable=true caveAir=true light=0");
|
||||
}
|
||||
|
||||
private RuntimeSpawn runtimeSpawn(Chunk chunk, String biomeKey) {
|
||||
try {
|
||||
Engine engine = IrisToolbelt.access(chunk.getWorld()).getEngine();
|
||||
BiomeEnvironment surface = engine.getSurfaceBiomeEnvironment(8, 8);
|
||||
IrisBiome biome = surface.data().getBiomeLoader().load(biomeKey);
|
||||
require(biome != null, "Synthetic biome was not loaded");
|
||||
BiomeEnvironment selected = new BiomeEnvironment(surface.activationId(), biome, surface.region(),
|
||||
surface.dimension(), surface.data());
|
||||
Object manager = engine.getWorldManager();
|
||||
Field field = manager.getClass().getDeclaredField("entitySpawner");
|
||||
field.setAccessible(true);
|
||||
Object spawner = field.get(manager);
|
||||
Method method = spawner.getClass().getDeclaredMethod("spawnAmbient", Chunk.class, boolean.class, BiomeEnvironment.class);
|
||||
method.setAccessible(true);
|
||||
return new RuntimeSpawn(engine, spawner, method, selected);
|
||||
} catch (ReflectiveOperationException failure) {
|
||||
throw new IllegalStateException("Could not access Iris ambient spawning", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private String spawnDiagnostics(RuntimeSpawn spawn, Chunk chunk) {
|
||||
World world = chunk.getWorld();
|
||||
int floor = world.getHighestBlockYAt(8, 8, HeightMap.OCEAN_FLOOR);
|
||||
int top = world.getHighestBlockYAt(8, 8, HeightMap.WORLD_SURFACE);
|
||||
Map<String, Integer> categories = new TreeMap<>();
|
||||
Map<String, Integer> types = new TreeMap<>();
|
||||
for (Entity entity : chunk.getEntities()) {
|
||||
if (entity instanceof LivingEntity && !(entity instanceof Player)) {
|
||||
categories.merge(BukkitEntityType.of(entity.getType()).spawnCategory(), 1, Integer::sum);
|
||||
types.merge(entity.getType().name(), 1, Integer::sum);
|
||||
}
|
||||
}
|
||||
StringBuilder diagnostics = new StringBuilder("SPAWN_QA_DIAGNOSTICS frogs=")
|
||||
.append(count(chunk, EntityType.FROG)).append(" zombies=").append(count(chunk, EntityType.ZOMBIE))
|
||||
.append(" categories=").append(categories).append(" types=").append(types)
|
||||
.append(" loadLevel=").append(chunk.getLoadLevel())
|
||||
.append(" spawnMobs=").append(world.getGameRuleValue(GameRules.SPAWN_MOBS))
|
||||
.append(" animals=").append(world.getAllowAnimals()).append(" monsters=").append(world.getAllowMonsters())
|
||||
.append(" floor=").append(floor).append(" top=").append(top)
|
||||
.append(" support=").append(world.getBlockAt(8, top, 8).getType())
|
||||
.append(" above=").append(world.getBlockAt(8, top + 1, 8).getType())
|
||||
.append(" light=").append(world.getBlockAt(8, top + 1, 8).getLightLevel())
|
||||
.append(" events=").append(ambientEvents).append(" cancelled=").append(cancelledAmbientEvents);
|
||||
for (String key : spawn.environment().biome().getEntitySpawners()) {
|
||||
IrisSpawner spawner = spawn.environment().data().getSpawnerLoader().load(key);
|
||||
if (spawner == null) {
|
||||
diagnostics.append(" spawner=").append(key).append(":missing");
|
||||
continue;
|
||||
}
|
||||
diagnostics.append(" spawner=").append(key).append(" canSpawn=")
|
||||
.append(spawner.canSpawn(spawn.engine(), chunk.getX(), chunk.getZ()))
|
||||
.append(" excluded=").append(spawner.isCompatExcluded())
|
||||
.append(" group=").append(spawner.getGroup()).append(" capacity=").append(spawner.getMaxEntitiesPerChunk());
|
||||
for (IrisEntitySpawn entry : spawner.getSpawns()) {
|
||||
IrisEntity entity = entry.getRealEntity(spawn.engine());
|
||||
diagnostics.append(" entity=").append(entry.getEntity());
|
||||
if (entity == null) {
|
||||
diagnostics.append(":missing");
|
||||
continue;
|
||||
}
|
||||
diagnostics.append(" type=").append(entity.getBukkitType()).append(" category=").append(entity.spawnCategory())
|
||||
.append(" surface=").append(entity.getSurface())
|
||||
.append(" supportMatches=").append(entity.getSurface().matches(world.getBlockAt(8, top, 8)))
|
||||
.append(" bounds=").append(BukkitPlatform.entityBoundingBox(entity.getBukkitType()));
|
||||
}
|
||||
}
|
||||
return diagnostics.toString();
|
||||
}
|
||||
|
||||
private void invokeAmbient(RuntimeSpawn spawn, Chunk chunk) {
|
||||
ambientInvocation.set(true);
|
||||
try (BiomeEnvironment.Scope ignored = spawn.engine().openBiomeEnvironmentScope(spawn.environment())) {
|
||||
spawn.method().invoke(spawn.spawner(), chunk, false, spawn.environment());
|
||||
} catch (InvocationTargetException failure) {
|
||||
throw new IllegalStateException("Iris ambient spawning failed", failure.getCause());
|
||||
} catch (ReflectiveOperationException failure) {
|
||||
throw new IllegalStateException("Could not invoke Iris ambient spawning", failure);
|
||||
} finally {
|
||||
ambientInvocation.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private int count(Chunk chunk, EntityType type) {
|
||||
int count = 0;
|
||||
for (Entity entity : chunk.getEntities()) {
|
||||
if (entity.getType() == type) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private void verifyPersistence(Chunk chunk) {
|
||||
for (Entity entity : chunk.getEntities()) {
|
||||
if (entity.getType() == EntityType.FROG || entity.getType() == EntityType.ZOMBIE) {
|
||||
require(entity.isPersistent(), "Ambient entity stopped participating in chunk saves");
|
||||
require(entity instanceof LivingEntity living && living.getRemoveWhenFarAway(),
|
||||
"Ambient entity has distance despawning disabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void view(CommandSender sender, double y) {
|
||||
teleport(sender, requireWorld(), y, "SPAWN_QA_VIEW y=" + (int) y);
|
||||
}
|
||||
|
||||
private void teleport(CommandSender sender, World world, double y, String message) {
|
||||
if (!(sender instanceof Player player)) {
|
||||
throw new IllegalArgumentException("Viewing requires a player");
|
||||
}
|
||||
player.getScheduler().run(this, task -> {
|
||||
player.setGameMode(GameMode.CREATIVE);
|
||||
player.setAllowFlight(true);
|
||||
player.setFlying(true);
|
||||
player.teleportAsync(new Location(world, 40.5D, y, 8.5D)).whenComplete((success, failure) -> {
|
||||
if (failure != null || !Boolean.TRUE.equals(success)) {
|
||||
fail(sender, failure == null ? new IllegalStateException("Fixture teleport failed") : failure);
|
||||
} else {
|
||||
notifySender(sender, message);
|
||||
}
|
||||
});
|
||||
}, null);
|
||||
}
|
||||
|
||||
private World requireWorld() {
|
||||
World world = Bukkit.getWorld(WORLD_KEY);
|
||||
require(ready && world != null, "Run setup before a spawning phase");
|
||||
return world;
|
||||
}
|
||||
|
||||
private void runRegion(CommandSender sender, Runnable operation) {
|
||||
World world = requireWorld();
|
||||
Bukkit.getRegionScheduler().execute(this, world, 0, 0, () -> {
|
||||
try {
|
||||
operation.run();
|
||||
} catch (Throwable failure) {
|
||||
fail(sender, failure);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void evidence(CommandSender sender, String message) {
|
||||
getLogger().info(message);
|
||||
notifySender(sender, message);
|
||||
}
|
||||
|
||||
private void notifySender(CommandSender sender, String message) {
|
||||
if (sender instanceof Player player) {
|
||||
player.getScheduler().run(this, task -> player.sendMessage(message), null);
|
||||
} else {
|
||||
sender.sendMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void fail(CommandSender sender, Throwable failure) {
|
||||
getLogger().log(Level.SEVERE, "Spawning acceptance failed", failure);
|
||||
notifySender(sender, "SPAWN_QA_FAILED " + failure.getMessage());
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) {
|
||||
throw new IllegalStateException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private record RuntimeSpawn(Engine engine, Object spawner, Method method, BiomeEnvironment environment) {
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "Cave Zombies",
|
||||
"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/cave-zombies"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "Frogs",
|
||||
"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/frogs"
|
||||
]
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "Spawn QA",
|
||||
"version": 1,
|
||||
"mode": {
|
||||
"type": "OVERWORLD"
|
||||
},
|
||||
"regions": [
|
||||
"flat"
|
||||
],
|
||||
"focusRegion": "flat",
|
||||
"focus": "frogs",
|
||||
"landChance": 1,
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": {
|
||||
"min": -64,
|
||||
"max": 320
|
||||
},
|
||||
"logicalHeight": 384,
|
||||
"fluidHeight": 0,
|
||||
"bedrock": false,
|
||||
"rockPalette": {
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:stone"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"type": "minecraft:frog",
|
||||
"reason": "NATURAL",
|
||||
"surface": "LAND",
|
||||
"ai": false,
|
||||
"aware": false,
|
||||
"gravity": false
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"reason": "NATURAL",
|
||||
"surface": "LAND",
|
||||
"ai": false,
|
||||
"aware": false,
|
||||
"gravity": false
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"interpolator": {
|
||||
"function": "NONE",
|
||||
"horizontalScale": 1
|
||||
},
|
||||
"seed": 310,
|
||||
"composite": [
|
||||
{
|
||||
"seed": 310,
|
||||
"style": {
|
||||
"style": "FLAT"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Flat",
|
||||
"landBiomes": [
|
||||
"frogs"
|
||||
],
|
||||
"seaBiomes": [
|
||||
"frogs"
|
||||
],
|
||||
"shoreBiomes": [
|
||||
"frogs"
|
||||
]
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"group": "CAVE",
|
||||
"maxEntitiesPerChunk": 2,
|
||||
"allowedLightLevels": {
|
||||
"min": 0,
|
||||
"max": 0
|
||||
},
|
||||
"spawns": [
|
||||
{
|
||||
"entity": "qa/zombie",
|
||||
"rarity": 1,
|
||||
"minSpawns": 7,
|
||||
"maxSpawns": 7
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"group": "NORMAL",
|
||||
"maxEntitiesPerChunk": 16,
|
||||
"spawns": [
|
||||
{
|
||||
"entity": "qa/frog",
|
||||
"rarity": 1,
|
||||
"minSpawns": 7,
|
||||
"maxSpawns": 7
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
export default {
|
||||
name: 'iris-natural-spawning',
|
||||
description: 'Observe category-limited Iris ambient frogs and cave zombies on Folia.',
|
||||
async run(context) {
|
||||
const wait = (predicate, label, timeoutMs = 30000) => context.waitUntil(predicate, { label, timeoutMs, intervalMs: 100 })
|
||||
const command = async (operation, expected, timeoutMs = 30000) => {
|
||||
const response = await context.command(`/irisspawnqa ${operation}`, /SPAWN_QA_[A-Z]+(?:\s|$)/, timeoutMs)
|
||||
context.expect(response.includes(expected) && !response.includes('SPAWN_QA_FAILED'), response)
|
||||
return response
|
||||
}
|
||||
const entities = name => Object.values(context.bot.entities).filter(entity => entity.name === name)
|
||||
await context.step('prepare the synthetic Iris world and dark cave', async () => {
|
||||
await command('setup', 'SPAWN_QA_SETUP world=iris:spawn_qa seed=78264193', 180000)
|
||||
await wait(() => Math.abs(context.bot.entity.position.y - 67) < 1, 'Fixture world teleport')
|
||||
})
|
||||
await context.step('fill the passive category through real Iris ambient spawning', async () => {
|
||||
await command('frogs', 'SPAWN_QA_FROGS frogs=16 persistent=true removable=true ambient=true', 60000)
|
||||
await wait(() => entities('frog').length === 16, 'All sixteen ambient frogs became visible')
|
||||
context.report.irisNaturalSpawning = { visibleFrogs: entities('frog').length }
|
||||
})
|
||||
await context.step('spawn an authored seven-zombie batch with capacity two despite existing frogs', async () => {
|
||||
await command('zombies', 'SPAWN_QA_ZOMBIES frogs=16 zombies=2 firstBatch=2 ambient=true', 60000)
|
||||
await command('cave', 'SPAWN_QA_VIEW y=-46')
|
||||
await wait(() => Math.abs(context.bot.entity.position.y + 46) < 1, 'Underground fixture teleport')
|
||||
await wait(() => context.bot.blockAt(context.bot.entity.position.clone().set(8, -47, 8))?.name === 'cave_air', 'Live cave_air block reached the client')
|
||||
await wait(() => entities('zombie').length === 2, 'Both ambient cave zombies became visible')
|
||||
context.expect(entities('zombie').every(entity => entity.position.y < -42), 'Zombies were not inside the underground chamber')
|
||||
context.report.irisNaturalSpawning.visibleZombies = entities('zombie').length
|
||||
context.report.irisNaturalSpawning.caveAir = true
|
||||
})
|
||||
await context.step('repeat fifty ambient attempts without exceeding the hostile capacity', async () => {
|
||||
await command('stress', 'SPAWN_QA_STRESS attempts=50 zombies=2', 60000)
|
||||
await command('check', 'SPAWN_QA_RESULT frogs=16 zombies=2 firstBatch=2 persistent=true removable=true caveAir=true light=0', 30000)
|
||||
context.expect(entities('zombie').length === 2, 'Client observed a hostile capacity overflow')
|
||||
context.report.irisNaturalSpawning.saturatedAttempts = 50
|
||||
context.report.irisNaturalSpawning.firstHostileBatch = 2
|
||||
context.report.irisNaturalSpawning.removeWhenFarAwayAllowed = true
|
||||
context.report.irisNaturalSpawning.chunkSaving = true
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFile, writeFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
const chunks = [[-64, -64], [64, -64], [-64, 64], [64, 64]]
|
||||
|
||||
export default {
|
||||
name: 'iris-seed-cohesion',
|
||||
description: 'Compare client-visible decorated chunks in two same-seed worlds explored in opposite orders.',
|
||||
async run(context) {
|
||||
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
|
||||
context.expect(/^isolated=true\s*$/m.test(source), 'Seed cohesion requires an isolated instance')
|
||||
const wait = (predicate, label) => context.waitUntil(predicate, { label, timeoutMs: 60000, intervalMs: 100 })
|
||||
const currentWorld = async () => {
|
||||
const observation = await context.observe()
|
||||
const player = observation.players.find(entry => entry.username === context.bot.username)
|
||||
return observation.worlds.find(entry => entry.id === player?.world)
|
||||
}
|
||||
const enter = async name => {
|
||||
await context.command(`/iris tp ${name}`)
|
||||
await wait(async () => (await currentWorld())?.key === `iris:${name}`
|
||||
|| (await currentWorld())?.name.endsWith(name), `Enter ${name}`)
|
||||
await context.command('/minecraft:gamemode spectator @s')
|
||||
await wait(() => context.bot.game.gameMode === 'spectator', 'Spectator mode')
|
||||
await context.command('/minecraft:seed', /Seed:.*1337/)
|
||||
}
|
||||
const capture = async coordinates => {
|
||||
const hashes = {}
|
||||
const decorators = {}
|
||||
let decorationCount = 0
|
||||
for (const [cx, cz] of coordinates) {
|
||||
const x = cx * 16
|
||||
const z = cz * 16
|
||||
await context.command(`/minecraft:tp @s ${x + 8.5} 48 ${z + 8.5}`, /Teleported/i, 30000)
|
||||
await wait(() => Math.abs(context.bot.entity.position.x - x - 8.5) < 1
|
||||
&& Math.abs(context.bot.entity.position.z - z - 8.5) < 1, 'Chunk sample teleport')
|
||||
const position = context.bot.entity.position.clone()
|
||||
await wait(() => context.bot.blockAt(position.clone().set(x, 1, z))
|
||||
&& context.bot.blockAt(position.clone().set(x + 15, 63, z + 15)), 'Complete sample chunk')
|
||||
const digest = createHash('sha256')
|
||||
let decoration = 0
|
||||
let terrain = 0
|
||||
for (let dx = 0; dx < 16; dx++) {
|
||||
for (let dz = 0; dz < 16; dz++) {
|
||||
for (let y = 0; y < 64; y++) {
|
||||
const block = context.bot.blockAt(position.set(x + dx, y, z + dz))
|
||||
context.expect(block !== null, 'Missing sampled block', { x: x + dx, y, z: z + dz })
|
||||
digest.update(`${block.stateId},`)
|
||||
if (block.name === 'granite' || block.name === 'andesite') decoration++
|
||||
if (block.name === 'stone') terrain++
|
||||
}
|
||||
}
|
||||
}
|
||||
context.expect(terrain > 1000, 'Fixture terrain was not generated', { cx, cz, terrain })
|
||||
decorationCount += decoration
|
||||
decorators[`${cx},${cz}`] = decoration
|
||||
hashes[`${cx},${cz}`] = digest.digest('hex')
|
||||
}
|
||||
context.expect(decorationCount > 0, 'Fixture decorators were not generated in sampled chunks', { decorators })
|
||||
context.report.decoratorCoverage ??= []
|
||||
context.report.decoratorCoverage.push(decorators)
|
||||
return hashes
|
||||
}
|
||||
const baseline = 'seed-cohesion-forward'
|
||||
const reverse = 'seed-cohesion-reverse'
|
||||
const stateFile = path.join(context.server.directory, '.seed-cohesion.json')
|
||||
const jarHash = createHash('sha256').update(await readFile(path.join(context.server.directory, 'plugins', 'Iris.jar'))).digest('hex')
|
||||
const restart = context.options.command === 'restart-check'
|
||||
const saved = restart ? JSON.parse(await readFile(stateFile, 'utf8')) : undefined
|
||||
if (restart) {
|
||||
context.expect(jarHash === saved.jarHash, 'Restart-check requires the same Iris jar', { before: saved.jarHash, after: jarHash })
|
||||
context.expect((await context.observe()).processId !== saved.processId, 'Restart-check requires a cold server restart')
|
||||
}
|
||||
const outputs = []
|
||||
for (const [name, coordinates] of [[baseline, chunks], [reverse, [...chunks].reverse()]]) {
|
||||
await context.step(`${restart ? 'Reload' : 'Create'} and sample ${name}`, async () => {
|
||||
if (!restart) {
|
||||
await context.command(`/iris create name=${name} type=cohesion seed=1337`, /Successfully created your world/i, 180000)
|
||||
}
|
||||
await enter(name)
|
||||
const output = await capture(coordinates)
|
||||
if (restart) {
|
||||
for (const [coordinate, hash] of Object.entries(output)) {
|
||||
context.expect(saved.hashes[coordinate] === hash, 'Persisted chunk changed after restart', { name, coordinate, before: saved.hashes[coordinate], after: hash })
|
||||
}
|
||||
Object.assign(output, await capture(coordinates.map(([cx, cz]) => [cx * 3, cz * 3])))
|
||||
}
|
||||
outputs.push(output)
|
||||
})
|
||||
}
|
||||
await context.step('Compare all sampled blocks', async () => {
|
||||
for (const [coordinate, hash] of Object.entries(outputs[0])) {
|
||||
context.expect(outputs[1][coordinate] === hash, 'Same-seed worlds differ',
|
||||
{ coordinate, forward: hash, reverse: outputs[1][coordinate] })
|
||||
}
|
||||
context.report.seedCohesion = { seed: 1337, jarHash, blocksPerChunk: 16384, restart, hashes: outputs[0] }
|
||||
if (!restart) {
|
||||
await writeFile(stateFile, JSON.stringify({ processId: (await context.observe()).processId, jarHash, hashes: outputs[0] }))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class GenerationRateWindowTest {
|
||||
@Test
|
||||
public void generationTimingsGoStaleOnceChunksStopGenerating() {
|
||||
GenerationRateWindow window = new GenerationRateWindow();
|
||||
|
||||
window.sampleChunksPerSecond(0, 1_000L);
|
||||
assertEquals(20D, window.sampleChunksPerSecond(20, 2_000L), 0D);
|
||||
assertTrue(window.generationTimingsFresh(2_000L));
|
||||
|
||||
assertEquals(0D, window.sampleChunksPerSecond(20, 7_000L), 0D);
|
||||
assertTrue(window.generationTimingsFresh(7_000L));
|
||||
assertEquals(0D, window.sampleChunksPerSecond(20, 12_001L), 0D);
|
||||
assertFalse(window.generationTimingsFresh(12_001L));
|
||||
|
||||
window.sampleChunksPerSecond(21, 13_001L);
|
||||
assertTrue(window.generationTimingsFresh(13_001L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anIdleWorldNeverPublishesTimings() {
|
||||
GenerationRateWindow window = new GenerationRateWindow();
|
||||
|
||||
window.sampleChunksPerSecond(0, 1_000L);
|
||||
window.sampleChunksPerSecond(0, 2_000L);
|
||||
|
||||
assertFalse(window.generationTimingsFresh(2_000L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chunksGeneratedBeforeTheFirstSampleCountAsRecentGeneration() {
|
||||
GenerationRateWindow window = new GenerationRateWindow();
|
||||
|
||||
assertEquals(0D, window.sampleChunksPerSecond(400, 5_000L), 0D);
|
||||
|
||||
assertTrue(window.generationTimingsFresh(5_000L));
|
||||
assertFalse(window.generationTimingsFresh(5_000L + GenerationRateWindow.TIMING_IDLE_MILLIS + 1L));
|
||||
}
|
||||
}
|
||||
+8
@@ -49,6 +49,7 @@ public class IrisIntegrationServiceTest {
|
||||
100L,
|
||||
200L,
|
||||
100L,
|
||||
48L,
|
||||
8D,
|
||||
2_000L,
|
||||
5_000L,
|
||||
@@ -64,6 +65,8 @@ public class IrisIntegrationServiceTest {
|
||||
IntegrationMetricSchema.IRIS_LOADED_CHUNKS,
|
||||
IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_TOTAL,
|
||||
IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT,
|
||||
IntegrationMetricSchema.IRIS_PREGEN_QUEUE,
|
||||
IntegrationMetricSchema.IRIS_PREGEN_REMAINING,
|
||||
IntegrationMetricSchema.IRIS_GENERATION_TOTAL_MS,
|
||||
"iris.unsupported"
|
||||
));
|
||||
@@ -73,6 +76,8 @@ public class IrisIntegrationServiceTest {
|
||||
assertEquals(20D, value(samples, IntegrationMetricSchema.IRIS_LOADED_CHUNKS), 0D);
|
||||
assertEquals(50D, value(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_TOTAL), 0D);
|
||||
assertEquals(8D, value(samples, IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT), 0D);
|
||||
assertEquals(48D, value(samples, IntegrationMetricSchema.IRIS_PREGEN_QUEUE), 0D);
|
||||
assertEquals(100D, value(samples, IntegrationMetricSchema.IRIS_PREGEN_REMAINING), 0D);
|
||||
assertEquals(4D, value(samples, IntegrationMetricSchema.IRIS_GENERATION_TOTAL_MS), 0D);
|
||||
assertFalse(samples.get("iris.unsupported").available());
|
||||
assertEquals("unsupported-key", samples.get("iris.unsupported").message());
|
||||
@@ -84,9 +89,12 @@ public class IrisIntegrationServiceTest {
|
||||
assertEquals("minecraft:overworld", overworld.scopeId());
|
||||
assertEquals(1D, value(overworld.samples(), IntegrationMetricSchema.IRIS_PREGEN_ACTIVE), 0D);
|
||||
assertEquals(8D, value(overworld.samples(), IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT), 0D);
|
||||
assertEquals(48D, value(overworld.samples(), IntegrationMetricSchema.IRIS_PREGEN_QUEUE), 0D);
|
||||
assertEquals(100D, value(overworld.samples(), IntegrationMetricSchema.IRIS_PREGEN_REMAINING), 0D);
|
||||
assertEquals("minecraft:the_nether", nether.scopeId());
|
||||
assertEquals(0D, value(nether.samples(), IntegrationMetricSchema.IRIS_PREGEN_ACTIVE), 0D);
|
||||
assertFalse(nether.samples().get(IntegrationMetricSchema.IRIS_PREGEN_QUEUE).available());
|
||||
assertFalse(nether.samples().get(IntegrationMetricSchema.IRIS_PREGEN_REMAINING).available());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -56,7 +56,7 @@ apply from: file('../../gradle/volmlib-stamp.gradle')
|
||||
|
||||
Properties rootProperties = new Properties()
|
||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.1.2-26.2'))
|
||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.0-26.2'))
|
||||
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
|
||||
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(rootProperties.getProperty('fabricLoaderVersion', '0.19.5'))
|
||||
Closure<String> irisArtifactName = { String platform, String targetVersion ->
|
||||
|
||||
@@ -56,7 +56,7 @@ apply from: file('../../gradle/volmlib-stamp.gradle')
|
||||
|
||||
Properties rootProperties = new Properties()
|
||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.1.2-26.2'))
|
||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.0-26.2'))
|
||||
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
|
||||
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(rootProperties.getProperty('forgeVersion', '26.3-66.0.8'))
|
||||
Closure<String> loaderDisplayVersion = { String loaderVersion ->
|
||||
|
||||
+9
@@ -242,6 +242,7 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
|
||||
this.announced.set(false);
|
||||
resetRuntimeCaches();
|
||||
this.engineBinding.complete(replacement);
|
||||
this.nativeGenerator.initializeStructureState(level);
|
||||
// Bind time: a feature-order cycle in the new pack is reported here, once, and degrades to features-off.
|
||||
// Never waits on a build owned by another thread: this method owns the generator monitor and the build
|
||||
// path can need it.
|
||||
@@ -442,12 +443,14 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
|
||||
}
|
||||
Engine current = engineIfBound();
|
||||
if (NativeDimensionRuntime.sameWorld(boundLevel, level) && current != null && current.getComplex() != null) {
|
||||
nativeGenerator.initializeStructureState(level);
|
||||
return;
|
||||
}
|
||||
requireCompletedShutdown(engine);
|
||||
unloading = false;
|
||||
Engine bound = bindEngine(level);
|
||||
nativeGenerator.installVolumeIndex(level, bound);
|
||||
nativeGenerator.initializeStructureState(level);
|
||||
// Bind time: a feature-order cycle is reported here, once, and degrades to features-off. Non-waiting for
|
||||
// the same reason as repointAndBind: this method owns the generator monitor.
|
||||
importedFeatures.prepareWithoutWaiting(bound);
|
||||
@@ -1097,6 +1100,12 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
|
||||
return current.getDimensionStackContext() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean terrainTransformed() {
|
||||
Engine current = engine;
|
||||
return current != null && current.getComplex().getTerrainTransform() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int runtimeId(Engine current) {
|
||||
return current.getCacheID();
|
||||
|
||||
+6
-6
@@ -61,11 +61,12 @@ public final class ModdedEntitySpawner {
|
||||
}
|
||||
|
||||
double x = blockX + 0.5;
|
||||
double y = blockY + 0.5;
|
||||
double verticalOffset = irisEntity.getSurface().isFluid() ? 0.5 : 0;
|
||||
double y = blockY + verticalOffset;
|
||||
double z = blockZ + 0.5;
|
||||
boolean riseEffectActive = irisEntity.isSpawnEffectRiseOutOfGround() && level.hasPlayersNearby(new NativeEntityRuntime.Position(x, y, z), PLAYER_EFFECT_RADIUS);
|
||||
int spawnBlockY = riseEffectActive ? blockY - 5 : blockY;
|
||||
double spawnY = spawnBlockY + 0.5;
|
||||
double spawnY = spawnBlockY + verticalOffset;
|
||||
|
||||
NativeSpawnedEntity created = create(irisEntity, level, x, spawnY, z);
|
||||
if (created == null) {
|
||||
@@ -98,8 +99,7 @@ public final class ModdedEntitySpawner {
|
||||
String customName = irisEntity.getCustomName();
|
||||
entity.applyBase(irisEntity, customName == null ? null : colorize(customName));
|
||||
|
||||
boolean persistent = irisEntity.isKeepEntity() || forcePersist();
|
||||
applyPersistence(entity, persistent);
|
||||
applyPersistence(irisEntity, entity, forcePersist());
|
||||
|
||||
applyPassengers(engine, irisEntity, entity, level, blockX, blockY, blockZ, rng);
|
||||
|
||||
@@ -273,8 +273,8 @@ public final class ModdedEntitySpawner {
|
||||
return IrisSettings.get().getWorld().isForcePersistEntities();
|
||||
}
|
||||
|
||||
private static void applyPersistence(NativeSpawnedEntity entity, boolean persistent) {
|
||||
if (!entity.persistence(persistent)) {
|
||||
static void applyPersistence(IrisEntity options, NativeSpawnedEntity entity, boolean forcePersistence) {
|
||||
if (!entity.persistence(options.isKeepEntity() || forcePersistence)) {
|
||||
String type = entity.typeKey();
|
||||
if (WARNED_PERSISTENCE_TYPES.add(type)) {
|
||||
IrisLogging.warn("Iris entity: vanilla cannot persist non-serializable entity type '" + type + "'");
|
||||
|
||||
+19
-14
@@ -55,6 +55,7 @@ import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
import art.arcane.volmlib.util.matter.slices.MarkerMatter;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
@@ -511,13 +512,16 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
}
|
||||
|
||||
private void spawnAmbient(NativeWorld level, int chunkX, int chunkZ, boolean initial, BiomeEnvironment environment) {
|
||||
if (!NativeSpawnQueries.ambientAllowed(level, chunkX, chunkZ, initial)) {
|
||||
return;
|
||||
}
|
||||
IrisComplex complex = engine.getComplex();
|
||||
if (complex == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
IrisBiome biome = environment.biome();
|
||||
int chunkMobs = countChunkLivingEntities(level, chunkX, chunkZ);
|
||||
Map<String, Integer> chunkMobs = NativeSpawnQueries.livingEntityCategories(level, chunkX, chunkZ);
|
||||
|
||||
KList<IrisEntitySpawn> pool = new KList<>();
|
||||
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.dimension().getEntitySpawners()), biome, chunkX, chunkZ, chunkMobs, initial);
|
||||
@@ -535,20 +539,18 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
if (!canSpawn(spawner, chunkX, chunkZ)) {
|
||||
return;
|
||||
}
|
||||
int spawned = spawnEntry(level, chosen, spawner, chunkX, chunkZ);
|
||||
int spawned = spawnEntry(level, chosen, spawner, chunkX, chunkZ,
|
||||
remainingCapacity(chosen, spawner, chunkMobs));
|
||||
if (spawned > 0) {
|
||||
spawner.spawn(engine, chunkX, chunkZ);
|
||||
}
|
||||
}
|
||||
|
||||
private void collectSpawns(KList<IrisEntitySpawn> pool, KList<IrisSpawner> spawners, IrisBiome biomeFilter, int chunkX, int chunkZ, int chunkMobs, boolean initial) {
|
||||
private void collectSpawns(KList<IrisEntitySpawn> pool, KList<IrisSpawner> spawners, IrisBiome biomeFilter, int chunkX, int chunkZ, Map<String, Integer> chunkMobs, boolean initial) {
|
||||
for (IrisSpawner spawner : spawners) {
|
||||
if (spawner == null) {
|
||||
continue;
|
||||
}
|
||||
if (spawner.getMaxEntitiesPerChunk() <= chunkMobs) {
|
||||
continue;
|
||||
}
|
||||
if (biomeFilter != null && !spawner.isValid(biomeFilter)) {
|
||||
continue;
|
||||
}
|
||||
@@ -559,12 +561,19 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
for (IrisEntitySpawn entry : spawns) {
|
||||
entry.setReferenceSpawner(spawner);
|
||||
entry.setReferenceMarker(spawner.getReferenceMarker());
|
||||
pool.add(entry);
|
||||
if (remainingCapacity(entry, spawner, chunkMobs) > 0) {
|
||||
pool.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int spawnEntry(NativeWorld level, IrisEntitySpawn entry, IrisSpawner spawner, int chunkX, int chunkZ) {
|
||||
private int remainingCapacity(IrisEntitySpawn entry, IrisSpawner spawner, Map<String, Integer> counts) {
|
||||
IrisEntity entity = entry.getRealEntity(engine);
|
||||
return spawner.remainingCapacity(entity, counts);
|
||||
}
|
||||
|
||||
private int spawnEntry(NativeWorld level, IrisEntitySpawn entry, IrisSpawner spawner, int chunkX, int chunkZ, int remainingCapacity) {
|
||||
IrisEntity irisEntity = entry.getRealEntity(engine);
|
||||
if (irisEntity == null) {
|
||||
return 0;
|
||||
@@ -572,7 +581,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
|
||||
int min = entry.getMinSpawns();
|
||||
int max = entry.getMaxSpawns();
|
||||
int count = LootResolver.inclusive(RNG.r, min, max);
|
||||
int count = Math.min(Math.max(0, remainingCapacity), LootResolver.inclusive(RNG.r, min, max));
|
||||
if (count <= 0) {
|
||||
return 0;
|
||||
}
|
||||
@@ -593,7 +602,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
}
|
||||
IrisPosition caveFloor = caveFloors.getRandom(RNG.r);
|
||||
worldX = caveFloor.getX();
|
||||
worldY = caveFloor.getY() + 1;
|
||||
worldY = caveFloor.getY();
|
||||
worldZ = caveFloor.getZ();
|
||||
} else {
|
||||
worldX = (chunkX << 4) + RNG.r.i(16);
|
||||
@@ -708,10 +717,6 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
|| NativeSpawnQueries.solid(level.getBlock(worldX, worldY + 2, worldZ));
|
||||
}
|
||||
|
||||
private int countChunkLivingEntities(NativeWorld level, int chunkX, int chunkZ) {
|
||||
return NativeSpawnQueries.livingEntities(level, chunkX, chunkZ);
|
||||
}
|
||||
|
||||
/**
|
||||
* ServerChunkCache.tickChunks rebuilds NaturalSpawner.SpawnState every tick from every entity in the
|
||||
* level, so read that instead of walking all entities again on an Iris timer. The count is the natural
|
||||
|
||||
+74
-5
@@ -2,6 +2,13 @@ package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeEntityBehavior;
|
||||
|
||||
import art.arcane.iris.world.entity.IrisEntity;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeSpawnedEntity;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import org.junit.BeforeClass;
|
||||
import net.minecraft.world.entity.Mob;
|
||||
import org.junit.Test;
|
||||
import org.junit.Before;
|
||||
import art.arcane.volmlib.nativelib.modded.EntityBehaviorTags;
|
||||
@@ -11,18 +18,31 @@ import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
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 ModdedEntityPersistenceTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void configureEntityTags() {
|
||||
NativeEntityBehavior.bind(new EntityBehaviorTags("iris_non_persistent", "iris_unaware"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generatedNonPersistentEntityIsExcludedFromVanillaSaves() {
|
||||
public void explicitlyExcludedEntityIsExcludedFromVanillaSaves() {
|
||||
Set<String> tags = new HashSet<>();
|
||||
|
||||
NativeEntityBehavior.configurePersistenceTags(tags, false);
|
||||
NativeEntityBehavior.configureSavingTags(tags, false);
|
||||
|
||||
assertFalse(NativeEntityBehavior.shouldSave(tags, true));
|
||||
}
|
||||
@@ -30,9 +50,9 @@ public class ModdedEntityPersistenceTest {
|
||||
@Test
|
||||
public void positivePersistenceRemovesTheSaveExclusion() {
|
||||
Set<String> tags = new HashSet<>();
|
||||
NativeEntityBehavior.configurePersistenceTags(tags, false);
|
||||
NativeEntityBehavior.configureSavingTags(tags, false);
|
||||
|
||||
NativeEntityBehavior.configurePersistenceTags(tags, true);
|
||||
NativeEntityBehavior.configureSavingTags(tags, true);
|
||||
|
||||
assertTrue(NativeEntityBehavior.shouldSave(tags, true));
|
||||
}
|
||||
@@ -41,8 +61,57 @@ public class ModdedEntityPersistenceTest {
|
||||
public void interceptionNeverOverridesVanillaSaveRejection() {
|
||||
Set<String> tags = new HashSet<>();
|
||||
|
||||
NativeEntityBehavior.configurePersistenceTags(tags, true);
|
||||
NativeEntityBehavior.configureSavingTags(tags, true);
|
||||
|
||||
assertFalse(NativeEntityBehavior.shouldSave(tags, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultAmbientMobSavesWithoutBecomingPermanent() {
|
||||
Mob mob = mob();
|
||||
NativeSpawnedEntity entity = new NativeSpawnedEntity(mob);
|
||||
IrisEntity options = new IrisEntity();
|
||||
|
||||
ModdedEntitySpawner.applyPersistence(options, entity, false);
|
||||
entity.configureMob(options);
|
||||
|
||||
assertTrue(NativeEntityBehavior.shouldSave(mob, true));
|
||||
verify(mob, never()).setPersistenceRequired();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void explicitKeepForceAndRemovalFlagsControlNativeDespawn() {
|
||||
for (boolean keep : new boolean[]{false, true}) {
|
||||
for (boolean force : new boolean[]{false, true}) {
|
||||
for (boolean removable : new boolean[]{false, true}) {
|
||||
Mob mob = mob();
|
||||
NativeSpawnedEntity entity = new NativeSpawnedEntity(mob);
|
||||
IrisEntity options = new IrisEntity().setKeepEntity(keep).setRemovable(removable);
|
||||
|
||||
ModdedEntitySpawner.applyPersistence(options, entity, force);
|
||||
entity.configureMob(options);
|
||||
|
||||
assertTrue(NativeEntityBehavior.shouldSave(mob, true));
|
||||
if (keep || force || !removable) {
|
||||
verify(mob, atLeastOnce()).setPersistenceRequired();
|
||||
} else {
|
||||
verify(mob, never()).setPersistenceRequired();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Mob mob() {
|
||||
Mob mob = mock(Mob.class);
|
||||
EntityType<?> type = mock(EntityType.class);
|
||||
Set<String> tags = new HashSet<>();
|
||||
when(type.canSerialize()).thenReturn(true);
|
||||
doReturn(type).when(mob).getType();
|
||||
when(mob.entityTags()).thenReturn(tags);
|
||||
when(mob.shouldBeSaved()).thenReturn(true);
|
||||
when(mob.removeTag(anyString())).thenAnswer(invocation -> tags.remove(invocation.getArgument(0)));
|
||||
when(mob.addTag(anyString())).thenAnswer(invocation -> tags.add(invocation.getArgument(0)));
|
||||
return mob;
|
||||
}
|
||||
}
|
||||
|
||||
+31
-1
@@ -21,7 +21,7 @@ import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ModdedEntitySpawnerParityTest {
|
||||
@Test
|
||||
public void collisionCheckUsesPaperInclusiveIntegerDimensions() {
|
||||
public void landBodyFitsTwoBlockCaveAndRejectsSecondBlockCeiling() {
|
||||
Set<BlockPos> checked = new HashSet<>();
|
||||
|
||||
boolean clear = NativeEntityRuntime.isAreaClearForSpawn(
|
||||
@@ -32,6 +32,36 @@ public class ModdedEntitySpawnerParityTest {
|
||||
|
||||
assertTrue(clear);
|
||||
assertEquals(Set.of(new BlockPos(10, 64, 20), new BlockPos(10, 65, 20)), checked);
|
||||
assertTrue(NativeEntityRuntime.isAreaClearForSpawn(10, 64, 20, 0.6F, 1.95F,
|
||||
(x, y, z) -> y >= 64 && y < 66));
|
||||
assertFalse(NativeEntityRuntime.isAreaClearForSpawn(10, 64, 20, 0.6F, 1.95F,
|
||||
(x, y, z) -> y == 64));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fractionalWideLandBodiesIncludeAdjacentObstructions() {
|
||||
Set<BlockPos> checked = new HashSet<>();
|
||||
assertTrue(NativeEntityRuntime.isAreaClearForSpawn(-1, -49, -1, 1.3F, 1.95F,
|
||||
(x, y, z) -> {
|
||||
checked.add(new BlockPos(x, y, z));
|
||||
return true;
|
||||
}));
|
||||
assertEquals(18, checked.size());
|
||||
assertTrue(checked.contains(new BlockPos(-2, -48, -2)));
|
||||
assertTrue(checked.contains(new BlockPos(0, -49, 0)));
|
||||
assertFalse(NativeEntityRuntime.isAreaClearForSpawn(-1, -49, -1, 1.3F, 1.95F,
|
||||
(x, y, z) -> !new BlockPos(x, y, z).equals(new BlockPos(0, -48, -1))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exactLandBodyBoundariesExcludeUntouchedBlocks() {
|
||||
Set<BlockPos> checked = new HashSet<>();
|
||||
assertTrue(NativeEntityRuntime.isAreaClearForSpawn(0, -49, 0, 1F, 2F,
|
||||
(x, y, z) -> {
|
||||
checked.add(new BlockPos(x, y, z));
|
||||
return true;
|
||||
}));
|
||||
assertEquals(Set.of(new BlockPos(0, -49, 0), new BlockPos(0, -48, 0)), checked);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -42,6 +42,7 @@ public class ModdedMixinAuditTest {
|
||||
"\\[\\[mixins]]\\s*config\\s*=\\s*\"([^\"]+)\"");
|
||||
// One applied mixin per config proves the config was registered, which is all the audit detects.
|
||||
private static final Set<String> UNAUDITED = Set.of(
|
||||
"volmlib.entity.mixins.json/ServerLevelStructureBootstrapMixin",
|
||||
"volmlib.entity.mixins.json/WorldGenerationWriteGuardMixin",
|
||||
"volmlib.entity.mixins.json/ChunkMapTerrainReceiptAccess",
|
||||
"volmlib.entity.mixins.json/ChunkTerrainReceiptMixin",
|
||||
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.EnginePlatformHooks;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedChunkGenerator;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ModdedStructureBindingOrderTest {
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initialBindingPublishesReadyEngineBeforeStartingStructureSearches() throws Exception {
|
||||
NativeModdedChunkGenerator<?, ?, ?> nativeGenerator = mock(NativeModdedChunkGenerator.class);
|
||||
ModdedEngineBinding<Engine> binding = new ModdedEngineBinding<>(1L, TimeUnit.MILLISECONDS);
|
||||
IrisModdedChunkGenerator generator = generator(nativeGenerator, binding);
|
||||
NativeWorld world = mock(NativeWorld.class);
|
||||
NativeModdedServer server = mock(NativeModdedServer.class);
|
||||
Engine engine = readyEngine();
|
||||
when(nativeGenerator.represents(world)).thenReturn(true);
|
||||
when(server.generateStructures()).thenReturn(true);
|
||||
doAnswer(invocation -> {
|
||||
assertSame(engine, generator.awaitStructureEngine());
|
||||
assertSame(engine, binding.await("overworld"));
|
||||
return null;
|
||||
}).when(nativeGenerator).initializeStructureState(world);
|
||||
|
||||
try (MockedStatic<NativeModdedServer> servers = mockStatic(NativeModdedServer.class);
|
||||
MockedStatic<ModdedWorldEngines> engines = mockStatic(ModdedWorldEngines.class);
|
||||
MockedStatic<ModdedIrisLog> log = mockStatic(ModdedIrisLog.class)) {
|
||||
servers.when(() -> NativeModdedServer.forWorld(world)).thenReturn(server);
|
||||
engines.when(() -> ModdedWorldEngines.get(eq(world), any(), any(), anyLong(), any())).thenReturn(engine);
|
||||
|
||||
generator.bindLevel(world);
|
||||
|
||||
verify(nativeGenerator).initializeStructureState(world);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void levelBindingStartsDeferredStructuresAfterAnEarlierDataQueryBoundTheEngine() throws Exception {
|
||||
NativeModdedChunkGenerator<?, ?, ?> nativeGenerator = mock(NativeModdedChunkGenerator.class);
|
||||
ModdedEngineBinding<Engine> binding = new ModdedEngineBinding<>(1L, TimeUnit.MILLISECONDS);
|
||||
IrisModdedChunkGenerator generator = generator(nativeGenerator, binding);
|
||||
NativeWorld world = mock(NativeWorld.class);
|
||||
Engine engine = readyEngine();
|
||||
when(nativeGenerator.represents(world)).thenReturn(true);
|
||||
when(world.nativeHandle()).thenReturn(new Object());
|
||||
field("engine").set(generator, engine);
|
||||
field("boundLevel").set(generator, world);
|
||||
binding.complete(engine);
|
||||
doAnswer(invocation -> {
|
||||
assertSame(engine, generator.awaitStructureEngine());
|
||||
assertSame(engine, binding.await("overworld"));
|
||||
return null;
|
||||
}).when(nativeGenerator).initializeStructureState(world);
|
||||
|
||||
generator.bindLevel(world);
|
||||
|
||||
verify(nativeGenerator).initializeStructureState(world);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void repointPublishesReplacementBindingBeforeStartingStructureSearches() throws Exception {
|
||||
NativeModdedChunkGenerator<?, ?, ?> nativeGenerator = mock(NativeModdedChunkGenerator.class);
|
||||
ModdedEngineBinding<Engine> binding = new ModdedEngineBinding<>(1L, TimeUnit.MILLISECONDS);
|
||||
IrisModdedChunkGenerator generator = generator(nativeGenerator, binding);
|
||||
NativeWorld world = mock(NativeWorld.class);
|
||||
NativeModdedServer server = mock(NativeModdedServer.class);
|
||||
Engine replacement = readyEngine();
|
||||
when(nativeGenerator.represents(world)).thenReturn(true);
|
||||
when(server.generateStructures()).thenReturn(true);
|
||||
doAnswer(invocation -> {
|
||||
assertSame(replacement, generator.awaitStructureEngine());
|
||||
assertSame(replacement, binding.await("overworld"));
|
||||
return null;
|
||||
}).when(nativeGenerator).initializeStructureState(world);
|
||||
|
||||
try (MockedStatic<NativeModdedServer> servers = mockStatic(NativeModdedServer.class);
|
||||
MockedStatic<ModdedWorldEngines> engines = mockStatic(ModdedWorldEngines.class)) {
|
||||
servers.when(() -> NativeModdedServer.forWorld(world)).thenReturn(server);
|
||||
engines.when(() -> ModdedWorldEngines.prepareReplacement(eq(world), eq("overworld"),
|
||||
eq("overworld"), eq(42L), any())).thenReturn(replacement);
|
||||
|
||||
generator.repointAndBind(world, "overworld", "overworld", 42L);
|
||||
|
||||
verify(nativeGenerator).initializeStructureState(world);
|
||||
}
|
||||
}
|
||||
|
||||
private static IrisModdedChunkGenerator generator(NativeModdedChunkGenerator<?, ?, ?> nativeGenerator,
|
||||
ModdedEngineBinding<Engine> binding) throws Exception {
|
||||
IrisModdedChunkGenerator generator = mock(IrisModdedChunkGenerator.class, CALLS_REAL_METHODS);
|
||||
field("nativeGenerator").set(generator, nativeGenerator);
|
||||
field("engineBinding").set(generator, binding);
|
||||
field("dimensionKey").set(generator, "overworld");
|
||||
field("generationMode").set(generator, ModdedGenerationMode.PERSISTENT_RESTORE);
|
||||
field("announced").set(generator, new AtomicBoolean());
|
||||
field("importedFeatures").set(generator, mock(ModdedImportedFeatureStage.class));
|
||||
return generator;
|
||||
}
|
||||
|
||||
private static Engine readyEngine() {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisData data = mock(IrisData.class);
|
||||
when(engine.getComplex()).thenReturn(mock(IrisComplex.class));
|
||||
when(engine.getData()).thenReturn(data);
|
||||
when(data.getDataFolder()).thenReturn(new File("build/structure-binding-test/pack"));
|
||||
when(engine.getPlatformHooks()).thenReturn(mock(EnginePlatformHooks.class));
|
||||
return engine;
|
||||
}
|
||||
|
||||
private static Field field(String name) throws NoSuchFieldException {
|
||||
Field field = IrisModdedChunkGenerator.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
return field;
|
||||
}
|
||||
}
|
||||
+97
-1
@@ -3,12 +3,21 @@ package art.arcane.iris.modded;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
|
||||
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeEntityRuntime;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeSpawnedEntity;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||
import art.arcane.iris.generation.mantle.EngineMantle;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.SeedManager;
|
||||
import art.arcane.iris.pack.value.IrisPosition;
|
||||
|
||||
import art.arcane.iris.world.entity.IrisEntity;
|
||||
import art.arcane.iris.world.entity.IrisEntitySpawn;
|
||||
import art.arcane.iris.world.entity.IrisSpawnGroup;
|
||||
import art.arcane.iris.world.entity.IrisSpawner;
|
||||
import art.arcane.iris.generation.decoration.IrisSurface;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.matter.slices.MarkerMatter;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
@@ -23,7 +32,9 @@ import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -33,8 +44,15 @@ import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertSame;
|
||||
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.mockStatic;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ModdedWaterEntitySpawnTest {
|
||||
@@ -48,7 +66,8 @@ public class ModdedWaterEntitySpawnTest {
|
||||
}
|
||||
}
|
||||
BuiltInRegistries.BLOCK.prepareTagReload(new TagLoader.LoadResult<>(Registries.BLOCK, Map.of(
|
||||
BlockTags.BLOCKS_MOTION, List.of(Blocks.OAK_FENCE.builtInRegistryHolder())))).apply();
|
||||
BlockTags.BLOCKS_MOTION, List.of(Blocks.OAK_FENCE.builtInRegistryHolder(),
|
||||
Blocks.STONE.builtInRegistryHolder())))).apply();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -75,6 +94,22 @@ public class ModdedWaterEntitySpawnTest {
|
||||
assertFalse(ModdedEntitySpawner.isAreaClearForSpawn(new NativeEntityRuntime(level), entity, 0, 78, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativeLandClearanceAcceptsEveryAirBlockAndRejectsStone() {
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
NativeEntityRuntime runtime = new NativeEntityRuntime(level);
|
||||
IrisEntity entity = new IrisEntity().setType("zombie").setSurface(IrisSurface.LAND);
|
||||
for (Block air : List.of(Blocks.AIR, Blocks.CAVE_AIR, Blocks.VOID_AIR)) {
|
||||
when(level.getBlockState(any(BlockPos.class))).thenReturn(air.defaultBlockState());
|
||||
assertTrue(ModdedEntitySpawner.isAreaClearForSpawn(runtime, entity, -1, -49, -1));
|
||||
}
|
||||
when(level.getBlockState(any(BlockPos.class))).thenAnswer(invocation -> {
|
||||
BlockPos position = invocation.getArgument(0);
|
||||
return position.getY() == -48 ? Blocks.STONE.defaultBlockState() : Blocks.CAVE_AIR.defaultBlockState();
|
||||
});
|
||||
assertFalse(ModdedEntitySpawner.isAreaClearForSpawn(runtime, entity, -1, -49, -1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void waterloggedSolidBlocksStillObstructWaterSpawns() {
|
||||
assertTrue(ModdedWorldManager.matchesSurface(IrisSurface.WATER,
|
||||
@@ -107,6 +142,29 @@ public class ModdedWaterEntitySpawnTest {
|
||||
assertEquals(Set.of(new BlockPos(0, 78, 0)), checked);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void spawnAndRiseStartUseFloorHeightOnLandAndCenterHeightInFluids() {
|
||||
for (IrisSurface surface : List.of(IrisSurface.LAND, IrisSurface.WATER, IrisSurface.LAVA)) {
|
||||
for (boolean rise : new boolean[]{false, true}) {
|
||||
Engine engine = mock(Engine.class);
|
||||
NativeEntityRuntime runtime = mock(NativeEntityRuntime.class);
|
||||
NativeSpawnedEntity created = mock(NativeSpawnedEntity.class);
|
||||
IrisEntity entity = new IrisEntity().setSpecialType("test:mob")
|
||||
.setSurface(surface).setSpawnEffectRiseOutOfGround(rise);
|
||||
when(runtime.chunksSafe(-1, 0)).thenReturn(true);
|
||||
when(runtime.hasPlayersNearby(any(NativeEntityRuntime.Position.class), eq(32D))).thenReturn(true);
|
||||
when(runtime.spawnCustom(eq("test:mob"), any(NativeEntityRuntime.Position.class), any()))
|
||||
.thenReturn(created);
|
||||
|
||||
assertSame(created, ModdedEntitySpawner.spawn(engine, entity, runtime, -1, -49, 0, new RNG(1L)));
|
||||
|
||||
double targetY = -49 + (surface.isFluid() ? 0.5 : 0) - (rise ? 5 : 0);
|
||||
verify(runtime).spawnCustom(eq("test:mob"),
|
||||
eq(new NativeEntityRuntime.Position(-0.5, targetY, 0.5)), any());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mixedSpawnerUsesFluidDepthWithoutChangingLandMembers() {
|
||||
RNG rng = new RNG(1L);
|
||||
@@ -117,4 +175,42 @@ public class ModdedWaterEntitySpawnTest {
|
||||
assertNull(IrisEntitySpawn.selectSurfaceSpawnY(IrisSpawnGroup.NORMAL, IrisSurface.WATER, 78, 78, rng));
|
||||
assertNull(IrisEntitySpawn.selectSurfaceSpawnY(IrisSpawnGroup.UNDERWATER, IrisSurface.WATER, 78, 78, rng));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void caveFloorMarkerSpawnsAboveSupportAtNegativeWorldHeight() throws ReflectiveOperationException {
|
||||
Engine engine = mock(Engine.class);
|
||||
EngineMantle mantle = mock(EngineMantle.class);
|
||||
when(engine.getMantle()).thenReturn(mantle);
|
||||
when(engine.getSeedManager()).thenReturn(mock(SeedManager.class));
|
||||
when(mantle.findMarkers(-2, 3, MarkerMatter.CAVE_FLOOR))
|
||||
.thenReturn(new KList<>(new IrisPosition(-25, -49, 57)));
|
||||
NativeWorld world = mock(NativeWorld.class);
|
||||
when(world.minHeight()).thenReturn(-64);
|
||||
when(world.maxHeight()).thenReturn(320);
|
||||
when(world.nativeHandle()).thenReturn(mock(ServerLevel.class));
|
||||
when(world.getBlock(anyInt(), anyInt(), anyInt())).thenAnswer(invocation ->
|
||||
ModdedBlockState.of((int) invocation.getArgument(1) <= -50
|
||||
? Blocks.STONE.defaultBlockState() : Blocks.CAVE_AIR.defaultBlockState(), null));
|
||||
IrisEntity entity = new IrisEntity().setSurface(IrisSurface.LAND);
|
||||
IrisEntitySpawn entry = spy(new IrisEntitySpawn().setMinSpawns(1).setMaxSpawns(1));
|
||||
doReturn(entity).when(entry).getRealEntity(engine);
|
||||
IrisSpawner spawner = new IrisSpawner().setGroup(IrisSpawnGroup.CAVE);
|
||||
Method spawn = ModdedWorldManager.class.getDeclaredMethod("spawnEntry", NativeWorld.class,
|
||||
IrisEntitySpawn.class, IrisSpawner.class, int.class, int.class, int.class);
|
||||
spawn.setAccessible(true);
|
||||
ModdedWorldManager manager = new ModdedWorldManager(engine);
|
||||
try (MockedStatic<ModdedEntitySpawner> entities = mockStatic(ModdedEntitySpawner.class)) {
|
||||
entities.when(() -> ModdedEntitySpawner.isAreaClearForSpawn(any(NativeEntityRuntime.class),
|
||||
eq(entity), eq(-25), eq(-49), eq(57))).thenReturn(true);
|
||||
entities.when(() -> ModdedEntitySpawner.spawn(eq(engine), eq(entity), any(NativeEntityRuntime.class),
|
||||
eq(-25), eq(-49), eq(57), any(RNG.class))).thenReturn(mock(NativeSpawnedEntity.class));
|
||||
|
||||
assertEquals(1, spawn.invoke(manager, world, entry, spawner, -2, 3, 1));
|
||||
|
||||
entities.verify(() -> ModdedEntitySpawner.spawn(eq(engine), eq(entity), any(NativeEntityRuntime.class),
|
||||
eq(-25), eq(-49), eq(57), any(RNG.class)));
|
||||
} finally {
|
||||
manager.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package art.arcane.volmlib.nativelib.minecraft26_2.modded;
|
||||
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.MobCategory;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
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.isNull;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class NativeSpawnPopulationTest {
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void categoryCountsExcludePlayersAndNeighboringChunks() {
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
NativeWorld world = mock(NativeWorld.class);
|
||||
when(world.nativeHandle()).thenReturn(level);
|
||||
when(level.getMinY()).thenReturn(-64);
|
||||
when(level.getMaxY()).thenReturn(320);
|
||||
LivingEntity frog = mob(MobCategory.CREATURE, -4, 7);
|
||||
LivingEntity zombie = mob(MobCategory.MONSTER, -4, 7);
|
||||
LivingEntity neighbor = mob(MobCategory.MONSTER, -3, 7);
|
||||
Player player = mock(Player.class);
|
||||
when(player.blockPosition()).thenReturn(new BlockPos(-60, 50, 120));
|
||||
List<Entity> entities = List.of(frog, frog, frog, zombie, neighbor, player);
|
||||
when(level.getEntities(isNull(Entity.class), any(AABB.class), any()))
|
||||
.thenAnswer(invocation -> {
|
||||
Predicate<? super Entity> filter = invocation.getArgument(2);
|
||||
return entities.stream().filter(filter).toList();
|
||||
});
|
||||
assertEquals(Map.of("creature", 3, "monster", 1),
|
||||
NativeSpawnQueries.livingEntityCategories(world, -4, 7));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ambientAdmissionRequiresMobSpawningAndTickingForRecurringAttempts() {
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
NativeWorld world = mock(NativeWorld.class);
|
||||
GameRules rules = mock(GameRules.class);
|
||||
when(world.nativeHandle()).thenReturn(level);
|
||||
when(level.getMinY()).thenReturn(-64);
|
||||
when(level.getGameRules()).thenReturn(rules);
|
||||
when(rules.get(GameRules.SPAWN_MOBS)).thenReturn(false);
|
||||
assertFalse(NativeSpawnQueries.ambientAllowed(world, -4, 7, true));
|
||||
assertFalse(NativeSpawnQueries.ambientAllowed(world, -4, 7, false));
|
||||
when(rules.get(GameRules.SPAWN_MOBS)).thenReturn(true);
|
||||
assertTrue(NativeSpawnQueries.ambientAllowed(world, -4, 7, true));
|
||||
assertFalse(NativeSpawnQueries.ambientAllowed(world, -4, 7, false));
|
||||
when(level.isPositionEntityTicking(new BlockPos(-56, -64, 120))).thenReturn(true);
|
||||
assertTrue(NativeSpawnQueries.ambientAllowed(world, -4, 7, false));
|
||||
}
|
||||
|
||||
private static LivingEntity mob(MobCategory category, int chunkX, int chunkZ) {
|
||||
EntityType<?> type = mock(EntityType.class);
|
||||
when(type.getCategory()).thenReturn(category);
|
||||
LivingEntity entity = mock(LivingEntity.class);
|
||||
doReturn(type).when(entity).getType();
|
||||
when(entity.blockPosition()).thenReturn(new BlockPos((chunkX << 4) + 8, 32, (chunkZ << 4) + 8));
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package art.arcane.volmlib.nativelib.minecraft26_2.modded.mixin;
|
||||
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedChunkGenerator;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.level.ServerChunkCache;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
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 NativeStructureBootstrapTest {
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void levelConstructionDefersNativeStructureInitialization() {
|
||||
ServerLevelStructureBootstrapMixin mixin = mock(ServerLevelStructureBootstrapMixin.class, CALLS_REAL_METHODS);
|
||||
ServerChunkCache chunks = mock(ServerChunkCache.class);
|
||||
NativeModdedChunkGenerator<?, ?, ?> generator = mock(NativeModdedChunkGenerator.class);
|
||||
ChunkGeneratorStructureState state = mock(ChunkGeneratorStructureState.class);
|
||||
when(mixin.getChunkSource()).thenReturn(chunks);
|
||||
when(chunks.getGenerator()).thenReturn(generator);
|
||||
|
||||
mixin.iris$initializeStructureState(state);
|
||||
|
||||
verify(state, never()).ensureStructuresGenerated();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void levelConstructionStillInitializesVanillaStructures() {
|
||||
ServerLevelStructureBootstrapMixin mixin = mock(ServerLevelStructureBootstrapMixin.class, CALLS_REAL_METHODS);
|
||||
ServerChunkCache chunks = mock(ServerChunkCache.class);
|
||||
ChunkGenerator generator = mock(ChunkGenerator.class);
|
||||
ChunkGeneratorStructureState state = mock(ChunkGeneratorStructureState.class);
|
||||
when(mixin.getChunkSource()).thenReturn(chunks);
|
||||
when(chunks.getGenerator()).thenReturn(generator);
|
||||
|
||||
mixin.iris$initializeStructureState(state);
|
||||
|
||||
verify(state).ensureStructuresGenerated();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void boundNativeWorldInitializesItsDeferredStructures() {
|
||||
NativeModdedChunkGenerator<?, ?, ?> generator = mock(NativeModdedChunkGenerator.class, CALLS_REAL_METHODS);
|
||||
NativeWorld world = mock(NativeWorld.class);
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
ServerChunkCache chunks = mock(ServerChunkCache.class);
|
||||
ChunkGeneratorStructureState state = mock(ChunkGeneratorStructureState.class);
|
||||
when(world.nativeHandle()).thenReturn(level);
|
||||
when(level.getChunkSource()).thenReturn(chunks);
|
||||
when(chunks.getGeneratorState()).thenReturn(state);
|
||||
|
||||
generator.initializeStructureState(world);
|
||||
|
||||
verify(state).ensureStructuresGenerated();
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ apply from: file('../../gradle/volmlib-stamp.gradle')
|
||||
|
||||
Properties rootProperties = new Properties()
|
||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.1.2-26.2'))
|
||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.0-26.2'))
|
||||
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
|
||||
String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse(rootProperties.getProperty('neoForgeVersion', '26.3.0.33-beta'))
|
||||
Closure<String> irisArtifactName = { String platform, String targetVersion ->
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ interface IrisFileOperations {
|
||||
IrisFileOperations irisFileOperations = objects.newInstance(IrisFileOperations)
|
||||
|
||||
group = 'art.arcane'
|
||||
version = providers.gradleProperty('irisVersion').getOrElse('4.1.2-26.2')
|
||||
version = providers.gradleProperty('irisVersion').getOrElse('4.3.0-26.2')
|
||||
|
||||
project(':core') {
|
||||
group = 'art.arcane'
|
||||
|
||||
@@ -197,7 +197,7 @@ public class IrisSettings {
|
||||
@Data
|
||||
public static class IrisSettingsWorld {
|
||||
public boolean postLoadBlockUpdates = true;
|
||||
public boolean forcePersistEntities = true;
|
||||
public boolean forcePersistEntities = false;
|
||||
public boolean ambientEntitySpawningSystem = true;
|
||||
public long asyncTickIntervalMS = 700;
|
||||
public double targetSpawnEntitiesPerChunk = 0.95;
|
||||
|
||||
@@ -51,7 +51,7 @@ public final class FloatingIslandSample {
|
||||
private static final double CARVE_MAX_VERTICAL_RUN_FRACTION = 0.18D;
|
||||
private static final ThreadLocal<int[]> LAST_REJECT = ThreadLocal.withInitial(() -> new int[1]);
|
||||
private static final ThreadLocal<double[]> LAST_DENSITY = ThreadLocal.withInitial(() -> new double[2]);
|
||||
private static final ThreadLocal<HashMap<Long, FloatingIslandSample>> CHUNK_MEMO = ThreadLocal.withInitial(HashMap::new);
|
||||
private static final ThreadLocal<ChunkMemo> CHUNK_MEMO = ThreadLocal.withInitial(ChunkMemo::new);
|
||||
private static final ThreadLocal<IdentityHashMap<CNG, Long2DoubleOpenHashMap>> FOOTPRINT_MEMO = ThreadLocal.withInitial(IdentityHashMap::new);
|
||||
private static final AtomicBoolean NULL_CNG_WARNED = new AtomicBoolean(false);
|
||||
|
||||
@@ -68,21 +68,32 @@ public final class FloatingIslandSample {
|
||||
}
|
||||
|
||||
public static void clearThreadCaches() {
|
||||
CHUNK_MEMO.remove();
|
||||
FOOTPRINT_MEMO.remove();
|
||||
LAST_REJECT.remove();
|
||||
LAST_DENSITY.remove();
|
||||
}
|
||||
|
||||
public static void clearChunkMemo() {
|
||||
CHUNK_MEMO.get().clear();
|
||||
CHUNK_MEMO.get().samples.clear();
|
||||
FOOTPRINT_MEMO.get().clear();
|
||||
}
|
||||
|
||||
public static FloatingIslandSample sampleMemoized(IrisBiome parent, int wx, int wz, int chunkHeight, long baseSeed, IrisData data, Engine engine, FloatingIslandBoundarySampler boundarySampler) {
|
||||
long key = (((long) wx) << 32) ^ (wz & 0xFFFFFFFFL);
|
||||
HashMap<Long, FloatingIslandSample> memo = CHUNK_MEMO.get();
|
||||
if (memo.containsKey(key)) {
|
||||
return memo.get(key);
|
||||
ChunkMemo memo = CHUNK_MEMO.get();
|
||||
if (memo.context == null || !memo.context.matches(wx >> 4, wz >> 4, chunkHeight, baseSeed,
|
||||
data, engine, boundarySampler)) {
|
||||
memo.samples.clear();
|
||||
FOOTPRINT_MEMO.get().clear();
|
||||
memo.context = new MemoContext(wx >> 4, wz >> 4, chunkHeight, baseSeed, data, engine, boundarySampler);
|
||||
}
|
||||
MemoizedSample cached = memo.samples.get(key);
|
||||
if (cached != null && cached.parent == parent) {
|
||||
return cached.sample;
|
||||
}
|
||||
FloatingIslandSample result = sample(parent, wx, wz, chunkHeight, baseSeed, data, engine, boundarySampler);
|
||||
memo.put(key, result);
|
||||
memo.samples.put(key, new MemoizedSample(parent, result));
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -799,4 +810,21 @@ public final class FloatingIslandSample {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static final class ChunkMemo {
|
||||
private final HashMap<Long, MemoizedSample> samples = new HashMap<>(256);
|
||||
private MemoContext context;
|
||||
}
|
||||
|
||||
private record MemoContext(int chunkX, int chunkZ, int height, long seed, IrisData data, Engine engine,
|
||||
FloatingIslandBoundarySampler boundarySampler) {
|
||||
private boolean matches(int chunkX, int chunkZ, int height, long seed, IrisData data, Engine engine,
|
||||
FloatingIslandBoundarySampler boundarySampler) {
|
||||
return this.chunkX == chunkX && this.chunkZ == chunkZ && this.height == height && this.seed == seed
|
||||
&& this.data == data && this.engine == engine && this.boundarySampler == boundarySampler;
|
||||
}
|
||||
}
|
||||
|
||||
private record MemoizedSample(IrisBiome parent, FloatingIslandSample sample) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ import java.util.Objects;
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class IrisBiome extends IrisRegistrant implements Rarity {
|
||||
private static final int BIOME_GENERATOR_CACHE_SIZE = 8;
|
||||
private static final int CHILDREN_SEED_SALT = 2137;
|
||||
|
||||
private final transient AtomicCache<KMap<String, IrisBiomeGeneratorLink>> genCache = new AtomicCache<>();
|
||||
private final transient AtomicCache<KMap<String, Integer>> genCacheMax = new AtomicCache<>();
|
||||
@@ -104,7 +105,12 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
|
||||
private final transient AtomicCache<Color> cacheColorDecoratorLoad = new AtomicCache<>();
|
||||
private final transient AtomicCache<Color> cacheColorLayerLoad = new AtomicCache<>();
|
||||
private final transient AtomicCache<Color> cacheColorDepositLoad = new AtomicCache<>();
|
||||
private final transient AtomicCache<CNG> childrenCell = new AtomicCache<>();
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private final transient LazyBoundedCache<ChildGeneratorKey, CNG> childrenGenerators = new LazyBoundedCache<>(8);
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private transient volatile CachedChildGenerator recentChildGenerator;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient LazyBoundedCache<Long, CNG> biomeGenerators =
|
||||
new LazyBoundedCache<>(BIOME_GENERATOR_CACHE_SIZE);
|
||||
@@ -511,7 +517,22 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
|
||||
}
|
||||
|
||||
public CNG getChildrenGenerator(RNG random, int sig, double scale) {
|
||||
return childrenCell.aquire(() -> getChildStyle().create(random.nextParallelRNG(sig * 2137), getLoader()).bake().scale(scale).bake());
|
||||
long seed = deriveChildStyleSeed(random, sig).getSeed();
|
||||
IrisData data = getLoader();
|
||||
Engine engine = data == null ? null : data.getEngine();
|
||||
CachedChildGenerator recent = recentChildGenerator;
|
||||
if (recent != null && recent.key.matches(seed, scale, data, engine)) {
|
||||
return recent.generator;
|
||||
}
|
||||
ChildGeneratorKey key = new ChildGeneratorKey(seed, scale, data, engine);
|
||||
CNG generator = childrenGenerators.computeIfAbsent(key,
|
||||
ignored -> getChildStyle().createScaledGenerator(new RNG(seed), data, scale));
|
||||
recentChildGenerator = new CachedChildGenerator(key, generator);
|
||||
return generator;
|
||||
}
|
||||
|
||||
static RNG deriveChildStyleSeed(RNG random, int signature) {
|
||||
return random.nextParallelRNG(signature * CHILDREN_SEED_SALT);
|
||||
}
|
||||
|
||||
public KList<NativeBlockState> generateLayers(IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex) {
|
||||
@@ -689,4 +710,27 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
|
||||
this.generator = generator;
|
||||
}
|
||||
}
|
||||
|
||||
private record ChildGeneratorKey(long seed, double scale, IrisData data, Engine engine) {
|
||||
private boolean matches(long seed, double scale, IrisData data, Engine engine) {
|
||||
return this.seed == seed && Double.doubleToLongBits(this.scale) == Double.doubleToLongBits(scale)
|
||||
&& this.data == data && this.engine == engine;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
return object instanceof ChildGeneratorKey other && matches(other.seed, other.scale, other.data, other.engine);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = Long.hashCode(seed);
|
||||
hash = 31 * hash + Double.hashCode(scale);
|
||||
hash = 31 * hash + System.identityHashCode(data);
|
||||
return 31 * hash + System.identityHashCode(engine);
|
||||
}
|
||||
}
|
||||
|
||||
private record CachedChildGenerator(ChildGeneratorKey key, CNG generator) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,10 @@ import art.arcane.volmlib.util.math.Rarity;
|
||||
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.volmlib.util.cache.AtomicCache;
|
||||
import art.arcane.iris.generation.cache.LazyBoundedCache;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.pack.schema.annotation.ArrayType;
|
||||
import art.arcane.volmlib.util.documentation.Description;
|
||||
@@ -43,7 +47,6 @@ import art.arcane.iris.pack.schema.annotation.RegistryListResource;
|
||||
import art.arcane.iris.pack.schema.annotation.Snippet;
|
||||
import art.arcane.volmlib.util.noise.CNG;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
@@ -59,48 +62,59 @@ import lombok.experimental.Accessors;
|
||||
@Data
|
||||
public class IrisFloatingChildBiomes implements Rarity {
|
||||
private final transient AtomicCache<IrisBiome> resolvedBiome = new AtomicCache<>();
|
||||
private final transient AtomicCache<CNG> footprintCache = new AtomicCache<>();
|
||||
private final transient AtomicCache<FloatingIslandEdgeProfile> edgeTaperProfileCache = new AtomicCache<>();
|
||||
private final transient AtomicCache<CNG> pickerCache = new AtomicCache<>();
|
||||
private final transient AtomicCache<CNG> altitudeCache = new AtomicCache<>();
|
||||
private final transient AtomicCache<CNG> topShapeCache = new AtomicCache<>();
|
||||
private final transient AtomicCache<CNG> bottomCache = new AtomicCache<>();
|
||||
private final transient AtomicCache<CNG> wallWarpCache = new AtomicCache<>();
|
||||
private final transient AtomicCache<CNG> carveCache = new AtomicCache<>();
|
||||
private final transient AtomicCache<IrisCaveProfileSampler> carvingProfileSamplerCache = new AtomicCache<>(true);
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private final transient LazyBoundedCache<RuntimeKey, FloatingIslandEdgeProfile> edgeTaperProfiles = new LazyBoundedCache<>(8);
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private transient volatile CachedEdgeProfile recentEdgeProfile;
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private final transient LazyBoundedCache<RuntimeKey, IrisCaveProfileSampler> carvingProfileSamplers = new LazyBoundedCache<>(8);
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private transient volatile CachedCarvingProfile recentCarvingProfile;
|
||||
private final transient AtomicCache<IrisObjectScale> shrinkScaleCache = new AtomicCache<>();
|
||||
|
||||
public CNG getFootprintCng(long baseSeed, IrisData data) {
|
||||
return footprintCache.aquire(() -> getFootprintStyle().create(new RNG(baseSeed ^ 0xF007B17DL), data));
|
||||
return getFootprintStyle().create(baseSeed ^ 0xF007B17DL, data);
|
||||
}
|
||||
|
||||
public FloatingIslandEdgeProfile getEdgeTaperProfile(long baseSeed, IrisData data) {
|
||||
return edgeTaperProfileCache.aquire(() -> {
|
||||
Engine engine = data == null ? null : data.getEngine();
|
||||
CachedEdgeProfile recent = recentEdgeProfile;
|
||||
if (recent != null && recent.key.matches(baseSeed, data, engine)) {
|
||||
return recent.profile;
|
||||
}
|
||||
RuntimeKey key = new RuntimeKey(baseSeed, data, engine);
|
||||
FloatingIslandEdgeProfile profile = edgeTaperProfiles.computeIfAbsent(key, ignored -> {
|
||||
int width = FloatingIslandEdgeProfile.clampWidth(getEdgeTaperWidth());
|
||||
double amplitude = FloatingIslandEdgeProfile.clampVariationAmplitude(
|
||||
getEdgeTaperVariationAmplitude(), width);
|
||||
IrisGeneratorStyle style = getEdgeTaperVariationStyle();
|
||||
CNG variation = amplitude > 0.0D && style != null
|
||||
? style.create(new RNG(baseSeed ^ 0xED6E7A9E5L), data)
|
||||
? style.create(baseSeed ^ 0xED6E7A9E5L, data)
|
||||
: null;
|
||||
return new FloatingIslandEdgeProfile(width, getEdgeTaperExponent(), amplitude, variation);
|
||||
});
|
||||
recentEdgeProfile = new CachedEdgeProfile(key, profile);
|
||||
return profile;
|
||||
}
|
||||
|
||||
public CNG getPickerCng(long baseSeed, IrisData data) {
|
||||
return pickerCache.aquire(() -> getPickerStyle().create(new RNG(baseSeed ^ 0x91C4E72DL), data));
|
||||
return getPickerStyle().create(baseSeed ^ 0x91C4E72DL, data);
|
||||
}
|
||||
|
||||
public CNG getAltitudeCng(long baseSeed, IrisData data) {
|
||||
return altitudeCache.aquire(() -> getAltitudeStyle().create(new RNG(baseSeed ^ 0xA17DEBBL), data));
|
||||
return getAltitudeStyle().create(baseSeed ^ 0xA17DEBBL, data);
|
||||
}
|
||||
|
||||
public CNG getTopShapeCng(long baseSeed, IrisData data) {
|
||||
return topShapeCache.aquire(() -> getTopShapeStyle().create(new RNG(baseSeed ^ 0x70970601DEFL), data));
|
||||
return getTopShapeStyle().create(baseSeed ^ 0x70970601DEFL, data);
|
||||
}
|
||||
|
||||
public CNG getBottomCng(long baseSeed, IrisData data) {
|
||||
return bottomCache.aquire(() -> getBottomStyle().create(new RNG(baseSeed ^ 0xB0770075CAFEL), data));
|
||||
return getBottomStyle().create(baseSeed ^ 0xB0770075CAFEL, data);
|
||||
}
|
||||
|
||||
public CNG getWallWarpCng(long baseSeed, IrisData data) {
|
||||
@@ -108,7 +122,7 @@ public class IrisFloatingChildBiomes implements Rarity {
|
||||
if (style == null) {
|
||||
return null;
|
||||
}
|
||||
return wallWarpCache.aquire(() -> style.create(new RNG(baseSeed ^ 0xA117BA17E0FL), data));
|
||||
return style.create(baseSeed ^ 0xA117BA17E0FL, data);
|
||||
}
|
||||
|
||||
public CNG getCarveCng(long baseSeed, IrisData data) {
|
||||
@@ -116,7 +130,7 @@ public class IrisFloatingChildBiomes implements Rarity {
|
||||
if (style == null) {
|
||||
return null;
|
||||
}
|
||||
return carveCache.aquire(() -> style.create(new RNG(baseSeed ^ 0xCA5EC1EE5EL), data));
|
||||
return style.create(baseSeed ^ 0xCA5EC1EE5EL, data);
|
||||
}
|
||||
|
||||
public IrisCaveProfileSampler getCarvingProfileSampler(Engine engine, IrisData data) {
|
||||
@@ -124,7 +138,12 @@ public class IrisFloatingChildBiomes implements Rarity {
|
||||
return null;
|
||||
}
|
||||
|
||||
return carvingProfileSamplerCache.aquire(() -> {
|
||||
CachedCarvingProfile recent = recentCarvingProfile;
|
||||
if (recent != null && recent.key.matches(0L, data, engine)) {
|
||||
return recent.sampler;
|
||||
}
|
||||
RuntimeKey key = new RuntimeKey(0L, data, engine);
|
||||
IrisCaveProfileSampler sampler = carvingProfileSamplers.computeIfAbsent(key, ignored -> {
|
||||
IrisBiome resolved = resolveCarvingBiome(carving, engine, data);
|
||||
if (resolved == null || resolved.getCaveProfile() == null || !resolved.getCaveProfile().isEnabled()) {
|
||||
return null;
|
||||
@@ -132,6 +151,8 @@ public class IrisFloatingChildBiomes implements Rarity {
|
||||
|
||||
return new IrisCaveProfileSampler(engine, resolved.getCaveProfile());
|
||||
});
|
||||
recentCarvingProfile = new CachedCarvingProfile(key, sampler);
|
||||
return sampler;
|
||||
}
|
||||
|
||||
public boolean hasCarvingReference() {
|
||||
@@ -379,4 +400,28 @@ public class IrisFloatingChildBiomes implements Rarity {
|
||||
return loaded;
|
||||
});
|
||||
}
|
||||
|
||||
private record RuntimeKey(long seed, IrisData data, Engine engine) {
|
||||
private boolean matches(long seed, IrisData data, Engine engine) {
|
||||
return this.seed == seed && this.data == data && this.engine == engine;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
return object instanceof RuntimeKey other && matches(other.seed, other.data, other.engine);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = Long.hashCode(seed);
|
||||
hash = 31 * hash + System.identityHashCode(data);
|
||||
return 31 * hash + System.identityHashCode(engine);
|
||||
}
|
||||
}
|
||||
|
||||
private record CachedEdgeProfile(RuntimeKey key, FloatingIslandEdgeProfile profile) {
|
||||
}
|
||||
|
||||
private record CachedCarvingProfile(RuntimeKey key, IrisCaveProfileSampler sampler) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,18 +6,28 @@ import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
|
||||
private final Map<Key, Map<Key, T>> map = new KMap<>();
|
||||
private transient volatile long modificationRevision;
|
||||
private final AtomicLong modificationRevision = new AtomicLong();
|
||||
private transient volatile OrderedSnapshot<T> orderedSnapshot;
|
||||
|
||||
public long modificationRevision() {
|
||||
return modificationRevision;
|
||||
return modificationRevision.get();
|
||||
}
|
||||
|
||||
private void changed() {
|
||||
modificationRevision.incrementAndGet();
|
||||
orderedSnapshot = null;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
@@ -30,7 +40,7 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
|
||||
|
||||
public boolean containsKey(@NonNull IrisBlockVector vector) {
|
||||
if (map.isEmpty()) return false;
|
||||
var chunk = map.get(chunk(vector));
|
||||
Map<Key, T> chunk = map.get(chunk(vector));
|
||||
return chunk != null && chunk.containsKey(relative(vector));
|
||||
}
|
||||
|
||||
@@ -40,14 +50,14 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
|
||||
|
||||
public @Nullable T get(@NonNull IrisBlockVector vector) {
|
||||
if (map.isEmpty()) return null;
|
||||
var chunk = map.get(chunk(vector));
|
||||
Map<Key, T> chunk = map.get(chunk(vector));
|
||||
return chunk == null ? null : chunk.get(relative(vector));
|
||||
}
|
||||
|
||||
public @Nullable T put(@NonNull IrisBlockVector vector, @NonNull T value) {
|
||||
T previous = map.computeIfAbsent(chunk(vector), k -> new KMap<>())
|
||||
.put(relative(vector), value);
|
||||
modificationRevision++;
|
||||
changed();
|
||||
return previous;
|
||||
}
|
||||
|
||||
@@ -59,7 +69,7 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
|
||||
return mappingFunction.apply(vector);
|
||||
});
|
||||
if (inserted[0]) {
|
||||
modificationRevision++;
|
||||
changed();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -77,7 +87,7 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
|
||||
});
|
||||
|
||||
if (removed[0] != null) {
|
||||
modificationRevision++;
|
||||
changed();
|
||||
}
|
||||
|
||||
return (T) removed[0];
|
||||
@@ -89,22 +99,47 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
|
||||
|
||||
public void clear() {
|
||||
if (!map.isEmpty()) {
|
||||
modificationRevision++;
|
||||
map.clear();
|
||||
changed();
|
||||
}
|
||||
map.clear();
|
||||
}
|
||||
|
||||
public void forEach(@NonNull BiConsumer<@NonNull IrisBlockVector, @NonNull T> consumer) {
|
||||
map.forEach((chunk, values) -> {
|
||||
int rX = chunk.x << 10;
|
||||
int rY = chunk.y << 10;
|
||||
int rZ = chunk.z << 10;
|
||||
Cursor cursor = cursor();
|
||||
while (cursor.next()) {
|
||||
consumer.accept(cursor.key().clone(), cursor.value());
|
||||
}
|
||||
}
|
||||
|
||||
values.forEach((relative, value) -> consumer.accept(
|
||||
relative.resolve(rX, rY, rZ),
|
||||
value
|
||||
));
|
||||
});
|
||||
private List<OrderedEntry<T>> orderedEntries() {
|
||||
long revision = modificationRevision.get();
|
||||
OrderedSnapshot<T> cached = orderedSnapshot;
|
||||
if (cached != null && cached.revision() == revision) {
|
||||
return cached.entries();
|
||||
}
|
||||
synchronized (this) {
|
||||
revision = modificationRevision.get();
|
||||
cached = orderedSnapshot;
|
||||
if (cached != null && cached.revision() == revision) {
|
||||
return cached.entries();
|
||||
}
|
||||
List<OrderedEntry<T>> entries = new ArrayList<>();
|
||||
map.forEach((chunk, values) -> values.forEach((relative, value) -> entries.add(
|
||||
new OrderedEntry<>((chunk.x << 10) + relative.x, (chunk.y << 10) + relative.y,
|
||||
(chunk.z << 10) + relative.z, value))));
|
||||
entries.sort(Comparator.comparingInt((OrderedEntry<T> entry) -> entry.x())
|
||||
.thenComparingInt(OrderedEntry::y).thenComparingInt(OrderedEntry::z));
|
||||
if (modificationRevision.get() == revision) {
|
||||
orderedSnapshot = new OrderedSnapshot<>(revision, entries);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
}
|
||||
|
||||
private record OrderedEntry<T>(int x, int y, int z, T value) {
|
||||
}
|
||||
|
||||
private record OrderedSnapshot<T>(long revision, List<OrderedEntry<T>> entries) {
|
||||
}
|
||||
|
||||
private static Key chunk(IrisBlockVector vector) {
|
||||
@@ -139,32 +174,21 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
|
||||
}
|
||||
|
||||
public final class Cursor {
|
||||
private final Iterator<Map.Entry<Key, Map<Key, T>>> chunkIterator = map.entrySet().iterator();
|
||||
private final List<OrderedEntry<T>> entries = orderedEntries();
|
||||
private final IrisBlockVector position = new IrisBlockVector(0, 0, 0);
|
||||
private Iterator<Map.Entry<Key, T>> relativeIterator;
|
||||
private int rX, rY, rZ;
|
||||
private int index;
|
||||
private T value;
|
||||
|
||||
public boolean next() {
|
||||
while (relativeIterator == null || !relativeIterator.hasNext()) {
|
||||
if (!chunkIterator.hasNext()) {
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
Map.Entry<Key, Map<Key, T>> chunk = chunkIterator.next();
|
||||
rX = chunk.getKey().x << 10;
|
||||
rY = chunk.getKey().y << 10;
|
||||
rZ = chunk.getKey().z << 10;
|
||||
relativeIterator = chunk.getValue().entrySet().iterator();
|
||||
if (index >= entries.size()) {
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
Map.Entry<Key, T> entry = relativeIterator.next();
|
||||
Key relative = entry.getKey();
|
||||
position.setX(rX + relative.x);
|
||||
position.setY(rY + relative.y);
|
||||
position.setZ(rZ + relative.z);
|
||||
value = entry.getValue();
|
||||
OrderedEntry<T> entry = entries.get(index++);
|
||||
position.setX(entry.x());
|
||||
position.setY(entry.y());
|
||||
position.setZ(entry.z());
|
||||
value = entry.value();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -181,79 +205,62 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
|
||||
}
|
||||
|
||||
public class EntryIterator implements Iterator<Map.Entry<IrisBlockVector, T>> {
|
||||
private final Iterator<Map.Entry<Key, Map<Key, T>>> chunkIterator = map.entrySet().iterator();
|
||||
private Iterator<Map.Entry<Key, T>> relativeIterator;
|
||||
private int rX, rY, rZ;
|
||||
private final List<OrderedEntry<T>> entries = orderedEntries();
|
||||
private int index;
|
||||
private OrderedEntry<T> last;
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return advance();
|
||||
return index < entries.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map.Entry<IrisBlockVector, T> next() {
|
||||
if (!advance()) throw new NoSuchElementException();
|
||||
|
||||
var entry = relativeIterator.next();
|
||||
return Map.entry(entry.getKey().resolve(rX, rY, rZ), entry.getValue());
|
||||
}
|
||||
|
||||
private boolean advance() {
|
||||
while (relativeIterator == null || !relativeIterator.hasNext()) {
|
||||
if (!chunkIterator.hasNext()) return false;
|
||||
var chunk = chunkIterator.next();
|
||||
rX = chunk.getKey().x << 10;
|
||||
rY = chunk.getKey().y << 10;
|
||||
rZ = chunk.getKey().z << 10;
|
||||
relativeIterator = chunk.getValue().entrySet().iterator();
|
||||
if (!hasNext()) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
|
||||
return true;
|
||||
OrderedEntry<T> entry = entries.get(index++);
|
||||
last = entry;
|
||||
return Map.entry(new IrisBlockVector(entry.x(), entry.y(), entry.z()), entry.value());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
if (relativeIterator == null) throw new IllegalStateException("No element to remove");
|
||||
relativeIterator.remove();
|
||||
modificationRevision++;
|
||||
if (last == null) {
|
||||
throw new IllegalStateException("No element to remove");
|
||||
}
|
||||
VectorMap.this.remove(new IrisBlockVector(last.x(), last.y(), last.z()));
|
||||
last = null;
|
||||
}
|
||||
}
|
||||
|
||||
public class KeyIterator implements Iterator<IrisBlockVector>, Iterable<IrisBlockVector> {
|
||||
private final Iterator<Map.Entry<Key, Map<Key, T>>> chunkIterator = map.entrySet().iterator();
|
||||
private Iterator<Key> relativeIterator;
|
||||
private int rX, rY, rZ;
|
||||
private final List<OrderedEntry<T>> entries = orderedEntries();
|
||||
private int index;
|
||||
private OrderedEntry<T> last;
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return advance();
|
||||
return index < entries.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IrisBlockVector next() {
|
||||
if (!advance()) throw new NoSuchElementException();
|
||||
|
||||
return relativeIterator.next().resolve(rX, rY, rZ);
|
||||
}
|
||||
|
||||
private boolean advance() {
|
||||
while (relativeIterator == null || !relativeIterator.hasNext()) {
|
||||
if (!chunkIterator.hasNext()) return false;
|
||||
var chunk = chunkIterator.next();
|
||||
rX = chunk.getKey().x << 10;
|
||||
rY = chunk.getKey().y << 10;
|
||||
rZ = chunk.getKey().z << 10;
|
||||
relativeIterator = chunk.getValue().keySet().iterator();
|
||||
if (!hasNext()) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
|
||||
return true;
|
||||
OrderedEntry<T> entry = entries.get(index++);
|
||||
last = entry;
|
||||
return new IrisBlockVector(entry.x(), entry.y(), entry.z());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
if (relativeIterator == null) throw new IllegalStateException("No element to remove");
|
||||
relativeIterator.remove();
|
||||
modificationRevision++;
|
||||
if (last == null) {
|
||||
throw new IllegalStateException("No element to remove");
|
||||
}
|
||||
VectorMap.this.remove(new IrisBlockVector(last.x(), last.y(), last.z()));
|
||||
last = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -263,35 +270,31 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
|
||||
}
|
||||
|
||||
public class ValueIterator implements Iterator<T>, Iterable<T> {
|
||||
private final Iterator<Map<Key, T>> chunkIterator = map.values().iterator();
|
||||
private Iterator<T> relativeIterator;
|
||||
private final List<OrderedEntry<T>> entries = orderedEntries();
|
||||
private int index;
|
||||
private OrderedEntry<T> last;
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return advance();
|
||||
return index < entries.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public T next() {
|
||||
if (!advance()) throw new NoSuchElementException();
|
||||
|
||||
return relativeIterator.next();
|
||||
}
|
||||
|
||||
private boolean advance() {
|
||||
while (relativeIterator == null || !relativeIterator.hasNext()) {
|
||||
if (!chunkIterator.hasNext()) return false;
|
||||
relativeIterator = chunkIterator.next().values().iterator();
|
||||
if (!hasNext()) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
|
||||
return true;
|
||||
last = entries.get(index++);
|
||||
return last.value();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
if (relativeIterator == null) throw new IllegalStateException("No element to remove");
|
||||
relativeIterator.remove();
|
||||
modificationRevision++;
|
||||
if (last == null) {
|
||||
throw new IllegalStateException("No element to remove");
|
||||
}
|
||||
VectorMap.this.remove(new IrisBlockVector(last.x(), last.y(), last.z()));
|
||||
last = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -35,7 +35,7 @@ public class FloatingDecorator {
|
||||
int xf, int zf, int realX, int realZ,
|
||||
int height, int max, Hunk<NativeBlockState> data, RNG rng,
|
||||
Runnable candidatesNullCallback) {
|
||||
RNG gRNG = new RNG(DecoratorCore.partSeed(engine.getSeedManager().getDecorator(), part));
|
||||
RNG gRNG = new RNG(DecoratorCore.partSeed(engine.getSeedManager().getComponent(), part));
|
||||
IrisDecorator decorator = DecoratorCore.pickDecorator(target, part, gRNG, rng, engine.getData(), realX, realZ);
|
||||
|
||||
if (decorator == null) {
|
||||
@@ -45,11 +45,11 @@ public class FloatingDecorator {
|
||||
|
||||
if (!decorator.isStacking()) {
|
||||
DecoratorCore.placeFloatingSimple(decorator, xf, zf, realX, realZ, height, max, data,
|
||||
rng, engine.getData(), engine.getMantle());
|
||||
gRNG, engine.getData(), engine.getMantle());
|
||||
return max > 1 ? 1 : 0;
|
||||
}
|
||||
|
||||
return DecoratorCore.placeFloatingStacked(decorator, xf, zf, realX, realZ, height, max, data,
|
||||
rng, engine.getData(), engine.getMantle());
|
||||
gRNG, engine.getData(), engine.getMantle());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,13 +60,13 @@ public class IrisCeilingDecorator extends IrisEngineDecorator {
|
||||
return;
|
||||
}
|
||||
}
|
||||
DecoratorCore.placeSingleAt(decorator, x, z, realX, height, realZ, data, rng, getData(), true, getEngine().getMantle());
|
||||
DecoratorCore.placeSingleAt(decorator, x, z, realX, height, realZ, data, partRNG, getData(), true, getEngine().getMantle());
|
||||
return;
|
||||
}
|
||||
|
||||
DecoratorCore.PlaceOpts opts = DecoratorCore.SCRATCH_OPTS.get();
|
||||
opts.reset();
|
||||
opts.caveSkipFluid = caveSkipFluid;
|
||||
DecoratorCore.placeStackDown(decorator, x, z, realX, realZ, height, 0, data, rng, getData(), max, opts, getEngine().getMantle());
|
||||
DecoratorCore.placeStackDown(decorator, x, z, realX, realZ, height, 0, data, partRNG, getData(), max, opts, getEngine().getMantle());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
package art.arcane.iris.generation.decoration;
|
||||
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.cache.LazyBoundedCache;
|
||||
import art.arcane.iris.generation.block.IrisBlockData;
|
||||
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||
import art.arcane.iris.generation.noise.NoiseStyle;
|
||||
@@ -50,9 +52,8 @@ import lombok.experimental.Accessors;
|
||||
@Description("A biome decorator is used for placing flowers, grass, cacti and so on")
|
||||
@Data
|
||||
public class IrisDecorator {
|
||||
private final transient AtomicCache<CNG> layerGenerator = new AtomicCache<>();
|
||||
private final transient AtomicCache<CNG> varianceGenerator = new AtomicCache<>();
|
||||
private final transient AtomicCache<CNG> heightGenerator = new AtomicCache<>();
|
||||
private final transient LazyBoundedCache<GeneratorKey, Generators> generators = new LazyBoundedCache<>(32);
|
||||
private transient volatile Generators recentGenerators;
|
||||
private final transient AtomicCache<KList<NativeBlockState>> blockData = new AtomicCache<>();
|
||||
private final transient AtomicCache<KList<NativeBlockState>> blockDataTops = new AtomicCache<>();
|
||||
private final transient AtomicCache<NativeBlockState[]> blockDataArray = new AtomicCache<>();
|
||||
@@ -129,37 +130,42 @@ public class IrisDecorator {
|
||||
}
|
||||
|
||||
public CNG getHeightGenerator(RNG rng, IrisData data) {
|
||||
CNG cached = heightGenerator.getIfPresent();
|
||||
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
return heightGenerator.aquire(() ->
|
||||
AtomicCache<CNG> cache = generators(rng, data).height;
|
||||
CNG cached = cache.getIfPresent();
|
||||
return cached != null ? cached : cache.aquire(() ->
|
||||
heightVariance.create(rng.nextParallelRNG(getBlockData(data).size() + stackMax + stackMin), data));
|
||||
}
|
||||
|
||||
public CNG getGenerator(RNG rng, IrisData data) {
|
||||
CNG cached = layerGenerator.getIfPresent();
|
||||
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
return layerGenerator.aquire(() -> style.create(rng.nextParallelRNG(getBlockData(data).size()), data));
|
||||
AtomicCache<CNG> cache = generators(rng, data).layer;
|
||||
CNG cached = cache.getIfPresent();
|
||||
return cached != null ? cached : cache.aquire(() -> style.create(rng.nextParallelRNG(getBlockData(data).size()), data));
|
||||
}
|
||||
|
||||
public CNG getVarianceGenerator(RNG rng, IrisData data) {
|
||||
CNG cached = varianceGenerator.getIfPresent();
|
||||
AtomicCache<CNG> cache = generators(rng, data).variance;
|
||||
CNG cached = cache.getIfPresent();
|
||||
return cached != null ? cached : cache.aquire(() -> createVarianceGenerator(rng, data));
|
||||
}
|
||||
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
private Generators generators(RNG rng, IrisData data) {
|
||||
long seed = rng.getSeed();
|
||||
Engine engine = data == null ? null : data.getEngine();
|
||||
Generators recent = recentGenerators;
|
||||
if (recent != null && recent.key.seed == seed && recent.key.data == data && recent.key.engine == engine) {
|
||||
return recent;
|
||||
}
|
||||
Generators selected = generators.computeIfAbsent(new GeneratorKey(seed, data, engine), Generators::new);
|
||||
recentGenerators = selected;
|
||||
return selected;
|
||||
}
|
||||
|
||||
return varianceGenerator.aquire(() ->
|
||||
variance.create(
|
||||
rng.nextParallelRNG(getBlockData(data).size()), data)
|
||||
.scale(1D / variance.getZoom()));
|
||||
private CNG createVarianceGenerator(RNG rng, IrisData data) {
|
||||
return variance.createNoCache(deriveVarianceSeed(rng, data), data).scale(1D / variance.getZoom());
|
||||
}
|
||||
|
||||
RNG deriveVarianceSeed(RNG rng, IrisData data) {
|
||||
return rng.nextParallelRNG(getBlockData(data).size());
|
||||
}
|
||||
|
||||
public KList<IrisBlockData> add(String b) {
|
||||
@@ -354,4 +360,29 @@ public class IrisDecorator {
|
||||
public boolean isStacking() {
|
||||
return getStackMax() > 1;
|
||||
}
|
||||
|
||||
private static final class Generators {
|
||||
private final GeneratorKey key;
|
||||
private final AtomicCache<CNG> layer = new AtomicCache<>();
|
||||
private final AtomicCache<CNG> variance = new AtomicCache<>();
|
||||
private final AtomicCache<CNG> height = new AtomicCache<>();
|
||||
|
||||
private Generators(GeneratorKey key) {
|
||||
this.key = key;
|
||||
}
|
||||
}
|
||||
|
||||
private record GeneratorKey(long seed, IrisData data, Engine engine) {
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return other instanceof GeneratorKey key
|
||||
&& seed == key.seed && data == key.data && engine == key.engine;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return 31 * (31 * Long.hashCode(seed) + System.identityHashCode(data))
|
||||
+ System.identityHashCode(engine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import art.arcane.volmlib.util.math.BlockPosition;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import lombok.AllArgsConstructor;
|
||||
@@ -269,7 +271,7 @@ public class IrisDepositGenerator {
|
||||
return new IrisObject(1, 1, 1);
|
||||
}
|
||||
|
||||
KSet<BlockPosition> cells = new KSet<>();
|
||||
Set<BlockPosition> cells = new LinkedHashSet<>();
|
||||
vanillaEllipsoidCells(rng, size, (x, y, z) -> cells.add(new BlockPosition(x, y, z)));
|
||||
return objectFromCells(cells, rng, rdata);
|
||||
}
|
||||
@@ -407,7 +409,7 @@ public class IrisDepositGenerator {
|
||||
}
|
||||
|
||||
IrisObject generateVanillaScattered(RNG rng, IrisData rdata, int size) {
|
||||
KSet<BlockPosition> cells = new KSet<>();
|
||||
Set<BlockPosition> cells = new LinkedHashSet<>();
|
||||
int candidates = rng.nextInt(Math.max(0, size) + 1);
|
||||
for (int i = 0; i < candidates; i++) {
|
||||
int magnitude = Math.min(i, 7);
|
||||
@@ -419,7 +421,7 @@ public class IrisDepositGenerator {
|
||||
return objectFromCells(cells, rng, rdata);
|
||||
}
|
||||
|
||||
private IrisObject objectFromCells(KSet<BlockPosition> cells, RNG rng, IrisData rdata) {
|
||||
private IrisObject objectFromCells(Set<BlockPosition> cells, RNG rng, IrisData rdata) {
|
||||
if (cells.isEmpty()) {
|
||||
return new IrisObject(1, 1, 1);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,10 @@ public abstract class IrisEngineDecorator extends EngineAssignedComponent implem
|
||||
this.part = part;
|
||||
}
|
||||
|
||||
public static long seedForPart(long baseSeed, IrisDecorationPart part) {
|
||||
return DecoratorCore.partSeed(baseSeed, part);
|
||||
}
|
||||
|
||||
@BlockCoordinates
|
||||
protected RNG getRNG(int x, int z) {
|
||||
long seed = DecoratorCore.partSeed(getSeed(), part);
|
||||
|
||||
@@ -24,7 +24,8 @@ import art.arcane.iris.generation.terrain.IrisMaterialPalette;
|
||||
import art.arcane.iris.pack.value.IrisRange;
|
||||
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.volmlib.util.cache.AtomicCache;
|
||||
import art.arcane.iris.generation.cache.LazyBoundedCache;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.volmlib.util.documentation.Description;
|
||||
import art.arcane.iris.pack.schema.annotation.MaxNumber;
|
||||
import art.arcane.iris.pack.schema.annotation.MinNumber;
|
||||
@@ -55,10 +56,11 @@ public class IrisOreGenerator {
|
||||
@Description("Vertical band (min, max) this ore can generate in, in engine-local Y where 0 is the bottom of the dimension, not world Y.")
|
||||
private IrisRange range = new IrisRange(30, 80);
|
||||
|
||||
private transient AtomicCache<OreChance> chanceCache = new AtomicCache<>();
|
||||
private final transient LazyBoundedCache<ChanceKey, OreChance> chanceCache = new LazyBoundedCache<>(32);
|
||||
private transient volatile CachedChance recentChance;
|
||||
|
||||
public void warm(RNG rng, IrisData data) {
|
||||
chanceCache.aquire(() -> OreChance.of(chanceStyle.create(rng, data)));
|
||||
chance(rng, data);
|
||||
palette.getLayerGenerator(rng, data);
|
||||
}
|
||||
|
||||
@@ -71,18 +73,43 @@ public class IrisOreGenerator {
|
||||
return null;
|
||||
}
|
||||
|
||||
OreChance chance = chanceCache.getIfPresent();
|
||||
if (chance == null) {
|
||||
chance = chanceCache.aquire(() -> OreChance.of(chanceStyle.create(rng, data)));
|
||||
}
|
||||
|
||||
if (chance.sample(x, y, z) > threshold) {
|
||||
if (chance(rng, data).sample(x, y, z) > threshold) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return palette.get(rng, x, y, z, data);
|
||||
}
|
||||
|
||||
private OreChance chance(RNG rng, IrisData data) {
|
||||
long seed = rng.getSeed();
|
||||
Engine engine = data == null ? null : data.getEngine();
|
||||
CachedChance recent = recentChance;
|
||||
if (recent != null && recent.key.seed == seed && recent.key.data == data && recent.key.engine == engine) {
|
||||
return recent.chance;
|
||||
}
|
||||
ChanceKey key = new ChanceKey(seed, data, engine);
|
||||
OreChance selected = chanceCache.computeIfAbsent(key, ignored -> OreChance.of(chanceStyle.create(rng, data)));
|
||||
recentChance = new CachedChance(key, selected);
|
||||
return selected;
|
||||
}
|
||||
|
||||
private record ChanceKey(long seed, IrisData data, Engine engine) {
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return other instanceof ChanceKey key
|
||||
&& seed == key.seed && data == key.data && engine == key.engine;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return 31 * (31 * Long.hashCode(seed) + System.identityHashCode(data))
|
||||
+ System.identityHashCode(engine);
|
||||
}
|
||||
}
|
||||
|
||||
private record CachedChance(ChanceKey key, OreChance chance) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The chance field, with its single value resolved once when it is constant (a FLAT style),
|
||||
* which bands such as a full deepslate layer sample at every block.
|
||||
|
||||
@@ -53,7 +53,7 @@ public class IrisSeaFloorDecorator extends IrisEngineDecorator {
|
||||
&& !decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ))) {
|
||||
return;
|
||||
}
|
||||
NativeBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData());
|
||||
NativeBlockState block = decorator.getBlockData100(biome, partRNG, realX, height, realZ, getData());
|
||||
if (block != null) {
|
||||
data.set(x, height, z, block);
|
||||
}
|
||||
@@ -61,13 +61,13 @@ public class IrisSeaFloorDecorator extends IrisEngineDecorator {
|
||||
}
|
||||
|
||||
int stack = Math.min(limit - height,
|
||||
DecoratorCore.computeStack(decorator, rng, realX, realZ, getData(), max - height));
|
||||
DecoratorCore.computeStack(decorator, partRNG, realX, realZ, getData(), max - height));
|
||||
for (int i = 0; i < stack; i++) {
|
||||
int h = height + i;
|
||||
double threshold = stack == 1 ? 1.0 : ((double) i) / (stack - 1);
|
||||
NativeBlockState block = threshold >= decorator.getTopThreshold()
|
||||
? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData())
|
||||
: decorator.getBlockData100(biome, rng, realX, h, realZ, getData());
|
||||
? decorator.getBlockDataForTop(biome, partRNG, realX, h, realZ, getData())
|
||||
: decorator.getBlockData100(biome, partRNG, realX, h, realZ, getData());
|
||||
if (block == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
+4
-4
@@ -51,7 +51,7 @@ public class IrisSeaSurfaceDecorator extends IrisEngineDecorator {
|
||||
if (!decorator.isStacking()) {
|
||||
int targetY = height + 1;
|
||||
if (DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) {
|
||||
NativeBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData());
|
||||
NativeBlockState block = decorator.getBlockData100(biome, partRNG, realX, height, realZ, getData());
|
||||
if (block != null) {
|
||||
data.set(x, targetY, z, block);
|
||||
}
|
||||
@@ -60,7 +60,7 @@ public class IrisSeaSurfaceDecorator extends IrisEngineDecorator {
|
||||
}
|
||||
|
||||
int stack = Math.min(maxBaseY - height,
|
||||
DecoratorCore.computeStack(decorator, rng, realX, realZ, getData(), max - height));
|
||||
DecoratorCore.computeStack(decorator, partRNG, realX, realZ, getData(), max - height));
|
||||
for (int i = 0; i < stack; i++) {
|
||||
int h = height + i;
|
||||
int targetY = h + 1;
|
||||
@@ -69,8 +69,8 @@ public class IrisSeaSurfaceDecorator extends IrisEngineDecorator {
|
||||
}
|
||||
double threshold = stack == 1 ? 1.0 : ((double) i) / (stack - 1);
|
||||
NativeBlockState block = threshold >= decorator.getTopThreshold()
|
||||
? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData())
|
||||
: decorator.getBlockData100(biome, rng, realX, h, realZ, getData());
|
||||
? decorator.getBlockDataForTop(biome, partRNG, realX, h, realZ, getData())
|
||||
: decorator.getBlockData100(biome, partRNG, realX, h, realZ, getData());
|
||||
if (block == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
+5
-5
@@ -102,7 +102,7 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
|
||||
|| !DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) {
|
||||
return;
|
||||
}
|
||||
NativeBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData());
|
||||
NativeBlockState block = decorator.getBlockData100(biome, partRNG, realX, height, realZ, getData());
|
||||
if (block != null && DecoratorCore.isValidShorelineSupport(decorator, block, support)
|
||||
&& IrisSugarCane.canPlace(block, data, x, targetY, z, realX, realZ, getEngine())) {
|
||||
data.set(x, targetY, z, block);
|
||||
@@ -111,7 +111,7 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
|
||||
return;
|
||||
}
|
||||
|
||||
int stack = decorator.getHeight(rng, realX, realZ, getData());
|
||||
int stack = decorator.getHeight(partRNG, realX, realZ, getData());
|
||||
if (decorator.isScaleStack()) {
|
||||
stack = (int) Math.ceil((double) (max - height) * ((double) stack / 100));
|
||||
} else {
|
||||
@@ -124,7 +124,7 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
|
||||
return;
|
||||
}
|
||||
|
||||
NativeBlockState block = decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData());
|
||||
NativeBlockState block = decorator.getBlockDataForTop(biome, partRNG, realX, height, realZ, getData());
|
||||
if (block != null && DecoratorCore.isValidShorelineSupport(decorator, block, support)
|
||||
&& IrisSugarCane.canPlace(block, data, x, targetY, z, realX, realZ, getEngine())) {
|
||||
data.set(x, targetY, z, block);
|
||||
@@ -142,8 +142,8 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
|
||||
}
|
||||
double threshold = ((double) i) / (stack - 1);
|
||||
NativeBlockState block = threshold >= decorator.getTopThreshold()
|
||||
? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData())
|
||||
: decorator.getBlockData100(biome, rng, realX, h, realZ, getData());
|
||||
? decorator.getBlockDataForTop(biome, partRNG, realX, h, realZ, getData())
|
||||
: decorator.getBlockData100(biome, partRNG, realX, h, realZ, getData());
|
||||
if (block == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -86,13 +86,13 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
|
||||
opts.fluidHeight = fluidHeight;
|
||||
opts.caveSkipFluid = caveSkipFluid;
|
||||
opts.mantle = getEngine().getMantle();
|
||||
DecoratorCore.placeStackUp(decorator, x, z, realX, realZ, height, max, data, rng, getData(), opts);
|
||||
DecoratorCore.placeStackUp(decorator, x, z, realX, realZ, height, max, data, partRNG, getData(), opts);
|
||||
aquaticSnapshot.restoreIfUnsupported(data, x, z);
|
||||
return;
|
||||
}
|
||||
|
||||
DecoratorCore.placeSurfaceSingle(decorator, x, z, realX, height, realZ,
|
||||
data, rng, getData(), underwater, caveSkipFluid, getEngine().getMantle());
|
||||
data, partRNG, getData(), underwater, caveSkipFluid, getEngine().getMantle());
|
||||
aquaticSnapshot.restoreIfUnsupported(data, x, z);
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -25,6 +25,7 @@ import art.arcane.volmlib.util.math.Vector3i;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -42,6 +43,8 @@ final class RuinDecoratorApplier {
|
||||
case BASE_SCATTER -> baseScatterCandidates(canvas, Math.max(1, decorator.getScatterRadius()));
|
||||
};
|
||||
|
||||
candidates.sort(Comparator.comparingInt(Vector3i::getX)
|
||||
.thenComparingInt(Vector3i::getY).thenComparingInt(Vector3i::getZ));
|
||||
RNG paletteRng = new RNG(seed);
|
||||
for (Vector3i v : candidates) {
|
||||
if (!rng.chance(decorator.getChance())) {
|
||||
|
||||
@@ -24,7 +24,15 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class TreeBlockCanvas {
|
||||
public record Vec(int x, int y, int z) {
|
||||
public record Vec(int x, int y, int z) implements Comparable<Vec> {
|
||||
@Override
|
||||
public int compareTo(Vec other) {
|
||||
int comparison = Integer.compare(x, other.x);
|
||||
if (comparison == 0) {
|
||||
comparison = Integer.compare(y, other.y);
|
||||
}
|
||||
return comparison == 0 ? Integer.compare(z, other.z) : comparison;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Role {
|
||||
|
||||
+23
-8
@@ -23,6 +23,9 @@ import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -90,7 +93,7 @@ public final class TreeDecoratorApplier {
|
||||
private static void trunkSurface(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) {
|
||||
int[][] sides = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
String[] facings = {"east", "west", "south", "north"};
|
||||
for (TreeBlockCanvas.Vec v : new KList<>(canvas.getTrunk())) {
|
||||
for (TreeBlockCanvas.Vec v : sortedPositions(canvas.getTrunk())) {
|
||||
for (int i = 0; i < sides.length; i++) {
|
||||
int nx = v.x() + sides[i][0];
|
||||
int nz = v.z() + sides[i][1];
|
||||
@@ -105,7 +108,7 @@ public final class TreeDecoratorApplier {
|
||||
private static void leafSurface(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) {
|
||||
int[][] sides = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
String[] facings = {"east", "west", "south", "north"};
|
||||
for (TreeBlockCanvas.Vec v : new KList<>(canvas.getLeaf())) {
|
||||
for (TreeBlockCanvas.Vec v : sortedPositions(canvas.getLeaf())) {
|
||||
for (int i = 0; i < sides.length; i++) {
|
||||
int nx = v.x() + sides[i][0];
|
||||
int nz = v.z() + sides[i][1];
|
||||
@@ -118,7 +121,7 @@ public final class TreeDecoratorApplier {
|
||||
}
|
||||
|
||||
private static void branchSurface(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) {
|
||||
for (TreeBlockCanvas.Vec v : new KList<>(canvas.getTrunk())) {
|
||||
for (TreeBlockCanvas.Vec v : sortedPositions(canvas.getTrunk())) {
|
||||
int ay = v.y() + 1;
|
||||
if (canvas.has(v.x(), ay, v.z()) || rng.nextDouble() > dec.getChance()) {
|
||||
continue;
|
||||
@@ -136,7 +139,7 @@ public final class TreeDecoratorApplier {
|
||||
colTop.put(key, v.y());
|
||||
}
|
||||
}
|
||||
for (Map.Entry<Long, Integer> e : colTop.entrySet()) {
|
||||
for (Map.Entry<Long, Integer> e : sortedColumns(colTop)) {
|
||||
int x = unpackX(e.getKey());
|
||||
int z = unpackZ(e.getKey());
|
||||
int aboveY = e.getValue() + 1;
|
||||
@@ -149,7 +152,7 @@ public final class TreeDecoratorApplier {
|
||||
|
||||
private static void canopyBottom(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) {
|
||||
KMap<Long, Integer> colBottom = columnLeafBottoms(canvas);
|
||||
for (Map.Entry<Long, Integer> e : colBottom.entrySet()) {
|
||||
for (Map.Entry<Long, Integer> e : sortedColumns(colBottom)) {
|
||||
int x = unpackX(e.getKey());
|
||||
int z = unpackZ(e.getKey());
|
||||
int belowY = e.getValue() - 1;
|
||||
@@ -163,7 +166,7 @@ public final class TreeDecoratorApplier {
|
||||
private static void canopyHang(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) {
|
||||
int maxLen = Math.max(1, dec.getLength());
|
||||
KMap<Long, Integer> colBottom = columnLeafBottoms(canvas);
|
||||
for (Map.Entry<Long, Integer> e : colBottom.entrySet()) {
|
||||
for (Map.Entry<Long, Integer> e : sortedColumns(colBottom)) {
|
||||
if (rng.nextDouble() > dec.getChance()) {
|
||||
continue;
|
||||
}
|
||||
@@ -182,7 +185,7 @@ public final class TreeDecoratorApplier {
|
||||
|
||||
private static void trunkBase(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) {
|
||||
int[][] sides = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
|
||||
for (TreeBlockCanvas.Vec v : new KList<>(canvas.getTrunk())) {
|
||||
for (TreeBlockCanvas.Vec v : sortedPositions(canvas.getTrunk())) {
|
||||
if (v.y() != 0) {
|
||||
continue;
|
||||
}
|
||||
@@ -205,7 +208,7 @@ public final class TreeDecoratorApplier {
|
||||
if (maxY == Integer.MIN_VALUE) {
|
||||
return;
|
||||
}
|
||||
for (TreeBlockCanvas.Vec v : new KList<>(canvas.getTrunk())) {
|
||||
for (TreeBlockCanvas.Vec v : sortedPositions(canvas.getTrunk())) {
|
||||
if (v.y() != maxY) {
|
||||
continue;
|
||||
}
|
||||
@@ -269,6 +272,18 @@ public final class TreeDecoratorApplier {
|
||||
return colBottom;
|
||||
}
|
||||
|
||||
private static List<TreeBlockCanvas.Vec> sortedPositions(Collection<TreeBlockCanvas.Vec> positions) {
|
||||
List<TreeBlockCanvas.Vec> sorted = new ArrayList<>(positions);
|
||||
sorted.sort(null);
|
||||
return sorted;
|
||||
}
|
||||
|
||||
private static List<Map.Entry<Long, Integer>> sortedColumns(Map<Long, Integer> columns) {
|
||||
List<Map.Entry<Long, Integer>> sorted = new ArrayList<>(columns.entrySet());
|
||||
sorted.sort(Comparator.comparingLong(Map.Entry::getKey));
|
||||
return sorted;
|
||||
}
|
||||
|
||||
private static String facingAway(int cx, int cz, int x, int z) {
|
||||
int dx = x - cx;
|
||||
int dz = z - cz;
|
||||
|
||||
@@ -44,15 +44,13 @@ public final class TreeSupport {
|
||||
int best = Integer.MAX_VALUE;
|
||||
for (TreeBlockCanvas.Vec wood : canvas.getTrunk()) {
|
||||
int cd = chebyshev(leaf, wood);
|
||||
if (cd < best) {
|
||||
if (cd < best || cd == best && nearest != null && wood.compareTo(nearest) < 0) {
|
||||
best = cd;
|
||||
nearest = wood;
|
||||
if (cd <= 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nearest != null && best > worstGap) {
|
||||
if (nearest != null && (best > worstGap
|
||||
|| best == worstGap && worstLeaf != null && leaf.compareTo(worstLeaf) < 0)) {
|
||||
worstGap = best;
|
||||
worstLeaf = leaf;
|
||||
worstWood = nearest;
|
||||
|
||||
@@ -29,6 +29,7 @@ public final class CaveTerrainSnapshot {
|
||||
private final MatterCavern[][] caverns;
|
||||
private final HydrologyCaveCell[][] hydrology;
|
||||
private boolean hydrologyPresent;
|
||||
private volatile long retainedBytes;
|
||||
|
||||
private CaveTerrainSnapshot(int chunkX, int chunkZ, int sections) {
|
||||
this.chunkX = chunkX;
|
||||
@@ -63,6 +64,36 @@ public final class CaveTerrainSnapshot {
|
||||
return hydrologyPresent;
|
||||
}
|
||||
|
||||
public long estimatedRetainedBytes() {
|
||||
long cached = retainedBytes;
|
||||
if (cached != 0L) {
|
||||
return cached;
|
||||
}
|
||||
long bytes = 80L + ((long) caverns.length + hydrology.length) * 8L;
|
||||
for (MatterCavern[] section : caverns) {
|
||||
if (section != null) {
|
||||
bytes += 16L + (long) section.length * 8L;
|
||||
for (MatterCavern cell : section) {
|
||||
if (cell != null) {
|
||||
bytes += 96L + (long) cell.getCustomBiome().length() * 2L;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (HydrologyCaveCell[] section : hydrology) {
|
||||
if (section != null) {
|
||||
bytes += 16L + (long) section.length * 8L;
|
||||
for (HydrologyCaveCell cell : section) {
|
||||
if (cell != null) {
|
||||
bytes += 208L + ((long) cell.fluidProfileKey().length() + cell.floodedBiomeKey().length()) * 2L;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
retainedBytes = bytes;
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public MatterCavern cavern(int x, int y, int z) {
|
||||
if (y < 0) {
|
||||
return null;
|
||||
|
||||
@@ -28,6 +28,7 @@ import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.EngineTarget;
|
||||
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.generation.terrain.Terrain3DColumn;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveStorage;
|
||||
import art.arcane.iris.pack.value.IrisPosition;
|
||||
@@ -108,6 +109,12 @@ public interface EngineMantle extends MatterGenerator {
|
||||
}
|
||||
|
||||
default int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
|
||||
IrisComplex complex = getComplex();
|
||||
int transformed = complex == null || complex.getTerrainTransform() == null ? Integer.MIN_VALUE
|
||||
: complex.transformedHeight(x, z, ignoreFluid);
|
||||
if (transformed != Integer.MIN_VALUE) {
|
||||
return transformed;
|
||||
}
|
||||
return ignoreFluid ? trueHeight(x, z) : Math.max(trueHeight(x, z), getFluidHeight(x, z));
|
||||
}
|
||||
|
||||
@@ -116,6 +123,10 @@ public interface EngineMantle extends MatterGenerator {
|
||||
}
|
||||
|
||||
default boolean isCarved(int x, int h, int z) {
|
||||
Terrain3DColumn transformed = getComplex().transformedColumn(x, z);
|
||||
if (transformed != null) {
|
||||
return h >= 0 && h < transformed.topY() && !transformed.isSolid(h);
|
||||
}
|
||||
HydrologyCaveCell hydrology = HydrologyCaveStorage.getIfPresent(getMantle(), x, h, z);
|
||||
if (hydrology != null) {
|
||||
return hydrology.carves();
|
||||
@@ -142,6 +153,12 @@ public interface EngineMantle extends MatterGenerator {
|
||||
}
|
||||
|
||||
default int getFluidHeight(int x, int z) {
|
||||
IrisComplex complex = getComplex();
|
||||
int transformed = complex == null || complex.getTerrainTransform() == null ? Integer.MIN_VALUE
|
||||
: complex.transformedFluidHeight(x, z);
|
||||
if (transformed != Integer.MIN_VALUE) {
|
||||
return transformed;
|
||||
}
|
||||
return (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(x, z));
|
||||
}
|
||||
|
||||
|
||||
@@ -582,6 +582,10 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
}
|
||||
|
||||
public boolean isPrerequisiteCarved(int x, int y, int z) {
|
||||
Terrain3DColumn transformed = engineMantle.getComplex().transformedColumn(x, z);
|
||||
if (transformed != null) {
|
||||
return y >= 0 && y < transformed.topY() && !transformed.isSolid(y);
|
||||
}
|
||||
HydrologyCaveCell hydrology = getPrerequisiteDataIfPresent(x, y, z, HydrologyCaveCell.class);
|
||||
if (hydrology != null) {
|
||||
return hydrology.carves();
|
||||
@@ -592,6 +596,10 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
|
||||
public byte[] getPrerequisiteCarvedColumn(int x, int z, int height) {
|
||||
int cappedHeight = Math.min(Math.max(height, 0), mantle.getWorldHeight());
|
||||
Terrain3DColumn transformed = engineMantle.getComplex().transformedColumn(x, z);
|
||||
if (transformed != null) {
|
||||
return carvedColumn(transformed, cappedHeight);
|
||||
}
|
||||
byte[] carvedColumn = new byte[cappedHeight];
|
||||
if (cappedHeight <= 0) {
|
||||
return carvedColumn;
|
||||
@@ -961,6 +969,10 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
|
||||
@Override
|
||||
public boolean isCarved(int x, int y, int z) {
|
||||
Terrain3DColumn transformed = engineMantle.getComplex().transformedColumn(x, z);
|
||||
if (transformed != null) {
|
||||
return y >= 0 && y < transformed.topY() && !transformed.isSolid(y);
|
||||
}
|
||||
HydrologyCaveCell hydrology = getDataIfPresent(x, y, z, HydrologyCaveCell.class);
|
||||
if (hydrology != null) {
|
||||
return hydrology.carves();
|
||||
@@ -972,6 +984,10 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
|
||||
public byte[] getCarvedColumn(int x, int z, int height) {
|
||||
int cappedHeight = Math.min(Math.max(height, 0), mantle.getWorldHeight());
|
||||
Terrain3DColumn transformed = engineMantle.getComplex().transformedColumn(x, z);
|
||||
if (transformed != null) {
|
||||
return carvedColumn(transformed, cappedHeight);
|
||||
}
|
||||
byte[] carvedColumn = new byte[cappedHeight];
|
||||
if (cappedHeight <= 0) {
|
||||
return carvedColumn;
|
||||
@@ -1589,6 +1605,17 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] carvedColumn(Terrain3DColumn terrain, int height) {
|
||||
byte[] result = new byte[height];
|
||||
int maximumY = Math.min(height, terrain.topY());
|
||||
for (int y = 0; y < maximumY; y++) {
|
||||
if (!terrain.isSolid(y)) {
|
||||
result[y] = 1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public final class ObjectPlacementCapture implements AutoCloseable {
|
||||
private final ObjectContinuationBundle.PlacementKey key;
|
||||
private final Long2ObjectOpenHashMap<Matter> fragments = new Long2ObjectOpenHashMap<>();
|
||||
|
||||
@@ -31,6 +31,7 @@ import art.arcane.iris.generation.stage.IrisDepositModifier;
|
||||
import art.arcane.iris.generation.stage.IrisFloatingChildBiomeModifier;
|
||||
import art.arcane.iris.generation.stage.IrisPerfectionModifier;
|
||||
import art.arcane.iris.generation.stage.IrisPostModifier;
|
||||
import art.arcane.iris.generation.context.ChunkContext;
|
||||
|
||||
public class ModeOverworld extends IrisEngineMode implements EngineMode {
|
||||
public ModeOverworld(Engine engine) {
|
||||
@@ -45,7 +46,7 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
|
||||
IrisCustomModifier custom = new IrisCustomModifier(getEngine());
|
||||
IrisFloatingChildBiomeModifier floatingChildBiomes = new IrisFloatingChildBiomeModifier(getEngine());
|
||||
EngineStage sBiome = (x, z, k, p, m, c) -> biome.actuate(x, z, p, m, c);
|
||||
EngineStage sGenMatter = (x, z, k, p, m, c) -> generateTerrainMatter(x >> 4, z >> 4, m || getEngine().isStudio(), c);
|
||||
EngineStage sGenMatter = (x, z, k, p, m, c) -> generateTerrainMatterForChunk(x >> 4, z >> 4, m, c);
|
||||
EngineStage sTerrain = (x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c);
|
||||
EngineStage sDecorant = (x, z, k, p, m, c) -> decorant.actuate(x, z, k, m, c);
|
||||
EngineStage sCave = (x, z, k, p, m, c) -> cave.modify(x >> 4, z >> 4, k, m, c);
|
||||
@@ -82,4 +83,10 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
|
||||
registerStage(sCustom);
|
||||
}
|
||||
}
|
||||
|
||||
void generateTerrainMatterForChunk(int x, int z, boolean multicore, ChunkContext context) {
|
||||
boolean studioParallel = getEngine().isStudio()
|
||||
&& !(context.isNaturalTerrain() && context.getComplex().getTerrainTransform() != null);
|
||||
generateTerrainMatter(x, z, multicore || studioParallel, context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@ import com.dfsek.paralithic.eval.parser.Parser;
|
||||
import com.dfsek.paralithic.eval.parser.Scope;
|
||||
import art.arcane.iris.pack.loading.IrisRegistrant;
|
||||
import art.arcane.volmlib.util.cache.AtomicCache;
|
||||
import art.arcane.iris.generation.cache.LazyBoundedCache;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import art.arcane.iris.generation.noise.IrisExpressionFunction.FunctionContext;
|
||||
import art.arcane.iris.pack.schema.annotation.ArrayType;
|
||||
import art.arcane.volmlib.util.documentation.Description;
|
||||
@@ -58,7 +62,9 @@ public class IrisExpression extends IrisRegistrant {
|
||||
private String expression;
|
||||
|
||||
private transient AtomicCache<Expression> expressionCache = new AtomicCache<>();
|
||||
private transient AtomicCache<ProceduralStream<Double>> streamCache = new AtomicCache<>();
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private final transient LazyBoundedCache<Long, ProceduralStream<Double>> streams = new LazyBoundedCache<>(8);
|
||||
|
||||
private Expression expression() {
|
||||
return expressionCache.aquire(() -> {
|
||||
@@ -94,8 +100,11 @@ public class IrisExpression extends IrisRegistrant {
|
||||
}
|
||||
|
||||
public ProceduralStream<Double> stream(RNG rng) {
|
||||
return streamCache.aquire(() -> ProceduralStream.of((x, z) -> evaluate(rng, x, z),
|
||||
(x, y, z) -> evaluate(rng, x, y, z), Interpolated.DOUBLE));
|
||||
return streams.computeIfAbsent(rng.getSeed(), seed -> {
|
||||
RNG streamRng = new RNG(seed);
|
||||
return ProceduralStream.of((x, z) -> evaluate(streamRng, x, z),
|
||||
(x, y, z) -> evaluate(streamRng, x, y, z), Interpolated.DOUBLE);
|
||||
});
|
||||
}
|
||||
|
||||
public double evaluate(RNG rng, double x, double z) {
|
||||
|
||||
@@ -10,7 +10,11 @@ import art.arcane.volmlib.util.documentation.Description;
|
||||
import art.arcane.iris.pack.schema.annotation.MinNumber;
|
||||
import art.arcane.iris.pack.schema.annotation.Required;
|
||||
import art.arcane.iris.pack.schema.annotation.Snippet;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.generation.cache.LazyBoundedCache;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
|
||||
import java.util.Objects;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
@@ -48,7 +52,10 @@ public class IrisExpressionFunction implements DynamicFunction {
|
||||
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private transient final KMap<FunctionContext, Provider> cache = new KMap<>();
|
||||
private transient final LazyBoundedCache<FunctionKey, Provider> providers = new LazyBoundedCache<>(8);
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private transient volatile CachedProvider recentProvider;
|
||||
private transient IrisData data;
|
||||
|
||||
public boolean isValid() {
|
||||
@@ -74,19 +81,53 @@ public class IrisExpressionFunction implements DynamicFunction {
|
||||
|
||||
@Override
|
||||
public double eval(@Nullable Context raw, double... args) {
|
||||
return cache.computeIfAbsent((FunctionContext) raw, context -> {
|
||||
assert context != null;
|
||||
if (engineStreamValue != null) {
|
||||
var stream = engineStreamValue.get(data.getEngine());
|
||||
return d -> stream.get(d[0], d[1]);
|
||||
}
|
||||
FunctionContext context = Objects.requireNonNull((FunctionContext) raw, "Expression context");
|
||||
IrisData currentData = data;
|
||||
Engine engine = currentData == null ? null : currentData.getEngine();
|
||||
long seed = context.rng.getSeed();
|
||||
CachedProvider recent = recentProvider;
|
||||
if (recent != null && recent.key.matches(seed, currentData, engine)) {
|
||||
return recent.provider.eval(args);
|
||||
}
|
||||
FunctionKey key = new FunctionKey(seed, currentData, engine);
|
||||
Provider provider = providers.computeIfAbsent(key, ignored -> createProvider(key));
|
||||
recentProvider = new CachedProvider(key, provider);
|
||||
return provider.eval(args);
|
||||
}
|
||||
|
||||
if (styleValue != null) {
|
||||
return styleValue.createNoCache(context.rng, data)::noise;
|
||||
private Provider createProvider(FunctionKey key) {
|
||||
if (engineStreamValue != null) {
|
||||
if (key.engine == null) {
|
||||
throw new IllegalStateException("Expression function '" + name + "' requires an active Iris engine.");
|
||||
}
|
||||
ProceduralStream<Double> stream = engineStreamValue.get(key.engine);
|
||||
return coordinates -> stream.get(coordinates[0], coordinates[1]);
|
||||
}
|
||||
if (styleValue != null) {
|
||||
return styleValue.create(new RNG(key.seed), key.data, key.engine)::noise;
|
||||
}
|
||||
return coordinates -> Double.NaN;
|
||||
}
|
||||
|
||||
return d -> Double.NaN;
|
||||
}).eval(args);
|
||||
private record FunctionKey(long seed, IrisData data, Engine engine) {
|
||||
private boolean matches(long seed, IrisData data, Engine engine) {
|
||||
return this.seed == seed && this.data == data && this.engine == engine;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
return object instanceof FunctionKey other && matches(other.seed, other.data, other.engine);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = Long.hashCode(seed);
|
||||
hash = 31 * hash + System.identityHashCode(data);
|
||||
return 31 * hash + System.identityHashCode(engine);
|
||||
}
|
||||
}
|
||||
|
||||
private record CachedProvider(FunctionKey key, Provider provider) {
|
||||
}
|
||||
|
||||
public record FunctionContext(@NonNull RNG rng) implements Context {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
package art.arcane.iris.generation.noise;
|
||||
|
||||
import art.arcane.iris.pack.loading.IrisRegistrant;
|
||||
import art.arcane.volmlib.util.cache.AtomicCache;
|
||||
import art.arcane.iris.generation.cache.LazyBoundedCache;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.pack.schema.annotation.ArrayType;
|
||||
@@ -54,7 +54,12 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
private static final int SURFACE_CACHE_STRIPES = 16;
|
||||
private static final int SURFACE_CACHE_STRIPE_SIZE = 1024;
|
||||
|
||||
private final transient AtomicCache<CellGenerator> cellGen = new AtomicCache<>();
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private final transient LazyBoundedCache<Long, CellGenerator> cellGenerators = new LazyBoundedCache<>(8);
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private transient volatile CachedCellGenerator recentCellGenerator;
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private transient volatile SurfaceCache surfaceSamples;
|
||||
@@ -115,7 +120,17 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
}
|
||||
|
||||
public CellGenerator getCellGenerator(long seed) {
|
||||
return cellGen.aquire(() -> new CellGenerator(new RNG(seed + 239466)));
|
||||
CachedCellGenerator recent = recentCellGenerator;
|
||||
if (recent != null && recent.seed == seed) {
|
||||
return recent.generator;
|
||||
}
|
||||
CellGenerator generator = cellGenerators.computeIfAbsent(seed, ignored -> {
|
||||
CellGenerator created = new CellGenerator(new RNG(seed + 239466));
|
||||
created.setShuffle(getCellFractureShuffle());
|
||||
return created;
|
||||
});
|
||||
recentCellGenerator = new CachedCellGenerator(seed, generator);
|
||||
return generator;
|
||||
}
|
||||
|
||||
public <T> T fit(T[] v, long superSeed, double rx, double rz) {
|
||||
@@ -296,7 +311,6 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
}
|
||||
|
||||
public double cell(double rx, double rz, double v, double superSeed) {
|
||||
getCellGenerator(getSeed() + 46222).setShuffle(getCellFractureShuffle());
|
||||
double fractureX = rx / getCellFractureZoom();
|
||||
double fractureZ = rz / getCellFractureZoom();
|
||||
return getCellGenerator(getSeed() + 46222).getDistance(fractureX, fractureZ) > getCellPercentSize() ? (v * getCellFractureHeight()) : v;
|
||||
@@ -363,4 +377,7 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
entries.set(slot, sample);
|
||||
}
|
||||
}
|
||||
|
||||
private record CachedCellGenerator(long seed, CellGenerator generator) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,9 @@ public class IrisGeneratorStyle {
|
||||
@Setter(AccessLevel.NONE)
|
||||
private final transient LazyBoundedCache<GeneratorCacheKey, CNG> generatorCache =
|
||||
new LazyBoundedCache<>(GENERATOR_CACHE_SIZE);
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private transient volatile CachedGenerator recentGenerator;
|
||||
@Description("The base noise style. Used when neither expression nor imageMap is set; a failed expression also falls back to this style.")
|
||||
private NoiseStyle style = NoiseStyle.FLAT;
|
||||
|
||||
@@ -102,6 +105,10 @@ public class IrisGeneratorStyle {
|
||||
return createNoCache(rng, data, 1, 0, false, resolveEngine(data));
|
||||
}
|
||||
|
||||
public CNG createScaledGenerator(RNG rng, IrisData data, double scaleFactor) {
|
||||
return createNoCache(rng, data).bake().scale(scaleFactor).bake();
|
||||
}
|
||||
|
||||
public CNG createForLayer(RNG rng, IrisData data, int octaveMultiplier) {
|
||||
return createNoCache(rng, data, Math.clamp(octaveMultiplier, 1, 16), 0, false, resolveEngine(data));
|
||||
}
|
||||
@@ -120,7 +127,7 @@ public class IrisGeneratorStyle {
|
||||
}
|
||||
|
||||
private int hash() {
|
||||
return Objects.hash(expression, imageMapHash(), multiplier, fracture != null ? fracture.hash() : 0, exponent, cacheSize, zoom, cellularZoom, cellularFrequency, style);
|
||||
return Objects.hash(expression, imageMapHash(), multiplier, fracture != null ? fracture.hash() : 0, exponent, cacheSize, zoom, cellularZoom, cellularFrequency, style == null ? null : style.name());
|
||||
}
|
||||
|
||||
public int prebakeSignature() {
|
||||
@@ -229,10 +236,24 @@ public class IrisGeneratorStyle {
|
||||
return create(rng, data, resolveEngine(data));
|
||||
}
|
||||
|
||||
public CNG create(long seed, IrisData data) {
|
||||
return cachedGenerator(seed, data, resolveEngine(data));
|
||||
}
|
||||
|
||||
public CNG create(RNG rng, IrisData data, Engine engine) {
|
||||
GeneratorCacheKey key = new GeneratorCacheKey(data, engine, rng.getSeed());
|
||||
return generatorCache.computeIfAbsent(key,
|
||||
ignored -> createNoCache(rng, data, 1, 0, false, engine));
|
||||
return cachedGenerator(rng.getSeed(), data, engine);
|
||||
}
|
||||
|
||||
private CNG cachedGenerator(long seed, IrisData data, Engine engine) {
|
||||
CachedGenerator recent = recentGenerator;
|
||||
if (recent != null && recent.key.matches(data, engine, seed)) {
|
||||
return recent.generator;
|
||||
}
|
||||
GeneratorCacheKey key = new GeneratorCacheKey(data, engine, seed);
|
||||
CNG generator = generatorCache.computeIfAbsent(key,
|
||||
ignored -> createNoCache(new RNG(seed), data, 1, 0, false, engine));
|
||||
recentGenerator = new CachedGenerator(key, generator);
|
||||
return generator;
|
||||
}
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
@@ -291,6 +312,10 @@ public class IrisGeneratorStyle {
|
||||
this.seed = seed;
|
||||
}
|
||||
|
||||
private boolean matches(IrisData data, Engine engine, long seed) {
|
||||
return this.data == data && this.engine == engine && this.seed == seed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (this == other) {
|
||||
@@ -309,4 +334,7 @@ public class IrisGeneratorStyle {
|
||||
return 31 * result + Long.hashCode(seed);
|
||||
}
|
||||
}
|
||||
|
||||
private record CachedGenerator(GeneratorCacheKey key, CNG generator) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import art.arcane.iris.generation.stream.GenerationStreams;
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.generation.image.IrisImageMapRuntime;
|
||||
import art.arcane.iris.generation.terrain.ContinentGenerator;
|
||||
import art.arcane.iris.generation.terrain.InferredType;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
@@ -125,8 +126,8 @@ public final class DimensionTerrainContext implements DataProvider {
|
||||
engine.getDimension(),
|
||||
engine.getData(),
|
||||
engine.getHeight(),
|
||||
natural ? complex.getNaturalHeightStream() : complex.getHeightStream(),
|
||||
natural ? complex.getNaturalSlopeStream() : complex.getSlopeStream(),
|
||||
natural ? complex.getNaturalHeightStream() : complex.getRawHeightStream(),
|
||||
natural ? complex.getNaturalSlopeStream() : complex.getRawSlopeStream(),
|
||||
fluidHeightStream,
|
||||
natural ? complex.getNaturalTrueBiomeStream() : complex.getTrueBiomeStream(),
|
||||
complex.getRegionStream(),
|
||||
@@ -258,9 +259,7 @@ public final class DimensionTerrainContext implements DataProvider {
|
||||
inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
|
||||
|
||||
ProceduralStream<InferredType> bridgeStream = focusBiome == null
|
||||
? dimension.getContinentalStyle()
|
||||
.create(rng.nextParallelRNG(234234565), dimensionData)
|
||||
.bake().scale(1D / dimension.getContinentZoom()).bake().stream()
|
||||
? ContinentGenerator.create(dimension, rng, dimensionData).stream()
|
||||
.convert(value -> value >= dimension.getLandChance()
|
||||
? InferredType.SEA
|
||||
: InferredType.LAND)
|
||||
@@ -653,7 +652,7 @@ public final class DimensionTerrainContext implements DataProvider {
|
||||
if (selfReferencing) {
|
||||
return naturalSelf || naturalFallback
|
||||
? engine.getComplex().naturalTerrainColumn(x, z)
|
||||
: engine.getComplex().terrainColumn(x, z);
|
||||
: engine.getComplex().originalTerrainColumn(x, z);
|
||||
}
|
||||
if (terrain3D == null) {
|
||||
return null;
|
||||
|
||||
@@ -567,6 +567,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
/** The terrain slope at a column, answered from natural terrain when the server thread may not wait. */
|
||||
@BlockCoordinates
|
||||
default double getSlope(int x, int z) {
|
||||
IrisComplex complex = getComplex();
|
||||
if (complex != null && complex.getTerrainTransform() != null) {
|
||||
return complex.getSlopeStream().getDouble(x, z);
|
||||
}
|
||||
if (answersFromNaturalTerrain(x, z)) {
|
||||
return getComplex().naturalSlope(x, z);
|
||||
}
|
||||
@@ -580,6 +584,12 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
@BlockCoordinates
|
||||
default int getHeight(int x, int z, boolean ignoreFluid) {
|
||||
IrisComplex complex = getComplex();
|
||||
int transformed = complex == null || complex.getTerrainTransform() == null ? Integer.MIN_VALUE
|
||||
: complex.transformedHeight(x, z, ignoreFluid);
|
||||
if (transformed != Integer.MIN_VALUE) {
|
||||
return transformed;
|
||||
}
|
||||
DimensionStackContext dimensionStackContext = getDimensionStackContext();
|
||||
if (dimensionStackContext != null) {
|
||||
return ignoreFluid
|
||||
@@ -595,6 +605,12 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
@BlockCoordinates
|
||||
static int hostHeight(Engine engine, int x, int z, boolean ignoreFluid) {
|
||||
IrisComplex complex = engine.getComplex();
|
||||
int transformed = complex == null || complex.getTerrainTransform() == null ? Integer.MIN_VALUE
|
||||
: complex.transformedHeight(x, z, ignoreFluid);
|
||||
if (transformed != Integer.MIN_VALUE) {
|
||||
return transformed;
|
||||
}
|
||||
EngineMantle mantle = engine.getMantle();
|
||||
if (mantle == null) {
|
||||
return engine.getHeight(x, z, ignoreFluid);
|
||||
|
||||
@@ -213,7 +213,11 @@ public interface EngineMode extends Staged {
|
||||
|
||||
EngineStage[] stages = getStages().toArray(new EngineStage[0]);
|
||||
try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), generationSessionId, ctx)) {
|
||||
generateTerrain(x, z, blocks, biomes, multicore, ctx);
|
||||
if (getComplex().getTerrainTransform() == null) {
|
||||
generateTerrain(x, z, blocks, biomes, multicore, ctx);
|
||||
} else {
|
||||
getComplex().getTerrainTransform().generate(x, z, blocks, biomes, multicore, ctx);
|
||||
}
|
||||
recordNaturalTerrain(x, z, blocks, biomes, ctx);
|
||||
IrisDimensionStackActuator.clearHostNaturalMetadata(getEngine(), x, z, biomes.getHeight(), ctx);
|
||||
ctx.beginContent();
|
||||
|
||||
@@ -52,7 +52,12 @@ public record EngineTelemetrySnapshot(
|
||||
generationTimingsMs = sanitizeTimings(generationTimingsMs);
|
||||
}
|
||||
|
||||
public static EngineTelemetrySnapshot capture(Engine engine, double chunksPerSecond, long sampledAtMs) {
|
||||
public static EngineTelemetrySnapshot capture(
|
||||
Engine engine,
|
||||
double chunksPerSecond,
|
||||
boolean generationTimingsFresh,
|
||||
long sampledAtMs
|
||||
) {
|
||||
if (engine == null) {
|
||||
throw new IllegalArgumentException("Engine cannot be null");
|
||||
}
|
||||
@@ -85,7 +90,7 @@ public record EngineTelemetrySnapshot(
|
||||
engine.getMantle().getLoadedRegionCount(),
|
||||
engine.getMantle().getUnloadRegionCount(),
|
||||
engine.getMantle().getAdjustedIdleDuration(),
|
||||
engine.getMetrics().telemetryAverages()
|
||||
generationTimingsFresh ? engine.getMetrics().telemetryAverages() : Map.of()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ package art.arcane.iris.generation.runtime;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.decoration.IrisDecorator;
|
||||
import art.arcane.iris.generation.decoration.IrisEngineDecorator;
|
||||
import art.arcane.iris.structure.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.generation.decoration.IrisOreGenerator;
|
||||
import art.arcane.iris.generation.decoration.IrisProceduralObjects;
|
||||
@@ -47,8 +48,8 @@ public final class GenerationCacheWarmer {
|
||||
biomes.sort(Comparator.comparing(IrisBiome::getLoadKey));
|
||||
for (IrisBiome biome : biomes) {
|
||||
warmPlacements(biome.getObjects(), root, counter, data, engine);
|
||||
warmDecorators(biome.getDecorators(), root, counter, data);
|
||||
warmOres(biome.getOres(), root, counter, data);
|
||||
warmDecorators(biome.getDecorators(), engine.getSeedManager().getComponent(), counter, data);
|
||||
warmOres(biome.getOres(), engine.getSeedManager().getTerrain(), counter, data);
|
||||
warmProcedural(biome.getProceduralObjects(), root, counter, data);
|
||||
}
|
||||
|
||||
@@ -56,11 +57,11 @@ public final class GenerationCacheWarmer {
|
||||
regions.sort(Comparator.comparing(IrisRegion::getLoadKey));
|
||||
for (IrisRegion region : regions) {
|
||||
warmPlacements(region.getObjects(), root, counter, data, engine);
|
||||
warmOres(region.getOres(), root, counter, data);
|
||||
warmOres(region.getOres(), engine.getSeedManager().getTerrain(), counter, data);
|
||||
warmProcedural(region.getProceduralObjects(), root, counter, data);
|
||||
}
|
||||
|
||||
warmOres(engine.getDimension().getOres(), root, counter, data);
|
||||
warmOres(engine.getDimension().getOres(), engine.getSeedManager().getTerrain(), counter, data);
|
||||
|
||||
IrisLogging.debug("[IrisEngine timing] cache warm " + counter[0] + " configs=" + (M.ms() - start) + "ms");
|
||||
}
|
||||
@@ -80,7 +81,7 @@ public final class GenerationCacheWarmer {
|
||||
}
|
||||
}
|
||||
|
||||
private static void warmDecorators(KList<IrisDecorator> decorators, RNG root, int[] counter, IrisData data) {
|
||||
private static void warmDecorators(KList<IrisDecorator> decorators, long componentSeed, int[] counter, IrisData data) {
|
||||
if (decorators == null) {
|
||||
return;
|
||||
}
|
||||
@@ -88,14 +89,15 @@ public final class GenerationCacheWarmer {
|
||||
if (decorator == null) {
|
||||
continue;
|
||||
}
|
||||
RNG rng = root.nextParallelRNG(counter[0]++);
|
||||
counter[0]++;
|
||||
RNG rng = new RNG(IrisEngineDecorator.seedForPart(componentSeed, decorator.getPartOf()));
|
||||
decorator.getHeightGenerator(rng, data);
|
||||
decorator.getGenerator(rng, data);
|
||||
decorator.getVarianceGenerator(rng, data);
|
||||
}
|
||||
}
|
||||
|
||||
private static void warmOres(KList<IrisOreGenerator> ores, RNG root, int[] counter, IrisData data) {
|
||||
private static void warmOres(KList<IrisOreGenerator> ores, long terrainSeed, int[] counter, IrisData data) {
|
||||
if (ores == null) {
|
||||
return;
|
||||
}
|
||||
@@ -103,7 +105,8 @@ public final class GenerationCacheWarmer {
|
||||
if (ore == null) {
|
||||
continue;
|
||||
}
|
||||
ore.warm(root.nextParallelRNG(counter[0]++), data);
|
||||
counter[0]++;
|
||||
ore.warm(new RNG(terrainSeed), data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,8 +52,13 @@ import art.arcane.iris.generation.decoration.IrisDecorationPart;
|
||||
import art.arcane.iris.generation.decoration.IrisDecorator;
|
||||
import art.arcane.iris.generation.noise.IrisGenerator;
|
||||
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||
import art.arcane.iris.generation.terrain.ContinentGenerator;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.generation.context.IrisContext;
|
||||
import art.arcane.iris.generation.context.ChunkContext;
|
||||
import art.arcane.iris.generation.terrain.transform.TerrainTransformRegistry;
|
||||
import art.arcane.iris.generation.terrain.transform.TerrainTransformRuntime;
|
||||
import art.arcane.iris.generation.terrain.transform.TerrainTransformer;
|
||||
import art.arcane.iris.generation.noise.IrisInterpolator;
|
||||
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
@@ -185,6 +190,20 @@ public class IrisComplex implements DataProvider {
|
||||
private final ProceduralTerrainHeightSampler proceduralTerrainHeight;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient Engine terrainEngine;
|
||||
@Setter(AccessLevel.NONE)
|
||||
private final transient TerrainTransformRuntime terrainTransform;
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private final transient ProceduralStream<Double> transformedHeightStream;
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private final transient ProceduralStream<Integer> transformedRoundedHeightStream;
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private final transient ProceduralStream<Double> transformedTopStream;
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private final transient ProceduralStream<Double> transformedSlopeStream;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private transient Terrain3DRuntime terrain3D;
|
||||
private transient HydrologyBankTerrainRuntime hydrologyBanks3D;
|
||||
@@ -372,8 +391,7 @@ public class IrisComplex implements DataProvider {
|
||||
.convertAware2D(ProceduralStream::get), "shoreBiomeStream", engine, cacheSize);
|
||||
inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
|
||||
bridgeStream = focusedBiomes != null ? focusedBiomes.convert(IrisBiome::getInferredType) :
|
||||
GenerationStreams.cache2D(engine.getDimension().getContinentalStyle().create(rng.nextParallelRNG(234234565), getData())
|
||||
.bake().scale(1D / engine.getDimension().getContinentZoom()).bake().stream()
|
||||
GenerationStreams.cache2D(ContinentGenerator.create(engine.getDimension(), rng, getData()).stream()
|
||||
.convert((v) -> v >= engine.getDimension().getLandChance() ? InferredType.SEA : InferredType.LAND), "bridgeStream", engine, cacheSize);
|
||||
ProceduralStream<IrisBiome> proceduralBaseBiomeStream = focusedBiomes != null ? focusedBiomes :
|
||||
GenerationStreams.cache2D(bridgeStream.convertAware2D((t, x, z) -> inferredStreams.get(t).get(x, z))
|
||||
@@ -494,6 +512,25 @@ public class IrisComplex implements DataProvider {
|
||||
d.hashCode());
|
||||
}), "", engine, cacheSize);
|
||||
//@done
|
||||
TerrainTransformer transformer = engine.getDimension().getTerrainTransform() == null ? null
|
||||
: TerrainTransformRegistry.discover().resolve(engine.getDimension().getTerrainTransform());
|
||||
terrainTransform = transformer == null ? null
|
||||
: new TerrainTransformRuntime(new TerrainTransformRuntime.Options(engine, this), transformer);
|
||||
transformedHeightStream = transformer == null ? null : ProceduralStream.ofDouble((x, z) -> {
|
||||
int transformed = transformedHeight((int) Math.floor(x), (int) Math.floor(z), true);
|
||||
return transformed == Integer.MIN_VALUE ? heightStream.getDouble(x, z) : transformed;
|
||||
});
|
||||
transformedRoundedHeightStream = transformer == null ? null : ProceduralStream.ofDouble((x, z) -> {
|
||||
int transformed = transformedHeight((int) Math.floor(x), (int) Math.floor(z), true);
|
||||
return transformed == Integer.MIN_VALUE ? roundedHeighteightStream.get(x, z) : transformed;
|
||||
}).round();
|
||||
transformedTopStream = transformer == null ? null : ProceduralStream.ofDouble((x, z) -> {
|
||||
int transformed = transformedHeight((int) Math.floor(x), (int) Math.floor(z), false);
|
||||
return transformed == Integer.MIN_VALUE ? heightFluidStream.getDouble(x, z) : transformed;
|
||||
});
|
||||
ProceduralStream<Double> effectiveSlope = transformer == null ? null : transformedHeightStream.slope(3);
|
||||
transformedSlopeStream = transformer == null ? null : ProceduralStream.ofDouble((x, z) ->
|
||||
isNaturalTerrainContext() ? slopeStream.getDouble(x, z) : effectiveSlope.getDouble(x, z));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -689,6 +726,14 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
|
||||
public Terrain3DColumn terrainColumn(int x, int z) {
|
||||
Terrain3DColumn transformed = transformedColumn(x, z);
|
||||
if (transformed != null) {
|
||||
return transformed;
|
||||
}
|
||||
return originalTerrainColumn(x, z);
|
||||
}
|
||||
|
||||
Terrain3DColumn originalTerrainColumn(int x, int z) {
|
||||
if (!hasTerrain3D()) {
|
||||
return null;
|
||||
}
|
||||
@@ -1204,7 +1249,70 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
|
||||
public ProceduralStream<Double> getHeightStream() {
|
||||
return heightStream;
|
||||
return terrainTransform == null ? heightStream : transformedHeightStream;
|
||||
}
|
||||
|
||||
ProceduralStream<Double> getRawSlopeStream() {
|
||||
return terrainEngine.getDimension().getTerrainTransform() == null ? slopeStream : heightStream.slope(3);
|
||||
}
|
||||
|
||||
public ProceduralStream<Integer> getRoundedHeighteightStream() {
|
||||
return terrainTransform == null ? roundedHeighteightStream : transformedRoundedHeightStream;
|
||||
}
|
||||
|
||||
public ProceduralStream<Double> getHeightFluidStream() {
|
||||
return terrainTransform == null ? heightFluidStream : transformedTopStream;
|
||||
}
|
||||
|
||||
public ProceduralStream<Double> getSlopeStream() {
|
||||
return terrainTransform == null ? slopeStream : transformedSlopeStream;
|
||||
}
|
||||
|
||||
public int transformedHeight(int x, int z, boolean ignoreFluid) {
|
||||
if (terrainTransform == null || isNaturalTerrainContext()) {
|
||||
return Integer.MIN_VALUE;
|
||||
}
|
||||
return readsReadyTerrainOnly()
|
||||
? terrainTransform.readyHeight(x, z, ignoreFluid)
|
||||
: terrainTransform.height(x, z, ignoreFluid);
|
||||
}
|
||||
|
||||
public Terrain3DColumn transformedColumn(int x, int z) {
|
||||
if (terrainTransform == null || isNaturalTerrainContext()) {
|
||||
return null;
|
||||
}
|
||||
return readsReadyTerrainOnly()
|
||||
? terrainTransform.readyColumn(x, z) : terrainTransform.column(x, z);
|
||||
}
|
||||
|
||||
public int transformedFluidHeight(int x, int z) {
|
||||
if (terrainTransform == null || isNaturalTerrainContext()) {
|
||||
return Integer.MIN_VALUE;
|
||||
}
|
||||
return readsReadyTerrainOnly()
|
||||
? terrainTransform.readyFluidHeight(x, z) : terrainTransform.fluidHeight(x, z);
|
||||
}
|
||||
|
||||
private boolean readsReadyTerrainOnly() {
|
||||
if (!terrainEngine.getPlatformHooks().isMainThread()) {
|
||||
return false;
|
||||
}
|
||||
IrisContext context = IrisContext.get();
|
||||
if (context == null || context.getEngine() != terrainEngine) {
|
||||
return true;
|
||||
}
|
||||
ChunkContext chunk = context.getChunkContext();
|
||||
if (chunk != null) {
|
||||
return chunk.getComplex() != this || chunk.isNaturalTerrain();
|
||||
}
|
||||
if (context.getGenerationSessionId() == 0L || terrainEngine.getComplex() != this) {
|
||||
return true;
|
||||
}
|
||||
if (terrainEngine instanceof IrisEngine irisEngine && irisEngine.threadState.assembly() != null
|
||||
&& irisEngine.threadState.assembly().mode == null) {
|
||||
return true;
|
||||
}
|
||||
return terrainEngine.getMode() == null;
|
||||
}
|
||||
|
||||
boolean isNaturalTerrainContext() {
|
||||
@@ -2144,6 +2252,9 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (terrainTransform != null) {
|
||||
terrainTransform.close();
|
||||
}
|
||||
if (hydrologyBanks3D != null) {
|
||||
hydrologyBanks3D.clear();
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ public class UpperDimensionContext implements DataProvider {
|
||||
private UpperDimensionContext(DimensionTerrainContext terrainContext, Engine engine) {
|
||||
this.terrainContext = terrainContext;
|
||||
IrisComplex complex = engine.getComplex();
|
||||
lowerHeightStream = complex.getHeightStream();
|
||||
lowerHeightStream = complex.getRawHeightStream();
|
||||
ceilingLayout = new CeilingLayout(engine.getHeight(), engine.getDimension().getUpperDimensionGap());
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ import art.arcane.iris.world.entity.IrisEntitySpawn;
|
||||
import art.arcane.iris.pack.value.IrisPosition;
|
||||
import art.arcane.iris.world.entity.IrisSpawner;
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.platform.bukkit.BukkitEntityType;
|
||||
import art.arcane.iris.world.entity.IrisEntity;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.platform.bukkit.plugin.Chunks;
|
||||
import art.arcane.iris.world.task.J;
|
||||
@@ -37,13 +39,16 @@ import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.math.PowerOfTwoCoordinates;
|
||||
import art.arcane.volmlib.util.math.Position2;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import lombok.Data;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.GameRules;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import art.arcane.iris.generation.runtime.MarkerSpawnScanner.PreparedMarkerSpawn;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -381,6 +386,9 @@ final class WorldEntitySpawner {
|
||||
}
|
||||
|
||||
private void spawnAmbient(Chunk c, boolean initial, BiomeEnvironment environment) {
|
||||
if (!ambientAllowed(c, initial)) {
|
||||
return;
|
||||
}
|
||||
//@builder
|
||||
// Excluded spawners cannot spawn anything on this Minecraft version, so they never enter the selection pool.
|
||||
Predicate<IrisSpawner> filter = i -> !i.isCompatExcluded() && i.canSpawn(manager.getEngine(), c.getX(), c.getZ());
|
||||
@@ -404,20 +412,25 @@ final class WorldEntitySpawner {
|
||||
.shuffleCopy(RNG.r)
|
||||
.stream()
|
||||
.filter(filter)))
|
||||
.filter(counter)
|
||||
.flatMap((i) -> stream(i, initial))
|
||||
.filter(entry -> counter.remainingCapacity(entry, manager.getEngine()) > 0)
|
||||
.collect(Collectors.toList()))
|
||||
.getRandom();
|
||||
//@done
|
||||
if (v == null || v.getReferenceSpawner() == null)
|
||||
return;
|
||||
|
||||
spawn(c, v);
|
||||
spawn(c, v, counter.remainingCapacity(v, manager.getEngine()));
|
||||
}
|
||||
|
||||
private void spawn(Chunk c, IrisEntitySpawn i) {
|
||||
static boolean ambientAllowed(Chunk chunk, boolean initial) {
|
||||
return Boolean.TRUE.equals(chunk.getWorld().getGameRuleValue(GameRules.SPAWN_MOBS))
|
||||
&& (initial || chunk.getLoadLevel() == Chunk.LoadLevel.ENTITY_TICKING);
|
||||
}
|
||||
|
||||
private void spawn(Chunk c, IrisEntitySpawn i, int remainingCapacity) {
|
||||
IrisSpawner ref = i.getReferenceSpawner();
|
||||
int s = i.spawn(manager.getEngine(), c, RNG.r);
|
||||
int s = i.spawn(manager.getEngine(), c, RNG.r, remainingCapacity);
|
||||
actuallySpawned.addAndGet(s);
|
||||
if (s > 0) {
|
||||
ref.spawn(manager.getEngine(), c.getX(), c.getZ());
|
||||
@@ -469,26 +482,20 @@ final class WorldEntitySpawner {
|
||||
return Rarity.expandWeighted(types);
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class ChunkCounter implements Predicate<IrisSpawner> {
|
||||
private final Entity[] entities;
|
||||
private transient int index = 0;
|
||||
private transient int count = 0;
|
||||
static final class ChunkCounter {
|
||||
private final Map<String, Integer> counts = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public boolean test(IrisSpawner spawner) {
|
||||
int max = spawner.getMaxEntitiesPerChunk();
|
||||
if (max <= count)
|
||||
return false;
|
||||
|
||||
while (index < entities.length) {
|
||||
if (entities[index++] instanceof LivingEntity) {
|
||||
if (++count >= max)
|
||||
return false;
|
||||
ChunkCounter(Entity[] entities) {
|
||||
for (Entity entity : entities) {
|
||||
if (entity instanceof LivingEntity && !(entity instanceof Player)) {
|
||||
counts.merge(BukkitEntityType.of(entity.getType()).spawnCategory(), 1, Integer::sum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
int remainingCapacity(IrisEntitySpawn entry, Engine engine) {
|
||||
IrisEntity entity = entry.getRealEntity(engine);
|
||||
return entry.getReferenceSpawner().remainingCapacity(entity, counts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -904,7 +904,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
zone.setFloor(floor);
|
||||
zone.setCeiling(y - 1);
|
||||
if (zone.isValid(resolver.height)
|
||||
&& !getComplex().isTerrain3DOpening(worldX, floor, worldZ)) {
|
||||
&& !isNaturalTerrainOpening(getComplex(), worldX, floor, worldZ)) {
|
||||
if (markerRoll(worldX, zone.ceiling, worldZ, 0x9E3779B97F4A7C15L)) {
|
||||
mantle.set(worldX, zone.ceiling, worldZ, MarkerMatter.CAVE_CEILING);
|
||||
}
|
||||
@@ -992,7 +992,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
}
|
||||
|
||||
IrisBiome resolveCaveBoundaryBiome(MatterCavern cavern, int worldX, int y, int worldZ, CaveInputs resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Map<String, IrisBiome> customBiomeCache) {
|
||||
if (resolverState.complex.isTerrain3DOpening(worldX, y, worldZ)) {
|
||||
if (isNaturalTerrainOpening(resolverState.complex, worldX, y, worldZ)) {
|
||||
return resolverState.complex.getTrueBiomeStream().get(worldX, worldZ);
|
||||
}
|
||||
if (cavern != null && !cavern.getCustomBiome().isEmpty()) {
|
||||
@@ -1001,6 +1001,18 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
return resolveCaveBiome(caveBiomeCache, worldX, y, worldZ, resolverState);
|
||||
}
|
||||
|
||||
private boolean isNaturalTerrainOpening(IrisComplex complex, int x, int y, int z) {
|
||||
if (complex.getTerrainTransform() == null) {
|
||||
return complex.isTerrain3DOpening(x, y, z);
|
||||
}
|
||||
if (!complex.hasTerrain3D()) {
|
||||
return false;
|
||||
}
|
||||
Terrain3DColumn column = complex.terrainColumn(x, z, complex.sampleHydrologyColumn(x, z));
|
||||
return column != null && y >= column.minY() && y < column.topY() && !column.isSolid(y)
|
||||
&& !getEngine().isAdditionalTerrainOwned(x, y, z);
|
||||
}
|
||||
|
||||
/** The dimension's underground river bed palette, or null when nothing paints. */
|
||||
private IrisRiverMaterialConfig undergroundBedMaterial() {
|
||||
IrisHydrology hydrology = getDimension().getHydrology();
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package art.arcane.iris.generation.terrain;
|
||||
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.noise.CNG;
|
||||
|
||||
public final class ContinentGenerator {
|
||||
private static final int CONTINENT_SEED_SALT = 234234565;
|
||||
|
||||
private ContinentGenerator() {
|
||||
}
|
||||
|
||||
public static CNG create(IrisDimension dimension, RNG parentSeedSource, IrisData packData) {
|
||||
return createScaledContinentalStyleGenerator(dimension, deriveContinentSeed(parentSeedSource), packData);
|
||||
}
|
||||
|
||||
private static CNG createScaledContinentalStyleGenerator(IrisDimension dimension, RNG continentSeed, IrisData packData) {
|
||||
return dimension.getContinentalStyle().createScaledGenerator(continentSeed, packData, inverseContinentZoom(dimension));
|
||||
}
|
||||
|
||||
static RNG deriveContinentSeed(RNG parentSeedSource) {
|
||||
return parentSeedSource.nextParallelRNG(CONTINENT_SEED_SALT);
|
||||
}
|
||||
|
||||
private static double inverseContinentZoom(IrisDimension dimension) {
|
||||
return 1D / dimension.getContinentZoom();
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ import art.arcane.iris.generation.image.IrisImageMapBinding;
|
||||
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||
import art.arcane.iris.generation.noise.IrisShapedGeneratorStyle;
|
||||
import art.arcane.iris.generation.noise.NoiseStyle;
|
||||
import art.arcane.iris.generation.terrain.transform.IrisTerrainTransform;
|
||||
import art.arcane.iris.pack.validation.CompatPools;
|
||||
import art.arcane.iris.pack.value.IrisRange;
|
||||
import art.arcane.iris.structure.nativegen.IrisImportedFeatureControl;
|
||||
@@ -66,6 +67,7 @@ import art.arcane.iris.pack.datapack.DataVersion;
|
||||
import art.arcane.iris.pack.datapack.IDataFixer;
|
||||
import art.arcane.iris.pack.datapack.IDataFixer.Dimension;
|
||||
import art.arcane.volmlib.util.cache.AtomicCache;
|
||||
import art.arcane.iris.generation.cache.LazyBoundedCache;
|
||||
import art.arcane.iris.world.history.GenerationRegistryContractFactory;
|
||||
import art.arcane.iris.pack.schema.annotation.ArrayType;
|
||||
import art.arcane.volmlib.util.documentation.Description;
|
||||
@@ -88,6 +90,9 @@ import art.arcane.volmlib.util.math.Position2;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.noise.CNG;
|
||||
import lombok.Data;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
@@ -129,7 +134,12 @@ public class IrisDimension extends IrisRegistrant {
|
||||
private final transient AtomicCache<IrisStaticObjectLayer> staticObjectLayer = new AtomicCache<>();
|
||||
private final transient AtomicCache<CNG> rockLayerGenerator = new AtomicCache<>();
|
||||
private final transient AtomicCache<CNG> fluidLayerGenerator = new AtomicCache<>();
|
||||
private final transient AtomicCache<CNG> coordFracture = new AtomicCache<>();
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private final transient LazyBoundedCache<Long, CNG> coordinateFractures = new LazyBoundedCache<>(8);
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private transient volatile CachedCoordinateFracture recentCoordinateFracture;
|
||||
private final transient AtomicCache<Double> sinr = new AtomicCache<>();
|
||||
private final transient AtomicCache<Double> cosr = new AtomicCache<>();
|
||||
private final transient AtomicCache<Double> rad = new AtomicCache<>();
|
||||
@@ -218,6 +228,8 @@ public class IrisDimension extends IrisRegistrant {
|
||||
private IrisHydrology hydrology = new IrisHydrology();
|
||||
@Description("Dimension-level river placement, routing, profile, biome, and geometry policy.")
|
||||
private IrisRiverPolicy riverPolicy = new IrisRiverPolicy();
|
||||
@Description("Optional external terrain transformation with an exact provider version and pack-owned settings.")
|
||||
private IrisTerrainTransform terrainTransform;
|
||||
@Description("Refuse to place surface objects and trees over carved surface openings.")
|
||||
private boolean requireObjectSurfaceSupport = true;
|
||||
@MinNumber(0)
|
||||
@@ -534,12 +546,15 @@ public class IrisDimension extends IrisRegistrant {
|
||||
}
|
||||
|
||||
public CNG getCoordFracture(RNG rng, int signature) {
|
||||
return coordFracture.aquire(() ->
|
||||
{
|
||||
CNG coordFracture = CNG.signature(rng.nextParallelRNG(signature));
|
||||
coordFracture.scale(0.012 / coordFractureZoom);
|
||||
return coordFracture;
|
||||
});
|
||||
long seed = rng.getSeed() + signature;
|
||||
CachedCoordinateFracture recent = recentCoordinateFracture;
|
||||
if (recent != null && recent.seed == seed) {
|
||||
return recent.generator;
|
||||
}
|
||||
CNG generator = coordinateFractures.computeIfAbsent(seed, ignored ->
|
||||
CNG.signature(new RNG(seed)).scale(0.012 / coordFractureZoom));
|
||||
recentCoordinateFracture = new CachedCoordinateFracture(seed, generator);
|
||||
return generator;
|
||||
}
|
||||
|
||||
public double getDimensionAngle() {
|
||||
@@ -1298,4 +1313,7 @@ public class IrisDimension extends IrisRegistrant {
|
||||
IO.writeAll(dimTypeVanilla, json);
|
||||
}
|
||||
}
|
||||
|
||||
private record CachedCoordinateFracture(long seed, CNG generator) {
|
||||
}
|
||||
}
|
||||
|
||||
+23
-7
@@ -31,6 +31,8 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
@Description("Dimension-level cave biome override with absolute world Y bounds.")
|
||||
@Data
|
||||
public class IrisDimensionCarvingEntry {
|
||||
private static final int CHILDREN_SEED_SALT = 2137;
|
||||
private static final double MINIMUM_CHILD_SHRINK_FACTOR = 0.0001D;
|
||||
private final transient AtomicCache<IrisBiome> realBiome = new AtomicCache<>(true);
|
||||
private final transient Map<Long, CNG> childGenerators = new ConcurrentHashMap<>();
|
||||
|
||||
@@ -81,13 +83,27 @@ public class IrisDimensionCarvingEntry {
|
||||
}
|
||||
|
||||
public CNG getChildrenGenerator(long seed, IrisData data) {
|
||||
return childGenerators.computeIfAbsent(deriveChildrenGeneratorSeed(seed), key -> createChildrenGenerator(key, data));
|
||||
}
|
||||
|
||||
private CNG createChildrenGenerator(long childrenGeneratorSeed, IrisData data) {
|
||||
return getChildStyle().createScaledGenerator(deriveChildStyleSeed(childrenGeneratorSeed), data, childShrinkFactor());
|
||||
}
|
||||
|
||||
long deriveChildrenGeneratorSeed(long worldSeed) {
|
||||
return worldSeed ^ (hashOfId() << 32) ^ CHILDREN_SEED_SALT;
|
||||
}
|
||||
|
||||
static RNG deriveChildStyleSeed(long childrenGeneratorSeed) {
|
||||
return new RNG(childrenGeneratorSeed).nextParallelRNG(CHILDREN_SEED_SALT);
|
||||
}
|
||||
|
||||
private double childShrinkFactor() {
|
||||
return Math.max(MINIMUM_CHILD_SHRINK_FACTOR, getChildShrinkFactor());
|
||||
}
|
||||
|
||||
private long hashOfId() {
|
||||
String entryId = getId();
|
||||
long idHash = entryId == null ? 0L : entryId.trim().hashCode();
|
||||
long generatorSeed = seed ^ (idHash << 32) ^ 2137L;
|
||||
return childGenerators.computeIfAbsent(generatorSeed, key -> {
|
||||
double scale = Math.max(0.0001D, getChildShrinkFactor());
|
||||
RNG random = new RNG(key);
|
||||
return getChildStyle().create(random.nextParallelRNG(2137), data).bake().scale(scale).bake();
|
||||
});
|
||||
return entryId == null ? 0L : entryId.trim().hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,6 @@ public class IrisMaterialPalette implements StructurePalette {
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private final transient AtomicReference<CachedLayerGenerator> recentLayerGenerator = new AtomicReference<>();
|
||||
private final transient AtomicCache<CNG> heightGenerator = new AtomicCache<>();
|
||||
@Description("The style of noise")
|
||||
private IrisGeneratorStyle style = NoiseStyle.STATIC.style();
|
||||
@MinNumber(0.0001)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.generation.terrain;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.function.IntPredicate;
|
||||
|
||||
public final class Terrain3DColumn {
|
||||
private final double baseHeight;
|
||||
@@ -20,6 +21,29 @@ public final class Terrain3DColumn {
|
||||
return new Terrain3DColumn(baseHeight, top + 1, false, new int[]{0, top});
|
||||
}
|
||||
|
||||
public static Terrain3DColumn fromOccupancy(double baseHeight, int height, IntPredicate solid) {
|
||||
if (height < 1) {
|
||||
throw new IllegalArgumentException("Terrain column height must be positive");
|
||||
}
|
||||
int[] spans = new int[height + 1];
|
||||
int count = 0;
|
||||
boolean occupied = false;
|
||||
for (int y = 0; y < height; y++) {
|
||||
boolean next = solid.test(y);
|
||||
if (next && !occupied) {
|
||||
spans[count++] = y;
|
||||
} else if (!next && occupied) {
|
||||
spans[count++] = y - 1;
|
||||
}
|
||||
occupied = next;
|
||||
}
|
||||
if (occupied) {
|
||||
spans[count++] = height - 1;
|
||||
}
|
||||
return new Terrain3DColumn(baseHeight, 0, true,
|
||||
count == 0 ? new int[]{0, -1} : Arrays.copyOf(spans, count));
|
||||
}
|
||||
|
||||
public double baseHeight() {
|
||||
return baseHeight;
|
||||
}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package art.arcane.iris.generation.terrain.transform;
|
||||
|
||||
import art.arcane.iris.pack.schema.annotation.Required;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.documentation.Description;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Description("An external terrain transformer with its exact implementation version and generation settings.")
|
||||
public class IrisTerrainTransform {
|
||||
@Required
|
||||
@Description("The installed terrain transform provider identifier.")
|
||||
private String id;
|
||||
|
||||
@Required
|
||||
@Description("The exact provider implementation version required to generate this dimension.")
|
||||
private String version;
|
||||
|
||||
@Description("Provider settings saved with the generation pack. Values are strings interpreted by the provider.")
|
||||
private KMap<String, String> settings = new KMap<>();
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package art.arcane.iris.generation.terrain.transform;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface TerrainColumnTransformer {
|
||||
void transform(TerrainTransformContext context, int localX, int localZ);
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package art.arcane.iris.generation.terrain.transform;
|
||||
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
|
||||
public interface TerrainTransformContext {
|
||||
int blockX();
|
||||
|
||||
int blockZ();
|
||||
|
||||
int minY();
|
||||
|
||||
int height();
|
||||
|
||||
NativeBlockState original(int worldX, int worldY, int worldZ);
|
||||
|
||||
void set(int localX, int localY, int localZ, NativeBlockState state);
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package art.arcane.iris.generation.terrain.transform;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface TerrainTransformProvider {
|
||||
String id();
|
||||
|
||||
String version();
|
||||
|
||||
TerrainTransformer create(Map<String, String> settings);
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package art.arcane.iris.generation.terrain.transform;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.ServiceLoader;
|
||||
|
||||
public final class TerrainTransformRegistry {
|
||||
private final Map<ProviderIdentity, TerrainTransformProvider> providers;
|
||||
|
||||
public TerrainTransformRegistry(Iterable<TerrainTransformProvider> providers) {
|
||||
Map<ProviderIdentity, TerrainTransformProvider> indexed = new HashMap<>();
|
||||
for (TerrainTransformProvider provider : Objects.requireNonNull(providers, "terrain transform providers")) {
|
||||
Objects.requireNonNull(provider, "terrain transform provider");
|
||||
ProviderIdentity identity = new ProviderIdentity(provider.id(), provider.version());
|
||||
if (indexed.putIfAbsent(identity, provider) != null) {
|
||||
throw new IllegalArgumentException("Duplicate terrain transform provider " + identity + ".");
|
||||
}
|
||||
}
|
||||
this.providers = Map.copyOf(indexed);
|
||||
}
|
||||
|
||||
public static TerrainTransformRegistry discover() {
|
||||
return new TerrainTransformRegistry(ServiceLoader.load(
|
||||
TerrainTransformProvider.class, TerrainTransformProvider.class.getClassLoader()));
|
||||
}
|
||||
|
||||
public TerrainTransformer resolve(IrisTerrainTransform descriptor) {
|
||||
if (descriptor == null) {
|
||||
return null;
|
||||
}
|
||||
ProviderIdentity identity = new ProviderIdentity(descriptor.getId(), descriptor.getVersion());
|
||||
TerrainTransformProvider provider = providers.get(identity);
|
||||
if (provider == null) {
|
||||
throw new IllegalStateException("Required terrain transform provider " + identity + " is unavailable.");
|
||||
}
|
||||
Map<String, String> settings = Map.copyOf(Objects.requireNonNull(
|
||||
descriptor.getSettings(), "terrain transform settings"));
|
||||
TerrainTransformer transformer = Objects.requireNonNull(
|
||||
provider.create(settings), "terrain transformer returned by " + identity);
|
||||
int radius = transformer.radius();
|
||||
if (radius < 0 || radius > 16) {
|
||||
throw new IllegalArgumentException("Terrain transform provider " + identity
|
||||
+ " requires radius " + radius + "; the supported range is 0 through 16 blocks.");
|
||||
}
|
||||
return transformer;
|
||||
}
|
||||
|
||||
private static String requireIdentity(String value, String label) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException(label + " must not be blank.");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private record ProviderIdentity(String id, String version) {
|
||||
private ProviderIdentity {
|
||||
id = requireIdentity(id, "Terrain transform provider ID");
|
||||
version = requireIdentity(version, "Terrain transform provider version");
|
||||
}
|
||||
}
|
||||
}
|
||||
+506
@@ -0,0 +1,506 @@
|
||||
package art.arcane.iris.generation.terrain.transform;
|
||||
|
||||
import art.arcane.iris.generation.block.B;
|
||||
import art.arcane.iris.generation.context.ChunkContext;
|
||||
import art.arcane.iris.generation.context.IrisContext;
|
||||
import art.arcane.iris.generation.mantle.CaveTerrainSnapshot;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.terrain.Terrain3DColumn;
|
||||
import art.arcane.iris.world.history.FloatingBiomeOverlay;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
import art.arcane.volmlib.util.hunk.storage.StorageHunk;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class TerrainTransformRuntime implements AutoCloseable {
|
||||
private static final long CACHE_BYTES = 64L * 1024 * 1024;
|
||||
|
||||
private final Engine engine;
|
||||
private final IrisComplex complex;
|
||||
private final TerrainTransformer transformer;
|
||||
private final TerrainColumnTransformer columnTransformer;
|
||||
private final int radius;
|
||||
private final int minY;
|
||||
private final int height;
|
||||
private final NativeBlockState air;
|
||||
private final Cache<Long, Snapshot> originals;
|
||||
private final Cache<Long, Snapshot> transformed;
|
||||
private final Cache<Long, ColumnSnapshot> transformedColumns;
|
||||
private volatile boolean closed;
|
||||
|
||||
public TerrainTransformRuntime(Options options, TerrainTransformer transformer) {
|
||||
engine = Objects.requireNonNull(options, "terrain runtime options").engine();
|
||||
complex = options.complex();
|
||||
this.transformer = Objects.requireNonNull(transformer, "terrain transformer");
|
||||
columnTransformer = transformer.columnTransformer();
|
||||
radius = transformer.radius();
|
||||
if (radius < 0 || radius > 16) {
|
||||
throw new IllegalArgumentException("Terrain transform radius must be between 0 and 16 blocks");
|
||||
}
|
||||
minY = engine.getMinHeight();
|
||||
height = engine.getHeight();
|
||||
if (height <= 0) {
|
||||
throw new IllegalArgumentException("Terrain height must be positive");
|
||||
}
|
||||
air = Objects.requireNonNull(B.getState("AIR"), "air block state");
|
||||
originals = newCache();
|
||||
transformed = newCache();
|
||||
transformedColumns = columnTransformer == null ? null : Caffeine.newBuilder().maximumWeight(CACHE_BYTES)
|
||||
.weigher((Long key, ColumnSnapshot snapshot) -> snapshot.weight()).build();
|
||||
}
|
||||
|
||||
public void generate(int x, int z, Hunk<NativeBlockState> blocks, Hunk<NativeBiome> biomes,
|
||||
boolean multicore, ChunkContext target) {
|
||||
requireOpen();
|
||||
if ((x & 15) != 0 || (z & 15) != 0 || blocks.getWidth() != 16 || blocks.getDepth() != 16
|
||||
|| blocks.getHeight() != height || biomes.getWidth() != 16 || biomes.getDepth() != 16
|
||||
|| biomes.getHeight() != height || target.getComplex() != complex) {
|
||||
throw new IllegalArgumentException("Terrain transform output must match its runtime and chunk");
|
||||
}
|
||||
Snapshot snapshot = transformed(x >> 4, z >> 4);
|
||||
target.setTerrainBiomeOutput(biomes);
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int localZ = 0; localZ < 16; localZ++) {
|
||||
for (int localX = 0; localX < 16; localX++) {
|
||||
int index = index(localX, y, localZ);
|
||||
blocks.setRaw(localX, y, localZ, snapshot.blocks[index]);
|
||||
NativeBiome biome = snapshot.biomes[index];
|
||||
if (biome != null) {
|
||||
biomes.setRaw(localX, y, localZ, biome);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int localZ = 0; localZ < 16; localZ++) {
|
||||
for (int localX = 0; localX < 16; localX++) {
|
||||
target.setTerrainHeight(localX, localZ, snapshot.solidHeights[(localZ << 4) | localX]);
|
||||
}
|
||||
}
|
||||
target.setCaveTerrain(snapshot.caves);
|
||||
target.setFloatingBiomes(snapshot.floating == null ? null : snapshot.floating.copy());
|
||||
}
|
||||
|
||||
public int height(int x, int z, boolean ignoreFluid) {
|
||||
requireOpen();
|
||||
Snapshot snapshot = fullForQuery(x, z);
|
||||
if (snapshot != null) {
|
||||
return snapshot.height(x & 15, z & 15, ignoreFluid);
|
||||
}
|
||||
ColumnSnapshot column = transformedColumn(x, z);
|
||||
return ignoreFluid ? column.ground : column.top;
|
||||
}
|
||||
|
||||
public int readyHeight(int x, int z, boolean ignoreFluid) {
|
||||
requireOpen();
|
||||
Snapshot snapshot = transformed.getIfPresent(key(x >> 4, z >> 4));
|
||||
if (snapshot != null) {
|
||||
return snapshot.height(x & 15, z & 15, ignoreFluid);
|
||||
}
|
||||
ColumnSnapshot column = readyTransformedColumn(x, z);
|
||||
return column == null ? Integer.MIN_VALUE : ignoreFluid ? column.ground : column.top;
|
||||
}
|
||||
|
||||
public NativeBlockState block(int worldX, int localY, int worldZ) {
|
||||
requireOpen();
|
||||
if (localY < 0 || localY >= height) {
|
||||
return air;
|
||||
}
|
||||
return transformed(worldX >> 4, worldZ >> 4).blocks[index(worldX & 15, localY, worldZ & 15)];
|
||||
}
|
||||
|
||||
public NativeBlockState readyBlock(int worldX, int localY, int worldZ) {
|
||||
requireOpen();
|
||||
if (localY < 0 || localY >= height) {
|
||||
return air;
|
||||
}
|
||||
Snapshot snapshot = transformed.getIfPresent(key(worldX >> 4, worldZ >> 4));
|
||||
return snapshot == null ? null : snapshot.blocks[index(worldX & 15, localY, worldZ & 15)];
|
||||
}
|
||||
|
||||
public Terrain3DColumn column(int x, int z) {
|
||||
requireOpen();
|
||||
Snapshot snapshot = fullForQuery(x, z);
|
||||
return snapshot == null ? transformedColumn(x, z).terrain : snapshot.column(x & 15, z & 15);
|
||||
}
|
||||
|
||||
public Terrain3DColumn readyColumn(int x, int z) {
|
||||
requireOpen();
|
||||
Snapshot snapshot = transformed.getIfPresent(key(x >> 4, z >> 4));
|
||||
if (snapshot != null) {
|
||||
return snapshot.column(x & 15, z & 15);
|
||||
}
|
||||
ColumnSnapshot column = readyTransformedColumn(x, z);
|
||||
return column == null ? null : column.terrain;
|
||||
}
|
||||
|
||||
public int fluidHeight(int x, int z) {
|
||||
requireOpen();
|
||||
Snapshot snapshot = fullForQuery(x, z);
|
||||
return snapshot == null ? transformedColumn(x, z).fluid : snapshot.fluidHeights[((z & 15) << 4) | (x & 15)];
|
||||
}
|
||||
|
||||
public int readyFluidHeight(int x, int z) {
|
||||
requireOpen();
|
||||
Snapshot snapshot = transformed.getIfPresent(key(x >> 4, z >> 4));
|
||||
if (snapshot != null) {
|
||||
return snapshot.fluidHeights[((z & 15) << 4) | (x & 15)];
|
||||
}
|
||||
ColumnSnapshot column = readyTransformedColumn(x, z);
|
||||
return column == null ? Integer.MIN_VALUE : column.fluid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
closed = true;
|
||||
originals.invalidateAll();
|
||||
transformed.invalidateAll();
|
||||
originals.cleanUp();
|
||||
transformed.cleanUp();
|
||||
if (transformedColumns != null) {
|
||||
transformedColumns.invalidateAll();
|
||||
transformedColumns.cleanUp();
|
||||
}
|
||||
}
|
||||
|
||||
private Cache<Long, Snapshot> newCache() {
|
||||
return Caffeine.newBuilder().maximumWeight(CACHE_BYTES)
|
||||
.weigher((Long key, Snapshot snapshot) -> snapshot.weight()).build();
|
||||
}
|
||||
|
||||
private Snapshot transformed(int chunkX, int chunkZ) {
|
||||
Snapshot snapshot = transformed.get(key(chunkX, chunkZ), ignored -> transform(chunkX, chunkZ));
|
||||
if (closed) {
|
||||
transformed.invalidateAll();
|
||||
requireOpen();
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private Snapshot original(int chunkX, int chunkZ) {
|
||||
Snapshot snapshot = originals.get(key(chunkX, chunkZ), ignored -> materialize(chunkX, chunkZ));
|
||||
if (closed) {
|
||||
originals.invalidateAll();
|
||||
requireOpen();
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private Snapshot fullForQuery(int x, int z) {
|
||||
return columnTransformer == null ? transformed(x >> 4, z >> 4)
|
||||
: transformed.getIfPresent(key(x >> 4, z >> 4));
|
||||
}
|
||||
|
||||
private ColumnSnapshot readyTransformedColumn(int x, int z) {
|
||||
return transformedColumns == null ? null : transformedColumns.getIfPresent(key(x, z));
|
||||
}
|
||||
|
||||
private ColumnSnapshot transformedColumn(int x, int z) {
|
||||
ColumnSnapshot snapshot = transformedColumns.get(key(x, z), ignored -> transformColumn(x, z));
|
||||
if (closed) {
|
||||
transformedColumns.invalidateAll();
|
||||
requireOpen();
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private Snapshot materialize(int chunkX, int chunkZ) {
|
||||
requireOpen();
|
||||
int x = chunkX << 4;
|
||||
int z = chunkZ << 4;
|
||||
NativeBlockState[] states = new NativeBlockState[Math.multiplyExact(256, height)];
|
||||
Arrays.fill(states, air);
|
||||
NativeBiome[] biomeStates = new NativeBiome[states.length];
|
||||
Hunk<NativeBlockState> blocks = new SnapshotHunk<>(states, air);
|
||||
Hunk<NativeBiome> biomes = new SnapshotHunk<>(biomeStates, null);
|
||||
long session = engine.getGenerationSessionId();
|
||||
ChunkContext context = new ChunkContext(x, z, complex, session, true,
|
||||
ChunkContext.PrefillPlan.NATURAL_TERRAIN, engine.getMetrics(), engine.getDimensionStackContext());
|
||||
try (IrisContext.Scope scope = IrisContext.open(engine, session, context)) {
|
||||
engine.getMode().generateTerrain(x, z, blocks, biomes, false, context);
|
||||
}
|
||||
FloatingBiomeOverlay floating = context.getFloatingBiomes();
|
||||
return new Snapshot(new SnapshotData(states, biomeStates, context.getCaveTerrain(),
|
||||
floating == null ? null : floating.copy()), false);
|
||||
}
|
||||
|
||||
private Snapshot transform(int chunkX, int chunkZ) {
|
||||
requireOpen();
|
||||
Snapshot source = original(chunkX, chunkZ);
|
||||
TransformContext context = new TransformContext(new TransformTarget(chunkX, chunkZ, false, 0, 0), source);
|
||||
try {
|
||||
transformer.transform(context);
|
||||
} finally {
|
||||
context.active = false;
|
||||
}
|
||||
return new Snapshot(new SnapshotData(context.blocks, source.biomes, source.caves, source.floating), true);
|
||||
}
|
||||
|
||||
private ColumnSnapshot transformColumn(int x, int z) {
|
||||
requireOpen();
|
||||
Snapshot source = original(x >> 4, z >> 4);
|
||||
TransformContext context = new TransformContext(
|
||||
new TransformTarget(x >> 4, z >> 4, true, x & 15, z & 15), source);
|
||||
try {
|
||||
columnTransformer.transform(context, x & 15, z & 15);
|
||||
} finally {
|
||||
context.active = false;
|
||||
}
|
||||
return new ColumnSnapshot(context.blocks);
|
||||
}
|
||||
|
||||
private void requireOpen() {
|
||||
if (closed) {
|
||||
throw new IllegalStateException("Terrain transform runtime is closed");
|
||||
}
|
||||
}
|
||||
|
||||
private static int index(int x, int y, int z) {
|
||||
return (y << 8) | (z << 4) | x;
|
||||
}
|
||||
|
||||
private static long key(int chunkX, int chunkZ) {
|
||||
return ((long) chunkX << 32) | (chunkZ & 0xffffffffL);
|
||||
}
|
||||
|
||||
public record Options(Engine engine, IrisComplex complex) {
|
||||
public Options {
|
||||
Objects.requireNonNull(engine, "terrain engine");
|
||||
Objects.requireNonNull(complex, "terrain complex");
|
||||
}
|
||||
}
|
||||
|
||||
private record TransformTarget(int chunkX, int chunkZ, boolean columnOnly, int localX, int localZ) {
|
||||
}
|
||||
|
||||
private record SnapshotData(NativeBlockState[] blocks, NativeBiome[] biomes,
|
||||
CaveTerrainSnapshot caves, FloatingBiomeOverlay floating) {
|
||||
}
|
||||
|
||||
private static final class SnapshotHunk<T> extends StorageHunk<T> {
|
||||
private final T[] values;
|
||||
private final T fallback;
|
||||
|
||||
private SnapshotHunk(T[] values, T fallback) {
|
||||
super(16, values.length / 256, 16);
|
||||
this.values = values;
|
||||
this.fallback = fallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getRaw(int x, int y, int z) {
|
||||
return y < 0 || y >= getHeight() ? fallback : values[index(x, y, z)];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRaw(int x, int y, int z, T value) {
|
||||
if (value != null) {
|
||||
values[index(x, y, z)] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class TransformContext implements TerrainTransformContext {
|
||||
private final int x;
|
||||
private final int z;
|
||||
private final NativeBlockState[] blocks;
|
||||
private final TransformTarget target;
|
||||
private final Snapshot[] sources = new Snapshot[9];
|
||||
private volatile boolean active = true;
|
||||
|
||||
private TransformContext(TransformTarget target, Snapshot source) {
|
||||
this.target = target;
|
||||
x = target.chunkX() << 4;
|
||||
z = target.chunkZ() << 4;
|
||||
if (target.columnOnly()) {
|
||||
blocks = new NativeBlockState[height];
|
||||
for (int y = 0; y < height; y++) {
|
||||
blocks[y] = source.blocks[index(target.localX(), y, target.localZ())];
|
||||
}
|
||||
} else {
|
||||
blocks = source.blocks.clone();
|
||||
}
|
||||
sources[4] = source;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int blockX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int blockZ() {
|
||||
return z;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int minY() {
|
||||
return minY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int height() {
|
||||
return height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState original(int worldX, int worldY, int worldZ) {
|
||||
requireActive();
|
||||
long localX = (long) worldX - x;
|
||||
long localZ = (long) worldZ - z;
|
||||
if (localX < -radius || localX >= 16L + radius || localZ < -radius || localZ >= 16L + radius) {
|
||||
throw new IllegalArgumentException("Terrain transform read exceeds its declared radius");
|
||||
}
|
||||
long localY = (long) worldY - minY;
|
||||
if (localY < 0 || localY >= height) {
|
||||
return air;
|
||||
}
|
||||
int sourceX = worldX >> 4;
|
||||
int sourceZ = worldZ >> 4;
|
||||
int slot = (sourceZ - (z >> 4) + 1) * 3 + sourceX - (x >> 4) + 1;
|
||||
Snapshot snapshot = sources[slot];
|
||||
if (snapshot == null) {
|
||||
snapshot = TerrainTransformRuntime.this.original(sourceX, sourceZ);
|
||||
sources[slot] = snapshot;
|
||||
}
|
||||
return snapshot.blocks[index(worldX & 15, (int) localY, worldZ & 15)];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(int localX, int localY, int localZ, NativeBlockState state) {
|
||||
requireActive();
|
||||
Objects.requireNonNull(state, "terrain transform block state");
|
||||
if (localX < 0 || localX >= 16 || localZ < 0 || localZ >= 16 || localY < 0 || localY >= height) {
|
||||
throw new IllegalArgumentException("Terrain transform write exceeds its output chunk");
|
||||
}
|
||||
if (target.columnOnly() && (localX != target.localX() || localZ != target.localZ())) {
|
||||
throw new IllegalArgumentException("Terrain column transform write exceeds its output column");
|
||||
}
|
||||
if (complex.allowsMantleWrite(x + localX, z + localZ)) {
|
||||
blocks[target.columnOnly() ? localY : index(localX, localY, localZ)] = state;
|
||||
}
|
||||
}
|
||||
|
||||
private void requireActive() {
|
||||
requireOpen();
|
||||
if (!active) {
|
||||
throw new IllegalStateException("Terrain transform context is no longer active");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ColumnSnapshot {
|
||||
private final Terrain3DColumn terrain;
|
||||
private final int ground;
|
||||
private final int top;
|
||||
private final int fluid;
|
||||
|
||||
private ColumnSnapshot(NativeBlockState[] blocks) {
|
||||
int solidHeight = -1;
|
||||
int topHeight = -1;
|
||||
int fluidHeight = -1;
|
||||
for (int y = blocks.length - 1; y >= 0; y--) {
|
||||
NativeBlockState state = blocks[y];
|
||||
if (state.isAir()) {
|
||||
continue;
|
||||
}
|
||||
if (topHeight == -1) {
|
||||
topHeight = y;
|
||||
}
|
||||
if ((state.isFluid() || state.isWaterLogged()) && fluidHeight == -1) {
|
||||
fluidHeight = y;
|
||||
}
|
||||
if (!state.isFluid() && solidHeight == -1) {
|
||||
solidHeight = y;
|
||||
}
|
||||
}
|
||||
ground = solidHeight;
|
||||
top = topHeight;
|
||||
fluid = fluidHeight;
|
||||
terrain = Terrain3DColumn.fromOccupancy(ground, blocks.length,
|
||||
y -> !blocks[y].isAir() && !blocks[y].isFluid());
|
||||
}
|
||||
|
||||
private int weight() {
|
||||
return 192 + terrain.spanCount() * 8;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Snapshot {
|
||||
private final NativeBlockState[] blocks;
|
||||
private final NativeBiome[] biomes;
|
||||
private final CaveTerrainSnapshot caves;
|
||||
private final FloatingBiomeOverlay floating;
|
||||
private final int[] solidHeights;
|
||||
private final int[] topHeights;
|
||||
private final int[] fluidHeights;
|
||||
private final Terrain3DColumn[] columns;
|
||||
|
||||
private Snapshot(SnapshotData data, boolean effective) {
|
||||
blocks = data.blocks();
|
||||
biomes = data.biomes();
|
||||
caves = data.caves();
|
||||
floating = data.floating();
|
||||
solidHeights = effective ? new int[256] : null;
|
||||
topHeights = effective ? new int[256] : null;
|
||||
fluidHeights = effective ? new int[256] : null;
|
||||
columns = effective ? new Terrain3DColumn[256] : null;
|
||||
if (!effective) {
|
||||
return;
|
||||
}
|
||||
Arrays.fill(solidHeights, -1);
|
||||
Arrays.fill(topHeights, -1);
|
||||
Arrays.fill(fluidHeights, -1);
|
||||
int height = blocks.length / 256;
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
int column = (z << 4) | x;
|
||||
for (int y = height - 1; y >= 0; y--) {
|
||||
NativeBlockState state = blocks[index(x, y, z)];
|
||||
if (state.isAir()) {
|
||||
continue;
|
||||
}
|
||||
if (topHeights[column] == -1) {
|
||||
topHeights[column] = y;
|
||||
}
|
||||
if ((state.isFluid() || state.isWaterLogged()) && fluidHeights[column] == -1) {
|
||||
fluidHeights[column] = y;
|
||||
}
|
||||
if (!state.isFluid() && solidHeights[column] == -1) {
|
||||
solidHeights[column] = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int height(int x, int z, boolean ignoreFluid) {
|
||||
return (ignoreFluid ? solidHeights : topHeights)[(z << 4) | x];
|
||||
}
|
||||
|
||||
private Terrain3DColumn column(int x, int z) {
|
||||
int index = (z << 4) | x;
|
||||
Terrain3DColumn column = columns[index];
|
||||
if (column == null) {
|
||||
column = Terrain3DColumn.fromOccupancy(solidHeights[index], blocks.length / 256, y -> {
|
||||
NativeBlockState state = blocks[index(x, y, z)];
|
||||
return !state.isAir() && !state.isFluid();
|
||||
});
|
||||
columns[index] = column;
|
||||
}
|
||||
return column;
|
||||
}
|
||||
|
||||
private int weight() {
|
||||
long blockReferences = (long) blocks.length * 20L;
|
||||
long caveReferences = caves == null ? 0L : caves.estimatedRetainedBytes();
|
||||
long floatingReferences = floating == null ? 0L : (long) blocks.length * 4L;
|
||||
return (int) Math.min(Integer.MAX_VALUE, 16384L + blockReferences + caveReferences + floatingReferences);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package art.arcane.iris.generation.terrain.transform;
|
||||
|
||||
public interface TerrainTransformer {
|
||||
int radius();
|
||||
|
||||
void transform(TerrainTransformContext context);
|
||||
|
||||
default TerrainColumnTransformer columnTransformer() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -138,6 +138,7 @@ public class IrisObject extends IrisRegistrant {
|
||||
@Getter
|
||||
@Setter
|
||||
protected transient volatile boolean smartBored = false;
|
||||
transient volatile IrisObjectShaping.SmartBoreVariant smartBoreVariant;
|
||||
@Setter
|
||||
protected transient AtomicCache<AxisAlignedBB> aabb = new AtomicCache<>();
|
||||
transient final AtomicCache<KList<IrisBlockVector>> surfaceSupportOffsets = new AtomicCache<>();
|
||||
@@ -335,7 +336,8 @@ public class IrisObject extends IrisRegistrant {
|
||||
}
|
||||
|
||||
public int place(int x, int yv, int z, IObjectPlacer oplacer, IrisObjectPlacement config, RNG rng, BiConsumer<BlockPosition, NativeBlockState> listener, CarveResult c, IrisData rdata) {
|
||||
return new IrisObjectPlacementRunner(this).place(x, yv, z, oplacer, config, rng, listener, c, rdata);
|
||||
IrisObject placedObject = config.isSmartBore() ? IrisObjectShaping.smartBoredVariant(this) : this;
|
||||
return new IrisObjectPlacementRunner(placedObject).place(x, yv, z, oplacer, config, rng, listener, c, rdata);
|
||||
}
|
||||
|
||||
KList<IrisBlockVector> getSurfaceSupportOffsets() {
|
||||
|
||||
@@ -49,6 +49,7 @@ import art.arcane.volmlib.util.math.Position2;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
@@ -123,10 +124,6 @@ final class IrisObjectPlacementRunner {
|
||||
}
|
||||
}
|
||||
|
||||
if (config.isSmartBore()) {
|
||||
IrisObjectShaping.ensureSmartBored(self);
|
||||
}
|
||||
|
||||
boolean warped = !config.getWarp().isFlat();
|
||||
// Placement-invariant hoists off the per-voxel loop: the warp CNG resolution went
|
||||
// through a keyed cache probe per block, and the two boolean gates below cut per-block
|
||||
@@ -902,7 +899,9 @@ final class IrisObjectPlacementRunner {
|
||||
if (heightmap != null) {
|
||||
RNG rngx = rng.nextParallelRNG(3468854);
|
||||
|
||||
for (Position2 i : heightmap.k()) {
|
||||
KList<Position2> snowColumns = heightmap.k();
|
||||
snowColumns.sort(Comparator.comparingInt(Position2::getX).thenComparingInt(Position2::getZ));
|
||||
for (Position2 i : snowColumns) {
|
||||
int vx = i.getX();
|
||||
int vy = heightmap.get(i);
|
||||
int vz = i.getZ();
|
||||
|
||||
@@ -39,6 +39,41 @@ final class IrisObjectShaping {
|
||||
private IrisObjectShaping() {
|
||||
}
|
||||
|
||||
static IrisObject smartBoredVariant(IrisObject self) {
|
||||
SmartBoreVariant cached = self.smartBoreVariant;
|
||||
if (cached != null && cached.matches(self)) {
|
||||
return cached.object();
|
||||
}
|
||||
self.writeLock.lock();
|
||||
try {
|
||||
cached = self.smartBoreVariant;
|
||||
if (cached != null && cached.matches(self)) {
|
||||
return cached.object();
|
||||
}
|
||||
IrisObject variant = self.copy();
|
||||
ensureSmartBored(variant);
|
||||
self.smartBoreVariant = new SmartBoreVariant(self.blocks, self.states,
|
||||
self.blocks.modificationRevision(), self.states.modificationRevision(),
|
||||
self.w, self.h, self.d, self.center.getX(), self.center.getY(), self.center.getZ(), variant);
|
||||
return variant;
|
||||
} finally {
|
||||
self.writeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
record SmartBoreVariant(VectorMap<NativeBlockState> blocks, VectorMap<TileData> states,
|
||||
long blockRevision, long stateRevision, int width, int height, int depth,
|
||||
int centerX, int centerY, int centerZ, IrisObject object) {
|
||||
boolean matches(IrisObject source) {
|
||||
return blocks == source.blocks && states == source.states
|
||||
&& blockRevision == source.blocks.modificationRevision()
|
||||
&& stateRevision == source.states.modificationRevision()
|
||||
&& width == source.w && height == source.h && depth == source.d
|
||||
&& centerX == source.center.getX() && centerY == source.center.getY()
|
||||
&& centerZ == source.center.getZ();
|
||||
}
|
||||
}
|
||||
|
||||
static void ensureSmartBored(IrisObject self) {
|
||||
if (self.smartBored) {
|
||||
return;
|
||||
|
||||
@@ -302,7 +302,8 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
||||
|
||||
public record PregenProgress(double percent, long generated, long totalChunks, double chunksPerSecond,
|
||||
double overallChunksPerSecond, double thirtySecondChunksPerSecond,
|
||||
double sixtySecondChunksPerSecond, long chunksRemaining, long eta, long elapsed,
|
||||
double sixtySecondChunksPerSecond, long chunksRemaining, long chunksInFlight,
|
||||
long eta, long elapsed,
|
||||
String method, boolean paused, long failed, String worldName, String worldIdentity) {
|
||||
}
|
||||
|
||||
@@ -322,6 +323,7 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
||||
Math.max(0D, lastThirtySecondChunksPerSecond),
|
||||
Math.max(0D, lastSixtySecondChunksPerSecond),
|
||||
Math.max(0L, lastChunksRemaining),
|
||||
pregenerator.getInFlightChunks(),
|
||||
lastEta,
|
||||
lastElapsed,
|
||||
lastMethod,
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
package art.arcane.iris.world.entity;
|
||||
|
||||
import art.arcane.volmlib.nativelib.entity.NativeEntityOptions;
|
||||
import art.arcane.volmlib.nativelib.entity.NativeEntityType;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
|
||||
import art.arcane.iris.command.IrisCommand;
|
||||
import art.arcane.iris.generation.decoration.IrisSurface;
|
||||
@@ -130,8 +134,8 @@ public class IrisEntity extends IrisRegistrant implements NativeEntityOptions {
|
||||
@Description("Should this entity be allowed to pickup items")
|
||||
private boolean pickupItems = false;
|
||||
|
||||
@Description("Should this entity be removed when far away")
|
||||
private boolean removable = false;
|
||||
@Description("Allow normal distance despawning. keepEntity and world.forcePersistEntities override this setting.")
|
||||
private boolean removable = true;
|
||||
|
||||
@Description("Entity helmet equipment")
|
||||
private IrisLoot helmet = null;
|
||||
@@ -197,6 +201,25 @@ public class IrisEntity extends IrisRegistrant implements NativeEntityOptions {
|
||||
@Description("Run raw commands when this entity is spawned. Use {x}, {y}, and {z} for location. /summon pig {x} {y} {z}")
|
||||
private KList<IrisCommand> rawCommands = new KList<>();
|
||||
|
||||
public String spawnCategory() {
|
||||
IrisPlatform platform = IrisPlatforms.getOrNull();
|
||||
if (platform == null || type == null || type.isBlank()) {
|
||||
return "misc";
|
||||
}
|
||||
PlatformRegistries registries = platform.registries();
|
||||
NativeEntityType resolved = registries == null ? null : registries.entity(type.trim().toLowerCase(Locale.ROOT));
|
||||
String category = resolved == null ? null : resolved.spawnCategory();
|
||||
return category == null || category.isBlank() ? "misc" : category;
|
||||
}
|
||||
|
||||
void applyPersistence(Entity entity, boolean forcePersistence) {
|
||||
boolean persistent = keepEntity || forcePersistence;
|
||||
entity.setPersistent(true);
|
||||
if (entity instanceof LivingEntity living) {
|
||||
living.setRemoveWhenFarAway(removable && !persistent);
|
||||
}
|
||||
}
|
||||
|
||||
public EntityType getBukkitType() {
|
||||
if (type == null || type.isBlank()) {
|
||||
return EntityType.UNKNOWN;
|
||||
@@ -292,7 +315,7 @@ public class IrisEntity extends IrisRegistrant implements NativeEntityOptions {
|
||||
e.setGravity(isGravity());
|
||||
e.setInvulnerable(isInvulnerable());
|
||||
e.setSilent(isSilent());
|
||||
e.setPersistent(isKeepEntity() || IrisSettings.get().getWorld().isForcePersistEntities());
|
||||
applyPersistence(e, IrisSettings.get().getWorld().isForcePersistEntities());
|
||||
|
||||
int gg = 0;
|
||||
for (IrisEntity i : passengers) {
|
||||
@@ -333,8 +356,6 @@ public class IrisEntity extends IrisRegistrant implements NativeEntityOptions {
|
||||
l.setLeashHolder(getLeashHolder().spawn(gen, at, rng.nextParallelRNG(234548)));
|
||||
}
|
||||
|
||||
l.setRemoveWhenFarAway(isRemovable());
|
||||
|
||||
if (getHelmet() != null && LootResolver.oneIn(rng, getHelmet().getRarity())) {
|
||||
l.getEquipment().setHelmet(getHelmet().get(gen.isStudio(), rng));
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ import org.bukkit.HeightMap;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Entity;
|
||||
|
||||
@Snippet("entity-spawn")
|
||||
@@ -60,6 +61,8 @@ import org.bukkit.entity.Entity;
|
||||
@Description("Represents an entity spawn during initial chunk generation")
|
||||
@Data
|
||||
public class IrisEntitySpawn implements Rarity {
|
||||
private static final int CAVE_COLUMN_ATTEMPTS = 8;
|
||||
private static final int CAVE_VERTICAL_ATTEMPTS = 128;
|
||||
private final transient AtomicCache<RNG> rng = new AtomicCache<>();
|
||||
private final transient AtomicCache<IrisEntity> ent = new AtomicCache<>();
|
||||
@RegistryListResource(IrisEntity.class)
|
||||
@@ -78,25 +81,32 @@ public class IrisEntitySpawn implements Rarity {
|
||||
private transient IrisSpawner referenceSpawner;
|
||||
private transient IrisMarker referenceMarker;
|
||||
|
||||
public int spawn(Engine gen, Chunk c, RNG rng) {
|
||||
public int spawn(Engine gen, Chunk c, RNG rng, int remainingCapacity) {
|
||||
if (remainingCapacity <= 0) {
|
||||
return 0;
|
||||
}
|
||||
IrisEntity definition = getRealEntity(gen);
|
||||
if (definition == null) {
|
||||
return 0;
|
||||
}
|
||||
int spawns = LootResolver.inclusive(rng, minSpawns, maxSpawns);
|
||||
int spawns = Math.min(remainingCapacity, LootResolver.inclusive(rng, minSpawns, maxSpawns));
|
||||
int s = 0;
|
||||
|
||||
if (spawns > 0) {
|
||||
for (int id = 0; id < spawns; id++) {
|
||||
int x = (c.getX() * 16) + rng.i(16);
|
||||
int z = (c.getZ() * 16) + rng.i(16);
|
||||
World world = c.getWorld();
|
||||
int h = world.getHighestBlockYAt(x, z, HeightMap.OCEAN_FLOOR);
|
||||
int hf = world.getHighestBlockYAt(x, z, HeightMap.WORLD_SURFACE);
|
||||
IrisSpawnGroup group = getReferenceSpawner().getGroup();
|
||||
Integer y = selectSurfaceSpawnY(group, definition.getSurface(), h, hf, rng);
|
||||
Location l = group == IrisSpawnGroup.CAVE ? findCaveSpawnLocation(gen, c, rng)
|
||||
: y == null ? null : new Location(world, x, y, z);
|
||||
Location l;
|
||||
if (group == IrisSpawnGroup.CAVE) {
|
||||
l = findCaveSpawnLocation(gen, c, rng, definition.getSurface());
|
||||
} else {
|
||||
int x = (c.getX() << 4) + rng.i(16);
|
||||
int z = (c.getZ() << 4) + rng.i(16);
|
||||
World world = c.getWorld();
|
||||
int h = world.getHighestBlockYAt(x, z, HeightMap.OCEAN_FLOOR);
|
||||
int hf = world.getHighestBlockYAt(x, z, HeightMap.WORLD_SURFACE);
|
||||
Integer y = selectSurfaceSpawnY(group, definition.getSurface(), h, hf, rng);
|
||||
l = y == null ? null : new Location(world, x, y, z);
|
||||
}
|
||||
|
||||
if (l != null) {
|
||||
if (referenceSpawner.getAllowedLightLevels().getMin() > 0 || referenceSpawner.getAllowedLightLevels().getMax() < 15) {
|
||||
@@ -193,16 +203,50 @@ public class IrisEntitySpawn implements Rarity {
|
||||
return null;
|
||||
}
|
||||
|
||||
static Location findCaveSpawnLocation(Engine engine, Chunk chunk, RNG rng) {
|
||||
static Location findCaveSpawnLocation(Engine engine, Chunk chunk, RNG rng, IrisSurface surface) {
|
||||
if (J.isFolia()) {
|
||||
return null;
|
||||
if (!J.isOwnedByCurrentRegion(chunk.getWorld(), chunk.getX(), chunk.getZ())) {
|
||||
return null;
|
||||
}
|
||||
return findLiveCaveSpawnLocation(chunk, rng, surface);
|
||||
}
|
||||
KList<IrisPosition> markers = engine.getMantle().findMarkers(chunk.getX(), chunk.getZ(), MarkerMatter.CAVE_FLOOR);
|
||||
return selectCaveSpawnLocation(markers, chunk.getWorld(), rng);
|
||||
}
|
||||
|
||||
static Location findLiveCaveSpawnLocation(Chunk chunk, RNG rng, IrisSurface surface) {
|
||||
boolean fluid = surface.isFluid();
|
||||
World world = chunk.getWorld();
|
||||
int minimumY = world.getMinHeight() + 1;
|
||||
for (int attempt = 0; attempt < CAVE_COLUMN_ATTEMPTS; attempt++) {
|
||||
int localX = rng.i(1, 15);
|
||||
int localZ = rng.i(1, 15);
|
||||
int worldX = (chunk.getX() << 4) + localX;
|
||||
int worldZ = (chunk.getZ() << 4) + localZ;
|
||||
int maximumY = Math.min(world.getMaxHeight() - 3,
|
||||
world.getHighestBlockYAt(worldX, worldZ, HeightMap.OCEAN_FLOOR) - 1);
|
||||
if (maximumY < minimumY) {
|
||||
continue;
|
||||
}
|
||||
int span = maximumY - minimumY + 1;
|
||||
int startY = LootResolver.inclusive(rng, minimumY, maximumY);
|
||||
int samples = Math.min(span, CAVE_VERTICAL_ATTEMPTS);
|
||||
for (int sample = 0; sample < samples; sample++) {
|
||||
int y = minimumY + Math.floorMod(startY - minimumY - sample, span);
|
||||
Block block = chunk.getBlock(localX, y, localZ);
|
||||
if (fluid ? surface.matches(block) : isAir(block.getType())
|
||||
&& chunk.getBlock(localX, y - 1, localZ).isSolid()
|
||||
&& isAir(chunk.getBlock(localX, y + 1, localZ).getType())) {
|
||||
return new Location(world, worldX, y, worldZ);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Location selectCaveSpawnLocation(KList<IrisPosition> markers, World world, RNG rng) {
|
||||
return markers.convert((marker) -> BukkitPlatform.toLocation(marker, world).add(0, 1, 0)).getRandom(rng);
|
||||
IrisPosition marker = markers.getRandom(rng);
|
||||
return marker == null ? null : BukkitPlatform.toLocation(marker, world);
|
||||
}
|
||||
|
||||
private Entity spawn100(Engine g, Location at) {
|
||||
@@ -217,6 +261,9 @@ public class IrisEntitySpawn implements Rarity {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (J.isFolia() && !J.isOwnedByCurrentRegion(at.getWorld(), at.getBlockX() >> 4, at.getBlockZ() >> 4)) {
|
||||
return null;
|
||||
}
|
||||
IrisSurface surface = irisEntity.getSurface();
|
||||
boolean checkPosition = !ignoreSurfaces || surface.isFluid();
|
||||
if (checkPosition && !surface.matches(at.clone().subtract(0, surface.isFluid() ? 0 : 1, 0).getBlock())) {
|
||||
@@ -231,7 +278,7 @@ public class IrisEntitySpawn implements Rarity {
|
||||
}
|
||||
}
|
||||
|
||||
Entity e = irisEntity.spawn(g, at.clone().add(0.5, 0.5, 0.5), rng.aquire(() -> new RNG(g.getSeedManager().getEntity())));
|
||||
Entity e = irisEntity.spawn(g, at.clone().add(0.5, surface.isFluid() ? 0.5 : 0, 0.5), rng.aquire(() -> new RNG(g.getSeedManager().getEntity())));
|
||||
if (e != null) {
|
||||
IrisLogging.debug("Spawned " + C.DARK_AQUA + "Entity<" + getEntity() + "> " + C.GREEN + e.getType() + C.LIGHT_PURPLE + " @ " + C.GRAY + e.getLocation().getX() + ", " + e.getLocation().getY() + ", " + e.getLocation().getZ());
|
||||
}
|
||||
@@ -244,26 +291,38 @@ public class IrisEntitySpawn implements Rarity {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isAir(Material material) {
|
||||
return material == Material.AIR || material == Material.CAVE_AIR || material == Material.VOID_AIR;
|
||||
}
|
||||
|
||||
private boolean isAreaClearForSpawn(Location center, Vector3d boundingBox, IrisSurface surface) {
|
||||
World world = center.getWorld();
|
||||
boolean fluid = surface.isFluid();
|
||||
int startX = fluid ? (int) Math.floor(center.getX() + 0.5 - boundingBox.x / 2)
|
||||
: center.getBlockX() - (int) (boundingBox.x / 2);
|
||||
int endX = fluid ? (int) Math.floor(Math.nextDown(center.getX() + 0.5 + boundingBox.x / 2))
|
||||
: center.getBlockX() + (int) (boundingBox.x / 2);
|
||||
int startY = fluid ? (int) Math.floor(center.getY() + 0.5) : center.getBlockY();
|
||||
int endY = fluid ? (int) Math.floor(Math.nextDown(center.getY() + 0.5 + boundingBox.y))
|
||||
: center.getBlockY() + (int) boundingBox.y;
|
||||
int startZ = fluid ? (int) Math.floor(center.getZ() + 0.5 - boundingBox.z / 2)
|
||||
: center.getBlockZ() - (int) (boundingBox.z / 2);
|
||||
int endZ = fluid ? (int) Math.floor(Math.nextDown(center.getZ() + 0.5 + boundingBox.z / 2))
|
||||
: center.getBlockZ() + (int) (boundingBox.z / 2);
|
||||
int startX = (int) Math.floor(center.getX() + 0.5 - boundingBox.x / 2);
|
||||
int endX = (int) Math.floor(Math.nextDown(center.getX() + 0.5 + boundingBox.x / 2));
|
||||
double baseY = center.getY() + (fluid ? 0.5 : 0);
|
||||
int startY = (int) Math.floor(baseY);
|
||||
int endY = (int) Math.floor(Math.nextDown(baseY + boundingBox.y));
|
||||
int startZ = (int) Math.floor(center.getZ() + 0.5 - boundingBox.z / 2);
|
||||
int endZ = (int) Math.floor(Math.nextDown(center.getZ() + 0.5 + boundingBox.z / 2));
|
||||
|
||||
if (startY < world.getMinHeight() || endY >= world.getMaxHeight()) {
|
||||
return false;
|
||||
}
|
||||
if (J.isFolia()) {
|
||||
for (int chunkX = startX >> 4; chunkX <= endX >> 4; chunkX++) {
|
||||
for (int chunkZ = startZ >> 4; chunkZ <= endZ >> 4; chunkZ++) {
|
||||
if (!J.isOwnedByCurrentRegion(world, chunkX, chunkZ)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int x = startX; x <= endX; x++) {
|
||||
for (int y = startY; y <= endY; y++) {
|
||||
for (int z = startZ; z <= endZ; z++) {
|
||||
if (fluid ? !surface.matches(world.getBlockAt(x, y, z))
|
||||
: world.getBlockAt(x, y, z).getType() != Material.AIR) {
|
||||
: !isAir(world.getBlockAt(x, y, z).getType())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Accessors(chain = true)
|
||||
@@ -62,7 +63,7 @@ public class IrisSpawner extends IrisRegistrant {
|
||||
@Description("The entity spawns to add initially. EXECUTES PER CHUNK!")
|
||||
private KList<IrisEntitySpawn> initialSpawns = new KList<>();
|
||||
|
||||
@Description("This spawner will not spawn in a given chunk if that chunk has more than the defined amount of living entities.")
|
||||
@Description("Maximum ambient entities of the selected entity's native spawn category in one chunk. Spawn groups are limited to the remaining capacity.")
|
||||
private int maxEntitiesPerChunk = 1;
|
||||
|
||||
@Description("The block of 24 hour time to contain this spawn in.")
|
||||
@@ -83,6 +84,21 @@ public class IrisSpawner extends IrisRegistrant {
|
||||
@Description("Where should these spawns be placed")
|
||||
private IrisSpawnGroup group = IrisSpawnGroup.NORMAL;
|
||||
|
||||
public int remainingCapacity(IrisEntity entity, Map<String, Integer> counts) {
|
||||
if (entity == null) {
|
||||
return 0;
|
||||
}
|
||||
String category = entity.spawnCategory();
|
||||
int count = counts.getOrDefault(category, 0);
|
||||
if ("misc".equals(category)) {
|
||||
count = 0;
|
||||
for (int population : counts.values()) {
|
||||
count += population;
|
||||
}
|
||||
}
|
||||
return Math.max(0, maxEntitiesPerChunk - count);
|
||||
}
|
||||
|
||||
public boolean isValid(IrisBiome biome) {
|
||||
return switch (group) {
|
||||
case NORMAL -> switch (biome.getInferredType()) {
|
||||
|
||||
@@ -64,6 +64,13 @@ public final class FloatingBiomeOverlay {
|
||||
return height;
|
||||
}
|
||||
|
||||
public FloatingBiomeOverlay copy() {
|
||||
FloatingBiomeOverlay copy = new FloatingBiomeOverlay(height);
|
||||
System.arraycopy(volume, 0, copy.volume, 0, volume.length);
|
||||
System.arraycopy(surfaces, 0, copy.surfaces, 0, surfaces.length);
|
||||
return copy;
|
||||
}
|
||||
|
||||
private int index(int localX, int y, int localZ) {
|
||||
return ((localX >> 2) * 4 + (localZ >> 2)) * quartHeight + (y >> 2);
|
||||
}
|
||||
|
||||
@@ -457,9 +457,7 @@ public final class GenerationHistory {
|
||||
"signatureCapture"
|
||||
);
|
||||
try (GenerationAdmission.CutoverLease ignored = admission.beginStartupCutover()) {
|
||||
synchronized (this) {
|
||||
return promotePendingLocked(requiredCapture, true, true);
|
||||
}
|
||||
return promoteCaptured(requiredCapture, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,9 +476,7 @@ public final class GenerationHistory {
|
||||
|
||||
GenerationActivation promote(BoundarySignatureCapture signatureCapture) throws IOException {
|
||||
requireOpen();
|
||||
synchronized (GenerationHistory.this) {
|
||||
return promotePendingLocked(Objects.requireNonNull(signatureCapture, "signature capture"), false, true);
|
||||
}
|
||||
return promoteCaptured(Objects.requireNonNull(signatureCapture, "signature capture"), false);
|
||||
}
|
||||
|
||||
private void requireOpen() {
|
||||
@@ -497,34 +493,67 @@ public final class GenerationHistory {
|
||||
}
|
||||
}
|
||||
|
||||
private GenerationActivation promoteCaptured(
|
||||
BoundarySignatureCapture signatureCapture, boolean startupRecovery
|
||||
) throws IOException {
|
||||
Optional<PreparedPromotion> prepared;
|
||||
synchronized (this) {
|
||||
prepared = preparePromotionLocked(startupRecovery, true);
|
||||
}
|
||||
if (prepared.isEmpty()) {
|
||||
return activeActivation();
|
||||
}
|
||||
TerrainBoundarySignatureStore.Snapshot terrainSnapshot = publishTerrainSignatures(prepared.get(), signatureCapture);
|
||||
synchronized (this) {
|
||||
return completePromotionLocked(prepared.get(), terrainSnapshot);
|
||||
}
|
||||
}
|
||||
|
||||
private GenerationActivation promotePendingLocked(
|
||||
BoundarySignatureCapture signatureCapture, boolean startupRecovery, boolean validateSemantics
|
||||
) throws IOException {
|
||||
Optional<PreparedPromotion> prepared = preparePromotionLocked(startupRecovery, validateSemantics);
|
||||
if (prepared.isEmpty()) {
|
||||
return store.activeActivation();
|
||||
}
|
||||
return completePromotionLocked(prepared.get(), publishTerrainSignatures(prepared.get(), signatureCapture));
|
||||
}
|
||||
|
||||
private Optional<PreparedPromotion> preparePromotionLocked(
|
||||
boolean startupRecovery, boolean validateSemantics
|
||||
) throws IOException {
|
||||
sync();
|
||||
WorldChunkInventory inventory = recoverUnstoredClaims(startupRecovery);
|
||||
validateReferencedState(Optional.empty(), validateSemantics);
|
||||
Optional<GenerationActivation> pending = store.pendingActivation();
|
||||
if (pending.isEmpty()) {
|
||||
return store.activeActivation();
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
long outgoingActivationId = store.activeActivation().activationId();
|
||||
ownership.assignUnassigned(inventory, outgoingActivationId);
|
||||
ownership.persist();
|
||||
requireExplicitOwnership(inventory);
|
||||
GenerationBoundary boundary = boundaries.publishOwnership(
|
||||
pending.get().activationId(),
|
||||
ownership
|
||||
);
|
||||
TerrainBoundarySignatureStore.Snapshot terrainSnapshot;
|
||||
long pendingActivationId = pending.get().activationId();
|
||||
return Optional.of(new PreparedPromotion(pendingActivationId, boundaries.publishOwnership(pendingActivationId, ownership)));
|
||||
}
|
||||
|
||||
private TerrainBoundarySignatureStore.Snapshot publishTerrainSignatures(
|
||||
PreparedPromotion prepared, BoundarySignatureCapture signatureCapture
|
||||
) throws IOException {
|
||||
try (TerrainBoundarySignatureStore.SignatureSampler sampler = Objects.requireNonNull(
|
||||
signatureCapture.capture(boundary), "boundary signature sampler"
|
||||
signatureCapture.capture(prepared.boundary()), "boundary signature sampler"
|
||||
)) {
|
||||
terrainSnapshot = terrainSignatures.publish(pending.get().activationId(), boundary, sampler);
|
||||
return terrainSignatures.publish(prepared.activationId(), prepared.boundary(), sampler);
|
||||
}
|
||||
}
|
||||
|
||||
private GenerationActivation completePromotionLocked(
|
||||
PreparedPromotion prepared, TerrainBoundarySignatureStore.Snapshot terrainSnapshot
|
||||
) throws IOException {
|
||||
GenerationActivation completed = store.completePendingTransition(
|
||||
pending.get().activationId(),
|
||||
boundary.identity(),
|
||||
prepared.activationId(),
|
||||
prepared.boundary().identity(),
|
||||
terrainSnapshot.identity()
|
||||
);
|
||||
GenerationActivation activated = store.activatePending(completed.activationId());
|
||||
@@ -1396,6 +1425,9 @@ public final class GenerationHistory {
|
||||
public interface BoundarySignatureCapture {
|
||||
TerrainBoundarySignatureStore.SignatureSampler capture(GenerationBoundary boundary) throws IOException;
|
||||
}
|
||||
|
||||
private record PreparedPromotion(long activationId, GenerationBoundary boundary) {
|
||||
}
|
||||
public record FreshCreation(Path dimensionRoot, Path packSource, String packFingerprint, long worldSeed,
|
||||
GenerationEpoch.DimensionContract dimensionContract, GenerationRegistryContract registryContract) {
|
||||
public FreshCreation {
|
||||
|
||||
@@ -105,6 +105,11 @@ public class AsyncOrMedievalPregenMethod implements PregeneratorMethod {
|
||||
method.reclaimMemory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int inFlightChunks() {
|
||||
return method.inFlightChunks();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mantle getMantle() {
|
||||
return method.getMantle();
|
||||
|
||||
@@ -1127,6 +1127,11 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int inFlightChunks() {
|
||||
return Math.max(0, inFlight.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mantle getMantle() {
|
||||
if (IrisToolbelt.isIrisWorld(world)) {
|
||||
|
||||
@@ -213,6 +213,11 @@ public class CachedPregenMethod implements PregeneratorMethod {
|
||||
method.reclaimMemory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int inFlightChunks() {
|
||||
return method.inFlightChunks();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mantle getMantle() {
|
||||
return method.getMantle();
|
||||
|
||||
@@ -98,6 +98,11 @@ public class HybridPregenMethod implements PregeneratorMethod {
|
||||
inWorld.reclaimMemory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int inFlightChunks() {
|
||||
return inWorld.inFlightChunks();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mantle getMantle() {
|
||||
return inWorld.getMantle();
|
||||
|
||||
@@ -464,6 +464,10 @@ public class IrisPregenerator {
|
||||
return failed.get();
|
||||
}
|
||||
|
||||
public int getInFlightChunks() {
|
||||
return generator.inFlightChunks();
|
||||
}
|
||||
|
||||
public PregenRates getRates() {
|
||||
return rates;
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ public class MedievalPregenMethod implements PregeneratorMethod {
|
||||
private static final long UNLOAD_TIMEOUT_SECONDS = 120L;
|
||||
private final World world;
|
||||
private final KList<CompletableFuture<?>> futures;
|
||||
private final AtomicInteger inFlight;
|
||||
private final Map<Chunk, Long> lastUse;
|
||||
private final int maxFutures;
|
||||
private final AtomicBoolean directAsyncDisabled;
|
||||
@@ -67,6 +68,7 @@ public class MedievalPregenMethod implements PregeneratorMethod {
|
||||
this.chunkFlush = new PregenSerialWorker("Iris Pregen Chunk Flush", world == null ? "unknown" : world.getName());
|
||||
this.chunkIoExecutor = chunkFlush.executor();
|
||||
futures = new KList<>();
|
||||
this.inFlight = new AtomicInteger();
|
||||
this.lastUse = new ConcurrentHashMap<>();
|
||||
int configuredThreads = IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism());
|
||||
this.maxFutures = J.isFolia() ? Math.max(2, Math.min(64, configuredThreads)) : Math.max(16, Math.min(128, configuredThreads * 4));
|
||||
@@ -229,6 +231,7 @@ public class MedievalPregenMethod implements PregeneratorMethod {
|
||||
}
|
||||
|
||||
listener.onChunkGenerating(x, z);
|
||||
inFlight.incrementAndGet();
|
||||
// Single choke point for failure accounting: without onChunkFailed a lossy run can
|
||||
// never satisfy allVisitsComplete, so a finished pregen reports as aborted forever.
|
||||
CompletableFuture<?> chunkFuture = J.isFolia()
|
||||
@@ -247,6 +250,7 @@ public class MedievalPregenMethod implements PregeneratorMethod {
|
||||
})
|
||||
: scheduleChunkLoad(x, z, listener);
|
||||
futures.add(chunkFuture.whenComplete((r, err) -> {
|
||||
inFlight.decrementAndGet();
|
||||
if (err != null) {
|
||||
listener.onChunkFailed(x, z);
|
||||
}
|
||||
@@ -358,6 +362,11 @@ public class MedievalPregenMethod implements PregeneratorMethod {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int inFlightChunks() {
|
||||
return Math.max(0, inFlight.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mantle getMantle() {
|
||||
if (IrisToolbelt.isIrisWorld(world)) {
|
||||
|
||||
@@ -95,5 +95,9 @@ public interface PregeneratorMethod {
|
||||
default void reclaimMemory() {
|
||||
}
|
||||
|
||||
default int inFlightChunks() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
Mantle getMantle();
|
||||
}
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package art.arcane.iris.generation.biome;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class FloatingIslandMemoContextTest {
|
||||
@After
|
||||
public void clearMemo() {
|
||||
FloatingIslandSample.clearThreadCaches();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectedColumnsAreCachedOnlyForTheirParentAndContext() {
|
||||
FloatingIslandSample.clearThreadCaches();
|
||||
IrisBiome first = parent();
|
||||
IrisBiome second = parent();
|
||||
IrisData data = mock(IrisData.class);
|
||||
Engine engine = mock(Engine.class);
|
||||
FloatingIslandBoundarySampler boundary = new FloatingIslandBoundarySampler((x, z) -> first);
|
||||
sample(first, 320, 7L, data, engine, boundary);
|
||||
sample(first, 320, 7L, data, engine, boundary);
|
||||
sample(second, 320, 7L, data, engine, boundary);
|
||||
verify(first, times(1)).getFloatingChildBiomes();
|
||||
verify(second, times(1)).getFloatingChildBiomes();
|
||||
sample(first, 320, 9L, data, engine, boundary);
|
||||
sample(first, 128, 9L, data, engine, boundary);
|
||||
sample(first, 128, 9L, data, mock(Engine.class), boundary);
|
||||
sample(first, 128, 9L, mock(IrisData.class), engine, boundary);
|
||||
sample(first, 128, 9L, data, engine, new FloatingIslandBoundarySampler((x, z) -> first));
|
||||
verify(first, times(6)).getFloatingChildBiomes();
|
||||
}
|
||||
|
||||
private static void sample(IrisBiome parent, int height, long seed, IrisData data, Engine engine,
|
||||
FloatingIslandBoundarySampler boundary) {
|
||||
assertNull(FloatingIslandSample.sampleMemoized(parent, -16, 32, height, seed, data, engine, boundary));
|
||||
}
|
||||
|
||||
private static IrisBiome parent() {
|
||||
IrisBiome parent = mock(IrisBiome.class);
|
||||
when(parent.getFloatingChildBiomes()).thenReturn(new KList<>());
|
||||
return parent;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user