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 committed 2026-09-06 04:52:13 -04:00
commit 04471678b2
90 files changed
+4602 -1067

No files matched your search

+2
View File
@@ -74,3 +74,5 @@ BUILDS/
# Version-content compat lane worktree. Generated, never source. # Version-content compat lane worktree. Generated, never source.
.compatwt/ .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.GenerationEpoch;
import art.arcane.iris.engine.history.GenerationEpochContractFactory; import art.arcane.iris.engine.history.GenerationEpochContractFactory;
import art.arcane.iris.engine.history.GenerationHistory; 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.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.history.IrisBoundarySignatureSampler; import art.arcane.iris.engine.history.IrisBoundarySignatureSampler;
import art.arcane.iris.engine.history.TransitionGenerationPlan; import art.arcane.iris.engine.history.TransitionGenerationPlan;
@@ -191,44 +192,46 @@ public final class ModdedWorldEngines {
ModdedGenerationHistoryStorage.ActivePack opened ModdedGenerationHistoryStorage.ActivePack opened
) throws IOException { ) throws IOException {
GenerationHistory history = opened.history(); GenerationHistory history = opened.history();
history.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()); try (GenerationAdmission.RuntimeLease startupAdmission = history.retainRuntime()) {
opened = ModdedGenerationHistoryStorage.resolveActive(history); history.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
long openedActivationId = history.activeActivation().activationId(); opened = ModdedGenerationHistoryStorage.resolveActive(history);
IrisEngine candidate = buildEngine(level, seed, opened); long openedActivationId = history.activeActivation().activationId();
boolean ready = false; IrisEngine candidate = buildEngine(level, seed, opened);
try { boolean ready = false;
GenerationHistoryRuntimeRouter router = candidate.attachGenerationHistory( try {
history, GenerationHistoryRuntimeRouter router = candidate.attachGenerationHistory(
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,
history, history,
IrisBoundarySignatureSampler.INSTANCE IrisBoundarySignatureSampler.INSTANCE,
IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()
); );
router.preloadActiveRuntimes(); router.preloadActiveRuntimes();
} if (history.activeActivation().activationId() != openedActivationId) {
requireReady(candidate, level, ModdedGenerationMode.PERSISTENT_RESTORE); close(candidate);
ready = true; ModdedGenerationHistoryStorage.ActivePack promoted =
ModdedIrisLog.info("Iris engine up for {}: pack={} dim={} seed={} height={}..{} activation={}", ModdedGenerationHistoryStorage.resolveActive(history);
level.dimension().identifier(), candidate = buildEngine(level, seed, promoted);
candidate.getData().getDataFolder().getAbsolutePath(), router = GenerationHistoryRuntimeRouter.attach(
candidate.getDimension().getLoadKey(), candidate,
seed, history,
candidate.getDimension().getMinHeight(), IrisBoundarySignatureSampler.INSTANCE
candidate.getDimension().getMaxHeight(), );
history.activeActivation().activationId()); router.preloadActiveRuntimes();
return candidate; }
} finally { requireReady(candidate, level, ModdedGenerationMode.PERSISTENT_RESTORE);
if (!ready) { ready = true;
close(candidate); 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 art.arcane.volmlib.util.json.JSONObject;
import java.io.File; import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; 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 { final class PackBiomeLayerValidator {
private static final int DEFAULT_LAYER_COUNT = 1;
private PackBiomeLayerValidator() { private PackBiomeLayerValidator() {
} }
static List<String> validateCeilingLayerCounts(File biomesFolder) { static List<String> validateLayers(File biomesFolder) {
List<String> blockingErrors = new ArrayList<>(); List<String> blockingErrors = new ArrayList<>();
if (biomesFolder == null || !biomesFolder.isDirectory()) { if (biomesFolder == null || !biomesFolder.isDirectory()) {
return blockingErrors; return blockingErrors;
@@ -49,24 +40,13 @@ final class PackBiomeLayerValidator {
biomeFiles.sort(Comparator.comparing(File::getPath)); biomeFiles.sort(Comparator.comparing(File::getPath));
for (File biomeFile : biomeFiles) { for (File biomeFile : biomeFiles) {
String biomeKey = PackValidationIo.deriveKey(biomesFolder, biomeFile); String biomeKey = PackValidationIo.deriveKey(biomesFolder, biomeFile);
JSONObject biome; JSONObject biome = PackValidationIo.readJson(biomeFile);
try { if (biome == null) {
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.
continue; continue;
} }
Integer layers = arrayLength(biome, "layers", biomeKey, DEFAULT_LAYER_COUNT, blockingErrors); validateLayerArray(biome, "layers", biomeKey, blockingErrors);
Integer ceiling = arrayLength(biome, "caveCeilingLayers", biomeKey, 0, blockingErrors); validateLayerArray(biome, "caveCeilingLayers", biomeKey, 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.");
}
} }
return 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) { List<String> blockingErrors) {
if (!biome.has(field) || biome.isNull(field)) { if (!biome.has(field) || biome.isNull(field)) {
return defaultCount; return;
} }
JSONArray array = biome.optJSONArray(field); JSONArray array = biome.optJSONArray(field);
if (array == null) { if (array == null) {
blockingErrors.add("Biome '" + biomeKey + "' " + field + " must be an array."); 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"))); new File(packFolder, "spawners"), new File(packFolder, "entities")));
blockingErrors.addAll(PackSpawnValidator.validateCustomBiomeSpawns( blockingErrors.addAll(PackSpawnValidator.validateCustomBiomeSpawns(
new File(packFolder, "biomes"), PackSpawnValidator::resolveEntitySpawnCategory)); 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( blockingErrors.addAll(PackBiomeLayerValidator.validateDecoratorPalettes(
new File(packFolder, "biomes"), new File(packFolder, "snippet/decorator"))); new File(packFolder, "biomes"), new File(packFolder, "snippet/decorator")));
PackStyledRangeDefaultValidator.Validation styledRanges = PackStyledRangeDefaultValidator.validate(packFolder); PackStyledRangeDefaultValidator.Validation styledRanges = PackStyledRangeDefaultValidator.validate(packFolder);
@@ -68,11 +68,12 @@ final class EngineHotloader {
engine.lifecycleState = LifecycleState.HOTLOADING; engine.lifecycleState = LifecycleState.HOTLOADING;
EngineRuntime previous = engine.runtime; EngineRuntime previous = engine.runtime;
GenerationRuntime previousGeneration = previous.generation(); GenerationRuntime previousGeneration = previous.generation();
IrisComplex nextComplex = null; RuntimeAssembly assembly = null;
boolean published = false;
try { try {
engine.sealForTransition("complex hotload", false); engine.sealForTransition("complex hotload", false);
engine.prepareRuntimeHotload(); engine.prepareRuntimeHotload();
RuntimeAssembly assembly = new RuntimeAssembly( assembly = new RuntimeAssembly(
RuntimeAssembly.nextRuntimeId(), RuntimeAssembly.nextRuntimeId(),
previousGeneration.target(), previousGeneration.target(),
previousGeneration.mantleStorageDirectory(), previousGeneration.mantleStorageDirectory(),
@@ -82,7 +83,6 @@ final class EngineHotloader {
EngineRuntime next; EngineRuntime next;
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) { try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
assembly.complex = assembly.runtimeKernel.createComplex(engine, assembly.transitionPlan); assembly.complex = assembly.runtimeKernel.createComplex(engine, assembly.transitionPlan);
nextComplex = assembly.complex;
assembly.dimensionStackContext = assembly.runtimeKernel.createDimensionStackContext(engine); assembly.dimensionStackContext = assembly.runtimeKernel.createDimensionStackContext(engine);
assembly.upperContext = assembly.runtimeKernel.createUpperContext(engine); assembly.upperContext = assembly.runtimeKernel.createUpperContext(engine);
BiomeMaxes biomeMaxes = engine.runtimeBuilder.computeBiomeMaxes(); BiomeMaxes biomeMaxes = engine.runtimeBuilder.computeBiomeMaxes();
@@ -98,25 +98,30 @@ final class EngineHotloader {
} }
Throwable retirementFailure = runCleanup(null, previousGeneration.complex()::close); Throwable retirementFailure = runCleanup(null, previousGeneration.complex()::close);
if (retirementFailure != null) { if (retirementFailure != null) {
retirementFailure = runCleanup(retirementFailure, nextComplex::close);
nextComplex = null;
engine.lifecycleState = LifecycleState.FAILED; engine.lifecycleState = LifecycleState.FAILED;
throw new IllegalStateException("Failed to retire the previous Iris biome complex.", retirementFailure); throw new IllegalStateException("Failed to retire the previous Iris biome complex.", retirementFailure);
} }
engine.runtime = next; engine.runtime = next;
published = true;
engine.getGenerationSessions().activateNextSession(); engine.getGenerationSessions().activateNextSession();
engine.lifecycleState = LifecycleState.RUNNING; engine.lifecycleState = LifecycleState.RUNNING;
engine.getClosing().set(false); engine.getClosing().set(false);
engine.backgroundTasks.openBackgroundTaskAdmission(); engine.backgroundTasks.openBackgroundTaskAdmission();
} catch (Throwable e) { } catch (Throwable e) {
if (nextComplex != null && nextComplex != previousGeneration.complex()) { if (published) {
Throwable cleanupFailure = runCleanup(null, nextComplex::close); engine.lifecycleState = LifecycleState.FAILED;
if (cleanupFailure != null) { engine.getClosing().set(true);
e.addSuppressed(cleanupFailure); 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); throw new IllegalStateException("Failed to rebuild the Iris biome complex.", e);
} }
@@ -175,7 +180,7 @@ final class EngineHotloader {
broadcastStudioHotload(false, ""); broadcastStudioHotload(false, "");
} catch (Throwable e) { } catch (Throwable e) {
if (!published) { if (!published) {
if (replacementData != null) { if (replacementData != null && !engine.shutdownSequence.retainsData(replacementData)) {
replacementData.unregisterEngine(engine); replacementData.unregisterEngine(engine);
Throwable replacementDataFailure = runCleanup(null, replacementData::close); Throwable replacementDataFailure = runCleanup(null, replacementData::close);
if (replacementDataFailure != null) { if (replacementDataFailure != null) {
@@ -209,7 +209,12 @@ final class EngineRuntimeBuilder {
next.generation().mantle(), next.generation().mantle(),
null); null);
if (retirementFailure != 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; engine.lifecycleState = LifecycleState.FAILED;
throw new IllegalStateException("Failed to retire the previous Iris engine runtime.", retirementFailure); throw new IllegalStateException("Failed to retire the previous Iris engine runtime.", retirementFailure);
} }
@@ -229,6 +234,7 @@ final class EngineRuntimeBuilder {
engine.getClosing().set(true); engine.getClosing().set(true);
engine.backgroundTasks.closeBackgroundTaskAdmission(); engine.backgroundTasks.closeBackgroundTaskAdmission();
engine.lifecycleState = LifecycleState.FAILED; engine.lifecycleState = LifecycleState.FAILED;
Throwable cleanupFailure = null;
try { try {
engine.getGenerationSessions().sealAndAwait( engine.getGenerationSessions().sealAndAwait(
"failed world manager start", "failed world manager start",
@@ -236,11 +242,17 @@ final class EngineRuntimeBuilder {
true true
); );
} catch (Throwable drainFailure) { } catch (Throwable drainFailure) {
e.addSuppressed(drainFailure); cleanupFailure = drainFailure;
} }
engine.shutdownSequence.closeRuntime(next, e); if (cleanupFailure == null) {
if (engine.runtime == next) { cleanupFailure = engine.shutdownSequence.closeRuntime(next, null);
engine.runtime = 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); throw new IllegalStateException("Failed to start the Iris world manager.", e);
} }
@@ -18,6 +18,7 @@
package art.arcane.iris.engine; package art.arcane.iris.engine;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.EngineBackgroundTasks.BackgroundTaskDrain; import art.arcane.iris.engine.EngineBackgroundTasks.BackgroundTaskDrain;
import art.arcane.iris.engine.EngineRuntimeBuilder.RuntimeAssembly; import art.arcane.iris.engine.EngineRuntimeBuilder.RuntimeAssembly;
import art.arcane.iris.engine.IrisEngine.LifecycleState; 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.NativeStructureOwnershipStore;
import art.arcane.iris.engine.framework.PreservationRegistry; import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.engine.mantle.EngineMantle; 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.IrisLogging;
import art.arcane.iris.spi.IrisServices; 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 * 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 * 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 static final long CLOSE_RETRY_DRAIN_TIMEOUT_MILLIS = 5000L;
private final IrisEngine engine; 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 runtimeReleased;
private boolean targetReleased; private boolean targetReleased;
private boolean engineDataReleased; private boolean engineDataReleased;
@@ -47,6 +64,10 @@ final class EngineShutdownSequence {
this.engine = engine; this.engine = engine;
} }
void retainGenerationHistory(GenerationHistory history) {
generationAdmissions.add(history.retainRuntime());
}
void close() { void close() {
if (!engine.beginShutdown()) { if (!engine.beginShutdown()) {
return; return;
@@ -150,9 +171,12 @@ final class EngineShutdownSequence {
&& targetReleased && targetReleased
&& engineDataReleased && engineDataReleased
&& preservationReleased) { && preservationReleased) {
engine.closed = true; failure = releaseGenerationAdmissions(failure);
engine.lifecycleState = LifecycleState.CLOSED; if (failure == null) {
IrisLogging.debug("Engine Fully Shutdown!"); engine.closed = true;
engine.lifecycleState = LifecycleState.CLOSED;
IrisLogging.debug("Engine Fully Shutdown!");
}
} }
} }
if (failure != null) { 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) { private void reportIncompleteClose(Throwable failure) {
IrisLogging.error("Iris engine shutdown remains incomplete after cleanup failures for " + engine.getWorld().name() + "."); IrisLogging.error("Iris engine shutdown remains incomplete after cleanup failures for " + engine.getWorld().name() + ".");
IrisLogging.reportError(failure); IrisLogging.reportError(failure);
@@ -172,17 +209,21 @@ final class EngineShutdownSequence {
EngineTickRegistry.unregisterTicking(engine); EngineTickRegistry.unregisterTicking(engine);
engine.lifecycleState = LifecycleState.FAILED; engine.lifecycleState = LifecycleState.FAILED;
Throwable cleanupFailure = null; Throwable cleanupFailure = null;
boolean sessionsDrained = true;
try { try {
engine.getGenerationSessions().sealAndAwait("failed initialization", 0L, true); engine.getGenerationSessions().sealAndAwait("failed initialization", 0L, true);
} catch (Throwable e) { } catch (Throwable e) {
sessionsDrained = false;
cleanupFailure = appendFailure(cleanupFailure, e); cleanupFailure = appendFailure(cleanupFailure, e);
} }
engine.backgroundTasks.cancelBackgroundTasks("failed initialization"); engine.backgroundTasks.cancelBackgroundTasks("failed initialization");
BackgroundTaskDrain backgroundDrain = engine.backgroundTasks.drainBackgroundTasks("failed initialization"); BackgroundTaskDrain backgroundDrain = engine.backgroundTasks.drainBackgroundTasks("failed initialization");
cleanupFailure = appendFailure(cleanupFailure, backgroundDrain.failure()); cleanupFailure = appendFailure(cleanupFailure, backgroundDrain.failure());
if (!backgroundDrain.allowsResourceRelease()) { if (!sessionsDrained || !backgroundDrain.allowsResourceRelease()) {
cleanupFailure = appendFailure(cleanupFailure, if (!backgroundDrain.allowsResourceRelease()) {
new IllegalStateException("Iris background tasks remain active after failed initialization.")); cleanupFailure = appendFailure(cleanupFailure,
new IllegalStateException("Iris background tasks remain active after failed initialization."));
}
if (cleanupFailure != original) { if (cleanupFailure != original) {
original.addSuppressed(cleanupFailure); original.addSuppressed(cleanupFailure);
} }
@@ -192,18 +233,22 @@ final class EngineShutdownSequence {
() -> NativeStructureOwnershipStore.close(engine)); () -> NativeStructureOwnershipStore.close(engine));
cleanupFailure = appendFailure(cleanupFailure, ownershipFailure); cleanupFailure = appendFailure(cleanupFailure, ownershipFailure);
if (ownershipFailure == null) { if (ownershipFailure == null) {
cleanupFailure = closeDetachedGenerationRuntimes(cleanupFailure); cleanupFailure = releaseRuntime(cleanupFailure);
cleanupFailure = closeRuntime(engine.runtime, cleanupFailure); if (runtimeReleased) {
engine.runtime = null; cleanupFailure = releaseTarget(cleanupFailure);
cleanupFailure = runCleanup(cleanupFailure, engine.publishedTarget::close); }
cleanupFailure = runCleanup(cleanupFailure, engine.engineDataStore::releaseEngineData); if (targetReleased) {
engine.closed = true; cleanupFailure = releaseEngineDataForShutdown(cleanupFailure);
cleanupFailure = runCleanup(cleanupFailure, () -> { }
PreservationRegistry registry = IrisServices.getOrNull(PreservationRegistry.class); if (engineDataReleased) {
if (registry != null) { cleanupFailure = releasePreservation(cleanupFailure);
registry.dereference(); }
if (runtimeReleased && targetReleased && engineDataReleased && preservationReleased) {
cleanupFailure = releaseGenerationAdmissions(cleanupFailure);
if (generationAdmissions.isEmpty()) {
engine.closed = true;
} }
}); }
} }
if (cleanupFailure != null && cleanupFailure != original) { if (cleanupFailure != null && cleanupFailure != original) {
original.addSuppressed(cleanupFailure); original.addSuppressed(cleanupFailure);
@@ -214,37 +259,55 @@ final class EngineShutdownSequence {
if (assembly == null) { if (assembly == null) {
return failure; return failure;
} }
failure = runCleanup(failure, () -> { incompleteAssemblies.add(assembly);
if (assembly.worldManager != null) { RuntimeAssembly previous = engine == null ? null : engine.runtimeAssembly.get();
assembly.worldManager.close(); if (engine != null) {
} engine.runtimeAssembly.set(assembly);
});
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);
} }
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) { Throwable closeRuntime(EngineRuntime engineRuntime, Throwable failure) {
@@ -259,8 +322,11 @@ final class EngineShutdownSequence {
if (engineRuntime == null) { if (engineRuntime == null) {
return failure; return failure;
} }
failure = runCleanup(failure, engineRuntime.worldManager()::close); Throwable serviceFailure = runCleanup(null, engineRuntime.worldManager()::close);
failure = runCleanup(failure, engineRuntime.effects()::close); serviceFailure = runCleanup(serviceFailure, engineRuntime.effects()::close);
if (serviceFailure != null) {
return appendFailure(failure, serviceFailure);
}
return closeGenerationRuntime(engineRuntime.generation(), retainedMantle, failure); return closeGenerationRuntime(engineRuntime.generation(), retainedMantle, failure);
} }
@@ -276,12 +342,18 @@ final class EngineShutdownSequence {
if (generationRuntime == null) { if (generationRuntime == null) {
return failure; return failure;
} }
failure = runCleanup(failure, generationRuntime.mode()::close); Throwable modeFailure = runCleanup(null, generationRuntime.mode()::close);
failure = runCleanup(failure, generationRuntime.complex()::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)); failure = runCleanup(failure, () -> generationRuntime.hash32().cancel(true));
if (generationRuntime.mantle() != retainedMantle) { if (generationRuntime.mantle() != retainedMantle) {
failure = runCleanup(failure, generationRuntime.mantle()::saveAllNow); failure = appendFailure(failure, closeMantle(generationRuntime.mantle()));
failure = runCleanup(failure, generationRuntime.mantle()::close);
} }
return failure; return failure;
} }
@@ -290,15 +362,31 @@ final class EngineShutdownSequence {
IrisEngine.GenerationRuntimeBinding binding = new IrisEngine.GenerationRuntimeBinding( IrisEngine.GenerationRuntimeBinding binding = new IrisEngine.GenerationRuntimeBinding(
engine, engine,
generationRuntime); generationRuntime);
Throwable runtimeFailure;
try (IrisEngine.GenerationRuntimeScope ignored = engine.generationRuntimeScopes.open(binding)) { try (IrisEngine.GenerationRuntimeScope ignored = engine.generationRuntimeScopes.open(binding)) {
failure = closeGenerationRuntime(generationRuntime, failure); runtimeFailure = closeGenerationRuntime(generationRuntime, null);
} catch (Throwable scopeFailure) { } catch (Throwable scopeFailure) {
failure = appendFailure(failure, scopeFailure); runtimeFailure = scopeFailure;
}
if (runtimeFailure != null) {
return appendFailure(failure, runtimeFailure);
} }
return closeDetachedTarget(generationRuntime.target(), failure); return closeDetachedTarget(generationRuntime.target(), failure);
} }
Throwable closeDetachedTarget(EngineTarget target, Throwable 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.getData().unregisterEngine(engine));
failure = runCleanup(failure, target::close); failure = runCleanup(failure, target::close);
failure = runCleanup(failure, target.getData()::close); failure = runCleanup(failure, target.getData()::close);
@@ -309,6 +397,10 @@ final class EngineShutdownSequence {
if (runtimeReleased) { if (runtimeReleased) {
return failure; return failure;
} }
Throwable incompleteFailure = closeIncompleteRuntimes();
if (incompleteFailure != null) {
return appendFailure(failure, incompleteFailure);
}
Throwable detachedFailure = closeDetachedGenerationRuntimes(null); Throwable detachedFailure = closeDetachedGenerationRuntimes(null);
if (detachedFailure != null) { if (detachedFailure != null) {
return appendFailure(failure, detachedFailure); return appendFailure(failure, detachedFailure);
@@ -322,6 +414,86 @@ final class EngineShutdownSequence {
return failure; 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) { private Throwable closeDetachedGenerationRuntimes(Throwable failure) {
GenerationRuntime[] detachedRuntimes = engine.detachedGenerationRuntimes.toArray(new GenerationRuntime[0]); GenerationRuntime[] detachedRuntimes = engine.detachedGenerationRuntimes.toArray(new GenerationRuntime[0]);
for (GenerationRuntime detached : detachedRuntimes) { 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.mantle.components.MantleHydrologyCaveVoxelView;
import art.arcane.iris.engine.object.InferredType; import art.arcane.iris.engine.object.InferredType;
import art.arcane.iris.engine.object.IrisBiome; 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.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator; import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.engine.object.IrisGenerator; import art.arcane.iris.engine.object.IrisGenerator;
@@ -1708,74 +1709,25 @@ public class IrisComplex implements DataProvider {
} }
static final class ChildSelectionPlan { static final class ChildSelectionPlan {
private final IrisBiome[] mappedBiomes; private final IrisRaritySelection<IrisBiome> selection;
private final int maxIndex;
private ChildSelectionPlan(IrisBiome[] mappedBiomes) { private ChildSelectionPlan(IrisRaritySelection<IrisBiome> selection) {
this.mappedBiomes = mappedBiomes; this.selection = selection;
this.maxIndex = mappedBiomes.length - 1;
} }
static ChildSelectionPlan create(KList<IrisBiome> options) { static ChildSelectionPlan create(KList<IrisBiome> options) {
if (options.isEmpty()) { return new ChildSelectionPlan(IrisRaritySelection.create(options));
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);
} }
IrisBiome select(CNG childCell, double x, double z) { IrisBiome select(CNG childCell, double x, double z) {
if (mappedBiomes.length == 0) { long maximumIndex = selection.size() - 1L;
return null; if (maximumIndex <= 0L) {
return selection.get(0L);
} }
if (maximumIndex <= Integer.MAX_VALUE) {
if (mappedBiomes.length == 1) { return selection.get(childCell.fit2D(0, (int) maximumIndex, x, z));
return mappedBiomes[0];
} }
return selection.select(childCell.noiseFast2D(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];
} }
} }
@@ -1783,9 +1735,9 @@ public class IrisComplex implements DataProvider {
} }
public void close() { public void close() {
resolvedTerrain.clear();
if (hydrologyRuntime != null) { if (hydrologyRuntime != null) {
hydrologyRuntime.close(); hydrologyRuntime.close();
} }
resolvedTerrain.clear();
} }
} }
@@ -1010,6 +1010,7 @@ public class IrisEngine implements Engine {
if (generationHistoryRuntimeRouter != null) { if (generationHistoryRuntimeRouter != null) {
throw new IllegalStateException("Iris engine already has a generation-history runtime router."); throw new IllegalStateException("Iris engine already has a generation-history runtime router.");
} }
shutdownSequence.retainGenerationHistory(required.history());
generationHistoryRuntimeRouter = required; generationHistoryRuntimeRouter = required;
generationHistoryRoutingRequired = true; generationHistoryRoutingRequired = true;
} }
@@ -277,26 +277,26 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
} }
if (!looperStopped) { if (!looperStopped) {
try { try {
looperStopped = true;
if (looper != null) { if (looper != null) {
looper.interrupt(); looper.interrupt();
joinQuietly(looper); awaitLooper(looper);
} }
looperStopped = true;
} catch (Throwable e) { } catch (Throwable e) {
failure = appendCloseFailure(failure, e); failure = appendCloseFailure(failure, e);
} }
} }
if (!cleanupServiceStopped) { if (!cleanupServiceStopped) {
try { try {
cleanupServiceStopped = true;
if (cleanupService != null) { if (cleanupService != null) {
for (Future<?> future : cleanup.values()) { for (Future<?> future : cleanup.values()) {
future.cancel(false); future.cancel(false);
} }
cleanup.clear(); cleanup.clear();
cleanupService.shutdownNow(); cleanupService.shutdownNow();
awaitQuietly(cleanupService); awaitCleanupService(cleanupService);
} }
cleanupServiceStopped = true;
} catch (Throwable e) { } catch (Throwable e) {
failure = appendCloseFailure(failure, 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()) { if (thread == Thread.currentThread()) {
return; throw new IllegalStateException("Cannot finish world manager shutdown from its own loop thread.");
} }
try { try {
thread.join(CLOSE_AWAIT_MS); thread.join(CLOSE_AWAIT_MS);
if (thread.isAlive()) { 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) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); 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 { try {
if (!service.awaitTermination(CLOSE_AWAIT_MS, TimeUnit.MILLISECONDS)) { 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) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); 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.Matter;
import art.arcane.volmlib.util.matter.MatterBiomeInject; import art.arcane.volmlib.util.matter.MatterBiomeInject;
import art.arcane.volmlib.util.matter.slices.BiomeInjectMatter; import art.arcane.volmlib.util.matter.slices.BiomeInjectMatter;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import java.util.List; import java.util.List;
@@ -49,7 +48,6 @@ import java.util.Objects;
public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> { public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
private final RNG rng; private final RNG rng;
private final ChronoLatch cl = new ChronoLatch(5000);
private final KMap<String, ResolvedBiome> resolvedBiomes = new KMap<>(); private final KMap<String, ResolvedBiome> resolvedBiomes = new KMap<>();
private final KMap<String, ResolvedBiome> resolvedPhysicalBiomes = 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; import lombok.Getter;
public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBlockState> { 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 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 @Getter
private final RNG rng; private final RNG rng;
@Getter @Getter
@@ -83,11 +79,6 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
*/ */
@BlockCoordinates @BlockCoordinates
public void terrainSliver(int x, int z, int xf, Hunk<PlatformBlockState> h, ChunkContext context) { 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 chunkHeight = h.getHeight();
int chunkDepth = h.getDepth(); int chunkDepth = h.getDepth();
IrisDimension dimension = getDimension(); IrisDimension dimension = getDimension();
@@ -42,9 +42,7 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
return; return;
} }
GeometrySource source = new GeometrySource(blocks, biomes, getEngine().getMinHeight(), context); 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, 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)); && (!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) { IrisData irisData, int max) {
int stack = decorator.getHeight(rng, realX, realZ, irisData); int stack = decorator.getHeight(rng, realX, realZ, irisData);
if (decorator.isScaleStack()) { if (decorator.isScaleStack()) {
@@ -39,6 +39,10 @@ public class IrisSeaFloorDecorator extends IrisEngineDecorator {
@Override @Override
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, 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) { 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); RNG rng = getRNG(realX, realZ);
IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), 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))) { && !decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ))) {
return; return;
} }
if (height >= 0 && height < getEngine().getHeight()) { PlatformBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData());
data.set(x, height, z, decorator.getBlockData100(biome, rng, realX, height, realZ, getData())); if (block != null) {
data.set(x, height, z, block);
} }
return; return;
} }
int stack = decorator.getHeight(rng, realX, realZ, getData()); int stack = Math.min(limit - height,
if (decorator.isScaleStack()) { DecoratorCore.computeStack(decorator, rng, realX, realZ, getData(), max - height));
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();
for (int i = 0; i < stack; i++) { for (int i = 0; i < stack; i++) {
int h = height + i; int h = height + i;
if (h > max || h > engineHeight) { double threshold = stack == 1 ? 1.0 : ((double) i) / (stack - 1);
continue; PlatformBlockState block = threshold >= decorator.getTopThreshold()
}
double threshold = ((double) i) / (stack - 1);
data.set(x, h, z, threshold >= decorator.getTopThreshold()
? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData()) ? 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 @Override
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, 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) { 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); RNG rng = getRNG(realX, realZ);
IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), 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()) { if (!decorator.isStacking()) {
int targetY = height + 1; int targetY = height + 1;
if (height >= 0 && targetY < getEngine().getHeight() if (DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) {
&& DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) {
PlatformBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData()); PlatformBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData());
if (block != null) { if (block != null) {
data.set(x, targetY, z, block); data.set(x, targetY, z, block);
@@ -58,33 +61,15 @@ public class IrisSeaSurfaceDecorator extends IrisEngineDecorator {
return; return;
} }
int stack = decorator.getHeight(rng, realX, realZ, getData()); int stack = Math.min(maxBaseY - height,
if (decorator.isScaleStack()) { DecoratorCore.computeStack(decorator, rng, realX, realZ, getData(), max - height));
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();
for (int i = 0; i < stack; i++) { for (int i = 0; i < stack; i++) {
int h = height + i; int h = height + i;
int targetY = h + 1; int targetY = h + 1;
if (h >= max || targetY >= engineHeight if (!DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) {
|| !DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) {
break; break;
} }
double threshold = ((double) i) / (stack - 1); double threshold = stack == 1 ? 1.0 : ((double) i) / (stack - 1);
PlatformBlockState block = threshold >= decorator.getTopThreshold() PlatformBlockState block = threshold >= decorator.getTopThreshold()
? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData()) ? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData())
: decorator.getBlockData100(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; return;
} }
closed = true;
dump(); dump();
terrainStages.forEach(EngineStage::close); terrainStages.forEach(EngineStage::close);
terrainStages.clear(); terrainStages.clear();
transitionStage.close(); transitionStage.close();
closed = true;
} }
@Override @Override
@@ -16,7 +16,12 @@ public final class GenerationAdmission {
Path root = Objects.requireNonNull(dimensionRoot, "dimensionRoot") Path root = Objects.requireNonNull(dimensionRoot, "dimensionRoot")
.toAbsolutePath() .toAbsolutePath()
.normalize(); .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() { public StageLease enterStage() {
@@ -34,6 +39,23 @@ public final class GenerationAdmission {
return new CutoverLease(state); 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 { public static final class StageLease implements AutoCloseable {
private final State state; private final State state;
private final AtomicBoolean closed; private final AtomicBoolean closed;
@@ -69,26 +91,71 @@ public final class GenerationAdmission {
} }
private static final class State { private static final class State {
private final Path root;
private final ReentrantLock lock; private final ReentrantLock lock;
private final Condition changed; private final Condition changed;
private int runtimeOwners;
private int waitingStages;
private int activeStages; private int activeStages;
private int waitingCutovers; private int waitingCutovers;
private boolean cutoverActive; private boolean cutoverActive;
private boolean generationAdmissionOpened; private boolean generationAdmissionOpened;
private boolean closed;
private State() { private State(Path root) {
this.root = root;
this.lock = new ReentrantLock(true); this.lock = new ReentrantLock(true);
this.changed = lock.newCondition(); 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() { private void enterStage() {
lock.lock(); lock.lock();
try { try {
requireOpen();
generationAdmissionOpened = true; generationAdmissionOpened = true;
while (cutoverActive || waitingCutovers > 0) { waitingStages++;
changed.awaitUninterruptibly(); try {
while (cutoverActive || waitingCutovers > 0) {
changed.awaitUninterruptibly();
requireOpen();
}
activeStages++;
} finally {
waitingStages--;
} }
activeStages++;
} finally { } finally {
lock.unlock(); lock.unlock();
} }
@@ -112,6 +179,7 @@ public final class GenerationAdmission {
private void beginCutover(boolean startupOnly) { private void beginCutover(boolean startupOnly) {
lock.lock(); lock.lock();
try { try {
requireOpen();
if (startupOnly && generationAdmissionOpened) { if (startupOnly && generationAdmissionOpened) {
throw new IllegalStateException( throw new IllegalStateException(
"Generation activation promotion is only allowed before generation admission opens." "Generation activation promotion is only allowed before generation admission opens."
@@ -121,6 +189,7 @@ public final class GenerationAdmission {
try { try {
while (cutoverActive || activeStages > 0) { while (cutoverActive || activeStages > 0) {
changed.awaitUninterruptibly(); changed.awaitUninterruptibly();
requireOpen();
} }
cutoverActive = true; cutoverActive = true;
} finally { } finally {
@@ -64,6 +64,10 @@ public final class GenerationHistory {
validateReferencedState(); validateReferencedState();
} }
public GenerationAdmission.RuntimeLease retainRuntime() {
return admission.retainRuntime();
}
public SavedBiomeStore savedBiomes() { public SavedBiomeStore savedBiomes() {
return savedBiomes; return savedBiomes;
} }
@@ -2646,7 +2646,7 @@ public final class HydrologyPlanner {
potential, potential,
parent, parent,
outletIndex, outletIndex,
routeLengths(grid, parent), routeLengths(parent, grid.width(), settings.routing()),
List.copyOf(outlets), List.copyOf(outlets),
false false
); );
@@ -3222,31 +3222,43 @@ public final class HydrologyPlanner {
return count + (HydrologyHash.unit(stable) < remainder ? 1 : 0); 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[] 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++) { 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; 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) { private int[] countContributions(List<Integer> sources, RoutingPlan routing) {
int[] counts = new int[routing.parent().length]; int[] counts = new int[routing.parent().length];
for (int source : sources) { for (int source : sources) {
@@ -16,6 +16,7 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@@ -131,6 +132,7 @@ public final class HydrologyTileCache implements AutoCloseable {
*/ */
public HydrologyTile get(HydrologyTileKey key) { public HydrologyTile get(HydrologyTileKey key) {
Objects.requireNonNull(key, "key"); Objects.requireNonNull(key, "key");
requireOpen();
if (prefetchExecutor == null) { if (prefetchExecutor == null) {
return planInline(key, cacheEpoch.get()); return planInline(key, cacheEpoch.get());
} }
@@ -156,7 +158,11 @@ public final class HydrologyTileCache implements AutoCloseable {
} }
CacheLoadKey<HydrologyTileKey> loadKey = new CacheLoadKey<>(epoch, key); CacheLoadKey<HydrologyTileKey> loadKey = new CacheLoadKey<>(epoch, key);
PendingLoad<HydrologyTile> owned = new PendingLoad<>(Thread.currentThread(), new CompletableFuture<>()); 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) { if (existing != null) {
return awaitLoad(existing); return awaitLoad(existing);
} }
@@ -376,10 +382,45 @@ public final class HydrologyTileCache implements AutoCloseable {
@Override @Override
public void close() { 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(); 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) { private boolean validSharedTile(HydrologyTile tile, SharedTileKey sharedKey) {
SharedCacheScope scope = sharedKey.scope(); SharedCacheScope scope = sharedKey.scope();
return tile.key().equals(sharedKey.tileKey()) return tile.key().equals(sharedKey.tileKey())
@@ -389,6 +430,7 @@ public final class HydrologyTileCache implements AutoCloseable {
} }
private ChunkColumns chunkColumns(int blockX, int blockZ) { private ChunkColumns chunkColumns(int blockX, int blockZ) {
requireOpen();
int chunkX = Math.floorDiv(blockX, CHUNK_SIZE); int chunkX = Math.floorDiv(blockX, CHUNK_SIZE);
int chunkZ = Math.floorDiv(blockZ, CHUNK_SIZE); int chunkZ = Math.floorDiv(blockZ, CHUNK_SIZE);
long epoch = cacheEpoch.get(); long epoch = cacheEpoch.get();
@@ -420,7 +462,11 @@ public final class HydrologyTileCache implements AutoCloseable {
return present; return present;
} }
PendingLoad<ChunkColumns> owned = new PendingLoad<>(Thread.currentThread(), new CompletableFuture<>()); 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) { if (existing != null) {
return awaitLoad(existing); return awaitLoad(existing);
} }
@@ -652,6 +698,7 @@ public final class HydrologyTileCache implements AutoCloseable {
*/ */
public List<HydrologyTile> tiles(List<HydrologyTileKey> keys) { public List<HydrologyTile> tiles(List<HydrologyTileKey> keys) {
Objects.requireNonNull(keys, "keys"); Objects.requireNonNull(keys, "keys");
requireOpen();
HydrologyTile[] loaded = new HydrologyTile[keys.size()]; HydrologyTile[] loaded = new HydrologyTile[keys.size()];
boolean missing = false; boolean missing = false;
for (int index = 0; index < loaded.length; index++) { for (int index = 0; index < loaded.length; index++) {
@@ -724,17 +771,29 @@ public final class HydrologyTileCache implements AutoCloseable {
return CompletableFuture.completedFuture(present); return CompletableFuture.completedFuture(present);
} }
CompletableFuture<HydrologyTile> future = new CompletableFuture<>(); 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) { if (existing != null) {
return existing; return existing;
} }
try { try {
prefetchExecutor.execute(() -> { prefetchExecutor.execute(() -> {
if (future.isDone()) {
planning.remove(loadKey, future);
return;
}
try { try {
future.complete(planInline(key, loadKey.epoch())); future.complete(planInline(key, loadKey.epoch()));
} catch (Throwable failure) { } catch (Throwable failure) {
future.completeExceptionally(failure); future.completeExceptionally(failure);
IrisLogging.reportError(failure); if (!(failure instanceof CancellationException) || !closed.get()) {
IrisLogging.reportError(failure);
}
} finally { } finally {
planning.remove(loadKey, future); planning.remove(loadKey, future);
} }
@@ -3,46 +3,48 @@ package art.arcane.iris.engine.hydrology;
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.TreeMap;
public final class RiverFootprint { public final class RiverFootprint {
private static final RiverFootprint EMPTY = new RiverFootprint(Map.of());
private final Long2ObjectLinkedOpenHashMap<HydrologyColumnSample> indexed;
private final Map<Long, HydrologyColumnSample> columns; private final Map<Long, HydrologyColumnSample> columns;
public RiverFootprint(Map<Long, HydrologyColumnSample> columns) { public RiverFootprint(Map<Long, HydrologyColumnSample> columns) {
if (columns == null) { if (columns == null) {
throw new IllegalArgumentException("columns must not be 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()) { for (Map.Entry<Long, HydrologyColumnSample> entry : columns.entrySet()) {
HydrologyColumnSample sample = entry.getValue(); HydrologyColumnSample sample = entry.getValue();
if (sample == null || entry.getKey() != pack(sample.x(), sample.z())) { if (sample == null || entry.getKey() != pack(sample.x(), sample.z())) {
throw new IllegalArgumentException("Footprint keys must match their column coordinates."); throw new IllegalArgumentException("Footprint keys must match their column coordinates.");
} }
ordered.put(entry.getKey(), sample); indexed.put(entry.getKey().longValue(), sample);
} }
Long2ObjectLinkedOpenHashMap<HydrologyColumnSample> indexed = long[] order = indexed.keySet().toLongArray();
new Long2ObjectLinkedOpenHashMap<>(ordered.size()); Arrays.sort(order);
for (Map.Entry<Long, HydrologyColumnSample> entry : ordered.entrySet()) { for (long key : order) {
indexed.put(entry.getKey().longValue(), entry.getValue()); indexed.getAndMoveToLast(key);
} }
columns = Collections.unmodifiableMap(indexed); this.columns = Collections.unmodifiableMap(indexed);
this.columns = columns;
} }
public static RiverFootprint empty() { public static RiverFootprint empty() {
return new RiverFootprint(Map.of()); return EMPTY;
} }
public Optional<HydrologyColumnSample> sample(int x, int z) { 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) { 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(); return sample == null ? new HydrologyRenderSample(x, z, List.of()) : sample.renderSample();
} }
@@ -61,7 +63,7 @@ public final class RiverFootprint {
if (area < columns.size()) { if (area < columns.size()) {
for (long z = minimumZ; z < maximumZ; z++) { for (long z = minimumZ; z < maximumZ; z++) {
for (long x = minimumX; x < maximumX; x++) { 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) { if (sample != null) {
selected.add(sample); selected.add(sample);
} }
@@ -9,7 +9,6 @@ import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap; import java.util.IdentityHashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
@@ -1132,8 +1131,8 @@ public final class HydrologyCaveContainmentPlanner {
List<CavePosition> throat List<CavePosition> throat
) { ) {
Queue<CavePosition> queue = new ArrayDeque<>(); Queue<CavePosition> queue = new ArrayDeque<>();
Set<CavePosition> queued = new HashSet<>(); Set<CavePosition> queued = new LinkedHashSet<>();
Set<CavePosition> resolved = new LinkedHashSet<>(); int visitedCount = 0;
queue.add(source.target()); queue.add(source.target());
queued.add(source.target()); queued.add(source.target());
@@ -1148,10 +1147,7 @@ public final class HydrologyCaveContainmentPlanner {
if (positionRejection != HydrologyCaveRejection.NONE) { if (positionRejection != HydrologyCaveRejection.NONE) {
return ComponentResult.rejected(positionRejection); return ComponentResult.rejected(positionRejection);
} }
if (!resolved.add(position)) { if (++visitedCount > settings.maxFloodVolume()) {
continue;
}
if (resolved.size() > settings.maxFloodVolume()) {
return ComponentResult.rejected(HydrologyCaveRejection.VOLUME_LIMIT); 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( 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.common.parallel.MultiBurst;
import art.arcane.iris.util.project.stream.ProceduralStream; import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.volmlib.util.math.RNG; 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.ArrayList;
import java.util.Collections; import java.util.Collections;
@@ -70,7 +72,6 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.IntConsumer; import java.util.function.IntConsumer;
import java.util.function.Predicate; import java.util.function.Predicate;
@@ -96,7 +97,9 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
private final Set<String> profileKeys; private final Set<String> profileKeys;
private final String defaultProfileKey; private final String defaultProfileKey;
private final Object terrainSampleLock; 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; private final LinkedHashMap<Long, HydrologyTerrainSample> terrainSamples;
public IrisHydrologyRuntime(IrisHydrologyRuntimeContext context) { public IrisHydrologyRuntime(IrisHydrologyRuntimeContext context) {
@@ -156,7 +159,7 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
if (!planned) { if (!planned) {
int tileSize = settings.routing().tileSize(); int tileSize = settings.routing().tileSize();
HydrologyTileKey key = new HydrologyTileKey(Math.floorDiv(blockX, tileSize), Math.floorDiv(blockZ, 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", IrisLogging.debug("Hydrology tile %d,%d queried before it was planned; natural terrain answers until the plan lands",
key.tileX(), key.tileZ()); key.tileX(), key.tileZ());
} }
@@ -479,6 +482,7 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
@Override @Override
public void close() { public void close() {
cache.close(); cache.close();
unplannedQueries.invalidateAll();
routingTerrainSampler.close(); routingTerrainSampler.close();
synchronized (terrainSampleLock) { synchronized (terrainSampleLock) {
terrainSamples.clear(); terrainSamples.clear();
@@ -91,11 +91,12 @@ public final class SurfaceFootprintCompiler {
&& column.x() == centerline.x()[0] && column.z() == centerline.z()[0]; && column.x() == centerline.x()[0] && column.z() == centerline.z()[0];
int y = column.role() == SurfaceRole.CHANNEL ? column.headY() : column.height(); 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); 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( columns.add(new SurfaceLayerColumn(
column.x(), column.x(),
column.z(), column.z(),
column.terrain(), column.terrain(),
pool ? poolLayer(feature, column, course.profileKey(), poolBiome) : layer(feature, column, course.profileKey()), layer(feature, column, course.profileKey(), biomeOverride),
column.role(), column.role(),
column.apron() column.apron()
)); ));
@@ -103,40 +104,6 @@ public final class SurfaceFootprintCompiler {
return new SurfaceFootprint(columns, field.uncontainedWetCells()); 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) { private ChannelProfile poolProfile(HydraulicSegment segment, SurfaceCenterline centerline) {
int count = centerline.size(); int count = centerline.size();
double[] halfWidth = new double[count]; double[] halfWidth = new double[count];
@@ -160,9 +127,15 @@ public final class SurfaceFootprintCompiler {
return null; 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(); HydrologyTerrainSample terrain = column.terrain();
if (column.apron()) { if (column.apron() && biomeOverride == null) {
return new HydrologyColumnLayer( return new HydrologyColumnLayer(
feature, feature,
column.headY(), column.headY(),
@@ -194,10 +167,10 @@ public final class SurfaceFootprintCompiler {
channel, channel,
false, false,
profileKey, profileKey,
terrain.surfaceBiomeKey(), biomeOverride == null ? terrain.surfaceBiomeKey() : biomeOverride,
terrain.mouthBiomeKey(), biomeOverride == null ? terrain.mouthBiomeKey() : biomeOverride,
terrain.shoreBiomeKey(), biomeOverride == null ? terrain.shoreBiomeKey() : biomeOverride,
terrain.bankBiomeKey(), biomeOverride == null ? terrain.bankBiomeKey() : biomeOverride,
terrain.floodedCaveBiomeKey() terrain.floodedCaveBiomeKey()
); );
} }
@@ -22,12 +22,13 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
public interface MatterGenerator { public interface MatterGenerator {
MultiBurst DISPATCHER = MultiBurst.burst; 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_POLL_MS = 1000L;
long COMPONENT_TASK_TIMEOUT_MS = Long.getLong("iris.mantle.componentTimeout", 120000L); long COMPONENT_TASK_TIMEOUT_MS = Long.getLong("iris.mantle.componentTimeout", 120000L);
@@ -368,38 +369,37 @@ public interface MatterGenerator {
) { ) {
MantleFlag flag = component.getFlag(); MantleFlag flag = component.getFlag();
MatterTaskKey key = new MatterTaskKey(getMantle(), chunkX, chunkZ, flag); MatterTaskKey key = new MatterTaskKey(getMantle(), chunkX, chunkZ, flag);
MatterComponentTask task = new MatterComponentTask(key);
if (chunk.isFlagged(flag)) { if (chunk.isFlagged(flag)) {
return new MatterComponentTask(key, CompletableFuture.completedFuture(null), null); task.finish(null);
return task;
} }
CompletableFuture<Void> future = new CompletableFuture<>(); MatterComponentTask existing = IN_FLIGHT_COMPONENTS.putIfAbsent(key, task);
CompletableFuture<Void> existing = IN_FLIGHT_COMPONENTS.putIfAbsent(key, future);
if (existing != null) { if (existing != null) {
return new MatterComponentTask(key, existing, null); return existing;
} }
try { try {
if (DISPATCHER.ownsCurrentThread()) { if (DISPATCHER.ownsCurrentThread()) {
// A pool thread runs its claim inline: a task queued behind pool threads that block // 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. // on unmanaged waits (structure builds, cache locks) could starve the whole pool.
completeComponentTask(future, key, chunk, component, writer, chunkX, chunkZ, context, IrisContext.get()); completeComponentTask(task, chunk, component, writer, chunkX, chunkZ, context, IrisContext.get());
return new MatterComponentTask(key, future, null); return task;
} }
IrisContext callerContext = IrisContext.get(); IrisContext callerContext = IrisContext.get();
Future<?> submission = DISPATCHER.submit(() -> completeComponentTask( task.setSubmission(DISPATCHER.submit(() -> completeComponentTask(
future, task,
key,
chunk, chunk,
component, component,
writer, writer,
chunkX, chunkX,
chunkZ, chunkZ,
context, context,
callerContext)); callerContext)));
return new MatterComponentTask(key, future, submission); return task;
} catch (Throwable throwable) { } catch (Throwable throwable) {
IN_FLIGHT_COMPONENTS.remove(key, future); task.cancel(throwable);
future.completeExceptionally(throwable);
throw throwable; throw throwable;
} }
} }
@@ -449,7 +449,7 @@ public interface MatterGenerator {
} }
private static void awaitComponentTask(MatterComponentTask task) { private static void awaitComponentTask(MatterComponentTask task) {
CompletableFuture<Void> future = task.future(); CompletableFuture<Void> future = task.future;
if (future.isDone() && !future.isCompletedExceptionally()) { if (future.isDone() && !future.isCompletedExceptionally()) {
return; return;
} }
@@ -459,11 +459,17 @@ public interface MatterGenerator {
try { try {
ForkJoinPool.managedBlock(new ComponentTaskBlocker(task)); ForkJoinPool.managedBlock(new ComponentTaskBlocker(task));
} catch (InterruptedException interruption) { } 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; return;
} }
awaitComponentTaskBlocking(task); task.await(COMPONENT_TASK_TIMEOUT_MS);
} }
/** /**
@@ -478,66 +484,22 @@ public interface MatterGenerator {
@Override @Override
public boolean block() { public boolean block() {
awaitComponentTaskBlocking(task); task.await(COMPONENT_TASK_TIMEOUT_MS);
return true; return true;
} }
@Override @Override
public boolean isReleasable() { public boolean isReleasable() {
return task.future().isDone(); if (!task.future.isDone()) {
} return false;
}
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();
} }
task.future.join();
return true;
} }
} }
private void completeComponentTask( private void completeComponentTask(
CompletableFuture<Void> future, MatterComponentTask task,
MatterTaskKey key,
MantleChunk<Matter> chunk, MantleChunk<Matter> chunk,
MantleComponent component, MantleComponent component,
MantleWriter writer, MantleWriter writer,
@@ -546,14 +508,17 @@ public interface MatterGenerator {
ChunkContext context, ChunkContext context,
IrisContext callerContext IrisContext callerContext
) { ) {
if (!task.start()) {
return;
}
Throwable failure = null;
try { try {
runComponentWithContext(chunk, component, writer, chunkX, chunkZ, context, callerContext); runComponentWithContext(chunk, component, writer, chunkX, chunkZ, context, callerContext);
future.complete(null);
} catch (Throwable throwable) { } catch (Throwable throwable) {
future.completeExceptionally(throwable); failure = throwable;
throw throwable; throw throwable;
} finally { } finally {
IN_FLIGHT_COMPONENTS.remove(key, future); task.finish(failure);
} }
} }
@@ -593,11 +558,98 @@ public interface MatterGenerator {
)); ));
} }
/** final class MatterComponentTask {
* A launched component task. {@code submission} is the dispatcher handle when this generation private final MatterTaskKey key;
* owns the task, and null when the future belongs to another generation or already completed. private final CompletableFuture<Void> future = new CompletableFuture<>();
*/ private volatile Future<?> submission;
record MatterComponentTask(MatterTaskKey key, CompletableFuture<Void> future, 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) { 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.MatterCavern;
import art.arcane.volmlib.util.matter.MatterSlice; import art.arcane.volmlib.util.matter.MatterSlice;
import java.util.Arrays;
final class CaveCarveScratch { final class CaveCarveScratch {
final int[] columnMaxY = new int[256]; final int[] columnMaxY = new int[256];
final int[] fluidMaxY = new int[256]; final int[] fluidMaxY = new int[256];
@@ -70,4 +72,9 @@ final class CaveCarveScratch {
int adaptiveGeometryStep = -1; int adaptiveGeometryStep = -1;
int adaptiveGeometryAxisCells = -1; int adaptiveGeometryAxisCells = -1;
boolean fullWeightsInitialized; boolean fullWeightsInitialized;
void releaseSections() {
Arrays.fill(sectionMatter, null);
Arrays.fill(sectionSlices, null);
}
} }
@@ -436,6 +436,7 @@ public class IrisCaveCarver3D {
return carved; return carved;
} finally { } finally {
scratchCache.get().releaseSections();
carving.set(nested); carving.set(nested);
if (nested) { if (nested) {
scratchCache.set(previousScratch); scratchCache.set(previousScratch);
@@ -2695,10 +2696,6 @@ public class IrisCaveCarver3D {
return weight; return weight;
} }
private double signed(double value) {
return (value * 2D) - 1D;
}
private double applySurfaceCeilingFade( private double applySurfaceCeilingFade(
CaveCarveScratch scratch, CaveCarveScratch scratch,
double threshold, double threshold,
@@ -19,6 +19,7 @@
package art.arcane.iris.engine.mantle.components; package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.engine.IrisComplex; import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.UpperDimensionContext; import art.arcane.iris.engine.UpperDimensionContext;
import art.arcane.iris.engine.mantle.MatterGenerationPhase; import art.arcane.iris.engine.mantle.MatterGenerationPhase;
import art.arcane.iris.engine.mantle.ComponentFlag; 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.context.ChunkContext;
import art.arcane.iris.util.project.stream.ProceduralStream; import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.iris.util.project.stream.utility.ChunkFillableDoubleStream2D; 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.iris.util.simd.SimdSupport;
import art.arcane.volmlib.util.documentation.ChunkCoordinates; import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag; 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 MIN_WEIGHT = 0.08D;
private static final double THRESHOLD_PENALTY = 0.24D; private static final double THRESHOLD_PENALTY = 0.24D;
private static final int MAX_BLENDED_PROFILE_PASSES = 2; 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_WIDTH = (BLEND_RADIUS * 2) + 1;
private static final int KERNEL_SIZE = KERNEL_WIDTH * KERNEL_WIDTH; private static final int KERNEL_SIZE = KERNEL_WIDTH * KERNEL_WIDTH;
private static final int[] KERNEL_DX = new int[KERNEL_SIZE]; private static final int[] KERNEL_DX = new int[KERNEL_SIZE];
@@ -107,8 +108,25 @@ public class MantleCarvingComponent extends IrisMantleComponent {
if (!complex.allowsMantleChunkWrite(x, z)) { if (!complex.allowsMantleChunkWrite(x, z)) {
return; 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(); IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State();
BlendScratch blendScratch = BLEND_SCRATCH.get();
int[] chunkSurfaceHeights = prepareChunkSurfaceHeights(x, z, context, blendScratch.chunkSurfaceHeights); int[] chunkSurfaceHeights = prepareChunkSurfaceHeights(x, z, context, blendScratch.chunkSurfaceHeights);
PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start(); PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start();
List<WeightedProfile> weightedProfiles = resolveWeightedProfiles(x, z, complex, resolverState); List<WeightedProfile> weightedProfiles = resolveWeightedProfiles(x, z, complex, resolverState);
@@ -217,12 +235,12 @@ public class MantleCarvingComponent extends IrisMantleComponent {
BlendScratch blendScratch = BLEND_SCRATCH.get(); BlendScratch blendScratch = BLEND_SCRATCH.get();
IrisCaveProfile[] profileField = blendScratch.profileField; IrisCaveProfile[] profileField = blendScratch.profileField;
Map<IrisCaveProfile, double[]> columnProfileWeights = blendScratch.columnProfileWeights; Map<IrisCaveProfile, double[]> columnProfileWeights = blendScratch.columnProfileWeights;
IdentityHashMap<IrisCaveProfile, Boolean> activeProfiles = blendScratch.activeProfiles;
List<IrisCaveProfile> profileOrder = blendScratch.profileOrder; List<IrisCaveProfile> profileOrder = blendScratch.profileOrder;
IrisCaveProfile[] kernelProfiles = blendScratch.kernelProfiles; IrisCaveProfile[] kernelProfiles = blendScratch.kernelProfiles;
double[] kernelProfileWeights = blendScratch.kernelProfileWeights; double[] kernelProfileWeights = blendScratch.kernelProfileWeights;
activeProfiles.clear(); columnProfileWeights.clear();
profileOrder.clear(); profileOrder.clear();
blendScratch.nextWeightBuffer = 0;
fillProfileField(profileField, chunkX, chunkZ, complex, resolverState, blendScratch); fillProfileField(profileField, chunkX, chunkZ, complex, resolverState, blendScratch);
for (int localX = 0; localX < CHUNK_SIZE; localX++) { for (int localX = 0; localX < CHUNK_SIZE; localX++) {
@@ -270,12 +288,8 @@ public class MantleCarvingComponent extends IrisMantleComponent {
double[] weights = columnProfileWeights.get(profile); double[] weights = columnProfileWeights.get(profile);
if (weights == null) { if (weights == null) {
weights = new double[CHUNK_AREA]; weights = blendScratch.acquireWeights();
columnProfileWeights.put(profile, weights); columnProfileWeights.put(profile, weights);
} else if (!activeProfiles.containsKey(profile)) {
Arrays.fill(weights, 0D);
}
if (activeProfiles.put(profile, Boolean.TRUE) == null) {
profileOrder.add(profile); profileOrder.add(profile);
} }
weights[columnIndex] = columnWeight; weights[columnIndex] = columnWeight;
@@ -316,28 +330,27 @@ public class MantleCarvingComponent extends IrisMantleComponent {
return weightedProfiles; return weightedProfiles;
} }
Map<IrisDimensionCarvingEntry, IrisDimensionCarvingEntry[]> dimensionColumnPlans = blendScratch.dimensionColumnPlans; Engine engine = getEngineMantle().getEngine();
dimensionColumnPlans.clear(); int baseX = PowerOfTwoCoordinates.chunkToBlock(chunkX);
int baseZ = PowerOfTwoCoordinates.chunkToBlock(chunkZ);
for (IrisDimensionCarvingEntry entry : entries) { for (IrisDimensionCarvingEntry entry : entries) {
if (entry == null || !entry.isEnabled()) { if (entry == null || !entry.isEnabled()) {
continue; continue;
} }
IrisBiome rootBiome = IrisDimensionCarvingResolver.resolveEntryBiome(getEngineMantle().getEngine(), entry, resolverState); IrisBiome rootBiome = IrisDimensionCarvingResolver.resolveEntryBiome(engine, entry, resolverState);
if (rootBiome == null) { if (rootBiome == null) {
continue; continue;
} }
IrisDimensionCarvingEntry[] columnPlan = dimensionColumnPlans.computeIfAbsent(entry, key -> new IrisDimensionCarvingEntry[CHUNK_AREA]);
buildDimensionColumnPlan(columnPlan, chunkX, chunkZ, entry, resolverState);
Map<IrisCaveProfile, double[]> rootProfileColumnWeights = new IdentityHashMap<>(); Map<IrisCaveProfile, double[]> rootProfileColumnWeights = new IdentityHashMap<>();
List<IrisCaveProfile> rootProfileOrder = new ArrayList<>(); List<IrisCaveProfile> rootProfileOrder = new ArrayList<>();
IrisRange worldYRange = entry.getWorldYRange(); IrisRange worldYRange = entry.getWorldYRange();
for (int columnIndex = 0; columnIndex < CHUNK_AREA; columnIndex++) { for (int columnIndex = 0; columnIndex < CHUNK_AREA; columnIndex++) {
IrisDimensionCarvingEntry resolvedEntry = columnPlan[columnIndex]; IrisDimensionCarvingEntry resolvedEntry = IrisDimensionCarvingResolver.resolveFromRoot(
IrisBiome resolvedBiome = IrisDimensionCarvingResolver.resolveEntryBiome(getEngineMantle().getEngine(), resolvedEntry, resolverState); engine, entry, baseX + (columnIndex >> 4), baseZ + (columnIndex & 15), resolverState);
IrisBiome resolvedBiome = IrisDimensionCarvingResolver.resolveEntryBiome(engine, resolvedEntry, resolverState);
if (resolvedBiome == null) { if (resolvedBiome == null) {
continue; continue;
} }
@@ -349,7 +362,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
double[] columnWeights = rootProfileColumnWeights.get(profile); double[] columnWeights = rootProfileColumnWeights.get(profile);
if (columnWeights == null) { if (columnWeights == null) {
columnWeights = new double[CHUNK_AREA]; columnWeights = blendScratch.acquireWeights();
rootProfileColumnWeights.put(profile, columnWeights); rootProfileColumnWeights.put(profile, columnWeights);
rootProfileOrder.add(profile); rootProfileOrder.add(profile);
} }
@@ -364,19 +377,6 @@ public class MantleCarvingComponent extends IrisMantleComponent {
return weightedProfiles; 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) { private void fillProfileField(IrisCaveProfile[] profileField, int chunkX, int chunkZ, IrisComplex complex, IrisDimensionCarvingResolver.State resolverState, BlendScratch blendScratch) {
int startX = PowerOfTwoCoordinates.chunkToBlock(chunkX) - BLEND_RADIUS; int startX = PowerOfTwoCoordinates.chunkToBlock(chunkX) - BLEND_RADIUS;
int startZ = PowerOfTwoCoordinates.chunkToBlock(chunkZ) - 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 IrisCaveProfile[] kernelProfiles = new IrisCaveProfile[KERNEL_SIZE];
private final double[] kernelProfileWeights = new double[KERNEL_SIZE]; private final double[] kernelProfileWeights = new double[KERNEL_SIZE];
private final IdentityHashMap<IrisCaveProfile, double[]> columnProfileWeights = new IdentityHashMap<>(); private final IdentityHashMap<IrisCaveProfile, double[]> columnProfileWeights = new IdentityHashMap<>();
private final IdentityHashMap<IrisDimensionCarvingEntry, IrisDimensionCarvingEntry[]> dimensionColumnPlans = new IdentityHashMap<>(); private final List<double[]> weightBuffers = new ArrayList<>();
private final IdentityHashMap<IrisCaveProfile, Boolean> activeProfiles = new IdentityHashMap<>();
private final List<IrisCaveProfile> profileOrder = new ArrayList<>(); private final List<IrisCaveProfile> profileOrder = new ArrayList<>();
private final double[] fieldSurfaceHeights = new double[FIELD_SIZE * FIELD_SIZE]; private final double[] fieldSurfaceHeights = new double[FIELD_SIZE * FIELD_SIZE];
private final double[] fieldFluidHeights = 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 IrisBiome[] fieldCaveBiomes = new IrisBiome[FIELD_SIZE * FIELD_SIZE];
private final int[] chunkSurfaceHeights = new int[CHUNK_AREA]; private final int[] chunkSurfaceHeights = new int[CHUNK_AREA];
private final double[] chunkSurfaceHeightSamples = new double[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.math.RNG;
import art.arcane.volmlib.util.matter.MatterCavern; import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.iris.util.common.parallel.BurstExecutor; 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.volmlib.util.scheduling.PrecisionStopwatch;
import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.B; 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> { 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; private final RNG rng;
public IrisDepositModifier(Engine engine) { 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) { public void generateDeposits(Hunk<PlatformBlockState> terrain, int x, int z, boolean multicore, ChunkContext context) {
IrisRegion region = context.getRegion().get(7, 7); IrisRegion region = context.getRegion().get(7, 7);
IrisBiome biome = context.getBiome().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 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(); MantleChunk chunk = getEngine().getMantle().getMantle().getChunk(x, z).use();
try { try {
for (IrisDepositGenerator k : getDimension().getDeposits()) { for (int i = 0; i < generators.size(); i++) {
long finalSeed = seed * ++mask; DepositPlan plan = plan(generators.get(i), rng.nextParallelRNG(seed * (i + 1L)));
burst.queue(scopedDepositTask( for (int first = 0; first < plan.attempts(); first += CLUMPS_PER_BATCH) {
() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context), context)); int firstAttempt = first;
} int limit = Math.min(plan.attempts(), first + CLUMPS_PER_BATCH);
int index = pending++;
for (IrisDepositGenerator k : region.getDeposits()) { burst.queue(scopedDepositTask(
long finalSeed = seed * ++mask; () -> prepared[index] = prepare(plan, firstAttempt, limit, x, z, null, context), context));
burst.queue(scopedDepositTask( if (pending == prepared.length) {
() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context), context)); completeBatches(burst, prepared, chunk, terrain, x, z, context);
} pending = 0;
}
for (IrisDepositGenerator k : biome.getDeposits()) { }
long finalSeed = seed * ++mask;
burst.queue(scopedDepositTask(
() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context), context));
} }
completeBatches(burst, prepared, chunk, terrain, x, z, context);
} finally { } finally {
// complete() must run before release() even when queueing throws — already // complete() must run before release() even when queueing throws — already
// submitted burst tasks must never write into a released chunk. // 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) { private Runnable scopedDepositTask(Runnable task, ChunkContext context) {
IrisEngine generationEngine = getEngine() instanceof IrisEngine irisEngine IrisEngine generationEngine = getEngine() instanceof IrisEngine irisEngine
&& irisEngine.hasGenerationRuntimeScope() ? irisEngine : null; && 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) { 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(); DepositPlan plan = plan(k, rng);
if (k.getSpawnChance() < rng.d()) for (int first = 0; first < plan.attempts(); first += CLUMPS_PER_BATCH) {
return; 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++) { private PreparedDeposit prepare(DepositPlan plan, int first, int limit, int cx, int cz, HeightMap he, ChunkContext context) {
if (k.getPerClumpSpawnChance() < rng.d()) 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; continue;
}
IrisObject clump = k.getClump(getEngine(), rng, getData()); IrisObject clump = k.getClump(getEngine(), clumpRng, getData());
int dim = clump.getW(); int dim = clump.getW();
int min = dim / 2; int min = dim / 2;
@@ -136,8 +183,8 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
max = 9; max = 9;
} }
int x = rng.i(min, max + 1); int x = clumpRng.i(min, max + 1);
int z = rng.i(min, max + 1); int z = clumpRng.i(min, max + 1);
int terrainSurface = getDepositTerrainSurface(cx, cz, x, z, he, context); int terrainSurface = getDepositTerrainSurface(cx, cz, x, z, he, context);
int height = k.getPlacementScope() == IrisDepositPlacementScope.TERRAIN int height = k.getPlacementScope() == IrisDepositPlacementScope.TERRAIN
? depositSurfaceLimit(terrainSurface, k.getSurfaceClearance()) ? depositSurfaceLimit(terrainSurface, k.getSurfaceClearance())
@@ -148,19 +195,20 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
} }
int y = sampleHeight( int y = sampleHeight(
k.getHeightDistribution(), rng, k.getMinHeight(), k.getMaxHeight(), k.getHeightDistribution(), clumpRng, k.getMinHeight(), k.getMaxHeight(),
Math.min(height, getEngine().getHeight() - 1)); Math.min(height, getEngine().getHeight() - 1));
if (y == Integer.MIN_VALUE) { if (y == Integer.MIN_VALUE) {
continue; continue;
} }
boolean clippedHeight = k.getHeightDistribution() == IrisDepositHeightDistribution.CLIPPED_UNIFORM; boolean clippedHeight = k.getHeightDistribution() == IrisDepositHeightDistribution.CLIPPED_UNIFORM;
if (clippedHeight && y > height - 2) if (clippedHeight && y > height - 2) {
continue; continue;
}
int biomeY = Math.max(0, Math.min(getEngine().getHeight() - 1, y)); int biomeY = Math.max(0, Math.min(getEngine().getHeight() - 1, y));
IrisBiome surfaceBiome = context.getBiome().get(x, z); IrisBiome surfaceBiome = context.getBiome().get(x, z);
IrisBiome depositBiome = oreDeposit || k.usesCaveBiomeFilter() IrisBiome depositBiome = needsCaveBiome
? getEngine().getCaveBiome( ? getEngine().getCaveBiome(
(cx << 4) + x, biomeY, (cz << 4) + z, carvingState) (cx << 4) + x, biomeY, (cz << 4) + z, carvingState)
: null; : null;
@@ -172,13 +220,13 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
if (depositBiome != null) { if (depositBiome != null) {
double frequencyMultiplier = depositBiome.getOreDepositFrequencyMultiplier(); double frequencyMultiplier = depositBiome.getOreDepositFrequencyMultiplier();
if (frequencyMultiplier < 1D if (frequencyMultiplier < 1D
&& !passesOreFrequency(frequencyMultiplier, rng.d())) { && !passesOreFrequency(frequencyMultiplier, clumpRng.d())) {
continue; continue;
} }
double sizeMultiplier = depositBiome.getOreDepositSizeMultiplier(); double sizeMultiplier = depositBiome.getOreDepositSizeMultiplier();
if (sizeMultiplier != 1D) { if (sizeMultiplier != 1D) {
IrisObject scaledClump = k.getClump(getEngine(), rng, getData(), sizeMultiplier); IrisObject scaledClump = k.getClump(getEngine(), clumpRng, getData(), sizeMultiplier);
int scaledDimension = scaledClump.getW(); int scaledDimension = scaledClump.getW();
x = clampDepositCenter(x, scaledDimension, 16); x = clampDepositCenter(x, scaledDimension, 16);
if (clippedHeight) { 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 nx = j.getBlockX() + x;
int ny = j.getBlockY() + y; int ny = j.getBlockY() + y;
int nz = j.getBlockZ() + z; int nz = j.getBlockZ() + z;
@@ -236,7 +305,7 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
} }
if (chunk.get(nx, ny, nz, MatterCavern.class) == null) { if (chunk.get(nx, ny, nz, MatterCavern.class) == null) {
PlatformBlockState ore = clump.getBlocks().get(j); PlatformBlockState ore = cursor.value();
PlatformBlockState remapped = resolveDepositVariant( PlatformBlockState remapped = resolveDepositVariant(
cx, cz, nx, ny, nz, ore, dimension, context, carvingState); cx, cz, nx, ny, nz, ore, dimension, context, carvingState);
PlatformBlockState finalBlock = remapped != null PlatformBlockState finalBlock = remapped != null
@@ -421,4 +490,20 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
return null; 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<IrisBiome> realCarveBiome = new AtomicCache<>();
private final transient AtomicCache<KList<IrisBiome>> realChildren = 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>> 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<CNG>> layerSeaHeightGenerators = new AtomicCache<>();
private final transient AtomicCache<KList<IrisOreGenerator>> surfaceOreCache = new AtomicCache<>(); private final transient AtomicCache<KList<IrisOreGenerator>> surfaceOreCache = new AtomicCache<>();
private final transient AtomicCache<KList<IrisOreGenerator>> undergroundOreCache = new AtomicCache<>(); private final transient AtomicCache<KList<IrisOreGenerator>> undergroundOreCache = new AtomicCache<>();
@@ -74,7 +74,7 @@ final class IrisBiomeLayerGenerator {
IrisBiomePaletteLayer layer = layers.get(i); IrisBiomePaletteLayer layer = layers.get(i);
double zoom = layer.getZoom(); double zoom = layer.getZoom();
CNG hgen = heightGenerators.get(i); 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(); IrisSlopeClip sc = layer.getSlopeCondition();
@@ -88,17 +88,7 @@ final class IrisBiomeLayerGenerator {
continue; continue;
} }
for (int j = 0; j < d; j++) { appendLayer(data, layer, i, d, wx, wz, random, maxDepth, rdata);
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);
}
}
if (data.size() >= maxDepth) { if (data.size() >= maxDepth) {
break; break;
@@ -132,31 +122,20 @@ final class IrisBiomeLayerGenerator {
return data; return data;
} }
KList<CNG> heightGenerators = getLayerHeightGenerators(biome, random, rdata); KList<CNG> heightGenerators = getHeightGenerators(
// Ceiling layers reuse the surface-layer height generators, so entries beyond layers.size() have no generator; skip them. biome.getLayerCeilingHeightGenerators(), ceilingLayers, 7235, random, rdata);
int usableLayers = Math.min(layerCount, heightGenerators.size());
for (int i = 0; i < usableLayers; i++) { for (int i = 0; i < layerCount; i++) {
IrisBiomePaletteLayer layer = ceilingLayers.get(i); IrisBiomePaletteLayer layer = ceilingLayers.get(i);
double zoom = layer.getZoom(); double zoom = layer.getZoom();
CNG hgen = heightGenerators.get(i); 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) { if (d <= 0) {
continue; continue;
} }
for (int j = 0; j < d; j++) { appendLayer(data, layer, i, d, wx, wz, random, maxDepth, rdata);
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);
}
}
if (data.size() >= maxDepth) { if (data.size() >= maxDepth) {
break; break;
@@ -198,7 +177,7 @@ final class IrisBiomeLayerGenerator {
IrisBiomePaletteLayer layer = layers.get(i); IrisBiomePaletteLayer layer = layers.get(i);
double zoom = layer.getZoom(); double zoom = layer.getZoom();
CNG hgen = heightGenerators.get(i); 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(); IrisSlopeClip sc = layer.getSlopeCondition();
@@ -212,13 +191,7 @@ final class IrisBiomeLayerGenerator {
continue; continue;
} }
for (int j = 0; j < d; j++) { appendLayer(data, layer, i, d, wx, wz, random, Integer.MAX_VALUE, rdata);
try {
data.add(layer.get(random, i + j, (wx + j) / zoom, j, (wz - j) / zoom, rdata));
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
} }
} }
@@ -227,9 +200,8 @@ final class IrisBiomeLayerGenerator {
} }
for (int i = 0; i < maxDepth; i++) { for (int i = 0; i < maxDepth; i++) {
int offset = (512 - height) - i; long offset = 512L - height - i;
int index = offset % data.size(); real.add(data.get(Math.floorMod(offset, data.size())));
real.add(data.get(Math.max(index, 0)));
} }
return real; return real;
@@ -264,17 +236,7 @@ final class IrisBiomeLayerGenerator {
continue; continue;
} }
for (int j = 0; j < d; j++) { appendLayer(data, layer, i, d, wx, wz, random, maxDepth, rdata);
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);
}
}
if (data.size() >= maxDepth) { if (data.size() >= maxDepth) {
break; break;
@@ -285,7 +247,16 @@ final class IrisBiomeLayerGenerator {
} }
static KList<CNG> getLayerHeightGenerators(IrisBiome biome, RNG rng, IrisData rdata) { 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(); KList<CNG> cached = cache.getIfPresent();
if (cached != null) { if (cached != null) {
@@ -296,36 +267,28 @@ final class IrisBiomeLayerGenerator {
{ {
KList<CNG> layerHeightGenerators = new KList<>(); KList<CNG> layerHeightGenerators = new KList<>();
int m = 7235; int m = seedOffset;
for (IrisBiomePaletteLayer i : biome.getLayers()) { for (IrisBiomePaletteLayer layer : layers) {
layerHeightGenerators.add(i.getHeightGenerator(rng.nextParallelRNG((m++) * m * m * m), rdata)); layerHeightGenerators.add(layer.getHeightGenerator(rng.nextParallelRNG((m++) * m * m * m), data));
} }
return layerHeightGenerators; return layerHeightGenerators;
}); });
} }
static KList<CNG> getLayerSeaHeightGenerators(IrisBiome biome, RNG rng, IrisData data) { private static void appendLayer(KList<PlatformBlockState> blocks, IrisBiomePaletteLayer layer,
AtomicCache<KList<CNG>> cache = biome.getLayerSeaHeightGenerators(); int layerIndex, int thickness, double x, double z,
KList<CNG> cached = cache.getIfPresent(); RNG random, int maxDepth, IrisData data) {
double zoom = layer.getZoom();
if (cached != null) { for (int offset = 0; offset < thickness && blocks.size() < maxDepth; offset++) {
return cached; try {
} blocks.add(layer.get(random, layerIndex + offset,
(x + offset) / zoom, offset, (z - offset) / zoom, data));
return cache.aquire(() -> } catch (Throwable error) {
{ IrisLogging.reportError(error);
KList<CNG> layerSeaHeightGenerators = new KList<>();
int m = 7735;
for (IrisBiomePaletteLayer i : biome.getSeaLayers()) {
layerSeaHeightGenerators.add(i.getHeightGenerator(rng.nextParallelRNG((m++) * m * m * m), data));
} }
}
return layerSeaHeightGenerators;
});
} }
static PlatformBlockState getSurfaceBlock(IrisBiome biome, int x, int z, RNG rng, IrisData idm) { 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.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine; 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.volmlib.util.collection.KList;
import art.arcane.iris.util.project.noise.CNG; import art.arcane.iris.util.project.noise.CNG;
@@ -149,21 +148,20 @@ public final class IrisDimensionCarvingResolver {
return parent; return parent;
} }
ParentSelectionPlan selectionPlan = state.selectionPlans.get(parent); IrisRaritySelection<CarvingChoice> selectionPlan = state.selectionPlans.get(parent);
if (selectionPlan == null) { if (selectionPlan == null) {
selectionPlan = buildSelectionPlan(engine, parent, parentBiome, entryIndex, state); selectionPlan = buildSelectionPlan(engine, parent, parentBiome, entryIndex, state);
state.selectionPlans.put(parent, selectionPlan); state.selectionPlans.put(parent, selectionPlan);
} }
if (selectionPlan.parentOnly) { if (selectionPlan.size() <= 1L) {
return parent; return parent;
} }
long seed = resolveChildSeed(engine, state); long seed = resolveChildSeed(engine, state);
CNG childGenerator = parent.getChildrenGenerator(seed, engine.getData()); CNG childGenerator = parent.getChildrenGenerator(seed, engine.getData());
double sample = childGenerator.noiseFast2D(worldX, worldZ); double sample = childGenerator.noiseFast2D(worldX, worldZ);
int selectedIndex = (int) Math.round(IrisInterpolation.lerp(0, selectionPlan.maxIndex, sample)); CarvingChoice selected = selectionPlan.select(sample);
CarvingChoice selected = selectionPlan.get(selectedIndex);
if (selected == null || selected.entry == null) { if (selected == null || selected.entry == null) {
return parent; return parent;
} }
@@ -171,7 +169,7 @@ public final class IrisDimensionCarvingResolver {
return selected.entry; return selected.entry;
} }
private static ParentSelectionPlan buildSelectionPlan( private static IrisRaritySelection<CarvingChoice> buildSelectionPlan(
Engine engine, Engine engine,
IrisDimensionCarvingEntry parent, IrisDimensionCarvingEntry parent,
IrisBiome parentBiome, IrisBiome parentBiome,
@@ -201,42 +199,7 @@ public final class IrisDimensionCarvingResolver {
} }
options.add(new CarvingChoice(parent, rarity(parentBiome))); options.add(new CarvingChoice(parent, rarity(parentBiome)));
if (options.size() <= 1) { return IrisRaritySelection.create(options);
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]);
} }
private static int rarity(IrisBiome biome) { private static int rarity(IrisBiome biome) {
@@ -285,7 +248,7 @@ public final class IrisDimensionCarvingResolver {
public static final class State { public static final class State {
private final Map<Integer, IrisDimensionCarvingEntry> rootEntriesByWorldY = new HashMap<>(); 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 final Map<IrisDimensionCarvingEntry, IrisBiome> biomeCache = new IdentityHashMap<>();
private WeakReference<Engine> engineIdentity; private WeakReference<Engine> engineIdentity;
private WeakReference<IrisDimension> dimensionIdentity; 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 static final class CarvingChoice implements IRare {
private final IrisDimensionCarvingEntry entry; private final IrisDimensionCarvingEntry entry;
private final int rarity; private final int rarity;
@@ -110,7 +110,7 @@ public class IrisObject extends IrisRegistrant {
this.d = d; this.d = d;
center = new Vector3i(w / 2, h / 2, d / 2); center = new Vector3i(w / 2, h / 2, d / 2);
shrinkOffset = new Vector3i(0, 0, 0); shrinkOffset = new Vector3i(0, 0, 0);
var lock = new ReentrantReadWriteLock(); ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
readLock = lock.readLock(); readLock = lock.readLock();
writeLock = lock.writeLock(); writeLock = lock.writeLock();
} }
@@ -135,17 +135,22 @@ public class IrisObject extends IrisRegistrant {
return aabb.aquire(() -> getAABBFor(new IrisBlockVector(w, h, d))); return aabb.aquire(() -> getAABBFor(new IrisBlockVector(w, h, d)));
} }
public synchronized IrisObject copy() { public IrisObject copy() {
IrisObject o = new IrisObject(w, h, d); readLock.lock();
o.setLoadKey(getLoadKey()); try {
o.setLoader(getLoader()); IrisObject copy = new IrisObject(w, h, d);
o.setLoadFile(getLoadFile()); copy.setLoadKey(getLoadKey());
o.setCenter(getCenter().clone()); copy.setLoader(getLoader());
copy.setLoadFile(getLoadFile());
copy.setCenter(getCenter().clone());
blocks.forEach((i, v) -> o.blocks.put(i.clone(), v)); blocks.forEach(copy.blocks::put);
states.forEach((i, v) -> o.states.put(i.clone(), v.clone())); states.forEach((position, tile) -> copy.states.put(position, tile.clone()));
return o; return copy;
} finally {
readLock.unlock();
}
} }
public void readLegacy(InputStream in) throws IOException { public void readLegacy(InputStream in) throws IOException {
@@ -350,7 +350,7 @@ final class IrisObjectPlacementRunner {
return -1; 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)) { yv < 0 && config.getMode() == ObjectPlaceMode.PAINT, warpMargin, x, y + yrand, z)) {
return -1; return -1;
} }
@@ -533,6 +533,9 @@ final class IrisObjectPlacementRunner {
} }
data = config.getRotation().rotate(data, spinx, spiny, spinz); data = config.getRotation().rotate(data, spinx, spiny, spinz);
if (data == null) {
continue;
}
xx = x + (int) Math.round(i.getX()); xx = x + (int) Math.round(i.getX());
int yy = y + (int) Math.round(i.getY()); 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 * 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. * 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 translating, IrisBlockVector translateOffset, boolean ceilingHang,
boolean paint, int warpMargin, int x, int y, int z) { boolean paint, int warpMargin, int x, int y, int z) {
Engine engine = placer.getEngine(); Engine engine = placer.getEngine();
@@ -963,23 +966,21 @@ final class IrisObjectPlacementRunner {
return false; return false;
} }
int margin = (Math.max(self.getW(), Math.max(self.getH(), self.getD())) / 2) + 1 + warpMargin; int minX = x + bounds.minX();
if (translating) { int maxX = x + bounds.maxX();
margin += Math.max(Math.abs(translateOffset.getBlockX()), int minZ = z + bounds.minZ();
Math.max(Math.abs(translateOffset.getBlockY()), Math.abs(translateOffset.getBlockZ()))); int maxZ = z + bounds.maxZ();
} KList<NativeStructureVolume> volumes = engine.getNativeStructureVolumes(minX, minZ, maxX, maxZ);
KList<NativeStructureVolume> volumes = engine.getNativeStructureVolumes(x - margin, z - margin, x + margin, z + margin);
if (volumes == null || volumes.isEmpty()) { if (volumes == null || volumes.isEmpty()) {
return false; return false;
} }
int worldY = y + engine.getMinHeight(); int worldY = y + engine.getMinHeight();
int envelopeMinY = paint ? Integer.MIN_VALUE : worldY - margin; int envelopeMinY = paint ? Integer.MIN_VALUE : worldY + bounds.minY() - warpMargin;
int envelopeMaxY = paint ? Integer.MAX_VALUE : worldY + margin; int envelopeMaxY = paint ? Integer.MAX_VALUE : worldY + bounds.maxY() + warpMargin;
boolean envelopeMeetsPiece = false; boolean envelopeMeetsPiece = false;
for (NativeStructureVolume volume : volumes) { 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; envelopeMeetsPiece = true;
break; break;
} }
@@ -22,17 +22,10 @@ import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.VectorMap; import art.arcane.iris.util.common.data.VectorMap;
import art.arcane.iris.util.common.math.IrisBlockVector; 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.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.format.Form;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; 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 * Volume shaping for {@link IrisObject}: smart boring, shrinkwrapping, compaction and the shared block
* classification helpers used by placement. * classification helpers used by placement.
@@ -62,30 +55,23 @@ final class IrisObjectShaping {
private static void ensureSmartBoredLocked(IrisObject self) { private static void ensureSmartBoredLocked(IrisObject self) {
PrecisionStopwatch p = PrecisionStopwatch.start(); PrecisionStopwatch p = PrecisionStopwatch.start();
PlatformBlockState vair = IrisObject.States.VAIR; PlatformBlockState vair = IrisObject.States.VAIR;
AtomicInteger applied = new AtomicInteger(); int applied = 0;
IrisBlockVector max = new IrisBlockVector(Double.MIN_VALUE, Double.MIN_VALUE, Double.MIN_VALUE); IrisBlockVector max = new IrisBlockVector(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE);
IrisBlockVector min = new IrisBlockVector(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE); IrisBlockVector min = new IrisBlockVector(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE);
VectorMap<PlatformBlockState> source; VectorMap<PlatformBlockState> source = self.blocks;
self.readLock.lock(); if (source.isEmpty()) {
try { IrisLogging.warn("Cannot Smart Bore " + self.getLoadKey() + " because it has 0 blocks in it.");
if (self.blocks.isEmpty()) { self.smartBored = true;
IrisLogging.warn("Cannot Smart Bore " + self.getLoadKey() + " because it has 0 blocks in it."); return;
self.smartBored = true; }
return;
}
source = self.blocks; for (IrisBlockVector i : source.keys()) {
max.setX(Math.max(i.getX(), max.getX()));
for (IrisBlockVector i : self.blocks.keys()) { min.setX(Math.min(i.getX(), min.getX()));
max.setX(Math.max(i.getX(), max.getX())); max.setY(Math.max(i.getY(), max.getY()));
min.setX(Math.min(i.getX(), min.getX())); min.setY(Math.min(i.getY(), min.getY()));
max.setY(Math.max(i.getY(), max.getY())); max.setZ(Math.max(i.getZ(), max.getZ()));
min.setY(Math.min(i.getY(), min.getY())); min.setZ(Math.min(i.getZ(), min.getZ()));
max.setZ(Math.max(i.getZ(), max.getZ()));
min.setZ(Math.min(i.getZ(), min.getZ()));
}
} finally {
self.readLock.unlock();
} }
VectorMap<PlatformBlockState> bore = new VectorMap<>(); 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) // 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, // thread, not from pool scheduling. This matches what burst-worker callers (studio,
// generation threads) already produced, so output is unchanged where it was stable. // generation threads) already produced, so output is unchanged where it was stable.
BurstExecutor burst = MultiBurst.burst.burst();
burst.setMulticore(false);
// Smash X // Smash X
for (int rayY = min.getBlockY(); rayY <= max.getBlockY(); rayY++) { for (int rayY = min.getBlockY(); rayY <= max.getBlockY(); rayY++) {
int finalRayY = rayY; for (int rayZ = min.getBlockZ(); rayZ <= max.getBlockZ(); rayZ++) {
burst.queue(() -> { int start = Integer.MAX_VALUE;
for (int rayZ = min.getBlockZ(); rayZ <= max.getBlockZ(); rayZ++) { int end = Integer.MIN_VALUE;
int start = Integer.MAX_VALUE;
int end = Integer.MIN_VALUE;
for (int ray = min.getBlockX(); ray <= max.getBlockX(); ray++) { for (int ray = min.getBlockX(); ray <= max.getBlockX(); ray++) {
if (boreContains(source, bore, new IrisBlockVector(ray, finalRayY, rayZ))) { if (boreContains(source, bore, new IrisBlockVector(ray, rayY, rayZ))) {
start = Math.min(ray, start); start = Math.min(ray, start);
end = Math.max(ray, end); 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);
}
} }
} }
});
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 // Smash Y
for (int rayX = min.getBlockX(); rayX <= max.getBlockX(); rayX++) { for (int rayX = min.getBlockX(); rayX <= max.getBlockX(); rayX++) {
int finalRayX = rayX; for (int rayZ = min.getBlockZ(); rayZ <= max.getBlockZ(); rayZ++) {
burst.queue(() -> { int start = Integer.MAX_VALUE;
for (int rayZ = min.getBlockZ(); rayZ <= max.getBlockZ(); rayZ++) { int end = Integer.MIN_VALUE;
int start = Integer.MAX_VALUE;
int end = Integer.MIN_VALUE;
for (int ray = min.getBlockY(); ray <= max.getBlockY(); ray++) { for (int ray = min.getBlockY(); ray <= max.getBlockY(); ray++) {
if (boreContains(source, bore, new IrisBlockVector(finalRayX, ray, rayZ))) { if (boreContains(source, bore, new IrisBlockVector(rayX, ray, rayZ))) {
start = Math.min(ray, start); start = Math.min(ray, start);
end = Math.max(ray, end); 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);
}
} }
} }
});
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 // Smash Z
for (int rayX = min.getBlockX(); rayX <= max.getBlockX(); rayX++) { for (int rayX = min.getBlockX(); rayX <= max.getBlockX(); rayX++) {
int finalRayX = rayX; for (int rayY = min.getBlockY(); rayY <= max.getBlockY(); rayY++) {
burst.queue(() -> { int start = Integer.MAX_VALUE;
for (int rayY = min.getBlockY(); rayY <= max.getBlockY(); rayY++) { int end = Integer.MIN_VALUE;
int start = Integer.MAX_VALUE;
int end = Integer.MIN_VALUE;
for (int ray = min.getBlockZ(); ray <= max.getBlockZ(); ray++) { for (int ray = min.getBlockZ(); ray <= max.getBlockZ(); ray++) {
if (boreContains(source, bore, new IrisBlockVector(finalRayX, rayY, ray))) { if (boreContains(source, bore, new IrisBlockVector(rayX, rayY, ray))) {
start = Math.min(ray, start); start = Math.min(ray, start);
end = Math.max(ray, end); 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);
}
} }
} }
});
}
burst.complete(); if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) {
for (int i = start; i <= end; i++) {
self.writeLock.lock(); applied += boreCell(source, bore, new IrisBlockVector(rayX, rayY, i), vair);
try { }
if (!self.smartBored) { }
bore.forEach((v, s) -> self.blocks.computeIfAbsent(v, (vv) -> s));
self.smartBored = true;
} }
} 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) { private static boolean boreContains(VectorMap<PlatformBlockState> source, VectorMap<PlatformBlockState> bore, IrisBlockVector v) {
return source.containsKey(v) || bore.containsKey(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); PlatformBlockState existing = source.get(v);
if (existing == null) { if (existing == null) {
if (vair.equals(bore.get(v))) { if (vair.equals(bore.get(v))) {
return; return 0;
} }
bore.computeIfAbsent(v, (vv) -> vair); bore.put(v, vair);
} else if (vair.equals(existing)) { } else if (vair.equals(existing)) {
return; return 0;
} }
applied.getAndIncrement(); return 1;
} }
static void shrinkwrap(IrisObject self) { static void shrinkwrap(IrisObject self) {
@@ -289,23 +255,4 @@ final class IrisObjectShaping {
static String materialKey(PlatformBlockState state) { static String materialKey(PlatformBlockState state) {
return IrisProceduralBlocks.materialKey(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.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set;
/** /**
* Geometric transforms for {@link IrisObject}: rotation, scaling and the interpolated upscalers. * Geometric transforms for {@link IrisObject}: rotation, scaling and the interpolated upscalers.
@@ -52,15 +54,23 @@ final class IrisObjectTransforms {
self.writeLock.lock(); self.writeLock.lock();
try { try {
VectorMap<PlatformBlockState> d = new VectorMap<>(); VectorMap<PlatformBlockState> d = new VectorMap<>();
Set<IrisBlockVector> omitted = new HashSet<>();
for (var entry : self.blocks) { for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : self.blocks) {
d.put(r.rotate(entry.getKey(), spinx, spiny, spinz), r.rotate(entry.getValue(), spinx, spiny, spinz)); 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<>(); VectorMap<TileData> dx = new VectorMap<>();
for (var entry : self.states) { for (Map.Entry<IrisBlockVector, TileData> entry : self.states) {
dx.put(r.rotate(entry.getKey(), spinx, spiny, spinz), entry.getValue()); if (!omitted.contains(entry.getKey())) {
dx.put(r.rotate(entry.getKey(), spinx, spiny, spinz), entry.getValue());
}
} }
self.blocks = d; self.blocks = d;
@@ -96,32 +106,31 @@ final class IrisObjectTransforms {
scale = scale - 0.0001; scale = scale - 0.0001;
} }
IrisPosition l1 = self.getAABB().max();
IrisPosition l2 = self.getAABB().min();
VectorMap<PlatformBlockState> placeBlock = new VectorMap<>(); VectorMap<PlatformBlockState> placeBlock = new VectorMap<>();
VectorMap<TileData> placeTile = new VectorMap<>(); VectorMap<TileData> placeTile = new VectorMap<>();
VectorMap<IrisBlockVector> placeMax = savedOrigin && scale > 1 ? new VectorMap<>() : null; VectorMap<IrisBlockVector> placeMax = savedOrigin && scale > 1 ? new VectorMap<>() : null;
IrisVector center = new IrisVector(self.getCenter().getX(), self.getCenter().getY(), self.getCenter().getZ()); IrisVector center;
if (self.getH() == 2) { IrisObject oo;
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());
boolean hasTiles; boolean hasTiles;
self.readLock.lock(); self.readLock.lock();
try { 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(); hasTiles = !self.states.isEmpty();
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : self.blocks) { for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : self.blocks) {
PlatformBlockState bd = entry.getValue(); PlatformBlockState bd = entry.getValue();
@@ -153,13 +162,27 @@ final class IrisObjectTransforms {
if (scale > 1) { if (scale > 1) {
IrisVector minimum = savedOrigin ? v : v.clone().add(center); IrisVector minimum = savedOrigin ? v : v.clone().add(center);
IrisVector maximum = savedOrigin ? placeMax.get(v) : v.clone().add(center).add(sm1); IrisVector maximum = savedOrigin ? placeMax.get(v) : v.clone().add(center).add(sm1);
for (IrisBlockVector vec : IrisObjectShaping.blocksBetweenTwoPoints(minimum, maximum)) { int minX = Math.min(minimum.getBlockX(), maximum.getBlockX());
oo.blocks.put(vec, entry.getValue()); int maxX = Math.max(minimum.getBlockX(), maximum.getBlockX());
if (hasTiles) { int minY = Math.min(minimum.getBlockY(), maximum.getBlockY());
if (tile == null) { int maxY = Math.max(minimum.getBlockY(), maximum.getBlockY());
oo.states.remove(vec); int minZ = Math.min(minimum.getBlockZ(), maximum.getBlockZ());
} else { int maxZ = Math.max(minimum.getBlockZ(), maximum.getBlockZ());
oo.states.put(vec, tile.clone()); 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) { 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; 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) { private static void buildFan(CoralCanvas canvas, IrisCoral coral, int height, long seed) {
int half = Math.max(1, coral.getFanWidth()); 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 t = y / (double) Math.max(1, height - 1);
double profile = Math.sin(Math.PI * t); double profile = Math.sin(Math.PI * t);
int w = (int) Math.round(half * profile); int w = (int) Math.round(half * profile);
for (int dx = -w; dx <= w; dx++) { for (int dx = -w; dx <= w; dx++) {
canvas.set(dx, y, 0, CoralCanvas.Role.STRUCTURE); canvas.set(dx, y, 0, CoralCanvas.Role.STRUCTURE);
if (Math.abs(dx) > tippedWidth) {
placeTip(canvas, coral, dx, y, 0, seed + dx);
}
} }
} tippedWidth = Math.max(tippedWidth, w);
for (int dx = -half; dx <= half; dx++) {
placeTip(canvas, coral, dx, height - 1, 0, seed + dx);
} }
} }
@@ -170,7 +172,9 @@ public final class CoralGenerator {
int y = (int) Math.round(oy + dy * t); int y = (int) Math.round(oy + dy * t);
int z = (int) Math.round(oz + dz * t - sway); int z = (int) Math.round(oz + dz * t - sway);
canvas.set(x, y, z, CoralCanvas.Role.STRUCTURE); 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; return tip;
} }
@@ -25,35 +25,48 @@ import art.arcane.iris.engine.object.IrisProceduralBlocks;
import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.math.Vector3i; import art.arcane.iris.util.common.math.Vector3i;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
public final class FormationBlockResolver { 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 x = raw.getBlockX();
int y = raw.getBlockY(); int y = raw.getBlockY();
int z = raw.getBlockZ(); int z = raw.getBlockZ();
RNG paletteRng = new RNG(f.getSeed());
if (role == FormationCanvas.Role.CAP && capDefined(f)) { if (role == FormationCanvas.Role.CAP && capDefined(formation)) {
PlatformBlockState cap = IrisProceduralBlocks.resolve(f.getCapBlock(), f.getCapPalette(), data, x, y, z, paletteRng); PlatformBlockState cap = IrisProceduralBlocks.resolve(formation.getCapBlock(), formation.getCapPalette(), data, x, y, z, paletteRng);
if (cap != null) { if (cap != null) {
return cap; return cap;
} }
} }
if (strataDefined(f)) { if (strataDefined(formation)) {
IrisMaterialPalette strata = f.getStrataPalette(); IrisMaterialPalette strata = formation.getStrataPalette();
int thickness = Math.max(1, f.getStrataThickness()); int thickness = Math.max(1, formation.getStrataThickness());
int band = Math.floorDiv(y, thickness); 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) { if (strataState != null) {
return strataState; 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) { private static boolean capDefined(IrisFormation f) {
@@ -62,8 +62,9 @@ public final class FormationGenerator {
} }
Map<Vector3i, PlatformBlockState> resolved = new HashMap<>(); Map<Vector3i, PlatformBlockState> resolved = new HashMap<>();
FormationBlockResolver resolver = new FormationBlockResolver(f, data);
for (Map.Entry<Vector3i, FormationCanvas.Role> entry : canvas.getCells().entrySet()) { 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) { if (bd == null) {
continue; 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) { private static void buildUpright(Map<Vector3i, FungusCellRole> roles, IrisFungus fungus, int height, RNG rng, long baseSeed) {
int stemHeight = Math.max(1, height); int stemHeight = Math.max(1, height);
Map<Vector3i, PlatformBlockState> stemCells = new HashMap<>(); double[] top = FungusStemBuilder.build(roles, fungus, stemHeight, baseSeed);
double[] top = FungusStemBuilder.build(stemCells, fungus, stemHeight, baseSeed);
for (Vector3i v : stemCells.keySet()) {
roles.put(v, FungusCellRole.STEM);
}
int radius = pickRadius(fungus, rng); int radius = pickRadius(fungus, rng);
double cx = top[0]; 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.IrisFungus;
import art.arcane.iris.engine.object.tree.TreeFunctions; import art.arcane.iris.engine.object.tree.TreeFunctions;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.math.Vector3i; import art.arcane.iris.util.common.math.Vector3i;
import java.util.Map; import java.util.Map;
@@ -29,7 +28,7 @@ public final class FungusStemBuilder {
private 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())); int width = Math.max(1, Math.min(3, fungus.getStemWidth()));
double maxLean = stemHeight * Math.tan(Math.toRadians(Math.max(0.0, fungus.getStemCurve()))); double maxLean = stemHeight * Math.tan(Math.toRadians(Math.max(0.0, fungus.getStemCurve())));
double leanRad = Math.toRadians(fungus.getStemLeanAzimuth()); double leanRad = Math.toRadians(fungus.getStemLeanAzimuth());
@@ -40,6 +39,7 @@ public final class FungusStemBuilder {
double waveAzimuth = Math.toRadians(fungus.getStemLeanAzimuth() + 90.0); double waveAzimuth = Math.toRadians(fungus.getStemLeanAzimuth() + 90.0);
double waveX = Math.sin(waveAzimuth); double waveX = Math.sin(waveAzimuth);
double waveZ = Math.cos(waveAzimuth); double waveZ = Math.cos(waveAzimuth);
double wavePhase = TreeFunctions.valueNoise1D(seed, seed) * Math.PI * 2.0;
double topCx = 0.0; double topCx = 0.0;
double topCz = 0.0; double topCz = 0.0;
@@ -48,12 +48,16 @@ public final class FungusStemBuilder {
for (int y = 0; y < stemHeight; y++) { for (int y = 0; y < stemHeight; y++) {
double t = stemHeight <= 1 ? 0.0 : y / (double) (stemHeight - 1); double t = stemHeight <= 1 ? 0.0 : y / (double) (stemHeight - 1);
double lean = maxLean * (t * t); 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 cx = lean * leanX + wave * waveX;
double cz = lean * leanZ + wave * waveZ; double cz = lean * leanZ + wave * waveZ;
for (int[] xz : squarePositions(cx, cz, width)) { int ox = (width % 2 == 1 ? (int) Math.round(cx) : (int) Math.floor(cx) + 1) - width / 2;
stemCells.put(new Vector3i(xz[0], y, xz[1]), null); 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; topCx = cx;
@@ -62,34 +66,4 @@ public final class FungusStemBuilder {
return new double[]{topCx, topY, topCz}; 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<>(); Map<TreeBlockCanvas.Vec, PlatformBlockState> resolved = new HashMap<>();
Set<TreeBlockCanvas.Vec> trunkPositions = new HashSet<>(); Set<TreeBlockCanvas.Vec> trunkPositions = new HashSet<>();
Set<TreeBlockCanvas.Vec> leafPositions = 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()) { for (Map.Entry<TreeBlockCanvas.Vec, TreeBlockCanvas.Cell> entry : canvas.getCells().entrySet()) {
TreeBlockCanvas.Cell cell = entry.getValue(); 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) { if (state == null) {
continue; 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.IrisProceduralBlocks;
import art.arcane.iris.engine.object.IrisProceduralTree; import art.arcane.iris.engine.object.IrisProceduralTree;
import art.arcane.iris.engine.object.IrisTreeDecorator; import art.arcane.iris.engine.object.IrisTreeDecorator;
import art.arcane.iris.engine.object.IrisTreeSecondaryLeaf;
import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.B; import art.arcane.iris.util.common.data.B;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
@@ -31,10 +32,7 @@ public final class TreeBlockResolver {
private TreeBlockResolver() { private TreeBlockResolver() {
} }
public static PlatformBlockState resolve(IrisProceduralTree tree, IrisData data, TreeBlockCanvas.Cell cell, TreeBlockCanvas.Vec pos) { public static PlatformBlockState resolve(IrisProceduralTree tree, IrisData data, TreeBlockCanvas.Cell cell, TreeBlockCanvas.Vec pos, RNG paletteRng) {
RNG paletteRng = new RNG(tree.getSeed());
RNG posRng = new RNG(tree.getSeed() ^ positionHash(pos));
switch (cell.role()) { switch (cell.role()) {
case TRUNK -> { case TRUNK -> {
PlatformBlockState state = resolveBlock(tree.getTrunk(), tree.getTrunkPalette(), data, pos, paletteRng); 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); return resolveBlock(tree.getLeaves(), tree.getLeavesPalette(), data, pos, paletteRng);
} }
case SECONDARY_LEAF -> { case SECONDARY_LEAF -> {
PlatformBlockState state = resolveSecondaryLeaf(tree, data, pos, paletteRng, posRng); PlatformBlockState state = resolveSecondaryLeaf(tree, data, pos, paletteRng);
if (state == null) { if (state == null) {
state = resolveBlock(tree.getLeaves(), tree.getLeavesPalette(), data, pos, paletteRng); state = resolveBlock(tree.getLeaves(), tree.getLeavesPalette(), data, pos, paletteRng);
} }
@@ -94,12 +92,12 @@ public final class TreeBlockResolver {
return state; 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())) { if (TreeTrunkBuilder.paletteSet(tree.getSecondaryLeavesPalette())) {
return tree.getSecondaryLeavesPalette().get(paletteRng, pos.x(), pos.y(), pos.z(), data); return tree.getSecondaryLeavesPalette().get(paletteRng, pos.x(), pos.y(), pos.z(), data);
} }
if (tree.getWeightedSecondaryLeaves() != null && !tree.getWeightedSecondaryLeaves().isEmpty()) { 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); return picked == null ? null : B.getStateOrNull(picked, false);
} }
if (tree.getSecondaryLeaves() != null && !tree.getSecondaryLeaves().isEmpty()) { if (tree.getSecondaryLeaves() != null && !tree.getSecondaryLeaves().isEmpty()) {
@@ -109,16 +107,16 @@ public final class TreeBlockResolver {
} }
private static String pickWeighted(IrisProceduralTree tree, RNG rng) { private static String pickWeighted(IrisProceduralTree tree, RNG rng) {
int total = 0; long total = 0;
for (art.arcane.iris.engine.object.IrisTreeSecondaryLeaf s : tree.getWeightedSecondaryLeaves()) { for (IrisTreeSecondaryLeaf s : tree.getWeightedSecondaryLeaves()) {
total += Math.max(0, s.getWeight()); total += Math.max(0, s.getWeight());
} }
if (total <= 0) { if (total <= 0) {
return null; return null;
} }
double r = rng.nextDouble() * total; double r = rng.nextDouble() * total;
double cumulative = 0; long cumulative = 0;
for (art.arcane.iris.engine.object.IrisTreeSecondaryLeaf s : tree.getWeightedSecondaryLeaves()) { for (IrisTreeSecondaryLeaf s : tree.getWeightedSecondaryLeaves()) {
cumulative += Math.max(0, s.getWeight()); cumulative += Math.max(0, s.getWeight());
if (r < cumulative) { if (r < cumulative) {
return s.getBlock(); return s.getBlock();
@@ -67,11 +67,24 @@ public final class TreeDecoratorApplier {
if (rng.nextDouble() > dec.getChance()) { if (rng.nextDouble() > dec.getChance()) {
continue; 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; continue;
} }
String facing = dec.isAxisAware() ? facingAway(e[3], e[4], e[0], e[2]) : null; 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; 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) { for (TreeBlockCanvas.Vec v : realLeaves) {
PlatformBlockState leaf = resolved.get(v); PlatformBlockState leaf = resolved.get(v);
@@ -18,7 +18,6 @@
package art.arcane.iris.engine.object.tree; package art.arcane.iris.engine.object.tree;
import java.util.ArrayList;
import java.util.Map; import java.util.Map;
public final class TreeSupport { public final class TreeSupport {
@@ -36,7 +35,7 @@ public final class TreeSupport {
TreeBlockCanvas.Vec worstWood = null; TreeBlockCanvas.Vec worstWood = null;
int worstGap = -1; int worstGap = -1;
for (TreeBlockCanvas.Vec leaf : new ArrayList<>(canvas.getLeaf())) { for (TreeBlockCanvas.Vec leaf : canvas.getLeaf()) {
Integer d = dist.get(leaf); Integer d = dist.get(leaf);
if (d != null && d <= LEGAL_DISTANCE) { if (d != null && d <= LEGAL_DISTANCE) {
continue; continue;
@@ -121,7 +121,7 @@ public final class TreeTrunkBuilder {
} }
private static void markExposedEnds(TreeBlockCanvas canvas) { 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())) { 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()); 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.GenerationActivation;
import art.arcane.iris.engine.history.GenerationEpoch; import art.arcane.iris.engine.history.GenerationEpoch;
import art.arcane.iris.engine.history.GenerationHistory; 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.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.history.IrisBoundarySignatureSampler; import art.arcane.iris.engine.history.IrisBoundarySignatureSampler;
import art.arcane.iris.engine.history.TransitionGenerationPlan; import art.arcane.iris.engine.history.TransitionGenerationPlan;
@@ -396,66 +397,68 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
} }
private void setupEngine() { private void setupEngine() {
lastMode = StudioMode.NORMAL; try (GenerationAdmission.RuntimeLease startupAdmission = generationHistory == null ? null : generationHistory.retainRuntime()) {
lastJigsawStudioRequestId = null; lastMode = StudioMode.NORMAL;
if (generationHistory != null) { lastJigsawStudioRequestId = null;
try { if (generationHistory != null) {
generationHistory.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()); try {
targetCache.reset(); generationHistory.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
} 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();
targetCache.reset(); 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( createdEngine.attachGenerationHistory(
generationHistory, generationHistory,
IrisBoundarySignatureSampler.INSTANCE, IrisBoundarySignatureSampler.INSTANCE,
transitionWidthBlocks); transitionWidthBlocks);
} if (generationHistory.activeActivation().activationId() != initialActivationId) {
} catch (Throwable failure) { createdEngine.close();
try { targetCache.reset();
createdEngine.close(); createdEngine = createEngine(loadActiveGenerationHistoryTarget());
} catch (Throwable closeFailure) { createdEngine.attachGenerationHistory(
if (failure != closeFailure) { generationHistory,
failure.addSuppressed(closeFailure); 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 { private EngineTarget loadActiveGenerationHistoryTarget() throws IOException {
@@ -672,7 +675,6 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
@Override @Override
public CompletableFuture<Void> closeAsync() { public CompletableFuture<Void> closeAsync() {
closing = true;
// Outside the exclusive-control block so every close path detaches the WorldInit // Outside the exclusive-control block so every close path detaches the WorldInit
// listener, including a rollback where the world never materialized. Guarded: with no // listener, including a rollback where the world never materialized. Guarded: with no
// hosted plugin (unit tests, teardown) volmitPlugin() throws, and that must not stop // 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; CompletableFuture<Void> operation;
try { try {
operation = withExclusiveControlFuture(() -> { operation = withExclusiveControlFuture(() -> {
@@ -712,11 +716,13 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
populators.clear(); populators.clear();
}); });
} catch (Throwable throwable) { } 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); closeFuture.compareAndSet(future, null);
// A failed close must stay retryable; leaving closing latched would permanently future.completeExceptionally(throwable);
// reject generation for a world that may still be loaded.
closing = false;
return future; 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 // 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 // failure above, resetting the gate here would advertise a healthy generator
// over a CLOSED or FAILED engine. Stay latched and surface the failure. // over a CLOSED or FAILED engine. Stay latched and surface the failure.
closeFuture.compareAndSet(future, null);
future.completeExceptionally(throwable); future.completeExceptionally(throwable);
} }
}); });
@@ -11,7 +11,6 @@ import java.nio.file.Path;
import java.util.List; import java.util.List;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
public class PackBiomeLayerValidatorTest { public class PackBiomeLayerValidatorTest {
@@ -19,14 +18,12 @@ public class PackBiomeLayerValidatorTest {
public TemporaryFolder temporaryFolder = new TemporaryFolder(); public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test @Test
public void rejectsMoreCeilingLayersThanLayers() throws Exception { public void acceptsMoreCeilingLayersThanSurfaceLayers() throws Exception {
File pack = temporaryFolder.newFolder("pack"); File pack = temporaryFolder.newFolder("pack");
write(pack, "biomes/nested/cave.json", write(pack, "biomes/nested/cave.json",
"{\"layers\":[{}],\"caveCeilingLayers\":[{},{},{}]}"); "{\"layers\":[{}],\"caveCeilingLayers\":[{},{},{}]}");
assertEquals(List.of( assertTrue(PackBiomeLayerValidator.validateLayers(new File(pack, "biomes")).isEmpty());
"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")));
} }
@Test @Test
@@ -35,9 +32,7 @@ public class PackBiomeLayerValidatorTest {
write(pack, "biomes/defaulted.json", "{\"name\":\"Defaulted\"}"); write(pack, "biomes/defaulted.json", "{\"name\":\"Defaulted\"}");
write(pack, "biomes/implicit.json", "{\"caveCeilingLayers\":[{},{}]}"); write(pack, "biomes/implicit.json", "{\"caveCeilingLayers\":[{},{}]}");
assertEquals(List.of( assertTrue(PackBiomeLayerValidator.validateLayers(new File(pack, "biomes")).isEmpty());
"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")));
} }
@Test @Test
@@ -45,7 +40,7 @@ public class PackBiomeLayerValidatorTest {
File pack = temporaryFolder.newFolder("pack"); File pack = temporaryFolder.newFolder("pack");
write(pack, "biomes/ok.json", "{\"layers\":[{},{},{}],\"caveCeilingLayers\":[{},{}]}"); write(pack, "biomes/ok.json", "{\"layers\":[{},{},{}],\"caveCeilingLayers\":[{},{}]}");
assertTrue(PackBiomeLayerValidator.validateCeilingLayerCounts(new File(pack, "biomes")).isEmpty()); assertTrue(PackBiomeLayerValidator.validateLayers(new File(pack, "biomes")).isEmpty());
} }
@Test @Test
@@ -56,11 +51,11 @@ public class PackBiomeLayerValidatorTest {
assertEquals(List.of( assertEquals(List.of(
"Biome 'bad' layers must be an array.", "Biome 'bad' layers must be an array.",
"Biome 'bad' caveCeilingLayers must be an array." "Biome 'bad' caveCeilingLayers must be an array."
), PackBiomeLayerValidator.validateCeilingLayerCounts(new File(pack, "biomes"))); ), PackBiomeLayerValidator.validateLayers(new File(pack, "biomes")));
} }
@Test @Test
public void ceilingLayerOverflowBlocksFullPackValidation() throws Exception { public void independentCeilingLayersPassFullPackValidation() throws Exception {
File pack = temporaryFolder.newFolder("pack"); File pack = temporaryFolder.newFolder("pack");
write(pack, "dimensions/main.json", "{\"regions\":[\"region\"]}"); write(pack, "dimensions/main.json", "{\"regions\":[\"region\"]}");
write(pack, "regions/region.json", "{\"landBiomes\":[\"biome\"]}"); write(pack, "regions/region.json", "{\"landBiomes\":[\"biome\"]}");
@@ -68,9 +63,7 @@ public class PackBiomeLayerValidatorTest {
PackValidationResult result = PackValidator.validate(pack); PackValidationResult result = PackValidator.validate(pack);
assertFalse(result.isLoadable()); assertTrue(result.getBlockingErrors().toString(), 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."));
} }
@Test @Test
@@ -33,9 +33,9 @@ public class EngineRuntimePublicationContractTest {
int publishEnd = source.indexOf("private void scheduleRuntimeTasks", publishStart); int publishEnd = source.indexOf("private void scheduleRuntimeTasks", publishStart);
String publish = source.substring(publishStart, publishEnd); String publish = source.substring(publishStart, publishEnd);
assertBefore(publish, "engine.getClosing().set(true)", "closeRuntime(next, e)"); assertBefore(publish, "engine.getClosing().set(true)", "cleanupFailure = engine.shutdownSequence.closeRuntime(next, null)");
assertBefore(publish, "closeBackgroundTaskAdmission()", "closeRuntime(next, e)"); assertBefore(publish, "closeBackgroundTaskAdmission()", "cleanupFailure = engine.shutdownSequence.closeRuntime(next, null)");
assertBefore(publish, "sealAndAwait(", "closeRuntime(next, e)"); assertBefore(publish, "sealAndAwait(", "cleanupFailure = engine.shutdownSequence.closeRuntime(next, null)");
} }
@Test @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); String cleanup = source.substring(cleanupStart, cleanupEnd);
int ownershipClose = cleanup.indexOf("NativeStructureOwnershipStore.close(engine)"); int ownershipClose = cleanup.indexOf("NativeStructureOwnershipStore.close(engine)");
int ownershipGate = cleanup.indexOf("if (ownershipFailure == null)", ownershipClose); int ownershipGate = cleanup.indexOf("if (ownershipFailure == null)", ownershipClose);
int detachedRelease = cleanup.indexOf("closeDetachedGenerationRuntimes(cleanupFailure)", ownershipGate); int runtimeRelease = cleanup.indexOf("releaseRuntime(cleanupFailure)", ownershipGate);
int runtimeRelease = cleanup.indexOf("closeRuntime(engine.runtime, cleanupFailure)", ownershipGate); int targetRelease = cleanup.indexOf("releaseTarget(cleanupFailure)", runtimeRelease);
int closedPublication = cleanup.indexOf("engine.closed = true", ownershipGate); int closedPublication = cleanup.indexOf("engine.closed = true", ownershipGate);
assertTrue(ownershipClose >= 0); assertTrue(ownershipClose >= 0);
assertTrue(ownershipGate > ownershipClose); assertTrue(ownershipGate > ownershipClose);
assertTrue(detachedRelease > ownershipGate); assertTrue(runtimeRelease > ownershipGate);
assertTrue(runtimeRelease > detachedRelease); assertTrue(targetRelease > runtimeRelease);
assertTrue(closedPublication > ownershipGate); assertTrue(closedPublication > ownershipGate);
} }
@@ -13,8 +13,12 @@ import java.util.List;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame; 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.doReturn;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
public class IrisComplexImplodeParityTest { public class IrisComplexImplodeParityTest {
private static Method childSelectionCreateMethod; 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 @Test
public void emptySelectionPlanMatchesLegacyEmptyBehavior() throws Exception { public void emptySelectionPlanMatchesLegacyEmptyBehavior() throws Exception {
KList<IrisBiome> options = new KList<>(); 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.IrisEngineMode;
import art.arcane.iris.engine.framework.NativeStructureVolumeMemo; import art.arcane.iris.engine.framework.NativeStructureVolumeMemo;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter; 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.mantle.EngineMantle;
import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiome;
@@ -515,9 +517,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
public void attachedHistoryRouterIsClearedWhenEngineShutdownBegins() throws Exception { public void attachedHistoryRouterIsClearedWhenEngineShutdownBegins() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D); RuntimeFixture active = runtime(1, 1D, 1D, 1D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class)); IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class); GenerationHistoryRuntimeRouter router = historyRouter(engine);
when(router.biomes()).thenReturn(mock(SavedBiomeRuntime.class));
when(router.engine()).thenReturn(engine);
engine.attachGenerationHistoryRuntimeRouter(router); engine.attachGenerationHistoryRuntimeRouter(router);
assertTrue(engine.beginShutdown()); assertTrue(engine.beginShutdown());
@@ -531,9 +531,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
public void attachedHistoryRejectsGenerationRuntimeHotload() throws Exception { public void attachedHistoryRejectsGenerationRuntimeHotload() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D); RuntimeFixture active = runtime(1, 1D, 1D, 1D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class)); IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class); GenerationHistoryRuntimeRouter router = historyRouter(engine);
when(router.biomes()).thenReturn(mock(SavedBiomeRuntime.class));
when(router.engine()).thenReturn(engine);
engine.attachGenerationHistoryRuntimeRouter(router); engine.attachGenerationHistoryRuntimeRouter(router);
IllegalStateException failure = assertThrows(IllegalStateException.class, engine::hotloadComplex); IllegalStateException failure = assertThrows(IllegalStateException.class, engine::hotloadComplex);
@@ -548,9 +546,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
RuntimeFixture active = runtime(1, 1D, 1D, 1D); RuntimeFixture active = runtime(1, 1D, 1D, 1D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class)); IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
assertNull(engine.openGenerationHistoryCoordinateScope(0, 0)); assertNull(engine.openGenerationHistoryCoordinateScope(0, 0));
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class); GenerationHistoryRuntimeRouter router = historyRouter(engine);
when(router.biomes()).thenReturn(mock(SavedBiomeRuntime.class));
when(router.engine()).thenReturn(engine);
engine.attachGenerationHistoryRuntimeRouter(router); engine.attachGenerationHistoryRuntimeRouter(router);
engine.detachGenerationHistoryRuntimeRouter(router); engine.detachGenerationHistoryRuntimeRouter(router);
@@ -710,6 +706,16 @@ public class IrisEngineGenerationRuntimeScopeTest {
assertSame(active.dimension, engine.getDimension()); 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( private static IrisEngine engine(
GenerationRuntime generationRuntime, GenerationRuntime generationRuntime,
EngineEffects effects, 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.Test;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.FutureTask; import java.util.concurrent.FutureTask;
@@ -103,6 +104,99 @@ public class GenerationAdmissionTest {
assertThrows(IllegalStateException.class, admission::beginStartupCutover); 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 { private static void awaitWaiting(Thread thread) throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L); long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
while (thread.getState() != Thread.State.WAITING && System.nanoTime() < deadline) { 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.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@@ -23,6 +24,7 @@ import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
@@ -43,6 +45,104 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
public class HydrologyTileCacheTest { 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 @Test
public void explicitAreaIsFullyQueuedBeforeExecutorCallbacksCanAddOldWork() throws Exception { public void explicitAreaIsFullyQueuedBeforeExecutorCallbacksCanAddOldWork() throws Exception {
for (boolean pregeneration : new boolean[]{false, true}) { for (boolean pregeneration : new boolean[]{false, true}) {
@@ -265,7 +365,7 @@ public class HydrologyTileCacheTest {
} }
@Test @Test
public void planCompletingAfterCloseDoesNotEnterTheSharedStudioCache() throws Exception { public void closeWaitsForActivePlansWithoutPublishingTheirSharedTiles() throws Exception {
HydrologyPlanner closingPlanner = mock(HydrologyPlanner.class); HydrologyPlanner closingPlanner = mock(HydrologyPlanner.class);
HydrologyPlanner succeedingPlanner = mock(HydrologyPlanner.class); HydrologyPlanner succeedingPlanner = mock(HydrologyPlanner.class);
HydrologyTile lateTile = mock(HydrologyTile.class); HydrologyTile lateTile = mock(HydrologyTile.class);
@@ -287,13 +387,15 @@ public class HydrologyTileCacheTest {
when(lateTile.tileSize()).thenReturn(64); when(lateTile.tileSize()).thenReturn(64);
HydrologyTileCache closing = new HydrologyTileCache(closingPlanner, 4, null, null, scope); HydrologyTileCache closing = new HydrologyTileCache(closingPlanner, 4, null, null, scope);
HydrologyTileCache succeeding = new HydrologyTileCache(succeedingPlanner, 4, null, null, scope); HydrologyTileCache succeeding = new HydrologyTileCache(succeedingPlanner, 4, null, null, scope);
ExecutorService executor = Executors.newSingleThreadExecutor(); ExecutorService executor = Executors.newFixedThreadPool(2);
try { try {
Future<HydrologyTile> late = executor.submit(() -> closing.get(key)); Future<HydrologyTile> late = executor.submit(() -> closing.get(key));
assertTrue(planningStarted.await(10L, TimeUnit.SECONDS)); assertTrue(planningStarted.await(10L, TimeUnit.SECONDS));
closing.close(); Future<?> shutdown = executor.submit(closing::close);
assertThrows(TimeoutException.class, () -> shutdown.get(100L, TimeUnit.MILLISECONDS));
allowCompletion.countDown(); allowCompletion.countDown();
assertSame(lateTile, late.get(10L, TimeUnit.SECONDS)); assertSame(lateTile, late.get(10L, TimeUnit.SECONDS));
shutdown.get(10L, TimeUnit.SECONDS);
assertEquals(0, closing.size()); assertEquals(0, closing.size());
assertSame(plannedTile, succeeding.get(key)); assertSame(plannedTile, succeeding.get(key));
verify(succeedingPlanner, times(1)).plan(key); verify(succeedingPlanner, times(1)).plan(key);
@@ -1451,7 +1553,14 @@ public class HydrologyTileCacheTest {
freshPrefetch.run(); freshPrefetch.run();
} }
oldDemand.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()); assertTrue(queued.isEmpty());
assertEquals(close ? 0 : 1, cache.size()); assertEquals(close ? 0 : 1, cache.size());
verify(planner, never()).plan(new HydrologyTileKey(2, 0)); 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.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; 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.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference; 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.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock; 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; import static org.mockito.Mockito.when;
public class MatterGeneratorConcurrencyTest { public class MatterGeneratorConcurrencyTest {
@@ -215,6 +223,167 @@ public class MatterGeneratorConcurrencyTest {
assertFalse(fixture.mantle.getChunk(0, 0).isFlagged(MantleFlag.PLANNED)); 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) { private static void await(CountDownLatch latch) {
try { try {
assertTrue(latch.await(5, TimeUnit.SECONDS)); assertTrue(latch.await(5, TimeUnit.SECONDS));
@@ -35,12 +35,15 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals; 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.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt;
@@ -114,6 +117,45 @@ public class IrisCaveCarver3DNearParityTest {
carveForcedAirField.setAccessible(true); 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 @Test
public void parallelDensityPreservesExactAndAdaptiveCavernsAndLiquids() throws Exception { public void parallelDensityPreservesExactAndAdaptiveCavernsAndLiquids() throws Exception {
try (ForkJoinPool pool = new ForkJoinPool(4)) { 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.IrisEngine;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDepositGenerator; 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.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState; 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.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test; import org.junit.Test;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.BeforeClass; import org.junit.BeforeClass;
@@ -33,14 +37,11 @@ import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString; 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.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; 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 static final class Fixture implements AutoCloseable {
private final ExecutorService executor = Executors.newSingleThreadExecutor(); private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final Engine engine; private final Engine engine;
@@ -138,16 +158,27 @@ public class IrisDepositModifierContextTest {
return null; return null;
}).when(scope).close(); }).when(scope).close();
} }
modifier = spy(new IrisDepositModifier(engine)); when(engine.getHeight()).thenReturn(16);
doAnswer(invocation -> { IrisObject clump = new IrisObject(1, 1, 1);
assertNotSame(caller, Thread.currentThread()); for (IrisDepositGenerator generator : new IrisDepositGenerator[]{dimensionDeposit, regionDeposit, biomeDeposit}) {
assertEquals(scoped, workerRuntime.get()); when(generator.getSpawnChance()).thenReturn(1D);
assertSame(engine, IrisContext.require().getEngine()); when(generator.getMinPerChunk()).thenReturn(1);
assertSame(context, IrisContext.require().getChunkContext()); when(generator.getMaxPerChunk()).thenReturn(1);
assertEquals(71L, IrisContext.require().getGenerationSessionId()); when(generator.getPerClumpSpawnChance()).thenReturn(1D);
completed.incrementAndGet(); when(generator.getPlacementScope()).thenReturn(IrisDepositPlacementScope.FULL_HEIGHT);
return null; when(generator.getHeightDistribution()).thenReturn(IrisDepositHeightDistribution.UNIFORM);
}).when(modifier).generate(any(), same(chunk), any(), any(), eq(2), eq(-3), eq(false), same(context)); 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 -> { doAnswer(invocation -> {
assertEquals(3, completed.get()); assertEquals(3, completed.get());
assertEquals(scoped ? 3 : 0, closedScopes.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.junit.Assert.assertTrue;
import static org.mockito.Answers.CALLS_REAL_METHODS; import static org.mockito.Answers.CALLS_REAL_METHODS;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
public class IrisDimensionCarvingResolverParityTest { public class IrisDimensionCarvingResolverParityTest {
private static final int MAX_CHILD_DEPTH = 32; private static final int MAX_CHILD_DEPTH = 32;
@@ -71,6 +76,31 @@ public class IrisDimensionCarvingResolverParityTest {
return data; 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 @Test
public void resolverStatefulOverloadsMatchLegacyResolverAcrossSampleGrid() { public void resolverStatefulOverloadsMatchLegacyResolverAcrossSampleGrid() {
Fixture fixture = createFixture(); 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.KList;
import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.RNG; 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.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@@ -19,6 +22,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
@@ -69,6 +73,36 @@ public class IrisObjectPlacementRunnerRegressionTest {
IrisPlatforms.unbind(); 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 @Test
public void placementFailurePropagatesToTheCaller() { public void placementFailurePropagatesToTheCaller() {
RecordingPlacer placer = new RecordingPlacer(null); 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.IdentityHashMap;
import java.util.Map; import java.util.Map;
import java.util.Set; 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.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
@@ -46,6 +49,40 @@ public class IrisObjectTransformsTileTest {
IrisPlatforms.unbind(); 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 @Test
public void enlargedTilesFollowEveryExpandedBlockAndCloneProperties() { public void enlargedTilesFollowEveryExpandedBlockAndCloneProperties() {
IrisObject source = new IrisObject(1, 1, 1); IrisObject source = new IrisObject(1, 1, 1);
@@ -186,6 +223,52 @@ public class IrisObjectTransformsTileTest {
assertEquals(8, scaled.getStates().size()); 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() { private static IrisObject asymmetricObject() {
IrisObject source = new IrisObject(7, 5, 3); IrisObject source = new IrisObject(7, 5, 3);
source.setCenter(new Vector3i(5, 1, 2)); 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.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformRegistries; 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.iris.util.project.stream.ProceduralStream;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
@@ -101,6 +102,35 @@ public class NativeStructureObjectVetoTest {
assertTrue(placer.written().isEmpty()); 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 @Test
public void pieceOverlappingOnlyTheEnvelopeStillPlaces() { public void pieceOverlappingOnlyTheEnvelopeStillPlaces() {
RecordingPlacer placer = new RecordingPlacer(engine); 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.IrisTreeFunction;
import art.arcane.iris.engine.object.IrisTreeLeafMode; import art.arcane.iris.engine.object.IrisTreeLeafMode;
import art.arcane.iris.engine.object.IrisTreeProfile; import art.arcane.iris.engine.object.IrisTreeProfile;
import art.arcane.iris.spi.PlatformBlockState;
import org.junit.Test; import org.junit.Test;
import java.util.HashSet; import java.util.HashSet;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; 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.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ProceduralTreeGeneratorTest { public class ProceduralTreeGeneratorTest {
@@ -268,4 +273,23 @@ public class ProceduralTreeGeneratorTest {
distances.get(new TreeBlockCanvas.Vec(x, 0, 0))); 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; 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.BlockDataMergeSupport;
import art.arcane.iris.engine.object.IrisObjectRotation; import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.engine.object.TileData; import art.arcane.iris.engine.object.TileData;
@@ -57,6 +58,8 @@ public final class StubPlatform implements IrisPlatform {
} }
public static void bindGenerationStateHandlers() { public static void bindGenerationStateHandlers() {
DecoratorPlatformHooks.bind((state, hunk, rX, rZ, x, y, z, mantle) -> state,
(surface, upward) -> surface.isOccluding());
IrisObjectRotation.bindPlatformRotator(StubPlatform::rotateState); IrisObjectRotation.bindPlatformRotator(StubPlatform::rotateState);
BlockDataMergeSupport.bindPlatformMerger(StubPlatform::mergeStates); BlockDataMergeSupport.bindPlatformMerger(StubPlatform::mergeStates);
TileData.bindPlatformReader(StubTileData::read); TileData.bindPlatformReader(StubTileData::read);
@@ -1,8 +1,10 @@
package art.arcane.iris.probe; package art.arcane.iris.probe;
import art.arcane.iris.engine.decorator.IrisSpeleothems;
import art.arcane.iris.engine.object.IrisObjectRotation; import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.hunk.Hunk;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
@@ -80,6 +82,18 @@ public final class StubPlatformStateTest {
assertFalse(state("minecraft:brain_coral_fan").isWaterLogged()); 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) { private PlatformBlockState state(String key) {
return IrisPlatforms.get().registries().block(key); return IrisPlatforms.get().registries().block(key);
} }