Improved world generation

- Terrain and cave scans skip repeated work
- River and structure lookups reuse cached results
- Chunk history saves share disk writes
- History lookups continue during disk writes
- Failed saves retain their original errors
- Deposit failures stop incomplete chunks
- Tests check output and save safety
This commit is contained in:
Brian Neumann-Fopiano
2026-09-07 03:17:09 -04:00
parent 151677f92f
commit 0dcd1900be
41 changed files with 4398 additions and 191 deletions
@@ -3,6 +3,7 @@ package art.arcane.iris.nativegen;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.NativeStructureVolume;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.volmlib.util.collection.KList;
import net.minecraft.SharedConstants;
import net.minecraft.core.HolderSet;
@@ -24,6 +25,7 @@ import java.lang.reflect.Proxy;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CopyOnWriteArraySet;
@@ -39,8 +41,10 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
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 NativeStructureVolumeIndexTest {
private static final String STRUCTURE_KEY = "minecraft:swamp_hut";
@@ -114,6 +118,47 @@ public class NativeStructureVolumeIndexTest {
assertEquals(867, resolver.resolutions());
}
@Test
public void pinnedRuntimeReusesCachedOriginsWithoutOpeningCoordinateScopes() throws Exception {
ScopedEngine scoped = scopedEngine(true, 1);
CountingResolver resolver = new CountingResolver(0, 0);
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
KList<NativeStructureVolume> first = index.resolve(scoped.engine(), 0, 0, 15, 15);
assertEquals(289, scoped.scopes().get());
index.resolve(scoped.engine(), 16, 0, 31, 15);
assertEquals(306, resolver.resolutions());
assertEquals(306, scoped.scopes().get());
assertEquals(first, index.resolve(scoped.engine(), 0, 0, 15, 15));
assertEquals(306, scoped.scopes().get());
}
@Test
public void absentOrMismatchedRuntimeOwnershipKeepsCoordinateRouting() throws Exception {
ScopedEngine detached = scopedEngine(true, 1);
when(detached.engine().getGenerationHistoryRuntimeRouter()).thenReturn(Optional.empty());
for (ScopedEngine scoped : List.of(scopedEngine(false, 1), scopedEngine(true, 0),
scopedEngine(true, 2), detached)) {
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
index.resolve(scoped.engine(), 0, 0, 15, 15);
index.resolve(scoped.engine(), 16, 0, 31, 15);
assertEquals(578, scoped.scopes().get());
}
}
@Test
public void retiredPinnedOriginsRebuildThroughCoordinateScopes() throws Exception {
ScopedEngine scoped = scopedEngine(true, 1);
CountingResolver resolver = new CountingResolver(0, 0);
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
KList<NativeStructureVolume> first = index.resolve(scoped.engine(), 0, 0, 15, 15);
index.evictRuntime(1);
assertEquals(first, index.resolve(scoped.engine(), 0, 0, 15, 15));
assertEquals(578, resolver.resolutions());
assertEquals(578, scoped.scopes().get());
}
@Test
public void invalidatedIndexReceivesRuntimeRetirementAndDetachesOnUninstall() throws Exception {
IrisEngine engine = mock(IrisEngine.class);
@@ -313,6 +358,27 @@ public class NativeStructureVolumeIndexTest {
assertTrue(source.contains("queryCache.keySet().removeIf(key -> key.runtimeId() == runtimeId)"));
}
private static ScopedEngine scopedEngine(boolean scoped, int ownerRuntimeId) throws Exception {
IrisEngine engine = mock(IrisEngine.class);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
when(engine.hasGenerationRuntimeScope()).thenReturn(scoped);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(engine.getCacheID()).thenReturn(1);
if (ownerRuntimeId != 0) {
IrisEngine.GenerationRuntimeBinding binding = mock(IrisEngine.GenerationRuntimeBinding.class);
when(binding.runtimeId()).thenReturn(ownerRuntimeId);
when(router.currentRuntimeOwnership()).thenReturn(Optional.of(
new GenerationHistoryRuntimeRouter.RuntimeOwnership(1L, binding)));
}
AtomicInteger scopes = new AtomicInteger();
when(engine.openGenerationHistoryCoordinateScope(anyInt(), anyInt())).thenAnswer(invocation -> {
scopes.incrementAndGet();
return null;
});
return new ScopedEngine(engine, scopes);
}
@SuppressWarnings("unchecked")
private static Map<Engine, NativeStructureVolumeIndex> installedIndexes() throws Exception {
Field indexes = NativeStructureVolumeIndex.class.getDeclaredField("INDEXES");
@@ -439,4 +505,6 @@ public class NativeStructureVolumeIndexTest {
15, 255, 15));
}
}
private record ScopedEngine(IrisEngine engine, AtomicInteger scopes) {
}
}
@@ -190,10 +190,11 @@ public final class NativeStructureVolumeIndex {
int minZ = chunkZ << 4;
int maxX = minX + 15;
int maxZ = minZ + 15;
Integer pinnedRuntimeId = pinnedRuntimeId(engine);
KList<NativeStructureVolume> volumes = null;
for (int originX = chunkX - ORIGIN_REACH_CHUNKS; originX <= chunkX + ORIGIN_REACH_CHUNKS; originX++) {
for (int originZ = chunkZ - ORIGIN_REACH_CHUNKS; originZ <= chunkZ + ORIGIN_REACH_CHUNKS; originZ++) {
for (NativeStructureVolume volume : originVolumes(engine, originX, originZ)) {
for (NativeStructureVolume volume : scopedOriginVolumes(engine, originX, originZ, pinnedRuntimeId)) {
if (!volume.intersectsRect(minX, minZ, maxX, maxZ)) {
continue;
}
@@ -207,6 +208,34 @@ public final class NativeStructureVolumeIndex {
return volumes == null ? NativeStructureVolume.NONE : volumes;
}
private static Integer pinnedRuntimeId(Engine engine) {
if (!(engine instanceof IrisEngine irisEngine) || !irisEngine.hasGenerationRuntimeScope()) {
return null;
}
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null);
if (router == null) {
return null;
}
GenerationHistoryRuntimeRouter.RuntimeOwnership ownership = router.currentRuntimeOwnership().orElse(null);
int runtimeId = runtimeId(engine);
return ownership != null && ownership.binding().runtimeId() == runtimeId ? runtimeId : null;
}
private KList<NativeStructureVolume> scopedOriginVolumes(
Engine engine, int chunkX, int chunkZ, Integer pinnedRuntimeId
) {
if (pinnedRuntimeId != null) {
synchronized (originCache) {
KList<NativeStructureVolume> cached = originCache.get(
new RuntimeChunkKey(pinnedRuntimeId, chunkKey(chunkX, chunkZ)));
if (cached != null) {
return cached;
}
}
}
return originVolumes(engine, chunkX, chunkZ);
}
static long originWindowStripeMask(int chunkX, int chunkZ) {
int minCellX = Math.floorDiv(chunkX - ORIGIN_REACH_CHUNKS, ORIGIN_WINDOW_DIAMETER);
int maxCellX = Math.floorDiv(chunkX + ORIGIN_REACH_CHUNKS, ORIGIN_WINDOW_DIAMETER);
@@ -131,6 +131,26 @@ public class IrisEngineMantle implements EngineMantle {
return getComponents().getLast().passChunkRadius();
}
@Override
public void cleanupChunksCoveredBy(int newRealX, int newRealZ, boolean force, ChunkCleanupCallback callback) {
Objects.requireNonNull(callback, "callback");
int radius = Math.max(getRadius(), getRealRadius());
MissingRealChunk missingChunk = radius > 0 ? new MissingRealChunk() : null;
for (int offsetX = -radius; offsetX <= radius; offsetX++) {
int candidateX = newRealX + offsetX;
for (int offsetZ = -radius; offsetZ <= radius; offsetZ++) {
int candidateZ = newRealZ + offsetZ;
if (getMantle().hasFlag(candidateX, candidateZ, MantleFlag.CLEANED)
|| !isCovered(candidateX, candidateZ, missingChunk)) {
continue;
}
if (cleanupCoveredChunk(candidateX, candidateZ, force)) {
callback.onChunkCleaned(candidateX, candidateZ);
}
}
}
}
@Override
public List<MantlePass> getComponents() {
return componentsCache.aquire(() -> {
@@ -228,6 +248,33 @@ public class IrisEngineMantle implements EngineMantle {
return Objects.requireNonNull(storageDirectory, "mantle storage directory").toAbsolutePath().normalize();
}
private boolean isCovered(int x, int z, MissingRealChunk missingChunk) {
int radius = Math.max(getRadius(), getRealRadius());
if (missingChunk != null && missingChunk.known
&& Math.abs((long) missingChunk.x - x) <= radius
&& Math.abs((long) missingChunk.z - z) <= radius) {
if (!getMantle().hasFlag(missingChunk.x, missingChunk.z, MantleFlag.REAL)) {
return false;
}
missingChunk.known = false;
}
for (int offsetX = -radius; offsetX <= radius; offsetX++) {
for (int offsetZ = -radius; offsetZ <= radius; offsetZ++) {
int chunkX = x + offsetX;
int chunkZ = z + offsetZ;
if (!getMantle().hasFlag(chunkX, chunkZ, MantleFlag.REAL)) {
if (missingChunk != null) {
missingChunk.x = chunkX;
missingChunk.z = chunkZ;
missingChunk.known = true;
}
return false;
}
}
}
return true;
}
private static Mantle<Matter> createMantle(
Engine engine,
Path storageDirectory,
@@ -467,4 +514,10 @@ public class IrisEngineMantle implements EngineMantle {
}
};
}
private static final class MissingRealChunk {
private int x;
private int z;
private boolean known;
}
}
@@ -5,6 +5,7 @@ import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Comparator;
import java.util.Objects;
import java.util.Set;
@@ -361,11 +362,11 @@ public final class ChunkGenerationSemantics {
this.chunkX = chunkX;
this.chunkZ = chunkZ;
this.activationId = activationId;
surfaceBiomeKeys = new TreeSet<>();
caveBiomeKeys = new TreeSet<>();
regionKeys = new TreeSet<>();
riverProfileKeys = new TreeSet<>();
objectKeys = new TreeSet<>();
surfaceBiomeKeys = new HashSet<>();
caveBiomeKeys = new HashSet<>();
regionKeys = new HashSet<>();
riverProfileKeys = new HashSet<>();
objectKeys = new HashSet<>();
riverFeatures = new TreeSet<>(RIVER_FEATURE_COMPARATOR);
structures = new TreeSet<>(STRUCTURE_COMPARATOR);
pointsOfInterest = new TreeSet<>(POI_COMPARATOR);
@@ -519,8 +520,11 @@ public final class ChunkGenerationSemantics {
}
private static void addKey(Set<String> target, String key, String kind) {
if (target.contains(Objects.requireNonNull(key, "key"))) {
return;
}
String validated = requireResourceKey(key);
if (target.size() >= MAX_KEYS_PER_KIND && !target.contains(validated)) {
if (target.size() >= MAX_KEYS_PER_KIND) {
throw new IllegalArgumentException(
"A chunk cannot contain more than " + MAX_KEYS_PER_KIND + " " + kind + " keys"
);
@@ -15,6 +15,7 @@ import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashMap;
@@ -23,11 +24,14 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.stream.Stream;
public final class GenerationHistory {
private static final int MAXIMUM_CACHED_BOUNDARIES = 4;
private static final int MAXIMUM_CACHED_TERRAIN_SIGNATURES = 4;
private static final int MAXIMUM_SEMANTIC_BATCH = 32;
private static final int MAXIMUM_PENDING_SEMANTIC_CLAIMS = 128;
private final GenerationHistoryPaths paths;
private final GenerationPackRepository packs;
@@ -41,6 +45,8 @@ public final class GenerationHistory {
private final GenerationAdmission admission;
private final Map<Long, GenerationBoundary> boundaryCache;
private final Map<Long, TerrainBoundarySignatureStore.Snapshot> terrainSignatureCache;
private final ArrayBlockingQueue<PendingSemanticClaim> pendingSemanticClaims =
new ArrayBlockingQueue<>(MAXIMUM_PENDING_SEMANTIC_CLAIMS);
private GenerationHistory(
GenerationHistoryPaths paths,
@@ -594,12 +600,64 @@ public final class GenerationHistory {
}
}
public synchronized boolean claimGeneratedSemantics(
public boolean claimGeneratedSemantics(
GenerationStage stage,
ChunkGenerationSemantics update
) throws IOException {
GenerationStage requiredStage = Objects.requireNonNull(stage, "generation stage");
ChunkGenerationSemantics requiredUpdate = Objects.requireNonNull(update, "update");
PendingSemanticClaim pending = new PendingSemanticClaim(
Objects.requireNonNull(stage, "generation stage"),
Objects.requireNonNull(update, "update"));
while (!pendingSemanticClaims.offer(pending)) {
synchronized (this) {
persistPendingSemanticClaims();
}
}
synchronized (this) {
while (!pending.claim.completed) {
persistPendingSemanticClaims();
}
return pending.claim.result();
}
}
private void persistPendingSemanticClaims() {
List<PendingSemanticClaim> batch = new ArrayList<>(MAXIMUM_SEMANTIC_BATCH);
List<GenerationSemanticIndex.Claim> claims = new ArrayList<>(MAXIMUM_SEMANTIC_BATCH);
PendingSemanticClaim current = null;
try {
for (int count = 0; count < MAXIMUM_SEMANTIC_BATCH; count++) {
current = pendingSemanticClaims.poll();
if (current == null) {
break;
}
batch.add(current);
try {
validateSemanticClaim(current.stage, current.claim.update);
claims.add(current.claim);
} catch (RuntimeException failure) {
current.claim.fail(failure);
}
current = null;
}
semantics.claimAndPersistBatch(claims);
} catch (RuntimeException | Error failure) {
boolean reported = false;
if (current != null) {
reported = current.claim.fail(failure);
}
for (PendingSemanticClaim pending : batch) {
reported |= pending.claim.fail(failure);
}
while ((current = pendingSemanticClaims.poll()) != null) {
reported |= current.claim.fail(failure);
}
if (!reported) {
throw failure;
}
}
}
private void validateSemanticClaim(GenerationStage requiredStage, ChunkGenerationSemantics requiredUpdate) {
requiredStage.requireOpen(this);
if (!requiredUpdate.sealed()) {
throw new IllegalArgumentException("Generated semantics must be sealed.");
@@ -611,14 +669,13 @@ public final class GenerationHistory {
if (requiredUpdate.activationId() != requiredStage.activation().activationId()) {
throw new IllegalArgumentException("Generated semantics activation does not match the open stage.");
}
return semantics.claimAndPersist(requiredUpdate);
}
public synchronized void forEachRecordedSemantic(GenerationSemanticIndex.RecordConsumer consumer) throws IOException {
semantics.forEachRecord(consumer);
}
public synchronized Optional<ChunkGenerationSemantics> semantics(int chunkX, int chunkZ) {
public Optional<ChunkGenerationSemantics> semantics(int chunkX, int chunkZ) {
return semantics.get(chunkX, chunkZ);
}
@@ -663,7 +720,7 @@ public final class GenerationHistory {
}
}
public synchronized GenerationActivation resolveActivation(int chunkX, int chunkZ) {
public GenerationActivation resolveActivation(int chunkX, int chunkZ) {
long activationId = ownership.resolve(
chunkX,
chunkZ,
@@ -1108,6 +1165,16 @@ public final class GenerationHistory {
};
}
private static final class PendingSemanticClaim {
private final GenerationStage stage;
private final GenerationSemanticIndex.Claim claim;
private PendingSemanticClaim(GenerationStage stage, ChunkGenerationSemantics update) {
this.stage = stage;
this.claim = new GenerationSemanticIndex.Claim(update);
}
}
public static final class GenerationStage implements AutoCloseable {
private final GenerationHistory owner;
private final int chunkX;
@@ -17,9 +17,8 @@ import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.matter.MatterStructurePOI;
import java.util.HashSet;
import java.util.BitSet;
import java.util.Objects;
import java.util.Set;
public final class GenerationSemanticCapture {
private static final int CHUNK_SIZE = 16;
@@ -137,7 +136,7 @@ public final class GenerationSemanticCapture {
Mantle<Matter> mantle = engine.getMantle().getMantle();
int minimumX = Math.multiplyExact(chunkX, CHUNK_SIZE);
int minimumZ = Math.multiplyExact(chunkZ, CHUNK_SIZE);
Set<CavePosition> resolvedCaves = new HashSet<>();
BitSet resolvedCaves = new BitSet();
mantle.iterateChunk(chunkX, chunkZ, MatterCavern.class, (localX, y, localZ, cavern) -> {
if (!caveSpace.isOpen(localX, y, localZ)) {
return;
@@ -197,16 +196,17 @@ public final class GenerationSemanticCapture {
private static void captureCave(
Engine engine,
ChunkGenerationSemantics.Builder semantics,
Set<CavePosition> resolvedCaves,
BitSet resolvedCaves,
int blockX,
int y,
int blockZ,
String explicitBiomeKey
) {
CavePosition position = new CavePosition(blockX, y, blockZ);
if (!resolvedCaves.add(position)) {
int position = (y << 8) | ((blockX & 15) << 4) | (blockZ & 15);
if (resolvedCaves.get(position)) {
return;
}
resolvedCaves.set(position);
if (explicitBiomeKey != null && !explicitBiomeKey.isBlank()) {
semantics.addCaveBiome(explicitBiomeKey);
return;
@@ -259,6 +259,4 @@ public final class GenerationSemanticCapture {
boolean isOpen(int localX, int internalY, int localZ);
}
private record CavePosition(int x, int y, int z) {
}
}
@@ -42,6 +42,7 @@ import java.util.Optional;
import java.util.OptionalLong;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Predicate;
import java.util.regex.Matcher;
@@ -85,10 +86,14 @@ public final class GenerationSemanticIndex {
private final Long2IntOpenHashMap journalEntries;
private final Long2IntOpenHashMap regionRecordCounts;
private final ReentrantReadWriteLock lock;
private final ReentrantLock publicationLock;
private final ShardPublisher publisher;
private final PointerPublisher pointerPublisher;
private final CatalogPublisher catalogPublisher;
private long regionDecodeCount;
private volatile IOException journalFailure;
private long appendingRegionKey;
private RegionShard appendingBase;
private GenerationSemanticIndex(
Path dimensionRoot,
@@ -104,6 +109,7 @@ public final class GenerationSemanticIndex {
journalEntries = new Long2IntOpenHashMap();
regionRecordCounts = new Long2IntOpenHashMap();
lock = new ReentrantReadWriteLock();
publicationLock = new ReentrantLock();
this.publisher = publisher;
this.pointerPublisher = pointerPublisher;
this.catalogPublisher = catalogPublisher;
@@ -174,36 +180,102 @@ public final class GenerationSemanticIndex {
if (!requiredClaim.sealed()) {
throw new IllegalArgumentException("Generation semantic claims must be sealed.");
}
Claim pending = new Claim(requiredClaim);
claimAndPersistBatch(List.of(pending));
return pending.result();
}
void claimAndPersistBatch(List<Claim> claims) {
lock.writeLock().lock();
publicationLock.lock();
try {
ChunkGenerationSemantics existing = getRecordLocked(
requiredClaim.chunkX(),
requiredClaim.chunkZ()
);
ChunkGenerationSemantics merged = existing == null
? requiredClaim
: existing.merge(requiredClaim);
if (merged == existing || merged.equals(existing)) {
return false;
requireJournalUsable();
Map<Long, List<Claim>> batches = new LinkedHashMap<>();
for (Claim pending : claims) {
if (!pending.update.sealed()) {
pending.fail(new IllegalArgumentException("Generation semantic claims must be sealed."));
continue;
}
long regionKey = packRegion(pending.update.chunkX() >> 5, pending.update.chunkZ() >> 5);
batches.computeIfAbsent(regionKey, ignored -> new ArrayList<>()).add(pending);
}
int regionX = requiredClaim.chunkX() >> 5;
int regionZ = requiredClaim.chunkZ() >> 5;
long regionKey = packRegion(regionX, regionZ);
RegionShard baseRegion = loadRegionLocked(regionKey, regionX, regionZ);
RegionShard nextRegion = baseRegion.withRecord(merged);
ensureStorageDirectory();
SemanticJournal.append(directory, regionX, regionZ, merged);
cacheRegion(regionKey, nextRegion);
if (existing == null) {
regionRecordCounts.addTo(regionKey, 1);
for (Map.Entry<Long, List<Claim>> batch : batches.entrySet()) {
persistClaimBatch(batch.getKey(), batch.getValue());
}
journalEntries.addTo(regionKey, 1);
return true;
} catch (IOException failure) {
for (Claim pending : claims) {
pending.fail(failure);
}
} catch (RuntimeException | Error failure) {
journalFailure = new IOException("Generation semantic batch failed unexpectedly.", failure);
for (Claim pending : claims) {
pending.fail(failure);
}
throw failure;
} finally {
publicationLock.unlock();
lock.writeLock().unlock();
}
}
private void persistClaimBatch(long regionKey, List<Claim> claims) {
try {
requireJournalUsable();
int regionX = regionX(regionKey);
int regionZ = regionZ(regionKey);
RegionShard baseRegion = loadRegionLocked(regionKey, regionX, regionZ);
RegionShard nextRegion = baseRegion;
List<ChunkGenerationSemantics> updates = new ArrayList<>(claims.size());
for (Claim pending : claims) {
ChunkGenerationSemantics update = pending.update;
ChunkGenerationSemantics existing = nextRegion.get(update.chunkX(), update.chunkZ());
ChunkGenerationSemantics merged;
try {
merged = existing == null ? update : existing.merge(update);
} catch (IllegalArgumentException | IllegalStateException failure) {
pending.fail(failure);
continue;
}
if (merged == existing || merged.equals(existing)) {
pending.completed = existing == baseRegion.get(update.chunkX(), update.chunkZ());
continue;
}
nextRegion = nextRegion.withRecord(merged);
updates.add(merged);
pending.persisted = true;
}
if (updates.isEmpty()) {
return;
}
ensureStorageDirectory();
appendingRegionKey = regionKey;
appendingBase = baseRegion;
publicationLock.unlock();
try {
SemanticJournal.append(directory, regionX, regionZ, updates);
} catch (JournalAppendFailure failure) {
journalFailure = failure;
throw failure;
} catch (RuntimeException | Error failure) {
journalFailure = new IOException("Generation semantic batch failed unexpectedly.", failure);
throw failure;
} finally {
publicationLock.lock();
appendingBase = null;
}
cacheRegion(regionKey, nextRegion);
regionRecordCounts.put(regionKey, nextRegion.recordCount());
journalEntries.addTo(regionKey, updates.size());
for (Claim pending : claims) {
pending.completed = true;
}
} catch (IOException failure) {
for (Claim pending : claims) {
pending.fail(failure);
}
}
}
public int discardUnstoredClaims(WorldChunkInventory stored, Set<Long> activationIds) throws IOException {
Objects.requireNonNull(stored, "stored chunks");
Set<Long> selected = Set.copyOf(activationIds);
@@ -213,6 +285,7 @@ public final class GenerationSemanticIndex {
}
}
lock.writeLock().lock();
publicationLock.lock();
try {
compactJournals();
int removed = 0;
@@ -248,19 +321,23 @@ public final class GenerationSemanticIndex {
}
return removed;
} finally {
publicationLock.unlock();
lock.writeLock().unlock();
}
}
public void compactJournals() throws IOException {
lock.writeLock().lock();
publicationLock.lock();
try {
requireJournalUsable();
ArrayList<Long> regionKeys = new ArrayList<>(journalEntries.keySet());
regionKeys.sort(GenerationSemanticIndex::compareRegionKeys);
for (long regionKey : regionKeys) {
compactJournal(regionKey);
}
} finally {
publicationLock.unlock();
lock.writeLock().unlock();
}
}
@@ -271,7 +348,9 @@ public final class GenerationSemanticIndex {
}
SealedClaimConsumer requiredConsumer = Objects.requireNonNull(consumer, "sealed claim consumer");
lock.writeLock().lock();
publicationLock.lock();
try {
requireJournalUsable();
for (long regionKey : allRegionKeys()) {
if (!loadSummaryLocked(regionKey).hasSealedActivation(activationId)) {
continue;
@@ -288,12 +367,13 @@ public final class GenerationSemanticIndex {
}
}
} finally {
publicationLock.unlock();
lock.writeLock().unlock();
}
}
public boolean hasSealedClaim(int chunkX, int chunkZ, long activationId) {
lock.writeLock().lock();
publicationLock.lock();
try {
ChunkGenerationSemantics semantics = getRecordLocked(chunkX, chunkZ);
return semantics != null
@@ -302,14 +382,16 @@ public final class GenerationSemanticIndex {
} catch (IOException error) {
throw new IllegalStateException("Unable to resolve a sealed generation semantic claim.", error);
} finally {
lock.writeLock().unlock();
publicationLock.unlock();
}
}
public boolean recordAndPersist(ChunkGenerationSemantics update) throws IOException {
ChunkGenerationSemantics requiredUpdate = Objects.requireNonNull(update, "update");
lock.writeLock().lock();
publicationLock.lock();
try {
requireJournalUsable();
ChunkGenerationSemantics existing = getRecordLocked(
requiredUpdate.chunkX(),
requiredUpdate.chunkZ()
@@ -345,34 +427,39 @@ public final class GenerationSemanticIndex {
deleteUnreferencedShard(regionX, regionZ, previousHash, shardHash);
return true;
} finally {
publicationLock.unlock();
lock.writeLock().unlock();
}
}
public Optional<ChunkGenerationSemantics> get(int chunkX, int chunkZ) {
lock.writeLock().lock();
publicationLock.lock();
try {
return Optional.ofNullable(getRecordLocked(chunkX, chunkZ));
} catch (IOException error) {
throw new IllegalStateException("Unable to load generation semantics for chunk "
+ chunkX + "," + chunkZ + ".", error);
} finally {
lock.writeLock().unlock();
publicationLock.unlock();
}
}
public int recordCount() {
lock.readLock().lock();
publicationLock.lock();
try {
return totalRecordCountLocked();
} finally {
publicationLock.unlock();
lock.readLock().unlock();
}
}
public List<ChunkGenerationSemantics> recordsSnapshot() {
lock.writeLock().lock();
publicationLock.lock();
try {
requireJournalUsable();
List<ChunkGenerationSemantics> snapshot = new ArrayList<>(totalRecordCountLocked());
for (long regionKey : allRegionKeys()) {
RegionShard region = loadRegionLocked(
@@ -388,6 +475,7 @@ public final class GenerationSemanticIndex {
} catch (IOException error) {
throw new IllegalStateException("Unable to load generation semantic snapshot.", error);
} finally {
publicationLock.unlock();
lock.writeLock().unlock();
}
}
@@ -395,7 +483,9 @@ public final class GenerationSemanticIndex {
public void forEachRecord(RecordConsumer consumer) throws IOException {
RecordConsumer requiredConsumer = Objects.requireNonNull(consumer, "consumer");
lock.writeLock().lock();
publicationLock.lock();
try {
requireJournalUsable();
for (long regionKey : allRegionKeys()) {
RegionShard region = loadRegionLocked(
regionKey,
@@ -407,33 +497,40 @@ public final class GenerationSemanticIndex {
}
}
} finally {
publicationLock.unlock();
lock.writeLock().unlock();
}
}
int cachedRegionCount() {
lock.readLock().lock();
publicationLock.lock();
try {
return regions.size();
} finally {
publicationLock.unlock();
lock.readLock().unlock();
}
}
long regionDecodeCount() {
lock.readLock().lock();
publicationLock.lock();
try {
return regionDecodeCount;
} finally {
publicationLock.unlock();
lock.readLock().unlock();
}
}
int cachedSummaryCount() {
lock.readLock().lock();
publicationLock.lock();
try {
return summaries.size();
} finally {
publicationLock.unlock();
lock.readLock().unlock();
}
}
@@ -452,12 +549,14 @@ public final class GenerationSemanticIndex {
"eligibility"
);
lock.writeLock().lock();
publicationLock.lock();
try {
if (requiredQuery.kind() == SemanticKind.STRUCTURE) {
return findNearestStructure(requiredQuery, requiredEligibility);
}
return findNearestChunk(requiredQuery, requiredEligibility);
} finally {
publicationLock.unlock();
lock.writeLock().unlock();
}
}
@@ -472,6 +571,7 @@ public final class GenerationSemanticIndex {
"eligibility"
);
lock.writeLock().lock();
publicationLock.lock();
try {
RiverMatch best = null;
BigInteger bestDistance = null;
@@ -540,6 +640,7 @@ public final class GenerationSemanticIndex {
} catch (IOException error) {
throw new IllegalStateException("Unable to search recorded river semantics.", error);
} finally {
publicationLock.unlock();
lock.writeLock().unlock();
}
}
@@ -874,6 +975,7 @@ public final class GenerationSemanticIndex {
}
private ChunkGenerationSemantics getRecordLocked(int chunkX, int chunkZ) throws IOException {
requireJournalUsable();
long regionKey = packRegion(chunkX >> 5, chunkZ >> 5);
if (!regionRecordCounts.containsKey(regionKey)) {
return null;
@@ -882,6 +984,10 @@ public final class GenerationSemanticIndex {
}
private RegionShard loadRegionLocked(long regionKey, int regionX, int regionZ) throws IOException {
requireJournalUsable();
if (appendingBase != null && appendingRegionKey == regionKey) {
return appendingBase;
}
RegionShard cached = regions.get(regionKey);
if (cached != null) {
return cached;
@@ -898,6 +1004,7 @@ public final class GenerationSemanticIndex {
}
private RegionSummary loadSummaryLocked(long regionKey) throws IOException {
requireJournalUsable();
RegionSummary cached = summaries.get(regionKey);
if (cached != null) {
return cached;
@@ -959,6 +1066,12 @@ public final class GenerationSemanticIndex {
}
}
private void requireJournalUsable() throws IOException {
if (journalFailure != null) {
throw new IOException("Generation semantic journal requires recovery before further access.", journalFailure);
}
}
private List<Long> allRegionKeys() {
ArrayList<Long> ordered = new ArrayList<>(regionRecordCounts.keySet());
ordered.sort(GenerationSemanticIndex::compareRegionKeys);
@@ -1165,7 +1278,8 @@ public final class GenerationSemanticIndex {
ChunkGenerationSemantics.BlockPosition origin,
int maximumChunkRadius,
boolean orderByBlockDistance
) {
) throws IOException {
requireJournalUsable();
int originChunkX = Math.floorDiv(origin.x(), 16);
int originChunkZ = Math.floorDiv(origin.z(), 16);
long minimumChunkX = (long) originChunkX - maximumChunkRadius;
@@ -1760,6 +1874,40 @@ public final class GenerationSemanticIndex {
void publish(Path directory, Set<Long> regionKeys) throws IOException;
}
static final class Claim {
final ChunkGenerationSemantics update;
boolean persisted;
boolean completed;
private Throwable failure;
Claim(ChunkGenerationSemantics update) {
this.update = update;
}
boolean fail(Throwable failure) {
if (!completed) {
this.failure = failure;
persisted = false;
completed = true;
return true;
}
return this.failure == failure;
}
boolean result() throws IOException {
if (failure instanceof IOException ioFailure) {
throw ioFailure;
}
if (failure instanceof RuntimeException runtimeFailure) {
throw runtimeFailure;
}
if (failure instanceof Error error) {
throw error;
}
return persisted;
}
}
private static final class SemanticJournal {
private static final int ENTRY_MAGIC = 0x4953574C;
private static final int HEADER_BYTES = Integer.BYTES * 2;
@@ -1768,8 +1916,65 @@ public final class GenerationSemanticIndex {
Path directory,
int regionX,
int regionZ,
ChunkGenerationSemantics claim
List<ChunkGenerationSemantics> claims
) throws IOException {
Path file = directory.resolve(journalFileName(regionX, regionZ));
boolean created = !Files.exists(file, LinkOption.NOFOLLOW_LINKS);
if (!created && !Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) {
throw invalid(file, "path is not a regular file");
}
FileChannel opened = FileChannel.open(
file,
StandardOpenOption.CREATE,
StandardOpenOption.WRITE
);
boolean uncertain = created;
boolean restored = false;
try (FileChannel channel = opened) {
long size = channel.size();
if (size > MAX_JOURNAL_BYTES) {
throw invalid(file, "journal exceeds its size limit");
}
uncertain = true;
try {
channel.position(size);
long written = size;
for (ChunkGenerationSemantics claim : claims) {
ByteBuffer entry = encodeEntry(regionX, regionZ, claim);
if (written > MAX_JOURNAL_BYTES - entry.remaining()) {
throw invalid(file, "journal exceeds its size limit");
}
written += entry.remaining();
while (entry.hasRemaining()) {
channel.write(entry);
}
}
channel.force(true);
if (created) {
RegionShard.forceDirectory(directory);
}
} catch (IOException failure) {
try {
channel.truncate(size);
channel.force(true);
if (created) {
RegionShard.forceDirectory(directory);
}
restored = true;
} catch (IOException rollback) {
failure.addSuppressed(rollback);
}
throw failure;
}
} catch (IOException failure) {
if (uncertain && !restored) {
throw new JournalAppendFailure(file, failure);
}
throw failure;
}
}
private static ByteBuffer encodeEntry(int regionX, int regionZ, ChunkGenerationSemantics claim) throws IOException {
RegionShard single = new RegionShard(regionX, regionZ).withRecord(claim);
byte[] payload = single.encode();
CRC32 checksum = new CRC32();
@@ -1780,30 +1985,7 @@ public final class GenerationSemanticIndex {
entry.put(payload);
entry.putInt((int) checksum.getValue());
entry.flip();
Path file = directory.resolve(journalFileName(regionX, regionZ));
boolean created = !Files.exists(file, LinkOption.NOFOLLOW_LINKS);
if (!created && !Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) {
throw invalid(file, "path is not a regular file");
}
try (FileChannel channel = FileChannel.open(
file,
StandardOpenOption.CREATE,
StandardOpenOption.WRITE
)) {
long size = channel.size();
if (size > MAX_JOURNAL_BYTES - entry.remaining()) {
throw invalid(file, "journal exceeds its size limit");
}
channel.position(size);
while (entry.hasRemaining()) {
channel.write(entry);
}
channel.force(true);
}
if (created) {
RegionShard.forceDirectory(directory);
}
return entry;
}
private static Replay replay(Path file, int expectedRegionX, int expectedRegionZ) throws IOException {
@@ -1916,6 +2098,12 @@ public final class GenerationSemanticIndex {
}
}
private static final class JournalAppendFailure extends IOException {
private JournalAppendFailure(Path file, IOException cause) {
super("Generation semantic journal could not confirm a durable append or rollback: " + file, cause);
}
}
private static final class ShardPointer {
private static final int MAGIC = 0x49535054;
private static final int FORMAT_VERSION = 1;
@@ -7,7 +7,9 @@ import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.OptionalInt;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
@@ -24,6 +26,7 @@ public final class NativeTerrainReceipt {
public static byte[] encode(SavedTerrainChunk chunk, long activationId, String epochId) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
UtfCache strings = new UtfCache();
try (DataOutputStream output = new DataOutputStream(new BufferedOutputStream(new GZIPOutputStream(bytes)))) {
output.writeInt(MAGIC);
output.writeInt(VERSION);
@@ -37,7 +40,7 @@ public final class NativeTerrainReceipt {
if (!chunk.hasColumn(x, z)) {
continue;
}
writeColumn(output, chunk.column((chunk.chunkX() << 4) + x, (chunk.chunkZ() << 4) + z));
writeColumn(output, chunk.column((chunk.chunkX() << 4) + x, (chunk.chunkZ() << 4) + z), strings);
}
}
}
@@ -109,7 +112,7 @@ public final class NativeTerrainReceipt {
}
}
private static void writeColumn(DataOutputStream output, TerrainBoundarySignature signature) throws IOException {
private static void writeColumn(DataOutputStream output, TerrainBoundarySignature signature, UtfCache strings) throws IOException {
output.writeInt(signature.surfaceHeight());
output.writeInt(signature.oceanFloorHeight());
output.writeInt(signature.fluidHeight().orElse(-1));
@@ -119,15 +122,15 @@ public final class NativeTerrainReceipt {
output.writeInt(layout.sampleStep());
output.writeInt(layout.sampleCount());
for (int index = 0; index < layout.sampleCount(); index++) {
output.writeUTF(signature.biomeAtSample(index));
strings.write(output, signature.biomeAtSample(index));
}
BoundaryColumnGeometry geometry = signature.geometry();
output.writeInt(geometry.minimumY());
output.writeInt(geometry.palette().size());
for (BoundaryColumnGeometry.Voxel voxel : geometry.palette()) {
output.writeUTF(voxel.stateKey());
strings.write(output, voxel.stateKey());
output.writeByte(voxel.phase().ordinal());
output.writeUTF(voxel.fluidStateKey());
strings.write(output, voxel.fluidStateKey());
output.writeBoolean(voxel.protectedContent());
}
int[] ends = geometry.runEnds();
@@ -194,4 +197,45 @@ public final class NativeTerrainReceipt {
public record Decoded(SavedTerrainChunk terrain, long activationId, String epochId) {
}
private static final class UtfCache {
private static final int MAXIMUM_ENTRIES = 256;
private static final int MAXIMUM_ENCODED_BYTES = 64 * 1024;
private final Map<String, byte[]> encoded = new HashMap<>();
private final ByteArrayOutputStream scratch = new ByteArrayOutputStream();
private final DataOutputStream encoder = new DataOutputStream(scratch);
private int encodedBytes;
private boolean entryLimitReached;
private void write(DataOutputStream output, String value) throws IOException {
if (entryLimitReached) {
output.writeUTF(value);
return;
}
byte[] cached = encoded.get(value);
if (cached != null) {
output.write(cached);
return;
}
if (encoded.size() >= MAXIMUM_ENTRIES) {
entryLimitReached = true;
encoded.clear();
output.writeUTF(value);
return;
}
if (value.length() > MAXIMUM_ENCODED_BYTES - encodedBytes - Short.BYTES) {
output.writeUTF(value);
return;
}
scratch.reset();
encoder.writeUTF(value);
if (scratch.size() <= MAXIMUM_ENCODED_BYTES - encodedBytes) {
byte[] bytes = scratch.toByteArray();
encoded.put(value, bytes);
encodedBytes += bytes.length;
}
scratch.writeTo(output);
}
}
}
@@ -23,6 +23,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.CRC32;
@@ -42,20 +43,22 @@ public final class SavedBiomeStore {
private static final int MAXIMUM_CACHED_REGIONS = 64;
private static final int MAXIMUM_CACHED_CHUNKS = 128;
private static final long MAXIMUM_CACHED_BYTES = 64L * 1024L * 1024L;
private static final int MAXIMUM_WRITE_BATCH = 64;
private final Path dimensionRoot;
private final Path directory;
private final Object[] regionLocks = new Object[64];
private final WriteStripe[] regionLocks = new WriteStripe[64];
private final LinkedHashMap<Long, RegionIndex> regions = new LinkedHashMap<>(64, 0.75F, true);
private final LinkedHashMap<Long, Optional<SavedBiomeChunk>> chunks = new LinkedHashMap<>(128, 0.75F, true);
private long cachedBytes;
private volatile IOException writeFailure;
private SavedBiomeStore(Path dimensionRoot) throws IOException {
this.dimensionRoot = Objects.requireNonNull(dimensionRoot, "dimensionRoot").toAbsolutePath().normalize();
directory = this.dimensionRoot.resolve("iris/generation/biomes");
requireSafeAncestors();
for (int index = 0; index < regionLocks.length; index++) {
regionLocks[index] = new Object();
regionLocks[index] = new WriteStripe();
}
}
@@ -76,6 +79,7 @@ public final class SavedBiomeStore {
}
}
synchronized (regionLock(chunkX, chunkZ)) {
requireWritable();
synchronized (chunks) {
Optional<SavedBiomeChunk> cached = chunks.get(chunkKey);
if (cached != null) {
@@ -99,46 +103,153 @@ public final class SavedBiomeStore {
public boolean claimAndPersist(SavedBiomeChunk chunk) throws IOException {
SavedBiomeChunk required = Objects.requireNonNull(chunk, "chunk");
Object lock = regionLock(required.chunkX(), required.chunkZ());
synchronized (lock) {
if (alreadyClaimed(required)) {
return false;
WriteStripe lock = regionLock(required.chunkX(), required.chunkZ());
if (!lock.writing) {
synchronized (lock) {
if (alreadyClaimed(required)) {
return false;
}
}
}
byte[] body = encode(required);
byte[] record = ByteBuffer.allocate(body.length + 8)
.putInt(body.length).put(body).putInt(checksum(body)).array();
PendingWrite pending = new PendingWrite(required, record);
lock.pending.add(pending);
synchronized (lock) {
if (alreadyClaimed(required)) {
return false;
if (!pending.completed) {
lock.writing = true;
try {
while (!pending.completed) {
persistPending(lock);
}
} finally {
lock.writing = false;
}
}
RegionIndex region = region(required.chunkX(), required.chunkZ());
if (pending.failure != null) {
throw pending.failure;
}
return pending.persisted;
}
}
private void persistPending(WriteStripe stripe) {
Map<Long, List<PendingWrite>> batches = new LinkedHashMap<>();
PendingWrite grouping = null;
try {
for (int count = 0; count < MAXIMUM_WRITE_BATCH; count++) {
grouping = stripe.pending.poll();
if (grouping == null) {
break;
}
long regionKey = key(grouping.chunk.chunkX() >> 5, grouping.chunk.chunkZ() >> 5);
batches.computeIfAbsent(regionKey, ignored -> new ArrayList<>()).add(grouping);
grouping = null;
}
for (List<PendingWrite> batch : batches.values()) {
persistBatch(batch);
}
} catch (RuntimeException | Error failure) {
IOException unavailable = new IOException("Saved biome append failed unexpectedly", failure);
writeFailure = unavailable;
if (grouping != null) {
grouping.failure = unavailable;
grouping.completed = true;
}
for (List<PendingWrite> batch : batches.values()) {
for (PendingWrite pending : batch) {
if (!pending.completed) {
pending.failure = unavailable;
pending.completed = true;
}
}
}
throw failure;
}
}
private void persistBatch(List<PendingWrite> batch) {
Map<Integer, PendingWrite> claimed = new LinkedHashMap<>();
try {
for (PendingWrite pending : batch) {
try {
if (alreadyClaimed(pending.chunk)) {
pending.completed = true;
continue;
}
int slot = slot(pending.chunk.chunkX(), pending.chunk.chunkZ());
PendingWrite previous = claimed.get(slot);
if (previous == null) {
claimed.put(slot, pending);
} else if (!previous.chunk.equals(pending.chunk)) {
throw new IOException("Conflicting saved biome claim at chunk "
+ pending.chunk.chunkX() + ", " + pending.chunk.chunkZ());
}
} catch (IOException failure) {
pending.failure = failure;
pending.completed = true;
}
}
if (claimed.isEmpty()) {
return;
}
requireWritable();
SavedBiomeChunk first = batch.getFirst().chunk;
RegionIndex region = region(first.chunkX(), first.chunkZ());
ensureRegionFile(region);
long offset;
long offset = 0L;
boolean durable = false;
try (RandomAccessFile output = new RandomAccessFile(region.path.toFile(), "rw")) {
offset = output.length();
if (offset + record.length > MAXIMUM_REGION_BYTES) {
long length = 0L;
for (PendingWrite pending : claimed.values()) {
length += pending.record.length;
}
if (offset + length > MAXIMUM_REGION_BYTES) {
throw new IOException("Saved biome region exceeds its storage limit: " + region.path);
}
try {
output.seek(offset);
output.write(record);
for (PendingWrite pending : claimed.values()) {
output.write(pending.record);
}
output.getChannel().force(true);
durable = true;
} catch (IOException failure) {
try {
output.setLength(offset);
output.getChannel().force(true);
} catch (IOException rollback) {
failure.addSuppressed(rollback);
writeFailure = failure;
}
throw failure;
}
} catch (IOException failure) {
if (durable) {
writeFailure = failure;
}
throw failure;
}
int slot = slot(required.chunkX(), required.chunkZ());
region.offsets[slot] = offset;
region.lengths[slot] = body.length;
cacheChunk(key(required.chunkX(), required.chunkZ()), Optional.of(required));
return true;
for (PendingWrite pending : claimed.values()) {
SavedBiomeChunk chunk = pending.chunk;
int slot = slot(chunk.chunkX(), chunk.chunkZ());
region.offsets[slot] = offset;
region.lengths[slot] = pending.record.length - 8;
cacheChunk(key(chunk.chunkX(), chunk.chunkZ()), Optional.of(chunk));
pending.persisted = true;
offset += pending.record.length;
}
} catch (IOException failure) {
for (PendingWrite pending : batch) {
if (!pending.completed) {
pending.failure = failure;
}
}
}
for (PendingWrite pending : batch) {
pending.completed = true;
}
}
@@ -164,6 +275,7 @@ public final class SavedBiomeStore {
int regionX = parseRegionCoordinate(matcher.group(1));
int regionZ = parseRegionCoordinate(matcher.group(2));
synchronized (regionLock(regionX << 5, regionZ << 5)) {
requireWritable();
RegionIndex region = region(regionX << 5, regionZ << 5);
boolean[] discarded = new boolean[1024];
int count = 0;
@@ -205,6 +317,7 @@ public final class SavedBiomeStore {
}
private boolean alreadyClaimed(SavedBiomeChunk chunk) throws IOException {
requireWritable();
Optional<SavedBiomeChunk> existing = get(chunk.chunkX(), chunk.chunkZ());
if (existing.isEmpty()) {
return false;
@@ -440,7 +553,7 @@ public final class SavedBiomeStore {
}
}
private Object regionLock(int chunkX, int chunkZ) {
private WriteStripe regionLock(int chunkX, int chunkZ) {
long key = key(chunkX >> 5, chunkZ >> 5);
return regionLocks[(int) (key ^ (key >>> 32)) & (regionLocks.length - 1)];
}
@@ -656,6 +769,30 @@ public final class SavedBiomeStore {
throw new IOException("Invalid saved biome variable integer");
}
private void requireWritable() throws IOException {
if (writeFailure != null) {
throw new IOException("Saved biome writes are unavailable after an uncertain append", writeFailure);
}
}
private static final class WriteStripe {
private final ConcurrentLinkedQueue<PendingWrite> pending = new ConcurrentLinkedQueue<>();
private volatile boolean writing;
}
private static final class PendingWrite {
private final SavedBiomeChunk chunk;
private final byte[] record;
private boolean completed;
private boolean persisted;
private IOException failure;
private PendingWrite(SavedBiomeChunk chunk, byte[] record) {
this.chunk = chunk;
this.record = record;
}
}
private static final class DecodeBudget {
private long remaining = SavedBiomeChunk.MAXIMUM_ESTIMATED_BYTES - 8192L;
@@ -16,12 +16,16 @@ public final class RiverPolicyResolver {
}
public static EffectiveRiverPolicy resolve(IrisDimension dimension, IrisRegion region, IrisBiome biome) {
return resolve(
dimension == null ? null : dimension.getRiverPolicy(),
return resolve(dimension == null ? null : dimension.getRiverPolicy(),
region == null ? null : region.getRiverPolicy(),
biome == null ? null : biome.getRiverPolicy(),
loaderOf(biome, region, dimension)
);
biome == null ? null : biome.getRiverPolicy(), loaderOf(biome, region, dimension));
}
public static Resolution resolveWithStatus(IrisDimension dimension, IrisRegion region, IrisBiome biome) {
State state = resolveState(dimension == null ? null : dimension.getRiverPolicy(),
region == null ? null : region.getRiverPolicy(),
biome == null ? null : biome.getRiverPolicy(), loaderOf(biome, region, dimension), true);
return new Resolution(state.build(), state.complete);
}
public static EffectiveRiverPolicy resolve(
@@ -42,11 +46,16 @@ public final class RiverPolicyResolver {
IrisRiverPolicy biomePolicy,
IrisData data
) {
State state = new State();
return resolveState(dimensionPolicy, regionPolicy, biomePolicy, data, false).build();
}
private static State resolveState(IrisRiverPolicy dimensionPolicy, IrisRiverPolicy regionPolicy,
IrisRiverPolicy biomePolicy, IrisData data, boolean trackCompleteness) {
State state = new State(trackCompleteness);
state.apply(dimensionPolicy, RiverConfinement.REGION, data);
state.apply(regionPolicy, RiverConfinement.REGION, data);
state.apply(biomePolicy, RiverConfinement.BIOME, data);
return state.build();
return state;
}
private static IrisData loaderOf(IrisRegistrant... registrants) {
@@ -59,7 +68,12 @@ public final class RiverPolicyResolver {
return null;
}
public record Resolution(EffectiveRiverPolicy policy, boolean complete) {
}
private static final class State {
private final Runnable unresolvedReference;
private boolean complete = true;
private IrisRiverPlacementMode placement = IrisRiverPlacementMode.NATURAL;
private IrisRiverRoutingMode routing = IrisRiverRoutingMode.ALLOW;
private boolean outletAdmission = true;
@@ -80,6 +94,10 @@ public final class RiverPolicyResolver {
private Double shoreWidth = null;
private Boolean erosion = null;
private State(boolean trackCompleteness) {
unresolvedReference = trackCompleteness ? this::unresolvedReference : null;
}
/**
* {@code scope} is the confinement a {@code confined: true} at this level binds courses to; {@code data} is the
* pack whose version-content gate filters the biome selections (null skips filtering).
@@ -113,19 +131,19 @@ public final class RiverPolicyResolver {
profiles = List.copyOf(policy.getProfiles());
}
if (policy.getSurfaceBiomes() != null) {
surfaceBiomes = List.copyOf(policy.compatBiomes(policy.getSurfaceBiomes(), data, "surfaceBiomes"));
surfaceBiomes = List.copyOf(policy.compatBiomes(policy.getSurfaceBiomes(), data, "surfaceBiomes", unresolvedReference));
}
if (policy.getMouthBiomes() != null) {
mouthBiomes = List.copyOf(policy.compatBiomes(policy.getMouthBiomes(), data, "mouthBiomes"));
mouthBiomes = List.copyOf(policy.compatBiomes(policy.getMouthBiomes(), data, "mouthBiomes", unresolvedReference));
}
if (policy.getShoreBiomes() != null) {
shoreBiomes = List.copyOf(policy.compatBiomes(policy.getShoreBiomes(), data, "shoreBiomes"));
shoreBiomes = List.copyOf(policy.compatBiomes(policy.getShoreBiomes(), data, "shoreBiomes", unresolvedReference));
}
if (policy.getBankBiomes() != null) {
bankBiomes = List.copyOf(policy.compatBiomes(policy.getBankBiomes(), data, "bankBiomes"));
bankBiomes = List.copyOf(policy.compatBiomes(policy.getBankBiomes(), data, "bankBiomes", unresolvedReference));
}
if (policy.getFloodedCaveBiomes() != null) {
floodedCaveBiomes = List.copyOf(policy.compatBiomes(policy.getFloodedCaveBiomes(), data, "floodedCaveBiomes"));
floodedCaveBiomes = List.copyOf(policy.compatBiomes(policy.getFloodedCaveBiomes(), data, "floodedCaveBiomes", unresolvedReference));
}
if (policy.getSurfacePools() != null) {
surfacePools = List.copyOf(policy.getSurfacePools());
@@ -147,6 +165,10 @@ public final class RiverPolicyResolver {
}
}
private void unresolvedReference() {
complete = false;
}
private EffectiveRiverPolicy build() {
return new EffectiveRiverPolicy(
placement,
@@ -1,5 +1,7 @@
package art.arcane.iris.engine.hydrology.runtime;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
import art.arcane.iris.engine.hydrology.HydrologyDiagnosticCandidate;
@@ -78,6 +80,7 @@ import java.util.function.Predicate;
public final class IrisHydrologyRuntime implements AutoCloseable {
private static final int MAXIMUM_CACHE_TILES = 64;
private static final int MAXIMUM_TERRAIN_SAMPLES = 65_536;
private static final int MAXIMUM_RESOLVED_POLICIES = 1024;
private static final int MAXIMUM_FEATURE_SEARCH_TILES = 1_089;
private static final int BIOME_PATCH_SCALE = 32;
private static final long SURFACE_WIDTH_SALT = 0x5355524657494454L;
@@ -100,6 +103,9 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
private final Cache<HydrologyTileKey, Boolean> unplannedQueries = Caffeine.newBuilder()
.maximumSize(MAXIMUM_CACHE_TILES)
.build();
private final Cache<PolicyKey, ResolvedPolicy> resolvedPolicies = Caffeine.newBuilder()
.maximumSize(MAXIMUM_RESOLVED_POLICIES)
.build();
private final LinkedHashMap<Long, HydrologyTerrainSample> terrainSamples;
public IrisHydrologyRuntime(IrisHydrologyRuntimeContext context) {
@@ -483,6 +489,7 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
public void close() {
cache.close();
unplannedQueries.invalidateAll();
resolvedPolicies.invalidateAll();
routingTerrainSampler.close();
synchronized (terrainSampleLock) {
terrainSamples.clear();
@@ -571,9 +578,10 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
boolean ocean = naturalSample.ocean();
IrisBiome biome = naturalSample.biome();
IrisRegion region = naturalSample.region();
EffectiveRiverPolicy policy = RiverPolicyResolver.resolve(context.dimension(), region, biome);
ResolvedPolicy resolvedPolicy = resolvePolicy(region, biome);
EffectiveRiverPolicy policy = resolvedPolicy.policy();
String parentBiomeKey = requireBiomeKey(biome);
List<String> profiles = validProfiles(policy.profiles());
List<String> profiles = resolvedPolicy.profiles();
int configuredFluidY = settings.underground().minimumFluidY()
+ (settings.underground().maximumFluidY() - settings.underground().minimumFluidY()) / 2;
if (!rivers.getUnderground().getFluidLevel().isFlat()) {
@@ -651,6 +659,35 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
};
}
private ResolvedPolicy resolvePolicy(IrisRegion region, IrisBiome biome) {
IrisData data = policyData(region, biome);
ResourceLoader<IrisBiome> loader = data == null ? null : data.getBiomeLoader();
PolicyKey key = new PolicyKey(region, biome, data, loader);
ResolvedPolicy cached = resolvedPolicies.getIfPresent(key);
if (cached != null) {
return cached;
}
RiverPolicyResolver.Resolution resolution = RiverPolicyResolver.resolveWithStatus(context.dimension(), region, biome);
EffectiveRiverPolicy policy = resolution.policy();
ResolvedPolicy resolved = new ResolvedPolicy(policy, validProfiles(policy.profiles()));
if (resolution.complete() && data == policyData(region, biome)
&& loader == (data == null ? null : data.getBiomeLoader())) {
resolvedPolicies.put(key, resolved);
}
return resolved;
}
private IrisData policyData(IrisRegion region, IrisBiome biome) {
IrisData data = biome == null ? null : biome.getLoader();
if (data == null && region != null) {
data = region.getLoader();
}
if (data == null) {
data = context.dimension().getLoader();
}
return data;
}
private List<String> validProfiles(List<String> requested) {
if (requested == null || requested.isEmpty()) {
return List.of(defaultProfileKey);
@@ -1230,4 +1267,22 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
}
return biome.getLoadKey();
}
private record ResolvedPolicy(EffectiveRiverPolicy policy, List<String> profiles) {
}
private record PolicyKey(IrisRegion region, IrisBiome biome, IrisData data, ResourceLoader<IrisBiome> loader) {
@Override
public boolean equals(Object compared) {
return compared instanceof PolicyKey other
&& region == other.region && biome == other.biome && data == other.data && loader == other.loader;
}
@Override
public int hashCode() {
return ((31 * System.identityHashCode(region) + System.identityHashCode(biome)) * 31
+ System.identityHashCode(data)) * 31 + System.identityHashCode(loader);
}
}
}
@@ -352,6 +352,10 @@ public interface EngineMantle extends MatterGenerator {
if (getMantle().hasFlag(x, z, MantleFlag.CLEANED) || !isCovered(x, z)) {
return false;
}
return cleanupCoveredChunk(x, z, force);
}
default boolean cleanupCoveredChunk(int x, int z, boolean force) {
MantleChunk<Matter> chunk = getMantle().getChunk(x, z).use();
try {
synchronized (chunk) {
@@ -42,6 +42,30 @@ public final class TerrainMatterView {
}
}
public static MatterCavern getComposedCavern(MantleChunk<Matter> chunk, int x, int y, int z) {
if (chunk == null || y < 0) {
return null;
}
synchronized (chunk) {
int section = y >> 4;
if (!chunk.exists(section)) {
return null;
}
Matter matter = chunk.get(section);
if (matter == null) {
return null;
}
PreObjectMatterCell cell = raw(matter, x, y, z, PreObjectMatterCell.class);
HydrologyCaveCell hydrology = cell != null && cell.hydrologyCaptured()
? cell.hydrology() : raw(matter, x, y, z, HydrologyCaveCell.class);
if (hydrology != null) {
return hydrology.asCavern();
}
return cell != null && cell.cavernCaptured()
? cell.cavern() : raw(matter, x, y, z, MatterCavern.class);
}
}
public static <T> void iterate(MantleChunk<Matter> chunk, Class<T> type,
Consumer4<Integer, Integer, Integer, T> consumer) {
Objects.requireNonNull(chunk, "Terrain mantle chunk");
@@ -54,7 +78,8 @@ public final class TerrainMatterView {
List<TerrainEntry<T>> entries = new ArrayList<>();
synchronized (chunk) {
chunk.iterate(type, (x, y, z, value) -> {
T original = get(chunk, x, y, z, type);
PreObjectMatterCell cell = raw(chunk, x, y, z, PreObjectMatterCell.class);
T original = cell != null && cell.captures(type) ? cell.original(type) : value;
if (original != null) {
entries.add(new TerrainEntry<>(x, y, z, original));
}
@@ -88,7 +113,10 @@ public final class TerrainMatterView {
return null;
}
Matter matter = chunk.get(section);
return matter == null || !matter.hasSlice(type) ? null
: matter.<T>getSlice(type).get(x & 15, y & 15, z & 15);
return matter == null ? null : raw(matter, x, y, z, type);
}
private static <T> T raw(Matter matter, int x, int y, int z, Class<T> type) {
return !matter.hasSlice(type) ? null : matter.<T>getSlice(type).get(x & 15, y & 15, z & 15);
}
}
@@ -153,6 +153,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start();
try {
IrisData wallData = getData();
walls.forEach((rx, yy, rz, cavern) -> {
HydrologyCaveCell hydrology = dataIfPresent(
mantleChunk, rx, yy, rz, HydrologyCaveCell.class);
@@ -167,7 +168,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
: resolveCustomBiome(customBiomeCache, customBiome);
if (biome != null) {
PlatformBlockState data = biome.getWall().get(rng, worldX, yy, worldZ, getData());
PlatformBlockState data = biome.getWall().get(rng, worldX, yy, worldZ, wallData);
int columnIndex = PowerOfTwoCoordinates.packLocal16(rx, rz);
if (data != null && B.isSolid(output.getRaw(rx, yy, rz)) && yy < surfaceHeights[columnIndex]) {
@@ -566,9 +567,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
}
private MatterCavern composedCavernAt(MantleChunk<Matter> mantleChunk, int x, int y, int z) {
MatterCavern baseline = dataIfPresent(mantleChunk, x, y, z, MatterCavern.class);
HydrologyCaveCell hydrology = dataIfPresent(mantleChunk, x, y, z, HydrologyCaveCell.class);
return composeCavern(baseline, hydrology);
return TerrainMatterView.getComposedCavern(mantleChunk, x, y, z);
}
private static <T> T dataIfPresent(MantleChunk<Matter> mantleChunk, int x, int y, int z, Class<T> type) {
@@ -875,12 +874,15 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
Long2ObjectOpenHashMap<IrisBiome> caveBiomes = new Long2ObjectOpenHashMap<>(256);
Map<String, IrisBiome> customBiomes = new HashMap<>();
try {
for (int localX = 0; localX < output.getWidth(); localX++) {
for (int localZ = 0; localZ < output.getDepth(); localZ++) {
int width = output.getWidth();
int depth = output.getDepth();
int height = output.getHeight();
for (int localX = 0; localX < width; localX++) {
for (int localZ = 0; localZ < depth; localZ++) {
int worldX = blockX + localX;
int worldZ = blockZ + localZ;
int floor = -1;
for (int y = 1; y < output.getHeight(); y++) {
for (int y = 1; y < height; y++) {
PlatformBlockState state = output.getRaw(localX, y, localZ);
if (B.isSolid(state)) {
if (floor >= 0) {
@@ -1093,30 +1095,18 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
return null;
}
IrisBiome xPos = sampleCaveBiome(caveBiomeCache, x + CAVE_BIOME_BLEND_RADIUS, y, z, resolverState);
IrisBiome xNeg = sampleCaveBiome(caveBiomeCache, x - CAVE_BIOME_BLEND_RADIUS, y, z, resolverState);
IrisBiome zPos = sampleCaveBiome(caveBiomeCache, x, y, z + CAVE_BIOME_BLEND_RADIUS, resolverState);
IrisBiome zNeg = sampleCaveBiome(caveBiomeCache, x, y, z - CAVE_BIOME_BLEND_RADIUS, resolverState);
if (xPos == center && xNeg == center && zPos == center && zNeg == center) {
return center;
}
int roll = Math.floorMod(rng.nextParallelRNG(BlockPosition.toLong(x, y, z)).nextInt(), CAVE_BIOME_BLEND_TOTAL_WEIGHT);
if (roll < CAVE_BIOME_BLEND_CENTER_WEIGHT) {
return center;
}
roll -= CAVE_BIOME_BLEND_CENTER_WEIGHT;
if (roll == 0) {
return xPos != null ? xPos : center;
}
if (roll == 1) {
return xNeg != null ? xNeg : center;
}
if (roll == 2) {
return zPos != null ? zPos : center;
}
return zNeg != null ? zNeg : center;
IrisBiome neighbor = switch (roll) {
case 0 -> sampleCaveBiome(caveBiomeCache, x + CAVE_BIOME_BLEND_RADIUS, y, z, resolverState);
case 1 -> sampleCaveBiome(caveBiomeCache, x - CAVE_BIOME_BLEND_RADIUS, y, z, resolverState);
case 2 -> sampleCaveBiome(caveBiomeCache, x, y, z + CAVE_BIOME_BLEND_RADIUS, resolverState);
default -> sampleCaveBiome(caveBiomeCache, x, y, z - CAVE_BIOME_BLEND_RADIUS, resolverState);
};
return neighbor != null ? neighbor : center;
}
private IrisBiome sampleCaveBiome(Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, int x, int y, int z, IrisDimensionCarvingResolver.State resolverState) {
@@ -83,7 +83,12 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
long seed = x * 341873128712L + z * 132897987541L;
PreparedDeposit[] prepared = new PreparedDeposit[PREPARATION_BATCH_COUNT];
int pending = 0;
IrisEngine generationEngine = getEngine() instanceof IrisEngine irisEngine
&& irisEngine.hasGenerationRuntimeScope() ? irisEngine : null;
PreparationContext preparation = new PreparationContext(context, generationEngine,
generationEngine == null ? null : generationEngine.captureGenerationRuntimeBinding(), new Throwable[PREPARATION_BATCH_COUNT]);
MantleChunk chunk = getEngine().getMantle().getMantle().getChunk(x, z).use();
Throwable callerFailure = null;
try {
for (int i = 0; i < generators.size(); i++) {
DepositPlan plan = plan(generators.get(i), rng.nextParallelRNG(seed * (i + 1L)));
@@ -92,19 +97,25 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
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));
() -> prepared[index] = prepare(plan, firstAttempt, limit, x, z, null, context), preparation, index));
if (pending == prepared.length) {
completeBatches(burst, prepared, chunk, terrain, x, z, context);
completeBatches(burst, prepared, chunk, terrain, x, z, preparation);
pending = 0;
}
}
}
completeBatches(burst, prepared, chunk, terrain, x, z, context);
completeBatches(burst, prepared, chunk, terrain, x, z, preparation);
} catch (Throwable failure) {
callerFailure = failure;
throw failure;
} finally {
// complete() must run before release() even when queueing throws — already
// submitted burst tasks must never write into a released chunk.
try {
burst.complete();
if (callerFailure != null) {
preparation.takeFailure(callerFailure);
}
} finally {
chunk.release();
}
@@ -112,29 +123,37 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
}
private void completeBatches(BurstExecutor burst, PreparedDeposit[] prepared, MantleChunk chunk,
Hunk<PlatformBlockState> terrain, int x, int z, ChunkContext context) {
Hunk<PlatformBlockState> terrain, int x, int z, PreparationContext preparation) {
burst.complete();
try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), context.getGenerationSessionId(), context)) {
Throwable failure = preparation.takeFailure(null);
if (failure instanceof Error error) {
throw error;
}
if (failure instanceof RuntimeException exception) {
throw exception;
}
if (failure != null) {
throw new IllegalStateException("Deposit preparation failed", failure);
}
try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), preparation.chunk().getGenerationSessionId(), preparation.chunk())) {
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);
place(deposit, chunk, terrain, x, z, null, preparation.chunk());
}
}
}
}
private Runnable scopedDepositTask(Runnable task, ChunkContext context) {
IrisEngine generationEngine = getEngine() instanceof IrisEngine irisEngine
&& irisEngine.hasGenerationRuntimeScope() ? irisEngine : null;
IrisEngine.GenerationRuntimeBinding binding = generationEngine == null
? null : generationEngine.captureGenerationRuntimeBinding();
private Runnable scopedDepositTask(Runnable task, PreparationContext preparation, int index) {
return () -> {
try (IrisEngine.GenerationRuntimeScope runtimeScope = generationEngine == null
? null : generationEngine.openGenerationRuntimeScope(binding);
IrisContext.Scope chunkScope = IrisContext.open(getEngine(), context.getGenerationSessionId(), context)) {
try (IrisEngine.GenerationRuntimeScope runtimeScope = preparation.engine() == null
? null : preparation.engine().openGenerationRuntimeScope(preparation.binding());
IrisContext.Scope chunkScope = IrisContext.open(getEngine(), preparation.chunk().getGenerationSessionId(), preparation.chunk())) {
task.run();
} catch (Throwable failure) {
preparation.failures()[index] = failure;
}
};
}
@@ -498,6 +517,34 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
return mixed ^ (mixed >>> 31);
}
private record PreparationContext(ChunkContext chunk, IrisEngine engine, IrisEngine.GenerationRuntimeBinding binding,
Throwable[] failures) {
private Throwable takeFailure(Throwable primary) {
for (int i = 0; i < failures.length; i++) {
Throwable failure = failures[i];
failures[i] = null;
if (failure == null || failure == primary) {
continue;
}
if (primary == null) {
primary = failure;
} else if (!isSuppressed(primary, failure)) {
primary.addSuppressed(failure);
}
}
return primary;
}
private boolean isSuppressed(Throwable primary, Throwable failure) {
for (Throwable suppressed : primary.getSuppressed()) {
if (suppressed == failure) {
return true;
}
}
return false;
}
}
private record DepositPlan(IrisDepositGenerator generator, long seed, boolean ore, int attempts) {
}
@@ -90,6 +90,16 @@ final class CompatPools {
String subjectType,
String subjectKey,
String field) {
return surviving(loader, keys, data, subjectType, subjectKey, field, null);
}
static <T extends IrisRegistrant> KList<String> surviving(ResourceLoader<T> loader,
List<String> keys,
IrisData data,
String subjectType,
String subjectKey,
String field,
Runnable unresolvedReference) {
KList<String> kept = new KList<>();
if (keys == null) {
@@ -104,6 +114,9 @@ final class CompatPools {
for (int index = 0; index < keys.size(); index++) {
String key = keys.get(index);
T loaded = loader.load(key);
if (loaded == null && unresolvedReference != null) {
unresolvedReference.run();
}
if (loaded != null && loaded.isCompatExcluded()) {
drop(data, loaded, subjectType, subjectKey, field + "[" + index + "] " + key, null);
@@ -28,7 +28,7 @@ public final class IrisDimensionCarvingResolver {
public static IrisDimensionCarvingEntry resolveRootEntry(Engine engine, int worldY, State state) {
State resolvedState = state == null ? threadState() : state;
resolvedState.bind(engine);
IrisData data = resolvedState.bind(engine);
if (resolvedState.rootEntriesByWorldY.containsKey(worldY)) {
return resolvedState.rootEntriesByWorldY.get(worldY);
}
@@ -42,7 +42,7 @@ public final class IrisDimensionCarvingResolver {
IrisDimensionCarvingEntry resolved = null;
for (IrisDimensionCarvingEntry entry : entries) {
if (!isRootCandidate(engine, entry, worldY, resolvedState)) {
if (!isRootCandidate(data, entry, worldY, resolvedState)) {
continue;
}
@@ -59,12 +59,12 @@ public final class IrisDimensionCarvingResolver {
public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ, State state) {
State resolvedState = state == null ? threadState() : state;
resolvedState.bind(engine);
IrisData data = resolvedState.bind(engine);
if (rootEntry == null) {
return null;
}
IrisBiome rootBiome = resolveEntryBiome(engine, rootEntry, resolvedState);
IrisBiome rootBiome = resolveBoundEntryBiome(data, rootEntry, resolvedState);
if (rootBiome == null) {
return null;
}
@@ -78,7 +78,7 @@ public final class IrisDimensionCarvingResolver {
IrisDimensionCarvingEntry current = rootEntry;
int depth = remainingDepth;
while (depth > 0) {
IrisDimensionCarvingEntry selected = selectChild(engine, current, worldX, worldZ, entryIndex, resolvedState);
IrisDimensionCarvingEntry selected = selectChild(engine, data, current, worldX, worldZ, entryIndex, resolvedState);
if (selected == null || selected == current) {
break;
}
@@ -107,17 +107,20 @@ public final class IrisDimensionCarvingResolver {
return entry.getRealBiome(engine.getData());
}
state.bind(engine);
return resolveBoundEntryBiome(state.bind(engine), entry, state);
}
private static IrisBiome resolveBoundEntryBiome(IrisData data, IrisDimensionCarvingEntry entry, State state) {
if (state.biomeCache.containsKey(entry)) {
return state.biomeCache.get(entry);
}
IrisBiome biome = entry.getRealBiome(engine.getData());
IrisBiome biome = entry.getRealBiome(data);
state.biomeCache.put(entry, biome);
return biome;
}
private static boolean isRootCandidate(Engine engine, IrisDimensionCarvingEntry entry, int worldY, State state) {
private static boolean isRootCandidate(IrisData data, IrisDimensionCarvingEntry entry, int worldY, State state) {
if (entry == null || !entry.isEnabled()) {
return false;
}
@@ -127,11 +130,12 @@ public final class IrisDimensionCarvingResolver {
return false;
}
return resolveEntryBiome(engine, entry, state) != null;
return resolveBoundEntryBiome(data, entry, state) != null;
}
private static IrisDimensionCarvingEntry selectChild(
Engine engine,
IrisData data,
IrisDimensionCarvingEntry parent,
int worldX,
int worldZ,
@@ -143,14 +147,14 @@ public final class IrisDimensionCarvingResolver {
return parent;
}
IrisBiome parentBiome = resolveEntryBiome(engine, parent, state);
IrisBiome parentBiome = resolveBoundEntryBiome(data, parent, state);
if (parentBiome == null) {
return parent;
}
IrisRaritySelection<CarvingChoice> selectionPlan = state.selectionPlans.get(parent);
if (selectionPlan == null) {
selectionPlan = buildSelectionPlan(engine, parent, parentBiome, entryIndex, state);
selectionPlan = buildSelectionPlan(data, parent, parentBiome, entryIndex, state);
state.selectionPlans.put(parent, selectionPlan);
}
@@ -159,7 +163,7 @@ public final class IrisDimensionCarvingResolver {
}
long seed = resolveChildSeed(engine, state);
CNG childGenerator = parent.getChildrenGenerator(seed, engine.getData());
CNG childGenerator = parent.getChildrenGenerator(seed, data);
double sample = childGenerator.noiseFast2D(worldX, worldZ);
CarvingChoice selected = selectionPlan.select(sample);
if (selected == null || selected.entry == null) {
@@ -170,7 +174,7 @@ public final class IrisDimensionCarvingResolver {
}
private static IrisRaritySelection<CarvingChoice> buildSelectionPlan(
Engine engine,
IrisData data,
IrisDimensionCarvingEntry parent,
IrisBiome parentBiome,
Map<String, IrisDimensionCarvingEntry> entryIndex,
@@ -189,7 +193,7 @@ public final class IrisDimensionCarvingResolver {
continue;
}
IrisBiome childBiome = resolveEntryBiome(engine, child, state);
IrisBiome childBiome = resolveBoundEntryBiome(data, child, state);
if (childBiome == null) {
continue;
}
@@ -256,13 +260,13 @@ public final class IrisDimensionCarvingResolver {
private Map<String, IrisDimensionCarvingEntry> entryIndex;
private Long childSeed;
private void bind(Engine engine) {
private IrisData bind(Engine engine) {
IrisDimension dimension = engine.getDimension();
IrisData data = engine.getData();
if (references(engineIdentity, engine)
&& references(dimensionIdentity, dimension)
&& references(dataIdentity, data)) {
return;
return data;
}
engineIdentity = new WeakReference<>(engine);
dimensionIdentity = new WeakReference<>(dimension);
@@ -272,6 +276,7 @@ public final class IrisDimensionCarvingResolver {
biomeCache.clear();
entryIndex = null;
childSeed = null;
return data;
}
private static boolean references(WeakReference<?> identity, Object value) {
@@ -105,12 +105,16 @@ public class IrisRiverPolicy {
* selection keeps its "explicitly clears the inherited selection" meaning; null (undeclared) stays null.
*/
public KList<String> compatBiomes(KList<String> declared, IrisData data, String field) {
return compatBiomes(declared, data, field, null);
}
public KList<String> compatBiomes(KList<String> declared, IrisData data, String field, Runnable unresolvedReference) {
if (declared == null) {
return null;
}
return CompatPools.surviving(data == null ? null : data.getBiomeLoader(), declared, data,
"river policy", "", field);
"river policy", "", field, unresolvedReference);
}
public KSet<String> getAllBiomeIds() {
@@ -1088,7 +1088,6 @@ public class CNG {
}
private double getSignedNoise(double x, double z) {
ensureFastPathState();
double scl = effectiveScale;
NoiseGenerator localGenerator = generator;
CNG localFracture = fracture;
@@ -1103,7 +1102,6 @@ public class CNG {
}
private double getSignedNoise(double x, double y, double z) {
ensureFastPathState();
double scl = effectiveScale;
NoiseGenerator localGenerator = generator;
CNG localFracture = fracture;
@@ -0,0 +1,245 @@
package art.arcane.iris.engine;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.EngineMetrics;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.mantle.TerrainMatterView;
import art.arcane.iris.engine.modifier.IrisCarveModifier;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomePaletteLayer;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.function.Consumer4;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
public class IrisCarveWallPaletteScopeTest {
@Test
public void naturalWallsUseOneSelectedDataLookup() throws Exception {
verifyWalls(Mode.NATURAL);
}
@Test
public void contentWallsRestoreDataAfterNestedSampling() throws Exception {
verifyWalls(Mode.CONTENT);
}
@Test
public void detachedWallsUseDetachedData() throws Exception {
verifyWalls(Mode.DETACHED);
}
@Test
public void assemblyWallsUseUnpublishedData() throws Exception {
verifyWalls(Mode.ASSEMBLY);
}
private static void verifyWalls(Mode mode) throws Exception {
PlatformBlockState air = block("minecraft:cave_air", false);
PlatformBlockState stone = block("minecraft:stone", true);
PlatformBlockState wall = block("minecraft:calcite", true);
try (MockedStatic<B> blocks = mockStatic(B.class)) {
blocks.when(() -> B.getState(anyString())).thenReturn(air);
blocks.when(() -> B.isSolid(any())).thenAnswer(call ->
call.<PlatformBlockState>getArgument(0).isSolid());
Fixture fixture = new Fixture(air, wall);
GenerationRuntime selected = mode == Mode.DETACHED ? fixture.detached : fixture.active;
IrisData expectedData = mode == Mode.ASSEMBLY ? fixture.assemblyData : selected.data();
IrisComplex expectedComplex = mode == Mode.ASSEMBLY ? fixture.assemblyComplex : selected.complex();
fixture.expectedData = expectedData;
doReturn(expectedComplex).when(fixture.context).getComplex();
doReturn(mode == Mode.NATURAL).when(fixture.context).isNaturalTerrain();
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(16, 64, 16);
for (int x = 0; x < 16; x++) {
for (int y = 0; y < 64; y++) {
for (int z = 0; z < 16; z++) {
output.setRaw(x, y, z, stone);
}
}
}
try (IrisEngine.GenerationRuntimeScope generation = fixture.engine.openGenerationRuntimeScope(
new IrisEngine.GenerationRuntimeBinding(fixture.engine, selected));
IrisContext.Scope context = IrisContext.open(fixture.engine, 71L, fixture.context);
MockedStatic<TerrainMatterView> terrain = mockStatic(TerrainMatterView.class)) {
if (mode == Mode.ASSEMBLY) {
fixture.engine.runtimeAssembly.set(fixture.assembly);
}
terrain.when(() -> TerrainMatterView.iterate(eq(fixture.chunk), eq(MatterCavern.class), any()))
.thenAnswer(call -> {
Consumer4<Integer, Integer, Integer, MatterCavern> consumer = call.getArgument(2);
for (int coordinate : new int[]{4, 8, 12}) {
consumer.accept(coordinate, 10, coordinate, new MatterCavern(true, "", (byte) 0));
}
return null;
});
try {
assertSame(expectedData, fixture.engine.getData());
assertEquals(mode == Mode.NATURAL, expectedComplex.isNaturalTerrainContext());
fixture.modifier.onModify(-2, 3, output, false, fixture.context);
assertSame(expectedData, fixture.engine.getData());
assertEquals(12, fixture.paletteCalls.get());
for (int x = 0; x < 16; x++) {
for (int y = 0; y < 64; y++) {
for (int z = 0; z < 16; z++) {
PlatformBlockState expected = stone;
for (int coordinate : new int[]{4, 8, 12}) {
if (y == 10 && x == coordinate && z == coordinate) {
expected = air;
} else if (y == 10 && Math.abs(x - coordinate) + Math.abs(z - coordinate) == 1) {
expected = wall;
}
}
assertSame(expected, output.getRaw(x, y, z));
}
}
}
verify(fixture.chunk).release();
assertEquals(1, fixture.dataLookups.get());
} finally {
fixture.engine.runtimeAssembly.remove();
}
}
assertSame(fixture.active.data(), fixture.engine.getData());
}
}
private static PlatformBlockState block(String key, boolean solid) {
PlatformBlockState block = mock(PlatformBlockState.class);
doReturn(key).when(block).key();
doReturn(solid).when(block).isSolid();
return block;
}
private static void field(Object owner, Class<?> type, String name, Object value) throws Exception {
Field field = type.getDeclaredField(name);
field.setAccessible(true);
field.set(owner, value);
}
private static GenerationRuntime runtime(IrisData data, IrisComplex complex, EngineMantle mantle) {
GenerationRuntime runtime = mock(GenerationRuntime.class);
doReturn(data).when(runtime).data();
doReturn(complex).when(runtime).complex();
doReturn(mantle).when(runtime).mantle();
doReturn(mock(IrisDimension.class)).when(runtime).dimension();
return runtime;
}
private enum Mode {
NATURAL,
CONTENT,
DETACHED,
ASSEMBLY
}
private static final class Fixture {
private final IrisEngine engine = mock(IrisEngine.class, CALLS_REAL_METHODS);
private final IrisCarveModifier modifier = mock(IrisCarveModifier.class, CALLS_REAL_METHODS);
private final ChunkContext context = mock(ChunkContext.class);
private final MantleChunk<Matter> chunk;
private final GenerationRuntime active;
private final GenerationRuntime detached;
private final IrisData assemblyData = mock(IrisData.class);
private final IrisComplex assemblyComplex = mock(IrisComplex.class);
private final EngineRuntimeBuilder.RuntimeAssembly assembly;
private final AtomicInteger dataLookups = new AtomicInteger();
private final AtomicInteger paletteCalls = new AtomicInteger();
private IrisData expectedData;
@SuppressWarnings("unchecked")
private Fixture(PlatformBlockState air, PlatformBlockState wall) throws Exception {
Mantle<Matter> mantle = mock(Mantle.class);
doReturn(new KMap<>()).when(mantle).getLoadedRegions();
chunk = mock(MantleChunk.class);
EngineMantle engineMantle = mock(EngineMantle.class);
doReturn(mantle).when(engineMantle).getMantle();
doReturn(chunk).when(mantle).getChunk(-2, 3);
doReturn(chunk).when(chunk).use();
active = runtime(mock(IrisData.class), mock(IrisComplex.class), engineMantle);
detached = runtime(mock(IrisData.class), mock(IrisComplex.class), engineMantle);
field(engine, IrisEngine.class, "lifecycleLock", new Object());
field(engine, IrisEngine.class, "runtimeAssembly", new ThreadLocal<EngineRuntimeBuilder.RuntimeAssembly>());
field(engine, IrisEngine.class, "biomeEnvironmentScopes", new ThreadLocal<>());
field(engine, IrisEngine.class, "generationRuntimeScopes", new GenerationRuntimeScopeState());
field(engine, IrisEngine.class, "detachedGenerationRuntimes", Collections.newSetFromMap(new IdentityHashMap<>()));
field(engine, IrisEngine.class, "retiringGenerationRuntimes", Collections.newSetFromMap(new IdentityHashMap<>()));
engine.detachedGenerationRuntimes.add(detached);
EngineRuntime published = mock(EngineRuntime.class);
doReturn(active).when(published).generation();
engine.runtime = published;
doReturn(IrisWorld.builder().minHeight(-64).maxHeight(0).build()).when(engine).getWorld();
doReturn(64).when(engine).getHeight();
doReturn(mock(EngineMetrics.class, RETURNS_DEEP_STUBS)).when(engine).getMetrics();
doReturn(48).when(context).getRoundedHeight(anyInt(), anyInt());
IrisComplex activeComplex = active.complex();
IrisComplex detachedComplex = detached.complex();
doCallRealMethod().when(activeComplex).isNaturalTerrainContext();
doCallRealMethod().when(detachedComplex).isNaturalTerrainContext();
doCallRealMethod().when(assemblyComplex).isNaturalTerrainContext();
EngineTarget assemblyTarget = mock(EngineTarget.class);
doReturn(engine.getWorld()).when(assemblyTarget).getWorld();
doReturn(assemblyData).when(assemblyTarget).getData();
doReturn(mock(IrisDimension.class)).when(assemblyTarget).getDimension();
assembly = new EngineRuntimeBuilder.RuntimeAssembly(19, assemblyTarget);
assembly.complex = assemblyComplex;
assembly.mantle = engineMantle;
IrisBiome biome = mock(IrisBiome.class);
IrisBiomePaletteLayer palette = mock(IrisBiomePaletteLayer.class);
doReturn(palette).when(biome).getWall();
doAnswer(call -> {
assertSame(expectedData, call.getArgument(4));
paletteCalls.incrementAndGet();
return wall;
}).when(palette).get(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), any(IrisData.class));
doAnswer(call -> {
try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope(
new IrisEngine.GenerationRuntimeBinding(engine, active))) {
assertSame(engine.runtimeAssembly.get() == null ? active.data() : assemblyData, engine.getData());
}
return biome;
}).when(engine).getCaveBiome(anyInt(), anyInt(), anyInt(), any(IrisDimensionCarvingResolver.State.class));
doReturn(engine).when(modifier).getEngine();
doAnswer(call -> {
dataLookups.incrementAndGet();
return call.callRealMethod();
}).when(modifier).getData();
field(modifier, IrisCarveModifier.class, "rng", new RNG(319L));
field(modifier, IrisCarveModifier.class, "AIR", air);
field(modifier, IrisCarveModifier.class, "LAVA", air);
}
}
}
@@ -23,7 +23,9 @@ 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;
import art.arcane.iris.engine.object.IrisDimensionCarvingEntry;
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
import art.arcane.iris.engine.object.IrisRange;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
@@ -32,7 +34,9 @@ import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import org.junit.AfterClass;
import org.junit.BeforeClass;
@@ -90,6 +94,51 @@ public class IrisEngineGenerationRuntimeScopeTest {
IrisPlatforms.unbind();
}
@Test
public void carvingResolverRestoresNestedRuntimeDefinitions() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
RuntimeFixture first = runtime(2, 2D, 2D, 2D);
RuntimeFixture second = runtime(3, 3D, 3D, 3D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
IrisDimensionCarvingEntry root = mock(IrisDimensionCarvingEntry.class);
IrisDimensionCarvingEntry child = mock(IrisDimensionCarvingEntry.class);
when(root.isEnabled()).thenReturn(true);
when(root.getWorldYRange()).thenReturn(new IrisRange(-64, 320));
when(root.getChildRecursionDepth()).thenReturn(1);
when(root.getChildren()).thenReturn(new KList<>("child"));
when(child.isEnabled()).thenReturn(true);
CNG generator = mock(CNG.class);
when(generator.noiseFast2D(19D, -3D)).thenReturn(1D);
IrisBiome[] expected = new IrisBiome[3];
RuntimeFixture[] fixtures = {active, first, second};
for (int index = 0; index < fixtures.length; index++) {
RuntimeFixture fixture = fixtures[index];
IrisBiome parentBiome = mock(IrisBiome.class);
IrisBiome childBiome = mock(IrisBiome.class);
when(parentBiome.getRarity()).thenReturn(1);
when(childBiome.getRarity()).thenReturn(1);
when(root.getRealBiome(fixture.data)).thenReturn(parentBiome);
when(child.getRealBiome(fixture.data)).thenReturn(childBiome);
when(root.getChildrenGenerator(fixture.runtime.seedManager().getCarve() ^ 0x9E3779B97F4A7C15L,
fixture.data)).thenReturn(generator);
when(fixture.dimension.getCarving()).thenReturn(new KList<>(root));
when(fixture.dimension.getCarvingEntryIndex()).thenReturn(Map.of("child", child));
expected[index] = childBiome;
}
IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State();
assertSame(expected[0], carvingBiome(engine, state));
try (IrisEngine.GenerationRuntimeScope outer = engine.openGenerationRuntimeScope(
detachedBinding(engine, first.runtime))) {
assertSame(expected[1], carvingBiome(engine, state));
try (IrisEngine.GenerationRuntimeScope inner = engine.openGenerationRuntimeScope(
detachedBinding(engine, second.runtime))) {
assertSame(expected[2], carvingBiome(engine, state));
}
assertSame(expected[1], carvingBiome(engine, state));
}
assertSame(expected[0], carvingBiome(engine, state));
}
@Test
@SuppressWarnings("unchecked")
public void naturalCaveSamplesKeepSelectedRuntimeAcrossChunkEdges() throws Exception {
@@ -813,6 +862,12 @@ public class IrisEngineGenerationRuntimeScopeTest {
assertSame(active.dimension, engine.getDimension());
}
private static IrisBiome carvingBiome(IrisEngine engine, IrisDimensionCarvingResolver.State state) {
IrisDimensionCarvingEntry root = IrisDimensionCarvingResolver.resolveRootEntry(engine, 80, state);
IrisDimensionCarvingEntry child = IrisDimensionCarvingResolver.resolveFromRoot(engine, root, 19, -3, state);
return IrisDimensionCarvingResolver.resolveEntryBiome(engine, child, state);
}
private static GenerationHistoryRuntimeRouter historyRouter(IrisEngine engine) {
GenerationHistory history = mock(GenerationHistory.class);
when(history.retainRuntime()).thenReturn(mock(GenerationAdmission.RuntimeLease.class));
@@ -7,9 +7,19 @@ import org.mockito.MockedStatic;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.nio.channels.FileChannel;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -23,6 +33,7 @@ import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.AdditionalAnswers.delegatesTo;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
@@ -55,10 +66,9 @@ public class GenerationHistoryAdmissionTest {
});
admitted.get(1, TimeUnit.SECONDS);
assertFalse(persisted.isDone());
Future<?> observed = executor.submit(() -> history.semantics(1, 2));
assertThrows(TimeoutException.class, () -> observed.get(100, TimeUnit.MILLISECONDS));
Future<Optional<ChunkGenerationSemantics>> observed = executor.submit(() -> history.semantics(1, 2));
assertTrue(observed.get(1, TimeUnit.SECONDS).isEmpty());
release.countDown();
observed.get(5, TimeUnit.SECONDS);
assertTrue(persisted.get(5, TimeUnit.SECONDS));
assertEquals(claim, history.semantics(1, 2).orElseThrow());
assertEquals(claim, GenerationSemanticIndex.loadRequired(history.paths().dimensionRoot()).get(1, 2).orElseThrow());
@@ -165,12 +175,212 @@ public class GenerationHistoryAdmissionTest {
}
}
@Test
public void queuedClaimsShareOneForceAndRetainIndividualValidation() throws Exception {
GenerationHistory history = history();
GenerationAdmission.RuntimeLease runtime = history.retainRuntime();
List<GenerationHistory.GenerationStage> stages = new ArrayList<>();
for (int x : new int[]{1, 2, 3, 1, 1, 4, 32, 33}) {
stages.add(history.openStage(x, 0));
}
history.claimGeneratedSemantics(stages.getFirst(), claim(stages.getFirst()));
List<Thread> workers = new CopyOnWriteArrayList<>();
ExecutorService executor = Executors.newFixedThreadPool(stages.size(), task -> {
Thread thread = new Thread(task);
workers.add(thread);
return thread;
});
AtomicInteger forces = new AtomicInteger();
List<Future<Boolean>> results = new ArrayList<>();
try {
synchronized (history) {
for (int index = 0; index < stages.size(); index++) {
GenerationHistory.GenerationStage stage = stages.get(index);
ChunkGenerationSemantics update = index == 4
? ChunkGenerationSemantics.builder(1, 0, stage.activation().activationId())
.addSurfaceBiome("iris:conflict").seal().build()
: claim(stage);
results.add(executor.submit(() -> persistCountingForces(history, stage, update, forces)));
awaitBlockedOnHistory(history, workers, index + 1);
}
stages.get(5).close();
}
assertFalse(results.get(0).get(5, TimeUnit.SECONDS));
assertTrue(results.get(1).get(5, TimeUnit.SECONDS));
assertTrue(results.get(2).get(5, TimeUnit.SECONDS));
assertFalse(results.get(3).get(5, TimeUnit.SECONDS));
assertTrue(assertThrows(ExecutionException.class, () -> results.get(4).get(5, TimeUnit.SECONDS))
.getCause() instanceof IllegalStateException);
assertTrue(assertThrows(ExecutionException.class, () -> results.get(5).get(5, TimeUnit.SECONDS))
.getCause() instanceof IllegalStateException);
assertTrue(results.get(6).get(5, TimeUnit.SECONDS));
assertTrue(results.get(7).get(5, TimeUnit.SECONDS));
assertEquals(2, forces.get());
GenerationSemanticIndex loaded = GenerationSemanticIndex.loadRequired(history.paths().dimensionRoot());
assertEquals(5, loaded.recordCount());
assertEquals(claim(stages.getFirst()), loaded.get(1, 0).orElseThrow());
assertTrue(loaded.get(4, 0).isEmpty());
} finally {
drain(executor);
for (GenerationHistory.GenerationStage stage : stages) {
stage.close();
}
runtime.close();
}
}
@Test
public void fullClaimQueueRetainsEveryAdmittedRequest() throws Exception {
GenerationHistory history = history();
GenerationAdmission.RuntimeLease runtime = history.retainRuntime();
List<GenerationHistory.GenerationStage> stages = new ArrayList<>();
List<Future<Boolean>> results = new ArrayList<>();
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
CountDownLatch started = new CountDownLatch(132);
try {
synchronized (history) {
for (int x = 0; x < 132; x++) {
GenerationHistory.GenerationStage stage = history.openStage(x, 0);
stages.add(stage);
results.add(executor.submit(() -> {
started.countDown();
return history.claimGeneratedSemantics(stage, claim(stage));
}));
}
assertTrue(started.await(5, TimeUnit.SECONDS));
Field queueField = GenerationHistory.class.getDeclaredField("pendingSemanticClaims");
queueField.setAccessible(true);
ArrayBlockingQueue<?> queue = (ArrayBlockingQueue<?>) queueField.get(history);
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
while (queue.remainingCapacity() != 0 && System.nanoTime() < deadline) {
Thread.sleep(1L);
}
assertEquals(0, queue.remainingCapacity());
assertEquals(128, queue.size());
}
for (Future<Boolean> result : results) {
assertTrue(result.get(10, TimeUnit.SECONDS));
}
assertEquals(132, GenerationSemanticIndex.loadRequired(history.paths().dimensionRoot()).recordCount());
} finally {
drain(executor);
for (GenerationHistory.GenerationStage stage : stages) {
stage.close();
}
runtime.close();
}
}
@Test
public void unexpectedLaterBatchFailureReachesWaitersAndKeepsEarlierSuccess() throws Exception {
GenerationHistory history = history();
GenerationAdmission.RuntimeLease runtime = history.retainRuntime();
List<GenerationHistory.GenerationStage> stages = new ArrayList<>();
for (int x : new int[]{0, 32, 32, 64}) {
stages.add(history.openStage(x, 0));
}
List<Thread> workers = new CopyOnWriteArrayList<>();
ExecutorService executor = Executors.newFixedThreadPool(stages.size(), task -> {
Thread thread = new Thread(task);
workers.add(thread);
return thread;
});
IllegalStateException failure = new IllegalStateException("Unexpected second journal failure");
List<Future<Boolean>> results = new ArrayList<>();
try {
synchronized (history) {
for (int index = 0; index < stages.size(); index++) {
GenerationHistory.GenerationStage stage = stages.get(index);
results.add(executor.submit(() -> persistFailingRegion(history, stage, failure)));
awaitBlockedOnHistory(history, workers, index + 1);
}
}
assertTrue(results.getFirst().get(5, TimeUnit.SECONDS));
for (int index = 1; index < results.size(); index++) {
Future<Boolean> result = results.get(index);
assertSame(failure, assertThrows(ExecutionException.class,
() -> result.get(5, TimeUnit.SECONDS)).getCause());
}
assertThrows(IllegalStateException.class, () -> history.semantics(0, 0));
GenerationSemanticIndex loaded = GenerationSemanticIndex.loadRequired(history.paths().dimensionRoot());
assertEquals(1, loaded.recordCount());
assertEquals(claim(stages.getFirst()), loaded.get(0, 0).orElseThrow());
} finally {
drain(executor);
for (GenerationHistory.GenerationStage stage : stages) {
stage.close();
}
runtime.close();
}
}
private static boolean persistFailingRegion(GenerationHistory history, GenerationHistory.GenerationStage stage,
IllegalStateException failure) throws Exception {
try (MockedStatic<FileChannel> ignored = mockStatic(FileChannel.class, invocation -> {
FileChannel source = (FileChannel) invocation.callRealMethod();
Path path = invocation.getArgument(0);
if (invocation.getMethod().getParameterCount() != 2
|| !path.getFileName().toString().equals("r.1.0.iswal")) {
return source;
}
FileChannel intercepted = mock(FileChannel.class, delegatesTo(source));
doAnswer(write -> {
throw failure;
}).when(intercepted).write(any(ByteBuffer.class));
return intercepted;
})) {
return history.claimGeneratedSemantics(stage, claim(stage));
}
}
private static boolean persistCountingForces(GenerationHistory history, GenerationHistory.GenerationStage stage,
ChunkGenerationSemantics claim, AtomicInteger forces) throws Exception {
try (MockedStatic<FileChannel> ignored = mockStatic(FileChannel.class, invocation -> {
FileChannel source = (FileChannel) invocation.callRealMethod();
Path path = invocation.getArgument(0);
if (invocation.getMethod().getParameterCount() != 2
|| !path.getFileName().toString().endsWith(".iswal")) {
return source;
}
FileChannel intercepted = mock(FileChannel.class, delegatesTo(source));
doAnswer(force -> {
forces.incrementAndGet();
source.force(true);
return null;
}).when(intercepted).force(true);
return intercepted;
})) {
return history.claimGeneratedSemantics(stage, claim);
}
}
private static void awaitBlockedOnHistory(GenerationHistory history, List<Thread> workers, int count)
throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
while (System.nanoTime() < deadline) {
int blocked = 0;
for (Thread worker : workers) {
ThreadInfo info = ManagementFactory.getThreadMXBean().getThreadInfo(worker.threadId());
if (info != null && info.getThreadState() == Thread.State.BLOCKED
&& info.getLockInfo().getIdentityHashCode() == System.identityHashCode(history)) {
blocked++;
}
}
if (blocked == count) {
return;
}
Thread.sleep(1L);
}
throw new AssertionError("Semantic claim callers did not queue behind the History monitor");
}
private static boolean persistPaused(GenerationHistory history, GenerationHistory.GenerationStage stage,
ChunkGenerationSemantics claim, CountDownLatch entered, CountDownLatch release) throws Exception {
try (MockedStatic<FileChannel> ignored = mockStatic(FileChannel.class, invocation -> {
FileChannel channel = (FileChannel) invocation.callRealMethod();
Path path = invocation.getArgument(0);
if (!path.getFileName().toString().endsWith(".iswal")) {
if (invocation.getMethod().getParameterCount() != 2
|| !path.getFileName().toString().endsWith(".iswal")) {
return channel;
}
FileChannel intercepted = mock(FileChannel.class, delegatesTo(channel));
@@ -0,0 +1,164 @@
package art.arcane.iris.engine.history;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.MockedStatic;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.AdditionalAnswers.delegatesTo;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
public class GenerationSemanticBatchWriteTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void batchedJournalRetainsSerialBytesAndPerClaimResults() throws Exception {
Path serialRoot = temporaryFolder.newFolder().toPath();
Path batchedRoot = temporaryFolder.newFolder().toPath();
GenerationSemanticIndex serial = GenerationSemanticIndex.initialize(serialRoot);
GenerationSemanticIndex batched = GenerationSemanticIndex.initialize(batchedRoot);
List<GenerationSemanticIndex.Claim> claims = List.of(
pending(1, "first"), pending(2, "second"), pending(1, "first"), pending(1, "conflict"));
serial.claimAndPersist(claims.get(0).update);
serial.claimAndPersist(claims.get(1).update);
batched.claimAndPersistBatch(claims);
assertTrue(claims.get(0).result());
assertTrue(claims.get(1).result());
assertFalse(claims.get(2).result());
assertThrows(IllegalStateException.class, claims.get(3)::result);
assertArrayEquals(Files.readAllBytes(serial.storageDirectory().resolve("r.0.0.iswal")),
Files.readAllBytes(batched.storageDirectory().resolve("r.0.0.iswal")));
assertEquals(serial.recordsSnapshot(), GenerationSemanticIndex.loadRequired(batchedRoot).recordsSnapshot());
}
@Test
public void partialBatchRollbackFailsDependentDuplicatesAndKeepsDurableDuplicates() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
GenerationSemanticIndex index = GenerationSemanticIndex.initialize(root);
index.claimAndPersist(pending(0, "durable").update);
Path journal = index.storageDirectory().resolve("r.0.0.iswal");
byte[] original = Files.readAllBytes(journal);
List<GenerationSemanticIndex.Claim> claims = List.of(
pending(1, "first"), pending(1, "first"), pending(2, "second"), pending(0, "durable"));
IOException failure = new IOException("Second frame write failed");
AtomicInteger writes = new AtomicInteger();
try (MockedStatic<FileChannel> ignored = channels((path, source, intercepted) ->
doAnswer(invocation -> {
ByteBuffer bytes = invocation.getArgument(0);
if (writes.incrementAndGet() == 2) {
int limit = bytes.limit();
bytes.limit(bytes.position() + 7);
source.write(bytes);
bytes.limit(limit);
throw failure;
}
return source.write(bytes);
}).when(intercepted).write(any(ByteBuffer.class)))) {
index.claimAndPersistBatch(claims);
}
for (int indexInBatch = 0; indexInBatch < 3; indexInBatch++) {
assertSame(failure, assertThrows(IOException.class, claims.get(indexInBatch)::result));
}
assertFalse(claims.get(3).result());
assertArrayEquals(original, Files.readAllBytes(journal));
assertEquals(1, GenerationSemanticIndex.loadRequired(root).recordCount());
List<GenerationSemanticIndex.Claim> retry = List.of(pending(1, "first"), pending(2, "second"));
index.claimAndPersistBatch(retry);
assertTrue(retry.get(0).result());
assertTrue(retry.get(1).result());
assertEquals(3, GenerationSemanticIndex.loadRequired(root).recordCount());
}
@Test
public void failedRegionDoesNotUndoOtherDurableRegionResults() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
GenerationSemanticIndex index = GenerationSemanticIndex.initialize(root);
List<GenerationSemanticIndex.Claim> claims = List.of(
pending(0, "first"), pending(32, "failed"), pending(64, "last"));
IOException failure = new IOException("Middle region force failed");
AtomicInteger forces = new AtomicInteger();
try (MockedStatic<FileChannel> ignored = channels((path, source, intercepted) -> {
if (!path.getFileName().toString().equals("r.1.0.iswal")) {
return;
}
doAnswer(invocation -> {
if (forces.incrementAndGet() == 1) {
throw failure;
}
source.force(true);
return null;
}).when(intercepted).force(true);
})) {
index.claimAndPersistBatch(claims);
}
assertTrue(claims.get(0).result());
assertSame(failure, assertThrows(IOException.class, claims.get(1)::result));
assertTrue(claims.get(2).result());
GenerationSemanticIndex loaded = GenerationSemanticIndex.loadRequired(root);
assertEquals(2, loaded.recordCount());
assertTrue(loaded.get(32, 0).isEmpty());
}
@Test
public void unexpectedBatchFailureCompletesEveryClaimAndPoisonsFurtherReads() throws Exception {
GenerationSemanticIndex index = GenerationSemanticIndex.initialize(temporaryFolder.newFolder().toPath());
List<GenerationSemanticIndex.Claim> claims = List.of(
pending(0, "first"), pending(0, "first"), pending(32, "other-region"));
IllegalStateException failure = new IllegalStateException("Unexpected channel failure");
try (MockedStatic<FileChannel> ignored = channels((path, source, intercepted) ->
doAnswer(invocation -> {
throw failure;
}).when(intercepted).write(any(ByteBuffer.class)))) {
assertSame(failure, assertThrows(IllegalStateException.class, () -> index.claimAndPersistBatch(claims)));
}
for (GenerationSemanticIndex.Claim claim : claims) {
assertTrue(claim.completed);
assertSame(failure, assertThrows(IllegalStateException.class, claim::result));
}
assertThrows(IllegalStateException.class, () -> index.get(0, 0));
assertThrows(IOException.class, () -> index.claimAndPersist(pending(1, "later").update));
}
private static GenerationSemanticIndex.Claim pending(int chunkX, String biome) {
return new GenerationSemanticIndex.Claim(ChunkGenerationSemantics.builder(chunkX, 0, 1L)
.addSurfaceBiome("iris:" + biome).seal().build());
}
private static MockedStatic<FileChannel> channels(ChannelMutation mutation) {
return mockStatic(FileChannel.class, invocation -> {
FileChannel source = (FileChannel) invocation.callRealMethod();
Path path = invocation.getArgument(0);
if (invocation.getMethod().getParameterCount() != 2
|| !path.getFileName().toString().endsWith(".iswal")) {
return source;
}
FileChannel intercepted = mock(FileChannel.class, delegatesTo(source));
mutation.apply(path, source, intercepted);
return intercepted;
});
}
private interface ChannelMutation {
void apply(Path path, FileChannel source, FileChannel intercepted) throws IOException;
}
}
@@ -8,6 +8,7 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.StructurePlacementMarker;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.B;
import art.arcane.volmlib.util.function.Consumer4;
@@ -20,6 +21,7 @@ import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@@ -106,6 +108,55 @@ public final class GenerationSemanticCaptureTest {
verify(fixture.engine, never()).getCaveBiome(anyInt(), anyInt(), anyInt());
}
@Test
public void cavePositionsRemainDistinctAcrossColumnsAndSectionBoundaries() {
Fixture fixture = new Fixture();
List<Integer> heights = List.of(0, 15, 16, 255, 256, 767, 4063);
Set<String> expected = new HashSet<>();
List<ChunkGenerationSemantics.BlockPosition> resolved = new ArrayList<>();
when(fixture.engine.getCaveBiome(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
int x = invocation.getArgument(0);
int y = invocation.getArgument(1);
int z = invocation.getArgument(2);
resolved.add(new ChunkGenerationSemantics.BlockPosition(x, y, z));
IrisBiome biome = new IrisBiome();
biome.setLoadKey(x + ":" + y + ":" + z);
return biome;
});
doAnswer(invocation -> {
Consumer4<Integer, Integer, Integer, MatterCavern> consumer = invocation.getArgument(3);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y : heights) {
consumer.accept(x, y, z, new MatterCavern(true, null, (byte) 0));
expected.add((x - 32) + ":" + y + ":" + (z + 48));
}
}
}
return null;
}).when(fixture.mantle).iterateChunk(eq(-2), eq(3), eq(MatterCavern.class), any());
HydrologyCaveCell duplicate = mock(HydrologyCaveCell.class);
when(duplicate.floodedBiomeKey()).thenReturn("iris:duplicate");
when(duplicate.fluidProfileKey()).thenReturn("iris:river");
doAnswer(invocation -> {
Consumer4<Integer, Integer, Integer, HydrologyCaveCell> consumer = invocation.getArgument(3);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y : heights) {
consumer.accept(x, y, z, duplicate);
}
}
}
return null;
}).when(fixture.mantle).iterateChunk(eq(-2), eq(3), eq(HydrologyCaveCell.class), any());
ChunkGenerationSemantics captured = GenerationSemanticCapture.capture(fixture.engine, -2, 3, 5L);
assertEquals(expected, captured.caveBiomeKeys());
assertEquals(expected.size(), resolved.size());
assertEquals(Set.of("iris:river"), captured.riverProfileKeys());
}
@Test
@SuppressWarnings("unchecked")
public void unsealedPoiQueryUsesMantleWithWorldHorizontalCoordinates() {
@@ -0,0 +1,276 @@
package art.arcane.iris.engine.history;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.MockedStatic;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import static org.mockito.AdditionalAnswers.delegatesTo;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
public class GenerationSemanticJournalWriteTest {
@Rule
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void partialAppendRollsBackBeforeAConcurrentClaimCanPublish() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
GenerationSemanticIndex index = GenerationSemanticIndex.initialize(root);
index.claimAndPersist(claim(0, "first"));
Path journal = index.storageDirectory().resolve("r.0.0.iswal");
byte[] original = Files.readAllBytes(journal);
IOException failure = new IOException("Partial journal write");
CountDownLatch written = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
Future<Boolean> rejected = executor.submit(() -> {
try (MockedStatic<FileChannel> ignored = journalChannels((source, intercepted) ->
doAnswer(invocation -> {
ByteBuffer bytes = invocation.getArgument(0);
int limit = bytes.limit();
bytes.limit(bytes.position() + 7);
source.write(bytes);
bytes.limit(limit);
written.countDown();
assertTrue(release.await(5L, TimeUnit.SECONDS));
throw failure;
}).when(intercepted).write(any(ByteBuffer.class)))) {
return index.claimAndPersist(claim(1, "failed"));
}
});
assertTrue(written.await(5L, TimeUnit.SECONDS));
CountDownLatch attempted = new CountDownLatch(1);
Future<Boolean> accepted = executor.submit(() -> {
attempted.countDown();
return index.claimAndPersist(claim(2, "second"));
});
assertTrue(attempted.await(5L, TimeUnit.SECONDS));
release.countDown();
assertSame(failure, assertThrows(ExecutionException.class,
() -> rejected.get(5L, TimeUnit.SECONDS)).getCause());
assertTrue(accepted.get(5L, TimeUnit.SECONDS));
assertFalse(index.get(1, 0).isPresent());
GenerationSemanticIndex replayed = GenerationSemanticIndex.loadRequired(root);
assertEquals(2, replayed.recordCount());
assertEquals(Optional.of(claim(0, "first")), replayed.get(0, 0));
assertEquals(Optional.of(claim(2, "second")), replayed.get(2, 0));
assertTrue(replayed.get(1, 0).isEmpty());
assertArrayEquals(original, Arrays.copyOf(Files.readAllBytes(journal), original.length));
} finally {
release.countDown();
executor.shutdownNow();
assertTrue(executor.awaitTermination(5L, TimeUnit.SECONDS));
}
}
@Test
public void failedForceRestoresBytesAndLeavesTheClaimRetryable() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
GenerationSemanticIndex index = GenerationSemanticIndex.initialize(root);
index.claimAndPersist(claim(0, "first"));
Path journal = index.storageDirectory().resolve("r.0.0.iswal");
byte[] original = Files.readAllBytes(journal);
IOException failure = new IOException("Journal force failed");
AtomicInteger forces = new AtomicInteger();
try (MockedStatic<FileChannel> ignored = journalChannels((source, intercepted) ->
doAnswer(invocation -> {
if (forces.incrementAndGet() == 1) {
throw failure;
}
source.force(true);
return null;
}).when(intercepted).force(true))) {
assertSame(failure, assertThrows(IOException.class, () -> index.claimAndPersist(claim(1, "second"))));
}
assertEquals(2, forces.get());
assertArrayEquals(original, Files.readAllBytes(journal));
assertTrue(index.get(1, 0).isEmpty());
assertTrue(index.claimAndPersist(claim(1, "second")));
assertEquals(Optional.of(claim(1, "second")), GenerationSemanticIndex.loadRequired(root).get(1, 0));
}
@Test
public void rollbackFailurePreservesCausesAndRejectsFurtherMutation() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
GenerationSemanticIndex index = GenerationSemanticIndex.initialize(root);
index.claimAndPersist(claim(0, "first"));
IOException failure = new IOException("Journal force failed");
IOException rollback = new IOException("Rollback force failed");
AtomicInteger forces = new AtomicInteger();
IOException reported;
try (MockedStatic<FileChannel> ignored = journalChannels((source, intercepted) ->
doAnswer(invocation -> {
throw forces.incrementAndGet() == 1 ? failure : rollback;
}).when(intercepted).force(true))) {
reported = assertThrows(IOException.class, () -> index.claimAndPersist(claim(1, "failed")));
}
assertSame(failure, reported.getCause());
assertArrayEquals(new Throwable[]{rollback}, failure.getSuppressed());
Path journal = index.storageDirectory().resolve("r.0.0.iswal");
byte[] afterFailure = Files.readAllBytes(journal);
assertThrows(IOException.class, () -> index.claimAndPersist(claim(2, "rejected")));
assertThrows(IOException.class, () -> index.recordAndPersist(claim(2, "rejected")));
assertThrows(IOException.class, index::compactJournals);
assertThrows(IllegalStateException.class, () -> index.get(0, 0));
assertThrows(IllegalStateException.class, () -> match(index, "first"));
assertThrows(IllegalStateException.class, () -> index.findNearest(GenerationSemanticIndex.Query.acrossActivations(
GenerationSemanticIndex.SemanticKind.REGION, "iris:first",
new ChunkGenerationSemantics.BlockPosition(1_000_000, 64, 1_000_000), 32)));
assertArrayEquals(afterFailure, Files.readAllBytes(journal));
assertEquals(1, index.recordCount());
}
@Test
public void closeFailureDoesNotAllowFurtherWritesOverAnUnpublishedClaim() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
GenerationSemanticIndex index = GenerationSemanticIndex.initialize(root);
IOException failure = new IOException("Journal close failed");
try (MockedStatic<FileChannel> ignored = journalChannels((source, intercepted) ->
doAnswer(invocation -> {
source.close();
throw failure;
}).when(intercepted).close())) {
IOException reported = assertThrows(IOException.class, () -> index.claimAndPersist(claim(1, "second")));
assertSame(failure, reported.getCause());
}
assertEquals(0, index.recordCount());
assertThrows(IOException.class, () -> index.claimAndPersist(claim(2, "rejected")));
assertThrows(IllegalStateException.class, index::recordsSnapshot);
assertThrows(IOException.class, () -> index.forEachRecord(semantics -> {
throw new AssertionError("Failed journal reached record consumer");
}));
assertThrows(IOException.class, () -> index.forEachSealedClaim(1L, (chunkX, chunkZ) -> {
throw new AssertionError("Failed journal reached sealed claim consumer");
}));
GenerationSemanticIndex replayed = GenerationSemanticIndex.loadRequired(root);
assertEquals(Optional.of(claim(1, "second")), replayed.get(1, 0));
assertTrue(replayed.get(2, 0).isEmpty());
}
@Test
public void newJournalDirectoryFailureRollsBackBeforeRetry() throws Exception {
assumeTrue(File.separatorChar != '\\');
Path root = temporaryFolder.newFolder().toPath();
GenerationSemanticIndex index = GenerationSemanticIndex.initialize(root);
IOException failure = new IOException("Journal directory force failed");
AtomicInteger directoryForces = new AtomicInteger();
try (MockedStatic<FileChannel> ignored = mockStatic(FileChannel.class, invocation -> {
FileChannel source = (FileChannel) invocation.callRealMethod();
Path path = invocation.getArgument(0);
if (invocation.getMethod().getParameterCount() != 2 || !path.equals(index.storageDirectory())) {
return source;
}
FileChannel intercepted = mock(FileChannel.class, delegatesTo(source));
doAnswer(force -> {
if (directoryForces.incrementAndGet() == 1) {
throw failure;
}
source.force(true);
return null;
}).when(intercepted).force(true);
return intercepted;
})) {
assertSame(failure, assertThrows(IOException.class, () -> index.claimAndPersist(claim(0, "first"))));
}
assertEquals(2, directoryForces.get());
assertEquals(0L, Files.size(index.storageDirectory().resolve("r.0.0.iswal")));
assertTrue(index.get(0, 0).isEmpty());
assertTrue(index.claimAndPersist(claim(0, "first")));
assertEquals(Optional.of(claim(0, "first")), GenerationSemanticIndex.loadRequired(root).get(0, 0));
}
@Test
public void queriedSummariesIncludeNewClaimsAfterMutationAndCompaction() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
GenerationSemanticIndex index = GenerationSemanticIndex.initialize(root);
index.claimAndPersist(claim(0, "first"));
assertEquals(0, match(index, "first").chunk().chunkX());
index.claimAndPersist(claim(1, "second"));
assertEquals(1, match(index, "second").chunk().chunkX());
assertEquals(0, match(index, "first").chunk().chunkX());
index.compactJournals();
GenerationSemanticIndex replayed = GenerationSemanticIndex.loadRequired(root);
assertEquals(0, match(replayed, "first").chunk().chunkX());
assertEquals(1, match(replayed, "second").chunk().chunkX());
}
@Test
public void queriedSummariesTrackRecordedUpdatesAndPrunedClaims() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
GenerationSemanticIndex index = GenerationSemanticIndex.initialize(root);
index.recordAndPersist(ChunkGenerationSemantics.builder(0, 0, 1L).addRegion("iris:first").build());
assertEquals(0, match(index, "first").chunk().chunkX());
index.recordAndPersist(ChunkGenerationSemantics.builder(0, 0, 1L).addRegion("iris:updated").build());
assertEquals(0, match(index, "updated").chunk().chunkX());
index.claimAndPersist(claim(1, "second"));
assertEquals(1, match(index, "second").chunk().chunkX());
WorldChunkInventory stored = WorldChunkInventory.ofPackedChunks(ChunkGenerationOwnership.packChunk(0, 0));
assertEquals(1, index.discardUnstoredClaims(stored, Set.of(1L)));
assertTrue(index.findNearest(GenerationSemanticIndex.Query.acrossActivations(
GenerationSemanticIndex.SemanticKind.REGION, "iris:second",
new ChunkGenerationSemantics.BlockPosition(0, 64, 0), 32)).isEmpty());
assertEquals(0, match(index, "first").chunk().chunkX());
assertEquals(0, match(index, "updated").chunk().chunkX());
GenerationSemanticIndex replayed = GenerationSemanticIndex.loadRequired(root);
assertEquals(0, match(replayed, "updated").chunk().chunkX());
assertTrue(replayed.get(1, 0).isEmpty());
}
private static GenerationSemanticIndex.Match match(GenerationSemanticIndex index, String region) {
return index.findNearest(GenerationSemanticIndex.Query.acrossActivations(
GenerationSemanticIndex.SemanticKind.REGION, "iris:" + region,
new ChunkGenerationSemantics.BlockPosition(0, 64, 0), 32)).orElseThrow();
}
private static ChunkGenerationSemantics claim(int chunkX, String region) {
return ChunkGenerationSemantics.builder(chunkX, 0, 1L).addRegion("iris:" + region).seal().build();
}
private static MockedStatic<FileChannel> journalChannels(ChannelMutation mutation) {
return mockStatic(FileChannel.class, invocation -> {
FileChannel source = (FileChannel) invocation.callRealMethod();
Path path = invocation.getArgument(0);
if (invocation.getMethod().getParameterCount() != 2
|| !path.getFileName().toString().endsWith(".iswal")) {
return source;
}
FileChannel intercepted = mock(FileChannel.class, delegatesTo(source));
mutation.apply(source, intercepted);
return intercepted;
});
}
@FunctionalInterface
private interface ChannelMutation {
void apply(FileChannel source, FileChannel intercepted) throws IOException;
}
}
@@ -0,0 +1,58 @@
package art.arcane.iris.engine.history;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
public final class GenerationSemanticKeyValidationTest {
@Test
public void duplicateKeysRetainValidationAndCanonicalOrdering() {
ChunkGenerationSemantics.Builder builder = ChunkGenerationSemantics.builder(-2, 3, 5L);
List<String> keys = List.of("iris:deep", "Caves/Crystal Gallery", "Région/Highlands", "cave/\ud801\udc00");
for (int repetition = 0; repetition < 8; repetition++) {
for (String key : keys) {
builder.addSurfaceBiome(new String(key)).addCaveBiome(new String(key))
.addRegion(new String(key)).addRiverProfile(new String(key)).addObject(new String(key));
}
}
for (String invalid : List.of("", " iris:deep", "iris:deep ", "iris:\\deep", "iris:\ndeep",
"é".repeat(ChunkGenerationSemantics.MAX_KEY_BYTES))) {
assertThrows(IllegalArgumentException.class, () -> builder.addSurfaceBiome(invalid));
assertThrows(IllegalArgumentException.class, () -> builder.addCaveBiome(invalid));
assertThrows(IllegalArgumentException.class, () -> builder.addRegion(invalid));
assertThrows(IllegalArgumentException.class, () -> builder.addRiverProfile(invalid));
assertThrows(IllegalArgumentException.class, () -> builder.addObject(invalid));
}
assertEquals("key", assertThrows(NullPointerException.class, () -> builder.addCaveBiome(null)).getMessage());
ChunkGenerationSemantics result = builder.seal().build();
assertEquals(Set.copyOf(keys), result.surfaceBiomeKeys());
assertEquals(Set.copyOf(keys), result.caveBiomeKeys());
assertEquals(Set.copyOf(keys), result.regionKeys());
assertEquals(Set.copyOf(keys), result.riverProfileKeys());
assertEquals(Set.copyOf(keys), result.objectKeys());
List<String> sorted = new ArrayList<>(keys);
Collections.sort(sorted);
for (Set<String> recorded : List.of(result.surfaceBiomeKeys(), result.caveBiomeKeys(), result.regionKeys(),
result.riverProfileKeys(), result.objectKeys())) {
assertEquals(sorted, List.copyOf(recorded));
}
}
@Test
public void fullKeySetAcceptsDuplicatesAndRejectsNewKeysWithoutMutation() {
ChunkGenerationSemantics.Builder builder = ChunkGenerationSemantics.builder(0, 0, 5L);
for (int index = 0; index < ChunkGenerationSemantics.MAX_KEYS_PER_KIND; index++) {
builder.addCaveBiome("iris:cave_" + index);
}
builder.addCaveBiome(new String("iris:cave_0"));
assertThrows(IllegalArgumentException.class, () -> builder.addCaveBiome("iris:overflow"));
assertThrows(IllegalArgumentException.class, () -> builder.addCaveBiome(" iris:cave_0"));
assertEquals(ChunkGenerationSemantics.MAX_KEYS_PER_KIND, builder.build().caveBiomeKeys().size());
}
}
@@ -0,0 +1,313 @@
package art.arcane.iris.engine.history;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.MockedStatic;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
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 java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
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.junit.Assert.assertTrue;
import static org.mockito.AdditionalAnswers.delegatesTo;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
public class GenerationSemanticPointReadTest {
@Rule
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void readsRetainDurableShardAfterEvictionDuringUnforcedAppend() throws Exception {
GenerationSemanticIndex index = index();
for (int region = 0; region <= 70; region++) {
index.claimAndPersist(claim(region * 32, 1));
}
ExecutorService reader = Executors.newSingleThreadExecutor();
try (AppendGate gate = append(() -> index.claimAndPersist(claim(1, 1)), Failure.NONE)) {
Future<?> inspected = reader.submit(() -> {
assertSameRecord(claim(0, 1), index.get(0, 0));
assertTrue(index.get(1, 0).isEmpty());
assertTrue(index.hasSealedClaim(0, 0, 1));
assertFalse(index.hasSealedClaim(1, 0, 1));
for (int region = 1; region <= 70; region++) {
assertSameRecord(claim(region * 32, 1), index.get(region * 32, 0));
}
assertSameRecord(claim(0, 1), index.get(0, 0));
assertTrue(index.get(1, 0).isEmpty());
assertTrue(index.get(-1, -1).isEmpty());
});
inspected.get(2, TimeUnit.SECONDS);
gate.finish();
assertNull(gate.failure.get());
assertSameRecord(claim(1, 1), index.get(1, 0));
assertTrue(index.cachedRegionCount() <= 64);
assertEquals(72, index.recordCount());
} finally {
drain(reader);
}
}
@Test
public void newRegionRemainsAbsentUntilItsFirstForceCompletes() throws Exception {
GenerationSemanticIndex index = index();
ExecutorService reader = Executors.newSingleThreadExecutor();
try (AppendGate gate = append(() -> index.claimAndPersist(claim(-32, 1)), Failure.NONE)) {
assertTrue(reader.submit(() -> index.get(-32, 0).isEmpty()).get(2, TimeUnit.SECONDS));
gate.finish();
assertNull(gate.failure.get());
assertSameRecord(claim(-32, 1), index.get(-32, 0));
} finally {
drain(reader);
}
}
@Test
public void confirmedRollbackKeepsPublishedReadsAndPermitsRetry() throws Exception {
GenerationSemanticIndex index = index();
index.claimAndPersist(claim(0, 1));
try (AppendGate gate = append(() -> index.claimAndPersist(claim(1, 1)), Failure.ROLLBACK_CONFIRMED)) {
gate.finish();
assertSame(gate.originalFailure, gate.failure.get());
assertSameRecord(claim(0, 1), index.get(0, 0));
assertTrue(index.get(1, 0).isEmpty());
assertTrue(index.claimAndPersist(claim(1, 1)));
assertEquals(index.recordsSnapshot(),
GenerationSemanticIndex.loadRequired(index.storageDirectory().getParent().getParent().getParent()).recordsSnapshot());
}
}
@Test
public void uncertainRollbackAndClosePoisonHitsAndMisses() throws Exception {
for (Failure mode : new Failure[]{Failure.ROLLBACK_UNCERTAIN, Failure.CLOSE_UNCERTAIN}) {
GenerationSemanticIndex index = index();
index.claimAndPersist(claim(0, 1));
try (AppendGate gate = append(() -> index.claimAndPersist(claim(1, 1)), mode)) {
gate.finish();
assertTrue(gate.failure.get() instanceof IOException);
assertSame(gate.originalFailure, gate.failure.get().getCause());
assertThrows(IllegalStateException.class, () -> index.get(0, 0));
assertThrows(IllegalStateException.class, () -> index.get(1, 0));
assertThrows(IllegalStateException.class, () -> index.get(999, 999));
assertThrows(IllegalStateException.class, () -> index.hasSealedClaim(0, 0, 1));
assertThrows(IOException.class, () -> index.claimAndPersist(claim(2, 1)));
}
}
}
@Test
public void maintenanceWaitsForAppendAndCallbacksRemainReentrant() throws Exception {
GenerationSemanticIndex index = index();
index.claimAndPersist(claim(0, 1));
ExecutorService maintenance = Executors.newFixedThreadPool(2);
Future<?> compact;
Future<Integer> prune;
try (AppendGate gate = append(() -> index.claimAndPersist(claim(1, 1)), Failure.NONE)) {
CountDownLatch started = new CountDownLatch(2);
compact = maintenance.submit(() -> {
started.countDown();
index.compactJournals();
return null;
});
prune = maintenance.submit(() -> {
started.countDown();
return index.discardUnstoredClaims(WorldChunkInventory.ofPackedChunks(
ChunkGenerationOwnership.packChunk(0, 0)), Set.of(1L));
});
assertTrue(started.await(2, TimeUnit.SECONDS));
assertThrows(TimeoutException.class, () -> compact.get(100, TimeUnit.MILLISECONDS));
assertThrows(TimeoutException.class, () -> prune.get(100, TimeUnit.MILLISECONDS));
gate.finish();
assertNull(gate.failure.get());
compact.get(5, TimeUnit.SECONDS);
assertEquals(1, prune.get(5, TimeUnit.SECONDS).intValue());
AtomicBoolean recorded = new AtomicBoolean();
maintenance.submit(() -> {
index.forEachRecord(record -> {
assertEquals(record, index.get(record.chunkX(), record.chunkZ()).orElseThrow());
if (recorded.compareAndSet(false, true)) {
index.claimAndPersist(claim(2, 1));
}
});
return null;
}).get(5, TimeUnit.SECONDS);
assertEquals(2, index.recordCount());
assertTrue(index.get(1, 0).isEmpty());
assertSameRecord(claim(2, 1), index.get(2, 0));
} finally {
drain(maintenance);
}
}
@Test
public void historyPointReadsProceedWhileClaimStillFencesPromotion() throws Exception {
GenerationHistory history = history();
Path replacement = pack("replacement", "beta");
GenerationActivation pending = history.stageUpdate(replacement, fingerprint(replacement),
history.activeEpoch().dimensionContract(), GenerationRegistryContract.empty(), 256);
ExecutorService tasks = Executors.newFixedThreadPool(2);
try (GenerationAdmission.RuntimeLease runtime = history.retainRuntime();
GenerationHistory.GenerationStage stage = history.openStage(1, 0);
AppendGate gate = append(() -> history.claimGeneratedSemantics(stage, claim(1, 1)), Failure.NONE)) {
tasks.submit(() -> {
assertTrue(history.semantics(1, 0).isEmpty());
assertEquals(stage.activation(), history.resolveActivation(1, 0));
assertEquals(stage.activation(), history.resolveActivation(-32, -32));
}).get(2, TimeUnit.SECONDS);
stage.close();
CountDownLatch cutoverEntered = new CountDownLatch(1);
Future<GenerationActivation> promoted = tasks.submit(() -> {
try (GenerationHistory.LiveCutover cutover = history.beginLiveCutover()) {
cutoverEntered.countDown();
return cutover.promote(boundary -> (x, z) -> null);
}
});
assertTrue(cutoverEntered.await(2, TimeUnit.SECONDS));
assertThrows(TimeoutException.class, () -> promoted.get(100, TimeUnit.MILLISECONDS));
assertTrue(tasks.submit(() -> history.semantics(1, 0).isEmpty()).get(2, TimeUnit.SECONDS));
gate.finish();
assertNull(gate.failure.get());
assertEquals(pending.activationId(), promoted.get(5, TimeUnit.SECONDS).activationId());
assertEquals(pending.activationId(), history.resolveActivation(1, 0).activationId());
assertEquals(pending.activationId(), history.resolveActivation(-32, -32).activationId());
assertTrue(history.semantics(1, 0).isEmpty());
} finally {
drain(tasks);
}
}
private GenerationSemanticIndex index() throws IOException {
return GenerationSemanticIndex.initialize(temporaryFolder.newFolder().toPath());
}
private GenerationHistory history() throws Exception {
Path world = temporaryFolder.newFolder().toPath();
Path pack = pack("initial", "alpha");
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, fingerprint(pack), 42L, contract, GenerationRegistryContract.empty());
}
private Path pack(String name, String content) throws IOException {
Path pack = temporaryFolder.newFolder(name).toPath();
Files.createDirectories(pack.resolve("dimensions"));
Files.writeString(pack.resolve("dimensions/main.json"), content);
return pack;
}
private static String fingerprint(Path pack) throws IOException {
return GenerationPackFingerprint.compute(pack, GenerationPackFingerprint.CURRENT_VERSION);
}
private static ChunkGenerationSemantics claim(int chunkX, long activation) {
return ChunkGenerationSemantics.builder(chunkX, 0, activation)
.addSurfaceBiome("iris:forest").seal().build();
}
private static void assertSameRecord(ChunkGenerationSemantics expected, Optional<ChunkGenerationSemantics> actual) {
assertEquals(expected, actual.orElseThrow());
}
private static AppendGate append(Callable<?> operation, Failure mode) throws InterruptedException {
AppendGate gate = new AppendGate(operation, mode);
gate.worker.start();
assertTrue(gate.forcing.await(5, TimeUnit.SECONDS));
return gate;
}
private static void drain(ExecutorService executor) throws InterruptedException {
executor.shutdown();
assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS));
}
private enum Failure {
NONE,
ROLLBACK_CONFIRMED,
ROLLBACK_UNCERTAIN,
CLOSE_UNCERTAIN
}
private static final class AppendGate implements AutoCloseable {
private final CountDownLatch forcing = new CountDownLatch(1);
private final CountDownLatch release = new CountDownLatch(1);
private final AtomicReference<Throwable> failure = new AtomicReference<>();
private final IOException originalFailure = new IOException("Semantic journal failed");
private final Thread worker;
private AppendGate(Callable<?> operation, Failure mode) {
worker = new Thread(() -> {
try (MockedStatic<FileChannel> ignored = mockStatic(FileChannel.class, invocation -> {
FileChannel source = (FileChannel) invocation.callRealMethod();
Path path = invocation.getArgument(0);
if (invocation.getMethod().getParameterCount() != 2
|| !path.getFileName().toString().endsWith(".iswal")) {
return source;
}
FileChannel intercepted = mock(FileChannel.class, delegatesTo(source));
AtomicInteger forces = new AtomicInteger();
doAnswer(call -> {
int count = forces.incrementAndGet();
if (count == 1) {
forcing.countDown();
if (!release.await(10, TimeUnit.SECONDS)) {
throw new IOException("Timed out waiting to resume semantic force");
}
if (mode == Failure.ROLLBACK_CONFIRMED || mode == Failure.ROLLBACK_UNCERTAIN) {
throw originalFailure;
}
}
if (count == 2 && mode == Failure.ROLLBACK_UNCERTAIN) {
throw new IOException("Semantic rollback failed");
}
source.force(true);
return null;
}).when(intercepted).force(true);
if (mode == Failure.CLOSE_UNCERTAIN) {
doAnswer(call -> {
source.close();
throw originalFailure;
}).when(intercepted).close();
}
return intercepted;
})) {
operation.call();
} catch (Throwable caught) {
failure.set(caught);
}
}, "semantic-point-read-test");
}
private void finish() throws InterruptedException {
release.countDown();
worker.join(5_000L);
assertFalse("Semantic append did not finish", worker.isAlive());
}
@Override
public void close() throws InterruptedException {
finish();
}
}
}
@@ -2,15 +2,23 @@ package art.arcane.iris.engine.history;
import org.junit.Test;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.UTFDataFormatException;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.List;
import java.util.OptionalInt;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
public class NativeTerrainReceiptTest {
@Test
@@ -19,6 +27,7 @@ public class NativeTerrainReceiptTest {
SavedTerrainChunk chunk = chunk(boundary);
String epoch = "epoch-\u0000-é-水-\ud83c\udf32";
byte[] encoded = NativeTerrainReceipt.encode(chunk, 7, epoch);
assertArrayEquals(legacyEncode(chunk, 7, epoch), encoded);
byte[] payload;
try (GZIPInputStream input = new GZIPInputStream(new ByteArrayInputStream(encoded))) {
payload = input.readAllBytes();
@@ -51,6 +60,114 @@ public class NativeTerrainReceiptTest {
}
}
@Test
public void saturatedStringCachesPreserveModifiedUtfBytes() throws Exception {
for (int padding : new int[]{0, 300}) {
List<String> keys = new ArrayList<>(300);
for (int index = 0; index < 300; index++) {
keys.add("iris:" + index + "-é-水-\u0000-\ud83c\udf32-\ud800-\udc00-" + "x".repeat(padding));
}
for (int index = 0; index < 300; index++) {
keys.add(keys.getFirst());
}
SavedTerrainChunk chunk = biomeChunk(keys);
byte[] encoded = NativeTerrainReceipt.encode(chunk, 9, "epoch");
assertArrayEquals(legacyEncode(chunk, 9, "epoch"), encoded);
TerrainBoundarySignature restored = NativeTerrainReceipt.decode(encoded, "minecraft:noise")
.terrain().column(0, 0);
for (int index = 0; index < keys.size() * 2; index++) {
assertEquals(keys.get(index % keys.size()), restored.biomeAtSample(index));
}
}
}
@Test
public void maximumModifiedUtfLengthAndOversizedFailuresRemainUnchanged() throws Exception {
for (String key : List.of("a".repeat(65_535), "é".repeat(32_767) + "a")) {
SavedTerrainChunk chunk = biomeChunk(List.of(key));
byte[] encoded = NativeTerrainReceipt.encode(chunk, 9, "epoch");
assertArrayEquals(legacyEncode(chunk, 9, "epoch"), encoded);
assertEquals(key, NativeTerrainReceipt.decode(encoded, "minecraft:noise")
.terrain().column(0, 0).biomeAtSample(0));
}
for (String key : List.of("a".repeat(65_536), "é".repeat(32_768), "\u0000".repeat(32_768))) {
SavedTerrainChunk chunk = biomeChunk(List.of(key));
UTFDataFormatException expected = assertThrows(UTFDataFormatException.class,
() -> legacyEncode(chunk, 9, "epoch"));
UTFDataFormatException actual = assertThrows(UTFDataFormatException.class,
() -> NativeTerrainReceipt.encode(chunk, 9, "epoch"));
assertEquals(expected.getMessage(), actual.getMessage());
}
}
private static SavedTerrainChunk biomeChunk(List<String> keys) {
List<TerrainBoundarySignature> columns = new ArrayList<>(256);
short[] samples = new short[keys.size() * 2];
for (int index = 0; index < samples.length; index++) {
samples[index] = (short) (index % keys.size());
}
BoundaryColumnGeometry geometry = new BoundaryColumnGeometry(0,
List.of(new BoundaryColumnGeometry.Voxel("minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false)),
new int[]{samples.length * 4}, new short[]{0});
columns.add(new TerrainBoundarySignature(new TerrainBoundarySignature.Column(0, 0, 1, 1,
OptionalInt.empty(), OptionalInt.empty()),
new TerrainBoundarySignature.Samples(new TerrainBoundarySignature.VerticalLayout(0, 4, samples.length),
new TerrainBoundarySignature.BiomeEncoding(keys, samples)), geometry));
for (int index = 1; index < 256; index++) {
columns.add(null);
}
return new SavedTerrainChunk(0, 0, "minecraft:noise", columns);
}
private static byte[] legacyEncode(SavedTerrainChunk chunk, long activation, String epoch) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream output = new DataOutputStream(new BufferedOutputStream(new GZIPOutputStream(bytes)))) {
output.writeInt(0x4952544E);
output.writeInt(1);
output.writeInt(chunk.chunkX());
output.writeInt(chunk.chunkZ());
output.writeLong(activation);
output.writeUTF(epoch);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
output.writeBoolean(chunk.hasColumn(x, z));
if (!chunk.hasColumn(x, z)) {
continue;
}
TerrainBoundarySignature signature = chunk.column((chunk.chunkX() << 4) + x, (chunk.chunkZ() << 4) + z);
output.writeInt(signature.surfaceHeight());
output.writeInt(signature.oceanFloorHeight());
output.writeInt(signature.fluidHeight().orElse(-1));
output.writeInt(signature.upperCeilingDepth().orElse(-1));
TerrainBoundarySignature.VerticalLayout layout = signature.samples().layout();
output.writeInt(layout.minimumY());
output.writeInt(layout.sampleStep());
output.writeInt(layout.sampleCount());
for (int index = 0; index < layout.sampleCount(); index++) {
output.writeUTF(signature.biomeAtSample(index));
}
BoundaryColumnGeometry geometry = signature.geometry();
output.writeInt(geometry.minimumY());
output.writeInt(geometry.palette().size());
for (BoundaryColumnGeometry.Voxel voxel : geometry.palette()) {
output.writeUTF(voxel.stateKey());
output.writeByte(voxel.phase().ordinal());
output.writeUTF(voxel.fluidStateKey());
output.writeBoolean(voxel.protectedContent());
}
int[] ends = geometry.runEnds();
short[] indices = geometry.paletteIndices();
output.writeInt(ends.length);
for (int index = 0; index < ends.length; index++) {
output.writeInt(ends[index]);
output.writeShort(indices[index]);
}
}
}
}
return bytes.toByteArray();
}
private static SavedTerrainChunk chunk(boolean boundary) {
List<TerrainBoundarySignature> columns = new ArrayList<>(256);
List<BoundaryColumnGeometry.Voxel> palette = List.of(
@@ -0,0 +1,277 @@
package art.arcane.iris.engine.history;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.MockedConstruction;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.reflect.Field;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
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.atomic.AtomicInteger;
import static org.junit.Assert.assertArrayEquals;
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.junit.Assert.assertTrue;
import static org.mockito.AdditionalAnswers.delegatesTo;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockConstruction;
import static org.mockito.Mockito.withSettings;
public class SavedBiomeStoreBatchTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void queuedClaimsShareOneForceAndPublishAfterItCompletes() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
SavedBiomeStore store = SavedBiomeStore.open(root);
store.claimAndPersist(chunk(0, 3L));
BatchControl control = new BatchControl();
ExecutorService workers = Executors.newFixedThreadPool(8);
try {
List<Future<ClaimResult>> results = enqueue(store, root, control, workers,
List.of(chunk(1, 3L), chunk(2, 3L), chunk(3, 3L), chunk(4, 3L),
chunk(5, 3L), chunk(6, 3L), chunk(7, 3L), chunk(8, 3L)));
assertTrue(control.forcing.await(5, TimeUnit.SECONDS));
for (int index = 1; index <= 8; index++) {
assertTrue(store.cached(index, 0).isEmpty());
assertFalse(results.get(index - 1).isDone());
}
control.release.countDown();
for (Future<ClaimResult> result : results) {
ClaimResult claim = result.get(5, TimeUnit.SECONDS);
assertNull(claim.failure);
assertTrue(claim.persisted);
}
assertEquals(1, control.forces.get());
SavedBiomeStore reopened = SavedBiomeStore.open(root);
for (int index = 1; index <= 8; index++) {
assertEquals(chunk(index, 3L), reopened.get(index, 0).orElseThrow());
}
} finally {
control.release.countDown();
workers.shutdownNow();
assertTrue(workers.awaitTermination(10, TimeUnit.SECONDS));
}
}
@Test
public void batchForceFailureRollsBackEveryNewClaimButPreservesExistingDuplicates() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
SavedBiomeStore store = SavedBiomeStore.open(root);
store.claimAndPersist(chunk(0, 3L));
Path region = root.resolve("iris/generation/biomes/r.0.0.ibio");
byte[] original = Files.readAllBytes(region);
BatchControl control = new BatchControl();
control.failure = new IOException("Batch force failed");
ExecutorService workers = Executors.newFixedThreadPool(4);
try {
List<Future<ClaimResult>> results = enqueue(store, root, control, workers,
List.of(chunk(0, 3L), chunk(1, 3L), chunk(1, 3L), chunk(2, 3L)));
assertTrue(control.forcing.await(5, TimeUnit.SECONDS));
control.release.countDown();
ClaimResult existing = results.getFirst().get(5, TimeUnit.SECONDS);
assertFalse(existing.persisted);
assertNull(existing.failure);
for (int index = 1; index < results.size(); index++) {
ClaimResult result = results.get(index).get(5, TimeUnit.SECONDS);
assertFalse(result.persisted);
assertSame(control.failure, result.failure);
}
assertEquals(2, control.forces.get());
assertArrayEquals(original, Files.readAllBytes(region));
assertTrue(store.cached(1, 0).isEmpty());
assertTrue(store.claimAndPersist(chunk(1, 3L)));
assertTrue(SavedBiomeStore.open(root).get(2, 0).isEmpty());
} finally {
control.release.countDown();
workers.shutdownNow();
assertTrue(workers.awaitTermination(10, TimeUnit.SECONDS));
}
}
@Test
public void queuedDuplicateAndConflictingClaimsHaveOneWinner() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
SavedBiomeStore store = SavedBiomeStore.open(root);
store.claimAndPersist(chunk(0, 3L));
BatchControl control = new BatchControl();
ExecutorService workers = Executors.newFixedThreadPool(3);
try {
List<Future<ClaimResult>> results = enqueue(store, root, control, workers,
List.of(chunk(1, 3L), chunk(1, 3L), chunk(1, 4L)));
assertTrue(control.forcing.await(5, TimeUnit.SECONDS));
control.release.countDown();
int winners = 0;
SavedBiomeChunk actual = null;
for (int index = 0; index < results.size(); index++) {
ClaimResult result = results.get(index).get(5, TimeUnit.SECONDS);
if (result.persisted) {
winners++;
actual = chunk(1, index == 2 ? 4L : 3L);
}
}
assertEquals(1, winners);
assertEquals(1, control.forces.get());
assertEquals(actual, SavedBiomeStore.open(root).get(1, 0).orElseThrow());
for (int index = 0; index < results.size(); index++) {
ClaimResult result = results.get(index).get(5, TimeUnit.SECONDS);
assertEquals(index == 2 ? actual.activationId() != 4L : actual.activationId() != 3L,
result.failure != null);
}
} finally {
control.release.countDown();
workers.shutdownNow();
assertTrue(workers.awaitTermination(10, TimeUnit.SECONDS));
}
}
@Test
public void uncheckedBatchFailureSettlesClaimsAndRejectsEvictedRetries() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
SavedBiomeStore store = SavedBiomeStore.open(root);
store.claimAndPersist(chunk(0, 3L));
BatchControl control = new BatchControl();
control.uncheckedFailure = new IllegalStateException("Batch force failed unexpectedly");
ExecutorService workers = Executors.newFixedThreadPool(4);
try {
List<Future<ClaimResult>> results = enqueue(store, root, control, workers,
List.of(chunk(1, 3L), chunk(2, 3L), chunk(3, 3L), chunk(4, 3L)));
assertTrue(control.forcing.await(5, TimeUnit.SECONDS));
for (int region = 1; region <= 132; region++) {
if ((region & 63) != 0) {
assertTrue(store.get(region << 5, 0).isEmpty());
}
}
control.release.countDown();
for (Future<ClaimResult> future : results) {
ClaimResult result = future.get(5, TimeUnit.SECONDS);
assertFalse(result.persisted);
assertSame(control.uncheckedFailure, rootCause(result.failure));
}
assertEquals(1, control.forces.get());
IOException retry = assertThrows(IOException.class, () -> store.claimAndPersist(chunk(1, 3L)));
assertSame(control.uncheckedFailure, rootCause(retry));
IOException read = assertThrows(IOException.class, () -> store.get(1, 0));
assertSame(control.uncheckedFailure, rootCause(read));
} finally {
control.release.countDown();
workers.shutdownNow();
assertTrue(workers.awaitTermination(10, TimeUnit.SECONDS));
}
}
private static Throwable rootCause(Throwable failure) {
if (failure == null) {
return null;
}
Throwable cause = failure;
while (cause.getCause() != null) {
cause = cause.getCause();
}
return cause;
}
private static List<Future<ClaimResult>> enqueue(SavedBiomeStore store, Path root, BatchControl control,
ExecutorService workers, List<SavedBiomeChunk> chunks) throws Exception {
Field locksField = SavedBiomeStore.class.getDeclaredField("regionLocks");
locksField.setAccessible(true);
Object stripe = ((Object[]) locksField.get(store))[0];
Field writingField = stripe.getClass().getDeclaredField("writing");
Field pendingField = stripe.getClass().getDeclaredField("pending");
writingField.setAccessible(true);
pendingField.setAccessible(true);
Queue<?> pending = (Queue<?>) pendingField.get(stripe);
ArrayList<Future<ClaimResult>> results = new ArrayList<>(chunks.size());
synchronized (stripe) {
writingField.setBoolean(stripe, true);
try {
for (SavedBiomeChunk chunk : chunks) {
results.add(workers.submit(() -> claim(store, root, control, chunk)));
}
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
while (pending.size() != chunks.size() && System.nanoTime() < deadline) {
Thread.sleep(1L);
}
assertEquals(chunks.size(), pending.size());
} finally {
writingField.setBoolean(stripe, false);
}
}
return results;
}
private static ClaimResult claim(SavedBiomeStore store, Path root, BatchControl control,
SavedBiomeChunk chunk) throws Exception {
try (RandomAccessFile actual = new RandomAccessFile(
root.resolve("iris/generation/biomes/r.0.0.ibio").toFile(), "rw")) {
FileChannel channel = mock(FileChannel.class, delegatesTo(actual.getChannel()));
doAnswer(invocation -> {
int force = control.forces.incrementAndGet();
if (force == 1) {
control.forcing.countDown();
assertTrue(control.release.await(5, TimeUnit.SECONDS));
if (control.failure != null) {
throw control.failure;
}
if (control.uncheckedFailure != null) {
throw control.uncheckedFailure;
}
}
actual.getChannel().force(true);
return null;
}).when(channel).force(true);
try (MockedConstruction<RandomAccessFile> ignored = mockConstruction(RandomAccessFile.class,
withSettings().defaultAnswer(delegatesTo(actual)),
(file, context) -> doReturn(channel).when(file).getChannel())) {
try {
return new ClaimResult(store.claimAndPersist(chunk), null);
} catch (IOException | RuntimeException failure) {
return new ClaimResult(false, failure);
}
}
}
}
private static SavedBiomeChunk chunk(int x, long activation) {
SavedBiomeChunk.Cell cell = new SavedBiomeChunk.Cell(activation, "biome", "region");
SavedBiomeChunk.Column column = new SavedBiomeChunk.Column(cell, cell,
List.of(new SavedBiomeChunk.Span(-64, 320, cell)));
SavedBiomeChunk.Builder builder = SavedBiomeChunk.builder(new SavedBiomeChunk.Header(x, 0, activation, -64, 384));
for (int z = 0; z < 16; z++) {
for (int localX = 0; localX < 16; localX++) {
builder.column(localX, z, column);
}
}
return builder.build();
}
private static final class BatchControl {
private final CountDownLatch forcing = new CountDownLatch(1);
private final CountDownLatch release = new CountDownLatch(1);
private final AtomicInteger forces = new AtomicInteger();
private IOException failure;
private RuntimeException uncheckedFailure;
}
private record ClaimResult(boolean persisted, Throwable failure) {
}
}
@@ -157,7 +157,13 @@ public class SavedBiomeStoreWriteTest {
assertArrayEquals(original, Files.readAllBytes(regionPath(root)));
assertTrue(store.get(1, 0).isEmpty());
assertTrue(SavedBiomeStore.open(root).get(1, 0).isEmpty());
assertTrue(store.claimAndPersist(chunk(1, 3L)));
if (failRollback) {
assertSame(failure, assertThrows(IOException.class,
() -> store.claimAndPersist(chunk(1, 3L))).getCause());
assertTrue(SavedBiomeStore.open(root).claimAndPersist(chunk(1, 3L)));
} else {
assertTrue(store.claimAndPersist(chunk(1, 3L)));
}
}
}
@@ -0,0 +1,259 @@
package art.arcane.iris.engine.hydrology.runtime;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.compat.CompatStatus;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.hydrology.HydrologyTerrainSample;
import art.arcane.iris.engine.hydrology.policy.RiverPolicyResolver;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisRiverPolicy;
import art.arcane.iris.engine.object.IrisRiverProfile;
import art.arcane.volmlib.util.collection.KList;
import com.github.benmanes.caffeine.cache.Cache;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SuppressWarnings("unchecked")
public class IrisHydrologyPolicyCacheTest {
private static IrisSettings previousSettings;
@BeforeClass
public static void useDefaultSettings() {
previousSettings = IrisSettings.settings;
IrisSettings.settings = new IrisSettings();
}
@AfterClass
public static void restoreSettings() {
IrisSettings.settings = previousSettings;
}
@Test
public void repeatedOwnersReuseFilteredPolicyAndProfilesAcrossCoordinates() throws Exception {
Fixture fixture = new Fixture();
fixture.dimension.setRiverPolicy(new IrisRiverPolicy().setSurfaceBiomes(new KList<>("river"))
.setProfiles(new KList<>("water", "unknown", "water", "lava")));
fixture.dimension.getHydrology().getRivers().setProfiles(new KList<>(
new IrisRiverProfile().setId("water"), new IrisRiverProfile().setId("lava")));
try (IrisHydrologyRuntime runtime = fixture.runtime()) {
for (int index = 0; index < 128; index++) {
HydrologyTerrainSample sample = sample(runtime, index * 17, -index * 13);
assertEquals("river", sample.surfaceBiomeKey());
assertEquals(List.of("lava", "water"), sample.preferredProfileKeys());
}
verify(fixture.loader, times(1)).load("river");
}
}
@Test
public void ownerIdentityAndExplicitEmptyOverridesRemainDistinct() throws Exception {
Fixture fixture = new Fixture();
fixture.dimension.setRiverPolicy(new IrisRiverPolicy().setSurfaceBiomes(new KList<>("river"))
.setWidthMultiplier(2D));
IrisRegion first = new IdentityRegion();
first.setLoadKey("same-region");
first.setRiverPolicy(new IrisRiverPolicy().setWidthMultiplier(3D));
IrisBiome empty = new IdentityBiome();
empty.setLoadKey("same-biome");
empty.setRiverPolicy(new IrisRiverPolicy().setSurfaceBiomes(new KList<>()));
fixture.region.set(first);
fixture.biome.set(empty);
try (IrisHydrologyRuntime runtime = fixture.runtime()) {
assertEquals("same-biome", sample(runtime, 0, 0).surfaceBiomeKey());
assertEquals(3D, sample(runtime, 1, 0).widthMultiplier(), 0D);
IrisRegion second = new IdentityRegion();
second.setLoadKey("same-region");
second.setRiverPolicy(new IrisRiverPolicy().setWidthMultiplier(4D));
fixture.region.set(second);
IrisBiome inherited = new IdentityBiome();
inherited.setLoadKey("same-biome");
fixture.biome.set(inherited);
assertEquals("river", sample(runtime, 2, 0).surfaceBiomeKey());
assertEquals(4D, sample(runtime, 3, 0).widthMultiplier(), 0D);
}
}
@Test
public void selectedLoaderReplacementAndRuntimeReloadCannotReuseOldFiltering() throws Exception {
Fixture fixture = new Fixture();
fixture.dimension.setRiverPolicy(new IrisRiverPolicy().setSurfaceBiomes(new KList<>("river")));
IrisData regionData = mock(IrisData.class);
IrisData biomeData = mock(IrisData.class);
ResourceLoader<IrisBiome> excludedLoader = loader(true);
ResourceLoader<IrisBiome> replacementLoader = loader(false);
when(regionData.getBiomeLoader()).thenReturn(excludedLoader);
when(biomeData.getBiomeLoader()).thenReturn(excludedLoader);
fixture.region.get().setLoader(regionData);
fixture.biome.get().setLoader(biomeData);
try (IrisHydrologyRuntime runtime = fixture.runtime()) {
assertEquals("parent", sample(runtime, 0, 0).surfaceBiomeKey());
when(biomeData.getBiomeLoader()).thenReturn(replacementLoader);
assertEquals("river", sample(runtime, 1, 0).surfaceBiomeKey());
fixture.biome.get().setLoader(null);
assertEquals("parent", sample(runtime, 2, 0).surfaceBiomeKey());
verify(excludedLoader, times(2)).load("river");
fixture.region.get().setLoader(null);
assertEquals("river", sample(runtime, 3, 0).surfaceBiomeKey());
}
fixture.dimension.getRiverPolicy().setWidthMultiplier(7D);
try (IrisHydrologyRuntime replacement = fixture.runtime()) {
assertEquals(7D, sample(replacement, 0, 0).widthMultiplier(), 0D);
}
fixture.dimension.getRiverPolicy().setWidthMultiplier(8D);
assertEquals(8D, RiverPolicyResolver.resolve(fixture.dimension, fixture.region.get(), fixture.biome.get())
.widthMultiplier(), 0D);
}
@Test
public void unresolvedReferencesAreRecheckedInsteadOfFreezingAFirstLoadFailure() throws Exception {
Fixture fixture = new Fixture();
fixture.dimension.setRiverPolicy(new IrisRiverPolicy().setSurfaceBiomes(new KList<>("river")));
when(fixture.loader.load("river")).thenReturn(null);
when(fixture.loader.isLoaded("river")).thenReturn(false);
try (IrisHydrologyRuntime runtime = fixture.runtime()) {
assertEquals("river", sample(runtime, 0, 0).surfaceBiomeKey());
IrisBiome excluded = new IrisBiome();
excluded.setCompat(CompatStatus.excludedBy(List.of()));
when(fixture.loader.load("river")).thenReturn(excluded);
when(fixture.loader.isLoaded("river")).thenReturn(true);
assertEquals("parent", sample(runtime, 1, 0).surfaceBiomeKey());
assertEquals("parent", sample(runtime, 2, 0).surfaceBiomeKey());
verify(fixture.loader, times(2)).load("river");
}
}
@Test
public void aConcurrentLoadAfterTheResolutionMissCannotMakeThatResultCacheable() throws Exception {
Fixture fixture = new Fixture();
fixture.dimension.setRiverPolicy(new IrisRiverPolicy().setSurfaceBiomes(new KList<>("river")));
IrisBiome excluded = new IrisBiome();
excluded.setCompat(CompatStatus.excludedBy(List.of()));
when(fixture.loader.load("river")).thenReturn(null, excluded);
when(fixture.loader.isLoaded("river")).thenReturn(true);
try (IrisHydrologyRuntime runtime = fixture.runtime()) {
assertEquals("river", sample(runtime, 0, 0).surfaceBiomeKey());
assertEquals("parent", sample(runtime, 1, 0).surfaceBiomeKey());
assertEquals("parent", sample(runtime, 2, 0).surfaceBiomeKey());
verify(fixture.loader, times(2)).load("river");
}
}
@Test
public void policyEntriesRemainBoundedAndCloseClearsThem() throws Exception {
Fixture fixture = new Fixture();
IrisHydrologyRuntime runtime = fixture.runtime();
Cache<?, ?> policies = policies(runtime);
try {
for (int index = 0; index < 2048; index++) {
IrisBiome biome = new IdentityBiome();
biome.setLoadKey("parent-" + index);
fixture.biome.set(biome);
assertEquals("parent-" + index, sample(runtime, index, 0).parentBiomeKey());
}
policies.cleanUp();
assertTrue(policies.estimatedSize() <= 1024);
assertTrue(policies.estimatedSize() > 0);
} finally {
runtime.close();
}
assertEquals(0L, policies.estimatedSize());
}
private static HydrologyTerrainSample sample(IrisHydrologyRuntime runtime, int x, int z) throws Exception {
Method method = IrisHydrologyRuntime.class.getDeclaredMethod("createTerrainBasis", int.class, int.class, double.class);
method.setAccessible(true);
try {
return ((IrisHydrologyRoutingTerrainSampler.TerrainBasis) method.invoke(runtime, x, z, 180D)).terrain();
} catch (InvocationTargetException failure) {
if (failure.getCause() instanceof Error error) {
throw error;
}
if (failure.getCause() instanceof Exception exception) {
throw exception;
}
throw failure;
}
}
private static Cache<?, ?> policies(IrisHydrologyRuntime runtime) throws ReflectiveOperationException {
Field field = IrisHydrologyRuntime.class.getDeclaredField("resolvedPolicies");
field.setAccessible(true);
return (Cache<?, ?>) field.get(runtime);
}
private static ResourceLoader<IrisBiome> loader(boolean excluded) {
ResourceLoader<IrisBiome> loader = mock(ResourceLoader.class);
IrisBiome river = new IrisBiome();
river.setLoadKey("river");
river.setCompat(excluded ? CompatStatus.excludedBy(List.of()) : CompatStatus.OK);
when(loader.load("river")).thenReturn(river);
when(loader.isLoaded("river")).thenReturn(true);
when(loader.getPossibleKeys()).thenReturn(new String[0]);
when(loader.loadAll(any(String[].class))).thenReturn(new KList<>());
return loader;
}
private static final class Fixture {
private final IrisDimension dimension = new IrisDimension().setRegions(new KList<>());
private final IrisData data = mock(IrisData.class);
private final ResourceLoader<IrisBiome> loader = loader(false);
private final AtomicReference<IrisRegion> region = new AtomicReference<>(new IrisRegion());
private final AtomicReference<IrisBiome> biome = new AtomicReference<>(new IrisBiome());
private Fixture() {
dimension.setLoadKey("dimension");
dimension.setLoader(data);
region.get().setLoadKey("region");
biome.get().setLoadKey("parent");
when(data.getBiomeLoader()).thenReturn(loader);
}
private IrisHydrologyRuntime runtime() {
return new IrisHydrologyRuntime(new IrisHydrologyRuntimeContext(17L, 768, dimension, data,
(x, z, height) -> new IrisHydrologyNaturalSample(height, false, biome.get(), region.get()),
(x, z) -> 180D, (x, z) -> "constant", (x, z) -> false, footprint -> null, () -> false));
}
}
private static final class IdentityRegion extends IrisRegion {
@Override
public boolean equals(Object compared) {
throw new AssertionError("Policy cache must use region identity");
}
@Override
public int hashCode() {
throw new AssertionError("Policy cache must use region identity");
}
}
private static final class IdentityBiome extends IrisBiome {
@Override
public boolean equals(Object compared) {
throw new AssertionError("Policy cache must use biome identity");
}
@Override
public int hashCode() {
throw new AssertionError("Policy cache must use biome identity");
}
}
}
@@ -0,0 +1,227 @@
package art.arcane.iris.engine.mantle;
import art.arcane.iris.engine.IrisEngineMantle;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
import org.junit.Test;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
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.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class EngineMantleCoverageTest {
@Test
public void zeroRadiusCleansOnlyItsRealChunkOnce() {
CoverageFixture fixture = fixture(0);
List<Chunk> actual = new ArrayList<>();
fixture.engine().cleanupChunksCoveredBy(0, 0, true,
(x, z) -> actual.add(new Chunk(x, z)));
assertTrue(actual.isEmpty());
fixture.real().add(new Chunk(0, 0));
fixture.engine().cleanupChunksCoveredBy(0, 0, true,
(x, z) -> actual.add(new Chunk(x, z)));
fixture.engine().cleanupChunksCoveredBy(0, 0, true,
(x, z) -> actual.add(new Chunk(x, z)));
assertEquals(List.of(new Chunk(0, 0)), actual);
}
@Test
public void realRadiusDefinesTheLargerCoverageHalo() {
CoverageFixture fixture = fixture(0);
doReturn(2).when(fixture.engine()).getRealRadius();
fill(fixture, 2);
List<Chunk> actual = new ArrayList<>();
fixture.engine().cleanupChunksCoveredBy(0, 0, false,
(x, z) -> actual.add(new Chunk(x, z)));
assertEquals(List.of(new Chunk(0, 0)), actual);
}
@Test
public void cleanupMatchesFullScanAcrossCoverageShapes() {
for (int radius : new int[]{0, 1, 2, 4}) {
for (int shape = 0; shape < 4; shape++) {
CoverageFixture fixture = fixture(radius);
for (int x = -2 * radius; x <= 2 * radius; x++) {
for (int z = -2 * radius; z <= 2 * radius; z++) {
if (shape == 0 || shape == 1 && x <= 0 || shape == 2 && z <= x) {
fixture.real().add(new Chunk(x - 7, z - 4));
}
}
}
fixture.cleaned().add(new Chunk(-7, -4));
List<Chunk> expected = fullScan(fixture, -7, -4, radius);
List<Chunk> actual = new ArrayList<>();
fixture.engine().cleanupChunksCoveredBy(-7, -4, false,
(x, z) -> actual.add(new Chunk(x, z)));
assertEquals("radius=" + radius + ", shape=" + shape, expected, actual);
}
}
}
@Test
public void cleanupRechecksMissingNeighborThatFinishesGeneration() {
CoverageFixture fixture = fixture(1);
fill(fixture, 2);
Chunk neighbor = new Chunk(-2, 0);
fixture.real().remove(neighbor);
AtomicInteger reads = new AtomicInteger();
when(fixture.mantle().hasFlag(-2, 0, MantleFlag.REAL)).thenAnswer(invocation -> {
if (reads.incrementAndGet() > 1) {
fixture.real().add(neighbor);
return true;
}
return false;
});
List<Chunk> actual = new ArrayList<>();
fixture.engine().cleanupChunksCoveredBy(0, 0, false,
(x, z) -> actual.add(new Chunk(x, z)));
assertEquals(List.of(new Chunk(-1, 0), new Chunk(-1, 1),
new Chunk(0, -1), new Chunk(0, 0), new Chunk(0, 1),
new Chunk(1, -1), new Chunk(1, 0), new Chunk(1, 1)), actual);
}
@Test
public void cleanupCallbackCanInvalidateLaterCoverage() {
CoverageFixture fixture = fixture(1);
fill(fixture, 2);
List<Chunk> actual = new ArrayList<>();
fixture.engine().cleanupChunksCoveredBy(0, 0, true, (x, z) -> {
actual.add(new Chunk(x, z));
fixture.real().remove(new Chunk(0, 0));
});
assertEquals(List.of(new Chunk(-1, -1)), actual);
}
@Test
public void neighboringFrontierTargetsReuseARecheckedMissingNeighbor() {
CoverageFixture fixture = fixture(4);
for (int x = -8; x <= 8; x++) {
for (int z = -8; z <= 0; z++) {
fixture.real().add(new Chunk(x, z));
}
}
List<Chunk> expected = fullScan(fixture, 0, 0, 4);
List<Chunk> actual = new ArrayList<>();
int baselineReads = fullScanReads(fixture, 4);
fixture.engine().cleanupChunksCoveredBy(0, 0, false,
(x, z) -> actual.add(new Chunk(x, z)));
assertEquals(expected, actual);
assertTrue("baseline=" + baselineReads + ", actual=" + fixture.reads().get(),
fixture.reads().get() < baselineReads * 4 / 5);
}
private static void fill(CoverageFixture fixture, int radius) {
for (int x = -radius; x <= radius; x++) {
for (int z = -radius; z <= radius; z++) {
fixture.real().add(new Chunk(x, z));
}
}
}
private static List<Chunk> fullScan(CoverageFixture fixture, int x, int z, int radius) {
List<Chunk> result = new ArrayList<>();
for (int offsetX = -radius; offsetX <= radius; offsetX++) {
for (int offsetZ = -radius; offsetZ <= radius; offsetZ++) {
Chunk target = new Chunk(x + offsetX, z + offsetZ);
if (fixture.cleaned().contains(target)) {
continue;
}
boolean covered = true;
for (int dx = -radius; dx <= radius && covered; dx++) {
for (int dz = -radius; dz <= radius; dz++) {
if (!fixture.real().contains(new Chunk(target.x() + dx, target.z() + dz))) {
covered = false;
break;
}
}
}
if (covered) {
result.add(target);
}
}
}
return result;
}
private static int fullScanReads(CoverageFixture fixture, int radius) {
int reads = 0;
for (int x = -radius; x <= radius; x++) {
for (int z = -radius; z <= radius; z++) {
boolean covered = true;
for (int dx = -radius; dx <= radius && covered; dx++) {
for (int dz = -radius; dz <= radius; dz++) {
reads++;
if (!fixture.real().contains(new Chunk(x + dx, z + dz))) {
covered = false;
break;
}
}
}
}
}
return reads;
}
@SuppressWarnings("unchecked")
private static CoverageFixture fixture(int radius) {
EngineMantle engine = mock(IrisEngineMantle.class, CALLS_REAL_METHODS);
Mantle<Matter> mantle = mock(Mantle.class);
MantleChunk<Matter> chunk = mock(MantleChunk.class);
Set<Chunk> real = new HashSet<>();
Set<Chunk> cleaned = new HashSet<>();
AtomicInteger reads = new AtomicInteger();
doReturn(mantle).when(engine).getMantle();
doReturn(radius).when(engine).getRadius();
doReturn(0).when(engine).getRealRadius();
when(mantle.hasFlag(anyInt(), anyInt(), any())).thenAnswer(invocation -> {
Chunk position = new Chunk(invocation.getArgument(0), invocation.getArgument(1));
MantleFlag flag = invocation.getArgument(2);
if (flag == MantleFlag.REAL) {
reads.incrementAndGet();
return real.contains(position);
}
return cleaned.contains(position);
});
when(mantle.getChunk(anyInt(), anyInt())).thenAnswer(invocation -> {
cleaned.add(new Chunk(invocation.getArgument(0), invocation.getArgument(1)));
return chunk;
});
when(chunk.use()).thenReturn(chunk);
doAnswer(invocation -> null).when(chunk).raiseFlagUnchecked(eq(MantleFlag.CLEANED), any());
return new CoverageFixture(engine, mantle, real, cleaned, reads);
}
private record Chunk(int x, int z) {
}
private record CoverageFixture(EngineMantle engine, Mantle<Matter> mantle,
Set<Chunk> real, Set<Chunk> cleaned, AtomicInteger reads) {
}
}
@@ -0,0 +1,151 @@
package art.arcane.iris.engine.mantle;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.util.project.matter.PreObjectMatterCell;
import art.arcane.iris.util.project.matter.slices.HydrologyCaveMatter;
import art.arcane.iris.util.project.matter.slices.PreObjectMatter;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.IrisMatter;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.matter.MatterSlice;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
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.verifyNoInteractions;
public class TerrainComposedCavernTest {
@BeforeClass
public static void registerMatter() {
IrisMatter.registerSliceType(new PreObjectMatter());
IrisMatter.registerSliceType(new HydrologyCaveMatter());
}
@Test
public void allHydrologyActionsAndPartialJournalCapturesMatchTheExistingComposition() {
MatterCavern natural = new MatterCavern(true, "iris:natural", (byte) 0);
MatterCavern tagged = new MatterCavern(false, "iris:tagged", (byte) 2);
List<MatterCavern> caverns = Arrays.asList(null, natural, tagged);
List<HydrologyCaveCell> hydrology = new ArrayList<>();
hydrology.add(null);
for (HydrologyCaveAction action : HydrologyCaveAction.values()) {
hydrology.add(new HydrologyCaveCell(action, "river", "iris:flooded"));
}
List<PreObjectMatterCell> journals = new ArrayList<>();
journals.add(null);
journals.add(PreObjectMatterCell.string(null));
journals.add(PreObjectMatterCell.cavern(null));
journals.add(PreObjectMatterCell.cavern(natural));
for (HydrologyCaveCell cell : hydrology) {
journals.add(PreObjectMatterCell.hydrology(cell));
journals.add(PreObjectMatterCell.cavern(null).captureHydrology(cell));
journals.add(PreObjectMatterCell.cavern(natural).captureHydrology(cell));
}
Matter matter = new IrisMatter(16, 16, 16);
MantleChunk<Matter> chunk = chunk(matter);
for (MatterCavern rawCavern : caverns) {
for (HydrologyCaveCell rawHydrology : hydrology) {
for (PreObjectMatterCell journal : journals) {
matter.<MatterCavern>slice(MatterCavern.class).set(15, 3, 2, rawCavern);
matter.<HydrologyCaveCell>slice(HydrologyCaveCell.class).set(15, 3, 2, rawHydrology);
matter.<PreObjectMatterCell>slice(PreObjectMatterCell.class).set(15, 3, 2, journal);
MatterCavern effectiveCavern = journal != null && journal.cavernCaptured()
? journal.cavern() : rawCavern;
HydrologyCaveCell effectiveHydrology = journal != null && journal.hydrologyCaptured()
? journal.hydrology() : rawHydrology;
MatterCavern expected = effectiveHydrology == null
? effectiveCavern : effectiveHydrology.asCavern();
assertSame(expected, legacy(chunk, -1, 35, 18));
assertSame(expected, TerrainMatterView.getComposedCavern(chunk, -1, 35, 18));
}
}
}
}
@Test
public void absentChunksSectionsAndVerticalBoundsRemainEmpty() {
MantleChunk<Matter> chunk = chunk(null);
assertNull(TerrainMatterView.getComposedCavern(null, 1, 2, 3));
assertNull(TerrainMatterView.getComposedCavern(chunk, 1, -1, 3));
assertNull(TerrainMatterView.getComposedCavern(chunk, 1, Integer.MIN_VALUE, 3));
verifyNoInteractions(chunk);
assertNull(TerrainMatterView.getComposedCavern(chunk, 1, 0, 3));
assertNull(TerrainMatterView.getComposedCavern(chunk, 1, Integer.MAX_VALUE, 3));
assertNull(TerrainMatterView.getComposedCavern(chunk, 1, 35, 3));
assertNull(legacy(chunk, 1, 35, 3));
}
@Test
public void composedReadUsesOneLockedSectionAndJournalLookup() {
Matter matter = spy(new IrisMatter(16, 16, 16));
MatterCavern natural = new MatterCavern(true, "iris:natural", (byte) 0);
HydrologyCaveCell seal = HydrologyCaveCell.of(HydrologyCaveAction.SEAL_GUARD);
matter.<MatterCavern>slice(MatterCavern.class).set(15, 3, 2, natural);
matter.<HydrologyCaveCell>slice(HydrologyCaveCell.class).set(15, 3, 2, seal);
matter.<PreObjectMatterCell>slice(PreObjectMatterCell.class).set(15, 3, 2,
PreObjectMatterCell.string("iris:owner"));
MatterSlice<PreObjectMatterCell> journal = spy(matter.<PreObjectMatterCell>getSlice(PreObjectMatterCell.class));
MatterSlice<HydrologyCaveCell> hydrology = spy(matter.<HydrologyCaveCell>getSlice(HydrologyCaveCell.class));
matter.getSliceMap().put(PreObjectMatterCell.class, journal);
matter.getSliceMap().put(HydrologyCaveCell.class, hydrology);
MantleChunk<Matter> chunk = chunk(matter);
clearInvocations(matter);
assertNull(TerrainMatterView.getComposedCavern(chunk, -1, 35, 18));
verify(chunk, times(1)).exists(2);
verify(chunk, times(1)).get(2);
verify(journal, times(1)).get(15, 3, 2);
verify(hydrology, times(1)).get(15, 3, 2);
verify(matter, never()).hasSlice(MatterCavern.class);
verify(matter, never()).getSlice(MatterCavern.class);
}
@Test
public void lookupFailuresRetainTheirOriginalCause() {
MantleChunk<Matter> chunk = chunk(null);
IllegalStateException failure = new IllegalStateException("section unavailable");
doThrow(failure).when(chunk).get(2);
assertSame(failure, assertThrows(IllegalStateException.class,
() -> TerrainMatterView.getComposedCavern(chunk, 1, 35, 3)));
assertSame(failure, assertThrows(IllegalStateException.class,
() -> legacy(chunk, 1, 35, 3)));
}
private static MatterCavern legacy(MantleChunk<Matter> chunk, int x, int y, int z) {
MatterCavern cavern = TerrainMatterView.get(chunk, x, y, z, MatterCavern.class);
HydrologyCaveCell hydrology = TerrainMatterView.get(chunk, x, y, z, HydrologyCaveCell.class);
return hydrology == null ? cavern : hydrology.asCavern();
}
@SuppressWarnings("unchecked")
private static MantleChunk<Matter> chunk(Matter matter) {
MantleChunk<Matter> chunk = mock(MantleChunk.class);
doAnswer(invocation -> {
assertTrue(Thread.holdsLock(chunk));
return true;
}).when(chunk).exists(2);
doAnswer(invocation -> {
assertTrue(Thread.holdsLock(chunk));
return matter;
}).when(chunk).get(2);
return chunk;
}
}
@@ -1,5 +1,8 @@
package art.arcane.iris.engine.mantle;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.matter.IrisMatterSupport;
import art.arcane.iris.util.project.matter.PreObjectMatterCell;
import art.arcane.iris.util.project.matter.slices.PreObjectMatterTest;
@@ -14,14 +17,24 @@ import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
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.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
public class TerrainMatterViewTest {
@@ -59,12 +72,115 @@ public class TerrainMatterViewTest {
assertEquals(natural, TerrainMatterView.get(chunk, 3, 3, 2, MatterCavern.class));
}
@Test
public void journaledTypesReuseIteratedValuesAndKeepOriginalIdentityAndOrder() {
assertJournalIteration(MatterCavern.class,
new MatterCavern(true, "iris:natural", (byte) 0),
new MatterCavern(true, "iris:object", (byte) 0), PreObjectMatterCell::cavern);
assertJournalIteration(String.class, "iris:natural", "iris:object", PreObjectMatterCell::string);
assertJournalIteration(HydrologyCaveCell.class,
HydrologyCaveCell.of(HydrologyCaveAction.DRY_AIR),
HydrologyCaveCell.of(HydrologyCaveAction.WET_SOURCE), PreObjectMatterCell::hydrology);
assertJournalIteration(PlatformBlockState.class,
mock(PlatformBlockState.class), mock(PlatformBlockState.class), PreObjectMatterCell::block);
}
@Test
public void callbackFailuresEscapeOutsideTheChunkLock() {
Matter matter = new IrisMatter(16, 16, 16);
MatterCavern original = new MatterCavern(true, "iris:natural", (byte) 0);
matter.<MatterCavern>slice(MatterCavern.class).set(1, 3, 2, original);
MantleChunk<Matter> chunk = chunk(matter);
IllegalStateException failure = new IllegalStateException("terrain callback");
assertSame(failure, assertThrows(IllegalStateException.class,
() -> TerrainMatterView.iterate(chunk, MatterCavern.class, (x, y, z, value) -> {
assertFalse(Thread.holdsLock(chunk));
assertSame(original, value);
throw failure;
})));
List<MatterCavern> recovered = new ArrayList<>();
TerrainMatterView.iterate(chunk, MatterCavern.class, (x, y, z, value) -> recovered.add(value));
assertEquals(List.of(original), recovered);
}
@Test
public void firstPassDoesNotReadTheSuppliedRawValuesAgain() {
Matter matter = new IrisMatter(16, 16, 16);
MatterCavern original = new MatterCavern(true, "iris:natural", (byte) 0);
matter.<MatterCavern>slice(MatterCavern.class).set(1, 3, 2, original);
matter.<MatterCavern>slice(MatterCavern.class).set(4, 3, 2, original);
MantleChunk<Matter> chunk = chunk(matter);
List<MatterCavern> values = new ArrayList<>();
TerrainMatterView.iterate(chunk, MatterCavern.class, (x, y, z, value) -> values.add(value));
assertEquals(List.of(original, original), values);
verify(chunk, times(2)).get(0);
verify(chunk, times(2)).exists(0);
}
@Test
@SuppressWarnings("unchecked")
public void unjournaledTypesKeepTheDirectIteratorPath() {
MantleChunk<Matter> chunk = mock(MantleChunk.class);
Consumer4<Integer, Integer, Integer, Integer> consumer = (x, y, z, value) -> {
assertFalse(Thread.holdsLock(chunk));
assertEquals(7, value.intValue());
};
doAnswer(invocation -> {
Consumer4<Integer, Integer, Integer, Integer> supplied = invocation.getArgument(1);
assertSame(consumer, supplied);
supplied.accept(1, 3, 2, 7);
return null;
}).when(chunk).iterate(Integer.class, consumer);
TerrainMatterView.iterate(chunk, Integer.class, consumer);
verify(chunk).iterate(Integer.class, consumer);
verifyNoMoreInteractions(chunk);
}
private static <T> void assertJournalIteration(Class<T> type, T original, T content,
Function<T, PreObjectMatterCell> capture) {
Matter matter = new IrisMatter(16, 16, 16);
matter.<T>slice(type).set(1, 3, 2, content);
matter.<T>slice(type).set(2, 3, 2, content);
matter.<T>slice(type).set(4, 3, 2, original);
matter.<T>slice(type).set(5, 3, 2, content);
matter.<PreObjectMatterCell>slice(PreObjectMatterCell.class).set(1, 3, 2, capture.apply(null));
matter.<PreObjectMatterCell>slice(PreObjectMatterCell.class).set(2, 3, 2, capture.apply(original));
matter.<PreObjectMatterCell>slice(PreObjectMatterCell.class).set(3, 3, 2, capture.apply(original));
matter.<PreObjectMatterCell>slice(PreObjectMatterCell.class).set(5, 3, 2,
type == String.class ? PreObjectMatterCell.cavern(null) : PreObjectMatterCell.string(null));
matter.<PreObjectMatterCell>slice(PreObjectMatterCell.class).set(6, 3, 2, capture.apply(null));
MantleChunk<Matter> chunk = chunk(matter);
List<Integer> positions = new ArrayList<>();
List<T> values = new ArrayList<>();
TerrainMatterView.iterate(chunk, type, (x, y, z, value) -> {
assertFalse(Thread.holdsLock(chunk));
assertEquals(3, y.intValue());
assertEquals(2, z.intValue());
positions.add(x);
values.add(value);
matter.<T>slice(type).set(4, 3, 2, content);
});
assertEquals(List.of(2, 4, 5, 3), positions);
assertSame(original, values.get(0));
assertSame(original, values.get(1));
assertSame(content, values.get(2));
assertSame(original, values.get(3));
}
@SuppressWarnings("unchecked")
private static MantleChunk<Matter> chunk(Matter matter) {
MantleChunk<Matter> chunk = mock(MantleChunk.class);
when(chunk.exists(0)).thenReturn(true);
when(chunk.get(0)).thenReturn(matter);
doAnswer(invocation -> {
assertTrue(Thread.holdsLock(chunk));
Class<Object> type = invocation.getArgument(0);
Consumer4<Integer, Integer, Integer, Object> consumer = invocation.getArgument(1);
if (!matter.hasSlice(type)) {
@@ -0,0 +1,203 @@
package art.arcane.iris.engine.modifier;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
import art.arcane.volmlib.util.math.BlockPosition;
import art.arcane.volmlib.util.math.RNG;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import org.junit.Test;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
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.anyInt;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
public class IrisCarveModifierBiomeBlendTest {
private static final long SEED = 371924L;
@Test
public void everyRollPreservesAllNeighborIdentityAndNullCombinations() throws Exception {
IrisBiome center = new IrisBiome();
IrisBiome first = new IrisBiome();
IrisBiome second = new IrisBiome();
IrisBiome[] choices = {null, center, first, second};
AtomicReference<IrisBiome[]> samples = new AtomicReference<>();
int[] position = new int[3];
Engine engine = mock(Engine.class);
doAnswer(call -> {
int x = call.getArgument(0);
int z = call.getArgument(2);
return samples.get()[sampleIndex(x - position[0], z - position[2])];
}).when(engine).getCaveBiome(anyInt(), anyInt(), anyInt(), any(IrisDimensionCarvingResolver.State.class));
RNG parent = new RNG(SEED);
RNG control = new RNG(SEED);
assertEquals(control.nextInt(), parent.nextInt());
IrisCarveModifier modifier = modifier(engine, parent);
for (int sign : new int[]{1, -1}) {
for (int roll = 0; roll < 8; roll++) {
int[] coordinate = coordinate(roll, sign);
System.arraycopy(coordinate, 0, position, 0, position.length);
for (int pattern = 0; pattern < 256; pattern++) {
IrisBiome[] values = {center, choices[pattern & 3], choices[(pattern >> 2) & 3],
choices[(pattern >> 4) & 3], choices[(pattern >> 6) & 3]};
samples.set(values);
IrisBiome selected = roll < 4 ? center : values[roll - 3];
assertSame(selected == null ? center : selected,
resolve(modifier, coordinate, new Long2ObjectOpenHashMap<>()));
}
samples.set(new IrisBiome[]{null, first, second, first, second});
assertNull(resolve(modifier, coordinate, new Long2ObjectOpenHashMap<>()));
}
}
assertEquals(control.nextLong(), parent.nextLong());
}
@Test
public void onlyCenterAndChosenNeighborAreQueriedInThatOrder() throws Exception {
List<Long> queries = new ArrayList<>();
IrisBiome center = new IrisBiome();
Engine engine = mock(Engine.class);
doAnswer(call -> {
queries.add(BlockPosition.toLong(call.getArgument(0), call.getArgument(1), call.getArgument(2)));
return center;
}).when(engine).getCaveBiome(anyInt(), anyInt(), anyInt(), any(IrisDimensionCarvingResolver.State.class));
IrisCarveModifier modifier = modifier(engine, new RNG(SEED));
for (int roll = 0; roll < 8; roll++) {
int[] coordinate = coordinate(roll, -1);
queries.clear();
assertSame(center, resolve(modifier, coordinate, new Long2ObjectOpenHashMap<>()));
List<Long> expected = new ArrayList<>();
expected.add(BlockPosition.toLong(coordinate[0], coordinate[1], coordinate[2]));
if (roll >= 4) {
int[] neighbor = neighbor(coordinate, roll - 4);
expected.add(BlockPosition.toLong(neighbor[0], neighbor[1], neighbor[2]));
}
assertEquals(expected, queries);
}
}
@Test
public void sharedCacheTraversalMatchesEagerSampling() throws Exception {
IrisBiome[] biomes = {new IrisBiome(), new IrisBiome(), null, new IrisBiome()};
Engine engine = mock(Engine.class);
doAnswer(call -> biomeAt(biomes, call.getArgument(0), call.getArgument(1), call.getArgument(2)))
.when(engine).getCaveBiome(anyInt(), anyInt(), anyInt(), any(IrisDimensionCarvingResolver.State.class));
IrisCarveModifier modifier = modifier(engine, new RNG(SEED));
Long2ObjectOpenHashMap<IrisBiome> cache = new Long2ObjectOpenHashMap<>();
Long2ObjectOpenHashMap<IrisBiome> eagerCache = new Long2ObjectOpenHashMap<>();
for (int x = -16; x < 0; x++) {
for (int z = -32; z < -16; z++) {
for (int y : new int[]{-12, 3, 17, 40}) {
int[] coordinate = {x, y, z};
assertSame(eager(biomes, coordinate, eagerCache), resolve(modifier, coordinate, cache));
}
}
}
}
@Test
public void selectedNeighborFailureKeepsOriginalCause() throws Exception {
int[] coordinate = coordinate(4, -1);
IllegalStateException failure = new IllegalStateException("selected cave query failed");
Engine engine = mock(Engine.class);
doAnswer(call -> {
if ((int) call.getArgument(0) != coordinate[0]) {
throw failure;
}
return new IrisBiome();
}).when(engine).getCaveBiome(anyInt(), anyInt(), anyInt(), any(IrisDimensionCarvingResolver.State.class));
IrisCarveModifier modifier = modifier(engine, new RNG(SEED));
assertSame(failure, assertThrows(IllegalStateException.class,
() -> resolve(modifier, coordinate, new Long2ObjectOpenHashMap<>())));
}
private static IrisCarveModifier modifier(Engine engine, RNG rng) throws ReflectiveOperationException {
IrisCarveModifier modifier = mock(IrisCarveModifier.class, CALLS_REAL_METHODS);
doReturn(engine).when(modifier).getEngine();
Field field = IrisCarveModifier.class.getDeclaredField("rng");
field.setAccessible(true);
field.set(modifier, rng);
return modifier;
}
private static IrisBiome resolve(IrisCarveModifier modifier, int[] coordinate,
Long2ObjectOpenHashMap<IrisBiome> cache) {
return modifier.resolveCaveBoundaryBiome(null, coordinate[0], coordinate[1], coordinate[2],
new IrisDimensionCarvingResolver.State(), cache, new HashMap<>());
}
private static int[] coordinate(int roll, int sign) {
for (int y = 1; y < 10_000; y++) {
int[] coordinate = {sign * 31, sign * y, sign * 23};
if (roll(coordinate) == roll) {
return coordinate;
}
}
throw new AssertionError("Missing cave blend roll " + roll);
}
private static int roll(int[] coordinate) {
return Math.floorMod(new RNG(SEED).nextParallelRNG(
BlockPosition.toLong(coordinate[0], coordinate[1], coordinate[2])).nextInt(), 8);
}
private static int[] neighbor(int[] coordinate, int index) {
return new int[]{coordinate[0] + (index == 0 ? 3 : index == 1 ? -3 : 0), coordinate[1],
coordinate[2] + (index == 2 ? 3 : index == 3 ? -3 : 0)};
}
private static int sampleIndex(int x, int z) {
if (x == 3) {
return 1;
}
if (x == -3) {
return 2;
}
return z == 3 ? 3 : z == -3 ? 4 : 0;
}
private static IrisBiome biomeAt(IrisBiome[] biomes, int x, int y, int z) {
return biomes[Math.floorMod(x * 13 + y * 7 + z * 5, biomes.length)];
}
private static IrisBiome eager(IrisBiome[] biomes, int[] coordinate, Long2ObjectOpenHashMap<IrisBiome> cache) {
IrisBiome center = cached(biomes, coordinate, cache);
if (center == null) {
return null;
}
IrisBiome[] neighbors = new IrisBiome[4];
boolean identical = true;
for (int index = 0; index < neighbors.length; index++) {
neighbors[index] = cached(biomes, neighbor(coordinate, index), cache);
identical &= neighbors[index] == center;
}
int roll = roll(coordinate);
IrisBiome selected = identical || roll < 4 ? center : neighbors[roll - 4];
return selected == null ? center : selected;
}
private static IrisBiome cached(IrisBiome[] biomes, int[] coordinate, Long2ObjectOpenHashMap<IrisBiome> cache) {
long key = BlockPosition.toLong(coordinate[0], coordinate[1], coordinate[2]);
IrisBiome value = cache.get(key);
if (value == null) {
value = biomeAt(biomes, coordinate[0], coordinate[1], coordinate[2]);
if (value != null) {
cache.put(key, value);
}
}
return value;
}
}
@@ -13,9 +13,11 @@ import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.matter.MatterSlice;
import art.arcane.volmlib.util.math.RNG;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import org.junit.Test;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
@@ -32,7 +34,7 @@ import static org.mockito.Mockito.mock;
public class IrisCarveModifierBoundarySupportTest {
@Test
@SuppressWarnings("unchecked")
public void boundaryBiomeUsesCustomMatterAtItsOwnY() {
public void boundaryBiomeUsesCustomMatterAtItsOwnY() throws ReflectiveOperationException {
IrisBiome customFloor = mock(IrisBiome.class);
IrisBiome resolvedCeiling = mock(IrisBiome.class);
IrisData data = mock(IrisData.class);
@@ -46,6 +48,9 @@ public class IrisCarveModifierBoundarySupportTest {
IrisCarveModifier modifier = mock(IrisCarveModifier.class, CALLS_REAL_METHODS);
doReturn(engine).when(modifier).getEngine();
Field rng = IrisCarveModifier.class.getDeclaredField("rng");
rng.setAccessible(true);
rng.set(modifier, new RNG(71L));
MantleChunk<Matter> mantleChunk = mock(MantleChunk.class);
Matter floorMatter = mock(Matter.class);
@@ -35,11 +35,13 @@ import static org.junit.Assert.assertNotSame;
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.any;
import static org.mockito.ArgumentMatchers.anyString;
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.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
@@ -68,9 +70,9 @@ public class IrisDepositModifierContextTest {
IrisEngine engine = mock(IrisEngine.class, RETURNS_DEEP_STUBS);
try (Fixture fixture = new Fixture(engine, true)) {
fixture.generateAndCheckCleanup();
verify(engine, times(3)).captureGenerationRuntimeBinding();
verify(engine, times(3)).openGenerationRuntimeScope(fixture.binding);
assertEquals(3, fixture.closedScopes.get());
verify(engine, times(1)).captureGenerationRuntimeBinding();
verify(engine, times(4)).openGenerationRuntimeScope(fixture.binding);
assertEquals(4, fixture.closedScopes.get());
}
}
@@ -91,6 +93,57 @@ public class IrisDepositModifierContextTest {
}
}
@Test
public void workerPreparationFailurePropagatesAfterAllScopesClose() throws Exception {
try (Fixture fixture = new Fixture(mock(IrisEngine.class, RETURNS_DEEP_STUBS), true)) {
IllegalStateException failure = new IllegalStateException("worker clump preparation failed");
doThrow(failure).when(fixture.dimensionDeposit).getClump(any(), any(), any());
assertSame(failure, assertThrows(IllegalStateException.class, () ->
fixture.modifier.modify(32, -48, Hunk.newArrayHunk(16, 16, 16), true, fixture.context)));
verify(fixture.chunk).release();
assertEquals(4, fixture.closedScopes.get());
assertNull(IrisContext.get());
}
}
@Test
public void callerPlanFailureStillDrainsWorkersAndRestoresContext() throws Exception {
try (Fixture fixture = new Fixture(mock(IrisEngine.class, RETURNS_DEEP_STUBS), true)) {
IllegalStateException failure = new IllegalStateException("caller deposit plan failed");
doThrow(failure).when(fixture.biomeDeposit).getSpawnChance();
ChunkContext callerContext = mock(ChunkContext.class);
try (IrisContext.Scope ignored = IrisContext.open(fixture.engine, 19L, callerContext)) {
assertSame(failure, assertThrows(IllegalStateException.class, () ->
fixture.modifier.modify(32, -48, Hunk.newArrayHunk(16, 16, 16), true, fixture.context)));
assertSame(callerContext, IrisContext.require().getChunkContext());
assertEquals(19L, IrisContext.require().getGenerationSessionId());
}
verify(fixture.chunk).release();
assertEquals(3, fixture.closedScopes.get());
assertEquals(10, fixture.completed.get());
assertNull(IrisContext.get());
}
}
@Test
public void callerFailureRetainsDistinctWorkerFailuresAfterDraining() throws Exception {
try (Fixture fixture = new Fixture(mock(IrisEngine.class, RETURNS_DEEP_STUBS), true)) {
IllegalStateException caller = new IllegalStateException("caller failure");
IllegalArgumentException firstWorker = new IllegalArgumentException("first worker failure");
UnsupportedOperationException secondWorker = new UnsupportedOperationException("second worker failure");
doThrow(firstWorker, secondWorker).when(fixture.dimensionDeposit).getClump(any(), any(), any());
doThrow(caller).when(fixture.biomeDeposit).getSpawnChance();
assertSame(caller, assertThrows(IllegalStateException.class, () ->
fixture.modifier.modify(32, -48, Hunk.newArrayHunk(16, 16, 16), true, fixture.context)));
assertEquals(2, caller.getSuppressed().length);
assertSame(firstWorker, caller.getSuppressed()[0]);
assertSame(secondWorker, caller.getSuppressed()[1]);
verify(fixture.chunk).release();
assertEquals(3, fixture.closedScopes.get());
assertNull(IrisContext.get());
}
}
@Test
public void failedClumpChanceDoesNotResampleTheConfiguredAttemptCount() {
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
@@ -119,6 +172,8 @@ public class IrisDepositModifierContextTest {
private final AtomicInteger completed = new AtomicInteger();
private final AtomicInteger closedScopes = new AtomicInteger();
private final IrisDepositModifier modifier;
private final IrisDepositGenerator dimensionDeposit = mock(IrisDepositGenerator.class);
private final IrisDepositGenerator biomeDeposit = mock(IrisDepositGenerator.class);
private final MantleChunk<Matter> chunk;
@SuppressWarnings("unchecked")
@@ -131,9 +186,7 @@ public class IrisDepositModifierContextTest {
burst.setMulticore(true);
when(engine.burst()).thenReturn(pool);
when(pool.burst(true)).thenReturn(burst);
IrisDepositGenerator dimensionDeposit = mock(IrisDepositGenerator.class);
IrisDepositGenerator regionDeposit = mock(IrisDepositGenerator.class);
IrisDepositGenerator biomeDeposit = mock(IrisDepositGenerator.class);
when(engine.getDimension().getDeposits()).thenReturn(new KList<>(dimensionDeposit));
when(context.getRegion().get(7, 7).getDeposits())
.thenReturn(new KList<>(regionDeposit));
@@ -178,12 +231,9 @@ public class IrisDepositModifierContextTest {
return clump;
}).when(generator).getClump(any(), any(), any());
}
when(dimensionDeposit.getMinPerChunk()).thenReturn(9);
when(dimensionDeposit.getMaxPerChunk()).thenReturn(9);
modifier = new IrisDepositModifier(engine);
doAnswer(invocation -> {
assertEquals(3, completed.get());
assertEquals(scoped ? 3 : 0, closedScopes.get());
return null;
}).when(chunk).release();
}
private void generateAndCheckCleanup() throws Exception {
@@ -194,7 +244,7 @@ public class IrisDepositModifierContextTest {
assertSame(callerContext, IrisContext.require().getChunkContext());
assertEquals(19L, IrisContext.require().getGenerationSessionId());
}
assertEquals(3, completed.get());
assertEquals(11, completed.get());
verify(chunk).release();
executor.submit(() -> {
assertNull(IrisContext.get());
@@ -0,0 +1,223 @@
package art.arcane.iris.engine.modifier;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDepositGenerator;
import art.arcane.iris.engine.object.IrisDepositHeightDistribution;
import art.arcane.iris.engine.object.IrisDepositShape;
import art.arcane.iris.engine.object.IrisDepositVariant;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRegion;
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.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.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.HashMap;
import java.util.Map;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class IrisDepositModifierParityTest {
private final Map<String, PlatformBlockState> states = new HashMap<>();
@Before
public void bindPlatform() {
IrisPlatforms.unbind();
PlatformRegistries registries = mock(PlatformRegistries.class);
when(registries.block(anyString())).thenAnswer(invocation -> state(invocation.getArgument(0)));
IrisPlatform platform = mock(IrisPlatform.class);
when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform);
}
@After
public void unbindPlatform() {
IrisPlatforms.unbind();
}
@Test
public void configuredShapesKeepRecordedBlocksAcrossScopesAndCavePolicies() {
long[] seeds = {101L, -73L, 90181L};
long[][] expected = {
{-4609692219192155345L, 784L, 2118L, 53L, 67L, 11L},
{-331296667371868787L, 818L, 2413L, 94L, 49L, 20L},
{-3365014205342608518L, 641L, 2340L, 58L, 45L, 27L}
};
for (int i = 0; i < seeds.length; i++) {
for (boolean multicore : new boolean[]{false, true}) {
assertArrayEquals("seed=" + seeds[i] + ", multicore=" + multicore,
expected[i], generateConfigured(seeds[i], multicore));
}
}
}
@SuppressWarnings("unchecked")
private long[] generateConfigured(long seed, boolean multicore) {
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
ChunkContext context = mock(ChunkContext.class, RETURNS_DEEP_STUBS);
IrisDimension dimension = new IrisDimension();
IrisRegion region = new IrisRegion();
IrisBiome surface = new IrisBiome();
IrisBiome cave = new IrisBiome();
cave.setLoadKey("allowed");
cave.setOreDepositFrequencyMultiplier(0.65D);
cave.setOreDepositSizeMultiplier(1.5D);
IrisBiome denied = new IrisBiome();
denied.setLoadKey("denied");
IrisDepositVariant variant = new IrisDepositVariant().setMinHeight(-64).setMaxHeight(64);
variant.getRemap().put("minecraft:gold_ore", "minecraft:redstone_ore");
cave.getDepositVariants().add(variant);
dimension.getDeposits().add(generator("granite", IrisDepositShape.IRIS, 25, 70,
IrisDepositHeightDistribution.CLIPPED_UNIFORM, "stone", "deepslate"));
region.getDeposits().add(generator("iron_ore", IrisDepositShape.VANILLA_ELLIPSOID, 20, 110,
IrisDepositHeightDistribution.TRIANGLE, "stone", "granite", "deepslate"));
IrisDepositGenerator diamond = generator("diamond_ore", IrisDepositShape.VANILLA_SCATTERED, 12, 100,
IrisDepositHeightDistribution.UNIFORM, "iron_ore", "deepslate_iron_ore", "granite");
diamond.setIncludedBiomes(new KList<>("allowed"));
diamond.setDiscardChanceOnAirExposure(0.5D);
surface.getDeposits().add(diamond);
IrisDepositGenerator gold = generator("gold_ore", IrisDepositShape.VANILLA_ELLIPSOID, 9, 70,
IrisDepositHeightDistribution.TRIANGLE, "stone", "granite");
gold.setExcludedBiomes(new KList<>("allowed"));
gold.setDiscardChanceOnAirExposure(0.3D);
surface.getDeposits().add(gold);
when(engine.getHeight()).thenReturn(48);
when(engine.getMinHeight()).thenReturn(-64);
when(engine.getSeedManager().getDeposit()).thenReturn(seed);
when(engine.getDimension()).thenReturn(dimension);
when(engine.getCaveBiome(anyInt(), anyInt(), anyInt(), any())).thenAnswer(invocation -> {
int x = invocation.getArgument(0);
int y = invocation.getArgument(1);
int z = invocation.getArgument(2);
return ((Math.floorDiv(x, 8) + Math.floorDiv(z, 8) + Math.floorDiv(y, 8)) & 1) == 0 ? cave : denied;
});
when(context.getRoundedHeight(anyInt(), anyInt())).thenReturn(40);
when(context.getBiome().get(anyInt(), anyInt())).thenReturn(surface);
when(context.getRegion().get(anyInt(), anyInt())).thenReturn(region);
MantleChunk<Matter> chunk = mock(MantleChunk.class);
Mantle<Matter> mantle = engine.getMantle().getMantle();
doReturn(chunk).when(mantle).getChunk(-3, 5);
when(chunk.use()).thenReturn(chunk);
MatterCavern cavern = new MatterCavern(true, "allowed", (byte) 0);
when(chunk.get(anyInt(), anyInt(), anyInt(), any())).thenAnswer(invocation -> {
int x = invocation.getArgument(0);
int y = invocation.getArgument(1);
return x == 4 && y % 7 == 0 ? cavern : null;
});
Hunk<PlatformBlockState> terrain = Hunk.newArrayHunk(16, 48, 16);
for (int y = 0; y < 48; y++) {
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
terrain.set(x, y, z, state(y >= 40 ? "air" : y == 0 ? "bedrock"
: x == 7 && z % 4 == 0 && y % 9 == 0 ? "cave_air"
: y < 12 ? "deepslate" : "stone"));
}
}
}
MultiBurst burst = new MultiBurst("Deposit parity", () -> 4);
when(engine.burst()).thenReturn(burst);
try (MockedStatic<B> blocks = mockStatic(B.class, CALLS_REAL_METHODS)) {
blocks.when(() -> B.getStateOrNull(anyString(), anyBoolean()))
.thenAnswer(invocation -> state(invocation.getArgument(0)));
blocks.when(() -> B.toDeepSlateOre(any(), any())).thenAnswer(invocation -> {
PlatformBlockState host = invocation.getArgument(0);
PlatformBlockState ore = invocation.getArgument(1);
return host.isDeepSlate() && ore.isOre()
? state("deepslate_" + ore.key().substring(10)) : ore;
});
new IrisDepositModifier(engine).generateDeposits(terrain, -3, 5, multicore, context);
} finally {
burst.close();
}
long[] result = {0xcbf29ce484222325L, 0L, 0L, 0L, 0L, 0L};
for (int y = 0; y < 48; y++) {
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
PlatformBlockState block = terrain.get(x, y, z);
result[0] = (result[0] ^ block.key().hashCode()) * 0x100000001b3L;
if (block.key().endsWith("granite")) {
result[1]++;
}
if (block.key().endsWith("iron_ore")) {
result[2]++;
}
if (block.key().endsWith("diamond_ore")) {
result[3]++;
}
if (block.key().endsWith("gold_ore")) {
result[4]++;
}
if (block.key().endsWith("redstone_ore")) {
result[5]++;
}
if (x == 4 && y % 7 == 0 && y > 0 && y < 40) {
assertEquals(y < 12 ? "minecraft:deepslate" : "minecraft:stone", block.key());
}
}
}
}
return result;
}
private IrisDepositGenerator generator(String block, IrisDepositShape shape, int size, int attempts,
IrisDepositHeightDistribution distribution, String... hosts) {
IrisDepositGenerator generator = new PaletteGenerator(state(block));
return generator.setShape(shape).setMinSize(size).setMaxSize(size)
.setMinPerChunk(attempts).setMaxPerChunk(attempts)
.setMinHeight(-4).setMaxHeight(44).setHeightDistribution(distribution)
.setSurfaceClearance(0).setPerClumpSpawnChance(0.7D)
.setReplaceableBlocks(new KList<>(hosts));
}
private PlatformBlockState state(String block) {
String key = block.contains(":") ? block : "minecraft:" + block;
return states.computeIfAbsent(key, value -> {
PlatformBlockState state = mock(PlatformBlockState.class);
when(state.key()).thenReturn(value);
when(state.materialKey()).thenReturn(value);
when(state.isOre()).thenReturn(value.endsWith("_ore"));
when(state.isDeepSlate()).thenReturn(value.contains("deepslate"));
when(state.isAir()).thenReturn(value.endsWith("air"));
return state;
});
}
private static final class PaletteGenerator extends IrisDepositGenerator {
private final KList<PlatformBlockState> blocks;
private PaletteGenerator(PlatformBlockState block) {
blocks = new KList<>(block);
}
@Override
public KList<PlatformBlockState> getBlockData(IrisData data) {
return blocks;
}
}
}
@@ -0,0 +1,239 @@
package art.arcane.iris.engine.modifier;
import art.arcane.iris.engine.actuator.IrisDecorantActuator;
import art.arcane.iris.engine.decorator.IrisCeilingDecorator;
import art.arcane.iris.engine.decorator.IrisSurfaceDecorator;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.SeedManager;
import art.arcane.iris.engine.mantle.EngineMantle;
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 art.arcane.iris.util.common.data.B;
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.matter.slices.MarkerMatter;
import org.junit.Test;
import org.junit.Before;
import org.junit.After;
import org.mockito.MockedStatic;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
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.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
public class IrisNaturalCaveScanTest {
private MockedStatic<B> blocks;
@Before
public void prepareBlocks() {
blocks = mockStatic(B.class, CALLS_REAL_METHODS);
blocks.when(() -> B.getState(anyString())).thenReturn(null);
}
@After
public void releaseBlocks() {
blocks.close();
}
@Test
public void narrowScanPreservesDecoratedBlocksMarkersAndMutationOrder() throws Exception {
verifyScan(5, 3);
}
@Test
public void chunkScanPreservesDecoratedBlocksMarkersAndMutationOrder() throws Exception {
verifyScan(16, 16);
}
@Test
public void listenerFailureReleasesMantleChunk() throws Exception {
Fixture fixture = new Fixture();
IllegalStateException failure = new IllegalStateException("listener rejected decoration");
Hunk<PlatformBlockState> output = fixture.output(3, 2).listen((x, y, z, state) -> {
throw failure;
});
assertSame(failure, assertThrows(IllegalStateException.class,
() -> fixture.modifier.decorateNaturalCaves(-32, 48, output)));
verify(fixture.chunk).release();
}
@Test
@SuppressWarnings("unchecked")
public void dimensionFailureReleasesMantleChunk() throws Exception {
Fixture fixture = new Fixture();
Hunk<PlatformBlockState> output = mock(Hunk.class);
doReturn(3).when(output).getWidth();
doReturn(2).when(output).getDepth();
IllegalStateException failure = new IllegalStateException("height unavailable");
doAnswer(call -> { throw failure; }).when(output).getHeight();
assertSame(failure, assertThrows(IllegalStateException.class,
() -> fixture.modifier.decorateNaturalCaves(-32, 48, output)));
verify(fixture.chunk).release();
}
private static void verifyScan(int width, int depth) throws Exception {
Fixture fixture = new Fixture();
Hunk<PlatformBlockState> output = fixture.output(width, depth);
List<String> writes = new ArrayList<>();
fixture.modifier.decorateNaturalCaves(-32, 48,
output.listen((x, y, z, state) -> writes.add(x + ":" + y + ":" + z + ":" + state.key())));
List<String> expectedCalls = new ArrayList<>();
List<String> expectedMarkers = new ArrayList<>();
List<String> expectedWrites = new ArrayList<>();
Method markerRoll = IrisCarveModifier.class.getDeclaredMethod("markerRoll", int.class, int.class, int.class, long.class);
markerRoll.setAccessible(true);
for (int x = 0; x < width; x++) {
for (int z = 0; z < depth; z++) {
for (int floor : new int[]{2, 20}) {
int ceiling = floor == 2 ? 5 : 22;
expectedCalls.add("floor:" + x + ":" + z + ":" + (floor - 1) + ":" + (ceiling - floor - 1));
expectedCalls.add("ceiling:" + x + ":" + z + ":" + ceiling + ":" + (ceiling - floor - 1));
expectedWrites.add(x + ":" + floor + ":" + z + ":minecraft:moss_block");
if (floor == 2) {
expectedWrites.add(x + ":15:" + z + ":minecraft:stone");
for (int y = 20; y <= 22; y++) {
expectedWrites.add(x + ":" + y + ":" + z + ":minecraft:cave_air");
}
}
expectedWrites.add(x + ":" + ceiling + ":" + z + ":minecraft:calcite");
if ((boolean) markerRoll.invoke(fixture.modifier, x - 32, ceiling, z + 48, 0x9E3779B97F4A7C15L)) {
expectedMarkers.add((x - 32) + ":" + ceiling + ":" + (z + 48) + ":" + MarkerMatter.CAVE_CEILING);
}
if ((boolean) markerRoll.invoke(fixture.modifier, x - 32, floor, z + 48, 0xC2B2AE3D27D4EB4FL)) {
expectedMarkers.add((x - 32) + ":" + floor + ":" + (z + 48) + ":" + MarkerMatter.CAVE_FLOOR);
}
}
for (int y = 0; y < 32; y++) {
PlatformBlockState expected = initialState(fixture, y);
if (y == 2 || y == 20) {
expected = fixture.floor;
} else if (y == 5 || y == 22) {
expected = fixture.ceiling;
} else if (y == 15) {
expected = fixture.stone;
} else if (y == 21) {
expected = fixture.air;
}
assertSame("block " + x + ":" + y + ":" + z, expected, output.getRaw(x, y, z));
}
}
}
assertEquals(expectedCalls, fixture.calls);
assertEquals(expectedWrites, writes);
assertEquals(expectedMarkers, fixture.markers);
assertTrue("fixture must exercise marker writes", !expectedMarkers.isEmpty());
verify(fixture.chunk).use();
verify(fixture.chunk).release();
}
private static PlatformBlockState initialState(Fixture fixture, int y) {
return y >= 2 && y <= 5 || y == 10 || y >= 14 && y <= 17 || y >= 28
? fixture.air : fixture.stone;
}
private static PlatformBlockState block(String key, boolean solid) {
PlatformBlockState state = mock(PlatformBlockState.class);
doReturn(key).when(state).key();
doReturn(key).when(state).materialKey();
doReturn(solid).when(state).isSolid();
return state;
}
private static final class Fixture {
private final PlatformBlockState air = block("minecraft:cave_air", false);
private final PlatformBlockState stone = block("minecraft:stone", true);
private final PlatformBlockState floor = block("minecraft:moss_block", true);
private final PlatformBlockState ceiling = block("minecraft:calcite", true);
private final IrisCarveModifier modifier = mock(IrisCarveModifier.class, CALLS_REAL_METHODS);
private final MantleChunk<Matter> chunk;
private final List<String> calls = new ArrayList<>();
private final List<String> markers = new ArrayList<>();
@SuppressWarnings("unchecked")
private Fixture() throws Exception {
Engine engine = mock(Engine.class);
EngineMantle engineMantle = mock(EngineMantle.class);
Mantle<Matter> mantle = mock(Mantle.class);
chunk = mock(MantleChunk.class);
doReturn(engine).when(modifier).getEngine();
doReturn(32).when(engine).getHeight();
doReturn(new SeedManager(1337L)).when(engine).getSeedManager();
doReturn(engineMantle).when(engine).getMantle();
doReturn(mantle).when(engineMantle).getMantle();
doReturn(chunk).when(mantle).getChunk(-2, 3);
doReturn(chunk).when(chunk).use();
doAnswer(call -> {
markers.add(call.getArgument(0) + ":" + call.getArgument(1) + ":" + call.getArgument(2) + ":" + call.getArgument(3));
return null;
}).when(mantle).set(anyInt(), anyInt(), anyInt(), any());
IrisBiome biome = mock(IrisBiome.class);
doReturn(new IrisDecorator[]{new IrisDecorator()}).when(biome).getDecoratorBucket(IrisDecorationPart.NONE);
doReturn(new IrisDecorator[]{new IrisDecorator()}).when(biome).getDecoratorBucket(IrisDecorationPart.CEILING);
doReturn(biome).when(modifier).resolveCaveBoundaryBiome(eq(chunk), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(), any(), any());
IrisDecorantActuator decorant = mock(IrisDecorantActuator.class);
IrisSurfaceDecorator surface = mock(IrisSurfaceDecorator.class);
IrisCeilingDecorator roof = mock(IrisCeilingDecorator.class);
doReturn(surface).when(decorant).getSurfaceDecorator();
doReturn(roof).when(decorant).getCeilingDecorator();
Field field = IrisCarveModifier.class.getDeclaredField("decorant");
field.setAccessible(true);
field.set(modifier, decorant);
doAnswer(call -> {
int x = call.getArgument(0);
int z = call.getArgument(1);
int y = call.getArgument(11);
Hunk<PlatformBlockState> output = call.getArgument(8);
calls.add("floor:" + x + ":" + z + ":" + y + ":" + call.getArgument(12));
output.setRaw(x, y + 1, z, floor);
if (y == 1) {
output.setRaw(x, 15, z, stone);
for (int height = 20; height <= 22; height++) {
output.setRaw(x, height, z, air);
}
}
return null;
}).when(surface).decorate(anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(), eq(biome), any(), anyInt(), anyInt());
doAnswer(call -> {
int x = call.getArgument(0);
int z = call.getArgument(1);
int y = call.getArgument(11);
Hunk<PlatformBlockState> output = call.getArgument(8);
calls.add("ceiling:" + x + ":" + z + ":" + y + ":" + call.getArgument(12));
output.setRaw(x, y, z, ceiling);
return null;
}).when(roof).decorate(anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(), eq(biome), any(), anyInt(), anyInt());
}
private Hunk<PlatformBlockState> output(int width, int depth) {
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(width, 32, depth);
for (int x = 0; x < width; x++) {
for (int z = 0; z < depth; z++) {
for (int y = 0; y < 32; y++) {
output.setRaw(x, y, z, initialState(this, y));
}
}
}
return output;
}
}
}
@@ -40,6 +40,7 @@ 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.clearInvocations;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -118,6 +119,7 @@ public class IrisDimensionCarvingResolverParityTest {
assertSame("entry mismatch at worldY=" + worldY + " worldX=" + worldX + " worldZ=" + worldZ, legacyResolved, statefulResolved);
}
}
fixture.clearRecordedCalls();
}
}
@@ -138,6 +140,7 @@ public class IrisDimensionCarvingResolverParityTest {
assertSame("mixed entry mismatch at worldY=" + worldY + " worldX=" + worldX + " worldZ=" + worldZ, legacyResolved, statefulResolved);
}
}
fixture.clearRecordedCalls();
}
}
@@ -154,6 +157,7 @@ public class IrisDimensionCarvingResolverParityTest {
assertSame("cave biome mismatch at x=" + x + " y=" + y + " z=" + z, defaultBiome, stateBiome);
}
}
fixture.clearRecordedCalls();
}
}
@@ -174,6 +178,7 @@ public class IrisDimensionCarvingResolverParityTest {
secondPlan[columnIndex]
);
}
fixture.clearRecordedCalls();
}
}
}
@@ -225,6 +230,26 @@ public class IrisDimensionCarvingResolverParityTest {
assertSame(replacementExpected, IrisDimensionCarvingResolver.resolveRootEntry(engine, worldY));
}
@Test
public void explicitStateInvalidatesWhenOnlyEngineDataChanges() {
Fixture fixture = createFixture();
Engine engine = fixture.engine;
IrisData firstData = engine.getData();
IrisData replacementData = mock(IrisData.class);
IrisDimensionCarvingEntry entry = mock(IrisDimensionCarvingEntry.class);
IrisBiome firstBiome = new IrisBiome();
IrisBiome replacementBiome = new IrisBiome();
doReturn(firstBiome).when(entry).getRealBiome(firstData);
doReturn(replacementBiome).when(entry).getRealBiome(replacementData);
IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State();
assertSame(firstBiome, IrisDimensionCarvingResolver.resolveEntryBiome(engine, entry, state));
doReturn(replacementData).when(engine).getData();
assertSame(replacementBiome, IrisDimensionCarvingResolver.resolveEntryBiome(engine, entry, state));
doReturn(firstData).when(engine).getData();
assertSame(firstBiome, IrisDimensionCarvingResolver.resolveEntryBiome(engine, entry, state));
}
@Test
public void resolverStateUsesWeakRuntimeIdentityBindings() throws NoSuchFieldException {
Field engineIdentity = IrisDimensionCarvingResolver.State.class.getDeclaredField("engineIdentity");
@@ -404,7 +429,9 @@ public class IrisDimensionCarvingResolverParityTest {
doReturn(surfaceBiome).when(engine).getSurfaceBiome(anyInt(), anyInt());
doReturn(fallbackBiome).when(engine).getCaveBiome(anyInt(), anyInt());
return new Fixture(engine);
return new Fixture(engine, new Object[]{engine, dimension, data, biomeLoader,
rootLowBiome, rootHighBiome, childABiome, childBBiome, childCBiome,
fallbackBiome, surfaceBiome});
}
private Fixture createMixedDepthFixture() {
@@ -490,7 +517,9 @@ public class IrisDimensionCarvingResolverParityTest {
doReturn(surfaceBiome).when(engine).getSurfaceBiome(anyInt(), anyInt());
doReturn(fallbackBiome).when(engine).getCaveBiome(anyInt(), anyInt());
return new Fixture(engine);
return new Fixture(engine, new Object[]{engine, dimension, data, biomeLoader,
rootLowBiome, rootHighBiome, childABiome, childBBiome, childCBiome,
childDBiome, childEBiome, childFBiome, childGBiome, fallbackBiome, surfaceBiome});
}
private IrisDimensionCarvingEntry[] buildColumnPlan(Engine engine, IrisDimensionCarvingEntry rootEntry, int chunkX, int chunkZ, IrisDimensionCarvingResolver.State state) {
@@ -660,7 +689,10 @@ public class IrisDimensionCarvingResolverParityTest {
return Math.min(depth, MAX_CHILD_DEPTH);
}
private record Fixture(Engine engine) {
private record Fixture(Engine engine, Object[] mocks) {
private void clearRecordedCalls() {
clearInvocations(mocks);
}
}
private record RetainedFixture(Engine engine, IrisDimensionCarvingEntry entry) {
@@ -0,0 +1,176 @@
package art.arcane.iris.util.project.noise;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import java.util.List;
import java.util.function.Consumer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
public class CNGSignedStateMutationTest {
private static final double EPSILON = 1.0E-12D;
@Test
public void eachTransformRefreshesPreviouslyCachedCoordinates() {
List<Consumer<CNG>> mutations = List.of(
noise -> noise.scale(0.375D),
noise -> noise.scale(0.5D).bake().scale(0.75D),
noise -> noise.zoom(2D),
noise -> noise.oct(3),
noise -> noise.up(0.125D),
noise -> noise.down(0.125D),
noise -> noise.patch(0.75D),
noise -> noise.pow(1.25D),
noise -> noise.fractureWith(simplex(13L), 4D),
noise -> noise.child(simplex(17L)).injectWith(CNG.MULTIPLY));
for (Consumer<CNG> mutation : mutations) {
CNG sampled = simplex(11L);
sampled.noiseFastSigned2D(4D, 8D);
sampled.noiseFastSigned3D(4D, 6D, 8D);
mutation.accept(sampled);
CNG expected = simplex(11L);
mutation.accept(expected);
assertEquals((expected.noiseFast2D(4D, 8D) * 2D) - 1D,
sampled.noiseFastSigned2D(4D, 8D), EPSILON);
assertEquals((expected.noiseFast3D(4D, 6D, 8D) * 2D) - 1D,
sampled.noiseFastSigned3D(4D, 6D, 8D), EPSILON);
}
}
@Test
public void generatorCallbackMutationInvalidatesOnlySubsequentSamples() {
for (boolean threeDimensions : new boolean[]{false, true}) {
RecordingNoise source = new RecordingNoise();
CNG noise = new CNG(new RNG(11L), source, 1D, 1);
source.onSample = () -> noise.scale(0.5D);
assertEquals(value(threeDimensions, 4D, 6D, 8D), sample(noise, threeDimensions), 0D);
assertEquals(value(threeDimensions, 2D, 3D, 4D), sample(noise, threeDimensions), 0D);
assertEquals(value(threeDimensions, 2D, 3D, 4D), sample(noise, threeDimensions), 0D);
assertEquals(2, source.calls);
}
}
@Test
public void nestedFractureOverridesRetainParentAndChildMutationBoundaries() {
for (boolean threeDimensions : new boolean[]{false, true}) {
RecordingNoise source = new RecordingNoise();
CNG parent = new CNG(new RNG(11L), source, 1D, 1);
RecordingNoise fractureSource = new RecordingNoise();
MutatingFracture fracture = new MutatingFracture(fractureSource);
fracture.beforeSample = () -> {
parent.scale(0.5D);
fracture.scale(0.25D);
};
parent.fractureWith(fracture, 8D);
double shiftedX = 4D + value(threeDimensions, 1D, 1.5D, 2D) * 4D;
double shiftedY = 6D + value(false, 1.5D, 0D, 1D) * 4D;
double shiftedZ = 8D + (threeDimensions
? value(true, 2D, 1D, 1.5D) : value(false, 2D, 0D, 1D)) * 4D;
assertEquals(value(threeDimensions, shiftedX, shiftedY, shiftedZ), sample(parent, threeDimensions), 0D);
assertEquals(value(threeDimensions, shiftedX * 0.5D, shiftedY * 0.5D, shiftedZ * 0.5D),
sample(parent, threeDimensions), 0D);
assertEquals(2, source.calls);
}
}
@Test
public void callbackFailureRetainsItsMutationAndDoesNotPublishASample() {
for (boolean threeDimensions : new boolean[]{false, true}) {
RecordingNoise source = new RecordingNoise();
CNG noise = new CNG(new RNG(11L), source, 1D, 1);
IllegalStateException failure = new IllegalStateException("sample failed");
source.onSample = () -> {
noise.scale(0.25D);
throw failure;
};
assertSame(failure, assertThrows(IllegalStateException.class, () -> sample(noise, threeDimensions)));
assertEquals(value(threeDimensions, 1D, 1.5D, 2D), sample(noise, threeDimensions), 0D);
assertEquals(value(threeDimensions, 1D, 1.5D, 2D), sample(noise, threeDimensions), 0D);
assertEquals(2, source.calls);
}
}
private static CNG simplex(long seed) {
return new CNG(new RNG(seed), new SimplexNoise(seed), 0.75D, 1);
}
private static double sample(CNG noise, boolean threeDimensions) {
return threeDimensions ? noise.noiseFastSigned3D(4D, 6D, 8D) : noise.noiseFastSigned2D(4D, 8D);
}
private static double value(boolean threeDimensions, double x, double y, double z) {
return threeDimensions ? x * 0.01D + y * 0.02D + z * 0.03D : x * 0.01D + z * 0.02D;
}
private static final class RecordingNoise implements NoiseGenerator {
private Runnable onSample;
private int calls;
@Override
public double noise(double x) {
return noise(x, 0D);
}
@Override
public double noise(double x, double z) {
return (noiseSigned(x, z) + 1D) * 0.5D;
}
@Override
public double noise(double x, double y, double z) {
return (noiseSigned(x, y, z) + 1D) * 0.5D;
}
@Override
public double noiseSigned(double x, double z) {
sampled();
return value(false, x, 0D, z);
}
@Override
public double noiseSigned(double x, double y, double z) {
sampled();
return value(true, x, y, z);
}
private void sampled() {
calls++;
Runnable action = onSample;
onSample = null;
if (action != null) {
action.run();
}
}
}
private static final class MutatingFracture extends CNG {
private Runnable beforeSample;
private MutatingFracture(NoiseGenerator source) {
super(new RNG(13L), source, 1D, 1);
}
@Override
public double noiseFastSigned2D(double x, double z) {
mutate();
return super.noiseFastSigned2D(x, z);
}
@Override
public double noiseFastSigned3D(double x, double y, double z) {
mutate();
return super.noiseFastSigned3D(x, y, z);
}
private void mutate() {
Runnable action = beforeSample;
beforeSample = null;
if (action != null) {
action.run();
}
}
}
}