Reworked terrain transitions

- New terrain heights meet saved boundaries while retaining local relief
- Surface water levels approach saved fluid heights across the band
- Cave and physical biome matching use a four-block seam patch
- Terrain generates once without speculative neighbor containment
- Bounded caches serve transition membership and boundary lookups
- Saved biome writes honor the selected durability policy
- Release labels and presentation code leave generation identity unchanged
- Unfinished objects retain blocks and metadata across pack changes
- Whole structure assemblies preserve pending excavation and foundations
- New placements keep pending historical pieces clear
- Transition probes cover cutovers and retained object fragments
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-27 15:31:31 -04:00
1 parent 9eb05fd987
commit 8c3e5d547b
159 files changed
+6092 -1905

No files matched your search

@@ -21,7 +21,6 @@ import java.util.concurrent.CompletableFuture;
public class ChunkContext {
private boolean naturalTerrain = true;
private boolean speculativeTerrain;
private Hunk<NativeBiome> terrainBiomes;
private FloatingBiomeOverlay floatingBiomes;
private final int x;
@@ -164,14 +163,6 @@ public class ChunkContext {
}
}
public boolean isSpeculativeTerrain() {
return speculativeTerrain;
}
public void beginSpeculativeTerrain() {
speculativeTerrain = true;
}
public boolean isNaturalTerrain() {
return naturalTerrain;
}
@@ -2,17 +2,14 @@ package art.arcane.iris.generation.decoration;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import java.util.Optional;
final class IrisSugarCane {
private static final int[][] NEIGHBORS = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
@@ -74,11 +71,6 @@ final class IrisSugarCane {
private static boolean plannedWater(Engine engine, int x, int y, int z) {
IrisComplex complex = engine.getComplex();
Optional<TerrainBoundarySignature> resolved = complex.resolvedTerrainColumn(x, z);
if (resolved.isPresent()) {
String key = resolved.get().geometry().voxelAt(y + engine.getMinHeight()).stateKey();
return supportsAdjacent(IrisPlatforms.get().registries().blockOrNull(key));
}
HydrologyColumnSample sample = complex.sampleHydrologyColumn(x, z);
HydrologyColumnLayer fluid = sample == null ? null : sample.primarySurfaceFluidLayerOrNull();
if (fluid != null) {
@@ -21,8 +21,6 @@ package art.arcane.iris.generation.mantle;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.generation.runtime.DimensionStackContext;
import art.arcane.iris.generation.runtime.DimensionStackLayout;
import art.arcane.iris.generation.runtime.UpperDimensionContext;
@@ -55,8 +53,6 @@ import org.jetbrains.annotations.UnmodifiableView;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.concurrent.TimeUnit;
public interface EngineMantle extends MatterGenerator {
@@ -112,10 +108,6 @@ public interface EngineMantle extends MatterGenerator {
}
default int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
OptionalInt resolved = getComplex().resolvedTerrainHeight(x, z, ignoreFluid);
if (resolved.isPresent()) {
return resolved.getAsInt();
}
return ignoreFluid ? trueHeight(x, z) : Math.max(trueHeight(x, z), getFluidHeight(x, z));
}
@@ -124,21 +116,6 @@ public interface EngineMantle extends MatterGenerator {
}
default boolean isCarved(int x, int h, int z) {
Optional<TerrainBoundarySignature> resolved = getComplex().resolvedTerrainColumn(x, z);
if (resolved.isPresent()) {
PreObjectMatterCell cell = getMantle().get(x, h, z, PreObjectMatterCell.class);
if (cell != null && cell.blockCaptured()) {
NativeBlockState block = getMantle().get(x, h, z, NativeBlockState.class);
if (block != null) {
return (block.isAir() || block.isFluid())
&& resolved.get().geometry().hasSolidAbove(h + getEngine().getMinHeight());
}
}
if (cell != null && cell.cavernCaptured()) {
return getMantle().get(x, h, z, MatterCavern.class) != null;
}
return resolved.get().geometry().isEnclosedOpenAt(h + getEngine().getMinHeight());
}
HydrologyCaveCell hydrology = HydrologyCaveStorage.getIfPresent(getMantle(), x, h, z);
if (hydrology != null) {
return hydrology.carves();
@@ -149,18 +126,6 @@ public interface EngineMantle extends MatterGenerator {
default NativeBlockState get(int x, int y, int z) {
NativeBlockState block = getMantle().get(x, y, z, NativeBlockState.class);
Optional<TerrainBoundarySignature> resolved = getComplex().resolvedTerrainColumn(x, z);
if (resolved.isPresent()) {
PreObjectMatterCell cell = getMantle().get(x, y, z, PreObjectMatterCell.class);
if (cell == null || !cell.blockCaptured()) {
String stateKey = resolved.get().geometry().voxelAt(y + getEngine().getMinHeight()).stateKey();
NativeBlockState natural = IrisPlatforms.get().registries().blockOrNull(stateKey);
if (natural == null) {
throw new IllegalStateException("Saved terrain state is unavailable: " + stateKey);
}
return natural;
}
}
return block == null ? AIR.get() : block;
}
@@ -177,10 +142,6 @@ public interface EngineMantle extends MatterGenerator {
}
default int getFluidHeight(int x, int z) {
Optional<TerrainBoundarySignature> resolved = getComplex().resolvedTerrainColumn(x, z);
if (resolved.isPresent()) {
return resolved.get().fluidHeight().orElse(-1);
}
return (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(x, z));
}
@@ -29,16 +29,24 @@ import art.arcane.volmlib.util.collection.KList;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.function.Supplier;
import java.util.Objects;
import art.arcane.volmlib.util.matter.IrisMatter;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
import java.util.Map;
final class FloatingObjectPlacementTransaction implements IObjectPlacer {
private final IslandObjectPlacer delegate;
private final IObjectPlacer delegate;
private final ObjectContinuationBundle.PlacementKey placementKey;
private final KList<BufferedMutation> mutations;
private final Map<PositionKey, NativeBlockState> bufferedBlocks;
private int blockWrites;
FloatingObjectPlacementTransaction(IslandObjectPlacer delegate) {
this.delegate = delegate;
FloatingObjectPlacementTransaction(IObjectPlacer delegate, ObjectContinuationBundle.PlacementKey placementKey) {
this.delegate = Objects.requireNonNull(delegate);
this.placementKey = Objects.requireNonNull(placementKey);
this.mutations = new KList<>();
this.bufferedBlocks = new HashMap<>();
}
@@ -56,7 +64,8 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
discard();
return CommitResult.REJECTED_TRANSITION;
}
if (!delegate.canWriteObjectBlock(mutation.x(), mutation.y(), mutation.z())) {
if (delegate instanceof IslandObjectPlacer island
&& !island.canWriteObjectBlock(mutation.x(), mutation.y(), mutation.z())) {
discard();
return CommitResult.REJECTED_SUPPORT;
}
@@ -64,10 +73,38 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
for (BufferedMutation mutation : mutations) {
mutation.apply(delegate);
}
persistContinuation(engine);
discard();
return CommitResult.COMMITTED;
}
private void persistContinuation(Engine engine) {
int minimumX = Integer.MAX_VALUE;
int minimumZ = Integer.MAX_VALUE;
int maximumX = Integer.MIN_VALUE;
int maximumZ = Integer.MIN_VALUE;
for (BufferedMutation mutation : mutations) {
minimumX = Math.min(minimumX, mutation.x());
minimumZ = Math.min(minimumZ, mutation.z());
maximumX = Math.max(maximumX, mutation.x());
maximumZ = Math.max(maximumZ, mutation.z());
}
if ((minimumX >> 4) == (maximumX >> 4) && (minimumZ >> 4) == (maximumZ >> 4)) {
return;
}
IrisMatterSupport.ensureRegistered();
LinkedHashMap<ObjectContinuationBundle.ChunkPosition, Matter> payloads = new LinkedHashMap<>();
for (BufferedMutation mutation : mutations) {
ObjectContinuationBundle.ChunkPosition chunk = new ObjectContinuationBundle.ChunkPosition(mutation.x() >> 4, mutation.z() >> 4);
Matter payload = payloads.computeIfAbsent(chunk, ignored -> new IrisMatter(16, engine.getHeight(), 16));
ObjectContinuationPersistence.put(payload, mutation.x() & 15, mutation.y(), mutation.z() & 15, mutation.value());
}
LinkedHashMap<ObjectContinuationBundle.ChunkPosition, Supplier<Matter>> destinations = new LinkedHashMap<>();
payloads.forEach((chunk, payload) -> destinations.put(chunk, () -> payload));
ObjectContinuationPersistence.persist(engine.getMantle().getMantle(), placementKey,
new ObjectContinuationBundle.Bounds(minimumX, minimumZ, maximumX, maximumZ), destinations);
}
void discard() {
mutations.clear();
bufferedBlocks.clear();
@@ -81,12 +81,14 @@ public class IrisStructureComponent extends IrisMantleComponent {
if (!complex.allowsNewGenerationChunk(x, z)) {
return;
}
int ordinal = 0;
for (IrisStructurePlacement placement : StructurePlacementScope.placementsAt(
getEngineMantle().getEngine(), x, z)) {
int placementOrdinal = ordinal++;
if (!placement.hasIrisStructures()) {
continue;
}
placeFromPlacement(writer, placement, x, z, complex);
placeFromPlacement(writer, placement, x, z, placementOrdinal);
}
}
@@ -96,7 +98,7 @@ public class IrisStructureComponent extends IrisMantleComponent {
IrisStructurePlacement placement,
int cx,
int cz,
IrisComplex complex
int ordinal
) {
IrisStructureLocator.ResolvedPlacement resolved = IrisStructureLocator.resolvePlacement(
getEngineMantle().getEngine(), placement, cx, cz);
@@ -117,16 +119,7 @@ public class IrisStructureComponent extends IrisMantleComponent {
return;
}
IrisStructureTerrain terrain = placement.resolvedTerrain();
int[] bounds = computePieceBounds(pieces);
int horizontalPadding = terrain.resolvedMode() == StructureTerrainMode.FORCE_CARVE
|| terrain.resolvedMode() == StructureTerrainMode.BORE
? Math.max(0, terrain.getHorizontalPadding())
: 0;
if (bounds == null || !complex.allowsNewGenerationFootprint(
saturatedOffset(bounds[0], -horizontalPadding),
saturatedOffset(bounds[2], -horizontalPadding),
saturatedOffset(bounds[3], horizontalPadding),
saturatedOffset(bounds[5], horizontalPadding))) {
if (!IrisStructureLocator.allowsResolvedFootprint(getEngineMantle().getEngine(), resolved)) {
return;
}
RNG rng = resolved.rng();
@@ -135,60 +128,66 @@ public class IrisStructureComponent extends IrisMantleComponent {
IrisLogging.debug("[StructTrace] ASSEMBLED chunk=" + cx + "," + cz + " key=" + key + " baseY=" + baseY + " pieces=" + pieces.size());
}
if (!placement.isAnchoredUnderground()) {
clearIntersectingObjectTrees(writer, resolved);
}
try (MantleWriter.ObjectPlacementCapture capture = writer.captureObjectPlacement(
new ObjectContinuationBundle.PlacementKey(ObjectContinuationBundle.Kind.STRUCTURE, cx, cz, ordinal))) {
if (!placement.isAnchoredUnderground()) {
clearIntersectingObjectTrees(writer, resolved);
}
StructureTerrainMode terrainMode = terrain.resolvedMode();
if (terrainMode == StructureTerrainMode.FORCE_CARVE) {
forceCarveStructure(writer, pieces, terrain);
} else if (terrainMode == StructureTerrainMode.BORE) {
boreStructure(writer, pieces, terrain);
} else if (terrainMode != StructureTerrainMode.PRESERVE
&& terrainMode != StructureTerrainMode.SOURCE) {
throw new IllegalStateException("Iris assembly terrain mode " + terrainMode
+ " is not implemented for structure '" + key + "'");
}
StructureTerrainMode terrainMode = terrain.resolvedMode();
if (terrainMode == StructureTerrainMode.FORCE_CARVE) {
forceCarveStructure(writer, pieces, terrain);
} else if (terrainMode == StructureTerrainMode.BORE) {
boreStructure(writer, pieces, terrain);
} else if (terrainMode != StructureTerrainMode.PRESERVE
&& terrainMode != StructureTerrainMode.SOURCE) {
throw new IllegalStateException("Iris assembly terrain mode " + terrainMode
+ " is not implemented for structure '" + key + "'");
}
ObjectPlaceMode mode = structure.getPlaceMode();
int failedPieces = 0;
IrisStructureStiltSettings stilt = placement.getStilt();
Long2IntOpenHashMap foundationColumns = stilt == null
? null : new Long2IntOpenHashMap();
boolean supportNonOccluding = stilt != null && stilt.isSupportNonOccluding();
if (placement.isAnchoredUnderground()) {
ObjectPlaceMode undergroundMode = (mode == ObjectPlaceMode.ORGANIC_STILT || mode == ObjectPlaceMode.CEILING_HANG)
? mode : ObjectPlaceMode.STRUCTURE_PIECE;
for (PlacedStructurePiece p : pieces) {
if (placeObject(writer, structure, p, undergroundMode, p.getY(), rng,
ObjectPlaceMode mode = structure.getPlaceMode();
int failedPieces = 0;
IrisStructureStiltSettings stilt = placement.getStilt();
Long2IntOpenHashMap foundationColumns = stilt == null
? null : new Long2IntOpenHashMap();
boolean supportNonOccluding = stilt != null && stilt.isSupportNonOccluding();
if (placement.isAnchoredUnderground()) {
ObjectPlaceMode undergroundMode = (mode == ObjectPlaceMode.ORGANIC_STILT || mode == ObjectPlaceMode.CEILING_HANG)
? mode : ObjectPlaceMode.STRUCTURE_PIECE;
for (PlacedStructurePiece p : pieces) {
if (placeObject(writer, structure, p, undergroundMode, p.getY(), rng,
foundationColumns, supportNonOccluding) == -1) {
failedPieces++;
}
}
} else if (mode == ObjectPlaceMode.STRUCTURE_PIECE || mode == ObjectPlaceMode.FLOATING) {
for (PlacedStructurePiece p : pieces) {
if (placeObject(writer, structure, p, ObjectPlaceMode.STRUCTURE_PIECE, p.getY(), rng,
foundationColumns, supportNonOccluding) == -1) {
failedPieces++;
}
}
} else if (pieces.size() == 1) {
if (placeObject(writer, structure, pieces.getFirst(), mode, -1, rng,
foundationColumns, supportNonOccluding) == -1) {
failedPieces++;
}
}
} else if (mode == ObjectPlaceMode.STRUCTURE_PIECE || mode == ObjectPlaceMode.FLOATING) {
for (PlacedStructurePiece p : pieces) {
if (placeObject(writer, structure, p, ObjectPlaceMode.STRUCTURE_PIECE, p.getY(), rng,
foundationColumns, supportNonOccluding) == -1) {
failedPieces++;
} else {
for (PlacedStructurePiece p : pieces) {
if (placeObject(writer, structure, p, ObjectPlaceMode.STRUCTURE_PIECE, p.getY(), rng,
foundationColumns, supportNonOccluding) == -1) {
failedPieces++;
}
}
}
} else if (pieces.size() == 1) {
if (placeObject(writer, structure, pieces.getFirst(), mode, -1, rng,
foundationColumns, supportNonOccluding) == -1) {
failedPieces++;
requireAppliedPieces(resolved, cx, cz, failedPieces);
if (failedPieces == 0 && stilt != null) {
placeFoundation(writer, foundationColumns, stilt, rng, !placement.isAnchoredUnderground());
}
} else {
for (PlacedStructurePiece p : pieces) {
if (placeObject(writer, structure, p, ObjectPlaceMode.STRUCTURE_PIECE, p.getY(), rng,
foundationColumns, supportNonOccluding) == -1) {
failedPieces++;
}
if (failedPieces == 0) {
capture.commit();
}
}
requireAppliedPieces(resolved, cx, cz, failedPieces);
if (failedPieces == 0 && stilt != null) {
placeFoundation(writer, foundationColumns, stilt, rng, !placement.isAnchoredUnderground());
}
}
private void placeFoundation(MantleWriter writer, Long2IntOpenHashMap columns,
@@ -558,11 +557,6 @@ public class IrisStructureComponent extends IrisMantleComponent {
return new int[]{minX, minY, minZ, maxX, maxY, maxZ};
}
private static int saturatedOffset(int coordinate, int offset) {
long result = (long) coordinate + offset;
return (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, result));
}
private int placeObject(MantleWriter writer, IrisStructure structure, PlacedStructurePiece p,
ObjectPlaceMode mode, int y, RNG rng, Long2IntOpenHashMap foundationColumns,
boolean supportNonOccluding) {
@@ -18,6 +18,7 @@
package art.arcane.iris.generation.mantle;
import java.io.UncheckedIOException;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.cache.Cache;
@@ -65,6 +66,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
return;
}
IrisData data = getData();
PlacementSequence placements = new PlacementSequence();
int minX = x << 4;
int minZ = z << 4;
RNG chunkRng = new RNG(Cache.key(x, z) + seed() + 0x0FA710BEL);
@@ -122,7 +124,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
KList<IrisObjectPlacement> floating = entry.getFloatingObjects();
if (floating != null && !floating.isEmpty()) {
for (IrisObjectPlacement placement : floating) {
tryPlaceFloatingChunk(writer, complex, chunkRng, data, placement, columns, minX, minZ, entry);
tryPlaceFloatingChunk(writer, complex, chunkRng, data, placement, columns, minX, minZ, entry, placements);
}
}
@@ -135,12 +137,12 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
interior = interiorColumns(sampleResolver, columns, minX, minZ, entry, IslandObjectPlacer.AnchorFace.TOP);
if (hasSurface) {
for (IrisObjectPlacement placement : surface) {
tryPlaceAnchoredChunk(writer, complex, chunkRng, data, placement, samples, sampleResolver, columns, interior, minX, minZ, entry);
tryPlaceAnchoredChunk(writer, complex, chunkRng, data, placement, samples, sampleResolver, columns, interior, minX, minZ, entry, placements);
}
}
if (hasExtras) {
for (IrisObjectPlacement placement : extras) {
tryPlaceAnchoredChunk(writer, complex, chunkRng, data, placement, samples, sampleResolver, columns, interior, minX, minZ, entry);
tryPlaceAnchoredChunk(writer, complex, chunkRng, data, placement, samples, sampleResolver, columns, interior, minX, minZ, entry, placements);
}
}
}
@@ -159,14 +161,14 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
if (bottom != null && !bottom.isEmpty()) {
KList<Integer> interior = interiorColumns(sampleResolver, columns, minX, minZ, entry, IslandObjectPlacer.AnchorFace.BOTTOM);
for (IrisObjectPlacement placement : bottom) {
tryPlaceInvertedChunk(writer, complex, chunkRng, data, placement, samples, sampleResolver, columns, interior, minX, minZ, entry);
tryPlaceInvertedChunk(writer, complex, chunkRng, data, placement, samples, sampleResolver, columns, interior, minX, minZ, entry, placements);
}
}
}
}
@ChunkCoordinates
private void tryPlaceFloatingChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, KList<Integer> columns, int minX, int minZ, IrisFloatingChildBiomes entry) {
private void tryPlaceFloatingChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, KList<Integer> columns, int minX, int minZ, IrisFloatingChildBiomes entry, PlacementSequence placements) {
if (placement == null || columns == null || columns.isEmpty()) {
return;
}
@@ -210,21 +212,32 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
}
int id = rng.i(0, Integer.MAX_VALUE);
FloatingObjectPlacementTransaction transaction = new FloatingObjectPlacementTransaction(
writer, placements.next(minX >> 4, minZ >> 4));
try {
obj.place(xx, -1, zz, writer, floatingPlacement, rng, (b, bd) -> {
int resultY = obj.place(xx, -1, zz, transaction, floatingPlacement, rng, (b, bd) -> {
String marker = placementMarker(obj, id);
if (marker != null && shouldWritePlacementMarker(writer, bd, b.getX(), b.getY(), b.getZ())) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
if (marker != null && shouldWritePlacementMarker(transaction, bd, b.getX(), b.getY(), b.getZ())) {
transaction.setData(b.getX(), b.getY(), b.getZ(), marker);
}
}, null, data);
if (resultY < 0) {
transaction.discard();
} else {
transaction.commit();
}
} catch (UncheckedIOException failure) {
transaction.discard();
throw failure;
} catch (Throwable e) {
transaction.discard();
IrisLogging.reportError(e);
}
}
}
@ChunkCoordinates
private void tryPlaceAnchoredChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, FloatingIslandSample[] samples, IslandObjectPlacer.SampleProvider sampleProvider, KList<Integer> columns, KList<Integer> interior, int minX, int minZ, IrisFloatingChildBiomes entry) {
private void tryPlaceAnchoredChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, FloatingIslandSample[] samples, IslandObjectPlacer.SampleProvider sampleProvider, KList<Integer> columns, KList<Integer> interior, int minX, int minZ, IrisFloatingChildBiomes entry, PlacementSequence placements) {
if (placement == null || columns.isEmpty()) {
return;
}
@@ -293,7 +306,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
int yv = pickTopY + 1 - fp.getLowestSolidKeyY();
IslandObjectPlacer islandPlacer = IslandObjectPlacer.top(writer, sampleProvider, entry, pickTopY);
FloatingObjectPlacementTransaction transaction = new FloatingObjectPlacementTransaction(islandPlacer);
FloatingObjectPlacementTransaction transaction = new FloatingObjectPlacementTransaction(islandPlacer, placements.next(minX >> 4, minZ >> 4));
int id = rng.i(0, Integer.MAX_VALUE);
try {
@@ -308,6 +321,9 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
} else {
transaction.commit();
}
} catch (UncheckedIOException failure) {
transaction.discard();
throw failure;
} catch (Throwable e) {
transaction.discard();
IrisLogging.reportError(e);
@@ -316,7 +332,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
}
@ChunkCoordinates
private void tryPlaceInvertedChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, FloatingIslandSample[] samples, IslandObjectPlacer.SampleProvider sampleProvider, KList<Integer> columns, KList<Integer> interior, int minX, int minZ, IrisFloatingChildBiomes entry) {
private void tryPlaceInvertedChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, FloatingIslandSample[] samples, IslandObjectPlacer.SampleProvider sampleProvider, KList<Integer> columns, KList<Integer> interior, int minX, int minZ, IrisFloatingChildBiomes entry, PlacementSequence placements) {
if (placement == null || columns.isEmpty()) {
return;
}
@@ -402,7 +418,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
int yv = invertedBaseY(pickBottomY, fp, invertedRotation);
IslandObjectPlacer islandPlacer = IslandObjectPlacer.bottom(writer, sampleProvider, entry, pickBottomY);
FloatingObjectPlacementTransaction transaction = new FloatingObjectPlacementTransaction(islandPlacer);
FloatingObjectPlacementTransaction transaction = new FloatingObjectPlacementTransaction(islandPlacer, placements.next(minX >> 4, minZ >> 4));
int id = rng.i(0, Integer.MAX_VALUE);
try {
@@ -417,6 +433,9 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
} else {
transaction.commit();
}
} catch (UncheckedIOException failure) {
transaction.discard();
throw failure;
} catch (Throwable e) {
transaction.discard();
IrisLogging.reportError(e);
@@ -636,4 +655,12 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
}
return radius;
}
private static final class PlacementSequence {
private int ordinal;
private ObjectContinuationBundle.PlacementKey next(int sourceX, int sourceZ) {
return new ObjectContinuationBundle.PlacementKey(ObjectContinuationBundle.Kind.FLOATING, sourceX, sourceZ, ordinal++);
}
}
}
@@ -23,9 +23,6 @@ import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.generation.block.B;
import art.arcane.iris.generation.cache.Cache;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import art.arcane.iris.world.history.BoundaryColumnGeometry;
import java.util.Optional;
import art.arcane.iris.generation.runtime.UpperDimensionContext;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
@@ -145,11 +142,12 @@ public class MantleObjectComponent extends IrisMantleComponent {
x,
z,
getRadius(),
(sourceX, sourceZ) -> transaction.apply(sourcePlans.get(
sourceX,
sourceZ,
() -> buildSourcePlan(writer, sourceX, sourceZ, context)
))
(sourceX, sourceZ) -> {
ObjectSourcePlan plan = sourcePlans.get(sourceX, sourceZ,
() -> buildSourcePlan(writer, sourceX, sourceZ, context));
transaction.apply(plan);
plan.persistContinuations(writer, sourceX, sourceZ, x, z);
}
);
transaction.commit();
}
@@ -668,6 +666,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
try {
int placeResult = -1;
CaveObjectPlacementTransaction.CommitResult commitResult = CaveObjectPlacementTransaction.CommitResult.EMPTY;
int continuation = writer.beginObjectPlacement();
if (carving) {
int caveFloorY = caveAnchor.y();
if (golden) {
@@ -707,6 +706,9 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
}, null, getData());
}
if (placeResult >= 0 && (!carving || commitResult == CaveObjectPlacementTransaction.CommitResult.COMMITTED)) {
writer.endObjectPlacement(continuation);
}
if (golden) {
IrisLogging.debug("Goldendebug procedural result: chunk=" + x + "," + z
+ " placement=" + p.getName()
@@ -778,19 +780,10 @@ public class MantleObjectComponent extends IrisMantleComponent {
private boolean acceptsCaveAnchorAt(int x, int y, int z, boolean underwater,
MatterCavern cavern, HydrologyCaveCell hydrology) {
Optional<TerrainBoundarySignature> resolved = getEngineMantle().getComplex().resolvedTerrainColumn(x, z);
if (resolved.isPresent()) {
return acceptsResolvedCaveAnchor(underwater,
resolved.get().geometry().voxelAt(y + getEngineMantle().getEngine().getMinHeight()));
}
return acceptsCaveAnchorFluid(underwater, hydrology == null ? cavern : hydrology.asCavern(),
hydrology, y, getDimension().getCaveLavaHeight());
}
static boolean acceptsResolvedCaveAnchor(boolean underwater, BoundaryColumnGeometry.Voxel voxel) {
return !voxel.protectedContent() && (voxel.phase() == BoundaryColumnGeometry.Phase.AIR
|| underwater && voxel.phase() == BoundaryColumnGeometry.Phase.FLUID);
}
static boolean acceptsCaveAnchorFluid(boolean underwater, MatterCavern cavern, int y, int lavaHeight) {
return acceptsCaveAnchorFluid(underwater, cavern, null, y, lavaHeight);
@@ -914,6 +907,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
try {
String marker = placementMarker(v, id, "surface");
int continuation = writer.beginObjectPlacement();
int result = v.place(xx, -1, zz, placePlacer, effectivePlacement, rng, (b, data) -> {
if (marker != null) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
@@ -927,6 +921,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
}, null, getData());
if (result >= 0) {
writer.endObjectPlacement(continuation);
placed++;
} else {
rejected++;
@@ -1096,6 +1091,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
IrisObjectPlacement effectivePlacement = resolveCavePlacement(objectPlacement, object, caveProfile);
try {
int continuation = writer.beginObjectPlacement();
ContainedPlacementResult contained = placeContainedCaveObject(
writer,
object,
@@ -1112,6 +1108,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
int result = contained.resultY();
boolean wroteBlocks = contained.commitResult() == CaveObjectPlacementTransaction.CommitResult.COMMITTED;
if (wroteBlocks) {
writer.endObjectPlacement(continuation);
placed++;
} else {
rejected++;
@@ -1286,7 +1283,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
boolean treePlacement = isTreePlacement(v, objectPlacement);
String marker = placementMarker(v, id, "upper");
int continuation = writer.beginObjectPlacement();
int result = v.place(xx, anchorY, zz, writer, placement, rng, (b, data) -> {
if (marker != null) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
@@ -1299,6 +1296,9 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
}, null, getData());
if (result >= 0) {
writer.endObjectPlacement(continuation);
}
if (traceRegen) {
IrisLogging.debug("Upper object placement: chunk=" + chunkX + "," + chunkZ
+ " scope=" + scope
@@ -32,9 +32,6 @@ import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.terrain.Terrain3DColumn;
import art.arcane.iris.world.history.TransitionGenerationPlan;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import art.arcane.iris.world.history.BoundaryColumnGeometry;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.pack.value.IrisPosition;
import art.arcane.iris.generation.block.TileData;
@@ -46,6 +43,7 @@ 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.IrisMatter;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.matter.MatterSlice;
import art.arcane.volmlib.util.noise.CNG;
@@ -61,9 +59,13 @@ import art.arcane.iris.generation.block.B;
import org.bukkit.util.Vector;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.function.Supplier;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import java.util.Map;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReferenceArray;
@@ -86,6 +88,8 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
@EqualsAndHashCode.Exclude
@ToString.Exclude
private final ThreadLocal<Integer> activeComponentPriority = new ThreadLocal<>();
@Getter(AccessLevel.NONE)
private final ThreadLocal<ObjectPlacementCapture> objectPlacementCapture = new ThreadLocal<>();
public MantleWriter(EngineMantle engineMantle, Mantle<Matter> mantle, int x, int z, int radius, boolean multicore) {
this(engineMantle, mantle, x, z, radius, radius * 2, multicore);
@@ -292,6 +296,22 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
(int) Math.round(style.warp(rng, data, z, -y, z, x)), t);
}
public ObjectPlacementCapture captureObjectPlacement(ObjectContinuationBundle.PlacementKey key) {
if (objectPlacementCapture.get() != null) {
throw new IllegalStateException("Object placement capture is already active");
}
ObjectPlacementCapture capture = new ObjectPlacementCapture(key);
objectPlacementCapture.set(capture);
return capture;
}
private void recordObjectValue(int x, int y, int z, Class<?> type, Object value) {
ObjectPlacementCapture capture = objectPlacementCapture.get();
if (capture != null) {
capture.set(x, y, z, type, value);
}
}
public <T> void setData(int x, int y, int z, T t) {
if (t == null || !allowsWrite(x, z)) {
return;
@@ -323,6 +343,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
Class<?> sliceType = t instanceof NativeBlockState ? NativeBlockState.class : matter.getClass(t);
capturePreObjectOriginal(matter, x, y, z, sliceType);
matter.slice(sliceType).set(x & 15, y & 15, z & 15, t);
recordObjectValue(x, y, z, sliceType, t);
}
}
@@ -361,6 +382,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
cavernSlice = matter.slice(MatterCavern.class);
}
cavernSlice.set(x & 15, y & 15, z & 15, value);
recordObjectValue(x, y, z, MatterCavern.class, value);
return true;
}
}
@@ -384,6 +406,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
if (blockSlice != null) {
capturePreObjectOriginal(matter, x, y, z, NativeBlockState.class);
blockSlice.set(x & 15, y & 15, z & 15, null);
recordObjectValue(x, y, z, NativeBlockState.class, AIR.get());
}
clearDeferredPlacement(matter, x, y, z);
MatterSlice<MatterCavern> cavernSlice = matter.getSlice(MatterCavern.class);
@@ -395,6 +418,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
cavernSlice = matter.slice(MatterCavern.class);
}
cavernSlice.set(x & 15, y & 15, z & 15, value);
recordObjectValue(x, y, z, MatterCavern.class, value);
return true;
}
}
@@ -413,14 +437,13 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
if (hasProtectedHydrology(matter, x, y, z)) {
return;
}
MatterSlice<NativeBlockState> blockSlice = matter.getSlice(NativeBlockState.class);
if (blockSlice != null) {
capturePreObjectOriginal(matter, x, y, z, NativeBlockState.class);
blockSlice.set(x & 15, y & 15, z & 15, null);
}
capturePreObjectOriginal(matter, x, y, z, NativeBlockState.class);
matter.<NativeBlockState>slice(NativeBlockState.class).set(x & 15, y & 15, z & 15, AIR.get());
clearDeferredPlacement(matter, x, y, z);
capturePreObjectOriginal(matter, x, y, z, MatterCavern.class);
matter.<MatterCavern>slice(MatterCavern.class).set(x & 15, y & 15, z & 15, value);
recordObjectValue(x, y, z, NativeBlockState.class, AIR.get());
recordObjectValue(x, y, z, MatterCavern.class, value);
}
}
@@ -448,6 +471,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
if (blockSlice != null) {
capturePreObjectOriginal(matter, x, y, z, NativeBlockState.class);
blockSlice.set(x & 15, y & 15, z & 15, null);
recordObjectValue(x, y, z, NativeBlockState.class, AIR.get());
}
clearDeferredPlacement(matter, x, y, z);
}
@@ -517,34 +541,14 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
}
}
private Optional<TerrainBoundarySignature> resolvedColumn(int x, int z) {
IrisComplex complex = engineMantle.getComplex();
return complex == null ? Optional.empty() : complex.resolvedTerrainColumn(x, z);
}
private NativeBlockState resolvedBlock(TerrainBoundarySignature column, int y) {
String stateKey = column.geometry().voxelAt(y + engineMantle.getEngine().getMinHeight()).stateKey();
NativeBlockState state = IrisPlatforms.get().registries().blockOrNull(stateKey);
if (state == null) {
throw new IllegalStateException("Saved terrain state is unavailable: " + stateKey);
}
return state;
}
public NativeBlockState getPrerequisiteBlock(int x, int y, int z) {
Optional<TerrainBoundarySignature> resolved = resolvedColumn(x, z);
if (resolved.isPresent()) {
return resolvedBlock(resolved.get(), y);
}
NativeBlockState block = getPrerequisiteDataIfPresent(x, y, z, NativeBlockState.class);
return block == null ? AIR.get() : block;
}
public boolean isPrerequisiteCarved(int x, int y, int z) {
Optional<TerrainBoundarySignature> resolved = resolvedColumn(x, z);
if (resolved.isPresent()) {
return resolved.get().geometry().isEnclosedOpenAt(y + engineMantle.getEngine().getMinHeight());
}
HydrologyCaveCell hydrology = getPrerequisiteDataIfPresent(x, y, z, HydrologyCaveCell.class);
if (hydrology != null) {
return hydrology.carves();
@@ -554,10 +558,6 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
}
public byte[] getPrerequisiteCarvedColumn(int x, int z, int height) {
Optional<TerrainBoundarySignature> resolved = resolvedColumn(x, z);
if (resolved.isPresent()) {
return resolvedCarvedColumn(resolved.get(), height);
}
int cappedHeight = Math.min(Math.max(height, 0), mantle.getWorldHeight());
byte[] carvedColumn = new byte[cappedHeight];
if (cappedHeight <= 0) {
@@ -646,6 +646,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
return false;
}
restoreRaw(matter, x, y, z, type, cell.original(type));
recordObjectValue(x, y, z, type, cell.original(type));
return true;
}
}
@@ -672,12 +673,15 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
}
if (cell.blockCaptured()) {
restoreRaw(matter, x, y, z, NativeBlockState.class, cell.block());
recordObjectValue(x, y, z, NativeBlockState.class, cell.block());
}
if (cell.stringCaptured()) {
restoreRaw(matter, x, y, z, String.class, cell.string());
recordObjectValue(x, y, z, String.class, cell.string());
}
if (cell.cavernCaptured()) {
restoreRaw(matter, x, y, z, MatterCavern.class, cell.cavern());
recordObjectValue(x, y, z, MatterCavern.class, cell.cavern());
}
return true;
}
@@ -705,6 +709,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
}
capturePreObjectOriginal(matter, x, y, z, type);
matter.getSlice(type).set(x & 15, y & 15, z & 15, null);
recordObjectValue(x, y, z, type, null);
}
}
@@ -747,6 +752,8 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
capturePreObjectOriginal(matter, x, y, z, NativeBlockState.class);
blockSlice.set(x & 15, y & 15, z & 15, baseState);
identifierSlice.set(x & 15, y & 15, z & 15, identifier);
recordObjectValue(x, y, z, NativeBlockState.class, baseState);
recordObjectValue(x, y, z, Identifier.class, identifier);
}
}
@@ -905,13 +912,6 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
// Read-only probe: getDataIfPresent returns the identical answer without materializing
// a 16^3 section + slice on a miss the way getData's getOrCreate path does.
NativeBlockState block = getDataIfPresent(x, y, z, NativeBlockState.class);
Optional<TerrainBoundarySignature> resolved = resolvedColumn(x, z);
if (resolved.isPresent()) {
PreObjectMatterCell cell = getDataIfPresent(x, y, z, PreObjectMatterCell.class);
if (cell == null || !cell.blockCaptured()) {
return resolvedBlock(resolved.get(), y);
}
}
if (block == null)
return AIR.get();
return block;
@@ -924,20 +924,6 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
@Override
public boolean isCarved(int x, int y, int z) {
Optional<TerrainBoundarySignature> resolved = resolvedColumn(x, z);
if (resolved.isPresent()) {
PreObjectMatterCell cell = getDataIfPresent(x, y, z, PreObjectMatterCell.class);
if (cell != null && cell.blockCaptured()) {
NativeBlockState block = getDataIfPresent(x, y, z, NativeBlockState.class);
if (block != null) {
return (block.isAir() || block.isFluid())
&& resolved.get().geometry().hasSolidAbove(y + engineMantle.getEngine().getMinHeight());
}
}
return cell != null && cell.cavernCaptured()
? getDataIfPresent(x, y, z, MatterCavern.class) != null
: resolved.get().geometry().isEnclosedOpenAt(y + engineMantle.getEngine().getMinHeight());
}
HydrologyCaveCell hydrology = getDataIfPresent(x, y, z, HydrologyCaveCell.class);
if (hydrology != null) {
return hydrology.carves();
@@ -946,45 +932,8 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|| engineMantle.getComplex().isTerrain3DOpening(x, y, z);
}
private byte[] resolvedCarvedColumn(TerrainBoundarySignature column, int height) {
int cappedHeight = Math.min(Math.max(height, 0), mantle.getWorldHeight());
byte[] carved = new byte[cappedHeight];
int minimumY = engineMantle.getEngine().getMinHeight();
BoundaryColumnGeometry geometry = column.geometry();
boolean ceiling = false;
for (int worldY = geometry.minimumY() + geometry.height() - 1; worldY >= minimumY; worldY--) {
BoundaryColumnGeometry.Voxel voxel = geometry.voxelAt(worldY);
int internalY = worldY - minimumY;
if (voxel.phase() == BoundaryColumnGeometry.Phase.SOLID) {
ceiling = true;
} else if (ceiling && internalY < cappedHeight) {
carved[internalY] = 1;
}
}
return carved;
}
public byte[] getCarvedColumn(int x, int z, int height) {
Optional<TerrainBoundarySignature> resolved = resolvedColumn(x, z);
if (resolved.isPresent()) {
byte[] carved = resolvedCarvedColumn(resolved.get(), height);
for (int y = 0; y < carved.length; y++) {
PreObjectMatterCell cell = getDataIfPresent(x, y, z, PreObjectMatterCell.class);
if (cell != null && cell.blockCaptured()) {
NativeBlockState block = getDataIfPresent(x, y, z, NativeBlockState.class);
if (block != null) {
carved[y] = (block.isAir() || block.isFluid())
&& resolved.get().geometry().hasSolidAbove(y + engineMantle.getEngine().getMinHeight())
? (byte) 1 : 0;
continue;
}
}
if (cell != null && cell.cavernCaptured()) {
carved[y] = getDataIfPresent(x, y, z, MatterCavern.class) != null ? (byte) 1 : 0;
}
}
return carved;
}
int cappedHeight = Math.min(Math.max(height, 0), mantle.getWorldHeight());
byte[] carvedColumn = new byte[cappedHeight];
if (cappedHeight <= 0) {
@@ -1045,11 +994,6 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
@Override
public boolean isSurfaceSolid(int x, int y, int z) {
Optional<TerrainBoundarySignature> resolved = resolvedColumn(x, z);
if (resolved.isPresent()) {
return resolved.get().geometry()
.voxelAt(y + engineMantle.getEngine().getMinHeight()).phase() == BoundaryColumnGeometry.Phase.SOLID;
}
return engineMantle.getEngine().isTerrainSurfaceSolid(x, y, z);
}
@@ -1564,4 +1508,64 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
}
}
}
public final class ObjectPlacementCapture implements AutoCloseable {
private final ObjectContinuationBundle.PlacementKey key;
private final Long2ObjectOpenHashMap<Matter> fragments = new Long2ObjectOpenHashMap<>();
private int minimumX = Integer.MAX_VALUE;
private int minimumZ = Integer.MAX_VALUE;
private int maximumX = Integer.MIN_VALUE;
private int maximumZ = Integer.MIN_VALUE;
private boolean closed;
private ObjectPlacementCapture(ObjectContinuationBundle.PlacementKey key) {
this.key = Objects.requireNonNull(key);
}
public void commit() {
close();
if (fragments.size() > 1) {
Map<ObjectContinuationBundle.ChunkPosition, Supplier<Matter>> pending = new LinkedHashMap<>();
for (Long2ObjectMap.Entry<Matter> entry : fragments.long2ObjectEntrySet()) {
long coordinate = entry.getLongKey();
Matter matter = entry.getValue();
pending.put(new ObjectContinuationBundle.ChunkPosition((int) (coordinate >> 32), (int) coordinate), () -> matter);
}
ObjectContinuationPersistence.persist(mantle, key,
new ObjectContinuationBundle.Bounds(minimumX, minimumZ, maximumX, maximumZ), pending);
}
}
@Override
public void close() {
if (closed) {
return;
}
if (objectPlacementCapture.get() != this) {
throw new IllegalStateException("Object placement capture closed outside its owner");
}
objectPlacementCapture.remove();
closed = true;
}
private void set(int x, int y, int z, Class<?> type, Object value) {
long coordinate = ((long) (x >> 4) << 32) | ((z >> 4) & 0xffffffffL);
Matter fragment = fragments.get(coordinate);
if (fragment == null) {
fragment = new IrisMatter(16, mantle.getWorldHeight(), 16);
fragments.put(coordinate, fragment);
}
minimumX = Math.min(minimumX, x);
minimumZ = Math.min(minimumZ, z);
maximumX = Math.max(maximumX, x);
maximumZ = Math.max(maximumZ, z);
if (type == NativeBlockState.class) {
fragment.slice(Identifier.class).set(x & 15, y, z & 15, null);
fragment.slice(type).set(x & 15, y, z & 15, value == null ? AIR.get() : value);
} else {
fragment.slice(type).set(x & 15, y, z & 15, value);
}
}
}
}
@@ -0,0 +1,178 @@
package art.arcane.iris.generation.mantle;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.IrisMatter;
import art.arcane.volmlib.util.matter.MatterSlice;
import art.arcane.volmlib.util.io.CountingDataInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.UncheckedIOException;
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Objects;
public record ObjectContinuationBundle(List<Fragment> fragments) {
private static final int MAX_ENTRIES = 1_048_576;
private static final int MAX_PAYLOAD_BYTES = 64 * 1024 * 1024;
public ObjectContinuationBundle {
fragments = List.copyOf(fragments);
}
public ObjectContinuationBundle with(Fragment fragment) {
LinkedHashMap<PlacementKey, Fragment> merged = new LinkedHashMap<>();
for (Fragment existing : fragments) {
merged.put(existing.key(), existing);
}
merged.put(fragment.key(), fragment);
return new ObjectContinuationBundle(new ArrayList<>(merged.values()));
}
public void write(DataOutputStream output) throws IOException {
output.writeInt(fragments.size());
for (Fragment fragment : fragments) {
output.writeUTF(fragment.key().kind().name());
output.writeInt(fragment.key().sourceChunkX());
output.writeInt(fragment.key().sourceChunkZ());
output.writeInt(fragment.key().ordinal());
output.writeInt(fragment.bounds().minimumX());
output.writeInt(fragment.bounds().minimumZ());
output.writeInt(fragment.bounds().maximumX());
output.writeInt(fragment.bounds().maximumZ());
output.writeInt(fragment.touchedChunks().size());
for (ChunkPosition chunk : fragment.touchedChunks()) {
output.writeInt(chunk.x());
output.writeInt(chunk.z());
}
output.writeInt(fragment.payload.length);
output.write(fragment.payload);
}
}
public static ObjectContinuationBundle read(DataInputStream input) throws IOException {
int count = checkedCount(input.readInt(), MAX_ENTRIES);
ArrayList<Fragment> fragments = new ArrayList<>(count);
for (int index = 0; index < count; index++) {
PlacementKey key = new PlacementKey(Kind.valueOf(input.readUTF()), input.readInt(), input.readInt(), input.readInt());
Bounds bounds = new Bounds(input.readInt(), input.readInt(), input.readInt(), input.readInt());
int touchedCount = checkedCount(input.readInt(), MAX_ENTRIES);
ArrayList<ChunkPosition> touched = new ArrayList<>(touchedCount);
for (int chunk = 0; chunk < touchedCount; chunk++) {
touched.add(new ChunkPosition(input.readInt(), input.readInt()));
}
int length = checkedCount(input.readInt(), MAX_PAYLOAD_BYTES);
byte[] payload = new byte[length];
input.readFully(payload);
fragments.add(new Fragment(key, bounds, touched, payload));
}
return new ObjectContinuationBundle(fragments);
}
public static byte[] encode(Matter payload) {
IrisMatterSupport.ensureRegistered();
try {
ByteArrayOutputStream output = new ByteArrayOutputStream();
payload.write(output);
return output.toByteArray();
} catch (IOException failure) {
throw new UncheckedIOException("Unable to encode object continuation", failure);
}
}
private static int checkedCount(int count, int maximum) throws IOException {
if (count < 0 || count > maximum) {
throw new IOException("Invalid object continuation size: " + count);
}
return count;
}
public enum Kind {
BIOME, FLOATING, STRUCTURE, STATIC
}
public record PlacementKey(Kind kind, int sourceChunkX, int sourceChunkZ, int ordinal) {
public PlacementKey {
Objects.requireNonNull(kind, "placement kind");
if (ordinal < 0) {
throw new IllegalArgumentException("Placement ordinal must be nonnegative");
}
}
}
public record ChunkPosition(int x, int z) {
}
public record Bounds(int minimumX, int minimumZ, int maximumX, int maximumZ) {
public Bounds {
if (minimumX > maximumX || minimumZ > maximumZ) {
throw new IllegalArgumentException("Object continuation bounds are inverted");
}
}
}
public record Fragment(PlacementKey key, Bounds bounds, List<ChunkPosition> touchedChunks, byte[] payload) {
public Fragment {
Objects.requireNonNull(key, "placement key");
Objects.requireNonNull(bounds, "placement bounds");
touchedChunks = List.copyOf(touchedChunks);
if (touchedChunks.size() < 2 || payload.length == 0 || payload.length > MAX_PAYLOAD_BYTES) {
throw new IllegalArgumentException("Object continuation must span chunks with a bounded payload");
}
payload = payload.clone();
}
@Override
public byte[] payload() {
return payload.clone();
}
public int encodedSize() {
return payload.length;
}
public Matter decode() throws IOException {
try (CountingDataInputStream input = CountingDataInputStream.wrap(new ByteArrayInputStream(payload))) {
int width = input.readInt();
int height = input.readInt();
int depth = input.readInt();
if (width != 16 || depth != 16 || height < 1 || height > 4096) {
throw new IOException("Invalid object continuation dimensions");
}
Matter matter = new IrisMatter(width, height, depth);
int sliceCount = input.readUnsignedByte();
matter.getHeader().read(input);
for (int index = 0; index < sliceCount; index++) {
int size = input.readInt();
long end = input.count() + size;
if (size <= 0 || end > payload.length) {
throw new IOException("Invalid object continuation slice size");
}
String identifier = input.readUTF();
Class<?> type = IrisMatter.getSliceType(identifier);
if (type == null || matter.hasSlice(type)) {
throw new IOException("Unavailable or duplicate object continuation slice: " + identifier);
}
MatterSlice<?> slice = matter.createSlice(type, matter);
if (slice == null) {
throw new IOException("Unavailable object continuation codec: " + identifier);
}
slice.read(input);
if (input.count() != end) {
throw new IOException("Object continuation slice size mismatch: " + identifier);
}
matter.putSlice(type, slice);
}
if (input.count() != payload.length) {
throw new IOException("Trailing object continuation bytes");
}
return matter;
}
}
}
}
@@ -0,0 +1,77 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.integration.Identifier;
import art.arcane.iris.world.storage.matter.TileWrapper;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
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 java.util.List;
import java.util.Map;
import java.util.function.Supplier;
public final class ObjectContinuationPersistence {
private ObjectContinuationPersistence() {
}
public static void persist(Mantle<Matter> mantle, ObjectContinuationBundle.PlacementKey key,
ObjectContinuationBundle.Bounds bounds,
Map<ObjectContinuationBundle.ChunkPosition, Supplier<Matter>> destinations) {
if (destinations.size() < 2) {
return;
}
List<ObjectContinuationBundle.ChunkPosition> touched = List.copyOf(destinations.keySet());
for (Map.Entry<ObjectContinuationBundle.ChunkPosition, Supplier<Matter>> entry : destinations.entrySet()) {
ObjectContinuationBundle.ChunkPosition destination = entry.getKey();
MantleChunk<Matter> chunk = mantle.getChunk(destination.x(), destination.z()).use();
try {
synchronized (chunk) {
if (chunk.isFlagged(MantleFlag.REAL)) {
continue;
}
Matter section = chunk.getOrCreate(0);
ObjectContinuationBundle bundle = section.hasSlice(ObjectContinuationBundle.class)
? section.<ObjectContinuationBundle>getSlice(ObjectContinuationBundle.class).get(0, 0, 0) : null;
if (contains(bundle, key)) {
continue;
}
ObjectContinuationBundle.Fragment fragment = new ObjectContinuationBundle.Fragment(
key, bounds, touched, ObjectContinuationBundle.encode(entry.getValue().get()));
section.slice(ObjectContinuationBundle.class).set(0, 0, 0,
bundle == null ? new ObjectContinuationBundle(List.of(fragment)) : bundle.with(fragment));
}
} finally {
chunk.release();
}
}
}
public static void put(Matter payload, int x, int y, int z, Object value) {
if (value instanceof NativeBlockState state) {
String customKey = state.deferredPlacementKey();
NativeBlockState base = state.placementBaseState();
boolean custom = state.isCustom() && customKey != null && base != null;
payload.slice(NativeBlockState.class).set(x, y, z, custom ? base : state);
payload.slice(Identifier.class).set(x, y, z, custom ? Identifier.fromString(customKey) : null);
} else if (value instanceof TileData tile) {
payload.slice(TileWrapper.class).set(x, y, z, new TileWrapper(tile));
} else {
payload.slice(payload.getClass(value)).set(x, y, z, value);
}
}
private static boolean contains(ObjectContinuationBundle bundle, ObjectContinuationBundle.PlacementKey key) {
if (bundle == null) {
return false;
}
for (ObjectContinuationBundle.Fragment fragment : bundle.fragments()) {
if (fragment.key().equals(key)) {
return true;
}
}
return false;
}
}
@@ -30,6 +30,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
private final int destinationChunkZ;
private final int worldHeight;
private final List<Mutation> mutations;
private final List<PlacementRange> placements = new ArrayList<>();
private final Long2ObjectOpenHashMap<Int2ObjectOpenHashMap<OverlayCell>> overlay;
ObjectDestinationTransaction(MantleWriter writer, int destinationChunkX, int destinationChunkZ) {
@@ -53,7 +54,25 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
if (checkpoint < 0 || checkpoint > mutations.size()) {
throw new IllegalArgumentException("Mutation checkpoint is outside the transaction");
}
return new ObjectSourcePlan(mutations.subList(checkpoint, mutations.size()));
ArrayList<List<Mutation>> accepted = new ArrayList<>();
for (PlacementRange placement : placements) {
if (placement.start() >= checkpoint) {
accepted.add(List.copyOf(mutations.subList(placement.start(), placement.end())));
}
}
return new ObjectSourcePlan(mutations.subList(checkpoint, mutations.size()), accepted);
}
@Override
public int beginObjectPlacement() {
return mutations.size();
}
@Override
public void endObjectPlacement(int checkpoint) {
if (checkpoint >= 0 && checkpoint < mutations.size()) {
placements.add(new PlacementRange(checkpoint, mutations.size()));
}
}
void apply(ObjectSourcePlan plan) {
@@ -413,6 +432,9 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
}
}
private record PlacementRange(int start, int end) {
}
record DataKey(int x, int y, int z, Class<?> type) {
}
@@ -8,6 +8,13 @@ public interface ObjectPassPlacer extends IObjectPlacer {
byte[] getCarvedColumn(int x, int z, int height);
default int beginObjectPlacement() {
return -1;
}
default void endObjectPlacement(int checkpoint) {
}
@Override
default <T> @Nullable T getData(int x, int y, int z, Class<T> type) {
return getDataIfPresent(x, y, z, type);
@@ -3,15 +3,39 @@ package art.arcane.iris.generation.mantle;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import art.arcane.iris.integration.Identifier;
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.IrisMatter;
import art.arcane.volmlib.util.matter.Matter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.List;
final class ObjectSourcePlan {
private static final int DESTINATION_INDEX_WEIGHT = 8;
private final Long2ObjectOpenHashMap<List<ObjectDestinationTransaction.Mutation>> destinations;
private final int mutationWeight;
private final List<Placement> placements;
ObjectSourcePlan(List<ObjectDestinationTransaction.Mutation> mutations) {
this(mutations, List.of());
}
ObjectSourcePlan(List<ObjectDestinationTransaction.Mutation> mutations, List<List<ObjectDestinationTransaction.Mutation>> placements) {
ArrayList<Placement> crossing = new ArrayList<>();
for (int ordinal = 0; ordinal < placements.size(); ordinal++) {
List<ObjectDestinationTransaction.Mutation> placement = placements.get(ordinal);
LinkedHashMap<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> fragments = group(placement);
if (fragments.size() > 1) {
crossing.add(new Placement(ordinal, bounds(placement), List.copyOf(fragments.keySet()), fragments));
}
}
this.placements = List.copyOf(crossing);
Long2ObjectOpenHashMap<ArrayList<ObjectDestinationTransaction.Mutation>> grouped = new Long2ObjectOpenHashMap<>(2);
long weight = 1L;
for (ObjectDestinationTransaction.Mutation mutation : mutations) {
@@ -29,6 +53,12 @@ final class ObjectSourcePlan {
for (Long2ObjectMap.Entry<ArrayList<ObjectDestinationTransaction.Mutation>> entry : grouped.long2ObjectEntrySet()) {
destinations.put(entry.getLongKey(), List.copyOf(entry.getValue()));
}
for (Placement placement : this.placements) {
weight += 8L + placement.touched().size() * DESTINATION_INDEX_WEIGHT;
for (List<ObjectDestinationTransaction.Mutation> fragment : placement.fragments().values()) {
weight += fragment.size();
}
}
this.mutationWeight = weight >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) weight;
}
@@ -37,10 +67,95 @@ final class ObjectSourcePlan {
return mutations == null ? List.of() : mutations;
}
void persistContinuations(MantleWriter writer, int sourceX, int sourceZ, int destinationX, int destinationZ) {
ObjectContinuationBundle.ChunkPosition destination = new ObjectContinuationBundle.ChunkPosition(destinationX, destinationZ);
for (Placement placement : placements) {
if (!placement.fragments().containsKey(destination)) {
continue;
}
ObjectContinuationBundle.PlacementKey key = new ObjectContinuationBundle.PlacementKey(ObjectContinuationBundle.Kind.BIOME, sourceX, sourceZ, placement.ordinal());
for (Map.Entry<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> entry : placement.fragments().entrySet()) {
ObjectContinuationBundle.ChunkPosition target = entry.getKey();
if (target.equals(destination)) {
continue;
}
writer.withChunkFence(target.x(), target.z(), () -> {
MantleChunk<Matter> chunk = writer.acquireChunk(target.x(), target.z());
if (chunk.isFlagged(MantleFlag.REAL)) {
return;
}
int x = target.x() << 4;
int z = target.z() << 4;
ObjectContinuationBundle bundle = writer.getDataIfPresent(x, 0, z, ObjectContinuationBundle.class);
if (bundle != null) {
for (ObjectContinuationBundle.Fragment existing : bundle.fragments()) {
if (existing.key().equals(key)) {
return;
}
}
}
ObjectContinuationBundle.Fragment fragment = new ObjectContinuationBundle.Fragment(key, placement.bounds(), placement.touched(),
encode(entry.getValue(), writer.getMantle().getWorldHeight()));
writer.setData(x, 0, z, bundle == null ? new ObjectContinuationBundle(List.of(fragment)) : bundle.with(fragment));
});
}
}
}
static byte[] encode(List<ObjectDestinationTransaction.Mutation> mutations, int height) {
IrisMatterSupport.ensureRegistered();
Matter payload = new IrisMatter(16, height, 16);
for (ObjectDestinationTransaction.Mutation mutation : mutations) {
ObjectDestinationTransaction.DataKey key = mutation.key();
int x = key.x() & 15;
int z = key.z() & 15;
if (mutation instanceof ObjectDestinationTransaction.CustomBlockMutation custom) {
payload.slice(NativeBlockState.class).set(x, key.y(), z, custom.state().placementBaseState());
payload.slice(Identifier.class).set(x, key.y(), z, Identifier.fromString(custom.state().deferredPlacementKey()));
} else if (mutation instanceof ObjectDestinationTransaction.SetMutation set) {
if (key.type() == NativeBlockState.class) {
payload.slice(Identifier.class).set(x, key.y(), z, null);
}
payload.slice(key.type()).set(x, key.y(), z, set.value());
}
}
return ObjectContinuationBundle.encode(payload);
}
private static LinkedHashMap<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> group(
List<ObjectDestinationTransaction.Mutation> mutations
) {
LinkedHashMap<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> grouped = new LinkedHashMap<>();
for (ObjectDestinationTransaction.Mutation mutation : mutations) {
ObjectContinuationBundle.ChunkPosition destination = new ObjectContinuationBundle.ChunkPosition(mutation.x() >> 4, mutation.z() >> 4);
grouped.computeIfAbsent(destination, ignored -> new ArrayList<>()).add(mutation);
}
return grouped;
}
private static ObjectContinuationBundle.Bounds bounds(List<ObjectDestinationTransaction.Mutation> mutations) {
int minimumX = Integer.MAX_VALUE;
int minimumZ = Integer.MAX_VALUE;
int maximumX = Integer.MIN_VALUE;
int maximumZ = Integer.MIN_VALUE;
for (ObjectDestinationTransaction.Mutation mutation : mutations) {
minimumX = Math.min(minimumX, mutation.x());
minimumZ = Math.min(minimumZ, mutation.z());
maximumX = Math.max(maximumX, mutation.x());
maximumZ = Math.max(maximumZ, mutation.z());
}
return new ObjectContinuationBundle.Bounds(minimumX, minimumZ, maximumX, maximumZ);
}
int mutationWeight() {
return mutationWeight;
}
private record Placement(int ordinal, ObjectContinuationBundle.Bounds bounds,
List<ObjectContinuationBundle.ChunkPosition> touched,
Map<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> fragments) {
}
private static long destinationKey(int chunkX, int chunkZ) {
return ((long) chunkX << 32) | (chunkZ & 0xffffffffL);
}
@@ -125,10 +125,10 @@ public final class DimensionTerrainContext implements DataProvider {
engine.getDimension(),
engine.getData(),
engine.getHeight(),
natural ? complex.getUnblendedNaturalHeightStream() : complex.getHeightStream(),
natural ? complex.getNaturalHeightStream() : complex.getHeightStream(),
natural ? complex.getNaturalSlopeStream() : complex.getSlopeStream(),
fluidHeightStream,
natural ? complex.getUnblendedNaturalTrueBiomeStream() : complex.getTrueBiomeStream(),
natural ? complex.getNaturalTrueBiomeStream() : complex.getTrueBiomeStream(),
complex.getRegionStream(),
complex.getRockStream(),
fluidBlockSampler,
@@ -47,7 +47,6 @@ import art.arcane.iris.structure.placement.IrisStructure;
import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveStorage;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.collection.KList;
@@ -68,20 +67,15 @@ import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.iris.generation.concurrent.BurstExecutor;
import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.iris.world.task.J;
import art.arcane.volmlib.util.stream.ProceduralStream;
import org.jetbrains.annotations.Nullable;
import java.awt.Color;
import java.nio.file.Path;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
@@ -496,10 +490,6 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
@BlockCoordinates
default int getHeight(int x, int z, boolean ignoreFluid) {
OptionalInt resolved = getComplex().resolvedTerrainHeight(x, z, ignoreFluid);
if (resolved.isPresent()) {
return resolved.getAsInt();
}
DimensionStackContext dimensionStackContext = getDimensionStackContext();
if (dimensionStackContext != null) {
return ignoreFluid
@@ -18,17 +18,31 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.generation.stage.IrisBiomeActuator;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.generation.mantle.EngineMantle;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.iris.generation.concurrent.BurstExecutor;
import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.iris.generation.mantle.EngineMantle;
import art.arcane.iris.generation.stage.IrisBiomeActuator;
import art.arcane.iris.generation.stage.IrisTransitionGeometryActuator;
import art.arcane.iris.integration.Identifier;
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
import art.arcane.iris.world.storage.matter.TileWrapper;
import art.arcane.iris.world.history.FloatingBiomeOverlay;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.world.history.SavedTerrainChunk;
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterSlice;
import java.io.IOException;
import java.util.List;
import java.util.Map;
public interface EngineMode extends Staged {
KList<EngineStage> getTerrainStages();
@@ -199,12 +213,115 @@ public interface EngineMode extends Staged {
EngineStage[] stages = getStages().toArray(new EngineStage[0]);
try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), generationSessionId, ctx)) {
getComplex().getResolvedTerrain().generate(this, x, z, blocks, biomes, multicore, ctx);
generateTerrain(x, z, blocks, biomes, multicore, ctx);
recordNaturalTerrain(x, z, blocks, biomes, ctx);
IrisBiomeActuator.publishNaturalMetadata(getEngine(), x, z, biomes, ctx);
ctx.beginContent();
List<Matter> continuations = restoreObjectContinuations(x, z);
for (EngineStage i : stages) {
i.generate(x, z, blocks, biomes, multicore, ctx);
}
for (Matter continuation : continuations) {
restoreObjectMetadata(x, z, continuation);
MatterSlice<NativeBlockState> states = continuation.getSlice(NativeBlockState.class);
if (states != null) {
states.iterateSync((localX, y, localZ, state) -> blocks.set(localX, y, localZ, state));
}
}
}
}
private List<Matter> restoreObjectContinuations(int x, int z) {
if (!(getEngine() instanceof IrisEngine irisEngine)) {
return List.of();
}
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null);
if (router == null) {
return List.of();
}
List<GenerationHistoryRuntimeRouter.ObjectContinuation> fragments = router.objectContinuations(
getComplex().getTransitionGenerationPlan(), x >> 4, z >> 4);
if (fragments.isEmpty()) {
return List.of();
}
try {
List<Matter> continuations = router.decodeObjectContinuations(fragments);
for (Matter continuation : continuations) {
if (continuation.getHeight() != getEngine().getHeight()) {
throw new IOException("Object continuation height does not match the world");
}
restoreObjectMetadata(x, z, continuation);
}
return continuations;
} catch (IOException failure) {
throw new IllegalStateException("Unable to restore object continuation at " + (x >> 4) + "," + (z >> 4), failure);
}
}
private void restoreObjectMetadata(int x, int z, Matter continuation) {
MantleChunk<Matter> chunk = getMantle().getMantle().getChunk(x >> 4, z >> 4).use();
try {
synchronized (chunk) {
MatterSlice<NativeBlockState> states = continuation.getSlice(NativeBlockState.class);
if (states != null) {
states.iterateSync((localX, y, localZ, state) -> {
Matter section = chunk.get(y >> 4);
if (section != null) {
clearObjectMetadata(section, localX, y & 15, localZ);
}
});
}
for (Map.Entry<Class<?>, MatterSlice<?>> entry : continuation.getSliceMap().entrySet()) {
entry.getValue().iterateSync((localX, y, localZ, value) ->
chunk.getOrCreate(y >> 4).slice(entry.getKey()).set(localX, y & 15, localZ, value));
}
}
} finally {
chunk.release();
}
}
private static void clearObjectMetadata(Matter section, int x, int y, int z) {
clearObjectMetadata(section.getSlice(Identifier.class), x, y, z);
clearObjectMetadata(section.getSlice(String.class), x, y, z);
clearObjectMetadata(section.getSlice(TreeBlockMaterial.class), x, y, z);
clearObjectMetadata(section.getSlice(TileWrapper.class), x, y, z);
}
private static void clearObjectMetadata(MatterSlice<?> slice, int x, int y, int z) {
if (slice != null) {
slice.set(x, y, z, null);
}
}
private void recordNaturalTerrain(int x, int z, Hunk<NativeBlockState> blocks,
Hunk<NativeBiome> biomes, ChunkContext context) {
if (!(getEngine() instanceof IrisEngine irisEngine)) {
return;
}
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null);
if (router == null) {
return;
}
FloatingBiomeOverlay floating = context.getFloatingBiomes();
if (floating != null) {
floating.retainHighestSurfaces((localX, localZ) -> {
for (int y = blocks.getHeight() - 1; y >= 0; y--) {
NativeBlockState state = blocks.getRaw(localX, y, localZ);
if (state != null && !state.isAir() && !state.isFluid()) {
return y;
}
}
return -1;
});
}
try {
SavedTerrainChunk terrain = IrisTransitionGeometryActuator.capture(
x, z, blocks, biomes, getEngine().getMinHeight(), context, true);
router.recordNaturalTerrain(terrain);
router.recordFloatingBiomes(x >> 4, z >> 4, floating);
} catch (IOException failure) {
throw new IllegalStateException("Unable to capture natural terrain at " + (x >> 4) + "," + (z >> 4), failure);
}
}
@@ -30,7 +30,7 @@ import art.arcane.iris.generation.cache.Cache;
import art.arcane.iris.studio.generation.BiomeBuffetLayout;
import art.arcane.iris.world.history.GenerationBlend;
import art.arcane.iris.world.history.TransitionGenerationPlan;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import art.arcane.iris.world.history.TransitionDisplacementField;
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
import art.arcane.iris.generation.hydrology.HydrologyColumnSnapshot;
@@ -41,6 +41,8 @@ import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntimeContext;
import art.arcane.iris.generation.image.IrisImageMapApplication;
import art.arcane.iris.generation.image.IrisImageMapRuntime;
import art.arcane.iris.generation.mantle.MantleHydrologyCaveVoxelView;
import art.arcane.iris.generation.mantle.ObjectContinuationBundle;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.generation.terrain.InferredType;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.biome.IrisBiomeGeneratorLink;
@@ -99,7 +101,6 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
@@ -175,7 +176,6 @@ public class IrisComplex implements DataProvider {
private final transient boolean reuseNaturalBaseBiome;
private ProceduralStream<UUID> baseBiomeIDStream;
private ProceduralStream<IrisBiome> naturalTrueBiomeStream;
private ProceduralStream<IrisBiome> unblendedNaturalTrueBiomeStream;
private ProceduralStream<IrisBiome> trueBiomeStream;
private ProceduralStream<NativeBiome> trueBiomeDerivativeStream;
private ProceduralStream<Double> naturalHeightStream;
@@ -188,8 +188,7 @@ public class IrisComplex implements DataProvider {
private transient Terrain3DRuntime terrain3D;
private transient HydrologyBankTerrainRuntime hydrologyBanks3D;
private ProceduralStream<Double> unblendedNaturalHeightStream;
private final ResolvedTerrainProvider resolvedTerrain;
private ProceduralStream<Double> placementHeightStream;
private final TransitionDisplacementField transitionDisplacement;
private ProceduralStream<Double> heightStream;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
@@ -248,7 +247,6 @@ public class IrisComplex implements DataProvider {
biomeBoundsSamplingStep = engine.getDimension().getBiomeBoundsSamplingStep();
this.detached = detached;
this.transitionGenerationPlan = transitionGenerationPlan;
this.resolvedTerrain = new ResolvedTerrainProvider(engine);
int cacheSize = noiseCacheSize(engine, IrisSettings.get().getPerformance().getNoiseCacheSize(), detached);
IrisBiome emptyBiome = new IrisBiome().setInferredType(InferredType.CAVE);
UUID focusUUID = UUID.nameUUIDFromBytes("focus".getBytes());
@@ -408,22 +406,21 @@ public class IrisComplex implements DataProvider {
unblendedNaturalHeightStream = terrain3DEnabled
? GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::sampleTerrain3DHeight), "unblendedNaturalHeightStream", engine, cacheSize)
: baseTerrainHeightStream;
naturalHeightStream = unblendedNaturalHeightStream;
transitionDisplacement = transitionGenerationPlan == null ? null : new TransitionDisplacementField(
transitionGenerationPlan, new TransitionDisplacementField.Sources(
(x, z) -> unblendedNaturalHeightStream.getDouble(x, z), engine.getHeight()));
naturalHeightStream = transitionDisplacement == null ? unblendedNaturalHeightStream
: GenerationStreams.cache2DDouble(ProceduralStream.ofDouble((x, z) -> transitionDisplacement.height(
blockCoordinate(x), blockCoordinate(z), unblendedNaturalHeightStream.getDouble(x, z))),
"naturalHeightStream", engine, cacheSize);
naturalTrueBiomeStream = focusedBiomes != null ? GenerationStreams.cache2D(focusedBiomes, "naturalTrueBiomeStream-focus", engine, cacheSize) : GenerationStreams.cache2D(naturalHeightStream
.convertAware2D((h, x, z) -> {
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
return mapped == null
? fixBiomeType(h, baseBiomeStream.get(x, z), regionStream.get(x, z), x, z, fluidHeight)
? fixBiomeType(h, baseBiomeStream.get(x, z), regionStream.get(x, z), x, z,
transitionFluidHeight(x, z, fluidHeight))
: mapped;
}), "naturalTrueBiomeStream", engine, cacheSize);
unblendedNaturalTrueBiomeStream = transitionGenerationPlan == null || focusedBiomes != null
? naturalTrueBiomeStream
: GenerationStreams.cache2D(unblendedNaturalHeightStream.convertAware2D((h, x, z) -> {
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
return mapped == null
? fixBiomeType(h, baseBiomeStream.get(x, z), regionStream.get(x, z), x, z, fluidHeight)
: mapped;
}), "unblendedNaturalTrueBiomeStream", engine, cacheSize);
IrisHydrology configuredHydrology = engine.getDimension().getHydrology();
if (hydrologyActive(configuredHydrology)) {
hydrologyRuntime = new IrisHydrologyRuntime(new IrisHydrologyRuntimeContext(
@@ -456,10 +453,9 @@ public class IrisComplex implements DataProvider {
ProceduralStream.ofDouble(this::resolveHydrologyTerrainHeight), cacheSize);
heightStream = ProceduralStream.ofDouble((x, z) -> terrainEngine.getPlatformHooks().isMainThread()
? nonblockingTerrainHeight(x, z) : cachedHeightStream.getDouble(x, z));
placementHeightStream = ProceduralStream.ofDouble(this::samplePlacementHeight);
roundedHeighteightStream = GenerationStreams.contextInjecting(placementHeightStream, engine, (c, x, z) -> c.getHeight().getDouble(x, z))
roundedHeighteightStream = GenerationStreams.contextInjecting(heightStream, engine, (c, x, z) -> c.getHeight().getDouble(x, z))
.round();
slopeStream = GenerationStreams.contextInjecting(placementHeightStream, engine, (c, x, z) -> c.getHeight().getDouble(x, z))
slopeStream = GenerationStreams.contextInjecting(heightStream, engine, (c, x, z) -> c.getHeight().getDouble(x, z))
.slope(3);
naturalSlopeStream = GenerationStreams.cache2DDouble(naturalHeightStream.slope(3), "naturalSlopeStream", engine, cacheSize);
// Terrain3D column heights resolve the surface biome, so the shore band reads the pre-terrain3D slope or it recurses.
@@ -534,7 +530,7 @@ public class IrisComplex implements DataProvider {
List<NoiseCacheCapacity> capacities = new ArrayList<>(5);
int additionalChunks = HydrologyNoiseCacheBudget.MAXIMUM_CHUNKS;
for (ProceduralStream<?> stream : new ProceduralStream<?>[]{
baseBiomeStream, baseTerrainHeightStream, unblendedNaturalHeightStream, regionStream, bridgeStream}) {
baseBiomeStream, baseTerrainHeightStream, unblendedNaturalHeightStream, naturalHeightStream, regionStream, bridgeStream}) {
if (!(stream instanceof CachedStream2D<?> || stream instanceof CachedDoubleStream2D)
|| selected.put(stream, Boolean.TRUE) != null) {
continue;
@@ -641,9 +637,7 @@ public class IrisComplex implements DataProvider {
}
private double sampleNaturalTerrainHeight(Engine engine, double x, double z) {
return terrain3D != null && terrain3D.active()
? sampleTerrain3DHeight(x, z)
: sampleUnblendedNaturalTerrainHeight(x, z);
return naturalHeightStream.getDouble(x, z);
}
private double sampleTerrain3DHeight(double x, double z) {
@@ -672,6 +666,10 @@ public class IrisComplex implements DataProvider {
return null;
}
Terrain3DColumn column = terrain3D.column(x, z);
if (transitionDisplacement != null && column.shaped()) {
int offset = (int) Math.round(naturalHeightStream.getDouble(x, z)) - column.topY();
column = column.displaced(offset, terrainEngine.getHeight());
}
return column.shaped() ? column : null;
}
@@ -740,7 +738,7 @@ public class IrisComplex implements DataProvider {
private double nearbyTerrainSurfaceHeight(int x, int surfaceY, int z) {
Terrain3DColumn column = terrainColumn(x, z);
return column == null ? getPlacementHeightStream().getDouble(x, z) : column.nearestSurfaceY(surfaceY);
return column == null ? getHeightStream().getDouble(x, z) : column.nearestSurfaceY(surfaceY);
}
private double sampleUnblendedNaturalTerrainHeight(double x, double z) {
@@ -783,17 +781,18 @@ public class IrisComplex implements DataProvider {
if (biome == null || region == null) {
return biome;
}
double shoreTop = shoreBandTop(height, region, x, z, fluidHeight, shoreSlopeStream);
if (height >= fluidHeight - 1 && height <= shoreTop && !biome.isShore()) {
double localFluidHeight = transitionFluidHeight(x, z, fluidHeight);
double shoreTop = shoreBandTop(height, region, x, z, localFluidHeight, shoreSlopeStream);
if (height >= localFluidHeight - 1 && height <= shoreTop && !biome.isShore()) {
return sampleInferredBiome(region, InferredType.SHORE, x, z);
}
if (height > shoreTop && !biome.isLand()) {
return sampleInferredBiome(region, InferredType.LAND, x, z);
}
if (height < fluidHeight && !biome.isAquatic()) {
if (height < localFluidHeight && !biome.isAquatic()) {
return sampleInferredBiome(region, InferredType.SEA, x, z);
}
if (height == fluidHeight && !biome.isShore()) {
if (height == localFluidHeight && !biome.isShore()) {
return sampleInferredBiome(region, InferredType.SHORE, x, z);
}
return biome;
@@ -955,6 +954,16 @@ public class IrisComplex implements DataProvider {
}
public NativeBlockState resolveSurfaceFluid(double x, double z) {
if (transitionDisplacement != null) {
String historical = transitionDisplacement.fluidStateKey(blockCoordinate(x), blockCoordinate(z));
if (historical != null && !historical.isEmpty()) {
NativeBlockState fluid = IrisPlatforms.get().registries().blockOrNull(historical);
if (fluid == null || !fluid.isFluid()) {
throw new IllegalStateException("Historical surface fluid is unavailable: " + historical);
}
return fluid;
}
}
HydrologyColumnLayer layer = surfaceFluidLayer(x, z);
if (layer != null) {
return resolveHydrologyFluid(layer.profileKey(), x, z);
@@ -1023,7 +1032,15 @@ public class IrisComplex implements DataProvider {
private double resolveHydrologyFluidSurface(double x, double z) {
HydrologyColumnLayer layer = surfaceFluidLayer(x, z);
return layer == null ? fluidHeight : layer.fluidHeadY();
if (layer != null) {
return layer.fluidHeadY();
}
return transitionFluidHeight(x, z, fluidHeight);
}
private double transitionFluidHeight(double x, double z, double nativeHead) {
return transitionDisplacement == null ? nativeHead
: transitionDisplacement.fluidHeight(blockCoordinate(x), blockCoordinate(z), nativeHead);
}
public HydrologyColumnSample sampleHydrologyColumn(double x, double z) {
@@ -1034,27 +1051,55 @@ public class IrisComplex implements DataProvider {
return hydrologyRuntime.sample(x, z).orElse(null);
}
double hydrologyWeight = transitionHydrologyWeight(x, z);
if (hydrologyWeight == 0D) {
return null;
}
HydrologyColumnSample sample = hydrologyRuntime.sample(x, z).orElse(null);
if (sample == null || hydrologyWeight == 1D) {
if (sample == null) {
return sample;
}
return taperHydrologySample(sample, hydrologyWeight);
return transitionHydrologySample(sample, hydrologyWeight, transitionDisplacement);
}
private HydrologyColumnSnapshot sampleHydrologySnapshot(int x, int z) {
if (hydrologyRuntime == null) {
return HydrologyColumnSnapshot.ready(null);
}
double weight = transitionHydrologyWeight(x, z);
if (weight == 0D) {
return HydrologyColumnSnapshot.ready(null);
}
HydrologyColumnSnapshot snapshot = hydrologyRuntime.sampleSnapshot(x, z);
return !snapshot.available() || snapshot.column() == null || weight == 1D ? snapshot
: HydrologyColumnSnapshot.ready(taperHydrologySample(snapshot.column(), weight));
return !snapshot.available() || snapshot.column() == null ? snapshot
: HydrologyColumnSnapshot.ready(transitionHydrologySample(
snapshot.column(), transitionHydrologyWeight(x, z), transitionDisplacement));
}
static HydrologyColumnSample transitionHydrologySample(
HydrologyColumnSample sample, double weight, TransitionDisplacementField transitionDisplacement
) {
HydrologyColumnSample tapered = taperHydrologySample(sample, weight);
if (transitionDisplacement == null
|| transitionDisplacement.sample(sample.x(), sample.z()).newTerrainWeight() == 1D) {
return tapered;
}
ArrayList<HydrologyColumnLayer> layers = new ArrayList<>(tapered.layers().size());
int seaLevel = (int) Math.round(transitionDisplacement.fluidHeight(sample.x(), sample.z(), tapered.seaLevel()));
for (int index = 0; index < tapered.layers().size(); index++) {
HydrologyColumnLayer layer = tapered.layers().get(index);
if (layer.feature().type().isUnderground() || layer.feature().type().isDeepFluid()) {
layers.add(layer);
continue;
}
HydrologyColumnLayer original = sample.layers().get(index);
int head = (int) Math.round(transitionDisplacement.fluidHeight(sample.x(), sample.z(), layer.fluidHeadY()));
if ((tapered.naturalHeight() < seaLevel || tapered.naturalHeight() == seaLevel && head > seaLevel)
&& (original.terrainOwned() || original.fluidOwned() || original.grading() || original.shore())) {
continue;
}
boolean dryChannel = original.channel() && layer.bedY() > head;
layers.add(new HydrologyColumnLayer(layer.feature(), layer.bedY(),
head, head, original.channel() && !dryChannel, layer.shore(), layer.grading(), original.connectedFluid() && !dryChannel,
original.fallingFluid() && !dryChannel, original.receivingPool() && !dryChannel,
layer.terrainOwned(), original.fluidOwned() && !dryChannel, layer.oceanApron(),
layer.profileKey(), layer.surfaceBiomeKey(), layer.mouthBiomeKey(), layer.shoreBiomeKey(),
layer.bankBiomeKey(), layer.floodedCaveBiomeKey()));
}
return new HydrologyColumnSample(tapered.x(), tapered.z(), tapered.naturalHeight(), Math.max(tapered.seaLevel(), seaLevel),
tapered.ocean(), tapered.parentBiomeKey(), layers);
}
static HydrologyColumnSample taperHydrologySample(
@@ -1067,6 +1112,9 @@ public class IrisComplex implements DataProvider {
}
ArrayList<HydrologyColumnLayer> taperedLayers = new ArrayList<>(sample.layers().size());
for (HydrologyColumnLayer layer : sample.layers()) {
if (hydrologyWeight == 0D && !layer.feature().type().isUnderground() && !layer.feature().type().isDeepFluid()) {
continue;
}
taperedLayers.add(taperHydrologyLayer(layer, sample.naturalHeight(), hydrologyWeight));
}
return new HydrologyColumnSample(
@@ -1087,19 +1135,21 @@ public class IrisComplex implements DataProvider {
if (layer.feature().type().isUnderground() || layer.feature().type().isDeepFluid()) {
return layer;
}
int bed = GenerationBlend.interpolateHeight(naturalHeight, layer.bedY(), hydrologyWeight);
boolean dryChannel = layer.channel() && bed > layer.fluidHeadY();
return new HydrologyColumnLayer(
layer.feature(),
GenerationBlend.interpolateHeight(naturalHeight, layer.bedY(), hydrologyWeight),
GenerationBlend.interpolateHeight(naturalHeight, layer.fluidHeadY(), hydrologyWeight),
GenerationBlend.interpolateHeight(naturalHeight, layer.ceilingY(), hydrologyWeight),
layer.channel(),
bed,
layer.fluidHeadY(),
layer.ceilingY(),
layer.channel() && !dryChannel,
layer.shore(),
layer.grading(),
layer.connectedFluid(),
layer.fallingFluid(),
layer.receivingPool(),
layer.connectedFluid() && !dryChannel,
layer.fallingFluid() && !dryChannel,
layer.receivingPool() && !dryChannel,
layer.terrainOwned(),
layer.fluidOwned(),
layer.fluidOwned() && !dryChannel,
layer.oceanApron(),
layer.profileKey(),
layer.surfaceBiomeKey(),
@@ -1114,14 +1164,14 @@ public class IrisComplex implements DataProvider {
}
private double transitionHydrologyWeight(double x, double z) {
return transitionGenerationPlan == null
return transitionDisplacement == null
? 1D
: transitionGenerationPlan.hydrologyWeightAt(blockCoordinate(x), blockCoordinate(z));
: transitionDisplacement.seamWeight(blockCoordinate(x), blockCoordinate(z));
}
/** Whether the column's hydrology can be sampled without waiting for a plan (see Engine.answersFromNaturalTerrain). */
public boolean isHydrologyPlanned(int x, int z) {
if (hydrologyRuntime == null || transitionHydrologyWeight(x, z) == 0D) {
if (hydrologyRuntime == null) {
return true;
}
if (!hydrologyRuntime.isPlanned(x, z)) {
@@ -1144,23 +1194,7 @@ public class IrisComplex implements DataProvider {
}
public ProceduralStream<Double> getHeightStream() {
return placementHeightStream;
}
public Optional<TerrainBoundarySignature> resolvedTerrainColumn(int blockX, int blockZ) {
if (transitionGenerationPlan == null || isNaturalTerrainContext()
|| !transitionGenerationPlan.hasTransitionAtChunk(blockX >> 4, blockZ >> 4)) {
return Optional.empty();
}
return Optional.of(resolvedTerrain.column(blockX, blockZ));
}
public OptionalInt resolvedTerrainHeight(int blockX, int blockZ, boolean ignoreFluid) {
if (transitionGenerationPlan == null || isNaturalTerrainContext()
|| !transitionGenerationPlan.hasTransitionAtChunk(blockX >> 4, blockZ >> 4)) {
return OptionalInt.empty();
}
return OptionalInt.of(resolvedTerrain.height(blockX, blockZ, ignoreFluid));
return heightStream;
}
boolean isNaturalTerrainContext() {
@@ -1170,11 +1204,6 @@ public class IrisComplex implements DataProvider {
&& context.getChunkContext().isNaturalTerrain();
}
private double samplePlacementHeight(double x, double z) {
OptionalInt resolved = resolvedTerrainHeight(blockCoordinate(x), blockCoordinate(z), true);
return resolved.isPresent() ? resolved.getAsInt() : heightStream.getDouble(x, z);
}
public boolean isHistoricalChunk(int chunkX, int chunkZ) {
return transitionGenerationPlan != null
&& transitionGenerationPlan.boundary().isHistoricalChunk(chunkX, chunkZ);
@@ -1214,8 +1243,16 @@ public class IrisComplex implements DataProvider {
int maximumX,
int maximumZ
) {
return transitionGenerationPlan == null
|| transitionGenerationPlan.allowsNewFootprint(minimumX, minimumZ, maximumX, maximumZ);
if (transitionGenerationPlan == null) {
return true;
}
if (!transitionGenerationPlan.allowsNewFootprint(minimumX, minimumZ, maximumX, maximumZ)) {
return false;
}
GenerationHistoryRuntimeRouter router = terrainEngine instanceof IrisEngine irisEngine
? irisEngine.getGenerationHistoryRuntimeRouter().orElse(null) : null;
return router == null || router.allowsObjectFootprint(transitionGenerationPlan,
new ObjectContinuationBundle.Bounds(minimumX, minimumZ, maximumX, maximumZ));
}
public Optional<String> historicalPhysicalBiomeKeyAt(int blockX, int blockY, int blockZ) {
@@ -1248,7 +1285,8 @@ public class IrisComplex implements DataProvider {
return mapped;
}
double terrainHeight = naturalHeightStream.getDouble(x, z);
return fixBiomeType(terrainHeight, baseBiomeStream.get(x, z), regionStream.get(x, z), (double) x, (double) z, fluidHeight);
return fixBiomeType(terrainHeight, baseBiomeStream.get(x, z), regionStream.get(x, z), (double) x, (double) z,
transitionFluidHeight(x, z, fluidHeight));
}
private HydrologyColumnLayer surfaceLayer(double x, double z) {
@@ -1336,7 +1374,7 @@ public class IrisComplex implements DataProvider {
regionStream.get(x, z),
x,
z,
fluidHeight
transitionFluidHeight(x, z, fluidHeight)
);
}
@@ -2094,6 +2132,8 @@ public class IrisComplex implements DataProvider {
hydrologyRuntime.close();
}
releaseHydrologyNoiseCaches();
resolvedTerrain.clear();
if (transitionDisplacement != null) {
transitionDisplacement.clear();
}
}
}
@@ -54,6 +54,9 @@ import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.matter.MatterStructurePOI;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.iris.generation.mantle.ObjectContinuationBundle;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import lombok.AccessLevel;
@@ -1478,7 +1481,18 @@ public class IrisEngine implements Engine {
boolean skipRealFlag = platformHooks.shouldBypassMantleStages(this);
if (!skipRealFlag) {
getMantle().getMantle().flag(x >> 4, z >> 4, MantleFlag.REAL, true);
MantleChunk<Matter> chunk = getMantle().getMantle().getChunk(x >> 4, z >> 4).use();
try {
synchronized (chunk) {
Matter section = chunk.get(0);
if (section != null) {
section.deleteSlice(ObjectContinuationBundle.class);
}
chunk.flag(MantleFlag.REAL, true);
}
} finally {
chunk.release();
}
}
getMetrics().getTotal().put(p.getMilliseconds());
generated.incrementAndGet();
@@ -1,261 +0,0 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.generation.stage.IrisTransitionGeometryActuator;
import art.arcane.iris.world.history.BoundaryColumnGeometry;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.world.history.FloatingBiomeOverlay;
import art.arcane.iris.world.history.SavedTerrainChunk;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import art.arcane.iris.world.history.TransitionGenerationPlan;
import art.arcane.iris.world.history.TransitionFluidContainment;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.volmlib.util.hunk.Hunk;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
public final class ResolvedTerrainProvider {
private static final int MAXIMUM_CACHED_CHUNKS = 64;
private static final int MAXIMUM_HEIGHT_CHUNKS = 4_096;
private final Engine engine;
private final LinkedHashMap<Long, ResolvedTerrain> completed = new LinkedHashMap<>(64, 0.75F, true);
private final ConcurrentHashMap<Long, CompletableFuture<ResolvedTerrain>> pending = new ConcurrentHashMap<>();
private final LinkedHashMap<Long, ResolvedTerrain> rawCompleted = new LinkedHashMap<>(64, 0.75F, true);
private final ConcurrentHashMap<Long, CompletableFuture<ResolvedTerrain>> rawPending = new ConcurrentHashMap<>();
private final LinkedHashMap<Long, int[]> heights = new LinkedHashMap<>(64, 0.75F, true);
public ResolvedTerrainProvider(Engine engine) {
this.engine = engine;
}
public TerrainBoundarySignature column(int blockX, int blockZ) {
return resolve(blockX >> 4, blockZ >> 4).terrain().column(blockX, blockZ);
}
public int height(int blockX, int blockZ, boolean ignoreFluid) {
long key = chunkKey(blockX >> 4, blockZ >> 4);
synchronized (heights) {
int[] cached = heights.get(key);
if (cached != null) {
return cached[(ignoreFluid ? 256 : 0) + (blockX & 15) * 16 + (blockZ & 15)];
}
}
TerrainBoundarySignature column = resolve(blockX >> 4, blockZ >> 4).terrain().column(blockX, blockZ);
return ignoreFluid ? column.oceanFloorHeight() : column.surfaceHeight();
}
public void generate(EngineMode mode, int x, int z, Hunk<NativeBlockState> blocks,
Hunk<NativeBiome> biomes, boolean multicore, ChunkContext context) {
SavedTerrainChunk terrain;
if (context.getComplex().getTransitionGenerationPlan() != null
&& context.getComplex().getTransitionGenerationPlan().hasTransitionAtChunk(x >> 4, z >> 4)) {
ResolvedTerrain resolved = resolve(x >> 4, z >> 4);
terrain = resolved.terrain();
context.setFloatingBiomes(resolved.floatingBiomes());
copy(terrain, blocks, biomes, context);
} else {
mode.generateTerrain(x, z, blocks, biomes, multicore, context);
terrain = capture(x, z, blocks, biomes, context, true);
}
if (engine instanceof IrisEngine irisEngine) {
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null);
if (router != null) {
router.recordNaturalTerrain(terrain.boundaryOnly());
router.recordFloatingBiomes(x >> 4, z >> 4, context.getFloatingBiomes());
}
}
}
public void clear() {
synchronized (completed) {
completed.clear();
}
synchronized (rawCompleted) {
rawCompleted.clear();
}
synchronized (heights) {
heights.clear();
}
}
private ResolvedTerrain resolve(int chunkX, int chunkZ) {
return resolve(chunkX, chunkZ, false);
}
private ResolvedTerrain resolve(int chunkX, int chunkZ, boolean raw) {
LinkedHashMap<Long, ResolvedTerrain> cache = raw ? rawCompleted : completed;
ConcurrentHashMap<Long, CompletableFuture<ResolvedTerrain>> loads = raw ? rawPending : pending;
long key = chunkKey(chunkX, chunkZ);
synchronized (cache) {
ResolvedTerrain cached = cache.get(key);
if (cached != null) {
return cached;
}
}
CompletableFuture<ResolvedTerrain> result = new CompletableFuture<>();
CompletableFuture<ResolvedTerrain> existing = loads.putIfAbsent(key, result);
if (existing != null) {
return existing.join();
}
try {
synchronized (cache) {
ResolvedTerrain published = cache.get(key);
if (published != null) {
result.complete(published);
return published;
}
}
ResolvedTerrain terrain = raw ? compute(chunkX, chunkZ) : contain(chunkX, chunkZ);
if (!raw) {
cacheHeights(key, terrain.terrain());
}
synchronized (cache) {
cache.put(key, terrain);
while (cache.size() > MAXIMUM_CACHED_CHUNKS) {
cache.pollFirstEntry();
}
}
result.complete(terrain);
return terrain;
} catch (RuntimeException | Error failure) {
result.completeExceptionally(failure);
throw failure;
} finally {
loads.remove(key, result);
}
}
private static long chunkKey(int chunkX, int chunkZ) {
return ((long) chunkX << 32) | (chunkZ & 0xffffffffL);
}
private void cacheHeights(long key, SavedTerrainChunk terrain) {
int[] values = new int[512];
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
TerrainBoundarySignature column = terrain.column((terrain.chunkX() << 4) + x,
(terrain.chunkZ() << 4) + z);
int index = x * 16 + z;
values[index] = column.surfaceHeight();
values[256 + index] = column.oceanFloorHeight();
}
}
synchronized (heights) {
heights.put(key, values);
while (heights.size() > MAXIMUM_HEIGHT_CHUNKS) {
heights.pollFirstEntry();
}
}
}
private ResolvedTerrain contain(int chunkX, int chunkZ) {
ResolvedTerrain raw = resolve(chunkX, chunkZ, true);
TransitionGenerationPlan plan = engine.getComplex().getTransitionGenerationPlan();
if (plan == null || !plan.hasTransitionAtChunk(chunkX, chunkZ)) {
return raw;
}
try {
return new ResolvedTerrain(TransitionFluidContainment.contain(raw.terrain(), plan,
(x, z) -> rawColumn(plan, raw.terrain(), x, z)), raw.floatingBiomes());
} catch (IOException failure) {
throw new IllegalStateException("Unable to contain transition fluids at " + chunkX + "," + chunkZ, failure);
}
}
private BoundaryColumnGeometry rawColumn(TransitionGenerationPlan plan, SavedTerrainChunk current, int x, int z)
throws IOException {
if (plan.isHistoricalBlock(x, z)) {
BoundaryColumnGeometry geometry = plan.terrainSignatures().signatureAt(x, z)
.orElseThrow(() -> new IOException("Missing saved terrain boundary column at " + x + "," + z))
.geometry();
if (geometry.minimumY() != engine.getMinHeight() || geometry.height() != engine.getHeight()) {
throw new IOException("Saved terrain boundary column has the wrong vertical layout at " + x + "," + z);
}
return geometry;
}
return (x >> 4) == current.chunkX() && (z >> 4) == current.chunkZ()
? current.column(x, z).geometry() : resolve(x >> 4, z >> 4, true).terrain().column(x, z).geometry();
}
private ResolvedTerrain compute(int chunkX, int chunkZ) {
int x = Math.multiplyExact(chunkX, 16);
int z = Math.multiplyExact(chunkZ, 16);
try (GenerationHistoryRuntimeRouter.CoordinateScope runtimeScope = engine instanceof IrisEngine irisEngine
? irisEngine.openGenerationHistoryCoordinateScope(x, z) : null) {
IrisContext active = IrisContext.get();
long sessionId = active == null ? 0L : active.getGenerationSessionId();
ChunkContext context = new ChunkContext(x, z, engine.getComplex(), sessionId, true,
ChunkContext.PrefillPlan.NATURAL_TERRAIN, engine.getMetrics(), engine.getDimensionStackContext());
context.beginSpeculativeTerrain();
Hunk<NativeBlockState> blocks = Hunk.newArrayHunk(16, engine.getHeight(), 16);
Hunk<NativeBiome> biomes = Hunk.newArrayHunk(16, engine.getHeight(), 16);
try (IrisContext.Scope ignored = IrisContext.open(engine, sessionId, context)) {
engine.getMode().generateTerrain(x, z, blocks, biomes, false, context);
return new ResolvedTerrain(capture(x, z, blocks, biomes, context, false), context.getFloatingBiomes());
}
} catch (IOException failure) {
throw new IllegalStateException("Unable to scope natural terrain at " + chunkX + "," + chunkZ, failure);
}
}
private SavedTerrainChunk capture(int x, int z, Hunk<NativeBlockState> blocks,
Hunk<NativeBiome> biomes, ChunkContext context, boolean boundaryOnly) {
FloatingBiomeOverlay floating = context.getFloatingBiomes();
if (floating != null) {
floating.retainHighestSurfaces((localX, localZ) -> highestSolid(blocks, localX, localZ));
}
try {
return IrisTransitionGeometryActuator.capture(x, z, blocks, biomes, engine.getMinHeight(), context, boundaryOnly);
} catch (IOException failure) {
throw new IllegalStateException("Unable to capture natural terrain at " + (x >> 4) + "," + (z >> 4), failure);
}
}
private static int highestSolid(Hunk<NativeBlockState> blocks, int localX, int localZ) {
for (int y = blocks.getHeight() - 1; y >= 0; y--) {
NativeBlockState state = blocks.getRaw(localX, y, localZ);
if (state != null && !state.isAir() && !state.isFluid()) {
return y;
}
}
return -1;
}
private static void copy(SavedTerrainChunk terrain, Hunk<NativeBlockState> blocks,
Hunk<NativeBiome> biomes, ChunkContext context) {
PlatformRegistries registries = IrisPlatforms.get().registries();
Map<String, NativeBlockState> states = new HashMap<>();
Map<String, NativeBiome> physicalBiomes = new HashMap<>();
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
TerrainBoundarySignature column = terrain.column((terrain.chunkX() << 4) + x, (terrain.chunkZ() << 4) + z);
BoundaryColumnGeometry geometry = column.geometry();
for (int offset = 0; offset < blocks.getHeight(); offset++) {
String stateKey = geometry.voxelAt(geometry.minimumY() + offset).stateKey();
NativeBlockState state = states.computeIfAbsent(stateKey, registries::blockOrNull);
if (state == null) {
throw new IllegalStateException("Resolved terrain requires unavailable block state " + stateKey);
}
blocks.setRaw(x, offset, z, state);
String biomeKey = column.biomeAtSample(offset / 4);
biomes.setRaw(x, offset, z, physicalBiomes.computeIfAbsent(biomeKey, registries::biome));
}
context.setTerrainHeight(x, z, column.oceanFloorHeight());
}
}
}
private record ResolvedTerrain(SavedTerrainChunk terrain, FloatingBiomeOverlay floatingBiomes) {
}
}
@@ -64,7 +64,7 @@ public class IrisBiomeActuator extends EngineAssignedActuator<NativeBiome> {
int depth = h.getDepth();
int height = h.getHeight();
Engine engine = getEngine();
Mantle<Matter> mantle = context.isSpeculativeTerrain() ? null : engine.getMantle().getMantle();
Mantle<Matter> mantle = engine.getMantle().getMantle();
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
IrisComplex complex = context.getComplex();
DimensionStackContext dimensionStackContext = engine.getDimensionStackContext();
@@ -109,7 +109,7 @@ public class IrisBiomeActuator extends EngineAssignedActuator<NativeBiome> {
public static void publishNaturalMetadata(Engine engine, int x, int z, Hunk<NativeBiome> biomes,
ChunkContext context) {
if (context.isSpeculativeTerrain() || !context.getComplex().allowsMantleChunkWrite(x >> 4, z >> 4)) {
if (!context.getComplex().allowsMantleChunkWrite(x >> 4, z >> 4)) {
return;
}
Mantle<Matter> mantle = engine.getMantle().getMantle();
@@ -146,7 +146,7 @@ public class IrisBiomeActuator extends EngineAssignedActuator<NativeBiome> {
IrisComplex complex
) {
TransitionGenerationPlan transitionPlan = complex.getTransitionGenerationPlan();
if (transitionPlan == null) {
if (transitionPlan == null || !transitionPlan.hasTransitionAtChunk(worldX >> 4, worldZ >> 4)) {
return;
}
TransitionGenerationPlan.TerrainSample terrainSample = transitionPlan.terrainSampleAt(worldX, worldZ);
@@ -82,8 +82,7 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator<Nat
if (getEngine().getDimensionStackContext() == null) {
return;
}
MantleChunk<Matter> mantleChunk = context.isSpeculativeTerrain() ? null
: getEngine().getMantle().getMantle().getChunk(x >> 4, z >> 4).use();
MantleChunk<Matter> mantleChunk = getEngine().getMantle().getMantle().getChunk(x >> 4, z >> 4).use();
try {
MetadataCleaner metadata = new MetadataCleaner(mantleChunk, blocks.getHeight());
for (int localX = 0; localX < blocks.getWidth(); localX++) {
@@ -431,9 +431,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
}
context.setNaturalBiome(wx & 15, y, wz & 15, selected);
context.floatingBiomes(chunkHeight).record(wx & 15, y, wz & 15, matter.identity());
if (!context.isSpeculativeTerrain()) {
getEngine().getMantle().getMantle().set(wx, y, wz, BiomeInjectMatter.get(selected.key()));
}
getEngine().getMantle().getMantle().set(wx, y, wz, BiomeInjectMatter.get(selected.key()));
}
} catch (Throwable e) {
IrisLogging.reportError(e);
@@ -149,7 +149,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
boolean riverOwned = padRiverBed && hydrologyTerrain != null && hydrologyTerrain.terrainOwned();
IrisRiverMaterialConfig roleMaterial = hydrologyRoleMaterial(
hydrologyTerrain, bedMaterial, shoreMaterial, bankMaterial);
NativeBlockState fluid = hydrologyFluid == null
NativeBlockState fluid = hydrologyFluid == null || complex.getTransitionDisplacement() != null
? complex.resolveSurfaceFluid(realX, realZ)
: complex.resolveHydrologyFluid(hydrologyFluid.profileKey(), realX, realZ);
NativeBlockState rock = rockCache.get(xf, zf);
@@ -5,7 +5,6 @@ import art.arcane.iris.generation.runtime.EngineAssignedComponent;
import art.arcane.iris.generation.runtime.EngineStage;
import art.arcane.iris.world.history.BoundaryColumnGeometry;
import art.arcane.iris.world.history.BoundaryGeometryInfluence;
import art.arcane.iris.world.history.GenerationBlend;
import art.arcane.iris.world.history.SavedTerrainChunk;
import art.arcane.iris.world.history.TransitionGenerationPlan;
import art.arcane.iris.world.history.TransitionGeometryBlender;
@@ -70,9 +69,7 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
if (blended == current) {
continue;
}
double expectedFloor = GenerationBlend.interpolate(
plan.terrainSampleAt(x + localX, z + localZ).historicalOceanFloorHeight(),
context.getRoundedHeight(localX, localZ), influence.newTerrainWeight());
double expectedFloor = context.getRoundedHeight(localX, localZ);
int floor = blended.surfaceOffsetNear(expectedFloor);
List<BoundaryColumnGeometry.Voxel> voxels = blended.voxels();
for (int offset = 0; offset < voxels.size(); offset++) {
@@ -86,6 +86,30 @@ public final class Terrain3DColumn {
return nearest;
}
public Terrain3DColumn displaced(int offset, int height) {
if (height < 1) {
throw new IllegalArgumentException("Terrain column height must be positive");
}
if (offset == 0) {
return this;
}
double displacedBase = Math.clamp(baseHeight + offset, 0D, height - 1D);
int[] displaced = new int[boundaries.length];
int count = 0;
for (int index = 0; index < boundaries.length; index += 2) {
long minimum = index == 0 && boundaries[index] == 0 ? 0L : (long) boundaries[index] + offset;
long maximum = (long) boundaries[index + 1] + offset;
if (minimum >= height || maximum < 0L) {
continue;
}
displaced[count++] = (int) Math.max(0L, minimum);
displaced[count++] = (int) Math.min(height - 1L, maximum);
}
return count == 0 ? unshaped(displacedBase, height)
: new Terrain3DColumn(displacedBase, Math.clamp((long) minY + offset, 0, height),
shaped, Arrays.copyOf(displaced, count));
}
@Override
public boolean equals(Object value) {
return this == value || value instanceof Terrain3DColumn other
@@ -98,7 +98,8 @@ public final class BukkitStructureStagePolicy implements StructureStagePolicy<
@Override
public NativeBlockPositionPredicate protectedPositions(Engine context) {
return GenerationWritePolicy.protectedPositions(context.getDimension().getStaticObjectLayer(context.getData()),
return GenerationWritePolicy.protectedPositions(context.getDimension().getStaticObjectLayer(context.getData())
.forTransition(context),
context.getDimensionStackContext(), context.getMinHeight());
}
@@ -8,7 +8,6 @@ import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.platform.generation.BukkitChunkGenerator;
import art.arcane.iris.studio.generation.JigsawStudioGenerator;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import art.arcane.iris.world.history.TerrainNativeBlockKeys;
import art.arcane.volmlib.nativelib.terrain.NativeBlockColumn;
import art.arcane.volmlib.nativelib.terrain.NativeTerrainColumnPolicy;
@@ -99,8 +98,7 @@ public final class BukkitTerrainColumnPolicy implements NativeTerrainColumnPolic
@Override
public Optional<? extends NativeBlockColumn> resolvedColumn() {
return engine.getComplex().resolvedTerrainColumn(query.blockX(), query.blockZ())
.map(TerrainBoundarySignature::geometry);
return Optional.empty();
}
@Override
@@ -94,7 +94,8 @@ public class ImportedFeaturePolicy implements NativeImportedFeaturePolicy<IrisDi
@Override
public Placement placement() {
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData())
.forTransition(engine);
int minimumY = engine.getMinHeight();
DimensionStackContext stack = engine.getDimensionStackContext();
if (staticObjects.isEmpty() && stack == null) {
@@ -22,9 +22,7 @@ import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import java.util.Optional;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import org.jetbrains.annotations.Nullable;
@@ -61,11 +59,6 @@ public interface IObjectPlacer {
return getFluidHeight();
}
int coordinateShift = getFluidHeight() - engine.getDimension().getFluidHeight();
Optional<TerrainBoundarySignature> resolved = engine.getComplex().resolvedTerrainColumn(x, z);
if (resolved.isPresent()) {
return resolved.get().fluidHeight().isPresent()
? coordinateShift + resolved.get().fluidHeight().getAsInt() : -1;
}
return coordinateShift + (int) Math.round(
engine.getComplex().getRiverWaterSurfaceStream().get(x, z)
);
@@ -1,12 +1,18 @@
package art.arcane.iris.structure.object;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.generation.mantle.ObjectContinuationBundle;
import art.arcane.iris.generation.mantle.ObjectContinuationPersistence;
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.pack.value.IrisPosition;
import art.arcane.iris.integration.Identifier;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.hunk.Hunk;
@@ -15,29 +21,44 @@ import art.arcane.volmlib.util.mantle.flag.MantleFlag;
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.IrisMatter;
import art.arcane.volmlib.util.matter.MatterMarker;
import art.arcane.volmlib.util.matter.MatterSlice;
import art.arcane.volmlib.util.matter.MatterStructurePOI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier;
/**
* Compiles fixed placements once per dimension snapshot and writes only the current chunk's cells.
* Stored Y coordinates are offsets from the dimension floor; configured positions use absolute world Y.
*/
public final class IrisStaticObjectLayer {
private static final IrisStaticObjectLayer EMPTY = new IrisStaticObjectLayer(Map.of());
private static final IrisStaticObjectLayer EMPTY = new IrisStaticObjectLayer(Map.of(), List.of());
private static final List<Class<?>> REPLACED_METADATA = List.of(
TileWrapper.class, Identifier.class, String.class, TreeBlockMaterial.class,
MatterMarker.class, MatterStructurePOI.class);
private final Map<Long, Chunk> chunks;
private final List<Placement> placements;
private final Map<Long, List<Placement>> placementsByChunk;
private volatile TransitionLayer transitionLayer;
private IrisStaticObjectLayer(Map<Long, Chunk> chunks) {
private IrisStaticObjectLayer(Map<Long, Chunk> chunks, List<Placement> placements) {
this.chunks = chunks;
this.placements = placements;
Map<Long, List<Placement>> indexed = new HashMap<>();
for (Placement placement : placements) {
for (long chunk : placement.chunks().keySet()) {
indexed.computeIfAbsent(chunk, key -> new ArrayList<>()).add(placement);
}
}
indexed.replaceAll((chunk, entries) -> List.copyOf(entries));
this.placementsByChunk = Map.copyOf(indexed);
}
public static IrisStaticObjectLayer compile(IrisDimension dimension, IrisData data) {
@@ -45,7 +66,7 @@ public final class IrisStaticObjectLayer {
if (dimension.getStaticObjects().isEmpty()) {
return EMPTY;
}
Compilation compilation = new Compilation(dimension.getMaxHeight() - dimension.getMinHeight());
List<Placement> placements = new ArrayList<>(dimension.getStaticObjects().size());
for (int index = 0; index < dimension.getStaticObjects().size(); index++) {
IrisStaticObject entry = dimension.getStaticObjects().get(index);
try {
@@ -65,15 +86,48 @@ public final class IrisStaticObjectLayer {
} else if (entry.isSmartBore()) {
object = object.copy();
}
Compilation compilation = new Compilation(dimension.getMaxHeight() - dimension.getMinHeight());
IrisPosition position = entry.getPosition();
object.place(position.getX(), position.getY() - dimension.getMinHeight(),
position.getZ(), compilation, entry.toPlacement(), new RNG(entry.getSeed()), data);
placements.add(compilation.freeze(new ObjectContinuationBundle.PlacementKey(
ObjectContinuationBundle.Kind.STATIC, position.getX() >> 4, position.getZ() >> 4, index)));
} catch (RuntimeException failure) {
throw new IllegalArgumentException("Invalid staticObjects[" + index + "] in dimension '"
+ dimension.getLoadKey() + "': " + failure.getMessage(), failure);
}
}
return compilation.freeze();
return merge(placements);
}
public IrisStaticObjectLayer forTransition(Engine engine) {
if (engine == null || placements.isEmpty()) {
return this;
}
IrisComplex complex = engine.getComplex();
if (complex.getTransitionGenerationPlan() == null) {
return this;
}
GenerationHistoryRuntimeRouter router = engine instanceof IrisEngine irisEngine
? irisEngine.getGenerationHistoryRuntimeRouter().orElse(null) : null;
boolean cacheable = !(engine instanceof IrisEngine) || router != null;
TransitionLayer cached = transitionLayer;
if (cacheable && cached != null && cached.complex() == complex && cached.router() == router) {
return cached.layer();
}
List<Placement> accepted = new ArrayList<>(placements.size());
for (Placement placement : placements) {
Footprint footprint = placement.footprint();
if (footprint == null || complex.allowsNewGenerationFootprint(
footprint.minimumX(), footprint.minimumZ(), footprint.maximumX(), footprint.maximumZ())) {
accepted.add(placement);
}
}
IrisStaticObjectLayer filtered = accepted.size() == placements.size() ? this : merge(accepted);
if (cacheable) {
transitionLayer = new TransitionLayer(complex, router, filtered);
}
return filtered;
}
public List<Block> blocks(int chunkX, int chunkZ) {
@@ -99,6 +153,7 @@ public final class IrisStaticObjectLayer {
if (blocks.isEmpty()) {
return;
}
persistContinuations(engine, x >> 4, z >> 4, output.getHeight());
MantleChunk<Matter> chunk = engine.getMantle().getMantle().getChunk(x >> 4, z >> 4);
chunk.use();
try {
@@ -127,6 +182,58 @@ public final class IrisStaticObjectLayer {
}
}
private void persistContinuations(Engine engine, int chunkX, int chunkZ, int height) {
for (Placement placement : placementsByChunk.getOrDefault(chunkKey(chunkX, chunkZ), List.of())) {
if (placement.chunks().size() < 2) {
continue;
}
Map<ObjectContinuationBundle.ChunkPosition, Supplier<Matter>> fragments = new HashMap<>();
for (Map.Entry<Long, Chunk> destination : placement.chunks().entrySet()) {
long key = destination.getKey();
fragments.put(new ObjectContinuationBundle.ChunkPosition((int) (key >> 32), (int) key),
() -> continuationPayload(destination.getValue(), height));
}
Footprint footprint = placement.footprint();
ObjectContinuationPersistence.persist(engine.getMantle().getMantle(), placement.key(),
new ObjectContinuationBundle.Bounds(footprint.minimumX(), footprint.minimumZ(),
footprint.maximumX(), footprint.maximumZ()), fragments);
}
}
private static Matter continuationPayload(Chunk chunk, int height) {
IrisMatterSupport.ensureRegistered();
Matter payload = new IrisMatter(16, height, 16);
for (Block block : chunk.ordered()) {
ObjectContinuationPersistence.put(payload, block.x(), block.y(), block.z(), block.state());
if (block.tile() != null) {
ObjectContinuationPersistence.put(payload, block.x(), block.y(), block.z(), block.tile().clone());
}
}
return payload;
}
private static IrisStaticObjectLayer merge(List<Placement> placements) {
if (placements.isEmpty()) {
return EMPTY;
}
Map<Long, Map<Integer, Block>> combined = new HashMap<>();
for (Placement placement : placements) {
for (Map.Entry<Long, Chunk> chunk : placement.chunks().entrySet()) {
combined.computeIfAbsent(chunk.getKey(), key -> new HashMap<>()).putAll(chunk.getValue().blocks());
}
}
return new IrisStaticObjectLayer(freezeChunks(combined), List.copyOf(placements));
}
private static Map<Long, Chunk> freezeChunks(Map<Long, Map<Integer, Block>> chunks) {
Map<Long, Chunk> compiled = new HashMap<>(chunks.size());
for (Map.Entry<Long, Map<Integer, Block>> chunk : chunks.entrySet()) {
Map<Integer, Block> blocks = Map.copyOf(chunk.getValue());
compiled.put(chunk.getKey(), new Chunk(blocks, List.copyOf(blocks.values())));
}
return Map.copyOf(compiled);
}
private static long chunkKey(int x, int z) {
return ((long) x << 32) | (z & 0xffffffffL);
}
@@ -137,21 +244,30 @@ public final class IrisStaticObjectLayer {
private record Chunk(Map<Integer, Block> blocks, List<Block> ordered) {
}
private record Footprint(int minimumX, int minimumZ, int maximumX, int maximumZ) {
}
private record Placement(ObjectContinuationBundle.PlacementKey key, Map<Long, Chunk> chunks, Footprint footprint) {
}
private record TransitionLayer(IrisComplex complex, GenerationHistoryRuntimeRouter router, IrisStaticObjectLayer layer) {
}
private static final class Compilation implements IObjectPlacer {
private final int height;
private final Map<Long, Map<Integer, Block>> chunks = new HashMap<>();
private int minimumX = Integer.MAX_VALUE;
private int minimumZ = Integer.MAX_VALUE;
private int maximumX = Integer.MIN_VALUE;
private int maximumZ = Integer.MIN_VALUE;
private Compilation(int height) {
this.height = height;
}
private IrisStaticObjectLayer freeze() {
Map<Long, Chunk> compiled = new HashMap<>(chunks.size());
for (Map.Entry<Long, Map<Integer, Block>> chunk : chunks.entrySet()) {
Map<Integer, Block> blocks = Map.copyOf(chunk.getValue());
compiled.put(chunk.getKey(), new Chunk(blocks, List.copyOf(blocks.values())));
}
return new IrisStaticObjectLayer(Map.copyOf(compiled));
private Placement freeze(ObjectContinuationBundle.PlacementKey key) {
return new Placement(key, freezeChunks(chunks), chunks.isEmpty() ? null
: new Footprint(minimumX, minimumZ, maximumX, maximumZ));
}
@Override
@@ -166,6 +282,10 @@ public final class IrisStaticObjectLayer {
if (state.isCustom() && (state.deferredPlacementKey() == null || state.placementBaseState() == null)) {
throw new IllegalArgumentException("Custom object block has no placement data: " + state.key());
}
minimumX = Math.min(minimumX, x);
minimumZ = Math.min(minimumZ, z);
maximumX = Math.max(maximumX, x);
maximumZ = Math.max(maximumZ, z);
Map<Integer, Block> chunk = chunks.computeIfAbsent(chunkKey(x >> 4, z >> 4), key -> new HashMap<>());
chunk.put(blockKey(x, y, z), new Block(x & 15, y, z & 15, state, null));
}
@@ -41,6 +41,7 @@ import art.arcane.iris.pack.value.IrisPosition;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.structure.nativegen.NativeStructureSuppression;
import art.arcane.iris.structure.object.ObjectPlaceMode;
import art.arcane.iris.structure.object.IrisObjectVacuum;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import com.github.benmanes.caffeine.cache.Cache;
@@ -589,7 +590,7 @@ public final class IrisStructureLocator {
"assembled pieces exceed the configured structure or world bounds")) {
return null;
}
if (!allowsResolvedFootprint(engine, placement, pieces)) {
if (!allowsResolvedFootprint(engine, placement, structure, pieces)) {
return null;
}
return new ResolvedPlacement(placement, selectedKey, structure, pieces, rng, originX, baseY, originZ, exactY);
@@ -597,7 +598,7 @@ public final class IrisStructureLocator {
public static boolean allowsResolvedFootprint(Engine engine, ResolvedPlacement resolved) {
Objects.requireNonNull(resolved, "resolved placement");
return allowsResolvedFootprint(engine, resolved.placement(), resolved.pieces());
return allowsResolvedFootprint(engine, resolved.placement(), resolved.structure(), resolved.pieces());
}
public static boolean requirePlacementOutput(IrisStructurePlacement placement, String structureKey,
@@ -909,6 +910,7 @@ public final class IrisStructureLocator {
private static boolean allowsResolvedFootprint(
Engine engine,
IrisStructurePlacement placement,
IrisStructure structure,
KList<PlacedStructurePiece> pieces
) {
int[] bounds = computeBounds(pieces);
@@ -921,6 +923,10 @@ public final class IrisStructureLocator {
|| mode == StructureTerrainMode.BORE
? Math.max(0, terrain.getHorizontalPadding())
: 0;
if (pieces.size() == 1 && !placement.isAnchoredUnderground()
&& IrisObjectVacuum.isVacuumMode(structure.getPlaceMode())) {
padding = Math.max(padding, IrisObjectVacuum.resolveRadius(structure.getPlaceMode(), null));
}
return engine.getComplex().allowsNewGenerationFootprint(
saturatedOffset(bounds[0], -padding),
saturatedOffset(bounds[2], -padding),
@@ -58,23 +58,6 @@ public final class GenerationBlend {
return (int) Math.round(historicalHeight + ((double) newHeight - historicalHeight) * newEpochWeight);
}
public static boolean usesHistoricalMaterial(int blockX, int blockY, int blockZ, double newWeight) {
validateWeight(newWeight);
if (newWeight == 0D) {
return true;
}
if (newWeight == 1D) {
return false;
}
long hash = Math.floorDiv(blockX, 8) * 341873128712L
^ Math.floorDiv(blockY, 8) * 132897987541L
^ Math.floorDiv(blockZ, 8) * 42317861L;
hash = (hash ^ (hash >>> 30)) * 0xbf58476d1ce4e5b9L;
hash = (hash ^ (hash >>> 27)) * 0x94d049bb133111ebL;
hash ^= hash >>> 31;
return newWeight < (hash >>> 11) * 0x1.0p-53;
}
private static void validateWeight(double weight) {
if (!Double.isFinite(weight) || weight < 0D || weight > 1D) {
throw new IllegalArgumentException("Blend weight must be finite and between zero and one");
@@ -29,6 +29,7 @@ import java.util.concurrent.ArrayBlockingQueue;
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_CACHED_TRANSITION_PLANS = 4;
private static final int MAXIMUM_SEMANTIC_BATCH = 32;
private static final int MAXIMUM_PENDING_SEMANTIC_CLAIMS = 128;
@@ -44,6 +45,7 @@ public final class GenerationHistory {
private final GenerationAdmission admission;
private final Map<Long, GenerationBoundary> boundaryCache;
private final Map<Long, TerrainBoundarySignatureStore.Snapshot> terrainSignatureCache;
private final Map<Long, TransitionGenerationPlan> transitionPlanCache;
private final ArrayBlockingQueue<PendingSemanticClaim> pendingSemanticClaims =
new ArrayBlockingQueue<>(MAXIMUM_PENDING_SEMANTIC_CLAIMS);
@@ -68,6 +70,7 @@ public final class GenerationHistory {
this.admission = new GenerationAdmission(paths.dimensionRoot());
this.boundaryCache = boundedCache(MAXIMUM_CACHED_BOUNDARIES);
this.terrainSignatureCache = boundedCache(MAXIMUM_CACHED_TERRAIN_SIGNATURES);
this.transitionPlanCache = boundedCache(MAXIMUM_CACHED_TRANSITION_PLANS);
validateReferencedState(publication, true);
}
@@ -872,6 +875,10 @@ public final class GenerationHistory {
}
public synchronized TransitionGenerationPlan transitionPlan(long activationId) throws IOException {
TransitionGenerationPlan cached = transitionPlanCache.get(activationId);
if (cached != null) {
return cached;
}
GenerationActivation activation = requireActivation(activationId);
if (activation.isInitial()) {
throw new IllegalArgumentException("The initial generation activation has no transition plan.");
@@ -888,7 +895,7 @@ public final class GenerationHistory {
GenerationBoundary boundary = boundary(activationId);
TerrainBoundarySignatureStore.Snapshot terrainSnapshot = terrainSignatures(activationId);
requireTransitionSnapshotIdentities(activation, boundary, terrainSnapshot);
return new TransitionGenerationPlan(
TransitionGenerationPlan plan = new TransitionGenerationPlan(
new TransitionGenerationPlan.Specification(
activation.activationId(),
requireEpoch(parent.epochId()).epochId(),
@@ -901,6 +908,8 @@ public final class GenerationHistory {
boundary,
terrainSnapshot
);
transitionPlanCache.put(activationId, plan);
return plan;
}
public synchronized int explicitChunkCount() {
@@ -10,14 +10,21 @@ import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.IrisEngineMantle;
import art.arcane.iris.generation.runtime.EngineTarget;
import art.arcane.iris.generation.runtime.GenerationTransitionGate;
import art.arcane.iris.generation.mantle.ObjectContinuationBundle;
import art.arcane.iris.world.storage.matter.IrisMatterContext;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.volmlib.util.mantle.MantleRegionFiles;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.mantle.runtime.TectonicPlate;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterSlice;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -32,6 +39,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
private final IrisEngine engine;
private final GenerationHistory history;
private final SavedBiomeRuntime biomes;
private final HistoricalObjectContinuations objectContinuations;
private final GenerationBoundarySignatureSampler signatureSampler;
private final ActivationRuntimeFactory runtimeFactory;
private final LinkedHashMap<Long, RuntimeCacheEntry> bindings;
@@ -65,6 +73,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
this.operationDepth = ThreadLocal.withInitial(() -> 0);
this.scopedRoute = new ThreadLocal<>();
this.biomes = new SavedBiomeRuntime(engine, history);
this.objectContinuations = new HistoricalObjectContinuations(history, this::readObjectContinuationRegion);
GenerationActivation active = history.activeActivation();
GenerationEpoch epoch = requireEpoch(active);
@@ -96,6 +105,41 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
return biomes;
}
public List<ObjectContinuation> objectContinuations(TransitionGenerationPlan plan, int chunkX, int chunkZ) {
return objectContinuations.at(plan, chunkX, chunkZ);
}
public List<Matter> decodeObjectContinuations(List<ObjectContinuation> continuations) throws IOException {
ArrayList<Matter> decoded = new ArrayList<>(continuations.size());
enterOperation();
try {
int index = 0;
while (index < continuations.size()) {
long activationId = continuations.get(index).activationId();
GenerationActivation activation = history.manifest().activation(activationId)
.orElseThrow(() -> new IOException("Missing object continuation activation " + activationId));
SavedMantleAccess access = acquireSavedMantle(activation, requireEpoch(activation));
try (IrisMatterContext.Scope scope = IrisMatterContext.open(access.data())) {
do {
decoded.add(continuations.get(index++).fragment().decode());
} while (index < continuations.size() && continuations.get(index).activationId() == activationId);
} finally {
access.release().run();
}
}
return List.copyOf(decoded);
} finally {
leaveOperation();
}
}
public record ObjectContinuation(long activationId, ObjectContinuationBundle.Fragment fragment) {
}
public boolean allowsObjectFootprint(TransitionGenerationPlan plan, ObjectContinuationBundle.Bounds bounds) {
return objectContinuations.allowsFootprint(plan, bounds.minimumX(), bounds.minimumZ(), bounds.maximumX(), bounds.maximumZ());
}
public static GenerationHistoryRuntimeRouter attach(
IrisEngine engine,
GenerationHistory history,
@@ -461,6 +505,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
} catch (Throwable biomeFailure) {
failure = appendFailure(failure, biomeFailure);
}
objectContinuations.clear();
try {
retireBindings(retired);
} catch (Throwable retirementFailure) {
@@ -493,6 +538,45 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
return signatureSampler.open(engine);
}
private Map<Long, ObjectContinuationBundle> readObjectContinuationRegion(
GenerationActivation activation, int regionX, int regionZ
) throws IOException {
Path root = history.paths().activationMantleRoot(activation.activationId());
if (!MantleRegionFiles.fileForRegion(root.toFile(), Mantle.key(regionX, regionZ), false).isFile()) {
return Map.of();
}
enterOperation();
try {
SavedMantleAccess access = acquireSavedMantle(activation, requireEpoch(activation));
try {
Mantle<Matter> mantle = access.mantle();
MantleChunk<Matter> anchor = mantle.getChunk(regionX << 5, regionZ << 5).use();
try {
TectonicPlate<Matter> plate = mantle.getLoadedRegions().get(Mantle.key(regionX, regionZ));
HashMap<Long, ObjectContinuationBundle> bundles = new HashMap<>();
for (int x = 0; x < 32; x++) {
for (int z = 0; z < 32; z++) {
MantleChunk<Matter> chunk = plate.get(x, z);
Matter section = chunk == null ? null : chunk.get(0);
MatterSlice<ObjectContinuationBundle> slice = section == null ? null : section.getSlice(ObjectContinuationBundle.class);
ObjectContinuationBundle bundle = slice == null ? null : slice.get(0, 0, 0);
if (bundle != null && !bundle.fragments().isEmpty()) {
bundles.put(ChunkGenerationOwnership.packChunk((regionX << 5) + x, (regionZ << 5) + z), bundle);
}
}
}
return Map.copyOf(bundles);
} finally {
anchor.release();
}
} finally {
access.release().run();
}
} finally {
leaveOperation();
}
}
private SavedMantleAccess acquireSavedMantle(GenerationActivation activation, GenerationEpoch epoch) throws IOException {
SavedMantleEntry saved = null;
RuntimeCacheEntry runtime;
@@ -526,7 +610,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
if (runtime != null) {
RuntimeLease lease = awaitRuntime(runtime, activation.activationId());
try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope(lease.binding())) {
return new SavedMantleAccess(engine.getMantle().getMantle(), lease::close);
return new SavedMantleAccess(engine.getMantle().getMantle(), engine.getData(), lease::close);
} catch (Throwable failure) {
lease.close();
throw failure;
@@ -552,7 +636,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
entry.leases--;
throw propagate(entry.failure, "Unable to load saved mantle for activation " + entry.activationId + ".");
}
return new SavedMantleAccess(entry.mantle, () -> releaseSavedMantle(entry));
return new SavedMantleAccess(entry.mantle, entry.data, () -> releaseSavedMantle(entry));
} finally {
stateLock.unlock();
}
@@ -1064,7 +1148,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
}
}
private record SavedMantleAccess(Mantle<Matter> mantle, Runnable release) {
private record SavedMantleAccess(Mantle<Matter> mantle, IrisData data, Runnable release) {
}
private record SavedChunkResources(GenerationHistory.GenerationStage stage,
@@ -112,7 +112,7 @@ final class GenerationKernelV1 implements GenerationKernelRegistry.RuntimeFactor
public void registerStaticObjects(IrisEngine engine, EngineMode mode) {
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
mode.registerStage((x, z, blocks, biomes, multicore, context) ->
staticObjects.apply(engine, x, z, blocks));
staticObjects.forTransition(engine).apply(engine, x, z, blocks));
}
@Override
@@ -208,8 +208,8 @@ public final class GenerationSemanticCapture {
}
private static boolean hasFullHydrologyWeight(IrisComplex complex, int blockX, int blockZ) {
TransitionGenerationPlan transition = complex.getTransitionGenerationPlan();
return transition == null || transition.hydrologyWeightAt(blockX, blockZ) == 1D;
TransitionDisplacementField displacement = complex.getTransitionDisplacement();
return displacement == null || displacement.seamWeight(blockX, blockZ) == 1D;
}
private static void captureCave(
@@ -205,8 +205,8 @@ public final class GenerationSemanticQueries {
}
private static boolean allowsActiveHydrologyPrediction(Engine engine, int blockX, int blockZ) {
TransitionGenerationPlan transition = engine.getComplex().getTransitionGenerationPlan();
if (transition != null && transition.hydrologyWeightAt(blockX, blockZ) != 1D) {
TransitionDisplacementField displacement = engine.getComplex().getTransitionDisplacement();
if (displacement != null && displacement.seamWeight(blockX, blockZ) != 1D) {
return false;
}
return allowsActivePrediction(
@@ -0,0 +1,120 @@
package art.arcane.iris.world.history;
import art.arcane.iris.generation.mantle.ObjectContinuationBundle;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter.ObjectContinuation;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
final class HistoricalObjectContinuations {
private static final Comparator<ObjectContinuation> PLACEMENT_ORDER = Comparator
.comparingLong(ObjectContinuation::activationId)
.thenComparing(continuation -> continuation.fragment().key().kind())
.thenComparingInt(continuation -> continuation.fragment().key().sourceChunkX())
.thenComparingInt(continuation -> continuation.fragment().key().sourceChunkZ())
.thenComparingInt(continuation -> continuation.fragment().key().ordinal());
private static final long MAXIMUM_BYTES = 32L * 1024L * 1024L;
private final GenerationHistory history;
private final RegionReader reader;
private final Cache<RegionKey, Region> regions = Caffeine.newBuilder()
.maximumWeight(MAXIMUM_BYTES)
.weigher((RegionKey key, Region region) -> region.weight())
.build();
HistoricalObjectContinuations(GenerationHistory history, RegionReader reader) {
this.history = history;
this.reader = reader;
}
List<ObjectContinuation> at(TransitionGenerationPlan plan, int chunkX, int chunkZ) {
if (plan == null || plan.boundary().isHistoricalChunk(chunkX, chunkZ)) {
return List.of();
}
RegionKey key = new RegionKey(plan.activationId(), chunkX >> 5, chunkZ >> 5);
Region region = regions.get(key, ignored -> load(plan, key));
return region.chunks().getOrDefault(ChunkGenerationOwnership.packChunk(chunkX, chunkZ), List.of());
}
boolean allowsFootprint(TransitionGenerationPlan plan, int minimumX, int minimumZ, int maximumX, int maximumZ) {
for (int chunkX = minimumX >> 4; chunkX <= maximumX >> 4; chunkX++) {
for (int chunkZ = minimumZ >> 4; chunkZ <= maximumZ >> 4; chunkZ++) {
for (ObjectContinuation continuation : at(plan, chunkX, chunkZ)) {
ObjectContinuationBundle.Bounds bounds = continuation.fragment().bounds();
if (bounds.minimumX() <= maximumX && bounds.maximumX() >= minimumX
&& bounds.minimumZ() <= maximumZ && bounds.maximumZ() >= minimumZ) {
return false;
}
}
}
}
return true;
}
void clear() {
regions.invalidateAll();
}
private Region load(TransitionGenerationPlan plan, RegionKey key) {
HashMap<Long, List<ObjectContinuation>> chunks = new HashMap<>();
long weight = 256L;
try {
for (GenerationActivation activation : history.manifest().activations()) {
if (activation.activationId() >= key.activationId()) {
continue;
}
Map<Long, ObjectContinuationBundle> stored = reader.read(activation, key.x(), key.z());
for (Map.Entry<Long, ObjectContinuationBundle> entry : stored.entrySet()) {
int chunkX = ChunkGenerationOwnership.chunkX(entry.getKey());
int chunkZ = ChunkGenerationOwnership.chunkZ(entry.getKey());
if (plan.boundary().isHistoricalChunk(chunkX, chunkZ)) {
continue;
}
for (ObjectContinuationBundle.Fragment fragment : entry.getValue().fragments()) {
if (!intersectsSavedPlacement(plan, activation.activationId(), fragment)) {
continue;
}
chunks.computeIfAbsent(entry.getKey(), ignored -> new ArrayList<>()).add(new ObjectContinuation(activation.activationId(), fragment));
weight += 128L + fragment.encodedSize() + fragment.touchedChunks().size() * 16L;
}
}
}
} catch (IOException failure) {
throw new UncheckedIOException("Unable to read pending objects for transition " + key.activationId(), failure);
}
chunks.replaceAll((coordinate, fragments) -> {
fragments.sort(PLACEMENT_ORDER);
return List.copyOf(fragments);
});
return new Region(Map.copyOf(chunks), (int) Math.min(Integer.MAX_VALUE, weight));
}
private boolean intersectsSavedPlacement(TransitionGenerationPlan plan, long activationId,
ObjectContinuationBundle.Fragment fragment) {
for (ObjectContinuationBundle.ChunkPosition chunk : fragment.touchedChunks()) {
if (plan.boundary().isHistoricalChunk(chunk.x(), chunk.z())
&& history.resolveActivation(chunk.x(), chunk.z()).activationId() == activationId) {
return true;
}
}
return false;
}
@FunctionalInterface
interface RegionReader {
Map<Long, ObjectContinuationBundle> read(GenerationActivation activation, int regionX, int regionZ) throws IOException;
}
private record RegionKey(long activationId, int x, int z) {
}
private record Region(Map<Long, List<ObjectContinuation>> chunks, int weight) {
}
}
@@ -1,5 +1,7 @@
package art.arcane.iris.world.history;
import art.arcane.iris.world.storage.Durability;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -225,14 +227,14 @@ public final class SavedBiomeStore {
}
}
if (!unpublished) {
output.getChannel().force(true);
Durability.force(output.getChannel());
}
appendCompleted = true;
} catch (IOException failure) {
try {
output.setLength(offset);
if (!unpublished) {
output.getChannel().force(true);
Durability.force(output.getChannel());
}
} catch (IOException rollback) {
failure.addSuppressed(rollback);
@@ -467,7 +469,7 @@ public final class SavedBiomeStore {
while (header.hasRemaining()) {
output.write(header);
}
output.force(true);
Durability.force(output);
}
Files.move(temporary, region.path, StandardCopyOption.ATOMIC_MOVE);
forceDirectory(directory);
@@ -477,11 +479,11 @@ public final class SavedBiomeStore {
}
private static void forceDirectory(Path path) throws IOException {
if (File.separatorChar == '\\') {
if (File.separatorChar == '\\' || !Durability.enabled()) {
return;
}
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
channel.force(true);
Durability.force(channel);
} catch (UnsupportedOperationException failure) {
throw new IOException("Saved biome directory cannot be durability-synced: " + path, failure);
}
@@ -498,7 +500,7 @@ public final class SavedBiomeStore {
transfer(input, output, region.offsets[slot], region.lengths[slot] + 8L);
}
}
output.force(true);
Durability.force(output);
}
Files.move(temporary, region.path, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
synchronized (regions) {
@@ -591,7 +593,7 @@ public final class SavedBiomeStore {
private static void truncateTail(RandomAccessFile file, long length) throws IOException {
file.setLength(length);
file.getChannel().force(true);
Durability.force(file.getChannel());
}
private static int checksum(byte[] bytes) {
@@ -48,6 +48,7 @@ import java.util.PriorityQueue;
import java.util.TreeSet;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.function.Consumer;
import java.util.concurrent.atomic.LongAdder;
import java.util.zip.CRC32;
@@ -243,11 +244,6 @@ public final class TerrainBoundarySignatureStore {
long[] cellKeys = new long[cellCount];
Long2ObjectOpenHashMap<CellReference> cells = new Long2ObjectOpenHashMap<>(cellCount);
LinkedHashMap<String, List<TerrainBoundarySignature>> validated = new LinkedHashMap<>(
MAXIMUM_CACHED_SHARDS,
0.75F,
true
);
int countedEntries = 0;
long previousCellKey = 0L;
for (int index = 0; index < cellCount; index++) {
@@ -266,19 +262,6 @@ public final class TerrainBoundarySignatureStore {
throw invalid(snapshot, "truncated shard hash");
}
String hash = HexFormat.of().formatHex(hashBytes);
List<TerrainBoundarySignature> signatures = validated.get(hash);
if (signatures == null) {
Path shard = directory.resolve(shardFileName(hash));
requireRegularFile(shard, "Terrain boundary shard");
signatures = CellShard.read(shard, hash, cellX, cellZ);
validated.put(hash, signatures);
trim(validated);
} else {
signatures = relocate(signatures, cellX, cellZ);
}
if (signatures.size() != count) {
throw invalid(snapshot, "terrain signature shard count does not match its catalog entry");
}
countedEntries = Math.addExact(countedEntries, count);
cellKeys[index] = cellKey;
cells.put(cellKey, new CellReference(hash, count));
@@ -603,33 +586,6 @@ public final class TerrainBoundarySignatureStore {
}
}
private static List<TerrainBoundarySignature> relocate(
List<TerrainBoundarySignature> source,
int cellX,
int cellZ
) {
int minimumX = Math.multiplyExact(cellX, CELL_SIZE);
int minimumZ = Math.multiplyExact(cellZ, CELL_SIZE);
ArrayList<TerrainBoundarySignature> relocated = new ArrayList<>(source.size());
for (TerrainBoundarySignature signature : source) {
int localX = Math.floorMod(signature.blockX(), CELL_SIZE);
int localZ = Math.floorMod(signature.blockZ(), CELL_SIZE);
relocated.add(new TerrainBoundarySignature(
new TerrainBoundarySignature.Column(
minimumX + localX,
minimumZ + localZ,
signature.surfaceHeight(),
signature.oceanFloorHeight(),
signature.fluidHeight(),
signature.upperCeilingDepth()
),
signature.samples(),
signature.geometry()
));
}
return List.copyOf(relocated);
}
private static void trim(LinkedHashMap<?, ?> cache) {
while (cache.size() > MAXIMUM_CACHED_SHARDS) {
Iterator<?> entries = cache.entrySet().iterator();
@@ -776,6 +732,14 @@ public final class TerrainBoundarySignatureStore {
}
}
void forEachSignature(BlockBounds bounds, Consumer<TerrainBoundarySignature> consumer) {
try {
source.forEachSignature(bounds, consumer);
} catch (IOException error) {
throw new IllegalStateException("Unable to query terrain boundary signatures", error);
}
}
boolean intersectsTerrainBand(BlockBounds footprint, int width) {
try {
return source.intersectsTerrainBand(footprint, width);
@@ -1045,6 +1009,25 @@ public final class TerrainBoundarySignatureStore {
});
}
private void forEachSignature(BlockBounds bounds, Consumer<TerrainBoundarySignature> consumer) throws IOException {
visitSuperCells(bounds, superKey -> {
for (long cellKey : cellsBySuperCell.get(superKey)) {
BlockBounds cell = BlockBounds.cell(cellKey, CELL_SIZE);
if (cell.maximumX() < bounds.minimumX() || cell.minimumX() > bounds.maximumX()
|| cell.maximumZ() < bounds.minimumZ() || cell.minimumZ() > bounds.maximumZ()) {
continue;
}
for (TerrainBoundarySignature signature : load(cellKey, cells.get(cellKey))) {
if (signature.blockX() >= bounds.minimumX() && signature.blockX() <= bounds.maximumX()
&& signature.blockZ() >= bounds.minimumZ() && signature.blockZ() <= bounds.maximumZ()) {
consumer.accept(signature);
}
}
}
return false;
});
}
private boolean visitSuperCells(BlockBounds bounds, SuperCellVisitor visitor) throws IOException {
int size = CELL_SIZE * SUPER_CELL_SIZE;
int minimumX = saturatedFloorDiv(bounds.minimumX(), size);
@@ -0,0 +1,93 @@
package art.arcane.iris.world.history;
import java.util.concurrent.atomic.AtomicLongArray;
import java.util.concurrent.atomic.AtomicReferenceArray;
final class TransitionBandIndex {
private static final int MAXIMUM_CACHED_REGIONS = 1_024;
private final GenerationBoundary boundary;
private final TransitionBoundarySampler sampler;
private final AtomicReferenceArray<Region> regions = new AtomicReferenceArray<>(MAXIMUM_CACHED_REGIONS);
private final ThreadLocal<Region> lastRegion = new ThreadLocal<>();
TransitionBandIndex(GenerationBoundary boundary, TransitionBoundarySampler sampler) {
this.boundary = boundary;
this.sampler = sampler;
}
boolean contains(int chunkX, int chunkZ) {
long key = GenerationBoundary.packChunk(chunkX >> 5, chunkZ >> 5);
Region region = lastRegion.get();
if (region == null || region.key != key) {
region = region(key);
lastRegion.set(region);
}
int index = ((chunkZ & 31) << 5) | (chunkX & 31);
int word = index >>> 5;
int shift = (index & 31) << 1;
long state = (region.states.get(word) >>> shift) & 3L;
if (state != 0L) {
return state == 3L;
}
synchronized (region) {
long states = region.states.get(word);
state = (states >>> shift) & 3L;
if (state == 0L) {
state = intersects(chunkX, chunkZ) ? 3L : 1L;
region.states.set(word, states | (state << shift));
}
return state == 3L;
}
}
int cachedRegionCount() {
int count = 0;
for (int index = 0; index < regions.length(); index++) {
if (regions.get(index) != null) {
count++;
}
}
return count;
}
private Region region(long key) {
long mixed = key;
mixed = (mixed ^ (mixed >>> 33)) * 0xff51afd7ed558ccdL;
mixed = (mixed ^ (mixed >>> 33)) * 0xc4ceb9fe1a85ec53L;
int slot = (int) (mixed ^ (mixed >>> 33)) & (MAXIMUM_CACHED_REGIONS - 1);
Region replacement = null;
while (true) {
Region current = regions.get(slot);
if (current != null && current.key == key) {
return current;
}
if (replacement == null) {
replacement = new Region(key);
}
if (regions.compareAndSet(slot, current, replacement)) {
return replacement;
}
}
}
private boolean intersects(int chunkX, int chunkZ) {
if (boundary.isHistoricalChunk(chunkX, chunkZ)) {
return false;
}
int minimumX = Math.multiplyExact(chunkX, GenerationBoundary.CHUNK_SIZE);
int minimumZ = Math.multiplyExact(chunkZ, GenerationBoundary.CHUNK_SIZE);
return sampler.intersectsTerrainBand(minimumX, minimumZ,
Math.addExact(minimumX, GenerationBoundary.CHUNK_SIZE - 1),
Math.addExact(minimumZ, GenerationBoundary.CHUNK_SIZE - 1));
}
private static final class Region {
private final long key;
private final AtomicLongArray states = new AtomicLongArray(32);
private Region(long key) {
this.key = key;
}
}
}
@@ -59,13 +59,14 @@ final class TransitionBoundarySampler {
BoundaryGeometryInfluence geometryAt(int blockX, int blockZ) {
CandidateIndex candidates = chunkCache(blockX, blockZ).candidates;
Nearest nearest = candidates.nearest(blockX, blockZ, square(terrainWidth));
if (nearest.count == 0 || nearest.distancesSquared[0] >= square(terrainWidth)) {
int seamWidth = Math.min(4, terrainWidth);
Nearest nearest = candidates.nearest(blockX, blockZ, square(seamWidth));
if (nearest.count == 0 || nearest.distancesSquared[0] >= square(seamWidth)) {
return BoundaryGeometryInfluence.none();
}
double distance = Math.sqrt(nearest.distancesSquared[0]);
double weight = GenerationBlend.newEpochWeight(Math.max(0D, distance - 1D),
Math.max(1, terrainWidth - 1));
Math.max(1, seamWidth - 1));
int count = nearest.distancesSquared[0] <= 1D ? 1 : nearest.count;
double total = 0D;
for (int index = 0; index < count; index++) {
@@ -77,10 +78,7 @@ final class TransitionBoundarySampler {
nearest.signatures[index].geometry(),
(1D / Math.max(1D, nearest.distancesSquared[index])) / total));
}
int openingDepth = Math.min(8, Math.max(1, terrainWidth / 4));
double openingWeight = GenerationBlend.newEpochWeight(Math.max(0D, distance - openingDepth),
Math.max(1, terrainWidth - openingDepth));
return new BoundaryGeometryInfluence(weight, openingWeight, contributions);
return new BoundaryGeometryInfluence(weight, weight, contributions);
}
boolean intersectsTerrainBand(int minimumX, int minimumZ, int maximumX, int maximumZ) {
@@ -0,0 +1,252 @@
package art.arcane.iris.world.history;
import java.util.LinkedHashMap;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
public final class TransitionDisplacementField {
private static final int MAXIMUM_CACHED_CHUNKS = 256;
private static final int LOCAL_SUPPORT = 8;
private static final Sample NONE = new Sample(0D, Double.MAX_VALUE, 1D, 0D, 0D, 0D, null);
private final TransitionGenerationPlan plan;
private final Sources sources;
private final LinkedHashMap<Long, ChunkField> completed = new LinkedHashMap<>(64, 0.75F, true);
private final ConcurrentHashMap<Long, CompletableFuture<ChunkField>> pending = new ConcurrentHashMap<>();
private final ThreadLocal<ChunkField> local = new ThreadLocal<>();
public TransitionDisplacementField(TransitionGenerationPlan plan, Sources sources) {
this.plan = Objects.requireNonNull(plan);
this.sources = Objects.requireNonNull(sources);
}
public double height(int x, int z, double nativeHeight) {
double displacement = sample(x, z).displacement();
return displacement == 0D ? nativeHeight : Math.clamp(nativeHeight + displacement, 0D, sources.height() - 1D);
}
public double fluidHeight(int x, int z, double nativeHead) {
Sample sample = sample(x, z);
if (sample.newTerrainWeight() == 1D) {
return nativeHead;
}
double historicalHead = sample.wetWeight() * sample.wetHeight()
+ (1D - sample.wetWeight()) * Math.min(sample.dryHeight(), nativeHead);
return GenerationBlend.interpolate(historicalHead, nativeHead, sample.newTerrainWeight());
}
public String fluidStateKey(int x, int z) {
Sample sample = sample(x, z);
return sample.newTerrainWeight() < 0.5D ? sample.fluidStateKey() : null;
}
public double seamWeight(int x, int z) {
Sample sample = sample(x, z);
return sample.newTerrainWeight() == 1D ? 1D
: GenerationBlend.newEpochWeight(Math.max(0D, sample.distance() - 1D), 16);
}
public Sample sample(int x, int z) {
int chunkX = x >> 4;
int chunkZ = z >> 4;
if (!plan.hasTransitionAtChunk(chunkX, chunkZ)) {
return NONE;
}
long key = ChunkGenerationOwnership.packChunk(chunkX, chunkZ);
ChunkField field = local.get();
if (field == null || field.key() != key) {
field = chunk(chunkX, chunkZ, key);
local.set(field);
}
return field.samples()[(x & 15) * 16 + (z & 15)];
}
public void clear() {
synchronized (completed) {
completed.clear();
}
local.remove();
}
private ChunkField chunk(int chunkX, int chunkZ, long key) {
synchronized (completed) {
ChunkField cached = completed.get(key);
if (cached != null) {
return cached;
}
}
CompletableFuture<ChunkField> result = new CompletableFuture<>();
CompletableFuture<ChunkField> existing = pending.putIfAbsent(key, result);
if (existing != null) {
return existing.join();
}
try {
synchronized (completed) {
ChunkField cached = completed.get(key);
if (cached != null) {
result.complete(cached);
return cached;
}
}
ChunkField field = build(chunkX, chunkZ, key);
synchronized (completed) {
completed.put(key, field);
while (completed.size() > MAXIMUM_CACHED_CHUNKS) {
completed.pollFirstEntry();
}
}
result.complete(field);
return field;
} catch (RuntimeException | Error failure) {
result.completeExceptionally(failure);
throw failure;
} finally {
pending.remove(key, result);
}
}
private ChunkField build(int chunkX, int chunkZ, long key) {
int minimumX = Math.multiplyExact(chunkX, 16);
int minimumZ = Math.multiplyExact(chunkZ, 16);
Accumulator[] columns = new Accumulator[256];
double maximumSupport = 0D;
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
TransitionGenerationPlan.TerrainSample terrain = plan.terrainSampleAt(minimumX + x, minimumZ + z);
double distance = terrain.distanceToHistoricalTerrain();
double support = Math.min(plan.widthBlocks(), distance + LOCAL_SUPPORT);
double weight = terrain.hasHistoricalSignature()
? GenerationBlend.newEpochWeight(Math.max(0D, distance - 1D), Math.max(1, plan.widthBlocks() - 1))
: 1D;
columns[x * 16 + z] = new Accumulator(minimumX + x, minimumZ + z, distance, support, weight);
if (weight < 1D) {
maximumSupport = Math.max(maximumSupport, support);
}
}
}
long radius = (long) Math.ceil(maximumSupport);
plan.terrainSignatures().forEachSignature(new TerrainBoundarySignatureStore.BlockBounds(
minimumX - radius, minimumZ - radius, minimumX + 15L + radius, minimumZ + 15L + radius),
signature -> accumulate(columns, signature));
Sample[] samples = new Sample[256];
for (int index = 0; index < samples.length; index++) {
samples[index] = columns[index].finish();
}
return new ChunkField(key, samples);
}
private void accumulate(Accumulator[] columns, TerrainBoundarySignature signature) {
double residual = Double.NaN;
String fluidKey = null;
boolean wet = signature.fluidHeight().isPresent();
for (Accumulator column : columns) {
double weight = column.weight(signature.blockX(), signature.blockZ());
if (weight == 0D) {
continue;
}
if (Double.isNaN(residual)) {
double nativeHeight = sources.heights().height(signature.blockX(), signature.blockZ());
if (!Double.isFinite(nativeHeight)) {
throw new IllegalStateException("Transition anchor has a nonfinite native height");
}
residual = signature.oceanFloorHeight() - nativeHeight;
if (wet) {
int y = signature.geometry().minimumY() + signature.fluidHeight().getAsInt();
fluidKey = signature.geometry().voxelAt(y).fluidStateKey();
}
}
column.total += weight;
column.displacement += weight * residual;
if (wet) {
column.wetTotal += weight;
column.wetHeight += weight * signature.fluidHeight().getAsInt();
column.minimumWetHeight = Math.min(column.minimumWetHeight, signature.fluidHeight().getAsInt());
column.maximumWetHeight = Math.max(column.maximumWetHeight, signature.fluidHeight().getAsInt());
if (weight > column.fluidKeyWeight) {
column.fluidKeyWeight = weight;
column.fluidKey = fluidKey;
}
} else {
column.dryHeight += weight * signature.oceanFloorHeight();
column.minimumDryHeight = Math.min(column.minimumDryHeight, signature.oceanFloorHeight());
column.maximumDryHeight = Math.max(column.maximumDryHeight, signature.oceanFloorHeight());
}
}
}
public record Sources(HeightSource heights, int height) {
public Sources {
Objects.requireNonNull(heights);
if (height < 1) {
throw new IllegalArgumentException("Transition world height must be positive");
}
}
}
@FunctionalInterface
public interface HeightSource {
double height(int x, int z);
}
public record Sample(double displacement, double distance, double newTerrainWeight, double wetWeight,
double wetHeight, double dryHeight, String fluidStateKey) {
}
private record ChunkField(long key, Sample[] samples) {
}
private static final class Accumulator {
private final int x;
private final int z;
private final double distance;
private final double support;
private final double newWeight;
private double total;
private double displacement;
private double wetTotal;
private double wetHeight;
private double dryHeight;
private double minimumWetHeight = Double.POSITIVE_INFINITY;
private double maximumWetHeight = Double.NEGATIVE_INFINITY;
private double minimumDryHeight = Double.POSITIVE_INFINITY;
private double maximumDryHeight = Double.NEGATIVE_INFINITY;
private double fluidKeyWeight;
private String fluidKey;
private Accumulator(int x, int z, double distance, double support, double newWeight) {
this.x = x;
this.z = z;
this.distance = distance;
this.support = support;
this.newWeight = newWeight;
}
private double weight(int anchorX, int anchorZ) {
if (newWeight == 1D) {
return 0D;
}
double dx = (double) anchorX - x;
double dz = (double) anchorZ - z;
double squared = dx * dx + dz * dz;
if (squared >= support * support) {
return 0D;
}
double q = Math.sqrt(squared) / support;
double remainder = 1D - q;
return remainder * remainder * remainder * remainder * (1D + 4D * q)
/ Math.max(1D, squared * squared);
}
private Sample finish() {
if (total == 0D) {
return NONE;
}
double dryTotal = total - wetTotal;
return new Sample((1D - newWeight) * displacement / total, distance, newWeight,
Math.clamp(wetTotal / total, 0D, 1D), wetTotal == 0D ? 0D
: Math.clamp(wetHeight / wetTotal, minimumWetHeight, maximumWetHeight),
dryTotal <= 0D ? 0D : Math.clamp(dryHeight / dryTotal, minimumDryHeight, maximumDryHeight), fluidKey);
}
}
}
@@ -1,149 +0,0 @@
package art.arcane.iris.world.history;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.OptionalInt;
import java.util.Set;
public final class TransitionFluidContainment {
private static final Set<String> BANK_MATERIALS = Set.of(
"minecraft:stone", "minecraft:deepslate", "minecraft:granite", "minecraft:diorite",
"minecraft:andesite", "minecraft:tuff", "minecraft:calcite", "minecraft:bedrock",
"minecraft:dirt", "minecraft:coarse_dirt", "minecraft:grass_block", "minecraft:podzol",
"minecraft:mycelium", "minecraft:clay", "minecraft:terracotta", "minecraft:netherrack",
"minecraft:basalt", "minecraft:smooth_basalt", "minecraft:blackstone", "minecraft:end_stone",
"minecraft:obsidian", "minecraft:crying_obsidian");
private static final BoundaryColumnGeometry.Voxel STONE = new BoundaryColumnGeometry.Voxel(
"minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false);
private TransitionFluidContainment() {
}
public static SavedTerrainChunk contain(SavedTerrainChunk raw, TransitionGenerationPlan plan, ColumnSource source)
throws IOException {
List<BoundaryColumnGeometry> columns = new ArrayList<>(256);
boolean changed = false;
for (int localX = 0; localX < 16; localX++) {
for (int localZ = 0; localZ < 16; localZ++) {
int x = Math.addExact(Math.multiplyExact(raw.chunkX(), 16), localX);
int z = Math.addExact(Math.multiplyExact(raw.chunkZ(), 16), localZ);
BoundaryColumnGeometry original = raw.column(x, z).geometry();
BoundaryColumnGeometry corrected = plan.isHistoricalBlock(x, z) || plan.newEpochWeightAt(x, z) == 1D
? original : containColumn(x, z, original, source);
columns.add(corrected);
changed |= corrected != original;
}
}
if (!changed) {
return raw;
}
BoundaryColumnGeometry first = columns.getFirst();
return SavedTerrainChunk.capture(raw.chunkX(), raw.chunkZ(), first.minimumY(), first.height(),
raw.nativeStatus(), new ContainedSource(raw, columns));
}
private static BoundaryColumnGeometry containColumn(int x, int z, BoundaryColumnGeometry original,
ColumnSource source) throws IOException {
List<BoundaryColumnGeometry.Voxel> voxels = original.voxels();
List<BoundaryColumnGeometry.Voxel> corrected = null;
BoundaryColumnGeometry.Voxel[] banks = null;
BoundaryColumnGeometry[] neighbors = null;
for (int offset = 0; offset < voxels.size(); offset++) {
BoundaryColumnGeometry.Voxel voxel = voxels.get(offset);
if (voxel.phase() != BoundaryColumnGeometry.Phase.FLUID || voxel.protectedContent()) {
continue;
}
if (neighbors == null) {
neighbors = new BoundaryColumnGeometry[]{
source.column(Math.subtractExact(x, 1), z), source.column(Math.addExact(x, 1), z),
source.column(x, Math.subtractExact(z, 1)), source.column(x, Math.addExact(z, 1))};
}
int worldY = Math.addExact(original.minimumY(), offset);
if (!exposed(original, neighbors, worldY)) {
continue;
}
if (corrected == null) {
corrected = new ArrayList<>(voxels);
banks = nearestDrySolids(voxels);
}
corrected.set(offset, banks[offset]);
}
return corrected == null ? original : BoundaryColumnGeometry.fromVoxels(original.minimumY(), corrected);
}
private static boolean exposed(BoundaryColumnGeometry original, BoundaryColumnGeometry[] neighbors, int worldY) {
if (original.voxelAt(Math.subtractExact(worldY, 1)).phase() == BoundaryColumnGeometry.Phase.AIR) {
return true;
}
for (BoundaryColumnGeometry neighbor : neighbors) {
if (neighbor.voxelAt(worldY).phase() == BoundaryColumnGeometry.Phase.AIR) {
return true;
}
}
return false;
}
private static BoundaryColumnGeometry.Voxel[] nearestDrySolids(List<BoundaryColumnGeometry.Voxel> voxels) {
BoundaryColumnGeometry.Voxel[] materials = new BoundaryColumnGeometry.Voxel[voxels.size()];
int[] distances = new int[voxels.size()];
BoundaryColumnGeometry.Voxel nearest = STONE;
int last = -voxels.size() - 1;
for (int offset = 0; offset < voxels.size(); offset++) {
if (drySolid(voxels.get(offset))) {
nearest = voxels.get(offset);
last = offset;
}
materials[offset] = nearest;
distances[offset] = offset - last;
}
last = voxels.size() * 2;
nearest = null;
for (int offset = voxels.size() - 1; offset >= 0; offset--) {
if (drySolid(voxels.get(offset))) {
nearest = voxels.get(offset);
last = offset;
}
if (nearest != null && last - offset < distances[offset]) {
materials[offset] = nearest;
}
}
return materials;
}
private static boolean drySolid(BoundaryColumnGeometry.Voxel voxel) {
String state = voxel.stateKey();
int properties = state.indexOf('[');
String key = properties < 0 ? state : state.substring(0, properties);
return voxel.phase() == BoundaryColumnGeometry.Phase.SOLID
&& voxel.fluidStateKey().isEmpty() && !voxel.protectedContent() && BANK_MATERIALS.contains(key);
}
@FunctionalInterface
public interface ColumnSource {
BoundaryColumnGeometry column(int blockX, int blockZ) throws IOException;
}
private record ContainedSource(SavedTerrainChunk raw, List<BoundaryColumnGeometry> columns)
implements SavedTerrainChunk.VoxelSource {
@Override
public OptionalInt groundSurface(int localX, int localZ) {
return OptionalInt.of(signature(localX, localZ).oceanFloorHeight());
}
@Override
public BoundaryColumnGeometry.Voxel voxel(int localX, int worldY, int localZ) {
return columns.get(localX * 16 + localZ).voxelAt(worldY);
}
@Override
public String biome(int localX, int worldY, int localZ) {
TerrainBoundarySignature signature = signature(localX, localZ);
return signature.biomeAtSample((worldY - signature.samples().layout().minimumY()) / 4);
}
private TerrainBoundarySignature signature(int x, int z) {
return raw.column((raw.chunkX() << 4) + x, (raw.chunkZ() << 4) + z);
}
}
}
@@ -8,6 +8,8 @@ public final class TransitionGenerationPlan {
private final GenerationBoundary boundary;
private final TerrainBoundarySignatureStore.Snapshot terrainSignatures;
private final TransitionBoundarySampler terrainSampler;
private final TransitionBandIndex terrainBand;
private final TerrainSample newTerrain;
public TransitionGenerationPlan(
Specification specification,
@@ -34,6 +36,8 @@ public final class TransitionGenerationPlan {
specification.widthBlocks(),
terrainSignatures
);
terrainBand = new TransitionBandIndex(boundary, terrainSampler);
newTerrain = TerrainSample.newTerrain(specification.widthBlocks());
}
public long activationId() {
@@ -93,18 +97,11 @@ public final class TransitionGenerationPlan {
}
public boolean hasTransitionAtChunk(int chunkX, int chunkZ) {
if (boundary.isHistoricalChunk(chunkX, chunkZ)) {
return false;
}
int minimumX = Math.multiplyExact(chunkX, GenerationBoundary.CHUNK_SIZE);
int minimumZ = Math.multiplyExact(chunkZ, GenerationBoundary.CHUNK_SIZE);
return terrainSampler.intersectsTerrainBand(minimumX, minimumZ,
Math.addExact(minimumX, GenerationBoundary.CHUNK_SIZE - 1),
Math.addExact(minimumZ, GenerationBoundary.CHUNK_SIZE - 1));
return terrainBand.contains(chunkX, chunkZ);
}
public BoundaryGeometryInfluence geometryAt(int blockX, int blockZ) {
if (boundary.isHistoricalBlock(blockX, blockZ)) {
if (!terrainBand.contains(blockX >> 4, blockZ >> 4)) {
return BoundaryGeometryInfluence.none();
}
return terrainSampler.geometryAt(blockX, blockZ);
@@ -114,7 +111,7 @@ public final class TransitionGenerationPlan {
if (boundary.isHistoricalBlock(blockX, blockZ)) {
return TerrainSample.historicalTerrain();
}
return terrainSampler.sample(blockX, blockZ);
return terrainBand.contains(blockX >> 4, blockZ >> 4) ? terrainSampler.sample(blockX, blockZ) : newTerrain;
}
public Optional<String> historicalPhysicalBiomeKeyAt(int blockX, int blockY, int blockZ) {
@@ -128,12 +125,7 @@ public final class TransitionGenerationPlan {
TerrainSample sample
) {
Objects.requireNonNull(sample, "Terrain sample");
if (sample.newEpochWeight() == 1D) {
return Optional.empty();
}
double weight = GenerationBlend.newEpochWeight(
Math.max(0D, sample.distanceToHistoricalTerrain() - 1D), Math.max(1, widthBlocks() - 1));
return GenerationBlend.usesHistoricalMaterial(blockX, blockY, blockZ, weight)
return sample.distanceToHistoricalTerrain() < Math.min(4, widthBlocks())
? sample.historicalPhysicalBiomeKeyAt(blockY) : Optional.empty();
}
@@ -5,11 +5,8 @@ import java.util.List;
import java.util.Objects;
public final class TransitionGeometryBlender {
private static final int MAXIMUM_OPENING_HEIGHT = 64;
private static final BoundaryColumnGeometry.Voxel AIR = new BoundaryColumnGeometry.Voxel(
"minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false);
private static final BoundaryColumnGeometry.Voxel FLUID_BARRIER = new BoundaryColumnGeometry.Voxel(
"minecraft:obsidian", BoundaryColumnGeometry.Phase.SOLID, "", false);
private TransitionGeometryBlender() {
}
@@ -49,8 +46,7 @@ public final class TransitionGeometryBlender {
boolean changed = false;
for (int offset = 0; offset < currentGeometry.height(); offset++) {
BoundaryColumnGeometry.Voxel value = blendVoxel(current, historical, offset,
influence.newTerrainWeight(), influence.openingWeight(), GenerationBlend.usesHistoricalMaterial(blockX,
currentGeometry.minimumY() + offset, blockZ, influence.newTerrainWeight()));
influence.newTerrainWeight());
result.add(value);
changed |= !value.equals(current.voxelAt(offset));
}
@@ -61,88 +57,58 @@ public final class TransitionGeometryBlender {
ColumnProfile current,
List<WeightedProfile> historical,
int offset,
double currentWeight,
double openingWeight,
boolean historicalMaterial
double currentWeight
) {
BoundaryColumnGeometry.Voxel currentVoxel = current.voxelAt(offset);
if (currentVoxel.protectedContent()) {
if (currentVoxel.protectedContent() || offset == 0) {
return currentVoxel;
}
boolean historicalOpening = false;
double solid = 0D;
double fluid = 0D;
double total = 0D;
double solidMaterialWeight = -1D;
double fluidMaterialWeight = -1D;
BoundaryColumnGeometry.Voxel oldSolid = null;
BoundaryColumnGeometry.Voxel oldFluid = null;
double fluidWeight = 0D;
for (WeightedProfile contribution : historical) {
ColumnProfile profile = contribution.profile();
if (profile.voxelAt(offset).protectedContent()) {
BoundaryColumnGeometry.Voxel oldVoxel = profile.voxelAt(offset);
if (oldVoxel.protectedContent()) {
continue;
}
historicalOpening |= profile.enclosedOpeningAt(offset);
double weight = contribution.weight();
total += weight;
solid += profile.solid.distanceAt(offset) * weight;
fluid += profile.fluid.distanceAt(offset) * weight;
if (profile.solid.materialAt(offset) != null && weight > solidMaterialWeight) {
total += contribution.weight();
solid += profile.solid.distanceAt(offset) * contribution.weight();
if (oldSolid == null) {
oldSolid = profile.solid.materialAt(offset);
solidMaterialWeight = weight;
}
if (profile.fluid.materialAt(offset) != null && weight > fluidMaterialWeight) {
oldFluid = profile.fluid.materialAt(offset);
fluidMaterialWeight = weight;
if (oldVoxel.phase() == BoundaryColumnGeometry.Phase.FLUID
&& profile.geometry.hasSolidAbove(profile.geometry.minimumY() + offset)
&& contribution.weight() > fluidWeight) {
oldFluid = oldVoxel;
fluidWeight = contribution.weight();
}
}
if (total == 0D) {
return currentVoxel;
}
double blendedSolid = GenerationBlend.interpolate(solid / total,
current.solid.distanceAt(offset), historicalOpening ? openingWeight : currentWeight);
current.solid.distanceAt(offset), currentWeight);
if (blendedSolid > 0D) {
return selectMaterial(oldSolid, current.solid.materialAt(offset), historicalMaterial);
BoundaryColumnGeometry.Voxel material = current.solid.materialAt(offset);
return material != null ? material : oldSolid != null ? oldSolid : currentVoxel;
}
double blendedFluid = GenerationBlend.interpolate(fluid / total,
current.fluid.distanceAt(offset), historicalOpening ? openingWeight : currentWeight);
if (blendedFluid > 0D) {
BoundaryColumnGeometry.Voxel newFluid = current.fluid.materialAt(offset);
if (oldFluid != null && newFluid != null && !fluidFamily(oldFluid).equals(fluidFamily(newFluid))) {
return FLUID_BARRIER;
}
return selectMaterial(oldFluid, newFluid, currentWeight <= 0.5D);
if (currentVoxel.phase() == BoundaryColumnGeometry.Phase.FLUID) {
return currentVoxel;
}
return AIR;
}
private static String fluidFamily(BoundaryColumnGeometry.Voxel voxel) {
String key = voxel.fluidStateKey();
int properties = key.indexOf('[');
return properties < 0 ? key : key.substring(0, properties);
}
private static BoundaryColumnGeometry.Voxel selectMaterial(
BoundaryColumnGeometry.Voxel historical,
BoundaryColumnGeometry.Voxel current,
boolean historicalMaterial
) {
if (historical == null) {
return current == null ? AIR : current;
}
return current == null || historicalMaterial ? historical : current;
return oldFluid != null && currentWeight < 0.5D ? oldFluid : AIR;
}
private static final class ColumnProfile {
private final BoundaryColumnGeometry geometry;
private final PhaseCursor solid;
private final PhaseCursor fluid;
private int run;
private ColumnProfile(BoundaryColumnGeometry geometry) {
this.geometry = geometry;
solid = new PhaseCursor(geometry, BoundaryColumnGeometry.Phase.SOLID);
fluid = new PhaseCursor(geometry, BoundaryColumnGeometry.Phase.FLUID);
}
private BoundaryColumnGeometry.Voxel voxelAt(int offset) {
@@ -152,12 +118,6 @@ public final class TransitionGeometryBlender {
return geometry.runVoxel(run);
}
private boolean enclosedOpeningAt(int offset) {
solid.advanceTo(offset);
return !solid.occupied && solid.start > 0 && solid.end < geometry.height()
&& solid.end - solid.start <= MAXIMUM_OPENING_HEIGHT
&& !voxelAt(offset).protectedContent();
}
}
private static final class PhaseCursor {
@@ -40,6 +40,7 @@ public final class IrisMatterSupport {
}
IrisMatter.registerSliceType(new EntityMatter());
IrisMatter.registerSliceType(new ObjectContinuationMatter());
IrisMatter.registerSliceType("art.arcane.iris.core.link.Identifier", new IdentifierMatter());
IrisMatter.registerSliceType("art.arcane.iris.engine.framework.NativeStructureOwnershipBundle", new NativeStructureOwnershipMatter());
IrisMatter.registerSliceType(new PlatformBlockMatter());
@@ -0,0 +1,36 @@
package art.arcane.iris.world.storage.matter;
import art.arcane.iris.generation.mantle.ObjectContinuationBundle;
import art.arcane.volmlib.util.data.palette.Palette;
import art.arcane.volmlib.util.matter.Sliced;
import art.arcane.volmlib.util.matter.slices.RawMatter;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
@Sliced
public final class ObjectContinuationMatter extends RawMatter<ObjectContinuationBundle> {
public ObjectContinuationMatter() {
this(1, 1, 1);
}
public ObjectContinuationMatter(int width, int height, int depth) {
super(width, height, depth, ObjectContinuationBundle.class);
}
@Override
public Palette<ObjectContinuationBundle> getGlobalPalette() {
return null;
}
@Override
public void writeNode(ObjectContinuationBundle bundle, DataOutputStream output) throws IOException {
bundle.write(output);
}
@Override
public ObjectContinuationBundle readNode(DataInputStream input) throws IOException {
return ObjectContinuationBundle.read(input);
}
}