mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 12:41:43 +00:00
Changes
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
[23:25:34] [Test worker/INFO]: [STDERR]: [Iris/WARN] Native structure terrain envelope at 0,0 was clipped to Minecraft's 8-chunk structure reference range
|
||||
[23:25:34] [Test worker/INFO]: [STDERR]: [Iris/WARN] Native structure burial at 0,0 clamped to world floor: wanted -19, used -4
|
||||
[23:25:34] [Test worker/INFO]: [STDERR]: [Iris/WARN] Native structure burial at 0,0 clamped to world floor: wanted -5, used -2
|
||||
|
||||
+117
-40
@@ -4,12 +4,15 @@ import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.framework.NativeStructurePlacementPlanner;
|
||||
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
|
||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisMaterialPalette;
|
||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.engine.object.IrisStructureStiltSettings;
|
||||
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
||||
import art.arcane.iris.nativegen.NativeStructureGenerationException;
|
||||
import art.arcane.iris.nativegen.NativeStructureStartInjector;
|
||||
import art.arcane.iris.nativegen.NativeStructureReferenceEnvelope;
|
||||
import art.arcane.iris.nativegen.NativeStructureLocateResults;
|
||||
import art.arcane.iris.nativegen.NativeStructurePostProcessor;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
@@ -86,6 +89,7 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
|
||||
public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
private static final WrappedField<ChunkGenerator, BiomeSource> BIOME_SOURCE;
|
||||
@@ -146,9 +150,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
throw new IllegalStateException("Native structure locate received an unregistered structure holder");
|
||||
}
|
||||
String structureId = id.toString();
|
||||
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine,
|
||||
structureId, NativeStructurePostProcessor.isUndergroundStep(holder.value().step()));
|
||||
if (decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
|
||||
if (!IrisStructureLocator.isPlaced(engine, structureId)) {
|
||||
continue;
|
||||
}
|
||||
IrisStructureLocator.LocateResult result = IrisStructureLocator.locate(
|
||||
@@ -250,11 +252,27 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
Map<Structure, StructureStart> previousStarts = new HashMap<>(access.getAllStarts());
|
||||
super.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey);
|
||||
adjustGeneratedStructures(registryAccess, access, previousStarts);
|
||||
Map<Structure, NativeStructureStartPlan> configuredStarts = NativeStructureStartInjector.inject(
|
||||
new NativeStructureStartInjector.InjectionContext(
|
||||
engine,
|
||||
registryAccess,
|
||||
structureState,
|
||||
structureManager,
|
||||
access,
|
||||
templateManager,
|
||||
levelKey,
|
||||
this,
|
||||
customBiomeSource
|
||||
));
|
||||
adjustGeneratedStructures(
|
||||
registryAccess, access, previousStarts, configuredStarts, templateManager);
|
||||
}
|
||||
}
|
||||
|
||||
private void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess access, Map<Structure, StructureStart> previousStarts) {
|
||||
private void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess access,
|
||||
Map<Structure, StructureStart> previousStarts,
|
||||
Map<Structure, NativeStructureStartPlan> configuredStarts,
|
||||
StructureTemplateManager templateManager) {
|
||||
Registry<Structure> registry = registryAccess.lookupOrThrow(Registries.STRUCTURE);
|
||||
ChunkPos chunkPos = access.getPos();
|
||||
for (Map.Entry<Structure, StructureStart> entry : access.getAllStarts().entrySet()) {
|
||||
@@ -263,6 +281,9 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
if (!start.isValid() || previousStarts.get(structure) == start) {
|
||||
continue;
|
||||
}
|
||||
if (configuredStarts.containsKey(structure)) {
|
||||
continue;
|
||||
}
|
||||
Identifier id = registry.getKey(structure);
|
||||
String structureId = id == null ? null : id.toString();
|
||||
if (structureId == null) {
|
||||
@@ -291,7 +312,13 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
access.getMinY(),
|
||||
access.getMinY() + access.getHeight(),
|
||||
undergroundStep,
|
||||
decision.preserveSourceY(),
|
||||
decision.yBand(),
|
||||
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight());
|
||||
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
|
||||
start, structure, start.getReferences(), templateManager,
|
||||
NativeStructurePostProcessor.resolveNativeTerrain(start, decision.terrain()));
|
||||
access.setStartForStructure(structure, wrapped);
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"vertical adjustment", structureId, chunkPos.x(), chunkPos.z(), error);
|
||||
@@ -330,7 +357,29 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ChunkAccess> fillFromNoise(Blender blender, RandomState randomstate, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
||||
return delegate.fillFromNoise(blender, randomstate, structuremanager, ichunkaccess);
|
||||
return delegate.fillFromNoise(blender, randomstate, structuremanager, ichunkaccess)
|
||||
.thenApply(filled -> {
|
||||
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_worldgen_heightmaps");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
primeWorldgenHeightmaps(filled);
|
||||
return filled;
|
||||
} catch (GenerationSessionException e) {
|
||||
throw new IllegalStateException(
|
||||
"Iris worldgen heightmap priming could not acquire its engine runtime.", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void primeWorldgenHeightmaps(ChunkAccess chunkAccess) {
|
||||
WorldgenTerrainHeightmaps.primeTerrain(chunkAccess, worldgenSurfaceHeight(), worldgenFloorHeight());
|
||||
}
|
||||
|
||||
private IntBinaryOperator worldgenSurfaceHeight() {
|
||||
return (x, z) -> engine.getHeight(x, z, false) + runtimeMinY + 1;
|
||||
}
|
||||
|
||||
private IntBinaryOperator worldgenFloorHeight() {
|
||||
return (x, z) -> engine.getHeight(x, z, true) + runtimeMinY + 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -397,8 +446,10 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
BoundingBox area = writableArea(chunk);
|
||||
int steps = GenerationStep.Decoration.values().length;
|
||||
List<NativePlacementGroup> placementGroups = new ArrayList<>();
|
||||
List<StructureStart> heightmapStarts = new ArrayList<>();
|
||||
List<StructureStart> nativeStarts = new ArrayList<>();
|
||||
List<NativeStructurePostProcessor.VegetationTarget> vegetationTargets = new ArrayList<>();
|
||||
List<NativeStructurePostProcessor.TerrainTarget> terrainTargets = new ArrayList<>();
|
||||
for (int step = 0; step < steps; step++) {
|
||||
int index = 0;
|
||||
for (Structure structure : byStep.get(step)) {
|
||||
@@ -409,23 +460,36 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
"resolution", null, chunkPos.x(), chunkPos.z());
|
||||
}
|
||||
try {
|
||||
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine,
|
||||
IrisNativeStructureDecision sourceDecision = NativeStructureGenerationPolicy.resolve(engine,
|
||||
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
|
||||
if (decision.generate()) {
|
||||
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
|
||||
if (!starts.isEmpty()) {
|
||||
List<StructureStart> resolvedStarts = List.copyOf(starts);
|
||||
placementGroups.add(new NativePlacementGroup(
|
||||
structureId, decision, index, step, resolvedStarts));
|
||||
nativeStarts.addAll(resolvedStarts);
|
||||
boolean clearEntireFootprint = NativeStructurePostProcessor
|
||||
.shouldClearEntireVegetationFootprint(
|
||||
structure.step(), decision.clearVegetation());
|
||||
for (StructureStart start : resolvedStarts) {
|
||||
vegetationTargets.add(new NativeStructurePostProcessor.VegetationTarget(
|
||||
start, clearEntireFootprint));
|
||||
}
|
||||
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
|
||||
List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size());
|
||||
for (StructureStart start : starts) {
|
||||
NativeStructureStartPlan plan = NativeStructurePlacementPlanner.matchingPlan(
|
||||
engine, structureId, start.getChunkPos().x(), start.getChunkPos().z());
|
||||
IrisNativeStructureDecision decision = plan == null
|
||||
? sourceDecision : NativeStructurePlacementPlanner.decisionFor(plan);
|
||||
if (!decision.generate()) {
|
||||
continue;
|
||||
}
|
||||
resolvedPlacements.add(new NativePlacement(start, decision));
|
||||
heightmapStarts.add(start);
|
||||
terrainTargets.add(new NativeStructurePostProcessor.TerrainTarget(
|
||||
structureId, start,
|
||||
NativeStructurePostProcessor.resolveNativeTerrain(
|
||||
start, decision.terrain())));
|
||||
if (plan == null || !plan.placement().isUnderground()) {
|
||||
nativeStarts.add(start);
|
||||
}
|
||||
boolean clearEntireFootprint = NativeStructurePostProcessor
|
||||
.shouldClearEntireVegetationFootprint(
|
||||
structure.step(), decision.clearVegetation());
|
||||
vegetationTargets.add(new NativeStructurePostProcessor.VegetationTarget(
|
||||
start, clearEntireFootprint));
|
||||
}
|
||||
if (!resolvedPlacements.isEmpty()) {
|
||||
placementGroups.add(new NativePlacementGroup(
|
||||
structureId, index, step, List.copyOf(resolvedPlacements)));
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
@@ -434,6 +498,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
index++;
|
||||
}
|
||||
}
|
||||
try {
|
||||
WorldgenTerrainHeightmaps.primeStructurePlacement(
|
||||
world, heightmapStarts, worldgenSurfaceHeight(), worldgenFloorHeight());
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"heightmap priming", nativeStructureBatchContext(placementGroups),
|
||||
chunkPos.x(), chunkPos.z(), error);
|
||||
}
|
||||
try {
|
||||
NativeStructurePostProcessor.prepareSurfaceStructures(
|
||||
world, area, nativeStarts,
|
||||
@@ -451,12 +523,20 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
"vegetation cleanup", nativeStructureBatchContext(placementGroups),
|
||||
chunkPos.x(), chunkPos.z(), error);
|
||||
}
|
||||
try {
|
||||
NativeStructurePostProcessor.prepareTerrain(
|
||||
world, area, terrainTargets, this::resolvePaletteBlock);
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"terrain carving", nativeStructureBatchContext(placementGroups),
|
||||
chunkPos.x(), chunkPos.z(), error);
|
||||
}
|
||||
for (NativePlacementGroup group : placementGroups) {
|
||||
random.setFeatureSeed(decoSeed, group.featureIndex(), group.step());
|
||||
try {
|
||||
for (StructureStart start : group.starts()) {
|
||||
for (NativePlacement placement : group.placements()) {
|
||||
placeVanillaStructure(world, structureManager, random, area, chunkPos,
|
||||
group.structureId(), start, group.decision());
|
||||
group.structureId(), placement.start(), placement.decision());
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
@@ -483,7 +563,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
BoundingBox area, ChunkPos chunkPos, String structureId, StructureStart start,
|
||||
IrisNativeStructureDecision decision) {
|
||||
NativeStructurePostProcessor.place(world, structureManager, this, random, area, chunkPos,
|
||||
structureId, start, decision, this::resolveStiltBlock,
|
||||
structureId, start, decision, this::resolvePaletteBlock,
|
||||
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight());
|
||||
}
|
||||
|
||||
@@ -514,13 +594,10 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private BlockState resolveStiltBlock(IrisStructureStiltSettings settings, RNG rng, int x, int y, int z) {
|
||||
if (settings.getPalette() == null) {
|
||||
return Blocks.COBBLESTONE.defaultBlockState();
|
||||
}
|
||||
PlatformBlockState platformState = settings.getPalette().get(rng, x, y, z, engine.getData());
|
||||
private BlockState resolvePaletteBlock(IrisMaterialPalette palette, RNG rng, int x, int y, int z) {
|
||||
PlatformBlockState platformState = palette.get(rng, x, y, z, engine.getData());
|
||||
if (platformState == null || !(platformState.nativeHandle() instanceof BlockData blockData)) {
|
||||
throw new IllegalStateException("Configured native structure stilt palette did not resolve a Bukkit block at "
|
||||
throw new IllegalStateException("Configured native structure palette did not resolve a Bukkit block at "
|
||||
+ x + "," + y + "," + z);
|
||||
}
|
||||
if (blockData instanceof IrisCustomData customData) {
|
||||
@@ -529,7 +606,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
if (blockData instanceof CraftBlockData craftBlockData) {
|
||||
return craftBlockData.getState();
|
||||
}
|
||||
throw new IllegalStateException("Configured native structure stilt palette resolved unsupported Bukkit block data "
|
||||
throw new IllegalStateException("Configured native structure palette resolved unsupported Bukkit block data "
|
||||
+ blockData.getClass().getName() + " at " + x + "," + y + "," + z);
|
||||
}
|
||||
|
||||
@@ -549,8 +626,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
SectionPos sectionPos = SectionPos.of(chunkAccess.getPos(), level.getMinSectionY());
|
||||
BlockPos blockPos = sectionPos.origin();
|
||||
|
||||
Heightmap surface = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||
Heightmap ocean = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||
primeWorldgenHeightmaps(chunkAccess);
|
||||
|
||||
Heightmap motion = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING);
|
||||
Heightmap motionNoLeaves = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES);
|
||||
int minHeight = engine.getMinHeight();
|
||||
@@ -561,9 +638,6 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
int wZ = z + blockPos.getZ();
|
||||
|
||||
int terrainTop = engine.getHeight(wX, wZ, false) + minHeight + 1;
|
||||
int terrainNoFluid = engine.getHeight(wX, wZ, true) + minHeight + 1;
|
||||
SET_HEIGHT.invoke(ocean, x, z, terrainNoFluid);
|
||||
SET_HEIGHT.invoke(surface, x, z, terrainTop);
|
||||
SET_HEIGHT.invoke(motion, x, z, terrainTop);
|
||||
SET_HEIGHT.invoke(motionNoLeaves, x, z, terrainTop);
|
||||
}
|
||||
@@ -714,8 +788,11 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
private record StructureStepCache(Registry<Structure> registry, List<List<Structure>> structures) {
|
||||
}
|
||||
|
||||
private record NativePlacementGroup(String structureId, IrisNativeStructureDecision decision,
|
||||
int featureIndex, int step, List<StructureStart> starts) {
|
||||
private record NativePlacement(StructureStart start, IrisNativeStructureDecision decision) {
|
||||
}
|
||||
|
||||
private record NativePlacementGroup(String structureId, int featureIndex, int step,
|
||||
List<NativePlacement> placements) {
|
||||
}
|
||||
|
||||
private record NativeLocateCandidate(Holder<Structure> holder, String key) {
|
||||
|
||||
+32
@@ -102,6 +102,8 @@ import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.WorldgenRandom;
|
||||
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureCheck;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure;
|
||||
import net.minecraft.world.level.levelgen.feature.AbstractHugeMushroomFeature;
|
||||
import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
|
||||
import net.minecraft.world.level.levelgen.feature.FallenTreeFeature;
|
||||
@@ -482,6 +484,36 @@ public class NMSBinding implements INMSBinding {
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KList<String> getJigsawStructureKeys() {
|
||||
KList<String> keys = new KList<>();
|
||||
try {
|
||||
Registry<Structure> structures = registry().lookupOrThrow(Registries.STRUCTURE);
|
||||
for (Map.Entry<ResourceKey<Structure>, Structure> entry : structures.entrySet()) {
|
||||
if (entry.getValue() instanceof JigsawStructure) {
|
||||
keys.add(entry.getKey().identifier().toString());
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
throw new IllegalStateException(
|
||||
"Iris failed to read registered jigsaw structure keys from the Minecraft registry", e);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KList<String> getTemplatePoolKeys() {
|
||||
KList<String> keys = new KList<>();
|
||||
try {
|
||||
registry().lookupOrThrow(Registries.TEMPLATE_POOL).keySet()
|
||||
.forEach(key -> keys.add(key.toString()));
|
||||
} catch (RuntimeException e) {
|
||||
throw new IllegalStateException(
|
||||
"Iris failed to read registered template pool keys from the Minecraft registry", e);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KList<String> getStructureSetKeys() {
|
||||
KList<String> keys = new KList<>();
|
||||
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.util.SimpleBitStorage;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
|
||||
/**
|
||||
* Writes WORLD_SURFACE_WG and OCEAN_FLOOR_WG from Iris engine heights.
|
||||
*
|
||||
* Iris writes its terrain straight into LevelChunkSection, so neither vanilla's noise fill nor
|
||||
* ProtoChunk.setBlockState ever updates the two worldgen heightmaps. Without these writes
|
||||
* ChunkAccess.getHeight lazily primes the map from finished blocks - Iris trees, leaves and snow
|
||||
* included - and vanilla pieces that self-snap against WORLD_SURFACE_WG (igloo) or OCEAN_FLOOR_WG
|
||||
* (ocean ruins, buried treasure, mineshaft interiors) anchor to the canopy instead of the terrain.
|
||||
*
|
||||
* Both resolvers take world block coordinates and return the absolute first free Y, matching
|
||||
* Heightmap's internal "first available" semantics: WORLD_SURFACE_WG counts fluid, OCEAN_FLOOR_WG
|
||||
* does not.
|
||||
*/
|
||||
final class WorldgenTerrainHeightmaps {
|
||||
private static final int COLUMNS = 256;
|
||||
private static final int PLACEMENT_CHUNK_MARGIN = 1;
|
||||
|
||||
private WorldgenTerrainHeightmaps() {
|
||||
}
|
||||
|
||||
static void primeTerrain(ChunkAccess chunk, IntBinaryOperator surfaceFirstFreeY,
|
||||
IntBinaryOperator floorFirstFreeY) {
|
||||
Objects.requireNonNull(chunk, "Iris worldgen heightmap priming requires a chunk");
|
||||
Objects.requireNonNull(surfaceFirstFreeY,
|
||||
"Iris worldgen heightmap priming requires a surface height resolver");
|
||||
Objects.requireNonNull(floorFirstFreeY,
|
||||
"Iris worldgen heightmap priming requires an ocean floor height resolver");
|
||||
write(chunk, Heightmap.Types.WORLD_SURFACE_WG, surfaceFirstFreeY);
|
||||
write(chunk, Heightmap.Types.OCEAN_FLOOR_WG, floorFirstFreeY);
|
||||
}
|
||||
|
||||
static void primeStructurePlacement(WorldGenLevel world, List<StructureStart> starts,
|
||||
IntBinaryOperator surfaceFirstFreeY,
|
||||
IntBinaryOperator floorFirstFreeY) {
|
||||
Objects.requireNonNull(world,
|
||||
"Iris worldgen heightmap priming requires a generation level");
|
||||
if (starts == null || starts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Set<Long> primed = new HashSet<>();
|
||||
for (StructureStart start : starts) {
|
||||
if (start == null || !start.isValid()) {
|
||||
continue;
|
||||
}
|
||||
BoundingBox bounds = start.getBoundingBox();
|
||||
int minChunkX = SectionPos.blockToSectionCoord(bounds.minX()) - PLACEMENT_CHUNK_MARGIN;
|
||||
int maxChunkX = SectionPos.blockToSectionCoord(bounds.maxX()) + PLACEMENT_CHUNK_MARGIN;
|
||||
int minChunkZ = SectionPos.blockToSectionCoord(bounds.minZ()) - PLACEMENT_CHUNK_MARGIN;
|
||||
int maxChunkZ = SectionPos.blockToSectionCoord(bounds.maxZ()) + PLACEMENT_CHUNK_MARGIN;
|
||||
for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) {
|
||||
for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) {
|
||||
if (!primed.add(ChunkPos.pack(chunkX, chunkZ))) {
|
||||
continue;
|
||||
}
|
||||
if (!world.hasChunk(chunkX, chunkZ)) {
|
||||
continue;
|
||||
}
|
||||
primeTerrain(world.getChunk(chunkX, chunkZ), surfaceFirstFreeY, floorFirstFreeY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void write(ChunkAccess chunk, Heightmap.Types type, IntBinaryOperator firstFreeY) {
|
||||
ChunkPos pos = chunk.getPos();
|
||||
int minY = chunk.getMinY();
|
||||
int height = chunk.getHeight();
|
||||
int baseX = pos.getMinBlockX();
|
||||
int baseZ = pos.getMinBlockZ();
|
||||
SimpleBitStorage storage = new SimpleBitStorage(Mth.ceillog2(height + 1), COLUMNS);
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
int firstFree = firstFreeY.applyAsInt(baseX + x, baseZ + z) - minY;
|
||||
storage.set(x + z * 16, Mth.clamp(firstFree, 0, height));
|
||||
}
|
||||
}
|
||||
Heightmap heightmap = chunk.getOrCreateHeightmapUnprimed(type);
|
||||
long[] packed = storage.getRaw();
|
||||
int expected = heightmap.getRawData().length;
|
||||
if (expected != packed.length) {
|
||||
throw new IllegalStateException("Iris cannot prime " + type + " for chunk "
|
||||
+ pos.x() + "," + pos.z() + ": packed " + packed.length
|
||||
+ " words for a heightmap storing " + expected
|
||||
+ "; Heightmap.setRawData would silently fall back to block scanning");
|
||||
}
|
||||
heightmap.setRawData(chunk, type, packed);
|
||||
}
|
||||
}
|
||||
+36
@@ -6,6 +6,7 @@ import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@@ -65,6 +66,41 @@ public class IrisChunkGeneratorFailureContractTest {
|
||||
assertTrue(source.contains("catch (GenerationSessionException e)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void terrainWritesPrimeTheWorldgenHeightmapsForEveryChunk() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")));
|
||||
int fillStart = source.indexOf("public CompletableFuture<ChunkAccess> fillFromNoise");
|
||||
int fillEnd = source.indexOf("public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt", fillStart);
|
||||
String fill = source.substring(fillStart, fillEnd);
|
||||
int decorationStart = source.indexOf("public void addVanillaDecorations");
|
||||
int decorationEnd = source.indexOf("public void spawnOriginalMobs", decorationStart);
|
||||
String decorations = source.substring(decorationStart, decorationEnd);
|
||||
int placementStart = source.indexOf("private void placeVanillaStructures");
|
||||
int placementEnd = source.indexOf("private static String nativeStructureBatchContext", placementStart);
|
||||
String placement = source.substring(placementStart, placementEnd);
|
||||
|
||||
assertTrue(fill.contains("thenApply"));
|
||||
assertTrue(fill.contains("primeWorldgenHeightmaps"));
|
||||
assertTrue(decorations.contains("primeWorldgenHeightmaps"));
|
||||
assertFalse(decorations.contains("Heightmap.Types.WORLD_SURFACE_WG"));
|
||||
assertFalse(decorations.contains("Heightmap.Types.OCEAN_FLOOR_WG"));
|
||||
assertEquals(1, occurrences(source, "WorldgenTerrainHeightmaps.primeTerrain("));
|
||||
assertTrue(placement.contains("WorldgenTerrainHeightmaps.primeStructurePlacement("));
|
||||
assertTrue(placement.indexOf("WorldgenTerrainHeightmaps.primeStructurePlacement(")
|
||||
< placement.indexOf("prepareSurfaceStructures"));
|
||||
assertTrue(source.contains("engine.acquireGenerationLease(\"bukkit_nms_worldgen_heightmaps\")"));
|
||||
}
|
||||
|
||||
private static int occurrences(String source, String needle) {
|
||||
int count = 0;
|
||||
int index = source.indexOf(needle);
|
||||
while (index >= 0) {
|
||||
count++;
|
||||
index = source.indexOf(needle, index + needle.length());
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void vanillaChunkGenerationMobsUseTheVisibleBiomesVanillaDerivative() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")));
|
||||
|
||||
+6
-9
@@ -27,12 +27,11 @@ public class IrisChunkGeneratorMonumentLocateContractTest {
|
||||
assertTrue(filterStart > irisHelperStart);
|
||||
String outerMethod = source.substring(findStart, irisHelperStart);
|
||||
String irisHelper = source.substring(irisHelperStart, filterStart);
|
||||
int policyResolution = irisHelper.indexOf("NativeStructureGenerationPolicy.resolve(engine,");
|
||||
int unexploredGuard = irisHelper.indexOf("if (findUnexplored)");
|
||||
int registryLookup = irisHelper.indexOf("level.registryAccess().lookupOrThrow(Registries.STRUCTURE)");
|
||||
int replacementCheck = irisHelper.indexOf(
|
||||
"decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS");
|
||||
int irisLocate = irisHelper.indexOf("IrisStructureLocator.locate(", replacementCheck);
|
||||
int placedCheck = irisHelper.indexOf(
|
||||
"if (!IrisStructureLocator.isPlaced(engine, structureId))");
|
||||
int irisLocate = irisHelper.indexOf("IrisStructureLocator.locate(", placedCheck);
|
||||
int searchLimit = irisHelper.indexOf("LocateStatus.SEARCH_LIMIT_REACHED", irisLocate);
|
||||
int limitSkip = irisHelper.indexOf("continue;", searchLimit);
|
||||
int nativeFilter = outerMethod.indexOf("filterReachableStructures(level, holders)");
|
||||
@@ -47,12 +46,10 @@ public class IrisChunkGeneratorMonumentLocateContractTest {
|
||||
int emptyNativePartition = filterMethod.indexOf("if (candidates.isEmpty())", filterContinue);
|
||||
int reachabilityLookup = filterMethod.indexOf("reachableStructureKeys(level)", emptyNativePartition);
|
||||
|
||||
assertTrue(policyResolution >= 0);
|
||||
assertTrue(unexploredGuard >= 0);
|
||||
assertTrue(registryLookup > unexploredGuard);
|
||||
assertTrue(policyResolution > registryLookup);
|
||||
assertTrue(replacementCheck > policyResolution);
|
||||
assertTrue(irisLocate > replacementCheck);
|
||||
assertTrue(placedCheck > registryLookup);
|
||||
assertTrue(irisLocate > placedCheck);
|
||||
assertTrue(searchLimit > irisLocate);
|
||||
assertTrue(limitSkip > searchLimit);
|
||||
assertTrue(nativeFilter >= 0);
|
||||
@@ -86,7 +83,7 @@ public class IrisChunkGeneratorMonumentLocateContractTest {
|
||||
int placement = source.indexOf("start.placeInChunk(world, structureManager, generator");
|
||||
int stiltPlacement = source.indexOf("placeStilts(world, area, structureId, start", placement);
|
||||
int occupancyCheck = source.indexOf("if (state.isSolid())", stiltPlacement);
|
||||
int terrainFloor = source.indexOf("y > terrainY", stiltPlacement);
|
||||
int terrainFloor = source.indexOf("Math.max(terrainY,", stiltPlacement);
|
||||
|
||||
assertTrue(placement >= 0);
|
||||
assertTrue(stiltPlacement > placement);
|
||||
|
||||
+303
@@ -0,0 +1,303 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.IdMapper;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
import net.minecraft.world.level.chunk.PalettedContainer;
|
||||
import net.minecraft.world.level.chunk.PalettedContainerFactory;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.Strategy;
|
||||
import net.minecraft.world.level.chunk.UpgradeData;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.IglooPieces;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class WorldgenTerrainHeightmapsTest {
|
||||
private static final int MIN_Y = -64;
|
||||
private static final int HEIGHT = 384;
|
||||
private static final int LAND_TERRAIN_TOP = 70;
|
||||
private static final int LAND_CANOPY_TOP = 80;
|
||||
private static final int OCEAN_FLOOR_TOP = 60;
|
||||
private static final int OCEAN_FLUID_TOP = 64;
|
||||
private static final int LAND_X = 3;
|
||||
private static final int LAND_Z = 5;
|
||||
private static final int OCEAN_X = 8;
|
||||
private static final int OCEAN_Z = 8;
|
||||
private static final int TERRAIN_SLAB_DEPTH = 4;
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void primeTerrainWritesEngineSurfaceAndOceanFloorHeights() {
|
||||
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
|
||||
|
||||
assertFalse(chunk.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
|
||||
assertFalse(chunk.hasPrimedHeightmap(Heightmap.Types.OCEAN_FLOOR_WG));
|
||||
|
||||
WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY(), floorFirstFreeY());
|
||||
|
||||
assertTrue(chunk.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
|
||||
assertTrue(chunk.hasPrimedHeightmap(Heightmap.Types.OCEAN_FLOOR_WG));
|
||||
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
|
||||
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
|
||||
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
|
||||
Heightmap.Types.OCEAN_FLOOR_WG, LAND_X, LAND_Z));
|
||||
assertEquals(OCEAN_FLUID_TOP, chunk.getHeight(
|
||||
Heightmap.Types.WORLD_SURFACE_WG, OCEAN_X, OCEAN_Z));
|
||||
assertEquals(OCEAN_FLOOR_TOP, chunk.getHeight(
|
||||
Heightmap.Types.OCEAN_FLOOR_WG, OCEAN_X, OCEAN_Z));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void finalHeightmapPrimingDoesNotClobberTheWorldgenHeightmaps() {
|
||||
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
|
||||
|
||||
WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY(), floorFirstFreeY());
|
||||
Heightmap.primeHeightmaps(chunk, ChunkStatus.FINAL_HEIGHTMAPS);
|
||||
|
||||
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
|
||||
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
|
||||
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
|
||||
Heightmap.Types.OCEAN_FLOOR_WG, LAND_X, LAND_Z));
|
||||
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(
|
||||
Heightmap.Types.WORLD_SURFACE, LAND_X, LAND_Z));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unprimedWorldgenHeightmapResolvesTheIrisCanopyInstead() {
|
||||
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
|
||||
|
||||
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(
|
||||
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void primeTerrainOverwritesAnAlreadyLazyPrimedCanopyHeight() {
|
||||
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
|
||||
|
||||
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(
|
||||
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
|
||||
WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY(), floorFirstFreeY());
|
||||
|
||||
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
|
||||
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structurePlacementPrimesTheWestNeighbourAnIglooEntranceReads() {
|
||||
Map<Long, ChunkAccess> chunks = new HashMap<>();
|
||||
chunks.put(ChunkPos.pack(0, 0), terrainChunk(new ChunkPos(0, 0)));
|
||||
chunks.put(ChunkPos.pack(-1, 0), terrainChunk(new ChunkPos(-1, 0)));
|
||||
WorldGenLevel world = world(chunks);
|
||||
StructureStart start = startInOrigin();
|
||||
|
||||
int canopySnap = IglooPieces.GENERATION_HEIGHT + iglooSnapOffset(world);
|
||||
WorldgenTerrainHeightmaps.primeStructurePlacement(
|
||||
world, List.of(start), surfaceFirstFreeY(), floorFirstFreeY());
|
||||
int terrainSnap = IglooPieces.GENERATION_HEIGHT + iglooSnapOffset(world);
|
||||
|
||||
assertEquals(LAND_TERRAIN_TOP + 1, world.getHeight(
|
||||
Heightmap.Types.WORLD_SURFACE_WG, -2, 5));
|
||||
assertEquals(LAND_TERRAIN_TOP, terrainSnap);
|
||||
assertEquals(LAND_CANOPY_TOP, canopySnap);
|
||||
assertEquals(LAND_CANOPY_TOP - LAND_TERRAIN_TOP, canopySnap - terrainSnap);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structurePlacementPrimesEveryChunkTheStartFootprintTouches() {
|
||||
Map<Long, ChunkAccess> chunks = new HashMap<>();
|
||||
for (int chunkX = -1; chunkX <= 1; chunkX++) {
|
||||
for (int chunkZ = -1; chunkZ <= 1; chunkZ++) {
|
||||
chunks.put(ChunkPos.pack(chunkX, chunkZ), terrainChunk(new ChunkPos(chunkX, chunkZ)));
|
||||
}
|
||||
}
|
||||
WorldGenLevel world = world(chunks);
|
||||
|
||||
WorldgenTerrainHeightmaps.primeStructurePlacement(
|
||||
world, List.of(startInOrigin()), surfaceFirstFreeY(), floorFirstFreeY());
|
||||
|
||||
for (ChunkAccess chunk : chunks.values()) {
|
||||
assertTrue(chunk.getPos().toString(),
|
||||
chunk.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
|
||||
assertTrue(chunk.getPos().toString(),
|
||||
chunk.hasPrimedHeightmap(Heightmap.Types.OCEAN_FLOOR_WG));
|
||||
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
|
||||
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structurePlacementSkipsChunksOutsideTheGenerationRegion() {
|
||||
Map<Long, ChunkAccess> chunks = new HashMap<>();
|
||||
ProtoChunk origin = terrainChunk(new ChunkPos(0, 0));
|
||||
chunks.put(ChunkPos.pack(0, 0), origin);
|
||||
WorldGenLevel world = world(chunks);
|
||||
|
||||
WorldgenTerrainHeightmaps.primeStructurePlacement(
|
||||
world, List.of(startInOrigin()), surfaceFirstFreeY(), floorFirstFreeY());
|
||||
|
||||
assertTrue(origin.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
|
||||
assertEquals(LAND_TERRAIN_TOP, origin.getHeight(
|
||||
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
|
||||
}
|
||||
|
||||
private static int iglooSnapOffset(WorldGenLevel world) {
|
||||
return world.getHeight(Heightmap.Types.WORLD_SURFACE_WG, -2, 5)
|
||||
- IglooPieces.GENERATION_HEIGHT - 1;
|
||||
}
|
||||
|
||||
private static StructureStart startInOrigin() {
|
||||
Structure structure = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty()));
|
||||
SwampHutPiece piece = new SwampHutPiece(RandomSource.create(13L), 0, 0);
|
||||
return new StructureStart(
|
||||
structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece)));
|
||||
}
|
||||
|
||||
private static IntBinaryOperator surfaceFirstFreeY() {
|
||||
return (x, z) -> isOcean(x, z) ? OCEAN_FLUID_TOP + 1 : LAND_TERRAIN_TOP + 1;
|
||||
}
|
||||
|
||||
private static IntBinaryOperator floorFirstFreeY() {
|
||||
return (x, z) -> isOcean(x, z) ? OCEAN_FLOOR_TOP + 1 : LAND_TERRAIN_TOP + 1;
|
||||
}
|
||||
|
||||
private static boolean isOcean(int x, int z) {
|
||||
return Math.floorMod(x, 16) == OCEAN_X && Math.floorMod(z, 16) == OCEAN_Z;
|
||||
}
|
||||
|
||||
private static ProtoChunk terrainChunk(ChunkPos pos) {
|
||||
ProtoChunk chunk = new ProtoChunk(
|
||||
pos, UpgradeData.EMPTY, LevelHeightAccessor.create(MIN_Y, HEIGHT),
|
||||
containerFactory(), null);
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
boolean ocean = isOcean(pos.getMinBlockX() + x, pos.getMinBlockZ() + z);
|
||||
int stoneTop = ocean ? OCEAN_FLOOR_TOP : LAND_TERRAIN_TOP;
|
||||
for (int y = stoneTop - TERRAIN_SLAB_DEPTH; y <= stoneTop; y++) {
|
||||
write(chunk, x, y, z, Blocks.STONE.defaultBlockState());
|
||||
}
|
||||
if (ocean) {
|
||||
for (int y = OCEAN_FLOOR_TOP + 1; y <= OCEAN_FLUID_TOP; y++) {
|
||||
write(chunk, x, y, z, Blocks.WATER.defaultBlockState());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (int y = LAND_TERRAIN_TOP + 1; y <= LAND_TERRAIN_TOP + 6; y++) {
|
||||
write(chunk, x, y, z, Blocks.SPRUCE_LOG.defaultBlockState());
|
||||
}
|
||||
for (int y = LAND_TERRAIN_TOP + 7; y <= LAND_CANOPY_TOP; y++) {
|
||||
write(chunk, x, y, z, Blocks.SPRUCE_LEAVES.defaultBlockState());
|
||||
}
|
||||
}
|
||||
}
|
||||
return chunk;
|
||||
}
|
||||
|
||||
private static void write(ProtoChunk chunk, int x, int y, int z, BlockState state) {
|
||||
LevelChunkSection section = chunk.getSection(chunk.getSectionIndex(y));
|
||||
section.setBlockState(x, y & 15, z, state, false);
|
||||
}
|
||||
|
||||
private static PalettedContainerFactory containerFactory() {
|
||||
Strategy<BlockState> blockStates = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY);
|
||||
BlockState air = Blocks.AIR.defaultBlockState();
|
||||
IdMapper<Holder<Biome>> biomeIds = new IdMapper<>();
|
||||
Holder<Biome> defaultBiome = Holder.direct((Biome) null);
|
||||
biomeIds.add(defaultBiome);
|
||||
Strategy<Holder<Biome>> biomes = Strategy.createForBiomes(biomeIds);
|
||||
Codec<Holder<Biome>> biomeCodec = Codec.STRING.xmap(
|
||||
name -> defaultBiome, holder -> "iris-test-biome");
|
||||
return new PalettedContainerFactory(
|
||||
blockStates,
|
||||
air,
|
||||
PalettedContainer.codecRW(BlockState.CODEC, blockStates, air),
|
||||
biomes,
|
||||
defaultBiome,
|
||||
PalettedContainer.codecRO(biomeCodec, biomes, defaultBiome),
|
||||
PalettedContainer.codecRW(biomeCodec, biomes, defaultBiome));
|
||||
}
|
||||
|
||||
private static WorldGenLevel world(Map<Long, ChunkAccess> chunks) {
|
||||
InvocationHandler handler = (proxy, method, arguments) -> {
|
||||
String methodName = method.getName();
|
||||
if (methodName.equals("hasChunk")) {
|
||||
return chunks.containsKey(ChunkPos.pack((int) arguments[0], (int) arguments[1]));
|
||||
}
|
||||
if (methodName.equals("getChunk") && arguments.length == 2) {
|
||||
ChunkAccess chunk = chunks.get(ChunkPos.pack((int) arguments[0], (int) arguments[1]));
|
||||
if (chunk == null) {
|
||||
throw new AssertionError("Heightmap priming requested an unavailable chunk "
|
||||
+ arguments[0] + "," + arguments[1]);
|
||||
}
|
||||
return chunk;
|
||||
}
|
||||
if (methodName.equals("getHeight") && arguments.length == 3) {
|
||||
Heightmap.Types type = (Heightmap.Types) arguments[0];
|
||||
int x = (int) arguments[1];
|
||||
int z = (int) arguments[2];
|
||||
ChunkAccess chunk = chunks.get(ChunkPos.pack(x >> 4, z >> 4));
|
||||
if (chunk == null) {
|
||||
throw new AssertionError("Heightmap read requested an unavailable chunk at "
|
||||
+ x + "," + z);
|
||||
}
|
||||
return chunk.getHeight(type, x & 15, z & 15) + 1;
|
||||
}
|
||||
if (methodName.equals("getBlockState")) {
|
||||
BlockPos position = (BlockPos) arguments[0];
|
||||
ChunkAccess chunk = chunks.get(ChunkPos.pack(position.getX() >> 4, position.getZ() >> 4));
|
||||
if (chunk == null) {
|
||||
throw new AssertionError("Block read requested an unavailable chunk at " + position);
|
||||
}
|
||||
return chunk.getBlockState(position);
|
||||
}
|
||||
if (methodName.equals("hashCode")) {
|
||||
return System.identityHashCode(proxy);
|
||||
}
|
||||
if (methodName.equals("equals")) {
|
||||
return proxy == arguments[0];
|
||||
}
|
||||
if (methodName.equals("toString")) {
|
||||
return "worldgen-heightmap-test-world";
|
||||
}
|
||||
throw new UnsupportedOperationException(method.toString());
|
||||
};
|
||||
return (WorldGenLevel) Proxy.newProxyInstance(
|
||||
WorldGenLevel.class.getClassLoader(), new Class<?>[]{WorldGenLevel.class}, handler);
|
||||
}
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisJigsawConfiguration;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeManager;
|
||||
import net.minecraft.world.level.biome.FixedBiomeSource;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.blending.Blender;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
public class NativeStructureFactoryTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void genericSourceDoesNotRequireJigsawType() {
|
||||
Structure source = new SwampHutStructure(
|
||||
new Structure.StructureSettings(HolderSet.empty()));
|
||||
|
||||
Structure configured = NativeStructureFactory.configure(
|
||||
null, source, null, false, 80);
|
||||
|
||||
assertSame(source, configured);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jigsawOptionsFailClosedForNonJigsawSource() {
|
||||
Structure source = new SwampHutStructure(
|
||||
new Structure.StructureSettings(HolderSet.empty()));
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> NativeStructureFactory.configure(
|
||||
null, source, new IrisJigsawConfiguration(), false, 80));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void forcedGeneratorSuppliesDeterministicPermissiveEnvironment() {
|
||||
ChunkGenerator delegate = new TestChunkGenerator();
|
||||
Holder<Biome> biome = Holder.direct((Biome) null);
|
||||
LevelHeightAccessor heightAccessor = LevelHeightAccessor.create(-64, 384);
|
||||
ForcedStructureChunkGenerator generator = new ForcedStructureChunkGenerator(
|
||||
delegate, biome, -20);
|
||||
|
||||
assertEquals(-63, generator.getSeaLevel());
|
||||
assertEquals(81, generator.getBaseHeight(
|
||||
0, 0, Heightmap.Types.WORLD_SURFACE_WG, heightAccessor, null));
|
||||
NoiseColumn column = generator.getBaseColumn(
|
||||
0, 0, heightAccessor, null);
|
||||
assertSame(Blocks.STONE, column.getBlock(80).getBlock());
|
||||
assertSame(Blocks.AIR, column.getBlock(81).getBlock());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void undergroundRelocationMovesGenericPiecesAndReturnsFreshStart() {
|
||||
Structure source = new SwampHutStructure(
|
||||
new Structure.StructureSettings(HolderSet.empty()));
|
||||
SwampHutPiece piece = new SwampHutPiece(RandomSource.create(17L), 0, 0);
|
||||
StructureStart start = new StructureStart(
|
||||
source,
|
||||
new ChunkPos(0, 0),
|
||||
0,
|
||||
new PiecesContainer(List.of(piece))
|
||||
);
|
||||
|
||||
StructureStart relocated = NativeStructurePostProcessor.relocateToMinY(
|
||||
start, source, -20, LevelHeightAccessor.create(-64, 384));
|
||||
|
||||
assertNotSame(start, relocated);
|
||||
assertEquals(-20, relocated.getPieces().getFirst().getBoundingBox().minY());
|
||||
assertEquals(-20, relocated.getBoundingBox().minY());
|
||||
}
|
||||
|
||||
private static final class TestChunkGenerator extends ChunkGenerator {
|
||||
private TestChunkGenerator() {
|
||||
super(new FixedBiomeSource(Holder.direct((Biome) null)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<? extends ChunkGenerator> codec() {
|
||||
return MapCodec.unit(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyCarvers(WorldGenRegion region, long seed, RandomState randomState,
|
||||
BiomeManager biomeManager, StructureManager structureManager,
|
||||
ChunkAccess chunk) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buildSurface(WorldGenRegion level, StructureManager structureManager,
|
||||
RandomState randomState, ChunkAccess protoChunk) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spawnOriginalMobs(WorldGenRegion worldGenRegion) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getGenDepth() {
|
||||
return 384;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ChunkAccess> fillFromNoise(
|
||||
Blender blender, RandomState randomState,
|
||||
StructureManager structureManager, ChunkAccess centerChunk) {
|
||||
return CompletableFuture.completedFuture(centerChunk);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSeaLevel() {
|
||||
return 63;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinY() {
|
||||
return -64;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBaseHeight(int x, int z, Heightmap.Types type,
|
||||
LevelHeightAccessor heightAccessor, RandomState randomState) {
|
||||
return 64;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor heightAccessor,
|
||||
RandomState randomState) {
|
||||
BlockState[] states = new BlockState[heightAccessor.getHeight()];
|
||||
for (int index = 0; index < states.length; index++) {
|
||||
states[index] = Blocks.STONE.defaultBlockState();
|
||||
}
|
||||
return new NoiseColumn(heightAccessor.getMinY(), states);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addDebugScreenInfo(List<String> result, RandomState randomState,
|
||||
BlockPos feetPos) {
|
||||
}
|
||||
}
|
||||
}
|
||||
+301
@@ -0,0 +1,301 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisMaterialPalette;
|
||||
import art.arcane.iris.engine.object.IrisStructureTerrain;
|
||||
import art.arcane.iris.engine.object.IrisStructureTerrainMode;
|
||||
import art.arcane.iris.engine.object.IrisStructureYBand;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NativeStructurePostProcessorEncaseTest {
|
||||
private static final long TEST_SEED = 1234L;
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraft() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encaseFillsAirAndLiquidWithoutOverwritingExistingTerrain() {
|
||||
StructureStart start = start(TerrainAdjustment.BURY, 64);
|
||||
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
|
||||
BoundingBox area = new BoundingBox(
|
||||
bounds.minX() - 1, bounds.minY() - 1, bounds.minZ() - 1,
|
||||
bounds.maxX() + 1, bounds.maxY() + 1, bounds.maxZ() + 1);
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
put(blocks, bounds.minX(), bounds.minY(), bounds.minZ(), Blocks.DIRT.defaultBlockState());
|
||||
put(blocks, bounds.maxX(), bounds.minY(), bounds.maxZ(), Blocks.WATER.defaultBlockState());
|
||||
|
||||
NativeStructurePostProcessor.integrateTerrain(
|
||||
world(blocks), area, "minecraft:stronghold", start,
|
||||
new IrisStructureTerrain()
|
||||
.setMode(IrisStructureTerrainMode.ENCASE)
|
||||
.setHorizontalPadding(1)
|
||||
.setCeilingPadding(1)
|
||||
.setFloorPadding(1),
|
||||
null);
|
||||
|
||||
assertEquals(Blocks.DIRT.defaultBlockState(),
|
||||
state(blocks, bounds.minX(), bounds.minY(), bounds.minZ()));
|
||||
assertEquals(Blocks.STONE.defaultBlockState(),
|
||||
state(blocks, bounds.maxX(), bounds.minY(), bounds.maxZ()));
|
||||
assertEquals(Blocks.STONE.defaultBlockState(),
|
||||
state(blocks, bounds.minX() - 1, bounds.minY() - 1, bounds.minZ() - 1));
|
||||
assertEquals(Blocks.STONE.defaultBlockState(),
|
||||
state(blocks, bounds.maxX() + 1, bounds.maxY() + 1, bounds.maxZ() + 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultEncasePaletteSplitsStoneAboveZeroAndDeepslateBelow() {
|
||||
StructureStart start = start(TerrainAdjustment.BURY, -1);
|
||||
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
|
||||
NativeStructurePostProcessor.integrateTerrain(
|
||||
world(blocks), bounds, "minecraft:stronghold", start,
|
||||
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.ENCASE),
|
||||
null);
|
||||
|
||||
assertEquals(-1, bounds.minY());
|
||||
assertEquals(Blocks.DEEPSLATE.defaultBlockState(),
|
||||
state(blocks, bounds.minX(), -1, bounds.minZ()));
|
||||
assertEquals(Blocks.STONE.defaultBlockState(),
|
||||
state(blocks, bounds.minX(), 0, bounds.minZ()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configuredEncasePaletteReplacesTheDefaultMaterial() {
|
||||
StructureStart start = start(TerrainAdjustment.BURY, 64);
|
||||
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
IrisMaterialPalette palette = new IrisMaterialPalette().qclear().qadd("minecraft:tuff");
|
||||
|
||||
NativeStructurePostProcessor.integrateTerrain(
|
||||
world(blocks), bounds, "minecraft:stronghold", start,
|
||||
new IrisStructureTerrain()
|
||||
.setMode(IrisStructureTerrainMode.ENCASE)
|
||||
.setEncasePalette(palette),
|
||||
NativeStructurePostProcessorEncaseTest::tuffBlock);
|
||||
|
||||
assertEquals(Blocks.TUFF.defaultBlockState(),
|
||||
state(blocks, bounds.minX(), bounds.minY(), bounds.minZ()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encaseFillRunsBeforeNativePlacement() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")));
|
||||
int prepareTerrain = source.indexOf("NativeStructurePostProcessor.prepareTerrain(");
|
||||
int placementLoop = source.indexOf("for (NativePlacementGroup group : placementGroups)", prepareTerrain);
|
||||
int placement = source.indexOf("placeVanillaStructure(world, structureManager, random", placementLoop);
|
||||
|
||||
assertTrue(prepareTerrain >= 0);
|
||||
assertTrue(placementLoop > prepareTerrain);
|
||||
assertTrue(placement > placementLoop);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buryAndEncapsulateAdaptationsAutoDefaultToEncase() {
|
||||
for (TerrainAdjustment adjustment : List.of(TerrainAdjustment.BURY, TerrainAdjustment.ENCAPSULATE)) {
|
||||
IrisStructureTerrain resolved = NativeStructurePostProcessor.resolveNativeTerrain(
|
||||
start(adjustment, 64), null);
|
||||
|
||||
assertEquals(IrisStructureTerrainMode.ENCASE, resolved.resolvedMode());
|
||||
assertEquals(3, resolved.getHorizontalPadding());
|
||||
assertEquals(3, resolved.getCeilingPadding());
|
||||
assertEquals(3, resolved.getFloorPadding());
|
||||
assertNull(resolved.getEncasePalette());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void otherAdaptationsNeverAutoDefaultToEncase() {
|
||||
for (TerrainAdjustment adjustment : List.of(
|
||||
TerrainAdjustment.NONE, TerrainAdjustment.BEARD_THIN, TerrainAdjustment.BEARD_BOX)) {
|
||||
assertNull(NativeStructurePostProcessor.resolveNativeTerrain(
|
||||
start(adjustment, 64), null));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void explicitTerrainConfigurationWinsOverAutoEncase() {
|
||||
IrisStructureTerrain configured = new IrisStructureTerrain()
|
||||
.setMode(IrisStructureTerrainMode.SOURCE);
|
||||
|
||||
assertSame(configured, NativeStructurePostProcessor.resolveNativeTerrain(
|
||||
start(TerrainAdjustment.BURY, 64), configured));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encaseReservesTheNeighborChunkEnvelope() {
|
||||
StructureStart generated = start(TerrainAdjustment.BURY, 64);
|
||||
BoundingBox content = generated.getBoundingBox();
|
||||
|
||||
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
|
||||
generated,
|
||||
generated.getStructure(),
|
||||
0,
|
||||
null,
|
||||
new IrisStructureTerrain()
|
||||
.setMode(IrisStructureTerrainMode.ENCASE)
|
||||
.setHorizontalPadding(4));
|
||||
|
||||
assertEquals(content.minX() - 4, wrapped.getBoundingBox().minX());
|
||||
assertEquals(content.maxX() + 4, wrapped.getBoundingBox().maxX());
|
||||
assertEquals(content.minZ() - 4, wrapped.getBoundingBox().minZ());
|
||||
assertEquals(content.maxZ() + 4, wrapped.getBoundingBox().maxZ());
|
||||
assertEquals(2, wrapped.getPieces().stream()
|
||||
.filter(NativeStructureReferenceEnvelope::isMarker)
|
||||
.count());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void yBandRelocatesTheStartMidpointDeterministically() {
|
||||
IrisStructureYBand band = new IrisStructureYBand().setMin(-120).setMax(-20);
|
||||
StructureStart first = start(TerrainAdjustment.BURY, 64);
|
||||
StructureStart second = start(TerrainAdjustment.BURY, 64);
|
||||
|
||||
NativeStructurePostProcessor.applyVerticalShift(
|
||||
first, -64, -256, 320, true, false, band, (x, z) -> 40);
|
||||
NativeStructurePostProcessor.applyVerticalShift(
|
||||
second, -64, -256, 320, true, false, band, (x, z) -> 40);
|
||||
|
||||
BoundingBox bounds = first.getBoundingBox();
|
||||
assertEquals(bounds.minY(), second.getBoundingBox().minY());
|
||||
assertTrue(bounds.minY() >= -120);
|
||||
assertTrue(bounds.maxY() <= -20);
|
||||
|
||||
int repeated = NativeStructurePostProcessor.applyVerticalShift(
|
||||
first, -64, -256, 320, true, false, band, (x, z) -> 40);
|
||||
assertEquals(0, repeated);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void yBandClampsWhenTheBandCannotContainTheStructure() {
|
||||
IrisStructureYBand band = new IrisStructureYBand().setMin(-50).setMax(-45);
|
||||
StructureStart start = start(TerrainAdjustment.BURY, 64);
|
||||
|
||||
NativeStructurePostProcessor.applyVerticalShift(
|
||||
start, 0, -256, 320, true, false, band, (x, z) -> 40);
|
||||
|
||||
BoundingBox bounds = start.getBoundingBox();
|
||||
int midpoint = bounds.minY() + (bounds.maxY() - bounds.minY()) / 2;
|
||||
assertTrue(midpoint >= -50);
|
||||
assertTrue(midpoint <= -45);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void yBandReplacesBothTheBlindShiftAndBurial() {
|
||||
IrisStructureYBand band = new IrisStructureYBand().setMin(-64).setMax(-64);
|
||||
StructureStart start = start(TerrainAdjustment.BURY, 64);
|
||||
BoundingBox bounds = start.getBoundingBox();
|
||||
int height = bounds.maxY() - bounds.minY();
|
||||
|
||||
NativeStructurePostProcessor.applyVerticalShift(
|
||||
start, -200, -256, 320, true, false, band, (x, z) -> 40);
|
||||
|
||||
assertEquals(-64 - height / 2, start.getBoundingBox().minY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preserveSourceYWinsOverTheConfiguredBand() {
|
||||
IrisStructureYBand band = new IrisStructureYBand().setMin(-120).setMax(-20);
|
||||
StructureStart start = start(TerrainAdjustment.BURY, 64);
|
||||
int minY = start.getBoundingBox().minY();
|
||||
|
||||
int offset = NativeStructurePostProcessor.applyVerticalShift(
|
||||
start, 0, -256, 320, true, true, band, (x, z) -> 40);
|
||||
|
||||
assertEquals(0, offset);
|
||||
assertEquals(minY, start.getBoundingBox().minY());
|
||||
}
|
||||
|
||||
private static BlockState tuffBlock(IrisMaterialPalette palette, RNG rng, int x, int y, int z) {
|
||||
assertNotNull(palette);
|
||||
assertNotNull(rng);
|
||||
return Blocks.TUFF.defaultBlockState();
|
||||
}
|
||||
|
||||
private static StructureStart start(TerrainAdjustment adjustment, int minY) {
|
||||
Structure structure = new DesertPyramidStructure(new Structure.StructureSettings(
|
||||
HolderSet.empty(), Map.of(), GenerationStep.Decoration.STRONGHOLDS, adjustment));
|
||||
StructurePiece piece = new DesertPyramidPiece(RandomSource.create(7L), 0, 0);
|
||||
piece.move(0, minY - piece.getBoundingBox().minY(), 0);
|
||||
return new StructureStart(
|
||||
structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece)));
|
||||
}
|
||||
|
||||
private static WorldGenLevel world(Map<BlockPos, BlockState> blocks) {
|
||||
InvocationHandler handler = (proxy, method, arguments) -> {
|
||||
String methodName = method.getName();
|
||||
if (methodName.equals("getBlockState")) {
|
||||
BlockPos position = (BlockPos) arguments[0];
|
||||
return state(blocks, position.getX(), position.getY(), position.getZ());
|
||||
}
|
||||
if (methodName.equals("setBlock")) {
|
||||
BlockPos position = (BlockPos) arguments[0];
|
||||
BlockState blockState = (BlockState) arguments[1];
|
||||
put(blocks, position.getX(), position.getY(), position.getZ(), blockState);
|
||||
return true;
|
||||
}
|
||||
if (methodName.equals("getSeed")) {
|
||||
return TEST_SEED;
|
||||
}
|
||||
if (methodName.equals("hashCode")) {
|
||||
return System.identityHashCode(proxy);
|
||||
}
|
||||
if (methodName.equals("equals")) {
|
||||
return proxy == arguments[0];
|
||||
}
|
||||
if (methodName.equals("toString")) {
|
||||
return "encase-test-world";
|
||||
}
|
||||
throw new UnsupportedOperationException(method.toString());
|
||||
};
|
||||
return (WorldGenLevel) Proxy.newProxyInstance(
|
||||
WorldGenLevel.class.getClassLoader(), new Class<?>[]{WorldGenLevel.class}, handler);
|
||||
}
|
||||
|
||||
private static void put(Map<BlockPos, BlockState> blocks,
|
||||
int x, int y, int z, BlockState state) {
|
||||
blocks.put(new BlockPos(x, y, z), state);
|
||||
}
|
||||
|
||||
private static BlockState state(Map<BlockPos, BlockState> blocks, int x, int y, int z) {
|
||||
return blocks.getOrDefault(new BlockPos(x, y, z), Blocks.AIR.defaultBlockState());
|
||||
}
|
||||
}
|
||||
+7
-7
@@ -34,7 +34,7 @@ public class NativeStructurePostProcessorMonumentTest {
|
||||
StructureStart start = monumentStart(1337L);
|
||||
|
||||
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
|
||||
start, "minecraft:monument", 0, 63, -64, 320, false, (x, z) -> 0);
|
||||
start, "minecraft:monument", 0, 63, -64, 320, false, false, null, (x, z) -> 0);
|
||||
|
||||
assertEquals(0, offset);
|
||||
assertEquals(39, start.getBoundingBox().minY());
|
||||
@@ -50,7 +50,7 @@ public class NativeStructurePostProcessorMonumentTest {
|
||||
BoundingBox cachedBounds = start.getBoundingBox();
|
||||
|
||||
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
|
||||
start, "minecraft:monument", 0, 50, -256, 512, false, (x, z) -> 0);
|
||||
start, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
|
||||
|
||||
assertEquals(-13, offset);
|
||||
assertSame(cachedBounds, start.getBoundingBox());
|
||||
@@ -64,7 +64,7 @@ public class NativeStructurePostProcessorMonumentTest {
|
||||
}
|
||||
|
||||
int repeatedOffset = NativeStructurePostProcessor.applyVerticalPlacement(
|
||||
start, "minecraft:monument", 0, 50, -256, 512, false, (x, z) -> 0);
|
||||
start, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
|
||||
assertEquals(0, repeatedOffset);
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ public class NativeStructurePostProcessorMonumentTest {
|
||||
StructureStart start = monumentStart(1337L);
|
||||
|
||||
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
|
||||
start, "minecraft:monument", 3, 50, -256, 512, false, (x, z) -> 0);
|
||||
start, "minecraft:monument", 3, 50, -256, 512, false, false, null, (x, z) -> 0);
|
||||
|
||||
assertEquals(-10, offset);
|
||||
assertEquals(29, start.getBoundingBox().minY());
|
||||
@@ -86,7 +86,7 @@ public class NativeStructurePostProcessorMonumentTest {
|
||||
ChunkPos chunkPos = new ChunkPos(0, 0);
|
||||
StructureStart initial = monumentStart(seed);
|
||||
NativeStructurePostProcessor.applyVerticalPlacement(
|
||||
initial, "minecraft:monument", 0, 50, -256, 512, false, (x, z) -> 0);
|
||||
initial, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
|
||||
|
||||
PiecesContainer regenerated = OceanMonumentStructure.regeneratePiecesAfterLoad(
|
||||
chunkPos, seed, new PiecesContainer(initial.getPieces()));
|
||||
@@ -95,7 +95,7 @@ public class NativeStructurePostProcessorMonumentTest {
|
||||
|
||||
assertEquals(39, reloaded.getBoundingBox().minY());
|
||||
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
|
||||
reloaded, "minecraft:monument", 0, 50, -256, 512, false, (x, z) -> 0);
|
||||
reloaded, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
|
||||
assertEquals(-13, offset);
|
||||
assertEquals(26, reloaded.getBoundingBox().minY());
|
||||
assertEquals(48, reloaded.getBoundingBox().maxY());
|
||||
@@ -106,7 +106,7 @@ public class NativeStructurePostProcessorMonumentTest {
|
||||
StructureStart start = monumentStart(1337L);
|
||||
try {
|
||||
NativeStructurePostProcessor.applyVerticalPlacement(
|
||||
start, "minecraft:monument", 0, -50, -64, 320, false, (x, z) -> 0);
|
||||
start, "minecraft:monument", 0, -50, -64, 320, false, false, null, (x, z) -> 0);
|
||||
} catch (IllegalStateException error) {
|
||||
assertTrue(error.getMessage().contains("cannot align"));
|
||||
return;
|
||||
|
||||
+21
-20
@@ -39,37 +39,38 @@ public class NativeStructurePostProcessorScatteredFeatureTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void desertPyramidUsesTheHighestFootprintSurfaceAndLocksVanillaReanchor() {
|
||||
public void desertPyramidUsesTheLowestFootprintSurfaceAndLocksVanillaReanchor() {
|
||||
TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(11L), 0, 0);
|
||||
StructureStart start = desertStart(piece);
|
||||
BoundingBox cachedBounds = start.getBoundingBox();
|
||||
BoundingBox footprint = piece.getBoundingBox();
|
||||
|
||||
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
|
||||
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false,
|
||||
(x, z) -> x == footprint.maxX() && z == footprint.maxZ() ? 92 : 64);
|
||||
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null,
|
||||
(x, z) -> x == footprint.maxX() && z == footprint.maxZ() ? 64 : 92);
|
||||
|
||||
assertEquals(29, offset);
|
||||
assertEquals(1, offset);
|
||||
assertSame(cachedBounds, start.getBoundingBox());
|
||||
assertEquals(93, piece.getBoundingBox().minY());
|
||||
assertEquals(93, start.getBoundingBox().minY());
|
||||
assertEquals(65, piece.getBoundingBox().minY());
|
||||
assertEquals(65, start.getBoundingBox().minY());
|
||||
assertTrue(piece.attemptVanillaAlignment(-2));
|
||||
assertEquals(93, piece.getBoundingBox().minY());
|
||||
assertEquals(65, piece.getBoundingBox().minY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jungleTempleHonorsConfiguredShiftAndLocksVanillaReanchor() {
|
||||
public void jungleTempleUsesAverageFootprintSurfaceAndHonorsConfiguredShift() {
|
||||
TestJungleTemplePiece piece = new TestJungleTemplePiece(RandomSource.create(19L), 32, -16);
|
||||
StructureStart start = jungleStart(piece);
|
||||
BoundingBox footprint = piece.getBoundingBox();
|
||||
|
||||
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
|
||||
start, "minecraft:jungle_pyramid", 4, 63, -64, 320, false,
|
||||
(x, z) -> x == 32 && z == -16 ? 88 : 70);
|
||||
start, "minecraft:jungle_pyramid", 4, 63, -64, 320, false, false, null,
|
||||
(x, z) -> x == footprint.minX() && z == footprint.minZ() ? 88 : 70);
|
||||
|
||||
assertEquals(29, offset);
|
||||
assertEquals(93, piece.getBoundingBox().minY());
|
||||
assertEquals(11, offset);
|
||||
assertEquals(75, piece.getBoundingBox().minY());
|
||||
assertTrue(piece.attemptVanillaAlignment());
|
||||
assertEquals(93, piece.getBoundingBox().minY());
|
||||
assertEquals(75, piece.getBoundingBox().minY());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -78,9 +79,9 @@ public class NativeStructurePostProcessorScatteredFeatureTest {
|
||||
StructureStart start = desertStart(piece);
|
||||
|
||||
int initialOffset = NativeStructurePostProcessor.applyVerticalPlacement(
|
||||
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, (x, z) -> 80);
|
||||
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 80);
|
||||
int repeatedOffset = NativeStructurePostProcessor.applyVerticalPlacement(
|
||||
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, (x, z) -> 80);
|
||||
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 80);
|
||||
|
||||
assertEquals(17, initialOffset);
|
||||
assertEquals(0, repeatedOffset);
|
||||
@@ -93,7 +94,7 @@ public class NativeStructurePostProcessorScatteredFeatureTest {
|
||||
StructureStart start = desertStart(piece);
|
||||
|
||||
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
|
||||
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, (x, z) -> 318);
|
||||
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 318);
|
||||
|
||||
assertEquals(241, offset);
|
||||
assertEquals(305, piece.getBoundingBox().minY());
|
||||
@@ -101,7 +102,7 @@ public class NativeStructurePostProcessorScatteredFeatureTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onlyPyramidsAndTemplesOptIntoScatteredSurfaceFitting() {
|
||||
public void scatteredStructuresDoNotOptIntoFullFootprintSurfaceFitting() {
|
||||
TestDesertPyramidPiece desert = new TestDesertPyramidPiece(RandomSource.create(31L), 0, 0);
|
||||
TestJungleTemplePiece jungle = new TestJungleTemplePiece(RandomSource.create(37L), 0, 0);
|
||||
SwampHutPiece swamp = new SwampHutPiece(RandomSource.create(41L), 0, 0);
|
||||
@@ -109,13 +110,13 @@ public class NativeStructurePostProcessorScatteredFeatureTest {
|
||||
StructureStart jungleStart = jungleStart(jungle);
|
||||
StructureStart swampStart = swampStart(swamp);
|
||||
|
||||
assertTrue(NativeStructurePostProcessor.requiresSurfaceTerrain(desertStart));
|
||||
assertTrue(NativeStructurePostProcessor.requiresSurfaceTerrain(jungleStart));
|
||||
assertFalse(NativeStructurePostProcessor.requiresSurfaceTerrain(desertStart));
|
||||
assertFalse(NativeStructurePostProcessor.requiresSurfaceTerrain(jungleStart));
|
||||
assertFalse(NativeStructurePostProcessor.requiresSurfaceTerrain(swampStart));
|
||||
|
||||
AtomicInteger terrainQueries = new AtomicInteger();
|
||||
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
|
||||
swampStart, "minecraft:swamp_hut", 0, 63, -64, 320, false,
|
||||
swampStart, "minecraft:swamp_hut", 0, 63, -64, 320, false, false, null,
|
||||
(x, z) -> terrainQueries.incrementAndGet());
|
||||
assertEquals(0, offset);
|
||||
assertEquals(0, terrainQueries.get());
|
||||
|
||||
+529
@@ -1,19 +1,32 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import art.arcane.iris.engine.mantle.components.StructureCarvingFootprint;
|
||||
import art.arcane.iris.engine.object.IrisStructureCarveShape;
|
||||
import art.arcane.iris.engine.object.IrisStructureTerrain;
|
||||
import art.arcane.iris.engine.object.IrisStructureTerrainMode;
|
||||
import com.mojang.datafixers.util.Either;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
@@ -26,11 +39,20 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NativeStructurePostProcessorSurfaceTerrainTest {
|
||||
private static final int SLAB_DEPTH = 128;
|
||||
private static final int SLAB_MIN_Y = 64;
|
||||
private static final int SLAB_PADDING = 14;
|
||||
private static final int SLAB_WIDTH = 16;
|
||||
private static final long TEST_SEED = 8675309L;
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraft() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
@@ -209,6 +231,408 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
|
||||
assertEquals(Blocks.GRAVEL.defaultBlockState(), state(blocks, 0, 62, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preserveSourceYSkipsBurialAndKeepsTheVanillaStartY() {
|
||||
StructureStart start = desertStart();
|
||||
int minY = start.getBoundingBox().minY();
|
||||
|
||||
int offset = NativeStructurePostProcessor.applyVerticalShift(
|
||||
start, 0, -64, 320, true, true, null, (x, z) -> 40);
|
||||
|
||||
assertEquals(0, offset);
|
||||
assertEquals(minY, start.getBoundingBox().minY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preserveSourceYStillAppliesAnExplicitShift() {
|
||||
StructureStart start = desertStart();
|
||||
int minY = start.getBoundingBox().minY();
|
||||
|
||||
int offset = NativeStructurePostProcessor.applyVerticalShift(
|
||||
start, -8, -64, 320, true, true, null, (x, z) -> 40);
|
||||
|
||||
assertEquals(-8, offset);
|
||||
assertEquals(minY - 8, start.getBoundingBox().minY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void burialStillSinksTheStructureBelowTheLowestTerrainColumn() {
|
||||
StructureStart start = desertStart();
|
||||
int minY = start.getBoundingBox().minY();
|
||||
int maxY = start.getBoundingBox().maxY();
|
||||
int expected = 40 - 1 - maxY;
|
||||
|
||||
int offset = NativeStructurePostProcessor.applyVerticalShift(
|
||||
start, 0, -64, 320, true, false, null, (x, z) -> 40);
|
||||
|
||||
assertEquals(expected, offset);
|
||||
assertEquals(minY + expected, start.getBoundingBox().minY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unfittableBurialClampsToTheWorldFloorInsteadOfAborting() {
|
||||
StructureStart start = desertStart();
|
||||
int minY = start.getBoundingBox().minY();
|
||||
int worldMinY = minY - 4;
|
||||
|
||||
int offset = NativeStructurePostProcessor.applyVerticalShift(
|
||||
start, 0, worldMinY, 320, true, false, null, (x, z) -> worldMinY);
|
||||
|
||||
assertEquals(-4, offset);
|
||||
assertEquals(worldMinY, start.getBoundingBox().minY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativeVacuumClearsEveryPieceEnvelopeBeforePlacement() {
|
||||
StructureStart start = desertStart();
|
||||
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
put(blocks, bounds.minX(), bounds.minY(), bounds.minZ(), Blocks.STONE.defaultBlockState());
|
||||
|
||||
NativeStructurePostProcessor.integrateTerrain(
|
||||
world(blocks), bounds, "minecraft:desert_pyramid", start,
|
||||
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.VACUUM), null);
|
||||
|
||||
assertEquals(Blocks.AIR.defaultBlockState(),
|
||||
state(blocks, bounds.minX(), bounds.minY(), bounds.minZ()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativeForceCarveHonorsConfiguredPadding() {
|
||||
StructureStart start = desertStart();
|
||||
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
|
||||
BoundingBox area = new BoundingBox(
|
||||
bounds.minX() - 1, bounds.minY(), bounds.minZ() - 1,
|
||||
bounds.maxX() + 1, bounds.maxY() + 1, bounds.maxZ() + 1);
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
put(blocks, bounds.maxX() + 1, bounds.minY(), bounds.maxZ(),
|
||||
Blocks.STONE.defaultBlockState());
|
||||
put(blocks, bounds.maxX(), bounds.maxY() + 1, bounds.maxZ(),
|
||||
Blocks.STONE.defaultBlockState());
|
||||
|
||||
NativeStructurePostProcessor.integrateTerrain(
|
||||
world(blocks), area, "minecraft:desert_pyramid", start,
|
||||
new IrisStructureTerrain()
|
||||
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
|
||||
.setHorizontalPadding(1)
|
||||
.setCeilingPadding(1), null);
|
||||
|
||||
assertEquals(Blocks.AIR.defaultBlockState(),
|
||||
state(blocks, bounds.maxX() + 1, bounds.minY(), bounds.maxZ()));
|
||||
assertEquals(Blocks.AIR.defaultBlockState(),
|
||||
state(blocks, bounds.maxX(), bounds.maxY() + 1, bounds.maxZ()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativeForceCarveUsesPieceUnionInsteadOfCombinedBounds() {
|
||||
Structure structure = new DesertPyramidStructure(
|
||||
new Structure.StructureSettings(HolderSet.empty()));
|
||||
DesertPyramidPiece first = new DesertPyramidPiece(RandomSource.create(7L), 0, 0);
|
||||
DesertPyramidPiece second = new DesertPyramidPiece(RandomSource.create(8L), 0, 0);
|
||||
second.move(64, 0, 0);
|
||||
StructureStart start = new StructureStart(
|
||||
structure, new ChunkPos(0, 0), 0,
|
||||
new PiecesContainer(List.of(first, second)));
|
||||
BoundingBox firstBounds = first.getBoundingBox();
|
||||
BoundingBox secondBounds = second.getBoundingBox();
|
||||
int gapX = (firstBounds.maxX() + secondBounds.minX()) / 2;
|
||||
int y = firstBounds.minY();
|
||||
int z = firstBounds.minZ();
|
||||
BoundingBox area = new BoundingBox(
|
||||
firstBounds.minX(), y, z,
|
||||
secondBounds.maxX(), y, z);
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
put(blocks, firstBounds.minX(), y, z, Blocks.STONE.defaultBlockState());
|
||||
put(blocks, gapX, y, z, Blocks.STONE.defaultBlockState());
|
||||
|
||||
NativeStructurePostProcessor.integrateTerrain(
|
||||
world(blocks), area, "minecraft:ancient_city", start,
|
||||
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.FORCE_CARVE), null);
|
||||
|
||||
assertEquals(Blocks.AIR.defaultBlockState(),
|
||||
state(blocks, firstBounds.minX(), y, z));
|
||||
assertEquals(Blocks.STONE.defaultBlockState(),
|
||||
state(blocks, gapX, y, z));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void templateBackedColumnsUseTheTemplateAirComplement() throws Exception {
|
||||
StructureTemplate template = template(List.of(
|
||||
new StructureTemplate.StructureBlockInfo(
|
||||
new BlockPos(0, 0, 0), Blocks.AIR.defaultBlockState(), null),
|
||||
new StructureTemplate.StructureBlockInfo(
|
||||
new BlockPos(0, 1, 0), Blocks.AIR.defaultBlockState(), null),
|
||||
new StructureTemplate.StructureBlockInfo(
|
||||
new BlockPos(0, 2, 0), Blocks.DEEPSLATE_BRICKS.defaultBlockState(), null),
|
||||
new StructureTemplate.StructureBlockInfo(
|
||||
new BlockPos(1, 0, 0), Blocks.STRUCTURE_VOID.defaultBlockState(), null),
|
||||
new StructureTemplate.StructureBlockInfo(
|
||||
new BlockPos(1, 1, 0), Blocks.DEEPSLATE.defaultBlockState(), null),
|
||||
new StructureTemplate.StructureBlockInfo(
|
||||
new BlockPos(1, 2, 0), Blocks.DEEPSLATE.defaultBlockState(), null)));
|
||||
Map<Long, int[]> columns = new HashMap<>();
|
||||
|
||||
assertTrue(NativeStructurePostProcessor.emitTemplateColumns(
|
||||
List.of(template), new BlockPos(0, 0, 0), Rotation.NONE,
|
||||
new BoundingBox(0, 0, 0, 1, 2, 0),
|
||||
(x, z, minY, maxY) -> columns.put((long) x << 32 | z & 0xffffffffL,
|
||||
new int[]{minY, maxY})));
|
||||
|
||||
assertEquals(2, columns.size());
|
||||
assertArrayEquals(new int[]{2, 2}, columns.get(0L));
|
||||
assertArrayEquals(new int[]{1, 2}, columns.get(1L << 32));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fullyVoidTemplateColumnsAreNotCarveSources() throws Exception {
|
||||
StructureTemplate template = template(List.of(
|
||||
new StructureTemplate.StructureBlockInfo(
|
||||
new BlockPos(0, 0, 0), Blocks.AIR.defaultBlockState(), null),
|
||||
new StructureTemplate.StructureBlockInfo(
|
||||
new BlockPos(0, 1, 0), Blocks.STRUCTURE_VOID.defaultBlockState(), null)));
|
||||
Map<Long, int[]> columns = new HashMap<>();
|
||||
|
||||
assertTrue(NativeStructurePostProcessor.emitTemplateColumns(
|
||||
List.of(template), new BlockPos(0, 0, 0), Rotation.NONE,
|
||||
new BoundingBox(0, 0, 0, 0, 1, 0),
|
||||
(x, z, minY, maxY) -> columns.put((long) x << 32 | z & 0xffffffffL,
|
||||
new int[]{minY, maxY})));
|
||||
|
||||
assertTrue(columns.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonTemplatePiecesFallBackToTheirBoundingBoxColumns() {
|
||||
StructureStart start = desertStart();
|
||||
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
|
||||
|
||||
StructureCarvingFootprint footprint = NativeStructurePostProcessor.carveFootprint(
|
||||
start, 4, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager);
|
||||
|
||||
assertEquals(bounds.minX() - 4, footprint.minX());
|
||||
assertEquals(bounds.maxX() + 4, footprint.maxX());
|
||||
assertEquals(bounds.minZ() - 4, footprint.minZ());
|
||||
assertEquals(bounds.maxZ() + 4, footprint.maxZ());
|
||||
assertEquals(0L, footprint.distanceSquaredAt(bounds.minX(), bounds.minZ()));
|
||||
assertEquals(32L, footprint.distanceSquaredAt(bounds.minX() - 4, bounds.minZ() - 4));
|
||||
assertEquals(bounds.minY(), footprint.sourceMinYAt(bounds.minX(), bounds.minZ()));
|
||||
assertEquals(bounds.maxY(), footprint.sourceMaxYAt(bounds.minX(), bounds.minZ()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void carveFootprintIsComputedOncePerStartAndPadding() {
|
||||
StructureStart start = desertStart();
|
||||
|
||||
StructureCarvingFootprint first = NativeStructurePostProcessor.carveFootprint(
|
||||
start, 6, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager);
|
||||
StructureCarvingFootprint repeated = NativeStructurePostProcessor.carveFootprint(
|
||||
start, 6, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager);
|
||||
StructureCarvingFootprint widened = NativeStructurePostProcessor.carveFootprint(
|
||||
start, 7, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager);
|
||||
StructureCarvingFootprint other = NativeStructurePostProcessor.carveFootprint(
|
||||
desertStart(), 6, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager);
|
||||
|
||||
assertSame(first, repeated);
|
||||
assertNotSame(first, widened);
|
||||
assertNotSame(first, other);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void organicCarveNeverCutsBelowTheColumnSupportingFloor() {
|
||||
StructureStart start = desertStart();
|
||||
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
|
||||
NativeStructurePostProcessor.OrganicCarve carve = organicCarve(start, 6);
|
||||
BoundingBox area = new BoundingBox(
|
||||
bounds.minX() - 6, bounds.minY() - 4, bounds.minZ() - 6,
|
||||
bounds.maxX() + 6, bounds.maxY() + 12, bounds.maxZ() + 6);
|
||||
Map<BlockPos, BlockState> blocks = fill(area);
|
||||
|
||||
NativeStructurePostProcessor.carveOrganicColumns(world(blocks), area, carve);
|
||||
|
||||
int centerX = bounds.minX() + bounds.getXSpan() / 2;
|
||||
int centerZ = bounds.minZ() + bounds.getZSpan() / 2;
|
||||
assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, centerX, bounds.minY(), centerZ));
|
||||
for (int x = area.minX(); x <= area.maxX(); x++) {
|
||||
for (int z = area.minZ(); z <= area.maxZ(); z++) {
|
||||
for (int y = area.minY(); y < bounds.minY(); y++) {
|
||||
assertEquals(Blocks.STONE.defaultBlockState(), state(blocks, x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lobedCarveStaysInsideTheUniformCarveAndRemovesLess() {
|
||||
StructureStart start = desertStart();
|
||||
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
|
||||
BoundingBox area = new BoundingBox(
|
||||
bounds.minX() - 10, bounds.minY(), bounds.minZ() - 10,
|
||||
bounds.maxX() + 10, bounds.maxY() + 12, bounds.maxZ() + 10);
|
||||
Map<BlockPos, BlockState> uniformBlocks = fill(area);
|
||||
Map<BlockPos, BlockState> lobedBlocks = fill(area);
|
||||
|
||||
NativeStructurePostProcessor.carveOrganicColumns(
|
||||
world(uniformBlocks), area, organicCarve(start, 10, 0D));
|
||||
NativeStructurePostProcessor.carveOrganicColumns(
|
||||
world(lobedBlocks), area, organicCarve(start, 10, 0.85D));
|
||||
|
||||
int uniform = 0;
|
||||
int lobed = 0;
|
||||
for (int x = area.minX(); x <= area.maxX(); x++) {
|
||||
for (int z = area.minZ(); z <= area.maxZ(); z++) {
|
||||
for (int y = area.minY(); y <= area.maxY(); y++) {
|
||||
boolean uniformAir = state(uniformBlocks, x, y, z).isAir();
|
||||
boolean lobedAir = state(lobedBlocks, x, y, z).isAir();
|
||||
assertTrue("lobe carved outside the uniform padding at "
|
||||
+ x + "," + y + "," + z, uniformAir || !lobedAir);
|
||||
uniform += uniformAir ? 1 : 0;
|
||||
lobed += lobedAir ? 1 : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(lobed > 0);
|
||||
assertTrue("uniform " + uniform + " lobed " + lobed, lobed < uniform);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lobedCarveDepthWandersAlongAStraightFootprintEdge() {
|
||||
int[] uniform = slabEdgeCarveDepths(0D);
|
||||
int[] lobed = slabEdgeCarveDepths(0.85D);
|
||||
|
||||
assertTrue("uniform depths wander " + span(uniform), span(uniform) <= 2);
|
||||
assertTrue("lobed depths wander " + span(lobed), span(lobed) >= 6);
|
||||
for (int depth : lobed) {
|
||||
assertTrue(depth >= 0 && depth <= SLAB_PADDING);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void organicCarveIsIdenticalAcrossNeighboringChunkContexts() {
|
||||
StructureStart start = desertStart();
|
||||
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
|
||||
BoundingBox wide = new BoundingBox(
|
||||
bounds.minX() - 6, bounds.minY(), bounds.minZ() - 6,
|
||||
bounds.maxX() + 6, bounds.maxY() + 12, bounds.maxZ() + 6);
|
||||
BoundingBox narrow = new BoundingBox(
|
||||
bounds.maxX() - 3, bounds.minY(), bounds.maxZ() - 3,
|
||||
bounds.maxX() + 6, bounds.maxY() + 12, bounds.maxZ() + 6);
|
||||
Map<BlockPos, BlockState> wideBlocks = fill(wide);
|
||||
Map<BlockPos, BlockState> narrowBlocks = fill(narrow);
|
||||
|
||||
// Each chunk context rebuilds its own noise channels from the shared start identity.
|
||||
NativeStructurePostProcessor.carveOrganicColumns(
|
||||
world(wideBlocks), wide, organicCarve(start, 6));
|
||||
NativeStructurePostProcessor.carveOrganicColumns(
|
||||
world(narrowBlocks), narrow, organicCarve(start, 6));
|
||||
|
||||
int carved = 0;
|
||||
for (int x = narrow.minX(); x <= narrow.maxX(); x++) {
|
||||
for (int z = narrow.minZ(); z <= narrow.maxZ(); z++) {
|
||||
for (int y = narrow.minY(); y <= narrow.maxY(); y++) {
|
||||
BlockState expected = state(wideBlocks, x, y, z);
|
||||
assertEquals(expected, state(narrowBlocks, x, y, z));
|
||||
if (expected.isAir()) {
|
||||
carved++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue(carved > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void erodedForceCarveShrinkwrapsWithoutUnderminingTheFloor() {
|
||||
StructureStart start = desertStart();
|
||||
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
|
||||
BoundingBox area = new BoundingBox(
|
||||
bounds.minX() - 6, bounds.minY() - 2, bounds.minZ() - 6,
|
||||
bounds.maxX() + 6, bounds.maxY() + 12, bounds.maxZ() + 6);
|
||||
Map<BlockPos, BlockState> blocks = fill(area);
|
||||
int centerX = bounds.minX() + bounds.getXSpan() / 2;
|
||||
int centerZ = bounds.minZ() + bounds.getZSpan() / 2;
|
||||
|
||||
NativeStructurePostProcessor.integrateTerrain(
|
||||
world(blocks), area, "minecraft:ancient_city", start,
|
||||
new IrisStructureTerrain()
|
||||
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
|
||||
.setShape(IrisStructureCarveShape.ERODED)
|
||||
.setHorizontalPadding(6)
|
||||
.setCeilingPadding(8)
|
||||
.setFloorPadding(0)
|
||||
.setErosionStrength(1D)
|
||||
.setErosionFrequency(0.05D), null);
|
||||
|
||||
assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, centerX, bounds.minY(), centerZ));
|
||||
assertEquals(Blocks.STONE.defaultBlockState(),
|
||||
state(blocks, centerX, bounds.minY() - 1, centerZ));
|
||||
assertEquals(Blocks.STONE.defaultBlockState(),
|
||||
state(blocks, area.minX(), area.maxY(), area.minZ()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sparseStiltGridIsDeterministicAndPreflightsGround() {
|
||||
assertTrue(NativeStructurePostProcessor.isStiltColumn(0, 0, 4));
|
||||
assertTrue(NativeStructurePostProcessor.isStiltColumn(-4, 8, 4));
|
||||
assertFalse(NativeStructurePostProcessor.isStiltColumn(1, 0, 4));
|
||||
assertTrue(NativeStructurePostProcessor.isStiltColumn(1, 1, 1));
|
||||
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
put(blocks, 0, 7, 0, Blocks.DEEPSLATE.defaultBlockState());
|
||||
put(blocks, 0, 8, 0, Blocks.SCULK_VEIN.defaultBlockState());
|
||||
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
|
||||
|
||||
assertEquals(7, NativeStructurePostProcessor.findStiltAnchorY(
|
||||
world(blocks), 0, 0, 10, 2, -64, -64, position));
|
||||
assertEquals(Integer.MIN_VALUE, NativeStructurePostProcessor.findStiltAnchorY(
|
||||
world(blocks), 0, 0, 10, 1, -64, -64, position));
|
||||
assertEquals(Integer.MIN_VALUE, NativeStructurePostProcessor.findStiltAnchorY(
|
||||
world(new HashMap<>()), 0, 0, 10, 64, -64, -64, position));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativeTerrainEnvelopePersistsHorizontalReferenceCoverage() {
|
||||
StructureStart generated = desertStart();
|
||||
BoundingBox content = generated.getBoundingBox();
|
||||
|
||||
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
|
||||
generated,
|
||||
generated.getStructure(),
|
||||
0,
|
||||
null,
|
||||
new IrisStructureTerrain()
|
||||
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
|
||||
.setHorizontalPadding(24));
|
||||
|
||||
assertEquals(content, NativeStructureReferenceEnvelope.contentBounds(wrapped));
|
||||
assertEquals(content.minX() - 24, wrapped.getBoundingBox().minX());
|
||||
assertEquals(content.minZ() - 24, wrapped.getBoundingBox().minZ());
|
||||
assertEquals(content.maxX() + 24, wrapped.getBoundingBox().maxX());
|
||||
assertEquals(content.maxZ() + 24, wrapped.getBoundingBox().maxZ());
|
||||
assertEquals(2, wrapped.getPieces().stream()
|
||||
.filter(NativeStructureReferenceEnvelope::isMarker)
|
||||
.count());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativeTerrainEnvelopeClipsToMinecraftReferenceCoverage() {
|
||||
StructureStart generated = desertStart();
|
||||
|
||||
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
|
||||
generated,
|
||||
generated.getStructure(),
|
||||
0,
|
||||
null,
|
||||
new IrisStructureTerrain()
|
||||
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
|
||||
.setHorizontalPadding(128));
|
||||
|
||||
assertEquals(-128, wrapped.getBoundingBox().minX());
|
||||
assertEquals(-128, wrapped.getBoundingBox().minZ());
|
||||
assertEquals(143, wrapped.getBoundingBox().maxX());
|
||||
assertEquals(143, wrapped.getBoundingBox().maxZ());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singlePoolTemplateFieldMatchesTheRuntimeContract() {
|
||||
Field field = NativeStructurePostProcessor.resolveSinglePoolTemplateField();
|
||||
@@ -302,6 +726,100 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
|
||||
return new NativeStructurePostProcessor.SurfaceAnchor(0, 4, 0, 4, meetY, strength);
|
||||
}
|
||||
|
||||
private static NativeStructurePostProcessor.OrganicCarve organicCarve(StructureStart start,
|
||||
int horizontalPadding) {
|
||||
return organicCarve(start, horizontalPadding, 0.85D);
|
||||
}
|
||||
|
||||
private static NativeStructurePostProcessor.OrganicCarve organicCarve(StructureStart start,
|
||||
int horizontalPadding,
|
||||
double lobeStrength) {
|
||||
IrisStructureTerrain terrain = new IrisStructureTerrain()
|
||||
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
|
||||
.setShape(IrisStructureCarveShape.ERODED)
|
||||
.setHorizontalPadding(horizontalPadding)
|
||||
.setCeilingPadding(8)
|
||||
.setFloorPadding(0)
|
||||
.setErosionStrength(1D)
|
||||
.setErosionFrequency(0.05D)
|
||||
.setLobeStrength(lobeStrength);
|
||||
return NativeStructurePostProcessor.organicCarve(
|
||||
NativeStructurePostProcessor.carveFootprint(start, horizontalPadding,
|
||||
NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager),
|
||||
terrain, IrisStructureCarveShape.ERODED, TEST_SEED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Outward carve depth for every column along the straight +X edge of a slab footprint, measured
|
||||
* inside the source vertical span so the boundary is decided purely by the horizontal threshold.
|
||||
* The slab spans several lobe wavelengths so a lobed boundary is distinguishable from a uniform one.
|
||||
*/
|
||||
private static int[] slabEdgeCarveDepths(double lobeStrength) {
|
||||
StructureCarvingFootprint footprint = StructureCarvingFootprint.fromColumns(sink -> {
|
||||
for (int x = 0; x < SLAB_WIDTH; x++) {
|
||||
for (int z = 0; z < SLAB_DEPTH; z++) {
|
||||
sink.column(x, z, SLAB_MIN_Y, SLAB_MIN_Y + 8);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}, SLAB_PADDING, 1_000_000);
|
||||
IrisStructureTerrain terrain = new IrisStructureTerrain()
|
||||
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
|
||||
.setShape(IrisStructureCarveShape.ERODED)
|
||||
.setHorizontalPadding(SLAB_PADDING)
|
||||
.setCeilingPadding(8)
|
||||
.setFloorPadding(0)
|
||||
.setErosionStrength(1D)
|
||||
.setErosionFrequency(0.05D)
|
||||
.setLobeStrength(lobeStrength);
|
||||
int transectY = SLAB_MIN_Y + 4;
|
||||
BoundingBox area = new BoundingBox(
|
||||
SLAB_WIDTH, transectY, 0,
|
||||
SLAB_WIDTH - 1 + SLAB_PADDING, transectY, SLAB_DEPTH - 1);
|
||||
Map<BlockPos, BlockState> blocks = fill(area);
|
||||
|
||||
NativeStructurePostProcessor.carveOrganicColumns(world(blocks), area,
|
||||
NativeStructurePostProcessor.organicCarve(
|
||||
footprint, terrain, IrisStructureCarveShape.ERODED, TEST_SEED));
|
||||
|
||||
int[] depths = new int[SLAB_DEPTH];
|
||||
for (int z = 0; z < SLAB_DEPTH; z++) {
|
||||
int depth = 0;
|
||||
while (depth < SLAB_PADDING
|
||||
&& state(blocks, SLAB_WIDTH + depth, transectY, z).isAir()) {
|
||||
depth++;
|
||||
}
|
||||
depths[z] = depth;
|
||||
}
|
||||
return depths;
|
||||
}
|
||||
|
||||
private static int span(int[] values) {
|
||||
int lowest = Integer.MAX_VALUE;
|
||||
int highest = Integer.MIN_VALUE;
|
||||
for (int value : values) {
|
||||
lowest = Math.min(lowest, value);
|
||||
highest = Math.max(highest, value);
|
||||
}
|
||||
return highest - lowest;
|
||||
}
|
||||
|
||||
private static StructureTemplateManager forbiddenTemplateManager() {
|
||||
throw new AssertionError("Bounding-box carve columns must not resolve templates");
|
||||
}
|
||||
|
||||
private static Map<BlockPos, BlockState> fill(BoundingBox area) {
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
for (int x = area.minX(); x <= area.maxX(); x++) {
|
||||
for (int z = area.minZ(); z <= area.maxZ(); z++) {
|
||||
for (int y = area.minY(); y <= area.maxY(); y++) {
|
||||
put(blocks, x, y, z, Blocks.STONE.defaultBlockState());
|
||||
}
|
||||
}
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private static StructureTemplate template(
|
||||
List<StructureTemplate.StructureBlockInfo> blocks) throws Exception {
|
||||
Constructor<StructureTemplate.Palette> constructor =
|
||||
@@ -313,6 +831,14 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
|
||||
return template;
|
||||
}
|
||||
|
||||
private static StructureStart desertStart() {
|
||||
Structure structure = new DesertPyramidStructure(
|
||||
new Structure.StructureSettings(HolderSet.empty()));
|
||||
DesertPyramidPiece piece = new DesertPyramidPiece(RandomSource.create(7L), 0, 0);
|
||||
return new StructureStart(
|
||||
structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece)));
|
||||
}
|
||||
|
||||
private static WorldGenLevel world(Map<BlockPos, BlockState> blocks) {
|
||||
InvocationHandler handler = (proxy, method, arguments) -> {
|
||||
String methodName = method.getName();
|
||||
@@ -326,6 +852,9 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
|
||||
put(blocks, position.getX(), position.getY(), position.getZ(), blockState);
|
||||
return true;
|
||||
}
|
||||
if (methodName.equals("getSeed")) {
|
||||
return TEST_SEED;
|
||||
}
|
||||
if (methodName.equals("hashCode")) {
|
||||
return System.identityHashCode(proxy);
|
||||
}
|
||||
|
||||
+3
-9
@@ -63,15 +63,9 @@ public class NativeStructurePostProcessorVegetationTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void undergroundStructuresFailWhenBurialWouldCrossWorldFloor() {
|
||||
public void undergroundBurialClampsToTheWorldFloorInsteadOfFailing() {
|
||||
BoundingBox bounds = new BoundingBox(0, 60, 0, 1, 80, 0);
|
||||
try {
|
||||
NativeStructurePostProcessor.resolveBuriedOffset(
|
||||
bounds, 0, 58, 320, (x, z) -> x == 1 ? 76 : 100);
|
||||
} catch (IllegalStateException e) {
|
||||
assertTrue(e.getMessage().contains("world minimum"));
|
||||
return;
|
||||
}
|
||||
throw new AssertionError("Expected underground burial to fail below the world floor");
|
||||
assertEquals(-2, NativeStructurePostProcessor.resolveBuriedOffset(
|
||||
bounds, 0, 58, 320, (x, z) -> x == 1 ? 76 : 100));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user