mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 14:30:14 +00:00
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:
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;
|
||||
}
|
||||
|
||||
+25
@@ -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<>();
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package art.arcane.iris.generation.terrain.transform;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface TerrainColumnTransformer {
|
||||
void transform(TerrainTransformContext context, int localX, int localZ);
|
||||
}
|
||||
+17
@@ -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);
|
||||
}
|
||||
+11
@@ -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);
|
||||
}
|
||||
+62
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+506
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user