Refined Iris generation core

- Terrain, hydrology, and feature generation avoid repeated work and temporary allocations
- Deposits preserve configured ordering and independent chance samples
- Object transforms and layer boundaries handle previously failing cases
- Runtime shutdown, storage failures, and world reload retain resource ownership through completion
This commit is contained in:
Brian Neumann-Fopiano
2026-09-06 04:52:13 -04:00
90 changed files with 4602 additions and 1067 deletions
+2
View File
@@ -74,3 +74,5 @@ BUILDS/
# Version-content compat lane worktree. Generated, never source.
.compatwt/
.worktrees/
@@ -28,6 +28,7 @@ import art.arcane.iris.engine.history.GenerationActivation;
import art.arcane.iris.engine.history.GenerationEpoch;
import art.arcane.iris.engine.history.GenerationEpochContractFactory;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.engine.history.GenerationAdmission;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.history.IrisBoundarySignatureSampler;
import art.arcane.iris.engine.history.TransitionGenerationPlan;
@@ -191,44 +192,46 @@ public final class ModdedWorldEngines {
ModdedGenerationHistoryStorage.ActivePack opened
) throws IOException {
GenerationHistory history = opened.history();
history.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
opened = ModdedGenerationHistoryStorage.resolveActive(history);
long openedActivationId = history.activeActivation().activationId();
IrisEngine candidate = buildEngine(level, seed, opened);
boolean ready = false;
try {
GenerationHistoryRuntimeRouter router = candidate.attachGenerationHistory(
history,
IrisBoundarySignatureSampler.INSTANCE,
IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()
);
router.preloadActiveRuntimes();
if (history.activeActivation().activationId() != openedActivationId) {
close(candidate);
ModdedGenerationHistoryStorage.ActivePack promoted =
ModdedGenerationHistoryStorage.resolveActive(history);
candidate = buildEngine(level, seed, promoted);
router = GenerationHistoryRuntimeRouter.attach(
candidate,
try (GenerationAdmission.RuntimeLease startupAdmission = history.retainRuntime()) {
history.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
opened = ModdedGenerationHistoryStorage.resolveActive(history);
long openedActivationId = history.activeActivation().activationId();
IrisEngine candidate = buildEngine(level, seed, opened);
boolean ready = false;
try {
GenerationHistoryRuntimeRouter router = candidate.attachGenerationHistory(
history,
IrisBoundarySignatureSampler.INSTANCE
IrisBoundarySignatureSampler.INSTANCE,
IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()
);
router.preloadActiveRuntimes();
}
requireReady(candidate, level, ModdedGenerationMode.PERSISTENT_RESTORE);
ready = true;
ModdedIrisLog.info("Iris engine up for {}: pack={} dim={} seed={} height={}..{} activation={}",
level.dimension().identifier(),
candidate.getData().getDataFolder().getAbsolutePath(),
candidate.getDimension().getLoadKey(),
seed,
candidate.getDimension().getMinHeight(),
candidate.getDimension().getMaxHeight(),
history.activeActivation().activationId());
return candidate;
} finally {
if (!ready) {
close(candidate);
if (history.activeActivation().activationId() != openedActivationId) {
close(candidate);
ModdedGenerationHistoryStorage.ActivePack promoted =
ModdedGenerationHistoryStorage.resolveActive(history);
candidate = buildEngine(level, seed, promoted);
router = GenerationHistoryRuntimeRouter.attach(
candidate,
history,
IrisBoundarySignatureSampler.INSTANCE
);
router.preloadActiveRuntimes();
}
requireReady(candidate, level, ModdedGenerationMode.PERSISTENT_RESTORE);
ready = true;
ModdedIrisLog.info("Iris engine up for {}: pack={} dim={} seed={} height={}..{} activation={}",
level.dimension().identifier(),
candidate.getData().getDataFolder().getAbsolutePath(),
candidate.getDimension().getLoadKey(),
seed,
candidate.getDimension().getMinHeight(),
candidate.getDimension().getMaxHeight(),
history.activeActivation().activationId());
return candidate;
} finally {
if (!ready) {
close(candidate);
}
}
}
}
@@ -22,24 +22,15 @@ import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/**
* Validates biome layer stacks. caveCeilingLayers reuses the height generators built from layers,
* so a biome with more ceiling entries than surface entries has no generator for the extras; the
* engine skips them, and this validator surfaces the mistake to the author at validate time.
*/
final class PackBiomeLayerValidator {
private static final int DEFAULT_LAYER_COUNT = 1;
private PackBiomeLayerValidator() {
}
static List<String> validateCeilingLayerCounts(File biomesFolder) {
static List<String> validateLayers(File biomesFolder) {
List<String> blockingErrors = new ArrayList<>();
if (biomesFolder == null || !biomesFolder.isDirectory()) {
return blockingErrors;
@@ -49,24 +40,13 @@ final class PackBiomeLayerValidator {
biomeFiles.sort(Comparator.comparing(File::getPath));
for (File biomeFile : biomeFiles) {
String biomeKey = PackValidationIo.deriveKey(biomesFolder, biomeFile);
JSONObject biome;
try {
biome = new JSONObject(Files.readString(biomeFile.toPath(), StandardCharsets.UTF_8));
} catch (Throwable e) {
// Invalid JSON is reported by the graph validators; layer counts have nothing to add.
JSONObject biome = PackValidationIo.readJson(biomeFile);
if (biome == null) {
continue;
}
Integer layers = arrayLength(biome, "layers", biomeKey, DEFAULT_LAYER_COUNT, blockingErrors);
Integer ceiling = arrayLength(biome, "caveCeilingLayers", biomeKey, 0, blockingErrors);
if (layers == null || ceiling == null) {
continue;
}
if (ceiling > layers) {
blockingErrors.add("Biome '" + biomeKey + "' declares " + ceiling + " caveCeilingLayers but only "
+ layers + " layers. caveCeilingLayers reuses the layers height generators and must not have more entries.");
}
validateLayerArray(biome, "layers", biomeKey, blockingErrors);
validateLayerArray(biome, "caveCeilingLayers", biomeKey, blockingErrors);
}
return blockingErrors;
}
@@ -126,17 +106,15 @@ final class PackBiomeLayerValidator {
}
}
private static Integer arrayLength(JSONObject biome, String field, String biomeKey, int defaultCount,
private static void validateLayerArray(JSONObject biome, String field, String biomeKey,
List<String> blockingErrors) {
if (!biome.has(field) || biome.isNull(field)) {
return defaultCount;
return;
}
JSONArray array = biome.optJSONArray(field);
if (array == null) {
blockingErrors.add("Biome '" + biomeKey + "' " + field + " must be an array.");
return null;
}
return array.length();
}
}
@@ -150,7 +150,7 @@ public final class PackValidator {
new File(packFolder, "spawners"), new File(packFolder, "entities")));
blockingErrors.addAll(PackSpawnValidator.validateCustomBiomeSpawns(
new File(packFolder, "biomes"), PackSpawnValidator::resolveEntitySpawnCategory));
blockingErrors.addAll(PackBiomeLayerValidator.validateCeilingLayerCounts(new File(packFolder, "biomes")));
blockingErrors.addAll(PackBiomeLayerValidator.validateLayers(new File(packFolder, "biomes")));
blockingErrors.addAll(PackBiomeLayerValidator.validateDecoratorPalettes(
new File(packFolder, "biomes"), new File(packFolder, "snippet/decorator")));
PackStyledRangeDefaultValidator.Validation styledRanges = PackStyledRangeDefaultValidator.validate(packFolder);
@@ -68,11 +68,12 @@ final class EngineHotloader {
engine.lifecycleState = LifecycleState.HOTLOADING;
EngineRuntime previous = engine.runtime;
GenerationRuntime previousGeneration = previous.generation();
IrisComplex nextComplex = null;
RuntimeAssembly assembly = null;
boolean published = false;
try {
engine.sealForTransition("complex hotload", false);
engine.prepareRuntimeHotload();
RuntimeAssembly assembly = new RuntimeAssembly(
assembly = new RuntimeAssembly(
RuntimeAssembly.nextRuntimeId(),
previousGeneration.target(),
previousGeneration.mantleStorageDirectory(),
@@ -82,7 +83,6 @@ final class EngineHotloader {
EngineRuntime next;
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
assembly.complex = assembly.runtimeKernel.createComplex(engine, assembly.transitionPlan);
nextComplex = assembly.complex;
assembly.dimensionStackContext = assembly.runtimeKernel.createDimensionStackContext(engine);
assembly.upperContext = assembly.runtimeKernel.createUpperContext(engine);
BiomeMaxes biomeMaxes = engine.runtimeBuilder.computeBiomeMaxes();
@@ -98,25 +98,30 @@ final class EngineHotloader {
}
Throwable retirementFailure = runCleanup(null, previousGeneration.complex()::close);
if (retirementFailure != null) {
retirementFailure = runCleanup(retirementFailure, nextComplex::close);
nextComplex = null;
engine.lifecycleState = LifecycleState.FAILED;
throw new IllegalStateException("Failed to retire the previous Iris biome complex.", retirementFailure);
}
engine.runtime = next;
published = true;
engine.getGenerationSessions().activateNextSession();
engine.lifecycleState = LifecycleState.RUNNING;
engine.getClosing().set(false);
engine.backgroundTasks.openBackgroundTaskAdmission();
} catch (Throwable e) {
if (nextComplex != null && nextComplex != previousGeneration.complex()) {
Throwable cleanupFailure = runCleanup(null, nextComplex::close);
if (cleanupFailure != null) {
e.addSuppressed(cleanupFailure);
if (published) {
engine.lifecycleState = LifecycleState.FAILED;
engine.getClosing().set(true);
engine.backgroundTasks.closeBackgroundTaskAdmission();
} else {
if (assembly != null && assembly.complex != previousGeneration.complex()) {
Throwable cleanupFailure = engine.shutdownSequence.closeAssembly(assembly, null);
if (cleanupFailure != null) {
e.addSuppressed(cleanupFailure);
}
}
if (engine.lifecycleState != LifecycleState.FAILED) {
engine.runtimeBuilder.restoreRuntimeAfterFailedTransition(previous);
}
}
if (engine.lifecycleState != LifecycleState.FAILED) {
engine.runtimeBuilder.restoreRuntimeAfterFailedTransition(previous);
}
throw new IllegalStateException("Failed to rebuild the Iris biome complex.", e);
}
@@ -175,7 +180,7 @@ final class EngineHotloader {
broadcastStudioHotload(false, "");
} catch (Throwable e) {
if (!published) {
if (replacementData != null) {
if (replacementData != null && !engine.shutdownSequence.retainsData(replacementData)) {
replacementData.unregisterEngine(engine);
Throwable replacementDataFailure = runCleanup(null, replacementData::close);
if (replacementDataFailure != null) {
@@ -209,7 +209,12 @@ final class EngineRuntimeBuilder {
next.generation().mantle(),
null);
if (retirementFailure != null) {
retirementFailure = engine.shutdownSequence.closeRuntime(next, retirementFailure);
Throwable cleanupFailure = engine.shutdownSequence.closeRuntime(
next, previous == null ? null : previous.generation().mantle(), null);
if (cleanupFailure != null) {
engine.shutdownSequence.retainUnpublishedRuntime(next);
retirementFailure = EngineShutdownSequence.appendFailure(retirementFailure, cleanupFailure);
}
engine.lifecycleState = LifecycleState.FAILED;
throw new IllegalStateException("Failed to retire the previous Iris engine runtime.", retirementFailure);
}
@@ -229,6 +234,7 @@ final class EngineRuntimeBuilder {
engine.getClosing().set(true);
engine.backgroundTasks.closeBackgroundTaskAdmission();
engine.lifecycleState = LifecycleState.FAILED;
Throwable cleanupFailure = null;
try {
engine.getGenerationSessions().sealAndAwait(
"failed world manager start",
@@ -236,11 +242,17 @@ final class EngineRuntimeBuilder {
true
);
} catch (Throwable drainFailure) {
e.addSuppressed(drainFailure);
cleanupFailure = drainFailure;
}
engine.shutdownSequence.closeRuntime(next, e);
if (engine.runtime == next) {
engine.runtime = null;
if (cleanupFailure == null) {
cleanupFailure = engine.shutdownSequence.closeRuntime(next, null);
}
if (cleanupFailure == null) {
if (engine.runtime == next) {
engine.runtime = null;
}
} else {
e.addSuppressed(cleanupFailure);
}
throw new IllegalStateException("Failed to start the Iris world manager.", e);
}
@@ -18,6 +18,7 @@
package art.arcane.iris.engine;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.EngineBackgroundTasks.BackgroundTaskDrain;
import art.arcane.iris.engine.EngineRuntimeBuilder.RuntimeAssembly;
import art.arcane.iris.engine.IrisEngine.LifecycleState;
@@ -26,9 +27,19 @@ import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.NativeStructureOwnershipStore;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.history.GenerationAdmission;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* Owns the ordered teardown of a single {@link IrisEngine}, both for a normal close and for a
* construction that failed part way through. Every resource release is gated on the previous one
@@ -38,6 +49,12 @@ final class EngineShutdownSequence {
private static final long CLOSE_RETRY_DRAIN_TIMEOUT_MILLIS = 5000L;
private final IrisEngine engine;
private final List<GenerationAdmission.RuntimeLease> generationAdmissions = new ArrayList<>(1);
private final Set<RuntimeAssembly> incompleteAssemblies = ConcurrentHashMap.newKeySet();
private final Set<EngineRuntime> unpublishedRuntimes = Collections.synchronizedSet(
Collections.newSetFromMap(new IdentityHashMap<>()));
private final Set<EngineTarget> incompleteTargets = Collections.synchronizedSet(
Collections.newSetFromMap(new IdentityHashMap<>()));
private boolean runtimeReleased;
private boolean targetReleased;
private boolean engineDataReleased;
@@ -47,6 +64,10 @@ final class EngineShutdownSequence {
this.engine = engine;
}
void retainGenerationHistory(GenerationHistory history) {
generationAdmissions.add(history.retainRuntime());
}
void close() {
if (!engine.beginShutdown()) {
return;
@@ -150,9 +171,12 @@ final class EngineShutdownSequence {
&& targetReleased
&& engineDataReleased
&& preservationReleased) {
engine.closed = true;
engine.lifecycleState = LifecycleState.CLOSED;
IrisLogging.debug("Engine Fully Shutdown!");
failure = releaseGenerationAdmissions(failure);
if (failure == null) {
engine.closed = true;
engine.lifecycleState = LifecycleState.CLOSED;
IrisLogging.debug("Engine Fully Shutdown!");
}
}
}
if (failure != null) {
@@ -160,6 +184,19 @@ final class EngineShutdownSequence {
}
}
private Throwable releaseGenerationAdmissions(Throwable failure) {
Iterator<GenerationAdmission.RuntimeLease> admissions = generationAdmissions.iterator();
while (admissions.hasNext()) {
GenerationAdmission.RuntimeLease admission = admissions.next();
Throwable admissionFailure = runCleanup(null, admission::close);
failure = appendFailure(failure, admissionFailure);
if (admissionFailure == null) {
admissions.remove();
}
}
return failure;
}
private void reportIncompleteClose(Throwable failure) {
IrisLogging.error("Iris engine shutdown remains incomplete after cleanup failures for " + engine.getWorld().name() + ".");
IrisLogging.reportError(failure);
@@ -172,17 +209,21 @@ final class EngineShutdownSequence {
EngineTickRegistry.unregisterTicking(engine);
engine.lifecycleState = LifecycleState.FAILED;
Throwable cleanupFailure = null;
boolean sessionsDrained = true;
try {
engine.getGenerationSessions().sealAndAwait("failed initialization", 0L, true);
} catch (Throwable e) {
sessionsDrained = false;
cleanupFailure = appendFailure(cleanupFailure, e);
}
engine.backgroundTasks.cancelBackgroundTasks("failed initialization");
BackgroundTaskDrain backgroundDrain = engine.backgroundTasks.drainBackgroundTasks("failed initialization");
cleanupFailure = appendFailure(cleanupFailure, backgroundDrain.failure());
if (!backgroundDrain.allowsResourceRelease()) {
cleanupFailure = appendFailure(cleanupFailure,
new IllegalStateException("Iris background tasks remain active after failed initialization."));
if (!sessionsDrained || !backgroundDrain.allowsResourceRelease()) {
if (!backgroundDrain.allowsResourceRelease()) {
cleanupFailure = appendFailure(cleanupFailure,
new IllegalStateException("Iris background tasks remain active after failed initialization."));
}
if (cleanupFailure != original) {
original.addSuppressed(cleanupFailure);
}
@@ -192,18 +233,22 @@ final class EngineShutdownSequence {
() -> NativeStructureOwnershipStore.close(engine));
cleanupFailure = appendFailure(cleanupFailure, ownershipFailure);
if (ownershipFailure == null) {
cleanupFailure = closeDetachedGenerationRuntimes(cleanupFailure);
cleanupFailure = closeRuntime(engine.runtime, cleanupFailure);
engine.runtime = null;
cleanupFailure = runCleanup(cleanupFailure, engine.publishedTarget::close);
cleanupFailure = runCleanup(cleanupFailure, engine.engineDataStore::releaseEngineData);
engine.closed = true;
cleanupFailure = runCleanup(cleanupFailure, () -> {
PreservationRegistry registry = IrisServices.getOrNull(PreservationRegistry.class);
if (registry != null) {
registry.dereference();
cleanupFailure = releaseRuntime(cleanupFailure);
if (runtimeReleased) {
cleanupFailure = releaseTarget(cleanupFailure);
}
if (targetReleased) {
cleanupFailure = releaseEngineDataForShutdown(cleanupFailure);
}
if (engineDataReleased) {
cleanupFailure = releasePreservation(cleanupFailure);
}
if (runtimeReleased && targetReleased && engineDataReleased && preservationReleased) {
cleanupFailure = releaseGenerationAdmissions(cleanupFailure);
if (generationAdmissions.isEmpty()) {
engine.closed = true;
}
});
}
}
if (cleanupFailure != null && cleanupFailure != original) {
original.addSuppressed(cleanupFailure);
@@ -214,37 +259,55 @@ final class EngineShutdownSequence {
if (assembly == null) {
return failure;
}
failure = runCleanup(failure, () -> {
if (assembly.worldManager != null) {
assembly.worldManager.close();
}
});
failure = runCleanup(failure, () -> {
if (assembly.effects != null) {
assembly.effects.close();
}
});
failure = runCleanup(failure, () -> {
if (assembly.mode != null) {
assembly.mode.close();
}
});
failure = runCleanup(failure, () -> {
if (assembly.complex != null) {
assembly.complex.close();
}
});
failure = runCleanup(failure, () -> {
if (assembly.hash32 != null) {
assembly.hash32.cancel(true);
}
});
if (assembly.mantle != null && assembly.ownsMantle) {
assembly.ownsMantle = false;
failure = runCleanup(failure, assembly.mantle::saveAllNow);
failure = runCleanup(failure, assembly.mantle::close);
incompleteAssemblies.add(assembly);
RuntimeAssembly previous = engine == null ? null : engine.runtimeAssembly.get();
if (engine != null) {
engine.runtimeAssembly.set(assembly);
}
return failure;
Throwable assemblyFailure;
try {
assemblyFailure = closeAssemblyResources(assembly);
} finally {
if (engine != null) {
if (previous == null) {
engine.runtimeAssembly.remove();
} else {
engine.runtimeAssembly.set(previous);
}
}
}
if (assemblyFailure == null) {
incompleteAssemblies.remove(assembly);
} else if (engine != null) {
engine.lifecycleState = LifecycleState.FAILED;
}
return appendFailure(failure, assemblyFailure);
}
void retainUnpublishedRuntime(EngineRuntime runtime) {
unpublishedRuntimes.add(runtime);
}
boolean retainsData(IrisData data) {
if (engine.runtime != null && engine.runtime.generation().data() == data) {
return true;
}
for (RuntimeAssembly assembly : incompleteAssemblies) {
if (assembly.target.getData() == data) {
return true;
}
}
for (EngineRuntime runtime : unpublishedRuntimes.toArray(new EngineRuntime[0])) {
if (runtime.generation().data() == data) {
return true;
}
}
for (EngineTarget target : incompleteTargets.toArray(new EngineTarget[0])) {
if (target.getData() == data) {
return true;
}
}
return false;
}
Throwable closeRuntime(EngineRuntime engineRuntime, Throwable failure) {
@@ -259,8 +322,11 @@ final class EngineShutdownSequence {
if (engineRuntime == null) {
return failure;
}
failure = runCleanup(failure, engineRuntime.worldManager()::close);
failure = runCleanup(failure, engineRuntime.effects()::close);
Throwable serviceFailure = runCleanup(null, engineRuntime.worldManager()::close);
serviceFailure = runCleanup(serviceFailure, engineRuntime.effects()::close);
if (serviceFailure != null) {
return appendFailure(failure, serviceFailure);
}
return closeGenerationRuntime(engineRuntime.generation(), retainedMantle, failure);
}
@@ -276,12 +342,18 @@ final class EngineShutdownSequence {
if (generationRuntime == null) {
return failure;
}
failure = runCleanup(failure, generationRuntime.mode()::close);
failure = runCleanup(failure, generationRuntime.complex()::close);
Throwable modeFailure = runCleanup(null, generationRuntime.mode()::close);
if (modeFailure != null) {
return appendFailure(failure, modeFailure);
}
Throwable complexFailure = runCleanup(null, generationRuntime.complex()::close);
failure = appendFailure(failure, complexFailure);
if (complexFailure != null) {
return failure;
}
failure = runCleanup(failure, () -> generationRuntime.hash32().cancel(true));
if (generationRuntime.mantle() != retainedMantle) {
failure = runCleanup(failure, generationRuntime.mantle()::saveAllNow);
failure = runCleanup(failure, generationRuntime.mantle()::close);
failure = appendFailure(failure, closeMantle(generationRuntime.mantle()));
}
return failure;
}
@@ -290,15 +362,31 @@ final class EngineShutdownSequence {
IrisEngine.GenerationRuntimeBinding binding = new IrisEngine.GenerationRuntimeBinding(
engine,
generationRuntime);
Throwable runtimeFailure;
try (IrisEngine.GenerationRuntimeScope ignored = engine.generationRuntimeScopes.open(binding)) {
failure = closeGenerationRuntime(generationRuntime, failure);
runtimeFailure = closeGenerationRuntime(generationRuntime, null);
} catch (Throwable scopeFailure) {
failure = appendFailure(failure, scopeFailure);
runtimeFailure = scopeFailure;
}
if (runtimeFailure != null) {
return appendFailure(failure, runtimeFailure);
}
return closeDetachedTarget(generationRuntime.target(), failure);
}
Throwable closeDetachedTarget(EngineTarget target, Throwable failure) {
for (RuntimeAssembly assembly : incompleteAssemblies) {
if (assembly.target == target) {
return appendFailure(failure,
new IllegalStateException("Iris runtime assembly still owns the detached target."));
}
}
for (EngineRuntime runtime : unpublishedRuntimes.toArray(new EngineRuntime[0])) {
if (runtime.generation().target() == target) {
return appendFailure(failure,
new IllegalStateException("Iris unpublished runtime still owns the detached target."));
}
}
failure = runCleanup(failure, () -> target.getData().unregisterEngine(engine));
failure = runCleanup(failure, target::close);
failure = runCleanup(failure, target.getData()::close);
@@ -309,6 +397,10 @@ final class EngineShutdownSequence {
if (runtimeReleased) {
return failure;
}
Throwable incompleteFailure = closeIncompleteRuntimes();
if (incompleteFailure != null) {
return appendFailure(failure, incompleteFailure);
}
Throwable detachedFailure = closeDetachedGenerationRuntimes(null);
if (detachedFailure != null) {
return appendFailure(failure, detachedFailure);
@@ -322,6 +414,86 @@ final class EngineShutdownSequence {
return failure;
}
private Throwable closeAssemblyResources(RuntimeAssembly assembly) {
Throwable failure = runCleanup(null, () -> {
if (assembly.worldManager != null) {
assembly.worldManager.close();
}
});
failure = runCleanup(failure, () -> {
if (assembly.effects != null) {
assembly.effects.close();
}
});
if (failure != null) {
return failure;
}
failure = runCleanup(null, () -> {
if (assembly.mode != null) {
assembly.mode.close();
}
});
if (failure != null) {
return failure;
}
failure = runCleanup(null, () -> {
if (assembly.complex != null) {
assembly.complex.close();
}
});
if (failure != null) {
return failure;
}
failure = runCleanup(null, () -> {
if (assembly.hash32 != null) {
assembly.hash32.cancel(true);
}
});
if (assembly.mantle != null && assembly.ownsMantle) {
Throwable mantleFailure = closeMantle(assembly.mantle);
if (mantleFailure == null) {
assembly.ownsMantle = false;
}
failure = appendFailure(failure, mantleFailure);
}
return failure;
}
private Throwable closeIncompleteRuntimes() {
Throwable failure = null;
for (RuntimeAssembly assembly : incompleteAssemblies.toArray(new RuntimeAssembly[0])) {
Throwable assemblyFailure = closeAssembly(assembly, null);
if (assemblyFailure == null && assembly.target != engine.publishedTarget) {
incompleteTargets.add(assembly.target);
}
failure = appendFailure(failure, assemblyFailure);
}
EngineMantle retainedMantle = engine.runtime == null ? null : engine.runtime.generation().mantle();
for (EngineRuntime unpublished : unpublishedRuntimes.toArray(new EngineRuntime[0])) {
Throwable runtimeFailure = closeRuntime(unpublished, retainedMantle, null);
if (runtimeFailure == null) {
unpublishedRuntimes.remove(unpublished);
if (unpublished.generation().target() != engine.publishedTarget) {
incompleteTargets.add(unpublished.generation().target());
}
}
failure = appendFailure(failure, runtimeFailure);
}
for (EngineTarget target : incompleteTargets.toArray(new EngineTarget[0])) {
Throwable targetFailure = closeDetachedTarget(target, null);
if (targetFailure == null) {
incompleteTargets.remove(target);
}
failure = appendFailure(failure, targetFailure);
}
return failure;
}
private static Throwable closeMantle(EngineMantle mantle) {
Throwable failure = runCleanup(null, mantle::saveAllNow);
return failure == null ? runCleanup(null, mantle::close) : failure;
}
private Throwable closeDetachedGenerationRuntimes(Throwable failure) {
GenerationRuntime[] detachedRuntimes = engine.detachedGenerationRuntimes.toArray(new GenerationRuntime[0]);
for (GenerationRuntime detached : detachedRuntimes) {
@@ -37,6 +37,7 @@ import art.arcane.iris.engine.image.IrisImageMapRuntime;
import art.arcane.iris.engine.mantle.components.MantleHydrologyCaveVoxelView;
import art.arcane.iris.engine.object.InferredType;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRaritySelection;
import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.engine.object.IrisGenerator;
@@ -1708,74 +1709,25 @@ public class IrisComplex implements DataProvider {
}
static final class ChildSelectionPlan {
private final IrisBiome[] mappedBiomes;
private final int maxIndex;
private final IrisRaritySelection<IrisBiome> selection;
private ChildSelectionPlan(IrisBiome[] mappedBiomes) {
this.mappedBiomes = mappedBiomes;
this.maxIndex = mappedBiomes.length - 1;
private ChildSelectionPlan(IrisRaritySelection<IrisBiome> selection) {
this.selection = selection;
}
static ChildSelectionPlan create(KList<IrisBiome> options) {
if (options.isEmpty()) {
return new ChildSelectionPlan(new IrisBiome[0]);
}
int maxRarity = 1;
for (IrisBiome biome : options) {
if (biome != null && biome.getRarity() > maxRarity) {
maxRarity = biome.getRarity();
}
}
int rarityMax = maxRarity + 1;
boolean flip = false;
KList<IrisBiome> mapped = new KList<>();
for (IrisBiome biome : options) {
if (biome == null) {
continue;
}
int rarity = Math.max(1, biome.getRarity());
int count = rarityMax - rarity;
for (int index = 0; index < count; index++) {
flip = !flip;
if (flip) {
mapped.add(biome);
} else {
mapped.add(0, biome);
}
}
}
if (mapped.isEmpty()) {
IrisBiome[] fallback = new IrisBiome[]{options.get(0)};
return new ChildSelectionPlan(fallback);
}
IrisBiome[] mappedBiomes = mapped.toArray(new IrisBiome[0]);
return new ChildSelectionPlan(mappedBiomes);
return new ChildSelectionPlan(IrisRaritySelection.create(options));
}
IrisBiome select(CNG childCell, double x, double z) {
if (mappedBiomes.length == 0) {
return null;
long maximumIndex = selection.size() - 1L;
if (maximumIndex <= 0L) {
return selection.get(0L);
}
if (mappedBiomes.length == 1) {
return mappedBiomes[0];
if (maximumIndex <= Integer.MAX_VALUE) {
return selection.get(childCell.fit2D(0, (int) maximumIndex, x, z));
}
int selectedIndex = childCell.fit2D(0, maxIndex, x, z);
if (selectedIndex < 0) {
return mappedBiomes[0];
}
if (selectedIndex > maxIndex) {
return mappedBiomes[maxIndex];
}
return mappedBiomes[selectedIndex];
return selection.select(childCell.noiseFast2D(x, z));
}
}
@@ -1783,9 +1735,9 @@ public class IrisComplex implements DataProvider {
}
public void close() {
resolvedTerrain.clear();
if (hydrologyRuntime != null) {
hydrologyRuntime.close();
}
resolvedTerrain.clear();
}
}
@@ -1010,6 +1010,7 @@ public class IrisEngine implements Engine {
if (generationHistoryRuntimeRouter != null) {
throw new IllegalStateException("Iris engine already has a generation-history runtime router.");
}
shutdownSequence.retainGenerationHistory(required.history());
generationHistoryRuntimeRouter = required;
generationHistoryRoutingRequired = true;
}
@@ -277,26 +277,26 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
}
if (!looperStopped) {
try {
looperStopped = true;
if (looper != null) {
looper.interrupt();
joinQuietly(looper);
awaitLooper(looper);
}
looperStopped = true;
} catch (Throwable e) {
failure = appendCloseFailure(failure, e);
}
}
if (!cleanupServiceStopped) {
try {
cleanupServiceStopped = true;
if (cleanupService != null) {
for (Future<?> future : cleanup.values()) {
future.cancel(false);
}
cleanup.clear();
cleanupService.shutdownNow();
awaitQuietly(cleanupService);
awaitCleanupService(cleanupService);
}
cleanupServiceStopped = true;
} catch (Throwable e) {
failure = appendCloseFailure(failure, e);
}
@@ -306,28 +306,30 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
}
}
private void joinQuietly(Thread thread) {
private void awaitLooper(Thread thread) {
if (thread == Thread.currentThread()) {
return;
throw new IllegalStateException("Cannot finish world manager shutdown from its own loop thread.");
}
try {
thread.join(CLOSE_AWAIT_MS);
if (thread.isAlive()) {
IrisLogging.warn("Thread " + thread.getName() + " did not stop within " + CLOSE_AWAIT_MS + "ms.");
throw new IllegalStateException("Thread " + thread.getName() + " did not stop within " + CLOSE_AWAIT_MS + "ms.");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while waiting for the world manager loop to stop.", e);
}
}
private void awaitQuietly(ScheduledExecutorService service) {
private void awaitCleanupService(ScheduledExecutorService service) {
try {
if (!service.awaitTermination(CLOSE_AWAIT_MS, TimeUnit.MILLISECONDS)) {
IrisLogging.warn("Mantle cleanup executor did not stop within " + CLOSE_AWAIT_MS + "ms.");
throw new IllegalStateException("Mantle cleanup executor did not stop within " + CLOSE_AWAIT_MS + "ms.");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while waiting for mantle cleanup to stop.", e);
}
}
@@ -41,7 +41,6 @@ import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterBiomeInject;
import art.arcane.volmlib.util.matter.slices.BiomeInjectMatter;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import java.util.List;
@@ -49,7 +48,6 @@ import java.util.Objects;
public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
private final RNG rng;
private final ChronoLatch cl = new ChronoLatch(5000);
private final KMap<String, ResolvedBiome> resolvedBiomes = new KMap<>();
private final KMap<String, ResolvedBiome> resolvedPhysicalBiomes = new KMap<>();
@@ -46,11 +46,7 @@ import art.arcane.iris.util.common.data.B;
import lombok.Getter;
public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBlockState> {
private static final PlatformBlockState AIR = B.getState("AIR");
private static final PlatformBlockState BEDROCK = B.getState("BEDROCK");
private static final PlatformBlockState LAVA = B.getState("LAVA");
private static final PlatformBlockState GLASS = B.getState("GLASS");
private static final PlatformBlockState CAVE_AIR = B.getState("CAVE_AIR");
@Getter
private final RNG rng;
@Getter
@@ -83,11 +79,6 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
*/
@BlockCoordinates
public void terrainSliver(int x, int z, int xf, Hunk<PlatformBlockState> h, ChunkContext context) {
terrainSliverOptimized(x, z, xf, h, context);
}
@BlockCoordinates
private void terrainSliverOptimized(int x, int z, int xf, Hunk<PlatformBlockState> h, ChunkContext context) {
int chunkHeight = h.getHeight();
int chunkDepth = h.getDepth();
IrisDimension dimension = getDimension();
@@ -42,9 +42,7 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
return;
}
GeometrySource source = new GeometrySource(blocks, biomes, getEngine().getMinHeight(), context);
if (plan != null && plan.hasTransitionAtChunk(x >> 4, z >> 4)) {
blendGeometry(x, z, blocks, context, source, plan);
}
blendGeometry(x, z, blocks, context, source, plan);
}
public static SavedTerrainChunk capture(int x, int z, Hunk<PlatformBlockState> blocks,
@@ -474,7 +474,7 @@ final class DecoratorCore {
&& (!caveSkipFluid || !B.isFluid(lower) && !B.isFluid(upper));
}
private static int computeStack(IrisDecorator decorator, RNG rng, double realX, double realZ,
static int computeStack(IrisDecorator decorator, RNG rng, double realX, double realZ,
IrisData irisData, int max) {
int stack = decorator.getHeight(rng, realX, realZ, irisData);
if (decorator.isScaleStack()) {
@@ -39,6 +39,10 @@ public class IrisSeaFloorDecorator extends IrisEngineDecorator {
@Override
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
Hunk<PlatformBlockState> data, IrisBiome biome, int height, int max) {
int limit = Math.min(max, Math.min(getEngine().getHeight(), data.getHeight()));
if (height < 0 || height >= limit) {
return;
}
RNG rng = getRNG(realX, realZ);
IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ);
@@ -51,34 +55,25 @@ public class IrisSeaFloorDecorator extends IrisEngineDecorator {
&& !decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ))) {
return;
}
if (height >= 0 && height < getEngine().getHeight()) {
data.set(x, height, z, decorator.getBlockData100(biome, rng, realX, height, realZ, getData()));
PlatformBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData());
if (block != null) {
data.set(x, height, z, block);
}
return;
}
int stack = decorator.getHeight(rng, realX, realZ, getData());
if (decorator.isScaleStack()) {
stack = (int) Math.ceil((double) (max - height) * ((double) stack / 100));
} else {
stack = Math.min(stack, max - height);
}
if (stack == 1) {
data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData()));
return;
}
int engineHeight = getEngine().getHeight();
int stack = Math.min(limit - height,
DecoratorCore.computeStack(decorator, rng, realX, realZ, getData(), max - height));
for (int i = 0; i < stack; i++) {
int h = height + i;
if (h > max || h > engineHeight) {
continue;
}
double threshold = ((double) i) / (stack - 1);
data.set(x, h, z, threshold >= decorator.getTopThreshold()
double threshold = stack == 1 ? 1.0 : ((double) i) / (stack - 1);
PlatformBlockState block = threshold >= decorator.getTopThreshold()
? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData())
: decorator.getBlockData100(biome, rng, realX, h, realZ, getData()));
: decorator.getBlockData100(biome, rng, realX, h, realZ, getData());
if (block == null) {
break;
}
data.set(x, h, z, block);
}
}
}
@@ -39,6 +39,10 @@ public class IrisSeaSurfaceDecorator extends IrisEngineDecorator {
@Override
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
Hunk<PlatformBlockState> data, IrisBiome biome, int height, int max) {
int maxBaseY = Math.min(max, Math.min(getEngine().getHeight(), data.getHeight()) - 1);
if (height < 0 || height >= maxBaseY) {
return;
}
RNG rng = getRNG(realX, realZ);
IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ);
@@ -48,8 +52,7 @@ public class IrisSeaSurfaceDecorator extends IrisEngineDecorator {
if (!decorator.isStacking()) {
int targetY = height + 1;
if (height >= 0 && targetY < getEngine().getHeight()
&& DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) {
if (DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) {
PlatformBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData());
if (block != null) {
data.set(x, targetY, z, block);
@@ -58,33 +61,15 @@ public class IrisSeaSurfaceDecorator extends IrisEngineDecorator {
return;
}
int stack = decorator.getHeight(rng, realX, realZ, getData());
if (decorator.isScaleStack()) {
stack = (int) Math.ceil((double) (max - height) * ((double) stack / 100));
}
if (stack == 1) {
int targetY = height + 1;
if (targetY >= data.getHeight() || !DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) {
return;
}
PlatformBlockState block = decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData());
if (block != null) {
data.set(x, targetY, z, block);
}
return;
}
int engineHeight = getEngine().getHeight();
int stack = Math.min(maxBaseY - height,
DecoratorCore.computeStack(decorator, rng, realX, realZ, getData(), max - height));
for (int i = 0; i < stack; i++) {
int h = height + i;
int targetY = h + 1;
if (h >= max || targetY >= engineHeight
|| !DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) {
if (!DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) {
break;
}
double threshold = ((double) i) / (stack - 1);
double threshold = stack == 1 ? 1.0 : ((double) i) / (stack - 1);
PlatformBlockState block = threshold >= decorator.getTopThreshold()
? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData())
: decorator.getBlockData100(biome, rng, realX, h, realZ, getData());
@@ -42,11 +42,11 @@ public abstract class IrisEngineMode implements EngineMode {
return;
}
closed = true;
dump();
terrainStages.forEach(EngineStage::close);
terrainStages.clear();
transitionStage.close();
closed = true;
}
@Override
@@ -16,7 +16,12 @@ public final class GenerationAdmission {
Path root = Objects.requireNonNull(dimensionRoot, "dimensionRoot")
.toAbsolutePath()
.normalize();
this.state = STATES.computeIfAbsent(root, ignored -> new State());
this.state = STATES.computeIfAbsent(root, State::new);
}
public RuntimeLease retainRuntime() {
state.retainRuntime();
return new RuntimeLease(state);
}
public StageLease enterStage() {
@@ -34,6 +39,23 @@ public final class GenerationAdmission {
return new CutoverLease(state);
}
public static final class RuntimeLease implements AutoCloseable {
private final State state;
private boolean closed;
private RuntimeLease(State state) {
this.state = state;
}
@Override
public synchronized void close() {
if (!closed) {
state.releaseRuntime();
closed = true;
}
}
}
public static final class StageLease implements AutoCloseable {
private final State state;
private final AtomicBoolean closed;
@@ -69,26 +91,71 @@ public final class GenerationAdmission {
}
private static final class State {
private final Path root;
private final ReentrantLock lock;
private final Condition changed;
private int runtimeOwners;
private int waitingStages;
private int activeStages;
private int waitingCutovers;
private boolean cutoverActive;
private boolean generationAdmissionOpened;
private boolean closed;
private State() {
private State(Path root) {
this.root = root;
this.lock = new ReentrantLock(true);
this.changed = lock.newCondition();
}
private void retainRuntime() {
lock.lock();
try {
requireOpen();
runtimeOwners++;
} finally {
lock.unlock();
}
}
private void releaseRuntime() {
lock.lock();
try {
requireOpen();
if (runtimeOwners == 1) {
if (activeStages > 0 || waitingStages > 0 || waitingCutovers > 0 || cutoverActive) {
throw new IllegalStateException("Generation admission still has active stages or cutovers.");
}
closed = true;
STATES.remove(root, this);
}
runtimeOwners--;
} finally {
lock.unlock();
}
}
private void requireOpen() {
if (closed) {
throw new IllegalStateException("Generation admission belongs to a closed runtime.");
}
}
private void enterStage() {
lock.lock();
try {
requireOpen();
generationAdmissionOpened = true;
while (cutoverActive || waitingCutovers > 0) {
changed.awaitUninterruptibly();
waitingStages++;
try {
while (cutoverActive || waitingCutovers > 0) {
changed.awaitUninterruptibly();
requireOpen();
}
activeStages++;
} finally {
waitingStages--;
}
activeStages++;
} finally {
lock.unlock();
}
@@ -112,6 +179,7 @@ public final class GenerationAdmission {
private void beginCutover(boolean startupOnly) {
lock.lock();
try {
requireOpen();
if (startupOnly && generationAdmissionOpened) {
throw new IllegalStateException(
"Generation activation promotion is only allowed before generation admission opens."
@@ -121,6 +189,7 @@ public final class GenerationAdmission {
try {
while (cutoverActive || activeStages > 0) {
changed.awaitUninterruptibly();
requireOpen();
}
cutoverActive = true;
} finally {
@@ -64,6 +64,10 @@ public final class GenerationHistory {
validateReferencedState();
}
public GenerationAdmission.RuntimeLease retainRuntime() {
return admission.retainRuntime();
}
public SavedBiomeStore savedBiomes() {
return savedBiomes;
}
@@ -2646,7 +2646,7 @@ public final class HydrologyPlanner {
potential,
parent,
outletIndex,
routeLengths(grid, parent),
routeLengths(parent, grid.width(), settings.routing()),
List.copyOf(outlets),
false
);
@@ -3222,31 +3222,43 @@ public final class HydrologyPlanner {
return count + (HydrologyHash.unit(stable) < remainder ? 1 : 0);
}
private int[] routeLengths(SampledGrid grid, int[] parent) {
static int[] routeLengths(int[] parent, int width, HydrologyPlannerSettings.Routing routing) {
int[] lengths = new int[parent.length];
int[] pending = new int[parent.length];
double[] distances = new double[parent.length];
Arrays.fill(distances, Double.NaN);
double diagonalDistance = StrictMath.hypot(routing.sampleSpacing(), routing.sampleSpacing());
for (int sourceIndex = 0; sourceIndex < parent.length; sourceIndex++) {
lengths[sourceIndex] = routeLength(sourceIndex, grid, parent);
if (!Double.isNaN(distances[sourceIndex])) {
continue;
}
int current = sourceIndex;
int pendingCount = 0;
while (current >= 0 && Double.isNaN(distances[current])) {
pending[pendingCount++] = current;
distances[current] = -1D;
current = parent[current];
}
if (current >= 0 && distances[current] < 0D) {
throw new IllegalStateException("Hydrology drainage routing contains a cycle.");
}
double distance = current < 0 ? 0D : distances[current];
while (pendingCount > 0) {
int node = pending[--pendingCount];
int next = parent[node];
if (next >= 0) {
boolean cardinal = node / width == next / width || node % width == next % width;
distance += cardinal ? routing.sampleSpacing() : diagonalDistance;
}
distances[node] = distance;
lengths[node] = distance > routing.maximumRouteLength()
? Integer.MAX_VALUE
: (int) StrictMath.ceil(distance);
}
}
return lengths;
}
private int routeLength(int sourceIndex, SampledGrid grid, int[] parent) {
int current = sourceIndex;
int steps = 0;
double length = 0D;
while (parent[current] >= 0) {
int next = parent[current];
GridNode currentNode = grid.node(current);
GridNode nextNode = grid.node(next);
length += StrictMath.hypot(currentNode.x() - nextNode.x(), currentNode.z() - nextNode.z());
if (length > settings.routing().maximumRouteLength() || ++steps > settings.routing().maximumRouteNodes()) {
return Integer.MAX_VALUE;
}
current = next;
}
return (int) StrictMath.ceil(length);
}
private int[] countContributions(List<Integer> sources, RoutingPlan routing) {
int[] counts = new int[routing.parent().length];
for (int source : sources) {
@@ -16,6 +16,7 @@ import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
@@ -131,6 +132,7 @@ public final class HydrologyTileCache implements AutoCloseable {
*/
public HydrologyTile get(HydrologyTileKey key) {
Objects.requireNonNull(key, "key");
requireOpen();
if (prefetchExecutor == null) {
return planInline(key, cacheEpoch.get());
}
@@ -156,7 +158,11 @@ public final class HydrologyTileCache implements AutoCloseable {
}
CacheLoadKey<HydrologyTileKey> loadKey = new CacheLoadKey<>(epoch, key);
PendingLoad<HydrologyTile> owned = new PendingLoad<>(Thread.currentThread(), new CompletableFuture<>());
PendingLoad<HydrologyTile> existing = loading.putIfAbsent(loadKey, owned);
PendingLoad<HydrologyTile> existing;
synchronized (publicationLock) {
requireOpen();
existing = loading.putIfAbsent(loadKey, owned);
}
if (existing != null) {
return awaitLoad(existing);
}
@@ -376,10 +382,45 @@ public final class HydrologyTileCache implements AutoCloseable {
@Override
public void close() {
closed.set(true);
ArrayList<CompletableFuture<?>> active = new ArrayList<>();
ArrayList<CompletableFuture<HydrologyTile>> queued = new ArrayList<>();
synchronized (publicationLock) {
for (PendingLoad<HydrologyTile> load : loading.values()) {
collectActiveLoad(active, load);
}
for (PendingLoad<ChunkColumns> load : composing.values()) {
collectActiveLoad(active, load);
}
closed.set(true);
planning.forEach((key, future) -> {
if (!loading.containsKey(key)) {
queued.add(future);
}
});
}
for (CompletableFuture<HydrologyTile> future : queued) {
future.completeExceptionally(new CancellationException("Hydrology tile cache is closed."));
}
CompletableFuture.allOf(active.toArray(CompletableFuture<?>[]::new))
.handle((ignored, failure) -> null)
.join();
planning.clear();
clear();
}
private static void collectActiveLoad(List<CompletableFuture<?>> active, PendingLoad<?> load) {
if (load.owner() == Thread.currentThread()) {
throw new IllegalStateException("Hydrology tile cache cannot close from an active load.");
}
active.add(load.future());
}
private void requireOpen() {
if (closed.get()) {
throw new CancellationException("Hydrology tile cache is closed.");
}
}
private boolean validSharedTile(HydrologyTile tile, SharedTileKey sharedKey) {
SharedCacheScope scope = sharedKey.scope();
return tile.key().equals(sharedKey.tileKey())
@@ -389,6 +430,7 @@ public final class HydrologyTileCache implements AutoCloseable {
}
private ChunkColumns chunkColumns(int blockX, int blockZ) {
requireOpen();
int chunkX = Math.floorDiv(blockX, CHUNK_SIZE);
int chunkZ = Math.floorDiv(blockZ, CHUNK_SIZE);
long epoch = cacheEpoch.get();
@@ -420,7 +462,11 @@ public final class HydrologyTileCache implements AutoCloseable {
return present;
}
PendingLoad<ChunkColumns> owned = new PendingLoad<>(Thread.currentThread(), new CompletableFuture<>());
PendingLoad<ChunkColumns> existing = composing.putIfAbsent(loadKey, owned);
PendingLoad<ChunkColumns> existing;
synchronized (publicationLock) {
requireOpen();
existing = composing.putIfAbsent(loadKey, owned);
}
if (existing != null) {
return awaitLoad(existing);
}
@@ -652,6 +698,7 @@ public final class HydrologyTileCache implements AutoCloseable {
*/
public List<HydrologyTile> tiles(List<HydrologyTileKey> keys) {
Objects.requireNonNull(keys, "keys");
requireOpen();
HydrologyTile[] loaded = new HydrologyTile[keys.size()];
boolean missing = false;
for (int index = 0; index < loaded.length; index++) {
@@ -724,17 +771,29 @@ public final class HydrologyTileCache implements AutoCloseable {
return CompletableFuture.completedFuture(present);
}
CompletableFuture<HydrologyTile> future = new CompletableFuture<>();
CompletableFuture<HydrologyTile> existing = planning.putIfAbsent(loadKey, future);
CompletableFuture<HydrologyTile> existing;
synchronized (publicationLock) {
if (closed.get()) {
return CompletableFuture.failedFuture(new CancellationException("Hydrology tile cache is closed."));
}
existing = planning.putIfAbsent(loadKey, future);
}
if (existing != null) {
return existing;
}
try {
prefetchExecutor.execute(() -> {
if (future.isDone()) {
planning.remove(loadKey, future);
return;
}
try {
future.complete(planInline(key, loadKey.epoch()));
} catch (Throwable failure) {
future.completeExceptionally(failure);
IrisLogging.reportError(failure);
if (!(failure instanceof CancellationException) || !closed.get()) {
IrisLogging.reportError(failure);
}
} finally {
planning.remove(loadKey, future);
}
@@ -3,46 +3,48 @@ package art.arcane.iris.engine.hydrology;
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
public final class RiverFootprint {
private static final RiverFootprint EMPTY = new RiverFootprint(Map.of());
private final Long2ObjectLinkedOpenHashMap<HydrologyColumnSample> indexed;
private final Map<Long, HydrologyColumnSample> columns;
public RiverFootprint(Map<Long, HydrologyColumnSample> columns) {
if (columns == null) {
throw new IllegalArgumentException("columns must not be null.");
}
TreeMap<Long, HydrologyColumnSample> ordered = new TreeMap<>();
indexed = new Long2ObjectLinkedOpenHashMap<>(columns.size());
for (Map.Entry<Long, HydrologyColumnSample> entry : columns.entrySet()) {
HydrologyColumnSample sample = entry.getValue();
if (sample == null || entry.getKey() != pack(sample.x(), sample.z())) {
throw new IllegalArgumentException("Footprint keys must match their column coordinates.");
}
ordered.put(entry.getKey(), sample);
indexed.put(entry.getKey().longValue(), sample);
}
Long2ObjectLinkedOpenHashMap<HydrologyColumnSample> indexed =
new Long2ObjectLinkedOpenHashMap<>(ordered.size());
for (Map.Entry<Long, HydrologyColumnSample> entry : ordered.entrySet()) {
indexed.put(entry.getKey().longValue(), entry.getValue());
long[] order = indexed.keySet().toLongArray();
Arrays.sort(order);
for (long key : order) {
indexed.getAndMoveToLast(key);
}
columns = Collections.unmodifiableMap(indexed);
this.columns = columns;
this.columns = Collections.unmodifiableMap(indexed);
}
public static RiverFootprint empty() {
return new RiverFootprint(Map.of());
return EMPTY;
}
public Optional<HydrologyColumnSample> sample(int x, int z) {
return Optional.ofNullable(columns.get(pack(x, z)));
return Optional.ofNullable(indexed.get(pack(x, z)));
}
public HydrologyRenderSample renderSample(int x, int z) {
HydrologyColumnSample sample = columns.get(pack(x, z));
HydrologyColumnSample sample = indexed.get(pack(x, z));
return sample == null ? new HydrologyRenderSample(x, z, List.of()) : sample.renderSample();
}
@@ -61,7 +63,7 @@ public final class RiverFootprint {
if (area < columns.size()) {
for (long z = minimumZ; z < maximumZ; z++) {
for (long x = minimumX; x < maximumX; x++) {
HydrologyColumnSample sample = columns.get(pack((int) x, (int) z));
HydrologyColumnSample sample = indexed.get(pack((int) x, (int) z));
if (sample != null) {
selected.add(sample);
}
@@ -9,7 +9,6 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
@@ -1132,8 +1131,8 @@ public final class HydrologyCaveContainmentPlanner {
List<CavePosition> throat
) {
Queue<CavePosition> queue = new ArrayDeque<>();
Set<CavePosition> queued = new HashSet<>();
Set<CavePosition> resolved = new LinkedHashSet<>();
Set<CavePosition> queued = new LinkedHashSet<>();
int visitedCount = 0;
queue.add(source.target());
queued.add(source.target());
@@ -1148,10 +1147,7 @@ public final class HydrologyCaveContainmentPlanner {
if (positionRejection != HydrologyCaveRejection.NONE) {
return ComponentResult.rejected(positionRejection);
}
if (!resolved.add(position)) {
continue;
}
if (resolved.size() > settings.maxFloodVolume()) {
if (++visitedCount > settings.maxFloodVolume()) {
return ComponentResult.rejected(HydrologyCaveRejection.VOLUME_LIMIT);
}
@@ -1171,7 +1167,7 @@ public final class HydrologyCaveContainmentPlanner {
}
}
return ComponentResult.accepted(resolved);
return ComponentResult.accepted(queued);
}
private HydrologyCaveRejection addThroatContacts(
@@ -58,6 +58,8 @@ import art.arcane.iris.util.common.data.DataProvider;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.volmlib.util.math.RNG;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.ArrayList;
import java.util.Collections;
@@ -70,7 +72,6 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.nio.file.Path;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.IntConsumer;
import java.util.function.Predicate;
@@ -96,7 +97,9 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
private final Set<String> profileKeys;
private final String defaultProfileKey;
private final Object terrainSampleLock;
private final Set<HydrologyTileKey> unplannedQueries = ConcurrentHashMap.newKeySet();
private final Cache<HydrologyTileKey, Boolean> unplannedQueries = Caffeine.newBuilder()
.maximumSize(MAXIMUM_CACHE_TILES)
.build();
private final LinkedHashMap<Long, HydrologyTerrainSample> terrainSamples;
public IrisHydrologyRuntime(IrisHydrologyRuntimeContext context) {
@@ -156,7 +159,7 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
if (!planned) {
int tileSize = settings.routing().tileSize();
HydrologyTileKey key = new HydrologyTileKey(Math.floorDiv(blockX, tileSize), Math.floorDiv(blockZ, tileSize));
if (unplannedQueries.add(key)) {
if (unplannedQueries.asMap().putIfAbsent(key, Boolean.TRUE) == null) {
IrisLogging.debug("Hydrology tile %d,%d queried before it was planned; natural terrain answers until the plan lands",
key.tileX(), key.tileZ());
}
@@ -479,6 +482,7 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
@Override
public void close() {
cache.close();
unplannedQueries.invalidateAll();
routingTerrainSampler.close();
synchronized (terrainSampleLock) {
terrainSamples.clear();
@@ -91,11 +91,12 @@ public final class SurfaceFootprintCompiler {
&& column.x() == centerline.x()[0] && column.z() == centerline.z()[0];
int y = column.role() == SurfaceRole.CHANNEL ? column.headY() : column.height();
HydrologyFeatureRef feature = features.feature(segment, column.role(), source, column.x(), y, column.z(), flowX, flowZ);
String biomeOverride = pool ? poolBiome == null ? column.terrain().parentBiomeKey() : poolBiome : null;
columns.add(new SurfaceLayerColumn(
column.x(),
column.z(),
column.terrain(),
pool ? poolLayer(feature, column, course.profileKey(), poolBiome) : layer(feature, column, course.profileKey()),
layer(feature, column, course.profileKey(), biomeOverride),
column.role(),
column.apron()
));
@@ -103,40 +104,6 @@ public final class SurfaceFootprintCompiler {
return new SurfaceFootprint(columns, field.uncontainedWetCells());
}
/** A pool's bowl and rim carry the pool biome when one is configured, otherwise the surrounding biome. */
private static HydrologyColumnLayer poolLayer(
HydrologyFeatureRef feature,
SurfaceColumn column,
String profileKey,
String poolBiome
) {
HydrologyTerrainSample terrain = column.terrain();
String biome = poolBiome == null ? terrain.parentBiomeKey() : poolBiome;
boolean channel = column.role() == SurfaceRole.CHANNEL;
boolean shore = column.role() == SurfaceRole.SHORE;
return new HydrologyColumnLayer(
feature,
column.height(),
column.headY(),
column.headY(),
channel,
shore,
!channel,
channel,
false,
false,
true,
channel,
false,
profileKey,
biome,
biome,
biome,
biome,
terrain.floodedCaveBiomeKey()
);
}
private ChannelProfile poolProfile(HydraulicSegment segment, SurfaceCenterline centerline) {
int count = centerline.size();
double[] halfWidth = new double[count];
@@ -160,9 +127,15 @@ public final class SurfaceFootprintCompiler {
return null;
}
private static HydrologyColumnLayer layer(HydrologyFeatureRef feature, SurfaceColumn column, String profileKey) {
/** A pool's bowl and rim carry the pool biome when one is configured, otherwise the surrounding biome. */
private static HydrologyColumnLayer layer(
HydrologyFeatureRef feature,
SurfaceColumn column,
String profileKey,
String biomeOverride
) {
HydrologyTerrainSample terrain = column.terrain();
if (column.apron()) {
if (column.apron() && biomeOverride == null) {
return new HydrologyColumnLayer(
feature,
column.headY(),
@@ -194,10 +167,10 @@ public final class SurfaceFootprintCompiler {
channel,
false,
profileKey,
terrain.surfaceBiomeKey(),
terrain.mouthBiomeKey(),
terrain.shoreBiomeKey(),
terrain.bankBiomeKey(),
biomeOverride == null ? terrain.surfaceBiomeKey() : biomeOverride,
biomeOverride == null ? terrain.mouthBiomeKey() : biomeOverride,
biomeOverride == null ? terrain.shoreBiomeKey() : biomeOverride,
biomeOverride == null ? terrain.bankBiomeKey() : biomeOverride,
terrain.floodedCaveBiomeKey()
);
}
@@ -22,12 +22,13 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public interface MatterGenerator {
MultiBurst DISPATCHER = MultiBurst.burst;
ConcurrentHashMap<MatterTaskKey, CompletableFuture<Void>> IN_FLIGHT_COMPONENTS = new ConcurrentHashMap<>();
ConcurrentHashMap<MatterTaskKey, MatterComponentTask> IN_FLIGHT_COMPONENTS = new ConcurrentHashMap<>();
long COMPONENT_TASK_POLL_MS = 1000L;
long COMPONENT_TASK_TIMEOUT_MS = Long.getLong("iris.mantle.componentTimeout", 120000L);
@@ -368,38 +369,37 @@ public interface MatterGenerator {
) {
MantleFlag flag = component.getFlag();
MatterTaskKey key = new MatterTaskKey(getMantle(), chunkX, chunkZ, flag);
MatterComponentTask task = new MatterComponentTask(key);
if (chunk.isFlagged(flag)) {
return new MatterComponentTask(key, CompletableFuture.completedFuture(null), null);
task.finish(null);
return task;
}
CompletableFuture<Void> future = new CompletableFuture<>();
CompletableFuture<Void> existing = IN_FLIGHT_COMPONENTS.putIfAbsent(key, future);
MatterComponentTask existing = IN_FLIGHT_COMPONENTS.putIfAbsent(key, task);
if (existing != null) {
return new MatterComponentTask(key, existing, null);
return existing;
}
try {
if (DISPATCHER.ownsCurrentThread()) {
// A pool thread runs its claim inline: a task queued behind pool threads that block
// on unmanaged waits (structure builds, cache locks) could starve the whole pool.
completeComponentTask(future, key, chunk, component, writer, chunkX, chunkZ, context, IrisContext.get());
return new MatterComponentTask(key, future, null);
completeComponentTask(task, chunk, component, writer, chunkX, chunkZ, context, IrisContext.get());
return task;
}
IrisContext callerContext = IrisContext.get();
Future<?> submission = DISPATCHER.submit(() -> completeComponentTask(
future,
key,
task.setSubmission(DISPATCHER.submit(() -> completeComponentTask(
task,
chunk,
component,
writer,
chunkX,
chunkZ,
context,
callerContext));
return new MatterComponentTask(key, future, submission);
callerContext)));
return task;
} catch (Throwable throwable) {
IN_FLIGHT_COMPONENTS.remove(key, future);
future.completeExceptionally(throwable);
task.cancel(throwable);
throw throwable;
}
}
@@ -449,7 +449,7 @@ public interface MatterGenerator {
}
private static void awaitComponentTask(MatterComponentTask task) {
CompletableFuture<Void> future = task.future();
CompletableFuture<Void> future = task.future;
if (future.isDone() && !future.isCompletedExceptionally()) {
return;
}
@@ -459,11 +459,17 @@ public interface MatterGenerator {
try {
ForkJoinPool.managedBlock(new ComponentTaskBlocker(task));
} catch (InterruptedException interruption) {
Thread.currentThread().interrupt();
try {
task.await(COMPONENT_TASK_TIMEOUT_MS);
} finally {
Thread.currentThread().interrupt();
}
} catch (RejectedExecutionException exhaustedPool) {
task.await(COMPONENT_TASK_TIMEOUT_MS);
}
return;
}
awaitComponentTaskBlocking(task);
task.await(COMPONENT_TASK_TIMEOUT_MS);
}
/**
@@ -478,66 +484,22 @@ public interface MatterGenerator {
@Override
public boolean block() {
awaitComponentTaskBlocking(task);
task.await(COMPONENT_TASK_TIMEOUT_MS);
return true;
}
@Override
public boolean isReleasable() {
return task.future().isDone();
}
}
private static void awaitComponentTaskBlocking(MatterComponentTask task) {
CompletableFuture<Void> future = task.future();
long start = System.currentTimeMillis();
boolean interrupted = false;
try {
while (true) {
try {
future.get(COMPONENT_TASK_POLL_MS, TimeUnit.MILLISECONDS);
return;
} catch (InterruptedException interruption) {
// The writer cannot close while this task still writes through it, so the wait is
// uninterruptible and the flag is restored on the way out.
interrupted = true;
} catch (TimeoutException timeout) {
Future<?> submission = task.submission();
boolean dropped = submission != null && submission.isDone();
if (future.isDone()) {
continue;
}
long waited = System.currentTimeMillis() - start;
if (!dropped && waited < COMPONENT_TASK_TIMEOUT_MS) {
continue;
}
// The task can no longer complete (the dispatcher dropped it) or is wedged. Drop
// the dedup entry so later generations do not inherit a future that never
// completes, and release anything else already waiting on it.
IllegalStateException failure = new IllegalStateException("Mantle component " + task.key()
+ (dropped ? " was dropped by the dispatcher" : " did not complete in " + waited + "ms"));
IN_FLIGHT_COMPONENTS.remove(task.key(), future);
future.completeExceptionally(failure);
IrisLogging.error(failure.getMessage());
throw new CompletionException(failure);
} catch (ExecutionException executionFailure) {
Throwable cause = executionFailure.getCause();
throw new CompletionException(cause != null ? cause : executionFailure);
}
}
} finally {
if (interrupted) {
Thread.currentThread().interrupt();
if (!task.future.isDone()) {
return false;
}
task.future.join();
return true;
}
}
private void completeComponentTask(
CompletableFuture<Void> future,
MatterTaskKey key,
MatterComponentTask task,
MantleChunk<Matter> chunk,
MantleComponent component,
MantleWriter writer,
@@ -546,14 +508,17 @@ public interface MatterGenerator {
ChunkContext context,
IrisContext callerContext
) {
if (!task.start()) {
return;
}
Throwable failure = null;
try {
runComponentWithContext(chunk, component, writer, chunkX, chunkZ, context, callerContext);
future.complete(null);
} catch (Throwable throwable) {
future.completeExceptionally(throwable);
failure = throwable;
throw throwable;
} finally {
IN_FLIGHT_COMPONENTS.remove(key, future);
task.finish(failure);
}
}
@@ -593,11 +558,98 @@ public interface MatterGenerator {
));
}
/**
* A launched component task. {@code submission} is the dispatcher handle when this generation
* owns the task, and null when the future belongs to another generation or already completed.
*/
record MatterComponentTask(MatterTaskKey key, CompletableFuture<Void> future, Future<?> submission) {
final class MatterComponentTask {
private final MatterTaskKey key;
private final CompletableFuture<Void> future = new CompletableFuture<>();
private volatile Future<?> submission;
private volatile Throwable failure;
private boolean started;
MatterComponentTask(MatterTaskKey key) {
this.key = key;
}
synchronized boolean start() {
if (started || future.isDone()) {
return false;
}
started = true;
return true;
}
synchronized void setSubmission(Future<?> submission) {
this.submission = submission;
if (failure != null) {
submission.cancel(true);
}
}
synchronized boolean cancel(Throwable cause) {
if (future.isDone() || failure != null) {
return false;
}
failure = cause;
if (!started) {
finish(null);
}
if (submission != null) {
submission.cancel(true);
}
return true;
}
synchronized void finish(Throwable cause) {
if (failure == null) {
failure = cause;
} else if (cause != null && cause != failure) {
failure.addSuppressed(cause);
}
IN_FLIGHT_COMPONENTS.remove(key, this);
if (failure == null) {
future.complete(null);
} else {
future.completeExceptionally(failure);
}
}
void await(long timeoutMillis) {
long start = System.nanoTime();
boolean interrupted = false;
try {
while (true) {
try {
future.get(COMPONENT_TASK_POLL_MS, TimeUnit.MILLISECONDS);
return;
} catch (InterruptedException interruption) {
// The writer cannot close while this task still writes through it, so the wait is
// uninterruptible and the flag is restored on the way out.
interrupted = true;
} catch (TimeoutException timeout) {
if (future.isDone() || failure != null) {
continue;
}
Future<?> submitted = submission;
boolean dropped = submitted != null && submitted.isDone();
long waited = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
if (!dropped && waited < timeoutMillis) {
continue;
}
IllegalStateException timeoutFailure = new IllegalStateException("Mantle component " + key
+ (dropped ? " was dropped by the dispatcher" : " did not complete in " + waited + "ms"));
if (cancel(timeoutFailure)) {
IrisLogging.error(timeoutFailure.getMessage());
}
} catch (ExecutionException executionFailure) {
Throwable cause = executionFailure.getCause();
throw new CompletionException(cause != null ? cause : executionFailure);
}
}
} finally {
if (interrupted) {
Thread.currentThread().interrupt();
}
}
}
}
record MatterPassPlan(MantlePass pass, int passChunkRadius, int downstreamBlockRadius) {
@@ -22,6 +22,8 @@ import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.matter.MatterSlice;
import java.util.Arrays;
final class CaveCarveScratch {
final int[] columnMaxY = new int[256];
final int[] fluidMaxY = new int[256];
@@ -70,4 +72,9 @@ final class CaveCarveScratch {
int adaptiveGeometryStep = -1;
int adaptiveGeometryAxisCells = -1;
boolean fullWeightsInitialized;
void releaseSections() {
Arrays.fill(sectionMatter, null);
Arrays.fill(sectionSlices, null);
}
}
@@ -436,6 +436,7 @@ public class IrisCaveCarver3D {
return carved;
} finally {
scratchCache.get().releaseSections();
carving.set(nested);
if (nested) {
scratchCache.set(previousScratch);
@@ -2695,10 +2696,6 @@ public class IrisCaveCarver3D {
return weight;
}
private double signed(double value) {
return (value * 2D) - 1D;
}
private double applySurfaceCeilingFade(
CaveCarveScratch scratch,
double threshold,
@@ -19,6 +19,7 @@
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.UpperDimensionContext;
import art.arcane.iris.engine.mantle.MatterGenerationPhase;
import art.arcane.iris.engine.mantle.ComponentFlag;
@@ -36,7 +37,6 @@ import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.iris.util.project.stream.utility.ChunkFillableDoubleStream2D;
import art.arcane.iris.util.simd.SimdKernels;
import art.arcane.iris.util.simd.SimdSupport;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
@@ -58,6 +58,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
private static final double MIN_WEIGHT = 0.08D;
private static final double THRESHOLD_PENALTY = 0.24D;
private static final int MAX_BLENDED_PROFILE_PASSES = 2;
private static final int MAX_POOLED_WEIGHT_BUFFERS = 64;
private static final int KERNEL_WIDTH = (BLEND_RADIUS * 2) + 1;
private static final int KERNEL_SIZE = KERNEL_WIDTH * KERNEL_WIDTH;
private static final int[] KERNEL_DX = new int[KERNEL_SIZE];
@@ -107,8 +108,25 @@ public class MantleCarvingComponent extends IrisMantleComponent {
if (!complex.allowsMantleChunkWrite(x, z)) {
return;
}
BlendScratch previous = BLEND_SCRATCH.get();
BlendScratch scratch = previous.inUse ? new BlendScratch() : previous;
if (scratch != previous) {
BLEND_SCRATCH.set(scratch);
}
scratch.inUse = true;
try {
generateLayer(writer, x, z, context, scratch);
} finally {
scratch.release();
if (scratch != previous) {
BLEND_SCRATCH.set(previous);
}
}
}
private void generateLayer(MantleWriter writer, int x, int z, ChunkContext context, BlendScratch blendScratch) {
IrisComplex complex = context.getComplex();
IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State();
BlendScratch blendScratch = BLEND_SCRATCH.get();
int[] chunkSurfaceHeights = prepareChunkSurfaceHeights(x, z, context, blendScratch.chunkSurfaceHeights);
PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start();
List<WeightedProfile> weightedProfiles = resolveWeightedProfiles(x, z, complex, resolverState);
@@ -217,12 +235,12 @@ public class MantleCarvingComponent extends IrisMantleComponent {
BlendScratch blendScratch = BLEND_SCRATCH.get();
IrisCaveProfile[] profileField = blendScratch.profileField;
Map<IrisCaveProfile, double[]> columnProfileWeights = blendScratch.columnProfileWeights;
IdentityHashMap<IrisCaveProfile, Boolean> activeProfiles = blendScratch.activeProfiles;
List<IrisCaveProfile> profileOrder = blendScratch.profileOrder;
IrisCaveProfile[] kernelProfiles = blendScratch.kernelProfiles;
double[] kernelProfileWeights = blendScratch.kernelProfileWeights;
activeProfiles.clear();
columnProfileWeights.clear();
profileOrder.clear();
blendScratch.nextWeightBuffer = 0;
fillProfileField(profileField, chunkX, chunkZ, complex, resolverState, blendScratch);
for (int localX = 0; localX < CHUNK_SIZE; localX++) {
@@ -270,12 +288,8 @@ public class MantleCarvingComponent extends IrisMantleComponent {
double[] weights = columnProfileWeights.get(profile);
if (weights == null) {
weights = new double[CHUNK_AREA];
weights = blendScratch.acquireWeights();
columnProfileWeights.put(profile, weights);
} else if (!activeProfiles.containsKey(profile)) {
Arrays.fill(weights, 0D);
}
if (activeProfiles.put(profile, Boolean.TRUE) == null) {
profileOrder.add(profile);
}
weights[columnIndex] = columnWeight;
@@ -316,28 +330,27 @@ public class MantleCarvingComponent extends IrisMantleComponent {
return weightedProfiles;
}
Map<IrisDimensionCarvingEntry, IrisDimensionCarvingEntry[]> dimensionColumnPlans = blendScratch.dimensionColumnPlans;
dimensionColumnPlans.clear();
Engine engine = getEngineMantle().getEngine();
int baseX = PowerOfTwoCoordinates.chunkToBlock(chunkX);
int baseZ = PowerOfTwoCoordinates.chunkToBlock(chunkZ);
for (IrisDimensionCarvingEntry entry : entries) {
if (entry == null || !entry.isEnabled()) {
continue;
}
IrisBiome rootBiome = IrisDimensionCarvingResolver.resolveEntryBiome(getEngineMantle().getEngine(), entry, resolverState);
IrisBiome rootBiome = IrisDimensionCarvingResolver.resolveEntryBiome(engine, entry, resolverState);
if (rootBiome == null) {
continue;
}
IrisDimensionCarvingEntry[] columnPlan = dimensionColumnPlans.computeIfAbsent(entry, key -> new IrisDimensionCarvingEntry[CHUNK_AREA]);
buildDimensionColumnPlan(columnPlan, chunkX, chunkZ, entry, resolverState);
Map<IrisCaveProfile, double[]> rootProfileColumnWeights = new IdentityHashMap<>();
List<IrisCaveProfile> rootProfileOrder = new ArrayList<>();
IrisRange worldYRange = entry.getWorldYRange();
for (int columnIndex = 0; columnIndex < CHUNK_AREA; columnIndex++) {
IrisDimensionCarvingEntry resolvedEntry = columnPlan[columnIndex];
IrisBiome resolvedBiome = IrisDimensionCarvingResolver.resolveEntryBiome(getEngineMantle().getEngine(), resolvedEntry, resolverState);
IrisDimensionCarvingEntry resolvedEntry = IrisDimensionCarvingResolver.resolveFromRoot(
engine, entry, baseX + (columnIndex >> 4), baseZ + (columnIndex & 15), resolverState);
IrisBiome resolvedBiome = IrisDimensionCarvingResolver.resolveEntryBiome(engine, resolvedEntry, resolverState);
if (resolvedBiome == null) {
continue;
}
@@ -349,7 +362,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
double[] columnWeights = rootProfileColumnWeights.get(profile);
if (columnWeights == null) {
columnWeights = new double[CHUNK_AREA];
columnWeights = blendScratch.acquireWeights();
rootProfileColumnWeights.put(profile, columnWeights);
rootProfileOrder.add(profile);
}
@@ -364,19 +377,6 @@ public class MantleCarvingComponent extends IrisMantleComponent {
return weightedProfiles;
}
private void buildDimensionColumnPlan(IrisDimensionCarvingEntry[] columnPlan, int chunkX, int chunkZ, IrisDimensionCarvingEntry entry, IrisDimensionCarvingResolver.State resolverState) {
int baseX = PowerOfTwoCoordinates.chunkToBlock(chunkX);
int baseZ = PowerOfTwoCoordinates.chunkToBlock(chunkZ);
for (int localX = 0; localX < CHUNK_SIZE; localX++) {
int worldX = baseX + localX;
for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) {
int worldZ = baseZ + localZ;
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
columnPlan[columnIndex] = IrisDimensionCarvingResolver.resolveFromRoot(getEngineMantle().getEngine(), entry, worldX, worldZ, resolverState);
}
}
}
private void fillProfileField(IrisCaveProfile[] profileField, int chunkX, int chunkZ, IrisComplex complex, IrisDimensionCarvingResolver.State resolverState, BlendScratch blendScratch) {
int startX = PowerOfTwoCoordinates.chunkToBlock(chunkX) - BLEND_RADIUS;
int startZ = PowerOfTwoCoordinates.chunkToBlock(chunkZ) - BLEND_RADIUS;
@@ -698,8 +698,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
private final IrisCaveProfile[] kernelProfiles = new IrisCaveProfile[KERNEL_SIZE];
private final double[] kernelProfileWeights = new double[KERNEL_SIZE];
private final IdentityHashMap<IrisCaveProfile, double[]> columnProfileWeights = new IdentityHashMap<>();
private final IdentityHashMap<IrisDimensionCarvingEntry, IrisDimensionCarvingEntry[]> dimensionColumnPlans = new IdentityHashMap<>();
private final IdentityHashMap<IrisCaveProfile, Boolean> activeProfiles = new IdentityHashMap<>();
private final List<double[]> weightBuffers = new ArrayList<>();
private final List<IrisCaveProfile> profileOrder = new ArrayList<>();
private final double[] fieldSurfaceHeights = new double[FIELD_SIZE * FIELD_SIZE];
private final double[] fieldFluidHeights = new double[FIELD_SIZE * FIELD_SIZE];
@@ -710,5 +709,33 @@ public class MantleCarvingComponent extends IrisMantleComponent {
private final IrisBiome[] fieldCaveBiomes = new IrisBiome[FIELD_SIZE * FIELD_SIZE];
private final int[] chunkSurfaceHeights = new int[CHUNK_AREA];
private final double[] chunkSurfaceHeightSamples = new double[CHUNK_AREA];
private int nextWeightBuffer;
private boolean inUse;
private double[] acquireWeights() {
if (nextWeightBuffer < weightBuffers.size()) {
double[] weights = weightBuffers.get(nextWeightBuffer++);
Arrays.fill(weights, 0D);
return weights;
}
double[] weights = new double[CHUNK_AREA];
if (weightBuffers.size() < MAX_POOLED_WEIGHT_BUFFERS) {
weightBuffers.add(weights);
nextWeightBuffer++;
}
return weights;
}
private void release() {
Arrays.fill(profileField, null);
Arrays.fill(kernelProfiles, null);
Arrays.fill(fieldRegions, null);
Arrays.fill(fieldSurfaceBiomes, null);
Arrays.fill(fieldCaveBiomes, null);
columnProfileWeights.clear();
profileOrder.clear();
nextWeightBuffer = 0;
inUse = false;
}
}
}
@@ -40,11 +40,19 @@ import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.iris.util.common.parallel.BurstExecutor;
import art.arcane.iris.util.common.math.IrisBlockVector;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.common.data.VectorMap;
import java.util.ArrayList;
import java.util.List;
public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockState> {
private static final int CLUMPS_PER_BATCH = 8;
private static final int PREPARATION_BATCH_COUNT = 4;
private final RNG rng;
public IrisDepositModifier(Engine engine) {
@@ -62,29 +70,36 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
public void generateDeposits(Hunk<PlatformBlockState> terrain, int x, int z, boolean multicore, ChunkContext context) {
IrisRegion region = context.getRegion().get(7, 7);
IrisBiome biome = context.getBiome().get(7, 7);
BurstExecutor burst = burst().burst(multicore);
List<IrisDepositGenerator> generators = new ArrayList<>(
getDimension().getDeposits().size() + region.getDeposits().size() + biome.getDeposits().size());
generators.addAll(getDimension().getDeposits());
generators.addAll(region.getDeposits());
generators.addAll(biome.getDeposits());
if (generators.isEmpty()) {
return;
}
BurstExecutor burst = burst().burst(multicore);
long seed = x * 341873128712L + z * 132897987541L;
long mask = 0;
PreparedDeposit[] prepared = new PreparedDeposit[PREPARATION_BATCH_COUNT];
int pending = 0;
MantleChunk chunk = getEngine().getMantle().getMantle().getChunk(x, z).use();
try {
for (IrisDepositGenerator k : getDimension().getDeposits()) {
long finalSeed = seed * ++mask;
burst.queue(scopedDepositTask(
() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context), context));
}
for (IrisDepositGenerator k : region.getDeposits()) {
long finalSeed = seed * ++mask;
burst.queue(scopedDepositTask(
() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context), context));
}
for (IrisDepositGenerator k : biome.getDeposits()) {
long finalSeed = seed * ++mask;
burst.queue(scopedDepositTask(
() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context), context));
for (int i = 0; i < generators.size(); i++) {
DepositPlan plan = plan(generators.get(i), rng.nextParallelRNG(seed * (i + 1L)));
for (int first = 0; first < plan.attempts(); first += CLUMPS_PER_BATCH) {
int firstAttempt = first;
int limit = Math.min(plan.attempts(), first + CLUMPS_PER_BATCH);
int index = pending++;
burst.queue(scopedDepositTask(
() -> prepared[index] = prepare(plan, firstAttempt, limit, x, z, null, context), context));
if (pending == prepared.length) {
completeBatches(burst, prepared, chunk, terrain, x, z, context);
pending = 0;
}
}
}
completeBatches(burst, prepared, chunk, terrain, x, z, context);
} finally {
// complete() must run before release() even when queueing throws — already
// submitted burst tasks must never write into a released chunk.
@@ -96,6 +111,20 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
}
}
private void completeBatches(BurstExecutor burst, PreparedDeposit[] prepared, MantleChunk chunk,
Hunk<PlatformBlockState> terrain, int x, int z, ChunkContext context) {
burst.complete();
try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), context.getGenerationSessionId(), context)) {
for (int i = 0; i < prepared.length; i++) {
PreparedDeposit deposit = prepared[i];
prepared[i] = null;
if (deposit != null) {
place(deposit, chunk, terrain, x, z, null, context);
}
}
}
}
private Runnable scopedDepositTask(Runnable task, ChunkContext context) {
IrisEngine generationEngine = getEngine() instanceof IrisEngine irisEngine
&& irisEngine.hasGenerationRuntimeScope() ? irisEngine : null;
@@ -115,17 +144,35 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
}
public void generate(IrisDepositGenerator k, MantleChunk chunk, Hunk<PlatformBlockState> data, RNG rng, int cx, int cz, boolean safe, HeightMap he, ChunkContext context) {
IrisDimensionCarvingResolver.State carvingState = new IrisDimensionCarvingResolver.State();
if (k.getSpawnChance() < rng.d())
return;
DepositPlan plan = plan(k, rng);
for (int first = 0; first < plan.attempts(); first += CLUMPS_PER_BATCH) {
int limit = Math.min(plan.attempts(), first + CLUMPS_PER_BATCH);
place(prepare(plan, first, limit, cx, cz, he, context), chunk, data, cx, cz, he, context);
}
}
boolean oreDeposit = k.isOre(getData());
private DepositPlan plan(IrisDepositGenerator generator, RNG rng) {
if (generator.getSpawnChance() < rng.d()) {
return new DepositPlan(generator, rng.getSeed(), false, 0);
}
boolean ore = generator.isOre(getData());
int attempts = rng.i(generator.getMinPerChunk(), generator.getMaxPerChunk() + 1);
return new DepositPlan(generator, rng.getSeed(), ore, attempts);
}
for (int l = 0; l < rng.i(k.getMinPerChunk(), k.getMaxPerChunk() + 1); l++) {
if (k.getPerClumpSpawnChance() < rng.d())
private PreparedDeposit prepare(DepositPlan plan, int first, int limit, int cx, int cz, HeightMap he, ChunkContext context) {
IrisDepositGenerator k = plan.generator();
boolean oreDeposit = plan.ore();
boolean needsCaveBiome = oreDeposit || k.usesCaveBiomeFilter();
IrisDimensionCarvingResolver.State carvingState = needsCaveBiome ? new IrisDimensionCarvingResolver.State() : null;
List<PreparedClump> clumps = new ArrayList<>(limit - first);
for (int l = first; l < limit; l++) {
RNG clumpRng = new RNG(clumpSeed(plan.seed(), l));
if (k.getPerClumpSpawnChance() < clumpRng.d()) {
continue;
}
IrisObject clump = k.getClump(getEngine(), rng, getData());
IrisObject clump = k.getClump(getEngine(), clumpRng, getData());
int dim = clump.getW();
int min = dim / 2;
@@ -136,8 +183,8 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
max = 9;
}
int x = rng.i(min, max + 1);
int z = rng.i(min, max + 1);
int x = clumpRng.i(min, max + 1);
int z = clumpRng.i(min, max + 1);
int terrainSurface = getDepositTerrainSurface(cx, cz, x, z, he, context);
int height = k.getPlacementScope() == IrisDepositPlacementScope.TERRAIN
? depositSurfaceLimit(terrainSurface, k.getSurfaceClearance())
@@ -148,19 +195,20 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
}
int y = sampleHeight(
k.getHeightDistribution(), rng, k.getMinHeight(), k.getMaxHeight(),
k.getHeightDistribution(), clumpRng, k.getMinHeight(), k.getMaxHeight(),
Math.min(height, getEngine().getHeight() - 1));
if (y == Integer.MIN_VALUE) {
continue;
}
boolean clippedHeight = k.getHeightDistribution() == IrisDepositHeightDistribution.CLIPPED_UNIFORM;
if (clippedHeight && y > height - 2)
if (clippedHeight && y > height - 2) {
continue;
}
int biomeY = Math.max(0, Math.min(getEngine().getHeight() - 1, y));
IrisBiome surfaceBiome = context.getBiome().get(x, z);
IrisBiome depositBiome = oreDeposit || k.usesCaveBiomeFilter()
IrisBiome depositBiome = needsCaveBiome
? getEngine().getCaveBiome(
(cx << 4) + x, biomeY, (cz << 4) + z, carvingState)
: null;
@@ -172,13 +220,13 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
if (depositBiome != null) {
double frequencyMultiplier = depositBiome.getOreDepositFrequencyMultiplier();
if (frequencyMultiplier < 1D
&& !passesOreFrequency(frequencyMultiplier, rng.d())) {
&& !passesOreFrequency(frequencyMultiplier, clumpRng.d())) {
continue;
}
double sizeMultiplier = depositBiome.getOreDepositSizeMultiplier();
if (sizeMultiplier != 1D) {
IrisObject scaledClump = k.getClump(getEngine(), rng, getData(), sizeMultiplier);
IrisObject scaledClump = k.getClump(getEngine(), clumpRng, getData(), sizeMultiplier);
int scaledDimension = scaledClump.getW();
x = clampDepositCenter(x, scaledDimension, 16);
if (clippedHeight) {
@@ -190,9 +238,30 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
}
}
IrisDimension dimension = getDimension();
clumps.add(new PreparedClump(clump, x, y, z, clumpRng));
}
return new PreparedDeposit(k, oreDeposit, clumps);
}
for (art.arcane.iris.util.common.math.IrisBlockVector j : clump.getBlocks().keys()) {
private void place(PreparedDeposit deposit, MantleChunk chunk, Hunk<PlatformBlockState> data,
int cx, int cz, HeightMap he, ChunkContext context) {
if (deposit.clumps().isEmpty()) {
return;
}
IrisDepositGenerator k = deposit.generator();
boolean oreDeposit = deposit.ore();
IrisDimensionCarvingResolver.State carvingState = new IrisDimensionCarvingResolver.State();
IrisDimension dimension = getDimension();
for (PreparedClump prepared : deposit.clumps()) {
IrisObject clump = prepared.object();
int x = prepared.x();
int y = prepared.y();
int z = prepared.z();
RNG rng = prepared.rng();
VectorMap<PlatformBlockState>.Cursor cursor = clump.getBlocks().cursor();
while (cursor.next()) {
IrisBlockVector j = cursor.key();
int nx = j.getBlockX() + x;
int ny = j.getBlockY() + y;
int nz = j.getBlockZ() + z;
@@ -236,7 +305,7 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
}
if (chunk.get(nx, ny, nz, MatterCavern.class) == null) {
PlatformBlockState ore = clump.getBlocks().get(j);
PlatformBlockState ore = cursor.value();
PlatformBlockState remapped = resolveDepositVariant(
cx, cz, nx, ny, nz, ore, dimension, context, carvingState);
PlatformBlockState finalBlock = remapped != null
@@ -421,4 +490,20 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
return null;
}
static long clumpSeed(long seed, int attempt) {
long mixed = seed + 0x9e3779b97f4a7c15L * (attempt + 1L);
mixed = (mixed ^ (mixed >>> 30)) * 0xbf58476d1ce4e5b9L;
mixed = (mixed ^ (mixed >>> 27)) * 0x94d049bb133111ebL;
return mixed ^ (mixed >>> 31);
}
private record DepositPlan(IrisDepositGenerator generator, long seed, boolean ore, int attempts) {
}
private record PreparedDeposit(IrisDepositGenerator generator, boolean ore, List<PreparedClump> clumps) {
}
private record PreparedClump(IrisObject object, int x, int y, int z, RNG rng) {
}
}
@@ -92,6 +92,7 @@ public class IrisBiome extends IrisRegistrant implements IRare {
private final transient AtomicCache<IrisBiome> realCarveBiome = new AtomicCache<>();
private final transient AtomicCache<KList<IrisBiome>> realChildren = new AtomicCache<>();
private final transient AtomicCache<KList<CNG>> layerHeightGenerators = new AtomicCache<>();
private final transient AtomicCache<KList<CNG>> layerCeilingHeightGenerators = new AtomicCache<>();
private final transient AtomicCache<KList<CNG>> layerSeaHeightGenerators = new AtomicCache<>();
private final transient AtomicCache<KList<IrisOreGenerator>> surfaceOreCache = new AtomicCache<>();
private final transient AtomicCache<KList<IrisOreGenerator>> undergroundOreCache = new AtomicCache<>();
@@ -74,7 +74,7 @@ final class IrisBiomeLayerGenerator {
IrisBiomePaletteLayer layer = layers.get(i);
double zoom = layer.getZoom();
CNG hgen = heightGenerators.get(i);
double d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom);
int d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom);
IrisSlopeClip sc = layer.getSlopeCondition();
@@ -88,17 +88,7 @@ final class IrisBiomeLayerGenerator {
continue;
}
for (int j = 0; j < d; j++) {
if (data.size() >= maxDepth) {
break;
}
try {
data.add(layer.get(random, i + j, (wx + j) / zoom, j, (wz - j) / zoom, rdata));
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
appendLayer(data, layer, i, d, wx, wz, random, maxDepth, rdata);
if (data.size() >= maxDepth) {
break;
@@ -132,31 +122,20 @@ final class IrisBiomeLayerGenerator {
return data;
}
KList<CNG> heightGenerators = getLayerHeightGenerators(biome, random, rdata);
// Ceiling layers reuse the surface-layer height generators, so entries beyond layers.size() have no generator; skip them.
int usableLayers = Math.min(layerCount, heightGenerators.size());
KList<CNG> heightGenerators = getHeightGenerators(
biome.getLayerCeilingHeightGenerators(), ceilingLayers, 7235, random, rdata);
for (int i = 0; i < usableLayers; i++) {
for (int i = 0; i < layerCount; i++) {
IrisBiomePaletteLayer layer = ceilingLayers.get(i);
double zoom = layer.getZoom();
CNG hgen = heightGenerators.get(i);
double d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom);
int d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom);
if (d <= 0) {
continue;
}
for (int j = 0; j < d; j++) {
if (data.size() >= maxDepth) {
break;
}
try {
data.add(layer.get(random, i + j, (wx + j) / zoom, j, (wz - j) / zoom, rdata));
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
appendLayer(data, layer, i, d, wx, wz, random, maxDepth, rdata);
if (data.size() >= maxDepth) {
break;
@@ -198,7 +177,7 @@ final class IrisBiomeLayerGenerator {
IrisBiomePaletteLayer layer = layers.get(i);
double zoom = layer.getZoom();
CNG hgen = heightGenerators.get(i);
double d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom);
int d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom);
IrisSlopeClip sc = layer.getSlopeCondition();
@@ -212,13 +191,7 @@ final class IrisBiomeLayerGenerator {
continue;
}
for (int j = 0; j < d; j++) {
try {
data.add(layer.get(random, i + j, (wx + j) / zoom, j, (wz - j) / zoom, rdata));
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
appendLayer(data, layer, i, d, wx, wz, random, Integer.MAX_VALUE, rdata);
}
}
@@ -227,9 +200,8 @@ final class IrisBiomeLayerGenerator {
}
for (int i = 0; i < maxDepth; i++) {
int offset = (512 - height) - i;
int index = offset % data.size();
real.add(data.get(Math.max(index, 0)));
long offset = 512L - height - i;
real.add(data.get(Math.floorMod(offset, data.size())));
}
return real;
@@ -264,17 +236,7 @@ final class IrisBiomeLayerGenerator {
continue;
}
for (int j = 0; j < d; j++) {
if (data.size() >= maxDepth) {
break;
}
try {
data.add(layer.get(random, i + j, (wx + j) / zoom, j, (wz - j) / zoom, rdata));
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
appendLayer(data, layer, i, d, wx, wz, random, maxDepth, rdata);
if (data.size() >= maxDepth) {
break;
@@ -285,7 +247,16 @@ final class IrisBiomeLayerGenerator {
}
static KList<CNG> getLayerHeightGenerators(IrisBiome biome, RNG rng, IrisData rdata) {
AtomicCache<KList<CNG>> cache = biome.getLayerHeightGenerators();
return getHeightGenerators(biome.getLayerHeightGenerators(), biome.getLayers(), 7235, rng, rdata);
}
static KList<CNG> getLayerSeaHeightGenerators(IrisBiome biome, RNG rng, IrisData data) {
return getHeightGenerators(biome.getLayerSeaHeightGenerators(), biome.getSeaLayers(), 7735, rng, data);
}
private static KList<CNG> getHeightGenerators(AtomicCache<KList<CNG>> cache,
KList<IrisBiomePaletteLayer> layers,
int seedOffset, RNG rng, IrisData data) {
KList<CNG> cached = cache.getIfPresent();
if (cached != null) {
@@ -296,36 +267,28 @@ final class IrisBiomeLayerGenerator {
{
KList<CNG> layerHeightGenerators = new KList<>();
int m = 7235;
int m = seedOffset;
for (IrisBiomePaletteLayer i : biome.getLayers()) {
layerHeightGenerators.add(i.getHeightGenerator(rng.nextParallelRNG((m++) * m * m * m), rdata));
for (IrisBiomePaletteLayer layer : layers) {
layerHeightGenerators.add(layer.getHeightGenerator(rng.nextParallelRNG((m++) * m * m * m), data));
}
return layerHeightGenerators;
});
}
static KList<CNG> getLayerSeaHeightGenerators(IrisBiome biome, RNG rng, IrisData data) {
AtomicCache<KList<CNG>> cache = biome.getLayerSeaHeightGenerators();
KList<CNG> cached = cache.getIfPresent();
if (cached != null) {
return cached;
}
return cache.aquire(() ->
{
KList<CNG> layerSeaHeightGenerators = new KList<>();
int m = 7735;
for (IrisBiomePaletteLayer i : biome.getSeaLayers()) {
layerSeaHeightGenerators.add(i.getHeightGenerator(rng.nextParallelRNG((m++) * m * m * m), data));
private static void appendLayer(KList<PlatformBlockState> blocks, IrisBiomePaletteLayer layer,
int layerIndex, int thickness, double x, double z,
RNG random, int maxDepth, IrisData data) {
double zoom = layer.getZoom();
for (int offset = 0; offset < thickness && blocks.size() < maxDepth; offset++) {
try {
blocks.add(layer.get(random, layerIndex + offset,
(x + offset) / zoom, offset, (z - offset) / zoom, data));
} catch (Throwable error) {
IrisLogging.reportError(error);
}
return layerSeaHeightGenerators;
});
}
}
static PlatformBlockState getSurfaceBlock(IrisBiome biome, int x, int z, RNG rng, IrisData idm) {
@@ -2,7 +2,6 @@ package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.util.project.interpolation.IrisInterpolation;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.project.noise.CNG;
@@ -149,21 +148,20 @@ public final class IrisDimensionCarvingResolver {
return parent;
}
ParentSelectionPlan selectionPlan = state.selectionPlans.get(parent);
IrisRaritySelection<CarvingChoice> selectionPlan = state.selectionPlans.get(parent);
if (selectionPlan == null) {
selectionPlan = buildSelectionPlan(engine, parent, parentBiome, entryIndex, state);
state.selectionPlans.put(parent, selectionPlan);
}
if (selectionPlan.parentOnly) {
if (selectionPlan.size() <= 1L) {
return parent;
}
long seed = resolveChildSeed(engine, state);
CNG childGenerator = parent.getChildrenGenerator(seed, engine.getData());
double sample = childGenerator.noiseFast2D(worldX, worldZ);
int selectedIndex = (int) Math.round(IrisInterpolation.lerp(0, selectionPlan.maxIndex, sample));
CarvingChoice selected = selectionPlan.get(selectedIndex);
CarvingChoice selected = selectionPlan.select(sample);
if (selected == null || selected.entry == null) {
return parent;
}
@@ -171,7 +169,7 @@ public final class IrisDimensionCarvingResolver {
return selected.entry;
}
private static ParentSelectionPlan buildSelectionPlan(
private static IrisRaritySelection<CarvingChoice> buildSelectionPlan(
Engine engine,
IrisDimensionCarvingEntry parent,
IrisBiome parentBiome,
@@ -201,42 +199,7 @@ public final class IrisDimensionCarvingResolver {
}
options.add(new CarvingChoice(parent, rarity(parentBiome)));
if (options.size() <= 1) {
return ParentSelectionPlan.parentOnly();
}
CarvingChoice[] mappedChoices = buildRarityMappedChoices(options);
if (mappedChoices.length == 0) {
return ParentSelectionPlan.parentOnly();
}
return new ParentSelectionPlan(mappedChoices);
}
private static CarvingChoice[] buildRarityMappedChoices(List<CarvingChoice> choices) {
int max = 1;
for (CarvingChoice choice : choices) {
if (choice.rarity > max) {
max = choice.rarity;
}
}
max++;
List<CarvingChoice> mapped = new ArrayList<>();
boolean flip = false;
for (CarvingChoice choice : choices) {
int count = max - choice.rarity;
for (int index = 0; index < count; index++) {
flip = !flip;
if (flip) {
mapped.add(choice);
} else {
mapped.add(0, choice);
}
}
}
return mapped.toArray(new CarvingChoice[0]);
return IrisRaritySelection.create(options);
}
private static int rarity(IrisBiome biome) {
@@ -285,7 +248,7 @@ public final class IrisDimensionCarvingResolver {
public static final class State {
private final Map<Integer, IrisDimensionCarvingEntry> rootEntriesByWorldY = new HashMap<>();
private final Map<IrisDimensionCarvingEntry, ParentSelectionPlan> selectionPlans = new IdentityHashMap<>();
private final Map<IrisDimensionCarvingEntry, IrisRaritySelection<CarvingChoice>> selectionPlans = new IdentityHashMap<>();
private final Map<IrisDimensionCarvingEntry, IrisBiome> biomeCache = new IdentityHashMap<>();
private WeakReference<Engine> engineIdentity;
private WeakReference<IrisDimension> dimensionIdentity;
@@ -316,44 +279,6 @@ public final class IrisDimensionCarvingResolver {
}
}
private static final class ParentSelectionPlan {
private final CarvingChoice[] mappedChoices;
private final int maxIndex;
private final boolean parentOnly;
private ParentSelectionPlan(CarvingChoice[] mappedChoices) {
this.mappedChoices = mappedChoices;
this.maxIndex = mappedChoices.length - 1;
this.parentOnly = false;
}
private ParentSelectionPlan() {
this.mappedChoices = null;
this.maxIndex = -1;
this.parentOnly = true;
}
private static ParentSelectionPlan parentOnly() {
return new ParentSelectionPlan();
}
private CarvingChoice get(int index) {
if (mappedChoices == null || mappedChoices.length == 0) {
return null;
}
if (index < 0) {
return mappedChoices[0];
}
if (index >= mappedChoices.length) {
return mappedChoices[mappedChoices.length - 1];
}
return mappedChoices[index];
}
}
private static final class CarvingChoice implements IRare {
private final IrisDimensionCarvingEntry entry;
private final int rarity;
@@ -110,7 +110,7 @@ public class IrisObject extends IrisRegistrant {
this.d = d;
center = new Vector3i(w / 2, h / 2, d / 2);
shrinkOffset = new Vector3i(0, 0, 0);
var lock = new ReentrantReadWriteLock();
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
readLock = lock.readLock();
writeLock = lock.writeLock();
}
@@ -135,17 +135,22 @@ public class IrisObject extends IrisRegistrant {
return aabb.aquire(() -> getAABBFor(new IrisBlockVector(w, h, d)));
}
public synchronized IrisObject copy() {
IrisObject o = new IrisObject(w, h, d);
o.setLoadKey(getLoadKey());
o.setLoader(getLoader());
o.setLoadFile(getLoadFile());
o.setCenter(getCenter().clone());
public IrisObject copy() {
readLock.lock();
try {
IrisObject copy = new IrisObject(w, h, d);
copy.setLoadKey(getLoadKey());
copy.setLoader(getLoader());
copy.setLoadFile(getLoadFile());
copy.setCenter(getCenter().clone());
blocks.forEach((i, v) -> o.blocks.put(i.clone(), v));
states.forEach((i, v) -> o.states.put(i.clone(), v.clone()));
blocks.forEach(copy.blocks::put);
states.forEach((position, tile) -> copy.states.put(position, tile.clone()));
return o;
return copy;
} finally {
readLock.unlock();
}
}
public void readLegacy(InputStream in) throws IOException {
@@ -350,7 +350,7 @@ final class IrisObjectPlacementRunner {
return -1;
}
if (!rawStructurePiece && nativeStructureVetoes(placer, config, spin, translating, translateOffset, ceilingHang,
if (!rawStructurePiece && nativeStructureVetoes(placer, config, spin, placementBounds, translating, translateOffset, ceilingHang,
yv < 0 && config.getMode() == ObjectPlaceMode.PAINT, warpMargin, x, y + yrand, z)) {
return -1;
}
@@ -533,6 +533,9 @@ final class IrisObjectPlacementRunner {
}
data = config.getRotation().rotate(data, spinx, spiny, spinz);
if (data == null) {
continue;
}
xx = x + (int) Math.round(i.getX());
int yy = y + (int) Math.round(i.getY());
@@ -955,7 +958,7 @@ final class IrisObjectPlacementRunner {
* near this placement means no per block work at all. Only when the placement envelope meets a piece does the
* precise pass run, and the first solid block inside a piece rejects the whole object before any write.
*/
private boolean nativeStructureVetoes(IObjectPlacer placer, IrisObjectPlacement config, SpinKernel spin,
private boolean nativeStructureVetoes(IObjectPlacer placer, IrisObjectPlacement config, SpinKernel spin, TransformedBounds bounds,
boolean translating, IrisBlockVector translateOffset, boolean ceilingHang,
boolean paint, int warpMargin, int x, int y, int z) {
Engine engine = placer.getEngine();
@@ -963,23 +966,21 @@ final class IrisObjectPlacementRunner {
return false;
}
int margin = (Math.max(self.getW(), Math.max(self.getH(), self.getD())) / 2) + 1 + warpMargin;
if (translating) {
margin += Math.max(Math.abs(translateOffset.getBlockX()),
Math.max(Math.abs(translateOffset.getBlockY()), Math.abs(translateOffset.getBlockZ())));
}
KList<NativeStructureVolume> volumes = engine.getNativeStructureVolumes(x - margin, z - margin, x + margin, z + margin);
int minX = x + bounds.minX();
int maxX = x + bounds.maxX();
int minZ = z + bounds.minZ();
int maxZ = z + bounds.maxZ();
KList<NativeStructureVolume> volumes = engine.getNativeStructureVolumes(minX, minZ, maxX, maxZ);
if (volumes == null || volumes.isEmpty()) {
return false;
}
int worldY = y + engine.getMinHeight();
int envelopeMinY = paint ? Integer.MIN_VALUE : worldY - margin;
int envelopeMaxY = paint ? Integer.MAX_VALUE : worldY + margin;
int envelopeMinY = paint ? Integer.MIN_VALUE : worldY + bounds.minY() - warpMargin;
int envelopeMaxY = paint ? Integer.MAX_VALUE : worldY + bounds.maxY() + warpMargin;
boolean envelopeMeetsPiece = false;
for (NativeStructureVolume volume : volumes) {
if (volume.intersects(x - margin, envelopeMinY, z - margin, x + margin, envelopeMaxY, z + margin)) {
if (volume.intersects(minX, envelopeMinY, minZ, maxX, envelopeMaxY, maxZ)) {
envelopeMeetsPiece = true;
break;
}
@@ -22,17 +22,10 @@ import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.VectorMap;
import art.arcane.iris.util.common.math.IrisBlockVector;
import art.arcane.iris.util.common.math.IrisVector;
import art.arcane.iris.util.common.math.Vector3i;
import art.arcane.iris.util.common.parallel.BurstExecutor;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Volume shaping for {@link IrisObject}: smart boring, shrinkwrapping, compaction and the shared block
* classification helpers used by placement.
@@ -62,30 +55,23 @@ final class IrisObjectShaping {
private static void ensureSmartBoredLocked(IrisObject self) {
PrecisionStopwatch p = PrecisionStopwatch.start();
PlatformBlockState vair = IrisObject.States.VAIR;
AtomicInteger applied = new AtomicInteger();
IrisBlockVector max = new IrisBlockVector(Double.MIN_VALUE, Double.MIN_VALUE, Double.MIN_VALUE);
IrisBlockVector min = new IrisBlockVector(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE);
VectorMap<PlatformBlockState> source;
self.readLock.lock();
try {
if (self.blocks.isEmpty()) {
IrisLogging.warn("Cannot Smart Bore " + self.getLoadKey() + " because it has 0 blocks in it.");
self.smartBored = true;
return;
}
int applied = 0;
IrisBlockVector max = new IrisBlockVector(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE);
IrisBlockVector min = new IrisBlockVector(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE);
VectorMap<PlatformBlockState> source = self.blocks;
if (source.isEmpty()) {
IrisLogging.warn("Cannot Smart Bore " + self.getLoadKey() + " because it has 0 blocks in it.");
self.smartBored = true;
return;
}
source = self.blocks;
for (IrisBlockVector i : self.blocks.keys()) {
max.setX(Math.max(i.getX(), max.getX()));
min.setX(Math.min(i.getX(), min.getX()));
max.setY(Math.max(i.getY(), max.getY()));
min.setY(Math.min(i.getY(), min.getY()));
max.setZ(Math.max(i.getZ(), max.getZ()));
min.setZ(Math.min(i.getZ(), min.getZ()));
}
} finally {
self.readLock.unlock();
for (IrisBlockVector i : source.keys()) {
max.setX(Math.max(i.getX(), max.getX()));
min.setX(Math.min(i.getX(), min.getX()));
max.setY(Math.max(i.getY(), max.getY()));
min.setY(Math.min(i.getY(), min.getY()));
max.setZ(Math.max(i.getZ(), max.getZ()));
min.setZ(Math.min(i.getZ(), min.getZ()));
}
VectorMap<PlatformBlockState> bore = new VectorMap<>();
@@ -93,114 +79,94 @@ final class IrisObjectShaping {
// bored volume must come from a fixed X->Y->Z order on the calling (lock-owning)
// thread, not from pool scheduling. This matches what burst-worker callers (studio,
// generation threads) already produced, so output is unchanged where it was stable.
BurstExecutor burst = MultiBurst.burst.burst();
burst.setMulticore(false);
// Smash X
for (int rayY = min.getBlockY(); rayY <= max.getBlockY(); rayY++) {
int finalRayY = rayY;
burst.queue(() -> {
for (int rayZ = min.getBlockZ(); rayZ <= max.getBlockZ(); rayZ++) {
int start = Integer.MAX_VALUE;
int end = Integer.MIN_VALUE;
for (int rayZ = min.getBlockZ(); rayZ <= max.getBlockZ(); rayZ++) {
int start = Integer.MAX_VALUE;
int end = Integer.MIN_VALUE;
for (int ray = min.getBlockX(); ray <= max.getBlockX(); ray++) {
if (boreContains(source, bore, new IrisBlockVector(ray, finalRayY, rayZ))) {
start = Math.min(ray, start);
end = Math.max(ray, end);
}
}
if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) {
for (int i = start; i <= end; i++) {
boreCell(source, bore, new IrisBlockVector(i, finalRayY, rayZ), vair, applied);
}
for (int ray = min.getBlockX(); ray <= max.getBlockX(); ray++) {
if (boreContains(source, bore, new IrisBlockVector(ray, rayY, rayZ))) {
start = Math.min(ray, start);
end = Math.max(ray, end);
}
}
});
if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) {
for (int i = start; i <= end; i++) {
applied += boreCell(source, bore, new IrisBlockVector(i, rayY, rayZ), vair);
}
}
}
}
// Smash Y
for (int rayX = min.getBlockX(); rayX <= max.getBlockX(); rayX++) {
int finalRayX = rayX;
burst.queue(() -> {
for (int rayZ = min.getBlockZ(); rayZ <= max.getBlockZ(); rayZ++) {
int start = Integer.MAX_VALUE;
int end = Integer.MIN_VALUE;
for (int rayZ = min.getBlockZ(); rayZ <= max.getBlockZ(); rayZ++) {
int start = Integer.MAX_VALUE;
int end = Integer.MIN_VALUE;
for (int ray = min.getBlockY(); ray <= max.getBlockY(); ray++) {
if (boreContains(source, bore, new IrisBlockVector(finalRayX, ray, rayZ))) {
start = Math.min(ray, start);
end = Math.max(ray, end);
}
}
if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) {
for (int i = start; i <= end; i++) {
boreCell(source, bore, new IrisBlockVector(finalRayX, i, rayZ), vair, applied);
}
for (int ray = min.getBlockY(); ray <= max.getBlockY(); ray++) {
if (boreContains(source, bore, new IrisBlockVector(rayX, ray, rayZ))) {
start = Math.min(ray, start);
end = Math.max(ray, end);
}
}
});
if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) {
for (int i = start; i <= end; i++) {
applied += boreCell(source, bore, new IrisBlockVector(rayX, i, rayZ), vair);
}
}
}
}
// Smash Z
for (int rayX = min.getBlockX(); rayX <= max.getBlockX(); rayX++) {
int finalRayX = rayX;
burst.queue(() -> {
for (int rayY = min.getBlockY(); rayY <= max.getBlockY(); rayY++) {
int start = Integer.MAX_VALUE;
int end = Integer.MIN_VALUE;
for (int rayY = min.getBlockY(); rayY <= max.getBlockY(); rayY++) {
int start = Integer.MAX_VALUE;
int end = Integer.MIN_VALUE;
for (int ray = min.getBlockZ(); ray <= max.getBlockZ(); ray++) {
if (boreContains(source, bore, new IrisBlockVector(finalRayX, rayY, ray))) {
start = Math.min(ray, start);
end = Math.max(ray, end);
}
}
if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) {
for (int i = start; i <= end; i++) {
boreCell(source, bore, new IrisBlockVector(finalRayX, rayY, i), vair, applied);
}
for (int ray = min.getBlockZ(); ray <= max.getBlockZ(); ray++) {
if (boreContains(source, bore, new IrisBlockVector(rayX, rayY, ray))) {
start = Math.min(ray, start);
end = Math.max(ray, end);
}
}
});
}
burst.complete();
self.writeLock.lock();
try {
if (!self.smartBored) {
bore.forEach((v, s) -> self.blocks.computeIfAbsent(v, (vv) -> s));
self.smartBored = true;
if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) {
for (int i = start; i <= end; i++) {
applied += boreCell(source, bore, new IrisBlockVector(rayX, rayY, i), vair);
}
}
}
} finally {
self.writeLock.unlock();
}
IrisLogging.debug("Smart Bore: " + self.getLoadKey() + " in " + Form.duration(p.getMilliseconds(), 2) + " (" + Form.f(applied.get()) + ")");
source.putAll(bore);
self.smartBored = true;
IrisLogging.debug("Smart Bore: " + self.getLoadKey() + " in " + Form.duration(p.getMilliseconds(), 2) + " (" + Form.f(applied) + ")");
}
private static boolean boreContains(VectorMap<PlatformBlockState> source, VectorMap<PlatformBlockState> bore, IrisBlockVector v) {
return source.containsKey(v) || bore.containsKey(v);
}
private static void boreCell(VectorMap<PlatformBlockState> source, VectorMap<PlatformBlockState> bore, IrisBlockVector v, PlatformBlockState vair, AtomicInteger applied) {
private static int boreCell(VectorMap<PlatformBlockState> source, VectorMap<PlatformBlockState> bore, IrisBlockVector v, PlatformBlockState vair) {
PlatformBlockState existing = source.get(v);
if (existing == null) {
if (vair.equals(bore.get(v))) {
return;
return 0;
}
bore.computeIfAbsent(v, (vv) -> vair);
bore.put(v, vair);
} else if (vair.equals(existing)) {
return;
return 0;
}
applied.getAndIncrement();
return 1;
}
static void shrinkwrap(IrisObject self) {
@@ -289,23 +255,4 @@ final class IrisObjectShaping {
static String materialKey(PlatformBlockState state) {
return IrisProceduralBlocks.materialKey(state);
}
static List<IrisBlockVector> blocksBetweenTwoPoints(IrisVector loc1, IrisVector loc2) {
List<IrisBlockVector> locations = new ArrayList<>();
int topBlockX = Math.max(loc1.getBlockX(), loc2.getBlockX());
int bottomBlockX = Math.min(loc1.getBlockX(), loc2.getBlockX());
int topBlockY = Math.max(loc1.getBlockY(), loc2.getBlockY());
int bottomBlockY = Math.min(loc1.getBlockY(), loc2.getBlockY());
int topBlockZ = Math.max(loc1.getBlockZ(), loc2.getBlockZ());
int bottomBlockZ = Math.min(loc1.getBlockZ(), loc2.getBlockZ());
for (int x = bottomBlockX; x <= topBlockX; x++) {
for (int z = bottomBlockZ; z <= topBlockZ; z++) {
for (int y = bottomBlockY; y <= topBlockY; y++) {
locations.add(new IrisBlockVector(x, y, z));
}
}
}
return locations;
}
}
@@ -30,10 +30,12 @@ import art.arcane.iris.util.project.interpolation.Interpolation3D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/**
* Geometric transforms for {@link IrisObject}: rotation, scaling and the interpolated upscalers.
@@ -52,15 +54,23 @@ final class IrisObjectTransforms {
self.writeLock.lock();
try {
VectorMap<PlatformBlockState> d = new VectorMap<>();
Set<IrisBlockVector> omitted = new HashSet<>();
for (var entry : self.blocks) {
d.put(r.rotate(entry.getKey(), spinx, spiny, spinz), r.rotate(entry.getValue(), spinx, spiny, spinz));
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : self.blocks) {
PlatformBlockState rotated = r.rotate(entry.getValue(), spinx, spiny, spinz);
if (rotated == null) {
omitted.add(entry.getKey());
continue;
}
d.put(r.rotate(entry.getKey(), spinx, spiny, spinz), rotated);
}
VectorMap<TileData> dx = new VectorMap<>();
for (var entry : self.states) {
dx.put(r.rotate(entry.getKey(), spinx, spiny, spinz), entry.getValue());
for (Map.Entry<IrisBlockVector, TileData> entry : self.states) {
if (!omitted.contains(entry.getKey())) {
dx.put(r.rotate(entry.getKey(), spinx, spiny, spinz), entry.getValue());
}
}
self.blocks = d;
@@ -96,32 +106,31 @@ final class IrisObjectTransforms {
scale = scale - 0.0001;
}
IrisPosition l1 = self.getAABB().max();
IrisPosition l2 = self.getAABB().min();
VectorMap<PlatformBlockState> placeBlock = new VectorMap<>();
VectorMap<TileData> placeTile = new VectorMap<>();
VectorMap<IrisBlockVector> placeMax = savedOrigin && scale > 1 ? new VectorMap<>() : null;
IrisVector center = new IrisVector(self.getCenter().getX(), self.getCenter().getY(), self.getCenter().getZ());
if (self.getH() == 2) {
center = center.setY(center.getBlockY() + 0.5);
}
if (self.getW() == 2) {
center = center.setX(center.getBlockX() + 0.5);
}
if (self.getD() == 2) {
center = center.setZ(center.getBlockZ() + 0.5);
}
IrisObject oo = savedOrigin ? createOriginScaledObject(self, scale)
: new IrisObject((int) Math.ceil((self.w * scale) + (scale * 2)), (int) Math.ceil((self.h * scale) + (scale * 2)), (int) Math.ceil((self.d * scale) + (scale * 2)));
oo.setLoadKey(self.getLoadKey());
oo.setLoader(self.getLoader());
oo.setLoadFile(self.getLoadFile());
IrisVector center;
IrisObject oo;
boolean hasTiles;
self.readLock.lock();
try {
center = new IrisVector(self.getCenter().getX(), self.getCenter().getY(), self.getCenter().getZ());
if (self.getH() == 2) {
center = center.setY(center.getBlockY() + 0.5);
}
if (self.getW() == 2) {
center = center.setX(center.getBlockX() + 0.5);
}
if (self.getD() == 2) {
center = center.setZ(center.getBlockZ() + 0.5);
}
oo = savedOrigin ? createOriginScaledObject(self, scale)
: new IrisObject((int) Math.ceil((self.w * scale) + (scale * 2)), (int) Math.ceil((self.h * scale) + (scale * 2)), (int) Math.ceil((self.d * scale) + (scale * 2)));
oo.setLoadKey(self.getLoadKey());
oo.setLoader(self.getLoader());
oo.setLoadFile(self.getLoadFile());
hasTiles = !self.states.isEmpty();
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : self.blocks) {
PlatformBlockState bd = entry.getValue();
@@ -153,13 +162,27 @@ final class IrisObjectTransforms {
if (scale > 1) {
IrisVector minimum = savedOrigin ? v : v.clone().add(center);
IrisVector maximum = savedOrigin ? placeMax.get(v) : v.clone().add(center).add(sm1);
for (IrisBlockVector vec : IrisObjectShaping.blocksBetweenTwoPoints(minimum, maximum)) {
oo.blocks.put(vec, entry.getValue());
if (hasTiles) {
if (tile == null) {
oo.states.remove(vec);
} else {
oo.states.put(vec, tile.clone());
int minX = Math.min(minimum.getBlockX(), maximum.getBlockX());
int maxX = Math.max(minimum.getBlockX(), maximum.getBlockX());
int minY = Math.min(minimum.getBlockY(), maximum.getBlockY());
int maxY = Math.max(minimum.getBlockY(), maximum.getBlockY());
int minZ = Math.min(minimum.getBlockZ(), maximum.getBlockZ());
int maxZ = Math.max(minimum.getBlockZ(), maximum.getBlockZ());
IrisBlockVector position = new IrisBlockVector(minX, minY, minZ);
for (int x = minX; x <= maxX; x++) {
position.setX(x);
for (int z = minZ; z <= maxZ; z++) {
position.setZ(z);
for (int y = minY; y <= maxY; y++) {
position.setY(y);
oo.blocks.put(position, entry.getValue());
if (hasTiles) {
if (tile == null) {
oo.states.remove(position);
} else {
oo.states.put(position, tile.clone());
}
}
}
}
}
@@ -58,7 +58,7 @@ public class IrisOreGenerator {
}
public PlatformBlockState generate(int x, int y, int z, RNG rng, IrisData data) {
if (!range.contains(y)) {
if (threshold <= 0 || !range.contains(y)) {
return null;
}
@@ -0,0 +1,60 @@
package art.arcane.iris.engine.object;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public final class IrisRaritySelection<T extends IRare> {
private final List<T> choices;
private final long[] cumulativeCounts;
private IrisRaritySelection(List<T> choices, long[] cumulativeCounts) {
this.choices = choices;
this.cumulativeCounts = cumulativeCounts;
}
public static <T extends IRare> IrisRaritySelection<T> create(List<T> options) {
ArrayList<T> choices = new ArrayList<>(options.size());
long maximumRarity = 1L;
for (T option : options) {
if (option != null) {
choices.add(option);
maximumRarity = Math.max(maximumRarity, option.getRarity());
}
}
long[] cumulativeCounts = new long[choices.size()];
long total = 0L;
for (int index = 0; index < choices.size(); index++) {
total += maximumRarity + 1L - Math.max(1, choices.get(index).getRarity());
cumulativeCounts[index] = total;
}
return new IrisRaritySelection<>(List.copyOf(choices), cumulativeCounts);
}
public long size() {
return cumulativeCounts.length == 0 ? 0L : cumulativeCounts[cumulativeCounts.length - 1];
}
public T select(double noiseValue) {
long maximumIndex = size() - 1L;
long selectedIndex = Math.round(noiseValue * maximumIndex);
return get(maximumIndex <= Integer.MAX_VALUE ? (int) selectedIndex : selectedIndex);
}
public T get(long index) {
long size = size();
if (size == 0L) {
return null;
}
if (choices.size() == 1) {
return choices.getFirst();
}
long selectedIndex = Math.max(0L, Math.min(size - 1L, index));
long frontSize = size / 2L;
long insertionIndex = selectedIndex < frontSize
? 2L * (frontSize - selectedIndex) - 1L
: 2L * (selectedIndex - frontSize);
int choiceIndex = Arrays.binarySearch(cumulativeCounts, insertionIndex + 1L);
return choices.get(choiceIndex < 0 ? -choiceIndex - 1 : choiceIndex);
}
}
@@ -88,16 +88,18 @@ public final class CoralGenerator {
private static void buildFan(CoralCanvas canvas, IrisCoral coral, int height, long seed) {
int half = Math.max(1, coral.getFanWidth());
for (int y = 0; y < height; y++) {
int tippedWidth = -1;
for (int y = height - 1; y >= 0; y--) {
double t = y / (double) Math.max(1, height - 1);
double profile = Math.sin(Math.PI * t);
int w = (int) Math.round(half * profile);
for (int dx = -w; dx <= w; dx++) {
canvas.set(dx, y, 0, CoralCanvas.Role.STRUCTURE);
if (Math.abs(dx) > tippedWidth) {
placeTip(canvas, coral, dx, y, 0, seed + dx);
}
}
}
for (int dx = -half; dx <= half; dx++) {
placeTip(canvas, coral, dx, height - 1, 0, seed + dx);
tippedWidth = Math.max(tippedWidth, w);
}
}
@@ -170,7 +172,9 @@ public final class CoralGenerator {
int y = (int) Math.round(oy + dy * t);
int z = (int) Math.round(oz + dz * t - sway);
canvas.set(x, y, z, CoralCanvas.Role.STRUCTURE);
tip = new double[]{x, y, z};
tip[0] = x;
tip[1] = y;
tip[2] = z;
}
return tip;
}
@@ -25,35 +25,48 @@ import art.arcane.iris.engine.object.IrisProceduralBlocks;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.math.Vector3i;
import art.arcane.volmlib.util.math.RNG;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
public final class FormationBlockResolver {
private FormationBlockResolver() {
private final IrisFormation formation;
private final IrisData data;
private final RNG paletteRng;
private final Int2ObjectOpenHashMap<RNG> strataRngs = new Int2ObjectOpenHashMap<>();
public FormationBlockResolver(IrisFormation formation, IrisData data) {
this.formation = formation;
this.data = data;
paletteRng = new RNG(formation.getSeed());
}
public static PlatformBlockState resolve(IrisFormation f, IrisData data, FormationCanvas.Role role, Vector3i raw) {
public PlatformBlockState resolve(FormationCanvas.Role role, Vector3i raw) {
int x = raw.getBlockX();
int y = raw.getBlockY();
int z = raw.getBlockZ();
RNG paletteRng = new RNG(f.getSeed());
if (role == FormationCanvas.Role.CAP && capDefined(f)) {
PlatformBlockState cap = IrisProceduralBlocks.resolve(f.getCapBlock(), f.getCapPalette(), data, x, y, z, paletteRng);
if (role == FormationCanvas.Role.CAP && capDefined(formation)) {
PlatformBlockState cap = IrisProceduralBlocks.resolve(formation.getCapBlock(), formation.getCapPalette(), data, x, y, z, paletteRng);
if (cap != null) {
return cap;
}
}
if (strataDefined(f)) {
IrisMaterialPalette strata = f.getStrataPalette();
int thickness = Math.max(1, f.getStrataThickness());
if (strataDefined(formation)) {
IrisMaterialPalette strata = formation.getStrataPalette();
int thickness = Math.max(1, formation.getStrataThickness());
int band = Math.floorDiv(y, thickness);
PlatformBlockState strataState = strata.get(new RNG(f.getSeed() + (band * 31L)), x, band, z, data);
RNG strataRng = strataRngs.get(band);
if (strataRng == null) {
strataRng = new RNG(formation.getSeed() + band * 31L);
strataRngs.put(band, strataRng);
}
PlatformBlockState strataState = strata.get(strataRng, x, band, z, data);
if (strataState != null) {
return strataState;
}
}
return IrisProceduralBlocks.resolve(f.getBlock(), f.getBlockPalette(), data, x, y, z, paletteRng);
return IrisProceduralBlocks.resolve(formation.getBlock(), formation.getBlockPalette(), data, x, y, z, paletteRng);
}
private static boolean capDefined(IrisFormation f) {
@@ -62,8 +62,9 @@ public final class FormationGenerator {
}
Map<Vector3i, PlatformBlockState> resolved = new HashMap<>();
FormationBlockResolver resolver = new FormationBlockResolver(f, data);
for (Map.Entry<Vector3i, FormationCanvas.Role> entry : canvas.getCells().entrySet()) {
PlatformBlockState bd = FormationBlockResolver.resolve(f, data, entry.getValue(), entry.getKey());
PlatformBlockState bd = resolver.resolve(entry.getValue(), entry.getKey());
if (bd == null) {
continue;
}
@@ -63,11 +63,7 @@ public final class FungusGenerator {
private static void buildUpright(Map<Vector3i, FungusCellRole> roles, IrisFungus fungus, int height, RNG rng, long baseSeed) {
int stemHeight = Math.max(1, height);
Map<Vector3i, PlatformBlockState> stemCells = new HashMap<>();
double[] top = FungusStemBuilder.build(stemCells, fungus, stemHeight, baseSeed);
for (Vector3i v : stemCells.keySet()) {
roles.put(v, FungusCellRole.STEM);
}
double[] top = FungusStemBuilder.build(roles, fungus, stemHeight, baseSeed);
int radius = pickRadius(fungus, rng);
double cx = top[0];
@@ -20,7 +20,6 @@ package art.arcane.iris.engine.object.fungi;
import art.arcane.iris.engine.object.IrisFungus;
import art.arcane.iris.engine.object.tree.TreeFunctions;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.math.Vector3i;
import java.util.Map;
@@ -29,7 +28,7 @@ public final class FungusStemBuilder {
private FungusStemBuilder() {
}
public static double[] build(Map<Vector3i, PlatformBlockState> stemCells, IrisFungus fungus, int stemHeight, long seed) {
public static double[] build(Map<Vector3i, FungusCellRole> roles, IrisFungus fungus, int stemHeight, long seed) {
int width = Math.max(1, Math.min(3, fungus.getStemWidth()));
double maxLean = stemHeight * Math.tan(Math.toRadians(Math.max(0.0, fungus.getStemCurve())));
double leanRad = Math.toRadians(fungus.getStemLeanAzimuth());
@@ -40,6 +39,7 @@ public final class FungusStemBuilder {
double waveAzimuth = Math.toRadians(fungus.getStemLeanAzimuth() + 90.0);
double waveX = Math.sin(waveAzimuth);
double waveZ = Math.cos(waveAzimuth);
double wavePhase = TreeFunctions.valueNoise1D(seed, seed) * Math.PI * 2.0;
double topCx = 0.0;
double topCz = 0.0;
@@ -48,12 +48,16 @@ public final class FungusStemBuilder {
for (int y = 0; y < stemHeight; y++) {
double t = stemHeight <= 1 ? 0.0 : y / (double) (stemHeight - 1);
double lean = maxLean * (t * t);
double wave = waveAmp * Math.sin(2.0 * Math.PI * wavePeriods * t + TreeFunctions.valueNoise1D(seed, seed) * Math.PI * 2.0);
double wave = waveAmp * Math.sin(2.0 * Math.PI * wavePeriods * t + wavePhase);
double cx = lean * leanX + wave * waveX;
double cz = lean * leanZ + wave * waveZ;
for (int[] xz : squarePositions(cx, cz, width)) {
stemCells.put(new Vector3i(xz[0], y, xz[1]), null);
int ox = (width % 2 == 1 ? (int) Math.round(cx) : (int) Math.floor(cx) + 1) - width / 2;
int oz = (width % 2 == 1 ? (int) Math.round(cz) : (int) Math.floor(cz) + 1) - width / 2;
for (int dx = 0; dx < width; dx++) {
for (int dz = 0; dz < width; dz++) {
roles.put(new Vector3i(ox + dx, y, oz + dz), FungusCellRole.STEM);
}
}
topCx = cx;
@@ -62,34 +66,4 @@ public final class FungusStemBuilder {
return new double[]{topCx, topY, topCz};
}
static int[][] squarePositions(double cx, double cz, int width) {
if (width % 2 == 1) {
int half = width / 2;
int icx = (int) Math.round(cx);
int icz = (int) Math.round(cz);
int[][] out = new int[width * width][2];
int idx = 0;
for (int dx = -half; dx <= half; dx++) {
for (int dz = -half; dz <= half; dz++) {
out[idx][0] = icx + dx;
out[idx][1] = icz + dz;
idx++;
}
}
return out;
}
int ox = (int) Math.floor(cx) - width / 2 + 1;
int oz = (int) Math.floor(cz) - width / 2 + 1;
int[][] out = new int[width * width][2];
int idx = 0;
for (int dx = 0; dx < width; dx++) {
for (int dz = 0; dz < width; dz++) {
out[idx][0] = ox + dx;
out[idx][1] = oz + dz;
idx++;
}
}
return out;
}
}
@@ -66,9 +66,10 @@ public final class ProceduralTreeGenerator {
Map<TreeBlockCanvas.Vec, PlatformBlockState> resolved = new HashMap<>();
Set<TreeBlockCanvas.Vec> trunkPositions = new HashSet<>();
Set<TreeBlockCanvas.Vec> leafPositions = new HashSet<>();
RNG paletteRng = new RNG(tree.getSeed());
for (Map.Entry<TreeBlockCanvas.Vec, TreeBlockCanvas.Cell> entry : canvas.getCells().entrySet()) {
TreeBlockCanvas.Cell cell = entry.getValue();
PlatformBlockState state = TreeBlockResolver.resolve(tree, data, cell, entry.getKey());
PlatformBlockState state = TreeBlockResolver.resolve(tree, data, cell, entry.getKey(), paletteRng);
if (state == null) {
continue;
}
@@ -23,6 +23,7 @@ import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisProceduralBlocks;
import art.arcane.iris.engine.object.IrisProceduralTree;
import art.arcane.iris.engine.object.IrisTreeDecorator;
import art.arcane.iris.engine.object.IrisTreeSecondaryLeaf;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.B;
import art.arcane.volmlib.util.math.RNG;
@@ -31,10 +32,7 @@ public final class TreeBlockResolver {
private TreeBlockResolver() {
}
public static PlatformBlockState resolve(IrisProceduralTree tree, IrisData data, TreeBlockCanvas.Cell cell, TreeBlockCanvas.Vec pos) {
RNG paletteRng = new RNG(tree.getSeed());
RNG posRng = new RNG(tree.getSeed() ^ positionHash(pos));
public static PlatformBlockState resolve(IrisProceduralTree tree, IrisData data, TreeBlockCanvas.Cell cell, TreeBlockCanvas.Vec pos, RNG paletteRng) {
switch (cell.role()) {
case TRUNK -> {
PlatformBlockState state = resolveBlock(tree.getTrunk(), tree.getTrunkPalette(), data, pos, paletteRng);
@@ -51,7 +49,7 @@ public final class TreeBlockResolver {
return resolveBlock(tree.getLeaves(), tree.getLeavesPalette(), data, pos, paletteRng);
}
case SECONDARY_LEAF -> {
PlatformBlockState state = resolveSecondaryLeaf(tree, data, pos, paletteRng, posRng);
PlatformBlockState state = resolveSecondaryLeaf(tree, data, pos, paletteRng);
if (state == null) {
state = resolveBlock(tree.getLeaves(), tree.getLeavesPalette(), data, pos, paletteRng);
}
@@ -94,12 +92,12 @@ public final class TreeBlockResolver {
return state;
}
private static PlatformBlockState resolveSecondaryLeaf(IrisProceduralTree tree, IrisData data, TreeBlockCanvas.Vec pos, RNG paletteRng, RNG posRng) {
private static PlatformBlockState resolveSecondaryLeaf(IrisProceduralTree tree, IrisData data, TreeBlockCanvas.Vec pos, RNG paletteRng) {
if (TreeTrunkBuilder.paletteSet(tree.getSecondaryLeavesPalette())) {
return tree.getSecondaryLeavesPalette().get(paletteRng, pos.x(), pos.y(), pos.z(), data);
}
if (tree.getWeightedSecondaryLeaves() != null && !tree.getWeightedSecondaryLeaves().isEmpty()) {
String picked = pickWeighted(tree, posRng);
String picked = pickWeighted(tree, new RNG(tree.getSeed() ^ positionHash(pos)));
return picked == null ? null : B.getStateOrNull(picked, false);
}
if (tree.getSecondaryLeaves() != null && !tree.getSecondaryLeaves().isEmpty()) {
@@ -109,16 +107,16 @@ public final class TreeBlockResolver {
}
private static String pickWeighted(IrisProceduralTree tree, RNG rng) {
int total = 0;
for (art.arcane.iris.engine.object.IrisTreeSecondaryLeaf s : tree.getWeightedSecondaryLeaves()) {
long total = 0;
for (IrisTreeSecondaryLeaf s : tree.getWeightedSecondaryLeaves()) {
total += Math.max(0, s.getWeight());
}
if (total <= 0) {
return null;
}
double r = rng.nextDouble() * total;
double cumulative = 0;
for (art.arcane.iris.engine.object.IrisTreeSecondaryLeaf s : tree.getWeightedSecondaryLeaves()) {
long cumulative = 0;
for (IrisTreeSecondaryLeaf s : tree.getWeightedSecondaryLeaves()) {
cumulative += Math.max(0, s.getWeight());
if (r < cumulative) {
return s.getBlock();
@@ -67,11 +67,24 @@ public final class TreeDecoratorApplier {
if (rng.nextDouble() > dec.getChance()) {
continue;
}
if (canvas.has(e[0], e[1], e[2])) {
int dx = e[0] - e[3];
int dz = e[2] - e[4];
boolean alongX = Math.abs(dx) >= Math.abs(dz);
int stepX = alongX ? (dx >= 0 ? 1 : -1) : 0;
int stepZ = alongX ? 0 : (dz >= 0 ? 1 : -1);
int x = e[0] + stepX;
int z = e[2] + stepZ;
TreeBlockCanvas.Cell cell = canvas.get(x, e[1], z);
while (cell != null && cell.role() != TreeBlockCanvas.Role.DECORATOR) {
x += stepX;
z += stepZ;
cell = canvas.get(x, e[1], z);
}
if (cell != null) {
continue;
}
String facing = dec.isAxisAware() ? facingAway(e[3], e[4], e[0], e[2]) : null;
canvas.setDecor(e[0], e[1], e[2], idx, facing);
canvas.setDecor(x, e[1], z, idx, facing);
}
}
@@ -50,7 +50,9 @@ public final class TreePlausibility {
return;
}
Map<TreeBlockCanvas.Vec, Integer> distance = computeDistances(trunkPositions, realLeaves);
Map<TreeBlockCanvas.Vec, Integer> distance = tree.isPlausible()
? computeDistances(trunkPositions, realLeaves)
: Map.of();
for (TreeBlockCanvas.Vec v : realLeaves) {
PlatformBlockState leaf = resolved.get(v);
@@ -18,7 +18,6 @@
package art.arcane.iris.engine.object.tree;
import java.util.ArrayList;
import java.util.Map;
public final class TreeSupport {
@@ -36,7 +35,7 @@ public final class TreeSupport {
TreeBlockCanvas.Vec worstWood = null;
int worstGap = -1;
for (TreeBlockCanvas.Vec leaf : new ArrayList<>(canvas.getLeaf())) {
for (TreeBlockCanvas.Vec leaf : canvas.getLeaf()) {
Integer d = dist.get(leaf);
if (d != null && d <= LEGAL_DISTANCE) {
continue;
@@ -121,7 +121,7 @@ public final class TreeTrunkBuilder {
}
private static void markExposedEnds(TreeBlockCanvas canvas) {
for (TreeBlockCanvas.Vec v : new ArrayList<>(canvas.getTrunk())) {
for (TreeBlockCanvas.Vec v : canvas.getTrunk()) {
if (!canvas.has(v.x(), v.y() + 1, v.z()) || !canvas.has(v.x(), v.y() - 1, v.z())) {
canvas.markExposed(v.x(), v.y(), v.z());
}
@@ -48,6 +48,7 @@ import art.arcane.iris.engine.framework.WrongEngineBroException;
import art.arcane.iris.engine.history.GenerationActivation;
import art.arcane.iris.engine.history.GenerationEpoch;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.engine.history.GenerationAdmission;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.history.IrisBoundarySignatureSampler;
import art.arcane.iris.engine.history.TransitionGenerationPlan;
@@ -396,66 +397,68 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
}
private void setupEngine() {
lastMode = StudioMode.NORMAL;
lastJigsawStudioRequestId = null;
if (generationHistory != null) {
try {
generationHistory.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
targetCache.reset();
} catch (IOException failure) {
throw new IllegalStateException("Unable to prepare saved Iris terrain for the current generator.", failure);
}
}
EngineTarget engineTarget = getTarget();
String packKey = engineTarget.getDimension().getLoadKey();
JigsawStudioActivation.Request request = studio
? JigsawStudioActivation.getGeneratorRequest(packKey)
: null;
JigsawStudioSession session = request == null
? null
: JigsawStudioActivation.getGeneratorSession(packKey);
jigsawStudioActive = request != null
&& session != null
&& session.sessionId().equals(request.requestId());
objectStudioActive = studio && ObjectStudioActivation.isActive(packKey);
IrisEngine createdEngine = createEngine(engineTarget);
if (generationHistory != null) {
long initialActivationId = generationHistory.activeActivation().activationId();
int transitionWidthBlocks = IrisSettings.get()
.getGenerator()
.getGenerationTransitionWidthBlocks();
try {
createdEngine.attachGenerationHistory(
generationHistory,
IrisBoundarySignatureSampler.INSTANCE,
transitionWidthBlocks);
if (generationHistory.activeActivation().activationId() != initialActivationId) {
createdEngine.close();
try (GenerationAdmission.RuntimeLease startupAdmission = generationHistory == null ? null : generationHistory.retainRuntime()) {
lastMode = StudioMode.NORMAL;
lastJigsawStudioRequestId = null;
if (generationHistory != null) {
try {
generationHistory.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
targetCache.reset();
createdEngine = createEngine(loadActiveGenerationHistoryTarget());
} catch (IOException failure) {
throw new IllegalStateException("Unable to prepare saved Iris terrain for the current generator.", failure);
}
}
EngineTarget engineTarget = getTarget();
String packKey = engineTarget.getDimension().getLoadKey();
JigsawStudioActivation.Request request = studio
? JigsawStudioActivation.getGeneratorRequest(packKey)
: null;
JigsawStudioSession session = request == null
? null
: JigsawStudioActivation.getGeneratorSession(packKey);
jigsawStudioActive = request != null
&& session != null
&& session.sessionId().equals(request.requestId());
objectStudioActive = studio && ObjectStudioActivation.isActive(packKey);
IrisEngine createdEngine = createEngine(engineTarget);
if (generationHistory != null) {
long initialActivationId = generationHistory.activeActivation().activationId();
int transitionWidthBlocks = IrisSettings.get()
.getGenerator()
.getGenerationTransitionWidthBlocks();
try {
createdEngine.attachGenerationHistory(
generationHistory,
IrisBoundarySignatureSampler.INSTANCE,
transitionWidthBlocks);
}
} catch (Throwable failure) {
try {
createdEngine.close();
} catch (Throwable closeFailure) {
if (failure != closeFailure) {
failure.addSuppressed(closeFailure);
if (generationHistory.activeActivation().activationId() != initialActivationId) {
createdEngine.close();
targetCache.reset();
createdEngine = createEngine(loadActiveGenerationHistoryTarget());
createdEngine.attachGenerationHistory(
generationHistory,
IrisBoundarySignatureSampler.INSTANCE,
transitionWidthBlocks);
}
} catch (Throwable failure) {
try {
createdEngine.close();
} catch (Throwable closeFailure) {
if (failure != closeFailure) {
failure.addSuppressed(closeFailure);
}
}
throw propagateEngineSetupFailure(failure);
}
throw propagateEngineSetupFailure(failure);
}
createdEngine.setNativeStructureVolumeQueriesEnabled(shouldGenerateNativeStructures(
isAuthoringStudio(),
studioEntryBootstrapActive.get(),
initializationFailure != null));
engine = createdEngine;
populators.clear();
targetCache.reset();
}
createdEngine.setNativeStructureVolumeQueriesEnabled(shouldGenerateNativeStructures(
isAuthoringStudio(),
studioEntryBootstrapActive.get(),
initializationFailure != null));
engine = createdEngine;
populators.clear();
targetCache.reset();
}
private EngineTarget loadActiveGenerationHistoryTarget() throws IOException {
@@ -672,7 +675,6 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
@Override
public CompletableFuture<Void> closeAsync() {
closing = true;
// Outside the exclusive-control block so every close path detaches the WorldInit
// listener, including a rollback where the world never materialized. Guarded: with no
// hosted plugin (unit tests, teardown) volmitPlugin() throws, and that must not stop
@@ -689,6 +691,8 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
}
}
boolean alreadyClosing = closing;
closing = true;
CompletableFuture<Void> operation;
try {
operation = withExclusiveControlFuture(() -> {
@@ -712,11 +716,13 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
populators.clear();
});
} catch (Throwable throwable) {
future.completeExceptionally(throwable);
if (!alreadyClosing) {
// A failed close must stay retryable; leaving closing latched would permanently
// reject generation for a world that may still be loaded.
closing = false;
}
closeFuture.compareAndSet(future, null);
// A failed close must stay retryable; leaving closing latched would permanently
// reject generation for a world that may still be loaded.
closing = false;
future.completeExceptionally(throwable);
return future;
}
@@ -727,6 +733,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
// The close body already ran and tore the engine down; unlike the pre-dispatch
// failure above, resetting the gate here would advertise a healthy generator
// over a CLOSED or FAILED engine. Stay latched and surface the failure.
closeFuture.compareAndSet(future, null);
future.completeExceptionally(throwable);
}
});
@@ -11,7 +11,6 @@ import java.nio.file.Path;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class PackBiomeLayerValidatorTest {
@@ -19,14 +18,12 @@ public class PackBiomeLayerValidatorTest {
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void rejectsMoreCeilingLayersThanLayers() throws Exception {
public void acceptsMoreCeilingLayersThanSurfaceLayers() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "biomes/nested/cave.json",
"{\"layers\":[{}],\"caveCeilingLayers\":[{},{},{}]}");
assertEquals(List.of(
"Biome 'nested/cave' declares 3 caveCeilingLayers but only 1 layers. caveCeilingLayers reuses the layers height generators and must not have more entries."
), PackBiomeLayerValidator.validateCeilingLayerCounts(new File(pack, "biomes")));
assertTrue(PackBiomeLayerValidator.validateLayers(new File(pack, "biomes")).isEmpty());
}
@Test
@@ -35,9 +32,7 @@ public class PackBiomeLayerValidatorTest {
write(pack, "biomes/defaulted.json", "{\"name\":\"Defaulted\"}");
write(pack, "biomes/implicit.json", "{\"caveCeilingLayers\":[{},{}]}");
assertEquals(List.of(
"Biome 'implicit' declares 2 caveCeilingLayers but only 1 layers. caveCeilingLayers reuses the layers height generators and must not have more entries."
), PackBiomeLayerValidator.validateCeilingLayerCounts(new File(pack, "biomes")));
assertTrue(PackBiomeLayerValidator.validateLayers(new File(pack, "biomes")).isEmpty());
}
@Test
@@ -45,7 +40,7 @@ public class PackBiomeLayerValidatorTest {
File pack = temporaryFolder.newFolder("pack");
write(pack, "biomes/ok.json", "{\"layers\":[{},{},{}],\"caveCeilingLayers\":[{},{}]}");
assertTrue(PackBiomeLayerValidator.validateCeilingLayerCounts(new File(pack, "biomes")).isEmpty());
assertTrue(PackBiomeLayerValidator.validateLayers(new File(pack, "biomes")).isEmpty());
}
@Test
@@ -56,11 +51,11 @@ public class PackBiomeLayerValidatorTest {
assertEquals(List.of(
"Biome 'bad' layers must be an array.",
"Biome 'bad' caveCeilingLayers must be an array."
), PackBiomeLayerValidator.validateCeilingLayerCounts(new File(pack, "biomes")));
), PackBiomeLayerValidator.validateLayers(new File(pack, "biomes")));
}
@Test
public void ceilingLayerOverflowBlocksFullPackValidation() throws Exception {
public void independentCeilingLayersPassFullPackValidation() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "dimensions/main.json", "{\"regions\":[\"region\"]}");
write(pack, "regions/region.json", "{\"landBiomes\":[\"biome\"]}");
@@ -68,9 +63,7 @@ public class PackBiomeLayerValidatorTest {
PackValidationResult result = PackValidator.validate(pack);
assertFalse(result.isLoadable());
assertTrue(result.getBlockingErrors().contains(
"Biome 'biome' declares 2 caveCeilingLayers but only 1 layers. caveCeilingLayers reuses the layers height generators and must not have more entries."));
assertTrue(result.getBlockingErrors().toString(), result.isLoadable());
}
@Test
@@ -33,9 +33,9 @@ public class EngineRuntimePublicationContractTest {
int publishEnd = source.indexOf("private void scheduleRuntimeTasks", publishStart);
String publish = source.substring(publishStart, publishEnd);
assertBefore(publish, "engine.getClosing().set(true)", "closeRuntime(next, e)");
assertBefore(publish, "closeBackgroundTaskAdmission()", "closeRuntime(next, e)");
assertBefore(publish, "sealAndAwait(", "closeRuntime(next, e)");
assertBefore(publish, "engine.getClosing().set(true)", "cleanupFailure = engine.shutdownSequence.closeRuntime(next, null)");
assertBefore(publish, "closeBackgroundTaskAdmission()", "cleanupFailure = engine.shutdownSequence.closeRuntime(next, null)");
assertBefore(publish, "sealAndAwait(", "cleanupFailure = engine.shutdownSequence.closeRuntime(next, null)");
}
@Test
@@ -0,0 +1,635 @@
package art.arcane.iris.engine;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.EngineRuntimeBuilder.RuntimeAssembly;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineAssignedComponent;
import art.arcane.iris.engine.framework.EngineEffects;
import art.arcane.iris.engine.framework.EnginePlatformHooks;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.EngineWorldManager;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionManager;
import art.arcane.iris.engine.framework.NativeStructureOwnershipStore;
import art.arcane.iris.engine.history.GenerationKernelRegistry;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.engine.history.GenerationAdmission;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.history.GenerationEpoch;
import art.arcane.iris.engine.history.GenerationEpochContractFactory;
import art.arcane.iris.engine.history.GenerationPackFingerprint;
import art.arcane.iris.engine.history.GenerationRegistryContract;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.engine.framework.EngineMode;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
import org.mockito.InOrder;
import org.mockito.MockedStatic;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public class EngineShutdownDrainTest {
private static IrisPlatform previousPlatform;
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@BeforeClass
public static void bindPlatform() {
previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
IrisPlatforms.unbind();
IrisPlatform platform = mock(IrisPlatform.class);
PlatformRegistries registries = mock(PlatformRegistries.class);
when(platform.registries()).thenReturn(registries);
when(registries.block(anyString())).thenReturn(mock(PlatformBlockState.class));
IrisPlatforms.bind(platform);
}
@AfterClass
public static void restorePlatform() {
IrisPlatforms.unbind();
if (previousPlatform != null) {
IrisPlatforms.bind(previousPlatform);
}
}
@Test
public void successfulEngineCloseAllowsTheSameWorldToPrepareGenerationAgain() throws Exception {
GenerationHistory history = history();
ShutdownFixture fixture = new ShutdownFixture();
fixture.attachHistory(history);
try (GenerationHistory.GenerationStage ignored = history.openStage(2, 3)) {
}
assertThrows(IllegalStateException.class, () -> history.prepareCurrentGenerator(32));
try (MockedStatic<NativeStructureOwnershipStore> ownership = mockStatic(NativeStructureOwnershipStore.class)) {
fixture.shutdown.close();
}
assertTrue(fixture.engine.closed);
GenerationHistory reopened = GenerationHistory.open(history.paths().dimensionRoot());
reopened.prepareCurrentGenerator(32);
try (GenerationHistory.GenerationStage ignored = reopened.openStage(3, 4)) {
}
}
@Test
public void failedEngineCloseKeepsAdmissionUntilSuccessfulRetry() throws Exception {
GenerationHistory history = history();
ShutdownFixture fixture = new ShutdownFixture();
fixture.attachHistory(history);
try (GenerationHistory.GenerationStage ignored = history.openStage(2, 3)) {
}
doThrow(new IllegalStateException("Planner still active")).when(fixture.complex).close();
try (MockedStatic<NativeStructureOwnershipStore> ownership = mockStatic(NativeStructureOwnershipStore.class)) {
assertThrows(IllegalStateException.class, fixture.shutdown::close);
GenerationHistory premature = GenerationHistory.open(history.paths().dimensionRoot());
assertThrows(IllegalStateException.class, () -> premature.prepareCurrentGenerator(32));
doNothing().when(fixture.complex).close();
fixture.shutdown.close();
}
GenerationHistory reopened = GenerationHistory.open(history.paths().dimensionRoot());
reopened.prepareCurrentGenerator(32);
}
@Test
public void startupOwnershipSpansReplacementEnginesUsingTheSameHistory() throws Exception {
GenerationHistory history = history();
ShutdownFixture first = new ShutdownFixture();
ShutdownFixture replacement = new ShutdownFixture();
try (GenerationAdmission.RuntimeLease startup = history.retainRuntime();
MockedStatic<NativeStructureOwnershipStore> ownership = mockStatic(NativeStructureOwnershipStore.class)) {
first.attachHistory(history);
first.shutdown.close();
history.prepareCurrentGenerator(32);
replacement.attachHistory(history);
}
try (GenerationHistory.GenerationStage ignored = history.openStage(2, 3)) {
}
try (MockedStatic<NativeStructureOwnershipStore> ownership = mockStatic(NativeStructureOwnershipStore.class)) {
replacement.shutdown.close();
}
GenerationHistory.open(history.paths().dimensionRoot()).prepareCurrentGenerator(32);
}
@Test
public void reopenedWorldPromotesPendingGenerationAfterTheOldEngineCloses() throws Exception {
GenerationHistory history = history();
ShutdownFixture fixture = new ShutdownFixture();
fixture.attachHistory(history);
try (GenerationHistory.GenerationStage ignored = history.openStage(2, 3)) {
}
Path replacementPack = temporaryFolder.newFolder("replacement-pack").toPath();
Files.createDirectories(replacementPack.resolve("dimensions"));
Files.writeString(replacementPack.resolve("dimensions/main.json"), "{\"name\":\"replacement\"}");
history.stageUpdate(replacementPack,
GenerationPackFingerprint.compute(replacementPack, GenerationPackFingerprint.CURRENT_VERSION),
history.activeEpoch().dimensionContract(), GenerationRegistryContract.empty(), 32);
assertThrows(IllegalStateException.class, () -> history.prepareCurrentGenerator(32));
try (MockedStatic<NativeStructureOwnershipStore> ownership = mockStatic(NativeStructureOwnershipStore.class)) {
fixture.shutdown.close();
}
GenerationHistory reopened = GenerationHistory.open(history.paths().dimensionRoot());
reopened.prepareCurrentGenerator(32);
assertEquals(2L, reopened.activeActivation().activationId());
assertFalse(reopened.pendingActivation().isPresent());
}
private GenerationHistory history() throws Exception {
Path world = temporaryFolder.newFolder("history-world").toPath();
Path pack = temporaryFolder.newFolder("history-pack").toPath();
Files.createDirectories(pack.resolve("dimensions"));
Files.writeString(pack.resolve("dimensions/main.json"), "{}");
GenerationEpoch.DimensionContract contract = new GenerationEpoch.DimensionContract(
"overworld", "iris:overworld_type", "NORMAL", "OVERWORLD", 127,
-64, 384, 384, 1D, false, "none", 0, "0".repeat(64),
GenerationEpochContractFactory.CURRENT_DIMENSION_TYPE_FINGERPRINT_SCHEMA, "c".repeat(64));
return GenerationHistory.create(world, pack,
GenerationPackFingerprint.compute(pack, GenerationPackFingerprint.CURRENT_VERSION),
42L, contract, GenerationRegistryContract.empty());
}
@Test
public void failedPlannerDrainRetainsRuntimeStorage() {
GenerationRuntime runtime = mock(GenerationRuntime.class);
IrisComplex complex = mock(IrisComplex.class);
EngineMantle mantle = mock(EngineMantle.class);
CompletableFuture<Long> hash = new CompletableFuture<>();
when(runtime.mode()).thenReturn(mock(EngineMode.class));
when(runtime.complex()).thenReturn(complex);
when(runtime.mantle()).thenReturn(mantle);
when(runtime.hash32()).thenReturn(hash);
IllegalStateException failure = new IllegalStateException("Planner still active");
doThrow(failure).when(complex).close();
assertSame(failure, new EngineShutdownSequence(null).closeGenerationRuntime(runtime, null));
verifyNoInteractions(mantle);
assertFalse(hash.isCancelled());
}
@Test
public void successfulPlannerDrainPrecedesMantleRelease() {
GenerationRuntime runtime = mock(GenerationRuntime.class);
IrisComplex complex = mock(IrisComplex.class);
EngineMantle mantle = mock(EngineMantle.class);
CompletableFuture<Long> hash = new CompletableFuture<>();
when(runtime.mode()).thenReturn(mock(EngineMode.class));
when(runtime.complex()).thenReturn(complex);
when(runtime.mantle()).thenReturn(mantle);
when(runtime.hash32()).thenReturn(hash);
assertNull(new EngineShutdownSequence(null).closeGenerationRuntime(runtime, null));
InOrder releases = inOrder(complex, mantle);
releases.verify(complex).close();
releases.verify(mantle).saveAllNow();
releases.verify(mantle).close();
assertTrue(hash.isCancelled());
}
@Test
public void failedAssemblyDrainRetainsMantleOwnership() {
RuntimeAssembly assembly = mock(RuntimeAssembly.class);
assembly.complex = mock(IrisComplex.class);
assembly.mantle = mock(EngineMantle.class);
assembly.ownsMantle = true;
assembly.hash32 = new CompletableFuture<>();
IllegalStateException failure = new IllegalStateException("Planner still active");
doThrow(failure).when(assembly.complex).close();
assertSame(failure, new EngineShutdownSequence(null).closeAssembly(assembly, null));
verifyNoInteractions(assembly.mantle);
assertTrue(assembly.ownsMantle);
assertFalse(assembly.hash32.isCancelled());
}
@Test
public void failedAssemblySaveKeepsOwnershipAndDoesNotCloseStorage() {
RuntimeAssembly assembly = mock(RuntimeAssembly.class);
assembly.mantle = mock(EngineMantle.class);
assembly.ownsMantle = true;
IllegalStateException failure = new IllegalStateException("Save failed");
doThrow(failure).doNothing().when(assembly.mantle).saveAllNow();
EngineShutdownSequence shutdown = new EngineShutdownSequence(null);
assertSame(failure, shutdown.closeAssembly(assembly, null));
assertTrue(assembly.ownsMantle);
verify(assembly.mantle, never()).close();
assertNull(shutdown.closeAssembly(assembly, null));
assertFalse(assembly.ownsMantle);
assertNull(shutdown.closeAssembly(assembly, null));
verify(assembly.mantle, times(2)).saveAllNow();
verify(assembly.mantle).close();
}
@Test
public void failedAssemblyCloseKeepsOwnershipUntilRetrySucceeds() {
RuntimeAssembly assembly = mock(RuntimeAssembly.class);
assembly.mantle = mock(EngineMantle.class);
assembly.ownsMantle = true;
IllegalStateException failure = new IllegalStateException("Close failed");
doThrow(failure).doNothing().when(assembly.mantle).close();
EngineShutdownSequence shutdown = new EngineShutdownSequence(null);
assertSame(failure, shutdown.closeAssembly(assembly, null));
assertTrue(assembly.ownsMantle);
assertNull(shutdown.closeAssembly(assembly, null));
assertFalse(assembly.ownsMantle);
verify(assembly.mantle, times(2)).saveAllNow();
verify(assembly.mantle, times(2)).close();
}
@Test
public void failedPartialAssemblyRetainsDataAndRetriesInsideItsAssemblyScope() throws Exception {
ShutdownFixture fixture = new ShutdownFixture();
RuntimeAssembly assembly = mock(RuntimeAssembly.class);
EngineTarget detachedTarget = target();
setField(assembly, "target", detachedTarget);
assembly.complex = mock(IrisComplex.class);
assembly.mantle = mock(EngineMantle.class);
assembly.ownsMantle = true;
RuntimeAssembly previous = mock(RuntimeAssembly.class);
fixture.engine.runtimeAssembly.set(previous);
IllegalStateException failure = new IllegalStateException("Planner still active");
doAnswer(invocation -> {
assertSame(assembly, fixture.engine.runtimeAssembly.get());
throw failure;
}).when(assembly.complex).close();
assertSame(failure, fixture.shutdown.closeAssembly(assembly, null));
assertSame(previous, fixture.engine.runtimeAssembly.get());
fixture.engine.runtimeAssembly.remove();
assertTrue(fixture.shutdown.retainsData(detachedTarget.getData()));
assertTrue(fixture.shutdown.closeDetachedTarget(detachedTarget, null) instanceof IllegalStateException);
verify(detachedTarget, never()).close();
try (MockedStatic<NativeStructureOwnershipStore> ignored = mockStatic(NativeStructureOwnershipStore.class)) {
assertThrows(IllegalStateException.class, fixture.shutdown::close);
verify(fixture.target, never()).close();
verifyNoInteractions(assembly.mantle);
doAnswer(invocation -> {
assertSame(assembly, fixture.engine.runtimeAssembly.get());
return null;
}).when(assembly.complex).close();
fixture.shutdown.close();
}
assertNull(fixture.engine.runtimeAssembly.get());
assertFalse(assembly.ownsMantle);
assertFalse(fixture.shutdown.retainsData(detachedTarget.getData()));
verify(assembly.mantle).close();
verify(detachedTarget).close();
verify(detachedTarget.getData()).close();
assertTrue(fixture.engine.closed);
assertNull(fixture.engine.runtime);
}
@Test
public void failedManagerStartKeepsPublishedRuntimeWhenCleanupFails() throws Exception {
ShutdownFixture fixture = new ShutdownFixture();
fixture.engine.runtime = null;
IllegalStateException startFailure = new IllegalStateException("Manager start failed");
IllegalStateException closeFailure = new IllegalStateException("Planner still active");
doThrow(startFailure).when(fixture.manager).start();
doThrow(closeFailure).when(fixture.complex).close();
IllegalStateException failure = assertThrows(IllegalStateException.class,
() -> new EngineRuntimeBuilder(fixture.engine).publishRuntime(fixture.runtime, null));
assertSame(startFailure, failure.getCause());
assertSame(closeFailure, startFailure.getSuppressed()[0]);
assertSame(fixture.runtime, fixture.engine.runtime);
assertTrue(fixture.shutdown.retainsData(fixture.target.getData()));
verify(fixture.mantle, never()).close();
doNothing().when(fixture.complex).close();
try (MockedStatic<NativeStructureOwnershipStore> ignored = mockStatic(NativeStructureOwnershipStore.class)) {
fixture.shutdown.close();
}
assertNull(fixture.engine.runtime);
assertTrue(fixture.engine.closed);
verify(fixture.target).close();
}
@Test
public void failedStartDrainRetainsResourcesUntilGenerationStops() throws Exception {
ShutdownFixture fixture = new ShutdownFixture();
fixture.engine.runtime = null;
doThrow(new IllegalStateException("Manager start failed")).when(fixture.manager).start();
doThrow(new GenerationSessionException("Generation still active", false)).when(fixture.sessions)
.sealAndAwait("failed world manager start", IrisEngine.SESSION_DRAIN_TIMEOUT_MILLIS, true);
assertThrows(IllegalStateException.class,
() -> new EngineRuntimeBuilder(fixture.engine).publishRuntime(fixture.runtime, null));
assertSame(fixture.runtime, fixture.engine.runtime);
verify(fixture.manager, never()).close();
verifyNoInteractions(fixture.complex, fixture.mantle);
try (MockedStatic<NativeStructureOwnershipStore> ignored = mockStatic(NativeStructureOwnershipStore.class)) {
fixture.shutdown.close();
}
assertNull(fixture.engine.runtime);
assertTrue(fixture.engine.closed);
}
@Test
public void failedRetirementRetainsUnpublishedRuntimeWithoutClosingSharedMantle() throws Exception {
ShutdownFixture fixture = new ShutdownFixture();
EngineTarget nextTarget = target();
GenerationRuntime nextGeneration = generation(nextTarget, fixture.mantle);
EngineRuntime next = new EngineRuntime(nextGeneration,
new ChangingEffects(fixture.engine), mock(EngineWorldManager.class));
EngineMode nextMode = nextGeneration.mode();
doThrow(new IllegalStateException("Previous planner active")).when(fixture.complex).close();
doThrow(new IllegalStateException("Next mode active")).when(nextMode).close();
assertThrows(IllegalStateException.class,
() -> new EngineRuntimeBuilder(fixture.engine).publishRuntime(next, fixture.runtime));
assertSame(fixture.runtime, fixture.engine.runtime);
assertTrue(fixture.shutdown.retainsData(nextTarget.getData()));
verifyNoInteractions(fixture.mantle);
doNothing().when(fixture.complex).close();
doNothing().when(nextMode).close();
try (MockedStatic<NativeStructureOwnershipStore> ignored = mockStatic(NativeStructureOwnershipStore.class)) {
fixture.shutdown.close();
}
assertFalse(fixture.shutdown.retainsData(nextTarget.getData()));
verify(nextTarget).close();
verify(fixture.target).close();
verify(fixture.mantle).saveAllNow();
verify(fixture.mantle).close();
assertTrue(fixture.engine.closed);
}
@Test
public void failedConstructionDrainPreventsAllRuntimeAndTargetRelease() throws Exception {
ShutdownFixture fixture = new ShutdownFixture();
doThrow(new GenerationSessionException("Generation still active", false)).doNothing().when(fixture.sessions)
.sealAndAwait("failed initialization", 0L, true);
IllegalStateException original = new IllegalStateException("Construction failed");
try (MockedStatic<NativeStructureOwnershipStore> ignored = mockStatic(NativeStructureOwnershipStore.class)) {
fixture.shutdown.cleanupFailedConstruction(original);
assertSame(fixture.runtime, fixture.engine.runtime);
assertFalse(fixture.engine.closed);
verifyNoInteractions(fixture.complex, fixture.mantle);
verify(fixture.target, never()).close();
fixture.shutdown.cleanupFailedConstruction(original);
}
assertNull(fixture.engine.runtime);
assertTrue(fixture.engine.closed);
verify(fixture.mantle).close();
verify(fixture.target).close();
}
@Test
public void failedComplexRetirementRetainsReplacementBeforeSharedStorageCanClose() throws Exception {
ShutdownFixture fixture = new ShutdownFixture();
IrisComplex replacement = mock(IrisComplex.class);
fixture.prepareComplexHotload(replacement);
doThrow(new IllegalStateException("Previous planner still active")).when(fixture.complex).close();
doThrow(new IllegalStateException("Replacement planner still active")).when(replacement).close();
assertThrows(IllegalStateException.class, () -> new EngineHotloader(fixture.engine).hotloadComplex());
assertSame(fixture.runtime, fixture.engine.runtime);
assertSame(IrisEngine.LifecycleState.FAILED, fixture.engine.lifecycleState);
doNothing().when(fixture.complex).close();
try (MockedStatic<NativeStructureOwnershipStore> ignored = mockStatic(NativeStructureOwnershipStore.class)) {
assertThrows(IllegalStateException.class, fixture.shutdown::close);
verify(replacement, times(2)).close();
verify(fixture.mantle, never()).close();
verify(fixture.target, never()).close();
doNothing().when(replacement).close();
fixture.shutdown.close();
}
assertTrue(fixture.engine.closed);
verify(replacement, times(3)).close();
verify(fixture.mantle).close();
}
@Test
public void failedComplexBuildDrainBlocksRollbackUntilShutdownRetry() throws Exception {
ShutdownFixture fixture = new ShutdownFixture();
IrisComplex replacement = mock(IrisComplex.class);
GenerationKernelRegistry.RuntimeKernel kernel = fixture.prepareComplexHotload(replacement);
IllegalStateException buildFailure = new IllegalStateException("Dimension stack failed");
when(kernel.createDimensionStackContext(fixture.engine)).thenThrow(buildFailure);
IllegalStateException drainFailure = new IllegalStateException("Replacement planner still active");
doThrow(drainFailure).when(replacement).close();
IllegalStateException failure = assertThrows(IllegalStateException.class,
() -> new EngineHotloader(fixture.engine).hotloadComplex());
assertSame(buildFailure, failure.getCause());
assertSame(drainFailure, buildFailure.getSuppressed()[0]);
assertSame(IrisEngine.LifecycleState.FAILED, fixture.engine.lifecycleState);
assertSame(fixture.runtime, fixture.engine.runtime);
verify(fixture.sessions, never()).activateNextSession();
verify(fixture.complex, never()).close();
doNothing().when(replacement).close();
try (MockedStatic<NativeStructureOwnershipStore> ignored = mockStatic(NativeStructureOwnershipStore.class)) {
fixture.shutdown.close();
}
assertTrue(fixture.engine.closed);
verify(replacement, times(2)).close();
verify(fixture.mantle).close();
}
@Test
public void successfullyDrainedFailedComplexBuildRestoresPreviousRuntime() throws Exception {
ShutdownFixture fixture = new ShutdownFixture();
IrisComplex replacement = mock(IrisComplex.class);
GenerationKernelRegistry.RuntimeKernel kernel = fixture.prepareComplexHotload(replacement);
when(kernel.createDimensionStackContext(fixture.engine))
.thenThrow(new IllegalStateException("Dimension stack failed"));
assertThrows(IllegalStateException.class, () -> new EngineHotloader(fixture.engine).hotloadComplex());
assertSame(fixture.runtime, fixture.engine.runtime);
assertSame(IrisEngine.LifecycleState.RUNNING, fixture.engine.lifecycleState);
verify(replacement).close();
verify(fixture.complex, never()).close();
verify(fixture.mantle, never()).close();
verify(fixture.sessions).activateNextSession();
}
@Test
public void successfulComplexHotloadPublishesReplacementWithoutClosingIt() throws Exception {
ShutdownFixture fixture = new ShutdownFixture();
IrisComplex replacement = mock(IrisComplex.class);
fixture.prepareComplexHotload(replacement);
new EngineHotloader(fixture.engine).hotloadComplex();
assertSame(replacement, fixture.engine.runtime.generation().complex());
assertSame(IrisEngine.LifecycleState.RUNNING, fixture.engine.lifecycleState);
verify(fixture.complex).close();
verify(replacement, never()).close();
verify(fixture.mantle, never()).close();
}
@Test
public void failedComplexActivationKeepsPublishedReplacementOwnedForShutdown() throws Exception {
ShutdownFixture fixture = new ShutdownFixture();
IrisComplex replacement = mock(IrisComplex.class);
fixture.prepareComplexHotload(replacement);
doThrow(new IllegalStateException("Session activation failed")).when(fixture.sessions).activateNextSession();
assertThrows(IllegalStateException.class, () -> new EngineHotloader(fixture.engine).hotloadComplex());
assertSame(replacement, fixture.engine.runtime.generation().complex());
assertSame(IrisEngine.LifecycleState.FAILED, fixture.engine.lifecycleState);
assertTrue(fixture.engine.getClosing().get());
verify(fixture.complex).close();
verify(replacement, never()).close();
try (MockedStatic<NativeStructureOwnershipStore> ignored = mockStatic(NativeStructureOwnershipStore.class)) {
fixture.shutdown.close();
}
assertTrue(fixture.engine.closed);
verify(replacement).close();
}
private static EngineTarget target() {
EngineTarget target = mock(EngineTarget.class);
when(target.getData()).thenReturn(mock(IrisData.class));
when(target.getWorld()).thenReturn(mock(IrisWorld.class));
return target;
}
private static GenerationRuntime generation(EngineTarget target, EngineMantle mantle) {
GenerationRuntime generation = mock(GenerationRuntime.class);
IrisData data = target.getData();
when(generation.target()).thenReturn(target);
when(generation.data()).thenReturn(data);
when(generation.mantle()).thenReturn(mantle);
when(generation.complex()).thenReturn(mock(IrisComplex.class));
when(generation.mode()).thenReturn(mock(EngineMode.class));
when(generation.hash32()).thenReturn(new CompletableFuture<>());
return generation;
}
private static void setField(Object target, String name, Object value) throws Exception {
Field field = target.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(target, value);
}
private static final class ChangingEffects extends EngineAssignedComponent implements EngineEffects {
private int hash = 1;
private ChangingEffects(Engine engine) {
super(engine, "changing effects");
}
@Override
public void updatePlayerMap() {
}
@Override
public void tickRandomPlayer() {
}
@Override
public void close() {
hash++;
}
@Override
public int hashCode() {
return hash;
}
}
private static final class ShutdownFixture {
private final IrisEngine engine = mock(IrisEngine.class);
private final EngineTarget target = target();
private final EngineMantle mantle = mock(EngineMantle.class);
private final GenerationRuntime generation = generation(target, mantle);
private final IrisComplex complex = generation.complex();
private final EngineWorldManager manager = mock(EngineWorldManager.class);
private final EngineRuntime runtime = new EngineRuntime(generation, mock(EngineEffects.class), manager);
private final GenerationSessionManager sessions = mock(GenerationSessionManager.class);
private final EngineShutdownSequence shutdown = new EngineShutdownSequence(engine);
private GenerationKernelRegistry.RuntimeKernel prepareComplexHotload(IrisComplex replacement) throws Exception {
GenerationKernelRegistry.RuntimeKernel kernel = mock(GenerationKernelRegistry.RuntimeKernel.class);
when(kernel.createComplex(engine, null)).thenReturn(replacement);
when(generation.runtimeKernel()).thenReturn(kernel);
GenerationRuntime nextGeneration = generation(target, mantle);
when(nextGeneration.complex()).thenReturn(replacement);
when(generation.withComplex(anyInt(), same(replacement), isNull(), isNull(), any()))
.thenReturn(nextGeneration);
EngineRuntimeBuilder builder = spy(new EngineRuntimeBuilder(engine));
doReturn(new GenerationRuntime.BiomeMaxes(0D, 0D, 0D)).when(builder).computeBiomeMaxes();
setField(engine, "runtimeBuilder", builder);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.empty());
return kernel;
}
private void attachHistory(GenerationHistory history) throws Exception {
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
when(router.engine()).thenReturn(engine);
when(router.history()).thenReturn(history);
setField(engine, "generationHistoryRuntimeRouterLock", new Object());
doCallRealMethod().when(engine).attachGenerationHistoryRuntimeRouter(router);
engine.attachGenerationHistoryRuntimeRouter(router);
}
private ShutdownFixture() throws Exception {
EngineBackgroundTasks background = mock(EngineBackgroundTasks.class);
when(background.drainBackgroundTasks(anyString()))
.thenReturn(new EngineBackgroundTasks.BackgroundTaskDrain(null, true));
setField(engine, "backgroundTasks", background);
setField(engine, "shutdownSequence", shutdown);
setField(engine, "engineDataStore", mock(EngineDataStore.class));
setField(engine, "lifecycleLock", new Object());
setField(engine, "runtimeAssembly", new ThreadLocal<RuntimeAssembly>());
setField(engine, "detachedGenerationRuntimes", ConcurrentHashMap.newKeySet());
when(engine.getClosing()).thenReturn(new AtomicBoolean());
when(engine.getGenerationSessions()).thenReturn(sessions);
when(engine.getPlatformHooks()).thenReturn(mock(EnginePlatformHooks.class));
when(engine.getMantle()).thenReturn(mantle);
IrisWorld world = target.getWorld();
when(engine.getWorld()).thenReturn(world);
when(engine.beginShutdown()).thenReturn(true);
engine.runtime = runtime;
engine.publishedTarget = target;
}
}
}
@@ -43,14 +43,14 @@ public class EngineShutdownOwnershipContractTest {
String cleanup = source.substring(cleanupStart, cleanupEnd);
int ownershipClose = cleanup.indexOf("NativeStructureOwnershipStore.close(engine)");
int ownershipGate = cleanup.indexOf("if (ownershipFailure == null)", ownershipClose);
int detachedRelease = cleanup.indexOf("closeDetachedGenerationRuntimes(cleanupFailure)", ownershipGate);
int runtimeRelease = cleanup.indexOf("closeRuntime(engine.runtime, cleanupFailure)", ownershipGate);
int runtimeRelease = cleanup.indexOf("releaseRuntime(cleanupFailure)", ownershipGate);
int targetRelease = cleanup.indexOf("releaseTarget(cleanupFailure)", runtimeRelease);
int closedPublication = cleanup.indexOf("engine.closed = true", ownershipGate);
assertTrue(ownershipClose >= 0);
assertTrue(ownershipGate > ownershipClose);
assertTrue(detachedRelease > ownershipGate);
assertTrue(runtimeRelease > detachedRelease);
assertTrue(runtimeRelease > ownershipGate);
assertTrue(targetRelease > runtimeRelease);
assertTrue(closedPublication > ownershipGate);
}
@@ -13,8 +13,12 @@ import java.util.List;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
public class IrisComplexImplodeParityTest {
private static Method childSelectionCreateMethod;
@@ -48,6 +52,27 @@ public class IrisComplexImplodeParityTest {
}
}
@Test
public void extremeCountsKeepTheRareBiomeReachableAcrossTheIntIndexBoundary() {
IrisBiome rare = createBiome(Integer.MAX_VALUE);
IrisBiome common = createBiome(1);
CNG generator = mock(CNG.class);
doReturn(1_073_741_824).when(generator).fit2D(0, Integer.MAX_VALUE, 12D, -32D);
IrisComplex.ChildSelectionPlan pair = IrisComplex.ChildSelectionPlan.create(new KList<>(rare, common));
assertSame(rare, pair.select(generator, 12D, -32D));
verify(generator).fit2D(0, Integer.MAX_VALUE, 12D, -32D);
verify(generator, never()).noiseFast2D(anyDouble(), anyDouble());
CNG largeGenerator = mock(CNG.class);
doReturn(0.5D).when(largeGenerator).noiseFast2D(12D, -32D);
IrisComplex.ChildSelectionPlan large = IrisComplex.ChildSelectionPlan.create(
new KList<>(rare, common, createBiome(2)));
assertSame(rare, large.select(largeGenerator, 12D, -32D));
verify(largeGenerator).noiseFast2D(12D, -32D);
verify(largeGenerator, never()).fit2D(anyInt(), anyInt(), anyDouble(), anyDouble());
}
@Test
public void emptySelectionPlanMatchesLegacyEmptyBehavior() throws Exception {
KList<IrisBiome> options = new KList<>();
@@ -18,6 +18,8 @@ import art.arcane.iris.engine.framework.SeedManager;
import art.arcane.iris.engine.framework.IrisEngineMode;
import art.arcane.iris.engine.framework.NativeStructureVolumeMemo;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.engine.history.GenerationAdmission;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisBiome;
@@ -515,9 +517,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
public void attachedHistoryRouterIsClearedWhenEngineShutdownBegins() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
when(router.biomes()).thenReturn(mock(SavedBiomeRuntime.class));
when(router.engine()).thenReturn(engine);
GenerationHistoryRuntimeRouter router = historyRouter(engine);
engine.attachGenerationHistoryRuntimeRouter(router);
assertTrue(engine.beginShutdown());
@@ -531,9 +531,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
public void attachedHistoryRejectsGenerationRuntimeHotload() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
when(router.biomes()).thenReturn(mock(SavedBiomeRuntime.class));
when(router.engine()).thenReturn(engine);
GenerationHistoryRuntimeRouter router = historyRouter(engine);
engine.attachGenerationHistoryRuntimeRouter(router);
IllegalStateException failure = assertThrows(IllegalStateException.class, engine::hotloadComplex);
@@ -548,9 +546,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
assertNull(engine.openGenerationHistoryCoordinateScope(0, 0));
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
when(router.biomes()).thenReturn(mock(SavedBiomeRuntime.class));
when(router.engine()).thenReturn(engine);
GenerationHistoryRuntimeRouter router = historyRouter(engine);
engine.attachGenerationHistoryRuntimeRouter(router);
engine.detachGenerationHistoryRuntimeRouter(router);
@@ -710,6 +706,16 @@ public class IrisEngineGenerationRuntimeScopeTest {
assertSame(active.dimension, engine.getDimension());
}
private static GenerationHistoryRuntimeRouter historyRouter(IrisEngine engine) {
GenerationHistory history = mock(GenerationHistory.class);
when(history.retainRuntime()).thenReturn(mock(GenerationAdmission.RuntimeLease.class));
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
when(router.biomes()).thenReturn(mock(SavedBiomeRuntime.class));
when(router.engine()).thenReturn(engine);
when(router.history()).thenReturn(history);
return router;
}
private static IrisEngine engine(
GenerationRuntime generationRuntime,
EngineEffects effects,
@@ -0,0 +1,127 @@
package art.arcane.iris.engine;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.volmlib.util.scheduling.Looper;
import org.junit.Test;
import java.lang.reflect.Field;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
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 IrisWorldManagerShutdownTest {
@Test
public void liveLooperPreventsSuccessfulCloseAndRemainsRetryable() throws Exception {
IrisWorldManager manager = manager();
HeldLooper looper = new HeldLooper();
setField(manager, "looper", looper);
looper.start();
try {
assertTrue(looper.started.await(5L, TimeUnit.SECONDS));
assertThrows(IllegalStateException.class, manager::close);
assertFalse(manager.isLooperStopped());
assertTrue(looper.isAlive());
assertTrue(manager.isCleanupServiceStopped());
} finally {
looper.release.countDown();
looper.join(5_000L);
manager.close();
}
assertFalse(looper.isAlive());
assertTrue(manager.isLooperStopped());
manager.close();
}
@Test
public void cleanupTimeoutRemainsRetryableAndSuccessfulCloseIsIdempotent() throws Exception {
IrisWorldManager manager = manager();
ScheduledThreadPoolExecutor executor = mock(ScheduledThreadPoolExecutor.class);
manager.getCleanupService().shutdownNow();
setField(manager, "cleanupService", executor);
when(executor.awaitTermination(anyLong(), eq(TimeUnit.MILLISECONDS))).thenReturn(false, true);
assertThrows(IllegalStateException.class, manager::close);
assertFalse(manager.isCleanupServiceStopped());
assertTrue(manager.isLooperStopped());
manager.close();
assertTrue(manager.isCleanupServiceStopped());
manager.close();
verify(executor, times(2)).shutdownNow();
verify(executor, times(2)).awaitTermination(anyLong(), eq(TimeUnit.MILLISECONDS));
}
@Test
public void interruptedCleanupWaitRestoresInterruptAndCanBeRetried() throws Exception {
IrisWorldManager manager = manager();
ScheduledThreadPoolExecutor executor = mock(ScheduledThreadPoolExecutor.class);
manager.getCleanupService().shutdownNow();
setField(manager, "cleanupService", executor);
when(executor.awaitTermination(anyLong(), eq(TimeUnit.MILLISECONDS)))
.thenThrow(new InterruptedException("Close interrupted"))
.thenReturn(true);
try {
assertThrows(IllegalStateException.class, manager::close);
assertTrue(Thread.currentThread().isInterrupted());
assertFalse(manager.isCleanupServiceStopped());
} finally {
Thread.interrupted();
manager.close();
}
assertTrue(manager.isCleanupServiceStopped());
verify(executor, times(2)).awaitTermination(anyLong(), eq(TimeUnit.MILLISECONDS));
}
private static IrisWorldManager manager() {
Engine engine = mock(Engine.class);
EngineTarget target = mock(EngineTarget.class);
IrisWorld world = mock(IrisWorld.class);
when(world.name()).thenReturn("shutdown-test");
when(target.getWorld()).thenReturn(world);
when(engine.getTarget()).thenReturn(target);
return new IrisWorldManager(engine);
}
private static void setField(IrisWorldManager manager, String name, Object value) throws Exception {
Field field = IrisWorldManager.class.getDeclaredField(name);
field.setAccessible(true);
field.set(manager, value);
}
private static final class HeldLooper extends Looper {
private final CountDownLatch started = new CountDownLatch(1);
private final CountDownLatch release = new CountDownLatch(1);
@Override
protected void onStart() {
}
@Override
protected void onStop() {
}
@Override
protected long loop() {
started.countDown();
while (true) {
try {
release.await();
return -1L;
} catch (InterruptedException ignored) {
}
}
}
}
}
@@ -0,0 +1,147 @@
package art.arcane.iris.engine.decorator;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.hunk.Hunk;
import org.junit.Test;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisSeaDecoratorBoundsTest {
@Test
public void floorPercentageCannotGrowAboveTheWaterColumn() {
Fixture fixture = new Fixture(12, false);
fixture.stacking(200, 30);
fixture.decorate(4, 8);
assertSame(fixture.body, fixture.output.get(0, 4, 0));
assertSame(fixture.top, fixture.output.get(0, 7, 0));
assertNull(fixture.output.get(0, 8, 0));
}
@Test
public void floorPercentageRespectsAbsoluteMaximum() {
Fixture fixture = new Fixture(64, false);
fixture.stacking(100, 3);
fixture.decorate(4, 60);
assertSame(fixture.body, fixture.output.get(0, 4, 0));
assertSame(fixture.top, fixture.output.get(0, 6, 0));
assertNull(fixture.output.get(0, 7, 0));
}
@Test
public void surfacePercentageRespectsAbsoluteMaximum() {
Fixture fixture = new Fixture(64, true);
fixture.stacking(100, 3);
fixture.decorate(4, 64);
assertSame(fixture.body, fixture.output.get(0, 5, 0));
assertSame(fixture.top, fixture.output.get(0, 7, 0));
assertNull(fixture.output.get(0, 8, 0));
}
@Test
public void floorStackAtOutputCeilingUsesOneTopBlock() {
Fixture fixture = new Fixture(8, false);
fixture.stacking(200, 30);
fixture.decorate(7, 20);
assertSame(fixture.top, fixture.output.get(0, 7, 0));
}
@Test
public void unresolvedFloorPalettePreservesExistingWater() {
Fixture fixture = new Fixture(8, false);
fixture.stacking(100, 30);
PlatformBlockState water = mock(PlatformBlockState.class);
fixture.output.set(0, 3, 0, water);
when(fixture.decorator.getBlockData100(any(), any(), anyDouble(), anyDouble(), anyDouble(), any())).thenReturn(null);
fixture.decorate(3, 8);
assertSame(water, fixture.output.get(0, 3, 0));
assertNull(fixture.output.get(0, 4, 0));
}
@Test
public void surfaceStackUsesTheShorterOutputHeight() {
Fixture fixture = new Fixture(8, true);
when(fixture.engine.getHeight()).thenReturn(64);
fixture.stacking(200, 30);
fixture.decorate(6, 64);
assertSame(fixture.top, fixture.output.get(0, 7, 0));
}
@Test
public void surfaceSingleRejectsOutputCeiling() {
Fixture fixture = new Fixture(8, true);
when(fixture.engine.getHeight()).thenReturn(64);
fixture.decorate(7, 64);
assertNull(fixture.output.get(0, 7, 0));
}
@Test
public void surfaceSingleStackRejectsNegativeBaseAndEngineCeiling() {
Fixture fixture = new Fixture(16, true);
when(fixture.engine.getHeight()).thenReturn(8);
fixture.stacking(1, 30);
when(fixture.decorator.isScaleStack()).thenReturn(false);
fixture.decorate(-1, 16);
fixture.decorate(7, 16);
assertNull(fixture.output.get(0, 0, 0));
assertNull(fixture.output.get(0, 8, 0));
}
private static final class Fixture {
private final IrisDecorator decorator = mock(IrisDecorator.class);
private final IrisBiome biome = mock(IrisBiome.class);
private final PlatformBlockState body = mock(PlatformBlockState.class);
private final PlatformBlockState top = mock(PlatformBlockState.class);
private final Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
private final Hunk<PlatformBlockState> output;
private final IrisEngineDecorator placement;
private Fixture(int height, boolean surface) {
when(engine.getHeight()).thenReturn(height);
output = Hunk.newArrayHunk(1, height, 1);
IrisDecorationPart part = surface ? IrisDecorationPart.SEA_SURFACE : IrisDecorationPart.SEA_FLOOR;
when(biome.getDecoratorBucket(part)).thenReturn(new IrisDecorator[]{decorator});
when(decorator.passesChanceGate(any(), anyDouble(), anyDouble(), any())).thenReturn(true);
when(decorator.getBlockData100(any(), any(), anyDouble(), anyDouble(), anyDouble(), any())).thenReturn(body);
when(decorator.getBlockDataForTop(any(), any(), anyDouble(), anyDouble(), anyDouble(), any())).thenReturn(top);
when(decorator.getTopThreshold()).thenReturn(1D);
placement = surface ? new IrisSeaSurfaceDecorator(engine) : new IrisSeaFloorDecorator(engine);
}
private void stacking(int percentage, int cap) {
when(decorator.isStacking()).thenReturn(true);
when(decorator.isScaleStack()).thenReturn(true);
when(decorator.getHeight(any(), anyDouble(), anyDouble(), any())).thenReturn(percentage);
when(decorator.getAbsoluteMaxStack()).thenReturn(cap);
}
private void decorate(int height, int maximum) {
placement.decorate(0, 0, 0, 0, 0, 0, 0, 0, output, biome, height, maximum);
}
}
}
@@ -0,0 +1,57 @@
package art.arcane.iris.engine.framework;
import org.junit.Test;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
public class IrisEngineModeLifecycleTest {
@Test
public void failedContentStageRemainsRetryable() {
IrisEngineMode mode = new IrisEngineMode(mock(Engine.class)) {
};
EngineStage content = mock(EngineStage.class);
EngineStage terrain = mock(EngineStage.class);
mode.registerStage(content);
mode.registerTerrainStage(terrain);
IllegalStateException failure = new IllegalStateException("Stage close failed");
doThrow(failure).doNothing().when(content).close();
assertSame(failure, assertThrows(IllegalStateException.class, mode::close));
verifyNoInteractions(terrain);
mode.close();
mode.close();
assertTrue(mode.getStages().isEmpty());
assertTrue(mode.getTerrainStages().isEmpty());
verify(content, times(2)).close();
verify(terrain).close();
}
@Test
public void failedTerrainStageDoesNotRepeatReleasedContent() {
IrisEngineMode mode = new IrisEngineMode(mock(Engine.class)) {
};
EngineStage content = mock(EngineStage.class);
EngineStage terrain = mock(EngineStage.class);
mode.registerStage(content);
mode.registerTerrainStage(terrain);
IllegalStateException failure = new IllegalStateException("Terrain stage close failed");
doThrow(failure).doNothing().when(terrain).close();
assertSame(failure, assertThrows(IllegalStateException.class, mode::close));
assertTrue(mode.getStages().isEmpty());
mode.close();
mode.close();
assertTrue(mode.getTerrainStages().isEmpty());
verify(content).close();
verify(terrain, times(2)).close();
}
}
@@ -4,6 +4,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.FutureTask;
@@ -103,6 +104,99 @@ public class GenerationAdmissionTest {
assertThrows(IllegalStateException.class, admission::beginStartupCutover);
}
@Test
public void lastRuntimeReleaseAllowsStartupOnAFreshAdmissionOnly() {
Path root = temporaryFolder.getRoot().toPath();
GenerationAdmission admission = new GenerationAdmission(root);
GenerationAdmission stale = new GenerationAdmission(root);
GenerationAdmission.RuntimeLease runtime = admission.retainRuntime();
try (GenerationAdmission.StageLease ignored = admission.enterStage()) {
}
runtime.close();
GenerationAdmission reopened = new GenerationAdmission(root);
try (GenerationAdmission.CutoverLease ignored = reopened.beginStartupCutover()) {
}
assertThrows(IllegalStateException.class, stale::enterStage);
assertThrows(IllegalStateException.class, stale::beginCutover);
assertThrows(IllegalStateException.class, stale::retainRuntime);
runtime.close();
try (GenerationAdmission.StageLease ignored = reopened.enterStage()) {
}
}
@Test
public void everyRuntimeMustReleaseBeforeAdmissionCanRetire() {
Path root = temporaryFolder.getRoot().toPath();
GenerationAdmission first = new GenerationAdmission(root);
GenerationAdmission second = new GenerationAdmission(root);
GenerationAdmission.RuntimeLease firstRuntime = first.retainRuntime();
GenerationAdmission.RuntimeLease secondRuntime = second.retainRuntime();
try (GenerationAdmission.StageLease ignored = first.enterStage()) {
}
firstRuntime.close();
assertThrows(IllegalStateException.class, new GenerationAdmission(root)::beginStartupCutover);
try (GenerationAdmission.StageLease ignored = second.enterStage()) {
}
secondRuntime.close();
try (GenerationAdmission.CutoverLease ignored = new GenerationAdmission(root).beginStartupCutover()) {
}
}
@Test
public void failedRetirementRetainsTheLeaseUntilActiveStagesFinish() {
GenerationAdmission admission = new GenerationAdmission(temporaryFolder.getRoot().toPath());
GenerationAdmission.RuntimeLease runtime = admission.retainRuntime();
GenerationAdmission.StageLease stage = admission.enterStage();
assertThrows(IllegalStateException.class, runtime::close);
assertThrows(IllegalStateException.class, admission::beginStartupCutover);
stage.close();
runtime.close();
assertThrows(IllegalStateException.class, admission::enterStage);
}
@Test
public void failedRetirementRetainsTheLeaseUntilActiveCutoversFinish() {
GenerationAdmission admission = new GenerationAdmission(temporaryFolder.getRoot().toPath());
GenerationAdmission.RuntimeLease runtime = admission.retainRuntime();
GenerationAdmission.CutoverLease cutover = admission.beginCutover();
assertThrows(IllegalStateException.class, runtime::close);
cutover.close();
runtime.close();
assertThrows(IllegalStateException.class, admission::beginCutover);
}
@Test
public void waitingCutoversKeepAdmissionOwnedUntilTheirWorkCompletes() throws Exception {
GenerationAdmission admission = new GenerationAdmission(temporaryFolder.getRoot().toPath());
GenerationAdmission.RuntimeLease runtime = admission.retainRuntime();
GenerationAdmission.StageLease stage = admission.enterStage();
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
FutureTask<Void> cutover = new FutureTask<>(() -> {
try (GenerationAdmission.CutoverLease ignored = admission.beginCutover()) {
entered.countDown();
await(release);
}
}, null);
Thread thread = Thread.ofPlatform().daemon().unstarted(cutover);
try {
thread.start();
awaitWaiting(thread);
assertThrows(IllegalStateException.class, runtime::close);
stage.close();
assertTrue(entered.await(5L, TimeUnit.SECONDS));
assertThrows(IllegalStateException.class, runtime::close);
release.countDown();
cutover.get(5L, TimeUnit.SECONDS);
runtime.close();
assertThrows(IllegalStateException.class, admission::beginCutover);
} finally {
stage.close();
release.countDown();
thread.join(5_000L);
}
}
private static void awaitWaiting(Thread thread) throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
while (thread.getState() != Thread.State.WAITING && System.nanoTime() < deadline) {
@@ -0,0 +1,126 @@
package art.arcane.iris.engine.hydrology;
import org.junit.Test;
import java.util.Arrays;
import java.util.Random;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
public class HydrologyRouteLengthsTest {
@Test
public void branchingRoutesMatchIndependentDownstreamWalks() {
Random random = new Random(912541L);
for (int width : new int[]{2, 3, 9, 17, 33, 65}) {
for (int spacing : new int[]{4, 16, 64}) {
for (int maximumLength : new int[]{63, 128, 257, 4096}) {
int[] parent = forest(width, random);
HydrologyPlannerSettings.Routing routing = routing(spacing, maximumLength);
assertArrayEquals(walks(parent, width, routing),
HydrologyPlanner.routeLengths(parent, width, routing));
reverse(parent);
assertArrayEquals(walks(parent, width, routing),
HydrologyPlanner.routeLengths(parent, width, routing));
}
}
}
}
@Test
public void exactLengthLimitAndMissingOutletsKeepTheirBounds() {
int[] parent = {1, 2, 3, 4, -1, -1, -1, -1, -1};
assertArrayEquals(new int[]{Integer.MAX_VALUE, 12, 8, 4, 0, 0, 0, 0, 0},
HydrologyPlanner.routeLengths(parent, 9, routing(4, 12)));
}
@Test
public void longRoutesDoNotUseTheCallStack() {
int width = 257;
int[] parent = new int[width * width];
for (int row = 0; row < width; row++) {
for (int column = 0; column < width; column++) {
int index = row * width + column;
int direction = row % 2 == 0 ? 1 : -1;
int nextColumn = column + direction;
parent[index] = nextColumn >= 0 && nextColumn < width ? index + direction : index + width;
}
}
parent[parent.length - 1] = -1;
int[] lengths = HydrologyPlanner.routeLengths(parent, width, routing(4, Integer.MAX_VALUE));
assertEquals((parent.length - 1) * 4, lengths[0]);
assertEquals(0, lengths[lengths.length - 1]);
reverse(parent);
lengths = HydrologyPlanner.routeLengths(parent, width, routing(4, Integer.MAX_VALUE));
assertEquals((parent.length - 1) * 4, lengths[lengths.length - 1]);
assertEquals(0, lengths[0]);
}
@Test
public void cyclesFailBeforeTheyCanStallPlanning() {
assertThrows(IllegalStateException.class,
() -> HydrologyPlanner.routeLengths(new int[]{1, 0}, 2, routing(4, 128)));
}
private static int[] forest(int width, Random random) {
int[] parent = new int[width * width];
Arrays.fill(parent, -1);
int[] candidates = new int[4];
for (int index = 1; index < parent.length; index++) {
int x = index % width;
int z = index / width;
int count = 0;
if (x > 0) {
candidates[count++] = index - 1;
}
if (z > 0) {
candidates[count++] = index - width;
if (x > 0) {
candidates[count++] = index - width - 1;
}
if (x + 1 < width) {
candidates[count++] = index - width + 1;
}
}
if (random.nextInt(16) != 0) {
parent[index] = candidates[random.nextInt(count)];
}
}
return parent;
}
private static void reverse(int[] parent) {
int[] original = parent.clone();
for (int index = 0; index < parent.length; index++) {
int next = original[parent.length - 1 - index];
parent[index] = next < 0 ? -1 : parent.length - 1 - next;
}
}
private static int[] walks(int[] parent, int width, HydrologyPlannerSettings.Routing routing) {
int[] lengths = new int[parent.length];
for (int source = 0; source < parent.length; source++) {
double distance = 0D;
int steps = 0;
int current = source;
while (parent[current] >= 0) {
int next = parent[current];
distance += StrictMath.hypot((current % width - next % width) * routing.sampleSpacing(),
(current / width - next / width) * routing.sampleSpacing());
if (distance > routing.maximumRouteLength() || ++steps > routing.maximumRouteNodes()) {
distance = Integer.MAX_VALUE;
break;
}
current = next;
}
lengths[source] = (int) StrictMath.ceil(distance);
}
return lengths;
}
private static HydrologyPlannerSettings.Routing routing(int spacing, int maximumLength) {
return new HydrologyPlannerSettings.Routing(spacing * 8, spacing, 1_000_000, maximumLength,
0, 0, 1D, 1D, 1D, 1D, 1D, 0);
}
}
@@ -15,6 +15,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ExecutionException;
@@ -23,6 +24,7 @@ import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
@@ -43,6 +45,104 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.when;
public class HydrologyTileCacheTest {
@Test
public void closeDrainsBackgroundPlanningAndRejectsNewDemand() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyTileKey key = new HydrologyTileKey(0, 0);
CountDownLatch started = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
when(planner.plan(key)).thenAnswer(invocation -> {
started.countDown();
assertTrue(release.await(5, TimeUnit.SECONDS));
return tile;
});
ExecutorService workers = Executors.newSingleThreadExecutor();
ExecutorService callers = Executors.newFixedThreadPool(2);
HydrologyTileCache cache = new HydrologyTileCache(planner, 4, workers);
try {
Future<HydrologyTile> result = callers.submit(() -> cache.get(key));
assertTrue(started.await(5, TimeUnit.SECONDS));
Future<?> closing = callers.submit(cache::close);
awaitClosed(cache);
assertThrows(TimeoutException.class, () -> closing.get(100, TimeUnit.MILLISECONDS));
assertThrows(CancellationException.class, () -> cache.get(new HydrologyTileKey(1, 0)));
assertThrows(CancellationException.class, () -> cache.columnAt(0, 0));
release.countDown();
assertSame(tile, result.get(5, TimeUnit.SECONDS));
closing.get(5, TimeUnit.SECONDS);
assertEquals(0, cache.size());
verify(planner, never()).plan(new HydrologyTileKey(1, 0));
} finally {
release.countDown();
workers.shutdownNow();
callers.shutdownNow();
assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
assertTrue(callers.awaitTermination(5, TimeUnit.SECONDS));
}
}
@Test
public void closeWaitsForColumnCompositionBeforeReturning() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
CountDownLatch started = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
when(tile.columnAt(anyInt(), anyInt())).thenAnswer(invocation -> {
if (invocation.<Integer>getArgument(0) == 0 && invocation.<Integer>getArgument(1) == 0) {
started.countDown();
assertTrue(release.await(5, TimeUnit.SECONDS));
}
return Optional.empty();
});
HydrologyTileCache cache = new HydrologyTileCache(planner, 4);
ExecutorService callers = Executors.newFixedThreadPool(2);
try {
Future<Optional<HydrologyColumnSample>> result = callers.submit(() -> cache.columnAt(0, 0));
assertTrue(started.await(5, TimeUnit.SECONDS));
Future<?> closing = callers.submit(cache::close);
awaitClosed(cache);
assertThrows(TimeoutException.class, () -> closing.get(100, TimeUnit.MILLISECONDS));
release.countDown();
assertTrue(result.get(5, TimeUnit.SECONDS).isEmpty());
closing.get(5, TimeUnit.SECONDS);
assertEquals(0, cache.size());
} finally {
release.countDown();
callers.shutdownNow();
assertTrue(callers.awaitTermination(5, TimeUnit.SECONDS));
}
}
@Test
public void closingFromAnOwnedLoadFailsWithoutDeadlockingOrClosingTheCache() {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyTileCache cache = new HydrologyTileCache(planner, 4);
HydrologyTileKey key = new HydrologyTileKey(0, 0);
when(planner.plan(key)).thenAnswer(invocation -> {
assertThrows(IllegalStateException.class, cache::close);
return tile;
});
assertSame(tile, cache.get(key));
assertSame(tile, cache.get(key));
cache.close();
assertThrows(CancellationException.class, () -> cache.get(key));
}
private static void awaitClosed(HydrologyTileCache cache) throws Exception {
Field field = HydrologyTileCache.class.getDeclaredField("closed");
field.setAccessible(true);
AtomicBoolean closed = (AtomicBoolean) field.get(cache);
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
while (!closed.get() && System.nanoTime() < deadline) {
Thread.sleep(1);
}
assertTrue(closed.get());
}
@Test
public void explicitAreaIsFullyQueuedBeforeExecutorCallbacksCanAddOldWork() throws Exception {
for (boolean pregeneration : new boolean[]{false, true}) {
@@ -265,7 +365,7 @@ public class HydrologyTileCacheTest {
}
@Test
public void planCompletingAfterCloseDoesNotEnterTheSharedStudioCache() throws Exception {
public void closeWaitsForActivePlansWithoutPublishingTheirSharedTiles() throws Exception {
HydrologyPlanner closingPlanner = mock(HydrologyPlanner.class);
HydrologyPlanner succeedingPlanner = mock(HydrologyPlanner.class);
HydrologyTile lateTile = mock(HydrologyTile.class);
@@ -287,13 +387,15 @@ public class HydrologyTileCacheTest {
when(lateTile.tileSize()).thenReturn(64);
HydrologyTileCache closing = new HydrologyTileCache(closingPlanner, 4, null, null, scope);
HydrologyTileCache succeeding = new HydrologyTileCache(succeedingPlanner, 4, null, null, scope);
ExecutorService executor = Executors.newSingleThreadExecutor();
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
Future<HydrologyTile> late = executor.submit(() -> closing.get(key));
assertTrue(planningStarted.await(10L, TimeUnit.SECONDS));
closing.close();
Future<?> shutdown = executor.submit(closing::close);
assertThrows(TimeoutException.class, () -> shutdown.get(100L, TimeUnit.MILLISECONDS));
allowCompletion.countDown();
assertSame(lateTile, late.get(10L, TimeUnit.SECONDS));
shutdown.get(10L, TimeUnit.SECONDS);
assertEquals(0, closing.size());
assertSame(plannedTile, succeeding.get(key));
verify(succeedingPlanner, times(1)).plan(key);
@@ -1451,7 +1553,14 @@ public class HydrologyTileCacheTest {
freshPrefetch.run();
}
oldDemand.run();
assertSame(tile, result.get(5, TimeUnit.SECONDS));
if (close) {
ExecutionException failure = assertThrows(ExecutionException.class,
() -> result.get(5, TimeUnit.SECONDS));
assertTrue(failure.getCause() instanceof CancellationException);
verify(planner, never()).plan(new HydrologyTileKey(1, 0));
} else {
assertSame(tile, result.get(5, TimeUnit.SECONDS));
}
assertTrue(queued.isEmpty());
assertEquals(close ? 0 : 1, cache.size());
verify(planner, never()).plan(new HydrologyTileKey(2, 0));
@@ -0,0 +1,71 @@
package art.arcane.iris.engine.hydrology;
import org.junit.Test;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class RiverFootprintTest {
@Test
public void primitiveIndexKeepsSignedKeyOrderAndFullCoordinateRange() {
Random random = new Random(836412L);
HashMap<Long, HydrologyColumnSample> input = new HashMap<>();
for (int index = 0; index < 2_000; index++) {
HydrologyColumnSample column = column(random.nextInt(), random.nextInt());
input.put(RiverFootprint.pack(column.x(), column.z()), column);
}
for (int x : new int[]{Integer.MIN_VALUE, -1, 0, 1, Integer.MAX_VALUE}) {
for (int z : new int[]{Integer.MIN_VALUE, -1, 0, 1, Integer.MAX_VALUE}) {
input.put(RiverFootprint.pack(x, z), column(x, z));
}
}
TreeMap<Long, HydrologyColumnSample> expected = new TreeMap<>(input);
RiverFootprint footprint = new RiverFootprint(input);
input.clear();
assertEquals(new ArrayList<>(expected.keySet()), new ArrayList<>(footprint.columns().keySet()));
assertEquals(expected, footprint.columns());
for (HydrologyColumnSample column : expected.values()) {
assertSame(column, footprint.sample(column.x(), column.z()).orElseThrow());
}
}
@Test
public void exposedEntriesCannotMutateTheLookupIndex() {
HydrologyColumnSample original = column(-5, 9);
RiverFootprint footprint = new RiverFootprint(Map.of(RiverFootprint.pack(-5, 9), original));
Map.Entry<Long, HydrologyColumnSample> entry = footprint.columns().entrySet().iterator().next();
assertThrows(UnsupportedOperationException.class, () -> entry.setValue(column(-5, 9)));
assertThrows(UnsupportedOperationException.class, () -> footprint.columns().values().clear());
assertSame(original, footprint.sample(-5, 9).orElseThrow());
}
@Test
public void areaQueriesKeepBothLookupPathsAndExclusiveBounds() {
HashMap<Long, HydrologyColumnSample> input = new HashMap<>();
for (int x = -8; x < 8; x++) {
for (int z = -8; z < 8; z++) {
input.put(RiverFootprint.pack(x, z), column(x, z));
}
}
RiverFootprint footprint = new RiverFootprint(input);
assertEquals(List.of(column(-1, -1), column(0, -1), column(-1, 0), column(0, 0)),
footprint.columnsIn(-1, -1, 1, 1));
assertEquals(new ArrayList<>(new TreeMap<>(input).values()),
footprint.columnsIn(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE));
assertTrue(footprint.columnsIn(0, 0, 0, 1).isEmpty());
assertTrue(footprint.columnsIn(100, 100, 101, 101).isEmpty());
}
private static HydrologyColumnSample column(int x, int z) {
return new HydrologyColumnSample(x, z, 90, 63, false, "parent", List.of());
}
}
@@ -21,9 +21,14 @@ import art.arcane.iris.util.common.parallel.MultiBurst;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.Future;
import java.util.concurrent.CompletionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
@@ -41,6 +46,9 @@ import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class MatterGeneratorConcurrencyTest {
@@ -215,6 +223,167 @@ public class MatterGeneratorConcurrencyTest {
assertFalse(fixture.mantle.getChunk(0, 0).isFlagged(MantleFlag.PLANNED));
}
@Test
public void dispatcherPropagatesAnAlreadyFailedSharedComponent() {
IllegalStateException failure = new IllegalStateException("Shared component failed");
ExecutionException observed = sharedComponentFailure(failure, ExecutionException.class);
assertSame(failure, observed.getCause());
}
@Test
public void dispatcherPropagatesAnAlreadyCancelledSharedComponent() {
sharedComponentFailure(new CancellationException("Shared component cancelled"), CancellationException.class);
}
@Test
public void timedOutRunningComponentRetainsWriterAndSharedClaimUntilItExits() throws Exception {
GeneratorFixture fixture = new GeneratorFixture(true);
CountDownLatch started = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
CountDownLatch writersAcquired = new CountDownLatch(2);
CountDownLatch cancelled = new CountDownLatch(1);
AtomicInteger runs = new AtomicInteger();
MantleChunk<Matter> chunk = fixture.mantle.getChunk(0, 0);
when(chunk.use()).thenAnswer(invocation -> {
writersAcquired.countDown();
return chunk;
});
RecordingComponent component = new RecordingComponent(ReservedFlag.OBJECT, 0, 0) {
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
runs.incrementAndGet();
started.countDown();
await(release);
verify(chunk, never()).release();
}
};
TestMatterGenerator generator = fixture.generator(List.of(new MantlePass(List.of(component), 0, 0)));
CompletableFuture<Void> first = CompletableFuture.runAsync(
() -> generator.generateMatter(0, 0, true, fixture.context));
await(started);
MatterGenerator.MatterTaskKey key = new MatterGenerator.MatterTaskKey(fixture.mantle, 0, 0, ReservedFlag.OBJECT);
MatterGenerator.MatterComponentTask task = MatterGenerator.IN_FLIGHT_COMPONENTS.get(key);
Future<?> submission = mock(Future.class);
when(submission.cancel(true)).thenAnswer(invocation -> {
cancelled.countDown();
return true;
});
task.setSubmission(submission);
CompletableFuture<Void> second = MultiBurst.burst.completeValueAsync(() -> {
generator.generateMatter(0, 0, true, fixture.context);
return null;
});
CompletableFuture<Void> timeout = CompletableFuture.runAsync(() -> task.await(0L));
try {
await(writersAcquired);
await(cancelled);
assertSame(task, MatterGenerator.IN_FLIGHT_COMPONENTS.get(key));
assertFalse(first.isDone());
assertFalse(second.isDone());
assertFalse(timeout.isDone());
verify(chunk, never()).release();
assertEquals(1, runs.get());
} finally {
release.countDown();
}
assertThrows(ExecutionException.class, () -> timeout.get(5L, TimeUnit.SECONDS));
assertThrows(ExecutionException.class, () -> first.get(5L, TimeUnit.SECONDS));
assertThrows(ExecutionException.class, () -> second.get(5L, TimeUnit.SECONDS));
verify(chunk, times(2)).release();
assertFalse(MatterGenerator.IN_FLIGHT_COMPONENTS.containsKey(key));
}
@Test
public void queuedTimeoutPreventsLateExecutionAndAllowsANewClaim() {
GeneratorFixture fixture = new GeneratorFixture();
MatterGenerator.MatterTaskKey key = new MatterGenerator.MatterTaskKey(fixture.mantle, 0, 0, ReservedFlag.OBJECT);
MatterGenerator.MatterComponentTask task = new MatterGenerator.MatterComponentTask(key);
MatterGenerator.IN_FLIGHT_COMPONENTS.put(key, task);
Future<?> submission = mock(Future.class);
task.setSubmission(submission);
CompletionException failure = assertThrows(CompletionException.class, () -> task.await(0L));
assertTrue(failure.getCause().getMessage().contains("did not complete"));
assertFalse(task.start());
assertFalse(MatterGenerator.IN_FLIGHT_COMPONENTS.containsKey(key));
verify(submission).cancel(true);
MatterGenerator.MatterComponentTask replacement = new MatterGenerator.MatterComponentTask(key);
assertNull(MatterGenerator.IN_FLIGHT_COMPONENTS.putIfAbsent(key, replacement));
task.finish(null);
assertSame(replacement, MatterGenerator.IN_FLIGHT_COMPONENTS.get(key));
replacement.finish(null);
}
@Test
public void droppedQueuedSubmissionCannotRunAfterTheBarrier() {
GeneratorFixture fixture = new GeneratorFixture();
MatterGenerator.MatterTaskKey key = new MatterGenerator.MatterTaskKey(fixture.mantle, 0, 0, ReservedFlag.OBJECT);
MatterGenerator.MatterComponentTask task = new MatterGenerator.MatterComponentTask(key);
MatterGenerator.IN_FLIGHT_COMPONENTS.put(key, task);
Future<?> submission = mock(Future.class);
when(submission.isDone()).thenReturn(true);
task.setSubmission(submission);
CompletionException failure = assertThrows(CompletionException.class,
() -> task.await(MatterGenerator.COMPONENT_TASK_TIMEOUT_MS));
assertTrue(failure.getCause().getMessage().contains("was dropped"));
assertFalse(task.start());
assertFalse(MatterGenerator.IN_FLIGHT_COMPONENTS.containsKey(key));
}
@Test
public void interruptedWaiterDrainsRunningTaskAndRestoresInterrupt() throws Exception {
GeneratorFixture fixture = new GeneratorFixture();
MatterGenerator.MatterTaskKey key = new MatterGenerator.MatterTaskKey(fixture.mantle, 0, 0, ReservedFlag.OBJECT);
MatterGenerator.MatterComponentTask task = new MatterGenerator.MatterComponentTask(key);
assertTrue(task.start());
CountDownLatch waiting = new CountDownLatch(1);
AtomicBoolean restored = new AtomicBoolean();
CompletableFuture<Void> completion = new CompletableFuture<>();
Thread waiter = new Thread(() -> {
try {
Thread.currentThread().interrupt();
waiting.countDown();
task.await(MatterGenerator.COMPONENT_TASK_TIMEOUT_MS);
restored.set(Thread.currentThread().isInterrupted());
completion.complete(null);
} catch (Throwable failure) {
completion.completeExceptionally(failure);
}
});
waiter.start();
await(waiting);
try {
assertFalse(completion.isDone());
} finally {
task.finish(null);
}
completion.get(5L, TimeUnit.SECONDS);
waiter.join(5_000L);
assertTrue(restored.get());
}
private <T extends Throwable> T sharedComponentFailure(Throwable failure, Class<T> expectedFailure) {
GeneratorFixture fixture = new GeneratorFixture(true);
RecordingComponent component = new RecordingComponent(ReservedFlag.OBJECT, 0, 0);
TestMatterGenerator generator = fixture.generator(List.of(new MantlePass(List.of(component), 0, 0)));
MatterGenerator.MatterTaskKey key = new MatterGenerator.MatterTaskKey(fixture.mantle, 0, 0, ReservedFlag.OBJECT);
MatterGenerator.MatterComponentTask task = new MatterGenerator.MatterComponentTask(key);
task.finish(failure);
MatterGenerator.IN_FLIGHT_COMPONENTS.put(key, task);
try {
CompletableFuture<Void> generation = MultiBurst.burst.completeValueAsync(() -> {
generator.generateMatter(0, 0, true, fixture.context);
return null;
});
return assertThrows(expectedFailure, () -> generation.get(5L, TimeUnit.SECONDS));
} finally {
MatterGenerator.IN_FLIGHT_COMPONENTS.remove(key, task);
}
}
private static void await(CountDownLatch latch) {
try {
assertTrue(latch.await(5, TimeUnit.SECONDS));
@@ -35,12 +35,15 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -114,6 +117,45 @@ public class IrisCaveCarver3DNearParityTest {
carveForcedAirField.setAccessible(true);
}
@Test
public void carvingReleasesSectionReferencesAfterSuccessAndFailure() throws Exception {
for (boolean fail : new boolean[]{false, true}) {
Engine engine = createEngine(128, 110);
IrisCaveProfile profile = createProfile(false, false).setAdaptiveSampling(false)
.setAllowFluid(false).setDetailWeight(0D)
.setDensityThreshold(new IrisStyledRange(2D, 2D, new IrisGeneratorStyle(NoiseStyle.FLAT)));
IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile);
WriterCapture capture = createWriterCapture(128);
if (fail) {
AtomicInteger samples = new AtomicInteger();
CNG density = mock(CNG.class);
doAnswer(invocation -> {
if (samples.incrementAndGet() > 300) {
throw new IllegalStateException("Density sample failed");
}
return -1D;
}).when(density).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
baseDensityField.set(carver, density);
assertThrows(IllegalStateException.class, () -> carver.carve(capture.writer, 0, 0,
fullWeights(), 0D, 0D, null, filledHeights(110)));
} else {
carver.carve(capture.writer, 0, 0, fullWeights(), 0D, 0D, null, filledHeights(110));
}
assertFalse(capture.carvedCells.isEmpty());
Field scratchField = IrisCaveCarver3D.class.getDeclaredField("scratchCache");
scratchField.setAccessible(true);
ThreadLocal<?> scratchCache = (ThreadLocal<?>) scratchField.get(carver);
CaveCarveScratch scratch = (CaveCarveScratch) scratchCache.get();
assertTrue(scratch.sectionMatter.length > 0);
for (Matter matter : scratch.sectionMatter) {
assertNull(matter);
}
for (MatterSlice<?> slice : scratch.sectionSlices) {
assertNull(slice);
}
}
}
@Test
public void parallelDensityPreservesExactAndAdaptiveCavernsAndLiquids() throws Exception {
try (ForkJoinPool pool = new ForkJoinPool(4)) {
@@ -0,0 +1,170 @@
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineMetrics;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.mantle.MantleWriter;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisCaveProfile;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.matter.Matter;
import org.junit.Before;
import org.junit.Test;
import java.lang.reflect.Field;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class MantleCarvingComponentScratchTest {
private MantleCarvingComponent component;
private IrisDimension dimension;
private ChunkContext context;
private MantleWriter writer;
@Before
@SuppressWarnings("unchecked")
public void prepare() {
Engine engine = mock(Engine.class);
EngineMantle mantle = mock(EngineMantle.class);
when(mantle.getEngine()).thenReturn(engine);
component = new MantleCarvingComponent(mantle);
dimension = new IrisDimension();
when(engine.getDimension()).thenReturn(dimension);
when(engine.getWorld()).thenReturn(mock(IrisWorld.class));
when(engine.getMetrics()).thenReturn(new EngineMetrics(16));
IrisComplex complex = mock(IrisComplex.class);
when(complex.allowsMantleChunkWrite(anyInt(), anyInt())).thenReturn(true);
ProceduralStream<Double> heights = mock(ProceduralStream.class);
when(heights.getDouble(anyDouble(), anyDouble())).thenAnswer(invocation -> {
double x = invocation.getArgument(0);
return x < 0D ? 90D : 70D;
});
when(complex.getNaturalHeightStream()).thenReturn(heights);
ProceduralStream<IrisRegion> regions = mock(ProceduralStream.class);
ProceduralStream<IrisBiome> biomes = mock(ProceduralStream.class);
ProceduralStream<PlatformBlockState> fluids = mock(ProceduralStream.class);
when(regions.get(anyDouble(), anyDouble())).thenReturn(new IrisRegion());
when(biomes.get(anyDouble(), anyDouble())).thenReturn(new IrisBiome());
when(complex.getRegionStream()).thenReturn(regions);
when(complex.getNaturalTrueBiomeStream()).thenReturn(biomes);
when(complex.getCaveBiomeStream()).thenReturn(biomes);
when(complex.getFluidStream()).thenReturn(fluids);
context = mock(ChunkContext.class);
when(context.getComplex()).thenReturn(complex);
writer = mock(MantleWriter.class);
Mantle<Matter> storage = mock(Mantle.class);
when(storage.getWorldHeight()).thenReturn(128);
when(writer.getMantle()).thenReturn(storage);
}
@Test
public void nestedGenerationPreservesOuterColumnHeights() throws Exception {
IrisCaveCarver3D carver = installProfile();
AtomicBoolean entered = new AtomicBoolean();
AtomicInteger calls = new AtomicInteger();
doAnswer(invocation -> {
int chunkX = invocation.getArgument(1);
int[] heights = invocation.getArgument(7);
int expectedHeight = chunkX < 0 ? 90 : 70;
assertEquals(expectedHeight, heights[0]);
calls.incrementAndGet();
if (entered.compareAndSet(false, true)) {
component.generateLayer(writer, -3, -3, context);
}
for (int height : heights) {
assertEquals(expectedHeight, height);
}
return 0;
}).when(carver).carve(any(), anyInt(), anyInt(), any(), anyDouble(), anyDouble(),
any(), any(), any(), any(), any());
component.generateLayer(writer, 2, 2, context);
assertEquals(2, calls.get());
assertReleased();
}
@Test
public void generationReleasesConfigurationReferencesWhenCarvingFails() throws Exception {
IrisCaveCarver3D carver = installProfile();
doAnswer(invocation -> {
throw new IllegalStateException("Carving failed");
}).when(carver).carve(any(), anyInt(), anyInt(), any(), anyDouble(), anyDouble(),
any(), any(), any(), any(), any());
assertThrows(IllegalStateException.class, () -> component.generateLayer(writer, 2, 2, context));
assertReleased();
}
@Test
public void changedProfilesReuseNumericBuffersWithoutKeepingProfileKeys() throws Exception {
Object firstBuffer = null;
for (int index = 0; index < 4; index++) {
installProfile();
component.generateLayer(writer, 2, 2, context);
Object scratch = assertReleased();
List<?> buffers = (List<?>) field(scratch, "weightBuffers");
assertEquals(1, buffers.size());
if (firstBuffer == null) {
firstBuffer = buffers.get(0);
} else {
assertSame(firstBuffer, buffers.get(0));
}
}
}
private IrisCaveCarver3D installProfile() throws Exception {
IrisCaveProfile profile = new IrisCaveProfile().setEnabled(true);
dimension.setCaveProfile(profile);
IrisCaveCarver3D carver = mock(IrisCaveCarver3D.class);
Map<IrisCaveProfile, IrisCaveCarver3D> carvers = new IdentityHashMap<>();
carvers.put(profile, carver);
Field cache = MantleCarvingComponent.class.getDeclaredField("profileCarvers");
cache.setAccessible(true);
cache.set(component, carvers);
return carver;
}
private Object assertReleased() throws Exception {
Field scratchField = MantleCarvingComponent.class.getDeclaredField("BLEND_SCRATCH");
scratchField.setAccessible(true);
ThreadLocal<?> scratchCache = (ThreadLocal<?>) scratchField.get(null);
Object scratch = scratchCache.get();
for (String name : List.of("profileField", "kernelProfiles", "fieldRegions", "fieldSurfaceBiomes", "fieldCaveBiomes")) {
for (Object value : (Object[]) field(scratch, name)) {
assertNull(value);
}
}
assertEquals(0, ((Map<?, ?>) field(scratch, "columnProfileWeights")).size());
assertEquals(0, ((List<?>) field(scratch, "profileOrder")).size());
assertFalse((boolean) field(scratch, "inUse"));
return scratch;
}
private Object field(Object object, String name) throws Exception {
Field field = object.getClass().getDeclaredField(name);
field.setAccessible(true);
return field.get(object);
}
}
@@ -3,6 +3,9 @@ package art.arcane.iris.engine.modifier;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDepositGenerator;
import art.arcane.iris.engine.object.IrisDepositHeightDistribution;
import art.arcane.iris.engine.object.IrisDepositPlacementScope;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
@@ -16,6 +19,7 @@ import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import org.junit.AfterClass;
import org.junit.BeforeClass;
@@ -33,14 +37,11 @@ import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -90,6 +91,25 @@ public class IrisDepositModifierContextTest {
}
}
@Test
public void failedClumpChanceDoesNotResampleTheConfiguredAttemptCount() {
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
IrisDepositGenerator generator = mock(IrisDepositGenerator.class);
when(generator.getSpawnChance()).thenReturn(1D);
when(generator.getMinPerChunk()).thenReturn(0);
when(generator.getMaxPerChunk()).thenReturn(4);
when(generator.getPerClumpSpawnChance()).thenReturn(0D);
RNG rng = mock(RNG.class);
when(rng.d()).thenReturn(0.5D);
when(rng.i(0, 5)).thenReturn(4, 0);
new IrisDepositModifier(engine).generate(generator, null, null, rng, 0, 0, false, null);
verify(generator, times(4)).getPerClumpSpawnChance();
verify(rng, times(1)).i(0, 5);
verify(generator, never()).getClump(any(), any(), any());
}
private static final class Fixture implements AutoCloseable {
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final Engine engine;
@@ -138,16 +158,27 @@ public class IrisDepositModifierContextTest {
return null;
}).when(scope).close();
}
modifier = spy(new IrisDepositModifier(engine));
doAnswer(invocation -> {
assertNotSame(caller, Thread.currentThread());
assertEquals(scoped, workerRuntime.get());
assertSame(engine, IrisContext.require().getEngine());
assertSame(context, IrisContext.require().getChunkContext());
assertEquals(71L, IrisContext.require().getGenerationSessionId());
completed.incrementAndGet();
return null;
}).when(modifier).generate(any(), same(chunk), any(), any(), eq(2), eq(-3), eq(false), same(context));
when(engine.getHeight()).thenReturn(16);
IrisObject clump = new IrisObject(1, 1, 1);
for (IrisDepositGenerator generator : new IrisDepositGenerator[]{dimensionDeposit, regionDeposit, biomeDeposit}) {
when(generator.getSpawnChance()).thenReturn(1D);
when(generator.getMinPerChunk()).thenReturn(1);
when(generator.getMaxPerChunk()).thenReturn(1);
when(generator.getPerClumpSpawnChance()).thenReturn(1D);
when(generator.getPlacementScope()).thenReturn(IrisDepositPlacementScope.FULL_HEIGHT);
when(generator.getHeightDistribution()).thenReturn(IrisDepositHeightDistribution.UNIFORM);
when(generator.matchesBiome(any(), any())).thenReturn(true);
doAnswer(invocation -> {
assertNotSame(caller, Thread.currentThread());
assertEquals(scoped, workerRuntime.get());
assertSame(engine, IrisContext.require().getEngine());
assertSame(context, IrisContext.require().getChunkContext());
assertEquals(71L, IrisContext.require().getGenerationSessionId());
completed.incrementAndGet();
return clump;
}).when(generator).getClump(any(), any(), any());
}
modifier = new IrisDepositModifier(engine);
doAnswer(invocation -> {
assertEquals(3, completed.get());
assertEquals(scoped ? 3 : 0, closedScopes.get());
@@ -0,0 +1,223 @@
package art.arcane.iris.engine.modifier;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDepositGenerator;
import art.arcane.iris.engine.object.IrisDepositHeightDistribution;
import art.arcane.iris.engine.object.IrisDepositPlacementScope;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.common.math.IrisBlockVector;
import art.arcane.iris.util.common.parallel.BurstExecutor;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.math.RNG;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class IrisDepositModifierOrderingTest {
@Before
public void bindPlatform() {
IrisPlatforms.unbind();
PlatformRegistries registries = mock(PlatformRegistries.class);
PlatformBlockState stone = state("stone");
when(registries.block(anyString())).thenReturn(stone);
IrisPlatform platform = mock(IrisPlatform.class);
when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform);
}
@After
public void unbindPlatform() {
IrisPlatforms.unbind();
}
@Test
public void reverseWorkerCompletionKeepsDimensionRegionBiomePlacementOrder() throws Exception {
try (Fixture fixture = new Fixture(true, 1)) {
fixture.generate();
assertEquals(List.of("biome", "region", "dimension"), fixture.completed);
fixture.assertBiomeWins();
}
}
@Test
public void inlinePreparationUsesTheSameOverlappingHostRules() throws Exception {
try (Fixture fixture = new Fixture(false, 1)) {
fixture.generate();
assertEquals(List.of("dimension", "region", "biome"), fixture.completed);
fixture.assertBiomeWins();
}
}
@Test
public void manyAttemptBatchesKeepEveryClumpSeedAndOrderedHostReplacement() throws Exception {
List<String> expectedSamples = new ArrayList<>();
RNG generatorRng = new RNG(0L).nextParallelRNG(0L);
for (String name : List.of("dimension", "region", "biome")) {
for (int i = 0; i < 25; i++) {
expectedSamples.add(name + ":" + IrisDepositModifier.clumpSeed(generatorRng.getSeed(), i));
}
}
Collections.sort(expectedSamples);
for (boolean parallel : new boolean[]{false, true}) {
try (Fixture fixture = new Fixture(parallel, 25)) {
fixture.generate();
Collections.sort(fixture.samples);
assertEquals(expectedSamples, fixture.samples);
assertEquals(75, fixture.completed.size());
fixture.assertBiomeWins();
}
}
}
private static PlatformBlockState state(String material) {
PlatformBlockState state = mock(PlatformBlockState.class);
when(state.key()).thenReturn("minecraft:" + material);
when(state.materialKey()).thenReturn("minecraft:" + material);
return state;
}
private static final class Fixture implements AutoCloseable {
private final ExecutorService executor = Executors.newFixedThreadPool(3);
private final List<String> completed = Collections.synchronizedList(new ArrayList<>());
private final List<String> samples = Collections.synchronizedList(new ArrayList<>());
private final CountDownLatch biomeReady = new CountDownLatch(1);
private final CountDownLatch regionReady = new CountDownLatch(1);
private final Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
private final ChunkContext context = mock(ChunkContext.class, RETURNS_DEEP_STUBS);
private final PlatformBlockState stone = state("stone");
private final PlatformBlockState granite = state("granite");
private final PlatformBlockState diorite = state("diorite");
private final PlatformBlockState diamond = state("diamond_ore");
private final Hunk<PlatformBlockState> output = Hunk.newArrayHunk(16, 16, 16);
private final MantleChunk<Matter> chunk;
private final boolean parallel;
private final int attempts;
@SuppressWarnings("unchecked")
private Fixture(boolean parallel, int attempts) {
this.parallel = parallel;
this.attempts = attempts;
when(engine.getHeight()).thenReturn(16);
when(engine.getCaveBiome(anyInt(), anyInt(), anyInt(), any())).thenReturn(null);
when(engine.getDimension().getDepositVariants()).thenReturn(new KList<>());
when(context.getRegion().get(anyInt(), anyInt()).getDepositVariants()).thenReturn(new KList<>());
when(context.getGenerationSessionId()).thenReturn(11L);
chunk = mock(MantleChunk.class);
Mantle<Matter> mantle = engine.getMantle().getMantle();
doReturn(chunk).when(mantle).getChunk(0, 0);
when(chunk.use()).thenReturn(chunk);
MultiBurst pool = mock(MultiBurst.class);
BurstExecutor burst = new BurstExecutor(executor, 3);
burst.setMulticore(parallel);
when(engine.burst()).thenReturn(pool);
when(pool.burst(parallel)).thenReturn(burst);
IrisDepositGenerator dimensionDeposit = generator("dimension", stone, granite);
IrisDepositGenerator regionDeposit = generator("region", granite, diorite);
IrisDepositGenerator biomeDeposit = generator("biome", diorite, diamond);
when(engine.getDimension().getDeposits()).thenReturn(new KList<>(dimensionDeposit));
when(context.getRegion().get(7, 7).getDeposits()).thenReturn(new KList<>(regionDeposit));
when(context.getBiome().get(7, 7).getDeposits()).thenReturn(new KList<>(biomeDeposit));
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
output.set(x, 4, z, stone);
}
}
}
private IrisDepositGenerator generator(String name, PlatformBlockState requiredHost, PlatformBlockState replacement) {
IrisDepositGenerator generator = mock(IrisDepositGenerator.class);
when(generator.getSpawnChance()).thenReturn(1D);
when(generator.getPerClumpSpawnChance()).thenReturn(1D);
when(generator.getMinPerChunk()).thenReturn(attempts);
when(generator.getMaxPerChunk()).thenReturn(attempts);
when(generator.getMinHeight()).thenReturn(4);
when(generator.getMaxHeight()).thenReturn(4);
when(generator.getPlacementScope()).thenReturn(IrisDepositPlacementScope.FULL_HEIGHT);
when(generator.getHeightDistribution()).thenReturn(IrisDepositHeightDistribution.UNIFORM);
when(generator.matchesBiome(any(), any())).thenReturn(true);
when(generator.canReplace(requiredHost)).thenReturn(true);
IrisObject clump = new IrisObject(33, 1, 33);
for (int x = -16; x <= 16; x++) {
for (int z = -16; z <= 16; z++) {
clump.getBlocks().put(new IrisBlockVector(x, 0, z), replacement);
}
}
when(generator.getClump(any(), any(), any())).thenAnswer(invocation -> {
assertTrue("Placement must release the first clump window before more are prepared",
samples.size() < 32 || output.get(0, 4, 0) != stone);
if (parallel && attempts == 1 && name.equals("dimension")) {
assertTrue(regionReady.await(10, TimeUnit.SECONDS));
} else if (parallel && attempts == 1 && name.equals("region")) {
assertTrue(biomeReady.await(10, TimeUnit.SECONDS));
}
RNG rng = invocation.getArgument(1);
samples.add(name + ":" + rng.getSeed());
completed.add(name);
if (name.equals("biome")) {
biomeReady.countDown();
} else if (name.equals("region")) {
regionReady.countDown();
}
return clump;
});
return generator;
}
private void generate() {
try (MockedStatic<B> blocks = mockStatic(B.class, CALLS_REAL_METHODS)) {
blocks.when(() -> B.toDeepSlateOre(any(), any())).thenAnswer(invocation -> invocation.getArgument(1));
new IrisDepositModifier(engine).generateDeposits(output, 0, 0, parallel, context);
}
verify(chunk).release();
}
private void assertBiomeWins() {
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
assertSame("Deposit collision at " + x + ",4," + z, diamond, output.get(x, 4, z));
}
}
}
@Override
public void close() throws Exception {
executor.shutdownNow();
assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS));
}
}
}
@@ -0,0 +1,36 @@
package art.arcane.iris.engine.modifier;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class IrisDepositModifierProbabilityTest {
@Test
public void generatorAndClumpChancesRemainIndependent() {
int samples = 100_000;
int accepted = 0;
for (int seed = 0; seed < samples; seed++) {
if (new RNG(seed).d() <= 0.2D
&& new RNG(IrisDepositModifier.clumpSeed(seed, 0)).d() <= 0.2D) {
accepted++;
}
}
assertEquals(0.04D, accepted / (double) samples, 0.003D);
}
@Test
public void neighboringClumpChancesRemainIndependentAcrossBatchBoundaries() {
int samples = 100_000;
for (int first : new int[]{0, 7, 31}) {
int accepted = 0;
for (int seed = 0; seed < samples; seed++) {
if (new RNG(IrisDepositModifier.clumpSeed(seed, first)).d() <= 0.2D
&& new RNG(IrisDepositModifier.clumpSeed(seed, first + 1)).d() <= 0.2D) {
accepted++;
}
}
assertEquals("Clump pair at " + first, 0.04D, accepted / (double) samples, 0.003D);
}
}
}
@@ -1,55 +0,0 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
public class IrisBiomeCeilingLayerGuardTest {
private static IrisBiome biome(int layers, int ceilingLayers) {
IrisBiome biome = new IrisBiome();
KList<IrisBiomePaletteLayer> surface = new KList<>();
for (int i = 0; i < layers; i++) {
surface.add(new IrisBiomePaletteLayer().zero());
}
KList<IrisBiomePaletteLayer> ceiling = new KList<>();
for (int i = 0; i < ceilingLayers; i++) {
IrisBiomePaletteLayer layer = new IrisBiomePaletteLayer().zero();
layer.setMinHeight(0);
layer.setMaxHeight(0);
ceiling.add(layer);
}
biome.setLayers(surface);
biome.setCaveCeilingLayers(ceiling);
return biome;
}
@Test
public void moreCeilingLayersThanLayersMustNotThrow() {
IrisBiome biome = biome(1, 3);
KList<PlatformBlockState> result = biome.generateCeilingLayers(
new IrisDimension(), 0, 0, new RNG(1), 8, 64, mock(IrisData.class), null);
assertTrue("extra ceiling layers are skipped, not crashed on", result.isEmpty());
}
@Test
public void inRangeCeilingLayersKeepWorking() {
IrisBiome biome = biome(3, 2);
KList<PlatformBlockState> result = biome.generateCeilingLayers(
new IrisDimension(), 0, 0, new RNG(1), 8, 64, mock(IrisData.class), null);
assertTrue("zeroed palettes with zero heights produce no blocks", result.isEmpty());
}
@Test
public void omittedCeilingLayersLeaveCavesUnchanged() {
assertTrue(new IrisBiome().getCaveCeilingLayers().isEmpty());
}
}
@@ -0,0 +1,78 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
public class IrisBiomeCeilingLayerTest {
@Test
public void emitsEveryCeilingLayerWithoutSurfaceLayers() {
PlatformBlockState first = mock(PlatformBlockState.class);
PlatformBlockState second = mock(PlatformBlockState.class);
PlatformBlockState third = mock(PlatformBlockState.class);
IrisBiome biome = new IrisBiome().setLayers(new KList<>()).setCaveCeilingLayers(
new KList<>(layer(first, 2), layer(second, 1), layer(third, 2)));
KList<PlatformBlockState> result = biome.generateCeilingLayers(
new IrisDimension(), 13, -27, new RNG(1), 8, 64, null, null);
assertEquals(List.of(first, first, second, third, third), result);
assertNull(biome.getLayerHeightGenerators().getIfPresent());
}
@Test
public void ceilingThicknessDoesNotDependOnSurfaceDefinitionOrWarmOrder() {
IrisBiome first = variableBiome(1, 1);
IrisBiome second = variableBiome(7, 31);
RNG rng = new RNG(37);
second.getLayerHeightGenerators(rng, null);
for (int x = -200; x <= 200; x += 7) {
KList<PlatformBlockState> firstCeiling = first.generateCeilingLayers(
new IrisDimension(), x, -x, rng, 100, 120, null, null);
KList<PlatformBlockState> secondCeiling = second.generateCeilingLayers(
new IrisDimension(), x, -x, rng, 100, 120, null, null);
assertEquals(firstCeiling.size(), secondCeiling.size());
}
first.getLayerHeightGenerators(rng, null);
assertNotSame(first.getLayerHeightGenerators().getIfPresent().get(0),
first.getLayerCeilingHeightGenerators().getIfPresent().get(0));
}
@Test
public void capsCeilingLayersAtAvailableDepth() {
PlatformBlockState first = mock(PlatformBlockState.class);
PlatformBlockState second = mock(PlatformBlockState.class);
IrisBiome biome = new IrisBiome().setCaveCeilingLayers(new KList<>(layer(first, 2), layer(second, 3)));
assertEquals(List.of(first, first, second), biome.generateCeilingLayers(
new IrisDimension(), 0, 0, new RNG(1), 3, 64, null, null));
assertTrue(biome.generateCeilingLayers(
new IrisDimension(), 0, 0, new RNG(1), 0, 64, null, null).isEmpty());
}
@Test
public void omittedCeilingLayersLeaveCavesUnchanged() {
assertTrue(new IrisBiome().getCaveCeilingLayers().isEmpty());
}
static IrisBiomePaletteLayer layer(PlatformBlockState block, int thickness) {
IrisBiomePaletteLayer layer = new IrisBiomePaletteLayer().setMinHeight(thickness).setMaxHeight(thickness);
layer.getBlockData().aquire(() -> new KList<>(block));
return layer;
}
private static IrisBiome variableBiome(int surfaceMinimum, int surfaceMaximum) {
PlatformBlockState block = mock(PlatformBlockState.class);
return new IrisBiome()
.setLayers(new KList<>(layer(block, surfaceMinimum).setMaxHeight(surfaceMaximum)))
.setCaveCeilingLayers(new KList<>(layer(block, 1).setMaxHeight(9)));
}
}
@@ -0,0 +1,35 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
public class IrisBiomeLockedLayerTest {
@Test
public void repeatsLockedLayersAcrossTheReferenceHeight() {
PlatformBlockState first = mock(PlatformBlockState.class);
PlatformBlockState second = mock(PlatformBlockState.class);
PlatformBlockState third = mock(PlatformBlockState.class);
IrisBiome biome = new IrisBiome().setLockLayers(true).setLockLayersMax(8).setLayers(new KList<>(
IrisBiomeCeilingLayerTest.layer(first, 1),
IrisBiomeCeilingLayerTest.layer(second, 1),
IrisBiomeCeilingLayerTest.layer(third, 1)));
List<PlatformBlockState> cycle = List.of(first, second, third);
int[] heights = {1, 509, 512, 513, 1000, Integer.MAX_VALUE};
int[][] expectedCycles = {{1, 0, 2}, {0, 2, 1}, {0, 2, 1}, {2, 1, 0}, {1, 0, 2}, {1, 0, 2}};
for (int index = 0; index < heights.length; index++) {
KList<PlatformBlockState> blocks = biome.generateLockedLayers(
13, -27, new RNG(37), 8, heights[index], null, null);
assertEquals(8, blocks.size());
for (int depth = 0; depth < blocks.size(); depth++) {
assertEquals(cycle.get(expectedCycles[index][depth % 3]), blocks.get(depth));
}
}
}
}
@@ -35,11 +35,16 @@ import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Answers.CALLS_REAL_METHODS;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
public class IrisDimensionCarvingResolverParityTest {
private static final int MAX_CHILD_DEPTH = 32;
@@ -71,6 +76,31 @@ public class IrisDimensionCarvingResolverParityTest {
return data;
}
@Test
public void extremeChildRarityRemainsReachableWithOneNoiseSample() {
Fixture fixture = createFixture();
Engine engine = fixture.engine;
IrisData data = engine.getData();
IrisBiome rare = data.getBiomeLoader().load("child-a");
IrisBiome common = data.getBiomeLoader().load("child-b");
IrisBiome parentBiome = data.getBiomeLoader().load("root-low");
doReturn(Integer.MAX_VALUE).when(rare).getRarity();
doReturn(1).when(common).getRarity();
doReturn(2).when(parentBiome).getRarity();
IrisDimensionCarvingEntry parent = spy(engine.getDimension().getCarvingEntryIndex().get("root-low"));
parent.setChildRecursionDepth(1);
CNG generator = mock(CNG.class);
doReturn(0.5D).when(generator).noiseFast2D(-17D, 23D);
doReturn(generator).when(parent).getChildrenGenerator(anyLong(), any(IrisData.class));
IrisDimensionCarvingEntry selected = IrisDimensionCarvingResolver.resolveFromRoot(
engine, parent, -17, 23, new IrisDimensionCarvingResolver.State());
assertSame(engine.getDimension().getCarvingEntryIndex().get("child-a"), selected);
verify(generator).noiseFast2D(-17D, 23D);
verify(generator, never()).fit2D(anyInt(), anyInt(), anyDouble(), anyDouble());
}
@Test
public void resolverStatefulOverloadsMatchLegacyResolverAcrossSampleGrid() {
Fixture fixture = createFixture();
@@ -0,0 +1,74 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.math.IrisBlockVector;
import art.arcane.iris.util.common.math.Vector3i;
import art.arcane.volmlib.util.collection.KMap;
import org.junit.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
public class IrisObjectCopyTest {
@Test
public void copyWaitsForVolumeMutationToComplete() throws Exception {
IrisObject source = new IrisObject(1, 1, 1);
PlatformBlockState block = mock(PlatformBlockState.class);
CountDownLatch started = new CountDownLatch(1);
try (ExecutorService executor = Executors.newSingleThreadExecutor()) {
Future<IrisObject> result;
source.writeLock.lock();
try {
result = executor.submit(() -> {
started.countDown();
return source.copy();
});
assertTrue(started.await(5, TimeUnit.SECONDS));
assertThrows(TimeoutException.class, () -> result.get(150, TimeUnit.MILLISECONDS));
source.setW(3);
source.setCenter(new Vector3i(1, 0, 0));
source.setUnsigned(2, 0, 0, block);
} finally {
source.writeLock.unlock();
}
IrisObject copy = result.get(5, TimeUnit.SECONDS);
assertEquals(3, copy.getW());
assertEquals(new Vector3i(1, 0, 0), copy.getCenter());
assertSame(block, copy.getBlocks().get(new IrisBlockVector(1, 0, 0)));
}
}
@Test
public void copyOwnsItsVolumeCoordinatesAndTiles() {
IrisObject source = new IrisObject(1, 1, 1);
PlatformBlockState block = mock(PlatformBlockState.class);
TileData tile = new TileData("minecraft:chest", new KMap<>());
tile.getProperties().put("name", "source");
source.setUnsigned(0, 0, 0, block);
source.setUnsignedTile(0, 0, 0, tile);
IrisObject copy = source.copy();
IrisBlockVector position = new IrisBlockVector(0, 0, 0);
TileData copiedTile = copy.getStates().get(position);
assertNotSame(source.getCenter(), copy.getCenter());
assertNotSame(tile, copiedTile);
assertNotSame(tile.getProperties(), copiedTile.getProperties());
copiedTile.getProperties().put("name", "copy");
copy.getBlocks().remove(position);
assertEquals("source", tile.getProperties().get("name"));
assertSame(block, source.getBlocks().get(position));
}
}
@@ -10,6 +10,9 @@ import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.RNG;
import org.bukkit.Material;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.Directional;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -19,6 +22,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -69,6 +73,36 @@ public class IrisObjectPlacementRunnerRegressionTest {
IrisPlatforms.unbind();
}
@Test
public void unsupportedRotatedOrientationOmitsBlockTileAndListener() {
PlatformBlockState torch = state("minecraft:wall_torch", false);
Directional directional = mock(Directional.class);
Material material = mock(Material.class);
when(torch.nativeHandle()).thenReturn(directional);
when(directional.clone()).thenReturn(directional);
when(directional.getFacing()).thenReturn(BlockFace.NORTH);
when(directional.getFaces()).thenReturn(Set.of(BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST));
when(directional.getMaterial()).thenReturn(material);
IrisObjectPlacement placement = placement();
placement.setRotation(IrisObjectRotation.of(90, 0, 0));
assertNull(placement.getRotation().rotate(torch, 0, 0, 0));
IrisObject object = lineObject(3);
object.setUnsigned(1, 0, 0, torch);
object.setUnsignedTile(1, 0, 0, new TileData("minecraft:wall_torch", new KMap<>()));
RecordingPlacer placer = new RecordingPlacer(null);
List<PlatformBlockState> placed = new ArrayList<>();
int result = object.place(0, ANCHOR_Y, 0, placer, placement, new RNG(2L),
(position, state) -> placed.add(state), null, data);
assertEquals(ANCHOR_Y, result);
assertEquals(2, placer.writes().size());
assertEquals(List.of(solid, solid), placed);
assertNull(placer.get(0, ANCHOR_Y, 0));
assertNull(placer.getData(0, ANCHOR_Y, 0, TileData.class));
assertEquals(1, object.getStates().size());
}
@Test
public void placementFailurePropagatesToTheCaller() {
RecordingPlacer placer = new RecordingPlacer(null);
@@ -0,0 +1,127 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.util.common.math.IrisBlockVector;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisObjectShapingTest {
@Before
public void bindPlatform() {
IrisPlatforms.unbind();
PlatformRegistries registries = mock(PlatformRegistries.class);
Map<String, PlatformBlockState> states = new HashMap<>();
when(registries.block(anyString())).thenAnswer(invocation -> states.computeIfAbsent(
invocation.getArgument(0), key -> mock(PlatformBlockState.class)));
IrisPlatform platform = mock(IrisPlatform.class);
when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform);
}
@After
public void unbindPlatform() {
IrisPlatforms.unbind();
}
@Test
public void smartBoreMatchesOrderedAxisClosureAndPreservesOriginalBlocks() {
Random random = new Random(617431L);
PlatformBlockState stone = mock(PlatformBlockState.class);
for (int fixture = 0; fixture < 24; fixture++) {
int offset = switch (fixture % 3) {
case 0 -> -1050;
case 1 -> 1020;
default -> -3;
};
IrisObject object = new IrisObject(6, 6, 6);
Map<Cell, PlatformBlockState> expected = new HashMap<>();
for (int index = 0; index < 36; index++) {
Cell cell = new Cell(offset + random.nextInt(6), offset + random.nextInt(6), offset + random.nextInt(6));
PlatformBlockState state = switch (index % 3) {
case 0 -> IrisObject.States.AIR;
case 1 -> IrisObject.States.VAIR;
default -> stone;
};
object.getBlocks().put(cell.vector(), state);
expected.put(cell, state);
}
for (int axis = 0; axis < 3; axis++) {
closeAxis(expected, axis);
}
IrisObjectShaping.ensureSmartBored(object);
assertTrue(object.isSmartBored());
assertEquals("fixture " + fixture, expected.size(), object.getBlocks().size());
for (Map.Entry<Cell, PlatformBlockState> entry : expected.entrySet()) {
assertSame(entry.getValue(), object.getBlocks().get(entry.getKey().vector()));
}
long revision = object.getBlocks().modificationRevision();
IrisObjectShaping.ensureSmartBored(object);
assertEquals(revision, object.getBlocks().modificationRevision());
}
}
@Test
public void emptySmartBoreCompletesWithoutAddingBlocks() {
IrisObject object = new IrisObject(1, 1, 1);
IrisObjectShaping.ensureSmartBored(object);
assertTrue(object.isSmartBored());
assertTrue(object.getBlocks().isEmpty());
}
private static void closeAxis(Map<Cell, PlatformBlockState> blocks, int axis) {
List<Cell> occupied = new ArrayList<>(blocks.keySet());
for (Cell start : occupied) {
for (Cell end : occupied) {
if (!start.with(axis, 0).equals(end.with(axis, 0))) {
continue;
}
for (int position = start.coordinate(axis); position <= end.coordinate(axis); position++) {
blocks.putIfAbsent(start.with(axis, position), IrisObject.States.VAIR);
}
}
}
}
private record Cell(int x, int y, int z) {
private int coordinate(int axis) {
return switch (axis) {
case 0 -> x;
case 1 -> y;
default -> z;
};
}
private Cell with(int axis, int coordinate) {
return switch (axis) {
case 0 -> new Cell(coordinate, y, z);
case 1 -> new Cell(x, coordinate, z);
default -> new Cell(x, y, coordinate);
};
}
private IrisBlockVector vector() {
return new IrisBlockVector(x, y, z);
}
}
}
@@ -16,6 +16,9 @@ import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -46,6 +49,40 @@ public class IrisObjectTransformsTileTest {
IrisPlatforms.unbind();
}
@Test
public void rotationOmitsUnsupportedBlocksAndTheirTiles() {
IrisObject source = new IrisObject(3, 1, 1);
PlatformBlockState unsupported = state("minecraft:wall_torch");
source.setUnsigned(0, 0, 0, unsupported);
source.setUnsignedTile(0, 0, 0, tile("minecraft:wall_torch", "omitted"));
source.setUnsigned(1, 0, 0, state("minecraft:chest"));
source.setUnsignedTile(1, 0, 0, tile("minecraft:chest", "kept"));
source.setUnsigned(2, 0, 0, state("minecraft:stone"));
IrisObjectRotation.StateRotator previous = IrisObjectRotation.bindPlatformRotator(
(rotation, block, x, y, z) -> block == unsupported ? null : block);
IrisObject rotated;
try {
rotated = source.rotateCopy(IrisObjectRotation.of(0, 90, 0));
} finally {
IrisObjectRotation.restorePlatformRotator(previous);
}
assertEquals(2, rotated.getBlocks().size());
assertEquals(1, rotated.getStates().size());
for (Map.Entry<IrisBlockVector, TileData> entry : rotated.getStates()) {
assertEquals("minecraft:chest", rotated.getBlocks().get(entry.getKey()).materialKey());
assertEquals("kept", entry.getValue().getProperties().get("name"));
}
assertEquals(3, source.getBlocks().size());
assertEquals(2, source.getStates().size());
}
@Test
public void scalingWaitsForMatchingGeometryAndVolume() throws Exception {
assertConsistentScaling(false);
assertConsistentScaling(true);
}
@Test
public void enlargedTilesFollowEveryExpandedBlockAndCloneProperties() {
IrisObject source = new IrisObject(1, 1, 1);
@@ -186,6 +223,52 @@ public class IrisObjectTransformsTileTest {
assertEquals(8, scaled.getStates().size());
}
private static void assertConsistentScaling(boolean savedOrigin) throws Exception {
IrisObject source = new IrisObject(9, 5, 3);
source.setUnsigned(0, 0, 0, state("minecraft:stone"));
FutureTask<IrisObject> scaling = new FutureTask<>(() -> savedOrigin
? source.scaledAroundOrigin(2, IrisObjectPlacementScaleInterpolator.NONE)
: source.scaled(2, IrisObjectPlacementScaleInterpolator.NONE));
Thread worker = new Thread(scaling, "object-scaling-snapshot");
source.writeLock.lock();
try {
worker.start();
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
while (worker.getState() != Thread.State.WAITING && System.nanoTime() < deadline) {
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
}
assertEquals(Thread.State.WAITING, worker.getState());
source.setW(2);
source.setH(2);
source.setD(2);
source.setCenter(new Vector3i(1, 1, 1));
source.getBlocks().clear();
source.setUnsigned(0, 0, 0, state("minecraft:chest"));
source.setUnsignedTile(0, 0, 0, tile("minecraft:chest", "new volume"));
} finally {
source.writeLock.unlock();
}
try {
IrisObject actual = scaling.get(5, TimeUnit.SECONDS);
IrisObject expected = savedOrigin
? source.scaledAroundOrigin(2, IrisObjectPlacementScaleInterpolator.NONE)
: source.scaled(2, IrisObjectPlacementScaleInterpolator.NONE);
assertEquals(expected.getCenter(), actual.getCenter());
assertEquals(expected.getW(), actual.getW());
assertEquals(expected.getH(), actual.getH());
assertEquals(expected.getD(), actual.getD());
assertEquals(expected.getBlocks().size(), actual.getBlocks().size());
assertEquals(expected.getStates().size(), actual.getStates().size());
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : expected.getBlocks()) {
assertEquals(entry.getValue(), actual.getBlocks().get(entry.getKey()));
assertEquals(expected.getStates().get(entry.getKey()), actual.getStates().get(entry.getKey()));
}
} finally {
worker.interrupt();
worker.join(TimeUnit.SECONDS.toMillis(5));
}
}
private static IrisObject asymmetricObject() {
IrisObject source = new IrisObject(7, 5, 3);
source.setCenter(new Vector3i(5, 1, 2));
@@ -0,0 +1,66 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public class IrisOreGeneratorTest {
@Test
public void zeroThresholdDisablesEvenZeroNoiseSamples() {
Fixture fixture = new Fixture(0.0);
fixture.generator.setThreshold(0.0);
assertNull(fixture.generator.generate(2, 40, 3, fixture.rng, null));
verifyNoInteractions(fixture.noise, fixture.style);
}
@Test
public void positiveThresholdStillIncludesExactCutoffAndVerticalBounds() {
Fixture fixture = new Fixture(0.5);
fixture.generator.setThreshold(0.5);
assertSame(fixture.ore, fixture.generator.generate(2, 30, 3, fixture.rng, null));
assertSame(fixture.ore, fixture.generator.generate(2, 80, 3, fixture.rng, null));
assertNull(fixture.generator.generate(2, 29, 3, fixture.rng, null));
assertNull(fixture.generator.generate(2, 81, 3, fixture.rng, null));
fixture.generator.setThreshold(0.49);
assertNull(fixture.generator.generate(2, 40, 3, fixture.rng, null));
}
@Test
public void fullThresholdIncludesMaximumNoise() {
Fixture fixture = new Fixture(1.0);
fixture.generator.setThreshold(1.0);
assertSame(fixture.ore, fixture.generator.generate(2, 40, 3, fixture.rng, null));
}
private static final class Fixture {
private final RNG rng = new RNG(34L);
private final PlatformBlockState ore = mock(PlatformBlockState.class);
private final CNG noise = mock(CNG.class);
private final IrisGeneratorStyle style = mock(IrisGeneratorStyle.class);
private final IrisOreGenerator generator = new IrisOreGenerator();
private Fixture(double sample) {
IrisMaterialPalette palette = mock(IrisMaterialPalette.class);
when(palette.getPalette()).thenReturn(new KList<>(mock(IrisBlockData.class)));
when(palette.get(any(), anyDouble(), anyDouble(), anyDouble(), isNull())).thenReturn(ore);
when(noise.noise(anyDouble(), anyDouble(), anyDouble())).thenReturn(sample);
when(style.create(any(), isNull())).thenReturn(noise);
generator.setPalette(palette);
generator.setChanceStyle(style);
}
}
}
@@ -0,0 +1,141 @@
package art.arcane.iris.engine.object;
import com.google.gson.Gson;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
public class IrisRaritySelectionTest {
@Test
public void everyIndexMatchesExpandedInsertionOrderForRandomizedChoices() {
Random random = new Random(718938L);
for (int scenario = 0; scenario < 500; scenario++) {
ArrayList<Choice> choices = new ArrayList<>();
int count = random.nextInt(12);
for (int index = 0; index < count; index++) {
choices.add(random.nextInt(7) == 0 ? null : new Choice(index, random.nextInt(35) - 3));
}
List<Choice> expanded = expand(choices);
IrisRaritySelection<Choice> selection = IrisRaritySelection.create(choices);
assertEquals(expanded.size(), selection.size());
if (expanded.isEmpty()) {
assertNull(selection.get(0));
continue;
}
for (int index = 0; index < expanded.size(); index++) {
assertSame(expanded.get(index), selection.get(index));
}
assertSame(expanded.getFirst(), selection.get(Long.MIN_VALUE));
assertSame(expanded.getLast(), selection.get(Long.MAX_VALUE));
for (double sample : new double[]{-3D, -0D, 0.125D, 0.5D, 0.875D, 1D, 3D,
Double.NaN, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY}) {
int index = (int) Math.round(sample * (expanded.size() - 1));
assertSame(expanded.get(Math.max(0, Math.min(expanded.size() - 1, index))), selection.select(sample));
}
}
}
@Test(timeout = 1000)
public void maximumRarityCountsStayPositiveWithoutExpandingBillionsOfEntries() {
Choice rare = new Choice(0, Integer.MAX_VALUE);
Choice common = new Choice(1, 1);
Choice next = new Choice(2, 2);
IrisRaritySelection<Choice> selection = IrisRaritySelection.create(List.of(rare, common, next));
assertEquals(4_294_967_294L, selection.size());
assertSame(next, selection.get(0L));
assertSame(common, selection.get(2_147_483_646L));
assertSame(rare, selection.get(2_147_483_647L));
assertSame(common, selection.get(2_147_483_648L));
assertSame(next, selection.get(selection.size() - 1L));
assertSame(rare, selection.select(0.5D));
assertSame(next, selection.select(0D));
assertSame(next, selection.select(1D));
}
@Test
public void equalMaximumRaritiesRetainEveryChoice() {
Choice first = new Choice(0, Integer.MAX_VALUE);
Choice second = new Choice(1, Integer.MAX_VALUE);
Choice third = new Choice(2, Integer.MAX_VALUE);
IrisRaritySelection<Choice> selection = IrisRaritySelection.create(List.of(first, second, third));
assertEquals(3L, selection.size());
assertSame(second, selection.get(0L));
assertSame(first, selection.get(1L));
assertSame(third, selection.get(2L));
}
@Test
public void duplicateChoicesKeepTheirSeparateInsertionPositions() {
Choice repeated = new Choice(0, 1);
Choice other = new Choice(1, 3);
List<Choice> choices = List.of(repeated, other, repeated);
IrisRaritySelection<Choice> selection = IrisRaritySelection.create(choices);
List<Choice> expanded = expand(choices);
for (int index = 0; index < expanded.size(); index++) {
assertSame(expanded.get(index), selection.get(index));
}
}
@Test
public void deserializedRuntimeRarityValuesUseTheExistingMinimumClamp() {
Gson gson = new Gson();
IrisBiome zero = gson.fromJson("{\"rarity\":0}", IrisBiome.class);
IrisBiome negative = gson.fromJson("{\"rarity\":-2147483648}", IrisBiome.class);
IrisBiome maximum = gson.fromJson("{\"rarity\":2147483647}", IrisBiome.class);
IrisRaritySelection<IrisBiome> selection = IrisRaritySelection.create(List.of(zero, negative, maximum));
assertEquals(4_294_967_295L, selection.size());
assertSame(zero, selection.get(2_147_483_647L));
assertSame(maximum, selection.get(selection.size() - 1L));
assertEquals(0, zero.getRarity());
assertEquals(Integer.MIN_VALUE, negative.getRarity());
}
@Test
public void nullAndEmptyOptionsDoNotProduceCandidates() {
assertNull(IrisRaritySelection.<Choice>create(List.of()).select(0.5D));
assertNull(IrisRaritySelection.<Choice>create(Arrays.asList(null, null)).get(0L));
}
private static List<Choice> expand(List<Choice> choices) {
int maximum = 1;
for (Choice choice : choices) {
if (choice != null) {
maximum = Math.max(maximum, choice.rarity());
}
}
ArrayList<Choice> expanded = new ArrayList<>();
boolean append = false;
for (Choice choice : choices) {
if (choice == null) {
continue;
}
int count = maximum + 1 - Math.max(1, choice.rarity());
for (int index = 0; index < count; index++) {
append = !append;
if (append) {
expanded.add(choice);
} else {
expanded.add(0, choice);
}
}
}
return expanded;
}
private record Choice(int id, int rarity) implements IRare {
@Override
public int getRarity() {
return rarity;
}
}
}
@@ -8,6 +8,7 @@ import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.util.common.math.Vector3i;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
@@ -101,6 +102,35 @@ public class NativeStructureObjectVetoTest {
assertTrue(placer.written().isEmpty());
}
@Test
public void obliqueRotationRejectsNativePiecesBeyondTheUnrotatedRadius() {
IrisObject object = new IrisObject(21, 1, 21);
object.setUnsigned(20, 0, 20, log);
IrisObjectPlacement placement = placement();
placement.setBottom(false);
placement.setRotation(IrisObjectRotation.of(0, 45, 0));
RecordingPlacer placer = new RecordingPlacer(engine);
volumes(volume(14, SURFACE_Y, 0, 14, SURFACE_Y, 0));
assertEquals(-1, object.place(0, SURFACE_Y, 0, placer, placement, new RNG(1234L), data));
assertTrue(placer.written().isEmpty());
}
@Test
public void offCenterOriginRejectsNativePiecesAtTheFarEdge() {
IrisObject object = new IrisObject(9, 1, 1);
object.setCenter(new Vector3i(0, 0, 0));
object.setUnsigned(8, 0, 0, log);
IrisObjectPlacement placement = placement();
placement.setBottom(false);
placement.setRotation(IrisObjectRotation.of(0, 0, 0));
RecordingPlacer placer = new RecordingPlacer(engine);
volumes(volume(8, SURFACE_Y, 0, 8, SURFACE_Y, 0));
assertEquals(-1, object.place(0, SURFACE_Y, 0, placer, placement, new RNG(1234L), data));
assertTrue(placer.written().isEmpty());
}
@Test
public void pieceOverlappingOnlyTheEnvelopeStillPlaces() {
RecordingPlacer placer = new RecordingPlacer(engine);
@@ -0,0 +1,72 @@
package art.arcane.iris.engine.object.coral;
import art.arcane.iris.engine.object.IrisBlockData;
import art.arcane.iris.engine.object.IrisCoral;
import art.arcane.iris.engine.object.IrisCoralForm;
import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.math.IrisBlockVector;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class CoralGeneratorTest {
@Test
public void fanTipsFollowTheTopOfEachOccupiedColumn() {
PlatformBlockState structure = mock(PlatformBlockState.class);
PlatformBlockState tip = mock(PlatformBlockState.class);
for (int height : new int[]{2, 7, 14}) {
for (int width : new int[]{1, 3, 11}) {
IrisCoral coral = new IrisCoral().setForm(IrisCoralForm.FAN)
.setHeightMin(height).setHeightMax(height).setFanWidth(width)
.setWaterlogged(false).setBlockPalette(palette(structure));
IrisObject bare = CoralGenerator.generate(coral, 0, new RNG(0L), null);
Map<Integer, Integer> tops = new HashMap<>();
Set<IrisBlockVector> barePositions = new HashSet<>();
for (IrisBlockVector position : bare.getBlocks().keys()) {
barePositions.add(position);
tops.merge(position.getBlockX(), position.getBlockY(), Math::max);
}
coral.setTipPalette(palette(tip));
for (double chance : new double[]{0.0, 0.35, 1.0}) {
coral.setTipChance(chance);
for (long seed = 0; seed < 16; seed++) {
IrisObject decorated = CoralGenerator.generate(coral, 0, new RNG(seed), null);
Set<IrisBlockVector> positions = new HashSet<>();
int tipCount = 0;
for (IrisBlockVector position : decorated.getBlocks().keys()) {
positions.add(position);
if (decorated.getBlocks().get(position) == tip) {
assertTrue("Fan tip must occupy an existing column", tops.containsKey(position.getBlockX()));
assertEquals(tops.get(position.getBlockX()).intValue(), position.getBlockY());
tipCount++;
}
}
assertEquals(barePositions, positions);
if (chance == 1.0) {
assertEquals(tops.size(), tipCount);
}
}
}
}
}
}
private static IrisMaterialPalette palette(PlatformBlockState state) {
IrisBlockData block = mock(IrisBlockData.class);
when(block.getWeight()).thenReturn(1);
when(block.getBlockData(null)).thenReturn(state);
return new IrisMaterialPalette().setPalette(new KList<>(block));
}
}
@@ -0,0 +1,79 @@
package art.arcane.iris.engine.object.formation;
import art.arcane.iris.engine.object.IrisBlockData;
import art.arcane.iris.engine.object.IrisFormation;
import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.math.Vector3i;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class FormationBlockResolverTest {
@Test
public void sharedResolverMatchesFreshPaletteSeedsAcrossBands() {
IrisFormation formation = new IrisFormation().setSeed(43191L).setStrataThickness(3);
IrisMaterialPalette body = palette("stone", "andesite");
IrisMaterialPalette cap = palette("granite", "diorite");
IrisMaterialPalette strata = palette("terracotta", "red_terracotta");
formation.setBlockPalette(body);
List<Vector3i> positions = new ArrayList<>();
for (int x = -8; x <= 8; x++) {
for (int y = -12; y <= 24; y++) {
positions.add(new Vector3i(x, y, x - y));
}
}
Set<String> materials = new HashSet<>();
for (boolean withStrata : new boolean[]{false, true}) {
formation.setStrataPalette(withStrata ? strata : null);
for (boolean withCap : new boolean[]{false, true}) {
formation.setCapPalette(withCap ? cap : null);
FormationBlockResolver resolver = new FormationBlockResolver(formation, null);
for (int pass = 0; pass < 2; pass++) {
for (Vector3i position : positions) {
FormationCanvas.Role role = position.getBlockX() % 2 == 0
? FormationCanvas.Role.CAP : FormationCanvas.Role.BODY;
IrisMaterialPalette expectedPalette = withCap && role == FormationCanvas.Role.CAP
? cap : withStrata ? strata : body;
int sampleY = expectedPalette == strata
? Math.floorDiv(position.getBlockY(), formation.getStrataThickness()) : position.getBlockY();
long sampleSeed = formation.getSeed() + (expectedPalette == strata ? sampleY * 31L : 0L);
RNG fresh = new RNG(sampleSeed);
PlatformBlockState expected = expectedPalette.get(fresh,
position.getBlockX(), sampleY, position.getBlockZ(), null);
assertSame(expected, resolver.resolve(role, position));
assertEquals(new RNG(sampleSeed).nextLong(), fresh.nextLong());
materials.add(expected.key());
}
Collections.reverse(positions);
}
}
}
assertTrue(materials.size() == 6);
}
private static IrisMaterialPalette palette(String first, String second) {
return new IrisMaterialPalette().setPalette(new KList<>(block(first), block(second)));
}
private static IrisBlockData block(String material) {
PlatformBlockState state = mock(PlatformBlockState.class);
when(state.key()).thenReturn("minecraft:" + material);
IrisBlockData block = mock(IrisBlockData.class);
when(block.getWeight()).thenReturn(1);
when(block.getBlockData(null)).thenReturn(state);
return block;
}
}
@@ -0,0 +1,67 @@
package art.arcane.iris.engine.object.fungi;
import art.arcane.iris.engine.object.IrisFungus;
import art.arcane.iris.util.common.math.Vector3i;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class FungusStemBuilderTest {
@Test
public void straightStemsKeepOddAndEvenWidthAnchors() {
for (int width = 1; width <= 3; width++) {
IrisFungus fungus = new IrisFungus();
fungus.setStemWidth(width);
fungus.setStemCurve(0D);
fungus.setStemWaveAmplitude(0D);
Map<Vector3i, FungusCellRole> roles = new HashMap<>();
double[] top = FungusStemBuilder.build(roles, fungus, 7, 421L);
assertArrayEquals(new double[]{0D, 6D, 0D}, top, 0D);
assertEquals(width * width * 7, roles.size());
int origin = width == 3 ? -1 : 0;
for (int y = 0; y < 7; y++) {
for (int x = origin; x < origin + width; x++) {
for (int z = origin; z < origin + width; z++) {
assertEquals(FungusCellRole.STEM, roles.get(new Vector3i(x, y, z)));
}
}
}
}
}
@Test
public void curvedStemsKeepEveryLayerAndReturnTheirCapAnchor() {
IrisFungus fungus = new IrisFungus();
fungus.setStemWidth(3);
fungus.setStemCurve(22D);
fungus.setStemLeanAzimuth(225D);
fungus.setStemWaveAmplitude(2D);
fungus.setStemWavePeriods(1.5D);
Map<Vector3i, FungusCellRole> roles = new HashMap<>();
double[] top = FungusStemBuilder.build(roles, fungus, 24, -733L);
assertEquals(24 * 9, roles.size());
assertTrue(top[0] < 0D);
assertTrue(top[2] < 0D);
assertEquals(23D, top[1], 0D);
assertEquals(FungusCellRole.STEM, roles.get(new Vector3i(
(int) Math.round(top[0]), 23, (int) Math.round(top[2]))));
for (int y = 0; y < 24; y++) {
int count = 0;
for (Vector3i position : roles.keySet()) {
if (position.getY() == y) {
count++;
}
}
assertEquals(9, count);
}
}
}
@@ -6,9 +6,11 @@ import art.arcane.iris.engine.object.IrisTreeBranches;
import art.arcane.iris.engine.object.IrisTreeFunction;
import art.arcane.iris.engine.object.IrisTreeLeafMode;
import art.arcane.iris.engine.object.IrisTreeProfile;
import art.arcane.iris.spi.PlatformBlockState;
import org.junit.Test;
import java.util.HashSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -18,6 +20,9 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ProceduralTreeGeneratorTest {
@@ -268,4 +273,23 @@ public class ProceduralTreeGeneratorTest {
distances.get(new TreeBlockCanvas.Vec(x, 0, 0)));
}
}
@Test
public void disabledPlausibilityMakesDisconnectedLeavesPersistent() {
IrisProceduralTree tree = new IrisProceduralTree();
tree.setPlausible(false);
TreeBlockCanvas.Vec position = new TreeBlockCanvas.Vec(40, 20, -10);
PlatformBlockState leaf = mock(PlatformBlockState.class);
PlatformBlockState persistent = mock(PlatformBlockState.class);
PlatformBlockState supported = mock(PlatformBlockState.class);
when(leaf.key()).thenReturn("minecraft:oak_leaves[persistent=false,distance=7]");
when(leaf.withProperty("persistent", "true")).thenReturn(persistent);
when(persistent.withProperty("distance", "1")).thenReturn(supported);
Map<TreeBlockCanvas.Vec, PlatformBlockState> resolved = new HashMap<>();
resolved.put(position, leaf);
TreePlausibility.apply(resolved, Set.of(new TreeBlockCanvas.Vec(0, 0, 0)), Set.of(position), tree);
assertSame(supported, resolved.get(position));
}
}
@@ -0,0 +1,106 @@
package art.arcane.iris.engine.object.tree;
import art.arcane.iris.engine.object.IrisBlockData;
import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisProceduralTree;
import art.arcane.iris.engine.object.IrisTreeSecondaryLeaf;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.B;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class TreeBlockResolverTest {
@Test
public void largeSecondaryWeightsKeepTheirRelativeDistribution() {
IrisProceduralTree tree = new IrisProceduralTree();
tree.setSeed(91874L);
tree.setLeavesPalette(palette("oak_leaves", "birch_leaves"));
PlatformBlockState blossom = mock(PlatformBlockState.class);
PlatformBlockState light = mock(PlatformBlockState.class);
List<PlatformBlockState> normal = new ArrayList<>();
List<PlatformBlockState> large = new ArrayList<>();
TreeBlockCanvas.Cell cell = new TreeBlockCanvas.Cell(
TreeBlockCanvas.Role.SECONDARY_LEAF, TreeBlockCanvas.Axis.NONE, false, -1, null);
try (MockedStatic<B> blocks = mockStatic(B.class)) {
blocks.when(() -> B.getStateOrNull("minecraft:cherry_leaves", false)).thenReturn(blossom);
blocks.when(() -> B.getStateOrNull("minecraft:shroomlight", false)).thenReturn(light);
for (int weight : new int[]{1, Integer.MAX_VALUE}) {
tree.setWeightedSecondaryLeaves(new KList<>(
new IrisTreeSecondaryLeaf("minecraft:cherry_leaves", weight),
new IrisTreeSecondaryLeaf("minecraft:shroomlight", weight)));
List<PlatformBlockState> resolved = weight == 1 ? normal : large;
for (int x = -16; x <= 16; x++) {
for (int y = 0; y < 16; y++) {
resolved.add(TreeBlockResolver.resolve(tree, null, cell,
new TreeBlockCanvas.Vec(x, y, x + y), new RNG(tree.getSeed())));
}
}
}
}
assertTrue(normal.contains(blossom));
assertTrue(normal.contains(light));
assertEquals(normal, large);
}
@Test
public void sharedPaletteRandomSourceMatchesFreshPerVoxelSources() {
IrisProceduralTree tree = new IrisProceduralTree();
tree.setSeed(84261L);
tree.setTrunkPalette(palette("oak_log", "spruce_log"));
tree.setLeavesPalette(palette("oak_leaves", "birch_leaves"));
tree.setSecondaryTrunkPalette(palette("birch_log", "jungle_log"));
tree.setSecondaryLeavesPalette(palette("spruce_leaves", "jungle_leaves"));
RNG shared = new RNG(tree.getSeed());
List<String> freshBlocks = new ArrayList<>();
List<String> sharedBlocks = new ArrayList<>();
Set<String> materials = new HashSet<>();
TreeBlockCanvas.Role[] roles = {
TreeBlockCanvas.Role.TRUNK, TreeBlockCanvas.Role.LEAF,
TreeBlockCanvas.Role.SECONDARY_TRUNK, TreeBlockCanvas.Role.SECONDARY_LEAF
};
for (int x = -8; x <= 8; x++) {
for (int y = 0; y < 24; y++) {
TreeBlockCanvas.Vec position = new TreeBlockCanvas.Vec(x, y, x - y);
TreeBlockCanvas.Cell cell = new TreeBlockCanvas.Cell(
roles[Math.floorMod(x + y, roles.length)], TreeBlockCanvas.Axis.NONE, false, -1, null);
PlatformBlockState expected = TreeBlockResolver.resolve(tree, null, cell, position, new RNG(tree.getSeed()));
PlatformBlockState actual = TreeBlockResolver.resolve(tree, null, cell, position, shared);
freshBlocks.add(position + ":" + expected.key());
sharedBlocks.add(position + ":" + actual.key());
materials.add(actual.key());
}
}
assertEquals(freshBlocks, sharedBlocks);
assertEquals(new RNG(tree.getSeed()).nextLong(), shared.nextLong());
assertTrue(materials.size() > roles.length);
}
private static IrisMaterialPalette palette(String first, String second) {
IrisBlockData firstBlock = block(first);
IrisBlockData secondBlock = block(second);
return new IrisMaterialPalette().setPalette(new KList<>(firstBlock, secondBlock));
}
private static IrisBlockData block(String material) {
PlatformBlockState state = mock(PlatformBlockState.class);
when(state.key()).thenReturn("minecraft:" + material);
IrisBlockData block = mock(IrisBlockData.class);
when(block.getWeight()).thenReturn(1);
when(block.getBlockData(null)).thenReturn(state);
return block;
}
}
@@ -0,0 +1,45 @@
package art.arcane.iris.engine.object.tree;
import art.arcane.iris.engine.object.IrisProceduralTree;
import art.arcane.iris.engine.object.IrisTreeDecorator;
import art.arcane.iris.engine.object.IrisTreeDecoratorTarget;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
public class TreeDecoratorApplierTest {
@Test
public void branchTipsExtendOutwardThroughLeavesWithoutReplacingWood() {
TreeBlockCanvas canvas = new TreeBlockCanvas();
for (int x = -3; x <= 3; x++) {
canvas.setTrunk(x, 4, 0, TreeBlockCanvas.Role.TRUNK, TreeBlockCanvas.Axis.X);
}
canvas.setLeaf(-4, 4, 0, TreeBlockCanvas.Role.LEAF);
canvas.setLeaf(4, 4, 0, TreeBlockCanvas.Role.LEAF);
IrisTreeDecorator decorator = new IrisTreeDecorator()
.setTarget(IrisTreeDecoratorTarget.BRANCH_TIP)
.setBlock("minecraft:shroomlight")
.setChance(1D)
.setAxisAware(true);
IrisProceduralTree tree = new IrisProceduralTree();
tree.setDecorators(new KList<>(decorator));
List<int[]> endpoints = List.of(new int[]{-3, 4, 0, 0, 0}, new int[]{3, 4, 0, 0, 0});
TreeDecoratorApplier.apply(canvas, tree, 4321L, endpoints);
assertEquals(TreeBlockCanvas.Role.DECORATOR, canvas.get(-5, 4, 0).role());
assertEquals("west", canvas.get(-5, 4, 0).facing());
assertEquals(TreeBlockCanvas.Role.DECORATOR, canvas.get(5, 4, 0).role());
assertEquals("east", canvas.get(5, 4, 0).facing());
assertEquals(7, canvas.getTrunk().size());
assertEquals(2, canvas.getLeaf().size());
assertEquals(11, canvas.getCells().size());
TreeDecoratorApplier.apply(canvas, tree, 4321L, endpoints);
assertEquals(11, canvas.getCells().size());
}
}
@@ -0,0 +1,107 @@
package art.arcane.iris.engine.platform;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.io.ReactiveFolder;
import org.bukkit.generator.BlockPopulator;
import org.junit.Test;
import java.lang.reflect.Field;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
public class BukkitChunkGeneratorCloseTest {
@Test
public void asynchronousCloseFailureCanRetryWithoutReopeningGeneration() throws Exception {
CloseFixture fixture = new CloseFixture();
IllegalStateException failure = new IllegalStateException("Planner remains active");
doThrow(failure).doNothing().when(fixture.engine).close();
CompletableFuture<Void> firstOperation = fixture.queueOperation();
CompletableFuture<Void> firstClose = fixture.generator.closeAsync();
assertSame(firstClose, fixture.generator.closeAsync());
firstOperation.completeExceptionally(assertThrows(IllegalStateException.class, fixture.operation.get()::run));
assertTrue(firstClose.isCompletedExceptionally());
assertTrue(fixture.generator.isClosing());
CompletableFuture<Void> secondOperation = fixture.queueOperation();
CompletableFuture<Void> secondClose = fixture.generator.closeAsync();
assertNotSame(firstClose, secondClose);
fixture.operation.get().run();
secondOperation.complete(null);
secondClose.join();
assertTrue(fixture.generator.isClosing());
assertSame(secondClose, fixture.generator.closeAsync());
verify(fixture.engine, times(2)).close();
}
@Test
public void failedRetryDispatchKeepsPartiallyClosedGenerationSealed() throws Exception {
CloseFixture fixture = new CloseFixture();
CompletableFuture<Void> operation = fixture.queueOperation();
CompletableFuture<Void> firstClose = fixture.generator.closeAsync();
operation.completeExceptionally(new IllegalStateException("Planner remains active"));
assertTrue(firstClose.isCompletedExceptionally());
doThrow(new IllegalStateException("Scheduler unavailable")).when(fixture.generator)
.withExclusiveControlFuture(any(Runnable.class));
CompletableFuture<Void> retry = fixture.generator.closeAsync();
assertNotSame(firstClose, retry);
assertTrue(retry.isCompletedExceptionally());
assertTrue(fixture.generator.isClosing());
}
@Test
public void initialDispatchFailureLeavesTheUnchangedGeneratorAvailable() throws Exception {
CloseFixture fixture = new CloseFixture();
doThrow(new IllegalStateException("Scheduler unavailable")).when(fixture.generator)
.withExclusiveControlFuture(any(Runnable.class));
CompletableFuture<Void> failedClose = fixture.generator.closeAsync();
assertTrue(failedClose.isCompletedExceptionally());
assertFalse(fixture.generator.isClosing());
}
private static void setField(BukkitChunkGenerator generator, String name, Object value) throws Exception {
Field field = BukkitChunkGenerator.class.getDeclaredField(name);
field.setAccessible(true);
field.set(generator, value);
}
private static final class CloseFixture {
private final BukkitChunkGenerator generator = mock(BukkitChunkGenerator.class, CALLS_REAL_METHODS);
private final Engine engine = mock(Engine.class);
private final AtomicReference<Runnable> operation = new AtomicReference<>();
private final ConcurrentLinkedQueue<CompletableFuture<Void>> operations = new ConcurrentLinkedQueue<>();
private CloseFixture() throws Exception {
setField(generator, "closeFuture", new AtomicReference<CompletableFuture<Void>>());
setField(generator, "engine", engine);
setField(generator, "folder", mock(ReactiveFolder.class));
setField(generator, "populators", new KList<BlockPopulator>());
doAnswer(invocation -> {
operation.set(invocation.getArgument(0, Runnable.class));
return operations.remove();
}).when(generator).withExclusiveControlFuture(any(Runnable.class));
}
private CompletableFuture<Void> queueOperation() {
CompletableFuture<Void> future = new CompletableFuture<>();
operations.add(future);
return future;
}
}
}
@@ -18,6 +18,7 @@
package art.arcane.iris.probe;
import art.arcane.iris.engine.decorator.DecoratorPlatformHooks;
import art.arcane.iris.engine.object.BlockDataMergeSupport;
import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.engine.object.TileData;
@@ -57,6 +58,8 @@ public final class StubPlatform implements IrisPlatform {
}
public static void bindGenerationStateHandlers() {
DecoratorPlatformHooks.bind((state, hunk, rX, rZ, x, y, z, mantle) -> state,
(surface, upward) -> surface.isOccluding());
IrisObjectRotation.bindPlatformRotator(StubPlatform::rotateState);
BlockDataMergeSupport.bindPlatformMerger(StubPlatform::mergeStates);
TileData.bindPlatformReader(StubTileData::read);
@@ -1,8 +1,10 @@
package art.arcane.iris.probe;
import art.arcane.iris.engine.decorator.IrisSpeleothems;
import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.hunk.Hunk;
import org.junit.BeforeClass;
import org.junit.Test;
@@ -80,6 +82,18 @@ public final class StubPlatformStateTest {
assertFalse(state("minecraft:brain_coral_fan").isWaterLogged());
}
@Test
public void resolvesSpikeSupportWithoutServerClasses() {
Hunk<PlatformBlockState> blocks = Hunk.newArrayHunk(1, 3, 1);
PlatformBlockState spike = state("minecraft:pointed_dripstone[vertical_direction=up,thickness=tip]");
blocks.set(0, 0, 0, state("minecraft:stone"));
assertTrue(IrisSpeleothems.isSupported(spike, blocks, 0, 0, 1));
blocks.set(0, 0, 0, state("minecraft:water[level=0]"));
assertFalse(IrisSpeleothems.isSupported(spike, blocks, 0, 0, 1));
blocks.set(0, 0, 0, state("minecraft:air"));
assertFalse(IrisSpeleothems.isSupported(spike, blocks, 0, 0, 1));
}
private PlatformBlockState state(String key) {
return IrisPlatforms.get().registries().block(key);
}