Compare commits

...
Author SHA1 Message Date
Brian Neumann-Fopiano 12e8bb5a09 v 2026-10-02 17:13:30 -04:00
Brian Neumann-Fopiano a765e76aa3 Restore natural mob spawning
- Restore cave spawning and correct floor placement across platforms.
- Enforce category population limits and clamp ambient spawn batches.
- Honor mob-spawning rules and entity-ticking chunk admission.
- Separate chunk saving from configurable distance-despawn retention.
- Add spawning regressions and Folia gameplay acceptance coverage.
2026-10-02 16:55:52 -04:00
Brian Neumann-Fopiano 4ef6fc7cc6 Enforce seed cohesion
- Scope cached generation fields to their seed and context.
- Warm decorators and ores with their canonical generation seeds.
- Stabilize object traversal and procedural placement choices.
- Isolate smart-bore variants from shared source objects.
- Pin noise and full-engine output across generation orders.
- Verify same-jar generation through cold server restarts.
2026-10-02 14:43:48 -04:00
Brian Fopiano 6b82ea99e2 Isolate scaled noise generators
- Isolate biome-child and carving-child noise generators.
- Isolate decorator variance generators.
- Share fresh scaled-generator construction.
- Test shared-generator isolation at each caller.
2026-10-02 07:48:01 -04:00
Brian Fopiano 813f3c7bcf Fix continent zoom compounding
- Build independent continent generators before applying zoom.
- Share continent construction across runtime contexts.
- Cover repeated construction and shared-generator isolation.
2026-10-02 07:47:31 -04:00
CeeVeeR 318c1e100b chore:
- Extracted shared style generator sampling to GeneratorSampling.sampleSharedGenerator

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:37:51 +02:00
CeeVeeR 357f14e900 chore:
- Extracted shared SAMPLE_BLOCKS to GeneratorSampling

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:36:55 +02:00
CeeVeeR 99cc2383d6 chore:
- Replaced IrisBiome children generator scaling with createScaledGenerator

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:35:22 +02:00
CeeVeeR 76d71f8344 chore:
- Replaced IrisDimensionCarvingEntry shrink scaling with createScaledGenerator

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:34:21 +02:00
CeeVeeR 1544c34f44 chore:
- Moved continent zoom scaling to IrisGeneratorStyle.createScaledGenerator

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:33:17 +02:00
CeeVeeR 00601678d9 fix:
- Replaced IrisGeneratorStyle.create with createNoCache in IrisDecorator
- Removed @ExpectedToFailUntilFixed from the IrisDecorator test

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:17:57 +02:00
CeeVeeR 700dc53559 fix:
- Replaced IrisGeneratorStyle.create with createNoCache in IrisBiome
- Removed @ExpectedToFailUntilFixed from the IrisBiome test

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:17:19 +02:00
CeeVeeR 0faae93049 fix:
- Replaced IrisGeneratorStyle.create with createNoCache in IrisDimensionCarvingEntry
- Removed @ExpectedToFailUntilFixed from the IrisDimensionCarvingEntry test

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:16:41 +02:00
CeeVeeR 92310ef839 test:
- Added decorator shared variance style test, expected to fail until the fix

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:15:33 +02:00
CeeVeeR 5fa6d82b79 chore:
- Extracted first variance generator build to buildVarianceGeneratorOnce

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:14:58 +02:00
CeeVeeR 2078bece40 chore:
- Extracted variance generator construction to createVarianceGenerator

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:14:23 +02:00
CeeVeeR 077b48f47f chore:
- Extracted variance seed derivation to deriveVarianceSeed

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:13:48 +02:00
CeeVeeR 7981fa3ce5 test:
- Added biome shared child style test, expected to fail until the fix

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:11:32 +02:00
CeeVeeR 5c4f7bb521 chore:
- Extracted children generator construction to createChildrenGenerator

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:10:44 +02:00
CeeVeeR 0d16f8e57f chore:
- Extracted magic value 2137 to CHILDREN_SEED_SALT

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:10:10 +02:00
CeeVeeR cf7ebcece1 chore:
- Extracted child style seed derivation to deriveChildStyleSeed

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:09:35 +02:00
CeeVeeR 8f8a3b2355 chore:
- Extracted magic value 0.0001 to MINIMUM_CHILD_SHRINK_FACTOR

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:08:42 +02:00
CeeVeeR ffc409a4ac chore:
- Extracted magic value 2137 to CHILDREN_SEED_SALT

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:08:07 +02:00
CeeVeeR 5b5c1975c6 chore:
- Extracted generator sampling and running engine pack data to testsupport

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 12:07:10 +02:00
Brian Neumann-Fopiano 25c04c0157 Integrate optional terrain transforms
- Add versioned terrain providers that run before content placement.
- Resolve transformed terrain heights, occupancy, and cave metadata through bounded caches.
- Initialize native structures after binding the modded generation runtime.
- Cover transform behavior, provider resolution, and structure binding with regression tests.
2026-10-02 05:39:20 -04:00
CeeVeeR 1e25c07eac test:
- Added carving entry shared child style test, expected to fail until the fix

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 07:31:17 +02:00
CeeVeeR ff74a6f811 chore:
- Extracted children generator construction to createChildrenGenerator

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 07:30:25 +02:00
CeeVeeR 9e07009f1d chore:
- Extracted child shrink scaling to scaleByShrinkFactor

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 07:29:53 +02:00
CeeVeeR 53172c38ad chore:
- Extracted child style seed derivation to deriveChildStyleSeed

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 07:29:21 +02:00
CeeVeeR 658c6b0fde chore:
- Extracted children generator seed derivation to deriveChildrenGeneratorSeed

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 07:28:29 +02:00
CeeVeeR b998ff535a chore:
- Extracted entry id hashing to hashOfId

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 07:27:56 +02:00
CeeVeeR ba4708f2bb chore:
- Renamed takeSharedGenerator to createContinentalStyleGenerator

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 05:23:25 +02:00
CeeVeeR bda770650b fix:
- Replaced IrisGeneratorStyle.create with createNoCache in ContinentGenerator
- Removed @ExpectedToFailUntilFixed from the continent tests

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 05:14:52 +02:00
CeeVeeR 8bccb48c09 test:
- Added shared style generator test, expected to fail until the fix

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 05:13:26 +02:00
CeeVeeR a9df19cae2 chore:
- Opened ContinentGenerator.deriveContinentSeed to the package

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 05:13:22 +02:00
CeeVeeR 4ac49baaff test:
- Added overworld continental style rebuild test, expected to fail until the fix

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 05:12:56 +02:00
CeeVeeR 38a7e0fdb5 test:
- Added second rebuild test, expected to fail until the fix

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 05:12:51 +02:00
CeeVeeR 7e73e8f599 test:
- Added rebuild test, expected to fail until the fix

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 05:12:47 +02:00
CeeVeeR 788ac06713 test:
- Added continent zoom stretch test

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 05:12:43 +02:00
CeeVeeR 622b40f786 test:
- Added ContinentGeneratorTest checking the continent map at world start

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 05:12:40 +02:00
CeeVeeR 72eff9b775 feat:
- Added ExpectedToFailUntilFixed marker and rule

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 05:12:37 +02:00
CeeVeeR 789644b93f chore:
- Replaced DimensionTerrainContext continent chain with ContinentGenerator

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 04:23:14 +02:00
CeeVeeR f07dfb7a85 chore:
- Inlined IrisComplex.createContinentGenerator

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 04:22:42 +02:00
CeeVeeR 5bffa6eb04 chore:
- Moved continent generator construction to ContinentGenerator

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 04:22:10 +02:00
CeeVeeR 0b6ac19fa7 chore:
- Extracted continent generator construction to createContinentGenerator

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 04:21:38 +02:00
CeeVeeR b7e6259a05 chore:
- Extracted shared continent generator lookup to takeSharedContinentGenerator

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 04:21:05 +02:00
CeeVeeR a0aa62b8c6 chore:
- Extracted continent zoom scaling to scaleByContinentZoom

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 04:20:32 +02:00
CeeVeeR d80fcedd91 chore:
- Extracted continent seed derivation to createContinentSeedSource

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 04:19:59 +02:00
CeeVeeR 41664fc605 chore:
- Extracted magic value 234234565 to CONTINENT_SEED_SALT

