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