Integrate optional terrain transforms

- Add versioned terrain providers that run before content placement.
- Resolve transformed terrain heights, occupancy, and cave metadata through bounded caches.
- Initialize native structures after binding the modded generation runtime.
- Cover transform behavior, provider resolution, and structure binding with regression tests.
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-02 05:39:20 -04:00
1 parent 74df63f717
commit 25c04c0157
35 files changed
+2160 -21

No files matched your search

@@ -29,6 +29,7 @@ public final class CaveTerrainSnapshot {
private final MatterCavern[][] caverns;
private final HydrologyCaveCell[][] hydrology;
private boolean hydrologyPresent;
private volatile long retainedBytes;
private CaveTerrainSnapshot(int chunkX, int chunkZ, int sections) {
this.chunkX = chunkX;
@@ -63,6 +64,36 @@ public final class CaveTerrainSnapshot {
return hydrologyPresent;
}
public long estimatedRetainedBytes() {
long cached = retainedBytes;
if (cached != 0L) {
return cached;
}
long bytes = 80L + ((long) caverns.length + hydrology.length) * 8L;
for (MatterCavern[] section : caverns) {
if (section != null) {
bytes += 16L + (long) section.length * 8L;
for (MatterCavern cell : section) {
if (cell != null) {
bytes += 96L + (long) cell.getCustomBiome().length() * 2L;
}
}
}
}
for (HydrologyCaveCell[] section : hydrology) {
if (section != null) {
bytes += 16L + (long) section.length * 8L;
for (HydrologyCaveCell cell : section) {
if (cell != null) {
bytes += 208L + ((long) cell.fluidProfileKey().length() + cell.floodedBiomeKey().length()) * 2L;
}
}
}
}
retainedBytes = bytes;
return bytes;
}
public MatterCavern cavern(int x, int y, int z) {
if (y < 0) {
return null;
@@ -28,6 +28,7 @@ import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.EngineTarget;
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.Terrain3DColumn;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveStorage;
import art.arcane.iris.pack.value.IrisPosition;
@@ -108,6 +109,12 @@ public interface EngineMantle extends MatterGenerator {
}
default int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
IrisComplex complex = getComplex();
int transformed = complex == null || complex.getTerrainTransform() == null ? Integer.MIN_VALUE
: complex.transformedHeight(x, z, ignoreFluid);
if (transformed != Integer.MIN_VALUE) {
return transformed;
}
return ignoreFluid ? trueHeight(x, z) : Math.max(trueHeight(x, z), getFluidHeight(x, z));
}
@@ -116,6 +123,10 @@ public interface EngineMantle extends MatterGenerator {
}
default boolean isCarved(int x, int h, int z) {
Terrain3DColumn transformed = getComplex().transformedColumn(x, z);
if (transformed != null) {
return h >= 0 && h < transformed.topY() && !transformed.isSolid(h);
}
HydrologyCaveCell hydrology = HydrologyCaveStorage.getIfPresent(getMantle(), x, h, z);
if (hydrology != null) {
return hydrology.carves();
@@ -142,6 +153,12 @@ public interface EngineMantle extends MatterGenerator {
}
default int getFluidHeight(int x, int z) {
IrisComplex complex = getComplex();
int transformed = complex == null || complex.getTerrainTransform() == null ? Integer.MIN_VALUE
: complex.transformedFluidHeight(x, z);
if (transformed != Integer.MIN_VALUE) {
return transformed;
}
return (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(x, z));
}
@@ -582,6 +582,10 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
}
public boolean isPrerequisiteCarved(int x, int y, int z) {
Terrain3DColumn transformed = engineMantle.getComplex().transformedColumn(x, z);
if (transformed != null) {
return y >= 0 && y < transformed.topY() && !transformed.isSolid(y);
}
HydrologyCaveCell hydrology = getPrerequisiteDataIfPresent(x, y, z, HydrologyCaveCell.class);
if (hydrology != null) {
return hydrology.carves();
@@ -592,6 +596,10 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
public byte[] getPrerequisiteCarvedColumn(int x, int z, int height) {
int cappedHeight = Math.min(Math.max(height, 0), mantle.getWorldHeight());
Terrain3DColumn transformed = engineMantle.getComplex().transformedColumn(x, z);
if (transformed != null) {
return carvedColumn(transformed, cappedHeight);
}
byte[] carvedColumn = new byte[cappedHeight];
if (cappedHeight <= 0) {
return carvedColumn;
@@ -961,6 +969,10 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
@Override
public boolean isCarved(int x, int y, int z) {
Terrain3DColumn transformed = engineMantle.getComplex().transformedColumn(x, z);
if (transformed != null) {
return y >= 0 && y < transformed.topY() && !transformed.isSolid(y);
}
HydrologyCaveCell hydrology = getDataIfPresent(x, y, z, HydrologyCaveCell.class);
if (hydrology != null) {
return hydrology.carves();
@@ -972,6 +984,10 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
public byte[] getCarvedColumn(int x, int z, int height) {
int cappedHeight = Math.min(Math.max(height, 0), mantle.getWorldHeight());
Terrain3DColumn transformed = engineMantle.getComplex().transformedColumn(x, z);
if (transformed != null) {
return carvedColumn(transformed, cappedHeight);
}
byte[] carvedColumn = new byte[cappedHeight];
if (cappedHeight <= 0) {
return carvedColumn;
@@ -1589,6 +1605,17 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
}
}
private static byte[] carvedColumn(Terrain3DColumn terrain, int height) {
byte[] result = new byte[height];
int maximumY = Math.min(height, terrain.topY());
for (int y = 0; y < maximumY; y++) {
if (!terrain.isSolid(y)) {
result[y] = 1;
}
}
return result;
}
public final class ObjectPlacementCapture implements AutoCloseable {
private final ObjectContinuationBundle.PlacementKey key;
private final Long2ObjectOpenHashMap<Matter> fragments = new Long2ObjectOpenHashMap<>();
@@ -31,6 +31,7 @@ import art.arcane.iris.generation.stage.IrisDepositModifier;
import art.arcane.iris.generation.stage.IrisFloatingChildBiomeModifier;
import art.arcane.iris.generation.stage.IrisPerfectionModifier;
import art.arcane.iris.generation.stage.IrisPostModifier;
import art.arcane.iris.generation.context.ChunkContext;
public class ModeOverworld extends IrisEngineMode implements EngineMode {
public ModeOverworld(Engine engine) {
@@ -45,7 +46,7 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
IrisCustomModifier custom = new IrisCustomModifier(getEngine());
IrisFloatingChildBiomeModifier floatingChildBiomes = new IrisFloatingChildBiomeModifier(getEngine());
EngineStage sBiome = (x, z, k, p, m, c) -> biome.actuate(x, z, p, m, c);
EngineStage sGenMatter = (x, z, k, p, m, c) -> generateTerrainMatter(x >> 4, z >> 4, m || getEngine().isStudio(), c);
EngineStage sGenMatter = (x, z, k, p, m, c) -> generateTerrainMatterForChunk(x >> 4, z >> 4, m, c);
EngineStage sTerrain = (x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c);
EngineStage sDecorant = (x, z, k, p, m, c) -> decorant.actuate(x, z, k, m, c);
EngineStage sCave = (x, z, k, p, m, c) -> cave.modify(x >> 4, z >> 4, k, m, c);
@@ -82,4 +83,10 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
registerStage(sCustom);
}
}
void generateTerrainMatterForChunk(int x, int z, boolean multicore, ChunkContext context) {
boolean studioParallel = getEngine().isStudio()
&& !(context.isNaturalTerrain() && context.getComplex().getTerrainTransform() != null);
generateTerrainMatter(x, z, multicore || studioParallel, context);
}
}
@@ -125,8 +125,8 @@ public final class DimensionTerrainContext implements DataProvider {
engine.getDimension(),
engine.getData(),
engine.getHeight(),
natural ? complex.getNaturalHeightStream() : complex.getHeightStream(),
natural ? complex.getNaturalSlopeStream() : complex.getSlopeStream(),
natural ? complex.getNaturalHeightStream() : complex.getRawHeightStream(),
natural ? complex.getNaturalSlopeStream() : complex.getRawSlopeStream(),
fluidHeightStream,
natural ? complex.getNaturalTrueBiomeStream() : complex.getTrueBiomeStream(),
complex.getRegionStream(),
@@ -653,7 +653,7 @@ public final class DimensionTerrainContext implements DataProvider {
if (selfReferencing) {
return naturalSelf || naturalFallback
? engine.getComplex().naturalTerrainColumn(x, z)
: engine.getComplex().terrainColumn(x, z);
: engine.getComplex().originalTerrainColumn(x, z);
}
if (terrain3D == null) {
return null;
@@ -567,6 +567,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
/** The terrain slope at a column, answered from natural terrain when the server thread may not wait. */
@BlockCoordinates
default double getSlope(int x, int z) {
IrisComplex complex = getComplex();
if (complex != null && complex.getTerrainTransform() != null) {
return complex.getSlopeStream().getDouble(x, z);
}
if (answersFromNaturalTerrain(x, z)) {
return getComplex().naturalSlope(x, z);
}
@@ -580,6 +584,12 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
@BlockCoordinates
default int getHeight(int x, int z, boolean ignoreFluid) {
IrisComplex complex = getComplex();
int transformed = complex == null || complex.getTerrainTransform() == null ? Integer.MIN_VALUE
: complex.transformedHeight(x, z, ignoreFluid);
if (transformed != Integer.MIN_VALUE) {
return transformed;
}
DimensionStackContext dimensionStackContext = getDimensionStackContext();
if (dimensionStackContext != null) {
return ignoreFluid
@@ -595,6 +605,12 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
@BlockCoordinates
static int hostHeight(Engine engine, int x, int z, boolean ignoreFluid) {
IrisComplex complex = engine.getComplex();
int transformed = complex == null || complex.getTerrainTransform() == null ? Integer.MIN_VALUE
: complex.transformedHeight(x, z, ignoreFluid);
if (transformed != Integer.MIN_VALUE) {
return transformed;
}
EngineMantle mantle = engine.getMantle();
if (mantle == null) {
return engine.getHeight(x, z, ignoreFluid);
@@ -213,7 +213,11 @@ public interface EngineMode extends Staged {
EngineStage[] stages = getStages().toArray(new EngineStage[0]);
try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), generationSessionId, ctx)) {
generateTerrain(x, z, blocks, biomes, multicore, ctx);
if (getComplex().getTerrainTransform() == null) {
generateTerrain(x, z, blocks, biomes, multicore, ctx);
} else {
getComplex().getTerrainTransform().generate(x, z, blocks, biomes, multicore, ctx);
}
recordNaturalTerrain(x, z, blocks, biomes, ctx);
IrisDimensionStackActuator.clearHostNaturalMetadata(getEngine(), x, z, biomes.getHeight(), ctx);
ctx.beginContent();
@@ -54,6 +54,10 @@ import art.arcane.iris.generation.noise.IrisGenerator;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.terrain.transform.TerrainTransformRegistry;
import art.arcane.iris.generation.terrain.transform.TerrainTransformRuntime;
import art.arcane.iris.generation.terrain.transform.TerrainTransformer;
import art.arcane.iris.generation.noise.IrisInterpolator;
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
import art.arcane.iris.generation.terrain.IrisRegion;
@@ -185,6 +189,20 @@ public class IrisComplex implements DataProvider {
private final ProceduralTerrainHeightSampler proceduralTerrainHeight;
@Getter(AccessLevel.NONE)
private final transient Engine terrainEngine;
@Setter(AccessLevel.NONE)
private final transient TerrainTransformRuntime terrainTransform;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient ProceduralStream<Double> transformedHeightStream;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient ProceduralStream<Integer> transformedRoundedHeightStream;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient ProceduralStream<Double> transformedTopStream;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient ProceduralStream<Double> transformedSlopeStream;
@Getter(AccessLevel.NONE)
private transient Terrain3DRuntime terrain3D;
private transient HydrologyBankTerrainRuntime hydrologyBanks3D;
@@ -494,6 +512,25 @@ public class IrisComplex implements DataProvider {
d.hashCode());
}), "", engine, cacheSize);
//@done
TerrainTransformer transformer = engine.getDimension().getTerrainTransform() == null ? null
: TerrainTransformRegistry.discover().resolve(engine.getDimension().getTerrainTransform());
terrainTransform = transformer == null ? null
: new TerrainTransformRuntime(new TerrainTransformRuntime.Options(engine, this), transformer);
transformedHeightStream = transformer == null ? null : ProceduralStream.ofDouble((x, z) -> {
int transformed = transformedHeight((int) Math.floor(x), (int) Math.floor(z), true);
return transformed == Integer.MIN_VALUE ? heightStream.getDouble(x, z) : transformed;
});
transformedRoundedHeightStream = transformer == null ? null : ProceduralStream.ofDouble((x, z) -> {
int transformed = transformedHeight((int) Math.floor(x), (int) Math.floor(z), true);
return transformed == Integer.MIN_VALUE ? roundedHeighteightStream.get(x, z) : transformed;
}).round();
transformedTopStream = transformer == null ? null : ProceduralStream.ofDouble((x, z) -> {
int transformed = transformedHeight((int) Math.floor(x), (int) Math.floor(z), false);
return transformed == Integer.MIN_VALUE ? heightFluidStream.getDouble(x, z) : transformed;
});
ProceduralStream<Double> effectiveSlope = transformer == null ? null : transformedHeightStream.slope(3);
transformedSlopeStream = transformer == null ? null : ProceduralStream.ofDouble((x, z) ->
isNaturalTerrainContext() ? slopeStream.getDouble(x, z) : effectiveSlope.getDouble(x, z));
}
/**
@@ -689,6 +726,14 @@ public class IrisComplex implements DataProvider {
}
public Terrain3DColumn terrainColumn(int x, int z) {
Terrain3DColumn transformed = transformedColumn(x, z);
if (transformed != null) {
return transformed;
}
return originalTerrainColumn(x, z);
}
Terrain3DColumn originalTerrainColumn(int x, int z) {
if (!hasTerrain3D()) {
return null;
}
@@ -1204,7 +1249,70 @@ public class IrisComplex implements DataProvider {
}
public ProceduralStream<Double> getHeightStream() {
return heightStream;
return terrainTransform == null ? heightStream : transformedHeightStream;
}
ProceduralStream<Double> getRawSlopeStream() {
return terrainEngine.getDimension().getTerrainTransform() == null ? slopeStream : heightStream.slope(3);
}
public ProceduralStream<Integer> getRoundedHeighteightStream() {
return terrainTransform == null ? roundedHeighteightStream : transformedRoundedHeightStream;
}
public ProceduralStream<Double> getHeightFluidStream() {
return terrainTransform == null ? heightFluidStream : transformedTopStream;
}
public ProceduralStream<Double> getSlopeStream() {
return terrainTransform == null ? slopeStream : transformedSlopeStream;
}
public int transformedHeight(int x, int z, boolean ignoreFluid) {
if (terrainTransform == null || isNaturalTerrainContext()) {
return Integer.MIN_VALUE;
}
return readsReadyTerrainOnly()
? terrainTransform.readyHeight(x, z, ignoreFluid)
: terrainTransform.height(x, z, ignoreFluid);
}
public Terrain3DColumn transformedColumn(int x, int z) {
if (terrainTransform == null || isNaturalTerrainContext()) {
return null;
}
return readsReadyTerrainOnly()
? terrainTransform.readyColumn(x, z) : terrainTransform.column(x, z);
}
public int transformedFluidHeight(int x, int z) {
if (terrainTransform == null || isNaturalTerrainContext()) {
return Integer.MIN_VALUE;
}
return readsReadyTerrainOnly()
? terrainTransform.readyFluidHeight(x, z) : terrainTransform.fluidHeight(x, z);
}
private boolean readsReadyTerrainOnly() {
if (!terrainEngine.getPlatformHooks().isMainThread()) {
return false;
}
IrisContext context = IrisContext.get();
if (context == null || context.getEngine() != terrainEngine) {
return true;
}
ChunkContext chunk = context.getChunkContext();
if (chunk != null) {
return chunk.getComplex() != this || chunk.isNaturalTerrain();
}
if (context.getGenerationSessionId() == 0L || terrainEngine.getComplex() != this) {
return true;
}
if (terrainEngine instanceof IrisEngine irisEngine && irisEngine.threadState.assembly() != null
&& irisEngine.threadState.assembly().mode == null) {
return true;
}
return terrainEngine.getMode() == null;
}
boolean isNaturalTerrainContext() {
@@ -2144,6 +2252,9 @@ public class IrisComplex implements DataProvider {
}
public void close() {
if (terrainTransform != null) {
terrainTransform.close();
}
if (hydrologyBanks3D != null) {
hydrologyBanks3D.clear();
}
@@ -36,7 +36,7 @@ public class UpperDimensionContext implements DataProvider {
private UpperDimensionContext(DimensionTerrainContext terrainContext, Engine engine) {
this.terrainContext = terrainContext;
IrisComplex complex = engine.getComplex();
lowerHeightStream = complex.getHeightStream();
lowerHeightStream = complex.getRawHeightStream();
ceilingLayout = new CeilingLayout(engine.getHeight(), engine.getDimension().getUpperDimensionGap());
}
@@ -904,7 +904,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
zone.setFloor(floor);
zone.setCeiling(y - 1);
if (zone.isValid(resolver.height)
&& !getComplex().isTerrain3DOpening(worldX, floor, worldZ)) {
&& !isNaturalTerrainOpening(getComplex(), worldX, floor, worldZ)) {
if (markerRoll(worldX, zone.ceiling, worldZ, 0x9E3779B97F4A7C15L)) {
mantle.set(worldX, zone.ceiling, worldZ, MarkerMatter.CAVE_CEILING);
}
@@ -992,7 +992,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
}
IrisBiome resolveCaveBoundaryBiome(MatterCavern cavern, int worldX, int y, int worldZ, CaveInputs resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Map<String, IrisBiome> customBiomeCache) {
if (resolverState.complex.isTerrain3DOpening(worldX, y, worldZ)) {
if (isNaturalTerrainOpening(resolverState.complex, worldX, y, worldZ)) {
return resolverState.complex.getTrueBiomeStream().get(worldX, worldZ);
}
if (cavern != null && !cavern.getCustomBiome().isEmpty()) {
@@ -1001,6 +1001,18 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
return resolveCaveBiome(caveBiomeCache, worldX, y, worldZ, resolverState);
}
private boolean isNaturalTerrainOpening(IrisComplex complex, int x, int y, int z) {
if (complex.getTerrainTransform() == null) {
return complex.isTerrain3DOpening(x, y, z);
}
if (!complex.hasTerrain3D()) {
return false;
}
Terrain3DColumn column = complex.terrainColumn(x, z, complex.sampleHydrologyColumn(x, z));
return column != null && y >= column.minY() && y < column.topY() && !column.isSolid(y)
&& !getEngine().isAdditionalTerrainOwned(x, y, z);
}
/** The dimension's underground river bed palette, or null when nothing paints. */
private IrisRiverMaterialConfig undergroundBedMaterial() {
IrisHydrology hydrology = getDimension().getHydrology();
@@ -39,6 +39,7 @@ import art.arcane.iris.generation.image.IrisImageMapBinding;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.generation.noise.IrisShapedGeneratorStyle;
import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.iris.generation.terrain.transform.IrisTerrainTransform;
import art.arcane.iris.pack.validation.CompatPools;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.iris.structure.nativegen.IrisImportedFeatureControl;
@@ -218,6 +219,8 @@ public class IrisDimension extends IrisRegistrant {
private IrisHydrology hydrology = new IrisHydrology();
@Description("Dimension-level river placement, routing, profile, biome, and geometry policy.")
private IrisRiverPolicy riverPolicy = new IrisRiverPolicy();
@Description("Optional external terrain transformation with an exact provider version and pack-owned settings.")
private IrisTerrainTransform terrainTransform;
@Description("Refuse to place surface objects and trees over carved surface openings.")
private boolean requireObjectSurfaceSupport = true;
@MinNumber(0)
@@ -1,6 +1,7 @@
package art.arcane.iris.generation.terrain;
import java.util.Arrays;
import java.util.function.IntPredicate;
public final class Terrain3DColumn {
private final double baseHeight;
@@ -20,6 +21,29 @@ public final class Terrain3DColumn {
return new Terrain3DColumn(baseHeight, top + 1, false, new int[]{0, top});
}
public static Terrain3DColumn fromOccupancy(double baseHeight, int height, IntPredicate solid) {
if (height < 1) {
throw new IllegalArgumentException("Terrain column height must be positive");
}
int[] spans = new int[height + 1];
int count = 0;
boolean occupied = false;
for (int y = 0; y < height; y++) {
boolean next = solid.test(y);
if (next && !occupied) {
spans[count++] = y;
} else if (!next && occupied) {
spans[count++] = y - 1;
}
occupied = next;
}
if (occupied) {
spans[count++] = height - 1;
}
return new Terrain3DColumn(baseHeight, 0, true,
count == 0 ? new int[]{0, -1} : Arrays.copyOf(spans, count));
}
public double baseHeight() {
return baseHeight;
}
@@ -0,0 +1,25 @@
package art.arcane.iris.generation.terrain.transform;
import art.arcane.iris.pack.schema.annotation.Required;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.documentation.Description;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Accessors(chain = true)
@NoArgsConstructor
@Data
@Description("An external terrain transformer with its exact implementation version and generation settings.")
public class IrisTerrainTransform {
@Required
@Description("The installed terrain transform provider identifier.")
private String id;
@Required
@Description("The exact provider implementation version required to generate this dimension.")
private String version;
@Description("Provider settings saved with the generation pack. Values are strings interpreted by the provider.")
private KMap<String, String> settings = new KMap<>();
}
@@ -0,0 +1,6 @@
package art.arcane.iris.generation.terrain.transform;
@FunctionalInterface
public interface TerrainColumnTransformer {
void transform(TerrainTransformContext context, int localX, int localZ);
}
@@ -0,0 +1,17 @@
package art.arcane.iris.generation.terrain.transform;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
public interface TerrainTransformContext {
int blockX();
int blockZ();
int minY();
int height();
NativeBlockState original(int worldX, int worldY, int worldZ);
void set(int localX, int localY, int localZ, NativeBlockState state);
}
@@ -0,0 +1,11 @@
package art.arcane.iris.generation.terrain.transform;
import java.util.Map;
public interface TerrainTransformProvider {
String id();
String version();
TerrainTransformer create(Map<String, String> settings);
}
@@ -0,0 +1,62 @@
package art.arcane.iris.generation.terrain.transform;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.ServiceLoader;
public final class TerrainTransformRegistry {
private final Map<ProviderIdentity, TerrainTransformProvider> providers;
public TerrainTransformRegistry(Iterable<TerrainTransformProvider> providers) {
Map<ProviderIdentity, TerrainTransformProvider> indexed = new HashMap<>();
for (TerrainTransformProvider provider : Objects.requireNonNull(providers, "terrain transform providers")) {
Objects.requireNonNull(provider, "terrain transform provider");
ProviderIdentity identity = new ProviderIdentity(provider.id(), provider.version());
if (indexed.putIfAbsent(identity, provider) != null) {
throw new IllegalArgumentException("Duplicate terrain transform provider " + identity + ".");
}
}
this.providers = Map.copyOf(indexed);
}
public static TerrainTransformRegistry discover() {
return new TerrainTransformRegistry(ServiceLoader.load(
TerrainTransformProvider.class, TerrainTransformProvider.class.getClassLoader()));
}
public TerrainTransformer resolve(IrisTerrainTransform descriptor) {
if (descriptor == null) {
return null;
}
ProviderIdentity identity = new ProviderIdentity(descriptor.getId(), descriptor.getVersion());
TerrainTransformProvider provider = providers.get(identity);
if (provider == null) {
throw new IllegalStateException("Required terrain transform provider " + identity + " is unavailable.");
}
Map<String, String> settings = Map.copyOf(Objects.requireNonNull(
descriptor.getSettings(), "terrain transform settings"));
TerrainTransformer transformer = Objects.requireNonNull(
provider.create(settings), "terrain transformer returned by " + identity);
int radius = transformer.radius();
if (radius < 0 || radius > 16) {
throw new IllegalArgumentException("Terrain transform provider " + identity
+ " requires radius " + radius + "; the supported range is 0 through 16 blocks.");
}
return transformer;
}
private static String requireIdentity(String value, String label) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(label + " must not be blank.");
}
return value;
}
private record ProviderIdentity(String id, String version) {
private ProviderIdentity {
id = requireIdentity(id, "Terrain transform provider ID");
version = requireIdentity(version, "Terrain transform provider version");
}
}
}
@@ -0,0 +1,506 @@
package art.arcane.iris.generation.terrain.transform;
import art.arcane.iris.generation.block.B;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.iris.generation.mantle.CaveTerrainSnapshot;
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.FloatingBiomeOverlay;
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.volmlib.util.hunk.storage.StorageHunk;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.Arrays;
import java.util.Objects;
public final class TerrainTransformRuntime implements AutoCloseable {
private static final long CACHE_BYTES = 64L * 1024 * 1024;
private final Engine engine;
private final IrisComplex complex;
private final TerrainTransformer transformer;
private final TerrainColumnTransformer columnTransformer;
private final int radius;
private final int minY;
private final int height;
private final NativeBlockState air;
private final Cache<Long, Snapshot> originals;
private final Cache<Long, Snapshot> transformed;
private final Cache<Long, ColumnSnapshot> transformedColumns;
private volatile boolean closed;
public TerrainTransformRuntime(Options options, TerrainTransformer transformer) {
engine = Objects.requireNonNull(options, "terrain runtime options").engine();
complex = options.complex();
this.transformer = Objects.requireNonNull(transformer, "terrain transformer");
columnTransformer = transformer.columnTransformer();
radius = transformer.radius();
if (radius < 0 || radius > 16) {
throw new IllegalArgumentException("Terrain transform radius must be between 0 and 16 blocks");
}
minY = engine.getMinHeight();
height = engine.getHeight();
if (height <= 0) {
throw new IllegalArgumentException("Terrain height must be positive");
}
air = Objects.requireNonNull(B.getState("AIR"), "air block state");
originals = newCache();
transformed = newCache();
transformedColumns = columnTransformer == null ? null : Caffeine.newBuilder().maximumWeight(CACHE_BYTES)
.weigher((Long key, ColumnSnapshot snapshot) -> snapshot.weight()).build();
}
public void generate(int x, int z, Hunk<NativeBlockState> blocks, Hunk<NativeBiome> biomes,
boolean multicore, ChunkContext target) {
requireOpen();
if ((x & 15) != 0 || (z & 15) != 0 || blocks.getWidth() != 16 || blocks.getDepth() != 16
|| blocks.getHeight() != height || biomes.getWidth() != 16 || biomes.getDepth() != 16
|| biomes.getHeight() != height || target.getComplex() != complex) {
throw new IllegalArgumentException("Terrain transform output must match its runtime and chunk");
}
Snapshot snapshot = transformed(x >> 4, z >> 4);
target.setTerrainBiomeOutput(biomes);
for (int y = 0; y < height; y++) {
for (int localZ = 0; localZ < 16; localZ++) {
for (int localX = 0; localX < 16; localX++) {
int index = index(localX, y, localZ);
blocks.setRaw(localX, y, localZ, snapshot.blocks[index]);
NativeBiome biome = snapshot.biomes[index];
if (biome != null) {
biomes.setRaw(localX, y, localZ, biome);
}
}
}
}
for (int localZ = 0; localZ < 16; localZ++) {
for (int localX = 0; localX < 16; localX++) {
target.setTerrainHeight(localX, localZ, snapshot.solidHeights[(localZ << 4) | localX]);
}
}
target.setCaveTerrain(snapshot.caves);
target.setFloatingBiomes(snapshot.floating == null ? null : snapshot.floating.copy());
}
public int height(int x, int z, boolean ignoreFluid) {
requireOpen();
Snapshot snapshot = fullForQuery(x, z);
if (snapshot != null) {
return snapshot.height(x & 15, z & 15, ignoreFluid);
}
ColumnSnapshot column = transformedColumn(x, z);
return ignoreFluid ? column.ground : column.top;
}
public int readyHeight(int x, int z, boolean ignoreFluid) {
requireOpen();
Snapshot snapshot = transformed.getIfPresent(key(x >> 4, z >> 4));
if (snapshot != null) {
return snapshot.height(x & 15, z & 15, ignoreFluid);
}
ColumnSnapshot column = readyTransformedColumn(x, z);
return column == null ? Integer.MIN_VALUE : ignoreFluid ? column.ground : column.top;
}
public NativeBlockState block(int worldX, int localY, int worldZ) {
requireOpen();
if (localY < 0 || localY >= height) {
return air;
}
return transformed(worldX >> 4, worldZ >> 4).blocks[index(worldX & 15, localY, worldZ & 15)];
}
public NativeBlockState readyBlock(int worldX, int localY, int worldZ) {
requireOpen();
if (localY < 0 || localY >= height) {
return air;
}
Snapshot snapshot = transformed.getIfPresent(key(worldX >> 4, worldZ >> 4));
return snapshot == null ? null : snapshot.blocks[index(worldX & 15, localY, worldZ & 15)];
}
public Terrain3DColumn column(int x, int z) {
requireOpen();
Snapshot snapshot = fullForQuery(x, z);
return snapshot == null ? transformedColumn(x, z).terrain : snapshot.column(x & 15, z & 15);
}
public Terrain3DColumn readyColumn(int x, int z) {
requireOpen();
Snapshot snapshot = transformed.getIfPresent(key(x >> 4, z >> 4));
if (snapshot != null) {
return snapshot.column(x & 15, z & 15);
}
ColumnSnapshot column = readyTransformedColumn(x, z);
return column == null ? null : column.terrain;
}
public int fluidHeight(int x, int z) {
requireOpen();
Snapshot snapshot = fullForQuery(x, z);
return snapshot == null ? transformedColumn(x, z).fluid : snapshot.fluidHeights[((z & 15) << 4) | (x & 15)];
}
public int readyFluidHeight(int x, int z) {
requireOpen();
Snapshot snapshot = transformed.getIfPresent(key(x >> 4, z >> 4));
if (snapshot != null) {
return snapshot.fluidHeights[((z & 15) << 4) | (x & 15)];
}
ColumnSnapshot column = readyTransformedColumn(x, z);
return column == null ? Integer.MIN_VALUE : column.fluid;
}
@Override
public void close() {
closed = true;
originals.invalidateAll();
transformed.invalidateAll();
originals.cleanUp();
transformed.cleanUp();
if (transformedColumns != null) {
transformedColumns.invalidateAll();
transformedColumns.cleanUp();
}
}
private Cache<Long, Snapshot> newCache() {
return Caffeine.newBuilder().maximumWeight(CACHE_BYTES)
.weigher((Long key, Snapshot snapshot) -> snapshot.weight()).build();
}
private Snapshot transformed(int chunkX, int chunkZ) {
Snapshot snapshot = transformed.get(key(chunkX, chunkZ), ignored -> transform(chunkX, chunkZ));
if (closed) {
transformed.invalidateAll();
requireOpen();
}
return snapshot;
}
private Snapshot original(int chunkX, int chunkZ) {
Snapshot snapshot = originals.get(key(chunkX, chunkZ), ignored -> materialize(chunkX, chunkZ));
if (closed) {
originals.invalidateAll();
requireOpen();
}
return snapshot;
}
private Snapshot fullForQuery(int x, int z) {
return columnTransformer == null ? transformed(x >> 4, z >> 4)
: transformed.getIfPresent(key(x >> 4, z >> 4));
}
private ColumnSnapshot readyTransformedColumn(int x, int z) {
return transformedColumns == null ? null : transformedColumns.getIfPresent(key(x, z));
}
private ColumnSnapshot transformedColumn(int x, int z) {
ColumnSnapshot snapshot = transformedColumns.get(key(x, z), ignored -> transformColumn(x, z));
if (closed) {
transformedColumns.invalidateAll();
requireOpen();
}
return snapshot;
}
private Snapshot materialize(int chunkX, int chunkZ) {
requireOpen();
int x = chunkX << 4;
int z = chunkZ << 4;
NativeBlockState[] states = new NativeBlockState[Math.multiplyExact(256, height)];
Arrays.fill(states, air);
NativeBiome[] biomeStates = new NativeBiome[states.length];
Hunk<NativeBlockState> blocks = new SnapshotHunk<>(states, air);
Hunk<NativeBiome> biomes = new SnapshotHunk<>(biomeStates, null);
long session = engine.getGenerationSessionId();
ChunkContext context = new ChunkContext(x, z, complex, session, true,
ChunkContext.PrefillPlan.NATURAL_TERRAIN, engine.getMetrics(), engine.getDimensionStackContext());
try (IrisContext.Scope scope = IrisContext.open(engine, session, context)) {
engine.getMode().generateTerrain(x, z, blocks, biomes, false, context);
}
FloatingBiomeOverlay floating = context.getFloatingBiomes();
return new Snapshot(new SnapshotData(states, biomeStates, context.getCaveTerrain(),
floating == null ? null : floating.copy()), false);
}
private Snapshot transform(int chunkX, int chunkZ) {
requireOpen();
Snapshot source = original(chunkX, chunkZ);
TransformContext context = new TransformContext(new TransformTarget(chunkX, chunkZ, false, 0, 0), source);
try {
transformer.transform(context);
} finally {
context.active = false;
}
return new Snapshot(new SnapshotData(context.blocks, source.biomes, source.caves, source.floating), true);
}
private ColumnSnapshot transformColumn(int x, int z) {
requireOpen();
Snapshot source = original(x >> 4, z >> 4);
TransformContext context = new TransformContext(
new TransformTarget(x >> 4, z >> 4, true, x & 15, z & 15), source);
try {
columnTransformer.transform(context, x & 15, z & 15);
} finally {
context.active = false;
}
return new ColumnSnapshot(context.blocks);
}
private void requireOpen() {
if (closed) {
throw new IllegalStateException("Terrain transform runtime is closed");
}
}
private static int index(int x, int y, int z) {
return (y << 8) | (z << 4) | x;
}
private static long key(int chunkX, int chunkZ) {
return ((long) chunkX << 32) | (chunkZ & 0xffffffffL);
}
public record Options(Engine engine, IrisComplex complex) {
public Options {
Objects.requireNonNull(engine, "terrain engine");
Objects.requireNonNull(complex, "terrain complex");
}
}
private record TransformTarget(int chunkX, int chunkZ, boolean columnOnly, int localX, int localZ) {
}
private record SnapshotData(NativeBlockState[] blocks, NativeBiome[] biomes,
CaveTerrainSnapshot caves, FloatingBiomeOverlay floating) {
}
private static final class SnapshotHunk<T> extends StorageHunk<T> {
private final T[] values;
private final T fallback;
private SnapshotHunk(T[] values, T fallback) {
super(16, values.length / 256, 16);
this.values = values;
this.fallback = fallback;
}
@Override
public T getRaw(int x, int y, int z) {
return y < 0 || y >= getHeight() ? fallback : values[index(x, y, z)];
}
@Override
public void setRaw(int x, int y, int z, T value) {
if (value != null) {
values[index(x, y, z)] = value;
}
}
}
private final class TransformContext implements TerrainTransformContext {
private final int x;
private final int z;
private final NativeBlockState[] blocks;
private final TransformTarget target;
private final Snapshot[] sources = new Snapshot[9];
private volatile boolean active = true;
private TransformContext(TransformTarget target, Snapshot source) {
this.target = target;
x = target.chunkX() << 4;
z = target.chunkZ() << 4;
if (target.columnOnly()) {
blocks = new NativeBlockState[height];
for (int y = 0; y < height; y++) {
blocks[y] = source.blocks[index(target.localX(), y, target.localZ())];
}
} else {
blocks = source.blocks.clone();
}
sources[4] = source;
}
@Override
public int blockX() {
return x;
}
@Override
public int blockZ() {
return z;
}
@Override
public int minY() {
return minY;
}
@Override
public int height() {
return height;
}
@Override
public NativeBlockState original(int worldX, int worldY, int worldZ) {
requireActive();
long localX = (long) worldX - x;
long localZ = (long) worldZ - z;
if (localX < -radius || localX >= 16L + radius || localZ < -radius || localZ >= 16L + radius) {
throw new IllegalArgumentException("Terrain transform read exceeds its declared radius");
}
long localY = (long) worldY - minY;
if (localY < 0 || localY >= height) {
return air;
}
int sourceX = worldX >> 4;
int sourceZ = worldZ >> 4;
int slot = (sourceZ - (z >> 4) + 1) * 3 + sourceX - (x >> 4) + 1;
Snapshot snapshot = sources[slot];
if (snapshot == null) {
snapshot = TerrainTransformRuntime.this.original(sourceX, sourceZ);
sources[slot] = snapshot;
}
return snapshot.blocks[index(worldX & 15, (int) localY, worldZ & 15)];
}
@Override
public void set(int localX, int localY, int localZ, NativeBlockState state) {
requireActive();
Objects.requireNonNull(state, "terrain transform block state");
if (localX < 0 || localX >= 16 || localZ < 0 || localZ >= 16 || localY < 0 || localY >= height) {
throw new IllegalArgumentException("Terrain transform write exceeds its output chunk");
}
if (target.columnOnly() && (localX != target.localX() || localZ != target.localZ())) {
throw new IllegalArgumentException("Terrain column transform write exceeds its output column");
}
if (complex.allowsMantleWrite(x + localX, z + localZ)) {
blocks[target.columnOnly() ? localY : index(localX, localY, localZ)] = state;
}
}
private void requireActive() {
requireOpen();
if (!active) {
throw new IllegalStateException("Terrain transform context is no longer active");
}
}
}
private static final class ColumnSnapshot {
private final Terrain3DColumn terrain;
private final int ground;
private final int top;
private final int fluid;
private ColumnSnapshot(NativeBlockState[] blocks) {
int solidHeight = -1;
int topHeight = -1;
int fluidHeight = -1;
for (int y = blocks.length - 1; y >= 0; y--) {
NativeBlockState state = blocks[y];
if (state.isAir()) {
continue;
}
if (topHeight == -1) {
topHeight = y;
}
if ((state.isFluid() || state.isWaterLogged()) && fluidHeight == -1) {
fluidHeight = y;
}
if (!state.isFluid() && solidHeight == -1) {
solidHeight = y;
}
}
ground = solidHeight;
top = topHeight;
fluid = fluidHeight;
terrain = Terrain3DColumn.fromOccupancy(ground, blocks.length,
y -> !blocks[y].isAir() && !blocks[y].isFluid());
}
private int weight() {
return 192 + terrain.spanCount() * 8;
}
}
private static final class Snapshot {
private final NativeBlockState[] blocks;
private final NativeBiome[] biomes;
private final CaveTerrainSnapshot caves;
private final FloatingBiomeOverlay floating;
private final int[] solidHeights;
private final int[] topHeights;
private final int[] fluidHeights;
private final Terrain3DColumn[] columns;
private Snapshot(SnapshotData data, boolean effective) {
blocks = data.blocks();
biomes = data.biomes();
caves = data.caves();
floating = data.floating();
solidHeights = effective ? new int[256] : null;
topHeights = effective ? new int[256] : null;
fluidHeights = effective ? new int[256] : null;
columns = effective ? new Terrain3DColumn[256] : null;
if (!effective) {
return;
}
Arrays.fill(solidHeights, -1);
Arrays.fill(topHeights, -1);
Arrays.fill(fluidHeights, -1);
int height = blocks.length / 256;
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
int column = (z << 4) | x;
for (int y = height - 1; y >= 0; y--) {
NativeBlockState state = blocks[index(x, y, z)];
if (state.isAir()) {
continue;
}
if (topHeights[column] == -1) {
topHeights[column] = y;
}
if ((state.isFluid() || state.isWaterLogged()) && fluidHeights[column] == -1) {
fluidHeights[column] = y;
}
if (!state.isFluid() && solidHeights[column] == -1) {
solidHeights[column] = y;
}
}
}
}
}
private int height(int x, int z, boolean ignoreFluid) {
return (ignoreFluid ? solidHeights : topHeights)[(z << 4) | x];
}
private Terrain3DColumn column(int x, int z) {
int index = (z << 4) | x;
Terrain3DColumn column = columns[index];
if (column == null) {
column = Terrain3DColumn.fromOccupancy(solidHeights[index], blocks.length / 256, y -> {
NativeBlockState state = blocks[index(x, y, z)];
return !state.isAir() && !state.isFluid();
});
columns[index] = column;
}
return column;
}
private int weight() {
long blockReferences = (long) blocks.length * 20L;
long caveReferences = caves == null ? 0L : caves.estimatedRetainedBytes();
long floatingReferences = floating == null ? 0L : (long) blocks.length * 4L;
return (int) Math.min(Integer.MAX_VALUE, 16384L + blockReferences + caveReferences + floatingReferences);
}
}
}
@@ -0,0 +1,11 @@
package art.arcane.iris.generation.terrain.transform;
public interface TerrainTransformer {
int radius();
void transform(TerrainTransformContext context);
default TerrainColumnTransformer columnTransformer() {
return null;
}
}
@@ -64,6 +64,13 @@ public final class FloatingBiomeOverlay {
return height;
}
public FloatingBiomeOverlay copy() {
FloatingBiomeOverlay copy = new FloatingBiomeOverlay(height);
System.arraycopy(volume, 0, copy.volume, 0, volume.length);
System.arraycopy(surfaces, 0, copy.surfaces, 0, surfaces.length);
return copy;
}
private int index(int localX, int y, int localZ) {
return ((localX >> 2) * 4 + (localZ >> 2)) * quartHeight + (y >> 2);
}