Ref.: https://github.com/VolmitSoftware/Iris/issues/1251
2026-10-02 04:19:27 +02:00
Brian Neumann-Fopiano 74df63f717 Merged React metric fixes
- Pregen queue reports in-flight chunk requests
- Pregen remaining reports chunks left
- Generation timings go unavailable after ten seconds without generation
2026-09-30 10:10:15 -04:00
Brian Neumann-Fopiano f46041690b Test timing freshness gate
- Test that capture drops generation timings once generation is idle
- Test that capture carries the engine timing averages while fresh
- Remove the hand-built idle snapshot test that passed without the gate
2026-09-29 18:37:15 -04:00
Brian Neumann-Fopiano b8433340c9 Split pregen queue metrics
- Publish in-flight pregen chunk requests as iris.pregen-queue
- Publish chunks left in the job as iris.pregen-remaining
- Mark generation timings unavailable after 10 seconds without generation
2026-09-29 18:09:55 -04:00
Brian Neumann-Fopiano 593c3f732a Studio hotload deadlock fix
- Live boundary capture runs without the generation history lock
- Promotion prepares the boundary and activates under the lock in separate steps
- Tab completion and find lookups stay responsive while a Studio save captures terrain
- Admission test covers history reads during the boundary checkpoint
2026-09-29 11:58:25 -04:00
150 changed files with 6427 additions and 421 deletions
@@ -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;
}
}
@@ -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;
@@ -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);
@@ -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'
}
@@ -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'
@@ -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) {
}
}
@@ -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"
]
}
@@ -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"
]
}
@@ -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] }))
}
})
}
}
@@ -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));
}
}
@@ -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
+1 -1
View File
@@ -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 ->
+1 -1
View File
@@ -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 ->
@@ -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();
@@ -61,11 +61,12 @@ public final class ModdedEntitySpawner {
}
double x = blockX + 0.5;
double y = blockY + 0.5;
double verticalOffset = irisEntity.getSurface().isFluid() ? 0.5 : 0;
double y = blockY + verticalOffset;
double z = blockZ + 0.5;
boolean riseEffectActive = irisEntity.isSpawnEffectRiseOutOfGround() && level.hasPlayersNearby(new NativeEntityRuntime.Position(x, y, z), PLAYER_EFFECT_RADIUS);
int spawnBlockY = riseEffectActive ? blockY - 5 : blockY;
double spawnY = spawnBlockY + 0.5;
double spawnY = spawnBlockY + verticalOffset;
NativeSpawnedEntity created = create(irisEntity, level, x, spawnY, z);
if (created == null) {
@@ -98,8 +99,7 @@ public final class ModdedEntitySpawner {
String customName = irisEntity.getCustomName();
entity.applyBase(irisEntity, customName == null ? null : colorize(customName));
boolean persistent = irisEntity.isKeepEntity() || forcePersist();
applyPersistence(entity, persistent);
applyPersistence(irisEntity, entity, forcePersist());
applyPassengers(engine, irisEntity, entity, level, blockX, blockY, blockZ, rng);
@@ -273,8 +273,8 @@ public final class ModdedEntitySpawner {
return IrisSettings.get().getWorld().isForcePersistEntities();
}
private static void applyPersistence(NativeSpawnedEntity entity, boolean persistent) {
if (!entity.persistence(persistent)) {
static void applyPersistence(IrisEntity options, NativeSpawnedEntity entity, boolean forcePersistence) {
if (!entity.persistence(options.isKeepEntity() || forcePersistence)) {
String type = entity.typeKey();
if (WARNED_PERSISTENCE_TYPES.add(type)) {
IrisLogging.warn("Iris entity: vanilla cannot persist non-serializable entity type '" + type + "'");
@@ -55,6 +55,7 @@ import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterMarker;
import art.arcane.volmlib.util.matter.slices.MarkerMatter;
import java.util.Map;
import java.util.HashSet;
import java.util.List;
import java.util.ArrayList;
@@ -511,13 +512,16 @@ public final class ModdedWorldManager implements EngineWorldManager {
}
private void spawnAmbient(NativeWorld level, int chunkX, int chunkZ, boolean initial, BiomeEnvironment environment) {
if (!NativeSpawnQueries.ambientAllowed(level, chunkX, chunkZ, initial)) {
return;
}
IrisComplex complex = engine.getComplex();
if (complex == null) {
return;
}
IrisBiome biome = environment.biome();
int chunkMobs = countChunkLivingEntities(level, chunkX, chunkZ);
Map<String, Integer> chunkMobs = NativeSpawnQueries.livingEntityCategories(level, chunkX, chunkZ);
KList<IrisEntitySpawn> pool = new KList<>();
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.dimension().getEntitySpawners()), biome, chunkX, chunkZ, chunkMobs, initial);
@@ -535,20 +539,18 @@ public final class ModdedWorldManager implements EngineWorldManager {
if (!canSpawn(spawner, chunkX, chunkZ)) {
return;
}
int spawned = spawnEntry(level, chosen, spawner, chunkX, chunkZ);
int spawned = spawnEntry(level, chosen, spawner, chunkX, chunkZ,
remainingCapacity(chosen, spawner, chunkMobs));
if (spawned > 0) {
spawner.spawn(engine, chunkX, chunkZ);
}
}
private void collectSpawns(KList<IrisEntitySpawn> pool, KList<IrisSpawner> spawners, IrisBiome biomeFilter, int chunkX, int chunkZ, int chunkMobs, boolean initial) {
private void collectSpawns(KList<IrisEntitySpawn> pool, KList<IrisSpawner> spawners, IrisBiome biomeFilter, int chunkX, int chunkZ, Map<String, Integer> chunkMobs, boolean initial) {
for (IrisSpawner spawner : spawners) {
if (spawner == null) {
continue;
}
if (spawner.getMaxEntitiesPerChunk() <= chunkMobs) {
continue;
}
if (biomeFilter != null && !spawner.isValid(biomeFilter)) {
continue;
}
@@ -559,12 +561,19 @@ public final class ModdedWorldManager implements EngineWorldManager {
for (IrisEntitySpawn entry : spawns) {
entry.setReferenceSpawner(spawner);
entry.setReferenceMarker(spawner.getReferenceMarker());
pool.add(entry);
if (remainingCapacity(entry, spawner, chunkMobs) > 0) {
pool.add(entry);
}
}
}
}
private int spawnEntry(NativeWorld level, IrisEntitySpawn entry, IrisSpawner spawner, int chunkX, int chunkZ) {
private int remainingCapacity(IrisEntitySpawn entry, IrisSpawner spawner, Map<String, Integer> counts) {
IrisEntity entity = entry.getRealEntity(engine);
return spawner.remainingCapacity(entity, counts);
}
private int spawnEntry(NativeWorld level, IrisEntitySpawn entry, IrisSpawner spawner, int chunkX, int chunkZ, int remainingCapacity) {
IrisEntity irisEntity = entry.getRealEntity(engine);
if (irisEntity == null) {
return 0;
@@ -572,7 +581,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
int min = entry.getMinSpawns();
int max = entry.getMaxSpawns();
int count = LootResolver.inclusive(RNG.r, min, max);
int count = Math.min(Math.max(0, remainingCapacity), LootResolver.inclusive(RNG.r, min, max));
if (count <= 0) {
return 0;
}
@@ -593,7 +602,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
}
IrisPosition caveFloor = caveFloors.getRandom(RNG.r);
worldX = caveFloor.getX();
worldY = caveFloor.getY() + 1;
worldY = caveFloor.getY();
worldZ = caveFloor.getZ();
} else {
worldX = (chunkX << 4) + RNG.r.i(16);
@@ -708,10 +717,6 @@ public final class ModdedWorldManager implements EngineWorldManager {
|| NativeSpawnQueries.solid(level.getBlock(worldX, worldY + 2, worldZ));
}
private int countChunkLivingEntities(NativeWorld level, int chunkX, int chunkZ) {
return NativeSpawnQueries.livingEntities(level, chunkX, chunkZ);
}
/**
* ServerChunkCache.tickChunks rebuilds NaturalSpawner.SpawnState every tick from every entity in the
* level, so read that instead of walking all entities again on an Iris timer. The count is the natural
@@ -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;
}
}
@@ -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",
@@ -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;
}
}
@@ -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();
}
}
}
@@ -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;
}
}
@@ -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();
}
}
+1 -1
View File
@@ -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
View File
@@ -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;
}
@@ -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;
}
@@ -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);
}
@@ -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,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) {
}
}
@@ -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;
}
@@ -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<>();
}
@@ -0,0 +1,6 @@
package art.arcane.iris.generation.terrain.transform;
@FunctionalInterface
public interface TerrainColumnTransformer {
void transform(TerrainTransformContext context, int localX, int localZ);
}
@@ -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);
}
@@ -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);
}
@@ -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");
}
}
}
@@ -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);
}
}
}
@@ -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();
}
@@ -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