mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +00:00
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:
1 parent
151677f92f
commit
0dcd1900be
41 files changed
+4398
-191
No files matched your search
@@ -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;
|
||||
|
||||
|
||||
+34
-12
@@ -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,
|
||||
|
||||
+57
-2
@@ -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);
|
||||
|
||||
+21
-16
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user