Production-readiness suite fixes

Surface hydrology policy split, native terrain height cache, watched-edit
hotload cancellation, world-save requestSave with tracked ownership flush,
background-task drain that follows saves admitted mid-drain, peaceful spawn
guard, cached-completion terminal state, IO drains off the owner thread.

Studio pregen at scale: detached historical runtimes keep the configured
noise cache instead of the Studio-wide cache; retired runtimes are no longer
pinned by the volumetric fragment filter's per-thread scratch; pregen mantle
cleanup and native region flushes run on per-run threads instead of the
server thread and the shared Iris IO pool (the flush on the IO pool starved
tectonic plate loads and wedged every Paper worker in Mantle.getChunks);
background-task cancellation claims tasks atomically.
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-09 16:24:05 -04:00
1 parent 4de1aea8b9
commit 62e61970f8
150 files changed
+7892 -1341

No files matched your search

@@ -1,6 +1,7 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.nativegen.NativeTransitionColumn;
import art.arcane.iris.nativegen.NativeTerrainHeightCache;
import art.arcane.iris.engine.history.TerrainBoundarySignature;
import java.util.Optional;
import art.arcane.iris.nativegen.NativeGenerationWriteGuard;
@@ -124,6 +125,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.OptionalInt;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
@@ -155,6 +157,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator implements LongPred
private final int runtimeSeaLevel;
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
private final ImportedFeatureStage importedFeatures;
private final NativeTerrainHeightCache terrainHeights = new NativeTerrainHeightCache();
private final AtomicReference<StudioStructureState> retainedStudioStructureState = new AtomicReference<>();
private volatile ReachableStructureCache reachableStructureCache;
private volatile StructureStepCache structureStepCache;
@@ -184,6 +187,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator implements LongPred
}
private void evictRuntimeCaches(int runtimeId) {
terrainHeights.evictRuntime(runtimeId);
importedFeatures.evictRuntime(runtimeId);
mergedSpawnTables.keySet().removeIf(key -> key.runtimeId() == runtimeId);
ReachableStructureCache reachable = reachableStructureCache;
@@ -765,9 +769,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator implements LongPred
@Override
public CompletableFuture<ChunkAccess> fillFromNoise(Blender blender, RandomState randomstate, StructureManager structuremanager, ChunkAccess ichunkaccess) {
ChunkPos chunkPos = ichunkaccess.getPos();
BukkitChunkGenerator.GenerationStagePermit stage = requireNoiseGenerationStage(
chunkPos,
"bukkit_nms_chunk_pipeline");
BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_chunk_pipeline");
GenerationHistoryRuntimeRouter.RuntimeRoute route;
try {
route = openHistoryRoute(chunkPos.x(), chunkPos.z(), "bukkit_nms_chunk_pipeline");
@@ -1124,10 +1126,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator implements LongPred
}
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
Predicate<BlockPos> protectedPosition = nativeStructureProtection(staticObjects);
WorldGenLevel boundedWorld = staticObjects.isEmpty()
&& engine.getDimensionStackContext() == null
? world
: NativeStructureWorldgenAccess.create(
WorldGenLevel boundedWorld = NativeStructureWorldgenAccess.create(
world, chunkPos, hostWorldgenSurfaceHeight(), hostWorldgenFloorHeight(),
engine.getDimensionStackContext() != null,
protectedPosition);
@@ -1139,11 +1138,12 @@ public class IrisChunkGenerator extends CustomChunkGenerator implements LongPred
"vegetation cleanup", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
NativeStructureSurfaceFitter.VacuumFoundationPlan vacuumFoundationPlan;
NativeStructureSurfaceFitter.SurfaceTerrainPlan surfaceTerrainPlan;
try {
vacuumFoundationPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
surfaceTerrainPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
boundedWorld, area, terrainTargets,
(x, z) -> Engine.hostHeight(engine, x, z, true) + engine.getMinHeight());
surfaceTerrainPlan.primeHeightmaps(chunk);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"terrain integration", nativeStructureBatchContext(placementGroups),
@@ -1171,7 +1171,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator implements LongPred
}
try {
NativeStructureSurfaceFitter.repairVacuumFoundations(
boundedWorld, area, vacuumFoundationPlan);
boundedWorld, area, surfaceTerrainPlan);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"foundation repair", nativeStructureBatchContext(placementGroups),
@@ -1422,21 +1422,31 @@ public class IrisChunkGenerator extends CustomChunkGenerator implements LongPred
i, j, "bukkit_nms_base_height");
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_height");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
Optional<TerrainBoundarySignature> resolved = engine.getComplex().resolvedTerrainColumn(i, j);
NativeTerrainHeightCache.Query query = new NativeTerrainHeightCache.Query(
engine.getCacheID(), i, j, heightmap_type,
levelheightaccessor.getMinY(), levelheightaccessor.getHeight());
OptionalInt resolved = terrainHeights.resolvedHeight(query,
() -> resolvedBaseHeight(i, j, heightmap_type, levelheightaccessor));
if (resolved.isPresent()) {
return NativeTransitionColumn.height(resolved.get(), heightmap_type, levelheightaccessor);
return resolved.getAsInt();
}
boolean ignoreFluid = !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState());
int height = engine.getDimensionStackContext() == null
? engine.getHeight(i, j, ignoreFluid)
: Engine.hostHeight(engine, i, j, ignoreFluid);
return levelheightaccessor.getMinY()
+ height + 1;
return levelheightaccessor.getMinY() + height + 1;
} catch (GenerationSessionException e) {
throw new IllegalStateException("Iris base height query could not acquire its engine runtime.", e);
}
}
private OptionalInt resolvedBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor heightAccessor) {
Optional<TerrainBoundarySignature> resolved = engine.getComplex().resolvedTerrainColumn(x, z);
return resolved.isPresent()
? OptionalInt.of(NativeTransitionColumn.height(resolved.get(), type, heightAccessor))
: OptionalInt.empty();
}
@Override
public NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) {
@@ -1564,19 +1574,6 @@ public class IrisChunkGenerator extends CustomChunkGenerator implements LongPred
: platformGenerator.acquireGenerationStage(operation);
}
private BukkitChunkGenerator.GenerationStagePermit requireNoiseGenerationStage(
ChunkPos chunkPos,
String operation
) {
return platformGenerator == null
? BukkitChunkGenerator.GenerationStagePermit.noop()
: platformGenerator.acquireNoiseGenerationStage(
engine,
chunkPos.x(),
chunkPos.z(),
operation);
}
@Override
public Optional<Identifier> getTypeNameForDataFixer() {
return delegate.getTypeNameForDataFixer();
@@ -120,6 +120,7 @@ import net.minecraft.world.level.levelgen.flat.FlatLayerInfo;
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.Difficulty;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
@@ -1155,12 +1156,8 @@ public class NMSBinding implements INMSBinding {
@Override
public void flushChunkIO(World world) {
try {
ServerLevel level = ((CraftWorld) world).getHandle();
MoonriseRegionFileIO.flush(level);
} catch (Throwable e) {
IrisLogging.reportError(e);
}
ServerLevel level = ((CraftWorld) world).getHandle();
MoonriseRegionFileIO.flush(level);
}
@Override
@@ -1443,7 +1440,14 @@ public class NMSBinding implements INMSBinding {
if (location == null || location.getWorld() == null || type == null || type.getEntityClass() == null) {
return null;
}
return ((CraftWorld) location.getWorld()).spawn(location, type.getEntityClass(), null, reason);
CraftWorld world = (CraftWorld) location.getWorld();
if (world.getDifficulty() == Difficulty.PEACEFUL) {
EntityType<?> nativeType = BuiltInRegistries.ENTITY_TYPE.getValue(Identifier.parse(type.getKey().toString()));
if (nativeType == null || !nativeType.isAllowedInPeaceful()) {
return null;
}
}
return world.spawn(location, type.getEntityClass(), null, reason);
}
@Override
@@ -3,6 +3,8 @@ package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.engine.history.NativeTerrainReceipt;
import art.arcane.iris.engine.history.SavedTerrainChunk;
import art.arcane.iris.util.common.scheduling.J;
import ca.spottedleaf.concurrentutil.completable.Completable;
import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
import ca.spottedleaf.concurrentutil.util.Priority;
import ca.spottedleaf.moonrise.common.PlatformHooks;
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
@@ -26,31 +28,34 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.LockSupport;
final class NmsSavedTerrainCapture {
private static final long WRITE_TIMEOUT_NANOS = TimeUnit.SECONDS.toNanos(30L);
private static final int SNAPSHOT_BATCH_SIZE = 64;
private NmsSavedTerrainCapture() {
}
static CompletableFuture<SavedTerrainChunk> capture(World world, int chunkX, int chunkZ, int minimumY, int height) {
ServerLevel level = ((CraftWorld) world).getHandle();
CompletableFuture<CompoundTag> snapshot = new CompletableFuture<>();
CompletableFuture<CapturedChunk> snapshot = new CompletableFuture<>();
J.runRegionFuture(world, chunkX, chunkZ, () -> {
try {
NewChunkHolder holder = level.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ);
ChunkAccess chunk = holder == null ? null : holder.getCurrentChunk();
if (chunk == null) {
snapshot.complete(null);
snapshot.complete(new CapturedChunk(new ChunkPos(chunkX, chunkZ), CompletableFuture.completedFuture(null)));
return;
}
String status = BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getPersistedStatus()).toString();
if (!SavedTerrainChunk.hasTerrain(status)) {
throw new IOException("Native chunk " + chunkX + "," + chunkZ + " has no completed terrain at " + status);
}
snapshot.complete(saveSnapshot(level, chunk));
snapshot.complete(scheduleSnapshot(level, chunk));
} catch (Throwable failure) {
snapshot.completeExceptionally(failure);
}
@@ -59,10 +64,12 @@ final class NmsSavedTerrainCapture {
snapshot.completeExceptionally(failure);
}
});
return snapshot.thenApplyAsync(data -> {
return snapshot.thenApplyAsync(captured -> {
try {
long deadline = System.nanoTime() + WRITE_TIMEOUT_NANOS;
CompoundTag data = awaitSnapshot(level, captured, deadline);
Checkpoint checkpoint = checkpoint(chunkX, chunkZ, data);
awaitWrite(level, checkpoint.chunk(), System.nanoTime() + WRITE_TIMEOUT_NANOS);
awaitWrite(level, checkpoint.chunk(), deadline);
if (data == null) {
return SavedTerrainChunk.read(world.getWorldFolder().toPath(), chunkX, chunkZ, minimumY, height);
}
@@ -81,37 +88,73 @@ final class NmsSavedTerrainCapture {
static CompletableFuture<Void> flush(World world) {
ServerLevel level = ((CraftWorld) world).getHandle();
List<NewChunkHolder> holders = level.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolders();
List<CompletableFuture<Checkpoint>> saves = new ArrayList<>(holders.size());
for (NewChunkHolder holder : holders) {
CompletableFuture<Checkpoint> saved = new CompletableFuture<>();
saves.add(saved);
J.runRegionFuture(world, holder.chunkX, holder.chunkZ, () -> {
try {
ChunkAccess chunk = holder.getCurrentChunk();
saved.complete(checkpoint(holder.chunkX, holder.chunkZ,
chunk != null && chunk.isUnsaved() ? saveSnapshot(level, chunk) : null));
} catch (Throwable failure) {
saved.completeExceptionally(failure);
}
}).whenComplete((ignored, failure) -> {
if (failure != null) {
saved.completeExceptionally(failure);
}
});
}
return CompletableFuture.allOf(saves.toArray(CompletableFuture[]::new)).thenRunAsync(() -> {
return CompletableFuture.runAsync(() -> {
try {
List<Checkpoint> checkpoints = new ArrayList<>(saves.size());
for (CompletableFuture<Checkpoint> future : saves) {
checkpoints.add(future.join());
}
finishCheckpoint(level, world.getWorldFolder().toPath(), checkpoints);
flushSnapshots(world, level, holders);
} catch (IOException failure) {
throw new CompletionException(failure);
}
});
}
static void flushSnapshots(World world, ServerLevel level, List<NewChunkHolder> holders) throws IOException {
long deadline = System.nanoTime() + WRITE_TIMEOUT_NANOS;
List<Checkpoint> checkpoints = new ArrayList<>(holders.size());
for (int offset = 0; offset < holders.size(); offset += SNAPSHOT_BATCH_SIZE) {
int end = Math.min(holders.size(), offset + SNAPSHOT_BATCH_SIZE);
List<CompletableFuture<CapturedChunk>> snapshots = requestSnapshots(world, level, holders.subList(offset, end));
awaitSnapshots(snapshots, deadline);
for (CompletableFuture<CapturedChunk> future : snapshots) {
CapturedChunk captured = future.join();
CompoundTag data = awaitSnapshot(level, captured, deadline);
checkpoints.add(checkpoint(captured.chunk().x(), captured.chunk().z(), data));
}
for (int index = offset; index < end; index++) {
awaitWrite(level, checkpoints.get(index).chunk(), deadline);
}
}
finishCheckpoint(level, world.getWorldFolder().toPath(), checkpoints);
}
private static List<CompletableFuture<CapturedChunk>> requestSnapshots(
World world, ServerLevel level, List<NewChunkHolder> holders) {
List<CompletableFuture<CapturedChunk>> snapshots = new ArrayList<>(holders.size());
for (NewChunkHolder original : holders) {
ChunkPos position = new ChunkPos(original.chunkX, original.chunkZ);
CompletableFuture<CapturedChunk> snapshot = new CompletableFuture<>();
snapshots.add(snapshot);
J.runRegionFuture(world, position.x(), position.z(), () -> {
try {
NewChunkHolder holder = level.moonrise$getChunkTaskScheduler().chunkHolderManager
.getChunkHolder(position.x(), position.z());
ChunkAccess chunk = holder == null ? null : holder.getCurrentChunk();
snapshot.complete(chunk != null && chunk.isUnsaved() ? scheduleSnapshot(level, chunk)
: new CapturedChunk(position, CompletableFuture.completedFuture(null)));
} catch (Throwable failure) {
snapshot.completeExceptionally(failure);
}
}).whenComplete((ignored, failure) -> {
if (failure != null) {
snapshot.completeExceptionally(failure);
}
});
}
return snapshots;
}
private static void awaitSnapshots(List<CompletableFuture<CapturedChunk>> snapshots, long deadlineNanos)
throws IOException {
try {
CompletableFuture.allOf(snapshots.toArray(CompletableFuture[]::new))
.get(Math.max(0L, deadlineNanos - System.nanoTime()), TimeUnit.NANOSECONDS);
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new IOException("Interrupted while copying terrain checkpoint chunks.", failure);
} catch (ExecutionException | TimeoutException failure) {
throw new IOException("Unable to copy terrain checkpoint chunks.", failure);
}
}
static void finishCheckpoint(ServerLevel level, Path worldFolder, List<Checkpoint> checkpoints) throws IOException {
long deadline = System.nanoTime() + WRITE_TIMEOUT_NANOS;
for (Checkpoint checkpoint : checkpoints) {
@@ -147,31 +190,72 @@ final class NmsSavedTerrainCapture {
static void awaitWrite(ServerLevel level, ChunkPos chunk, long deadlineNanos) throws IOException {
ChunkTaskScheduler scheduler = level.moonrise$getChunkTaskScheduler();
while (true) {
if (Thread.currentThread().isInterrupted()) {
throw new IOException("Interrupted while saving terrain checkpoint chunk " + chunk);
}
if (System.nanoTime() - deadlineNanos >= 0L) {
throw new IOException("Timed out saving terrain checkpoint chunk " + chunk);
}
checkWriteWait(chunk, deadlineNanos);
if (MoonriseRegionFileIO.getPriority(level, chunk.x(), chunk.z(),
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA) == Priority.COMPLETING) {
return;
}
boolean helped = scheduler.saveExecutor.executeTask();
helped |= scheduler.compressionExecutor.executeTask();
if (!helped) {
LockSupport.parkNanos(1_000_000L);
}
helpSaveTasks(scheduler);
}
}
private static CompoundTag saveSnapshot(ServerLevel level, ChunkAccess chunk) {
static CapturedChunk scheduleSnapshot(ServerLevel level, ChunkAccess chunk) {
SerializableChunkData snapshot = SerializableChunkData.copyOf(level, chunk);
PlatformHooks.get().chunkSyncSave(level, chunk, snapshot);
CompoundTag data = snapshot.write();
MoonriseRegionFileIO.scheduleSave(level, chunk.getPos().x(), chunk.getPos().z(), data,
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA);
return data;
Completable<CompoundTag> nativeData = new Completable<>();
CompletableFuture<CompoundTag> data = new CompletableFuture<>();
PrioritisedExecutor.PrioritisedTask task = level.moonrise$getChunkTaskScheduler().saveExecutor
.createTask(() -> serializeSnapshot(snapshot, nativeData, data));
task.queue();
ChunkPos position = chunk.getPos();
MoonriseRegionFileIO.scheduleSave(level, position.x(), position.z(), nativeData, task,
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, Priority.NORMAL);
return new CapturedChunk(position, data);
}
static CompoundTag awaitSnapshot(ServerLevel level, CapturedChunk captured, long deadlineNanos) throws IOException {
ChunkTaskScheduler scheduler = level.moonrise$getChunkTaskScheduler();
while (!captured.data().isDone()) {
checkWriteWait(captured.chunk(), deadlineNanos);
helpSaveTasks(scheduler);
}
try {
return captured.data().join();
} catch (CompletionException failure) {
throw new IOException("Unable to serialize terrain checkpoint chunk " + captured.chunk(), failure.getCause());
}
}
private static void serializeSnapshot(SerializableChunkData snapshot, Completable<CompoundTag> nativeData,
CompletableFuture<CompoundTag> data) {
try {
CompoundTag serialized = snapshot.write();
nativeData.complete(serialized);
data.complete(serialized);
} catch (Throwable failure) {
nativeData.completeExceptionally(failure);
data.completeExceptionally(failure);
}
}
private static void checkWriteWait(ChunkPos chunk, long deadlineNanos) throws IOException {
if (Thread.currentThread().isInterrupted()) {
throw new IOException("Interrupted while saving terrain checkpoint chunk " + chunk);
}
if (System.nanoTime() - deadlineNanos >= 0L) {
throw new IOException("Timed out saving terrain checkpoint chunk " + chunk);
}
}
private static void helpSaveTasks(ChunkTaskScheduler scheduler) {
boolean helped = scheduler.saveExecutor.executeTask();
helped |= scheduler.compressionExecutor.executeTask();
if (!helped) {
LockSupport.parkNanos(1_000_000L);
}
}
record CapturedChunk(ChunkPos chunk, CompletableFuture<CompoundTag> data) {
}
record Checkpoint(ChunkPos chunk, Verification verification) {
@@ -1,9 +1,14 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.engine.object.IrisStructureTerrain;
import art.arcane.iris.engine.object.IrisStructureTerrainMode;
import art.arcane.iris.nativegen.NativeStructurePostProcessor;
import art.arcane.iris.nativegen.NativeStructureTerrainIntegrator;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.ChunkPos;
@@ -12,10 +17,25 @@ import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.block.Block;
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.chunk.status.ChunkStatus;
import net.minecraft.world.level.entity.EntityTypeTest;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
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.StructurePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure;
import net.minecraft.world.level.levelgen.structure.templatesystem.GravityProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.material.Fluid;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.phys.AABB;
@@ -30,11 +50,13 @@ import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class NativeStructureWorldgenAccessTest {
@@ -49,6 +71,69 @@ public class NativeStructureWorldgenAccessTest {
Bootstrap.bootStrap();
}
@Test
public void ordinaryWorldFlattenPreparationHandlesReferencedVillagePiecesBeyondEightChunks() {
int farX = (GENERATION_CENTER_X - 9) << 4;
int farZ = (GENERATION_CENTER_Z + 4) << 4;
BoundingBox bounds = new BoundingBox(farX, 64, farZ, farX + 4, 90, farZ + 4);
PoolElementStructurePiece piece = new PoolElementStructurePiece(
null, StructurePoolElement.single("minecraft:empty").apply(StructureTemplatePool.Projection.RIGID),
new BlockPos(farX, 64, farZ), 1, Rotation.NONE, bounds, LiquidSettings.APPLY_WATERLOGGING);
Structure structure = new DesertPyramidStructure(new Structure.StructureSettings(
HolderSet.empty(), Map.of(), GenerationStep.Decoration.SURFACE_STRUCTURES, TerrainAdjustment.BEARD_BOX));
StructureStart start = new StructureStart(structure, generationCenter(), 0, new PiecesContainer(List.of(piece)));
List<NativeStructureTerrainIntegrator.TerrainTarget> targets = List.of(
new NativeStructureTerrainIntegrator.TerrainTarget("minecraft:village_plains", start,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.FLATTEN)));
BoundingBox area = new BoundingBox(generationCenter().getMinBlockX(), -64, generationCenter().getMinBlockZ(),
generationCenter().getMaxBlockX(), 319, generationCenter().getMaxBlockZ());
RecordingDelegate unbounded = new RecordingDelegate();
unbounded.rejectDistantTerrainReads = true;
assertThrows(IllegalStateException.class, () -> NativeStructurePostProcessor.prepareTerrain(
unbounded.world(), area, targets, (palette, rng, x, y, z) -> Blocks.STONE.defaultBlockState()));
RecordingDelegate recording = new RecordingDelegate();
recording.rejectDistantTerrainReads = true;
NativeStructureWorldgenAccess access = access(recording);
NativeStructurePostProcessor.prepareTerrain(
access, area, targets, (palette, rng, x, y, z) -> Blocks.STONE.defaultBlockState());
assertEquals(0, recording.heightReads);
assertEquals(0, recording.chunkReads);
assertEquals(0, recording.terrainReads);
assertEquals(0, recording.mutations);
assertEquals(91, access.getHeight(Heightmap.Types.WORLD_SURFACE_WG,
generationCenter().getMinBlockX(), generationCenter().getMinBlockZ()));
assertEquals(1, recording.heightReads);
}
@Test
public void projectedTemplatePrefiltersUseRemoteTerrainAndRetainLocalHeightmaps() {
RecordingDelegate recording = new RecordingDelegate();
recording.rejectDistantTerrainReads = true;
NativeStructureWorldgenAccess access = access(recording);
GravityProcessor gravity = new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, -1);
BlockPos remote = new BlockPos((GENERATION_CENTER_X - 9) << 4, 64, (GENERATION_CENTER_Z + 4) << 4);
StructureTemplate.StructureBlockInfo raw = new StructureTemplate.StructureBlockInfo(
new BlockPos(0, 4, 0), Blocks.DIRT_PATH.defaultBlockState(), null);
StructurePlaceSettings settings = new StructurePlaceSettings().addProcessor(gravity);
StructureTemplate.StructureBlockInfo projected = StructureTemplate.processBlockInfos(
access, remote, BlockPos.ZERO, settings, List.of(raw)).getFirst();
assertEquals(remote.atY(SURFACE_FIRST_FREE_Y + 3), projected.pos());
assertEquals(0, recording.heightReads);
BlockPos local = generationCenter().getMiddleBlockPosition(64);
StructureTemplate.StructureBlockInfo localBlock = new StructureTemplate.StructureBlockInfo(
BlockPos.ZERO, Blocks.DIRT_PATH.defaultBlockState(), null);
StructureTemplate.StructureBlockInfo localProjection = StructureTemplate.processBlockInfos(
access, local, BlockPos.ZERO, settings, List.of(localBlock)).getFirst();
assertEquals(local.atY(90), localProjection.pos());
assertEquals(1, recording.heightReads);
assertEquals(0, recording.chunkReads);
}
@Test
public void distanceTwoTerrainReadsNeverReachTheDelegate() {
RecordingDelegate recording = new RecordingDelegate();
@@ -253,6 +338,7 @@ public class NativeStructureWorldgenAccessTest {
private int statusChunkReads;
private int mutations;
private int events;
private boolean rejectDistantTerrainReads;
private RecordingDelegate() {
this.biome = Holder.direct((Biome) null);
@@ -293,6 +379,11 @@ public class NativeStructureWorldgenAccessTest {
return 384;
}
if (name.equals("getHeight")) {
if (rejectDistantTerrainReads
&& (Math.abs(((int) arguments[1] >> 4) - GENERATION_CENTER_X) > 1
|| Math.abs(((int) arguments[2] >> 4) - GENERATION_CENTER_Z) > 1)) {
throw new IllegalStateException("Requested chunk unavailable during world generation");
}
heightReads++;
return 91;
}
@@ -0,0 +1,100 @@
package art.arcane.iris.core.nms.v26_2_R1;
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ServerLevel;
import org.bukkit.craftbukkit.CraftWorld;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class NmsChunkIOFlushTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void nativeDrainCanWaitOffOwnerUntilChunkPoiAndEntityIoFinish() throws Exception {
Fixture fixture = new Fixture();
AtomicBoolean chunkPending = new AtomicBoolean(true);
AtomicBoolean poiPending = new AtomicBoolean(true);
AtomicBoolean entityPending = new AtomicBoolean(true);
CountDownLatch chunkDrain = new CountDownLatch(1);
CountDownLatch poiDrain = new CountDownLatch(1);
CountDownLatch entityDrain = new CountDownLatch(1);
when(fixture.chunks.hasTasks()).thenAnswer(invocation -> {
chunkDrain.countDown();
return chunkPending.get();
});
when(fixture.pois.hasTasks()).thenAnswer(invocation -> {
poiDrain.countDown();
return poiPending.get();
});
when(fixture.entities.hasTasks()).thenAnswer(invocation -> {
entityDrain.countDown();
return entityPending.get();
});
ExecutorService io = Executors.newSingleThreadExecutor();
try {
Future<?> drain = io.submit(() -> fixture.binding.flushChunkIO(fixture.world));
assertTrue(chunkDrain.await(5L, TimeUnit.SECONDS));
assertFalse(drain.isDone());
chunkPending.set(false);
assertTrue(poiDrain.await(5L, TimeUnit.SECONDS));
assertFalse(drain.isDone());
poiPending.set(false);
assertTrue(entityDrain.await(5L, TimeUnit.SECONDS));
assertFalse(drain.isDone());
entityPending.set(false);
drain.get(5L, TimeUnit.SECONDS);
} finally {
chunkPending.set(false);
poiPending.set(false);
entityPending.set(false);
io.shutdownNow();
assertTrue(io.awaitTermination(5L, TimeUnit.SECONDS));
}
}
@Test
public void nativeDrainFailurePropagatesToItsCompletionReceipt() {
Fixture fixture = new Fixture();
IllegalStateException failure = new IllegalStateException("native IO failed");
when(fixture.chunks.hasTasks()).thenThrow(failure);
assertSame(failure, assertThrows(IllegalStateException.class,
() -> fixture.binding.flushChunkIO(fixture.world)));
}
private static final class Fixture {
private final NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
private final CraftWorld world = mock(CraftWorld.class);
private final ServerLevel level = mock(ServerLevel.class);
private final RegionDataController chunks = mock(RegionDataController.class);
private final RegionDataController pois = mock(RegionDataController.class);
private final RegionDataController entities = mock(RegionDataController.class);
private Fixture() {
when(world.getHandle()).thenReturn(level);
when(level.moonrise$getChunkDataController()).thenReturn(chunks);
when(level.moonrise$getPoiChunkDataController()).thenReturn(pois);
when(level.moonrise$getEntityChunkDataController()).thenReturn(entities);
}
}
}
@@ -0,0 +1,103 @@
package art.arcane.iris.core.nms.v26_2_R1;
import net.minecraft.SharedConstants;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.Bootstrap;
import org.bukkit.Difficulty;
import org.bukkit.Location;
import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class NmsEntitySpawnDifficultyTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void peacefulRejectsEveryNativeForbiddenTypeBeforeBukkitCreatesIt() {
CraftWorld world = mock(CraftWorld.class);
when(world.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
Location location = new Location(world, 0.5, 80, 0.5);
int rejected = 0;
for (EntityType type : EntityType.values()) {
if (type.getEntityClass() == null) {
continue;
}
if (BuiltInRegistries.ENTITY_TYPE.getValue(Identifier.parse(type.getKey().toString())).isAllowedInPeaceful()) {
continue;
}
assertNull(type.name(), binding.spawnEntity(location, type, CreatureSpawnEvent.SpawnReason.NATURAL));
rejected++;
}
assertTrue(rejected > 20);
verify(world, never()).spawn(any(Location.class), any(Class.class), isNull(), any(CreatureSpawnEvent.SpawnReason.class));
}
@Test
public void peacefulPreservesPassiveAndNativeMonsterCategoryExceptions() {
CraftWorld world = mock(CraftWorld.class);
when(world.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
Location location = new Location(world, 0.5, 80, 0.5);
Entity created = mock(Entity.class);
doReturn(created).when(world).spawn(eq(location), any(Class.class), isNull(), eq(CreatureSpawnEvent.SpawnReason.NATURAL));
for (EntityType type : List.of(EntityType.COW, EntityType.VILLAGER, EntityType.WOLF, EntityType.PIGLIN, EntityType.SHULKER)) {
assertSame(type.name(), created, binding.spawnEntity(location, type, CreatureSpawnEvent.SpawnReason.NATURAL));
}
}
@Test
public void nonPeacefulDifficultiesPreserveHostileSpawns() {
CraftWorld world = mock(CraftWorld.class);
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
Location location = new Location(world, 0.5, 80, 0.5);
Entity created = mock(Entity.class);
doReturn(created).when(world).spawn(eq(location), any(Class.class), isNull(), eq(CreatureSpawnEvent.SpawnReason.NATURAL));
for (Difficulty difficulty : List.of(Difficulty.EASY, Difficulty.NORMAL, Difficulty.HARD)) {
when(world.getDifficulty()).thenReturn(difficulty);
assertSame(created, binding.spawnEntity(location, EntityType.ZOMBIE, CreatureSpawnEvent.SpawnReason.NATURAL));
}
}
@Test
public void unrelatedBukkitSpawnFailuresStillPropagate() {
CraftWorld world = mock(CraftWorld.class);
when(world.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
Location location = new Location(world, 0.5, 80, 0.5);
IllegalStateException failure = new IllegalStateException("Spawn rejected by platform");
doThrow(failure).when(world).spawn(eq(location), any(Class.class), isNull(), eq(CreatureSpawnEvent.SpawnReason.NATURAL));
assertSame(failure, assertThrows(IllegalStateException.class,
() -> binding.spawnEntity(location, EntityType.COW, CreatureSpawnEvent.SpawnReason.NATURAL)));
}
}
@@ -0,0 +1,223 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.util.common.scheduling.J;
import ca.spottedleaf.concurrentutil.completable.Completable;
import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
import ca.spottedleaf.concurrentutil.executor.thread.BalancedPrioritisedThreadPool;
import ca.spottedleaf.concurrentutil.util.Priority;
import ca.spottedleaf.moonrise.common.PlatformHooks;
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
import net.minecraft.SharedConstants;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
import org.bukkit.World;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
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.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class NmsSavedTerrainSnapshotTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void ownerReservesSaveOrderBeforeOffOwnerSerializationAndLaterAutosave() throws Exception {
try (SaveFixture fixture = new SaveFixture();
ExecutorService checkpoint = Executors.newSingleThreadExecutor();
MockedStatic<SerializableChunkData> snapshots = mockStatic(SerializableChunkData.class);
MockedStatic<PlatformHooks> hooks = mockStatic(PlatformHooks.class);
MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class)) {
Thread owner = Thread.currentThread();
ChunkAccess chunk = mock(ChunkAccess.class);
SerializableChunkData snapshot = mock(SerializableChunkData.class);
PlatformHooks platform = mock(PlatformHooks.class);
CompoundTag capturedData = new CompoundTag();
CompoundTag autosave = new CompoundTag();
List<String> submissions = new ArrayList<>();
when(chunk.getPos()).thenReturn(new ChunkPos(4, -9));
snapshots.when(() -> SerializableChunkData.copyOf(fixture.level, chunk)).thenAnswer(invocation -> {
assertSame(owner, Thread.currentThread());
return snapshot;
});
hooks.when(PlatformHooks::get).thenReturn(platform);
doAnswer(invocation -> {
assertSame(owner, Thread.currentThread());
return null;
}).when(platform).chunkSyncSave(fixture.level, chunk, snapshot);
when(snapshot.write()).thenAnswer(invocation -> {
assertNotSame(owner, Thread.currentThread());
assertEquals(List.of("checkpoint", "autosave"), submissions);
return capturedData;
});
io.when(() -> MoonriseRegionFileIO.scheduleSave(eq(fixture.level), eq(4), eq(-9),
any(Completable.class), any(PrioritisedExecutor.PrioritisedTask.class),
eq(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), eq(Priority.NORMAL)))
.thenAnswer(invocation -> {
assertSame(owner, Thread.currentThread());
submissions.add("checkpoint");
return null;
});
io.when(() -> MoonriseRegionFileIO.scheduleSave(fixture.level, 4, -9, autosave,
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA)).thenAnswer(invocation -> {
submissions.add("autosave");
return null;
});
NmsSavedTerrainCapture.CapturedChunk captured = NmsSavedTerrainCapture.scheduleSnapshot(fixture.level, chunk);
assertEquals(List.of("checkpoint"), submissions);
assertFalse(captured.data().isDone());
verify(snapshot, never()).write();
verify(platform).chunkSyncSave(fixture.level, chunk, snapshot);
MoonriseRegionFileIO.scheduleSave(fixture.level, 4, -9, autosave,
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA);
Future<CompoundTag> result = checkpoint.submit(() -> NmsSavedTerrainCapture.awaitSnapshot(
fixture.level, captured, System.nanoTime() + TimeUnit.SECONDS.toNanos(3)));
assertSame(capturedData, result.get(5, TimeUnit.SECONDS));
verify(snapshot).write();
}
}
@Test
public void failedOffOwnerSerializationCannotSucceedAsACheckpoint() throws Exception {
try (SaveFixture fixture = new SaveFixture();
ExecutorService checkpoint = Executors.newSingleThreadExecutor();
MockedStatic<SerializableChunkData> snapshots = mockStatic(SerializableChunkData.class);
MockedStatic<PlatformHooks> hooks = mockStatic(PlatformHooks.class);
MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class)) {
ChunkAccess chunk = mock(ChunkAccess.class);
SerializableChunkData snapshot = mock(SerializableChunkData.class);
IllegalStateException failure = new IllegalStateException("serialization failed");
when(chunk.getPos()).thenReturn(new ChunkPos(4, -9));
snapshots.when(() -> SerializableChunkData.copyOf(fixture.level, chunk)).thenReturn(snapshot);
hooks.when(PlatformHooks::get).thenReturn(mock(PlatformHooks.class));
when(snapshot.write()).thenThrow(failure);
NmsSavedTerrainCapture.CapturedChunk captured = NmsSavedTerrainCapture.scheduleSnapshot(fixture.level, chunk);
Future<IOException> result = checkpoint.submit(() -> assertThrows(IOException.class,
() -> NmsSavedTerrainCapture.awaitSnapshot(fixture.level, captured,
System.nanoTime() + TimeUnit.SECONDS.toNanos(3))));
assertSame(failure, result.get(5, TimeUnit.SECONDS).getCause());
assertTrue(captured.data().isCompletedExceptionally());
}
}
@Test
public void checkpointBoundsOwnerRequestsUntilPriorNativeWritesFinish() throws Exception {
try (SaveFixture fixture = new SaveFixture();
ExecutorService owner = Executors.newSingleThreadExecutor();
ExecutorService checkpoint = Executors.newSingleThreadExecutor()) {
World world = mock(World.class);
when(world.getWorldFolder()).thenReturn(new File("checkpoint-world"));
List<NewChunkHolder> holders = new ArrayList<>(129);
for (int index = 0; index < 129; index++) {
NewChunkHolder holder = mock(NewChunkHolder.class);
setField(holder, "chunkX", index);
holders.add(holder);
}
AtomicInteger requested = new AtomicInteger();
AtomicBoolean writesBlocked = new AtomicBoolean(true);
CountDownLatch firstBatchWaiting = new CountDownLatch(1);
Future<?> result = checkpoint.submit(() -> {
try (MockedStatic<J> scheduling = mockStatic(J.class);
MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class)) {
scheduling.when(() -> J.runRegionFuture(eq(world), anyInt(), anyInt(), any(Runnable.class)))
.thenAnswer(invocation -> {
requested.incrementAndGet();
Runnable action = invocation.getArgument(3);
return CompletableFuture.runAsync(action, owner);
});
io.when(() -> MoonriseRegionFileIO.getPriority(eq(fixture.level), anyInt(), anyInt(),
eq(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))).thenAnswer(invocation -> {
firstBatchWaiting.countDown();
return writesBlocked.get() ? Priority.NORMAL : Priority.COMPLETING;
});
NmsSavedTerrainCapture.flushSnapshots(world, fixture.level, holders);
io.verify(() -> MoonriseRegionFileIO.getPriority(fixture.level, 128, 0,
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), times(2));
return null;
}
});
try {
assertTrue(firstBatchWaiting.await(5, TimeUnit.SECONDS));
assertEquals(64, requested.get());
assertTrue(owner.submit(() -> true).get(5, TimeUnit.SECONDS));
assertFalse(result.isDone());
writesBlocked.set(false);
result.get(5, TimeUnit.SECONDS);
assertEquals(129, requested.get());
} finally {
writesBlocked.set(false);
}
}
}
private static void setField(Object instance, String name, Object value) throws Exception {
Field field = instance.getClass().getField(name);
field.setAccessible(true);
field.set(instance, value);
}
private static final class SaveFixture implements AutoCloseable {
private final BalancedPrioritisedThreadPool pool = new BalancedPrioritisedThreadPool(
TimeUnit.MILLISECONDS.toNanos(1), Executors.defaultThreadFactory());
private final ServerLevel level = mock(ServerLevel.class);
private SaveFixture() throws Exception {
BalancedPrioritisedThreadPool.OrderedStreamGroup group = pool.createOrderedStreamGroup();
ChunkTaskScheduler scheduler = mock(ChunkTaskScheduler.class);
setField(scheduler, "saveExecutor", group.createExecutor());
setField(scheduler, "compressionExecutor", group.createExecutor());
setField(scheduler, "chunkHolderManager", mock(ChunkHolderManager.class));
when(level.moonrise$getChunkTaskScheduler()).thenReturn(scheduler);
}
@Override
public void close() throws Exception {
pool.halt(true);
assertTrue(pool.joinInterruptable(5_000));
}
}
}
@@ -0,0 +1,130 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.core.nms.v26_2_R1.IrisChunkGenerator;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.history.BoundaryColumnGeometry;
import art.arcane.iris.engine.history.TerrainBoundarySignature;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.levelgen.Heightmap;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public final class IrisChunkGeneratorHeightCacheTest {
@Rule
public final PlatformLeakGuard leakGuard = PlatformLeakGuard.clean();
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void repeatedNativeHeightSurvivesResolvedTerrainEvictionAndRuntimeChange() throws Exception {
Fixture fixture = fixture();
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
when(fixture.complex().resolvedTerrainColumn(-1, -33))
.thenThrow(new IllegalStateException("Full terrain was evicted"));
for (int index = 0; index < 100; index++) {
assertEquals(-11, fixture.generator().getBaseHeight(
-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
}
verify(fixture.complex(), times(1)).resolvedTerrainColumn(-1, -33);
when(fixture.engine().getCacheID()).thenReturn(2);
doReturn(Optional.of(signature())).when(fixture.complex()).resolvedTerrainColumn(-1, -33);
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
verify(fixture.complex(), times(2)).resolvedTerrainColumn(-1, -33);
}
@Test
public void ordinaryHeightRemainsLiveUntilResolvedTransitionTerrainExists() throws Exception {
Fixture fixture = fixture();
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
when(fixture.complex().resolvedTerrainColumn(0, 0))
.thenReturn(Optional.empty(), Optional.empty(), Optional.of(signature()));
when(fixture.engine().getHeight(0, 0, false)).thenReturn(20, 25);
assertEquals(5, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
assertEquals(10, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
verify(fixture.engine(), times(2)).getHeight(0, 0, false);
verify(fixture.complex(), times(3)).resolvedTerrainColumn(0, 0);
}
@Test
public void nativePredicatesAndAccessorBoundsRemainExact() throws Exception {
Fixture fixture = fixture();
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
for (int repeat = 0; repeat < 2; repeat++) {
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
assertEquals(-13, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null));
assertEquals(-13, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING, bounds, null));
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, bounds, null));
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE,
LevelHeightAccessor.create(-16, 2), null));
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES,
LevelHeightAccessor.create(-14, 3), null));
}
verify(fixture.complex(), times(6)).resolvedTerrainColumn(0, 0);
}
private static Fixture fixture() throws Exception {
Engine engine = mock(Engine.class);
IrisComplex complex = mock(IrisComplex.class);
when(engine.getComplex()).thenReturn(complex);
when(engine.getCacheID()).thenReturn(1);
when(engine.acquireGenerationLease(anyString())).thenReturn(GenerationSessionLease.noop());
when(complex.resolvedTerrainColumn(anyInt(), anyInt())).thenReturn(Optional.of(signature()));
IrisChunkGenerator generator = mock(IrisChunkGenerator.class, CALLS_REAL_METHODS);
set(generator, "engine", engine);
set(generator, "terrainHeights", new NativeTerrainHeightCache());
return new Fixture(generator, engine, complex);
}
private static void set(IrisChunkGenerator generator, String name, Object value) throws Exception {
Field field = IrisChunkGenerator.class.getDeclaredField(name);
field.setAccessible(true);
field.set(generator, value);
}
private static TerrainBoundarySignature signature() {
BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-16, List.of(
new BoundaryColumnGeometry.Voxel("minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false),
new BoundaryColumnGeometry.Voxel("minecraft:water", BoundaryColumnGeometry.Phase.FLUID, "minecraft:water", false),
new BoundaryColumnGeometry.Voxel("minecraft:oak_leaves[persistent=true]", BoundaryColumnGeometry.Phase.SOLID, "", false),
new BoundaryColumnGeometry.Voxel("minecraft:short_grass", BoundaryColumnGeometry.Phase.SOLID, "", false),
new BoundaryColumnGeometry.Voxel("minecraft:light[level=15]", BoundaryColumnGeometry.Phase.SOLID, "", false)));
return new TerrainBoundarySignature(
new TerrainBoundarySignature.Column(0, 0, 4, 4, OptionalInt.of(1), OptionalInt.empty()),
new TerrainBoundarySignature.Samples(new TerrainBoundarySignature.VerticalLayout(-16, 5, 1),
new TerrainBoundarySignature.BiomeEncoding(List.of("minecraft:plains"), new short[]{0})), geometry);
}
private record Fixture(IrisChunkGenerator generator, Engine engine, IrisComplex complex) {
}
}
@@ -16,6 +16,7 @@ import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
@@ -30,7 +31,9 @@ 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.GenerationStep;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.Structure;
@@ -51,6 +54,9 @@ import net.minecraft.world.level.levelgen.structure.templatesystem.ProcessorRule
import net.minecraft.world.level.levelgen.structure.templatesystem.RuleProcessor;
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.structures.OceanMonumentStructure;
import net.minecraft.world.level.levelgen.structure.structures.ShipwreckStructure;
import net.minecraft.world.level.levelgen.structure.structures.StrongholdStructure;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor;
@@ -99,6 +105,410 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
Bootstrap.bootStrap();
}
@Test
public void flattenCutsAndFillsSymmetricallyAcrossItsConfiguredBlend() {
NativeStructureSurfaceFitter.FlattenAnchor anchor = new NativeStructureSurfaceFitter.FlattenAnchor(
new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 80, 2), 24, 48);
List<NativeStructureSurfaceFitter.FlattenAnchor> anchors = List.of(anchor);
assertEquals(80, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, 0, 0, 128).targetY());
assertEquals(80, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, 0, 0, 32).targetY());
assertEquals(152, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, 0, 0, 200).targetY());
assertEquals(48, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, 0, 0, 0).targetY());
int previousCut = 80;
for (int x = 1; x <= 24; x++) {
int cut = NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, x, 0, 128).targetY();
int fill = NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, x, 0, 32).targetY();
assertEquals(160, cut + fill);
assertTrue(cut >= previousCut);
assertTrue(cut - previousCut <= 3);
previousCut = cut;
}
assertEquals(128, previousCut);
assertEquals(0, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, 24, 0, 128).range());
}
@Test
public void flattenBlendsNeighboringFoundationsWithoutOrderDependentRidges() {
NativeStructureSurfaceFitter.FlattenAnchor left = new NativeStructureSurfaceFitter.FlattenAnchor(
new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 80, 2), 24, 64);
NativeStructureSurfaceFitter.FlattenAnchor right = new NativeStructureSurfaceFitter.FlattenAnchor(
new NativeStructureSurfaceFitter.SurfaceAnchor(8, 8, 0, 0, 80, 2), 24, 64);
int previous = 80;
for (int x = 0; x <= 8; x++) {
NativeStructureSurfaceFitter.FlattenResolution forward = NativeStructureSurfaceFitter.resolveFlattenSurface(
List.of(left, right), x, 0, 104);
assertEquals(forward, NativeStructureSurfaceFitter.resolveFlattenSurface(
List.of(right, left), x, 0, 104));
assertTrue(forward.targetY() <= 82);
assertTrue(Math.abs(forward.targetY() - previous) <= 1);
previous = forward.targetY();
}
}
@Test
public void flattenLevelsNonJigsawFoundationsAndRemovesDetachedRoofs() {
StructureStart start = desertStart();
int groundY = start.getBoundingBox().minY() - 1;
BoundingBox area = new BoundingBox(0, 0, 0, 3, 127, 0);
Map<BlockPos, BlockState> blocks = new HashMap<>();
for (int x = 0; x <= 3; x++) {
int sourceY = x < 2 ? groundY - 32 : groundY + 32;
put(blocks, x, 12, 0, Blocks.STONE.defaultBlockState());
put(blocks, x, sourceY - 1, 0, Blocks.DIRT.defaultBlockState());
put(blocks, x, sourceY, 0, Blocks.GRASS_BLOCK.defaultBlockState());
}
assertTrue(NativeStructureSurfaceFitter.requiresFlattenTerrain(
surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)));
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)),
(x, z) -> x < 2 ? groundY - 32 : groundY + 32);
for (int x = 0; x <= 3; x++) {
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, x, groundY, 0));
for (int y = groundY + 1; y <= groundY + 32; y++) {
assertTrue("Roof at " + x + "," + y, state(blocks, x, y, 0).isAir());
}
for (int y = 12; y < groundY; y++) {
assertTrue("Missing foundation at " + x + "," + y, state(blocks, x, y, 0).isSolid());
}
}
}
@Test
public void flattenRestoresAnUnchangedFoundationSurfaceOverADeepAirGap() {
StructureStart start = desertStart();
int groundY = start.getBoundingBox().minY() - 1;
BoundingBox area = new BoundingBox(0, 0, 0, 0, 127, 0);
Map<BlockPos, BlockState> blocks = new HashMap<>();
put(blocks, 0, groundY - 19, 0, Blocks.DIRT.defaultBlockState());
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)), (x, z) -> groundY);
for (int y = groundY - 19; y <= groundY; y++) {
assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 0, y, 0));
}
assertTrue(state(blocks, 0, groundY + 1, 0).isAir());
}
@Test
public void flattenLeavesBuriedAndSubmergedInstancesUntouched() {
StructureStart start = desertStart();
int groundY = start.getBoundingBox().minY() - 1;
BoundingBox area = new BoundingBox(0, 0, 0, 0, 127, 0);
Map<BlockPos, BlockState> buried = flatTerrain(area, 100);
Map<BlockPos, BlockState> originalBuried = new HashMap<>(buried);
Map<BlockPos, BlockState> submerged = flatTerrain(area, groundY - 8);
put(submerged, 0, groundY - 7, 0, Blocks.WATER.defaultBlockState());
Map<BlockPos, BlockState> originalSubmerged = new HashMap<>(submerged);
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(buried), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)), (x, z) -> 100);
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(submerged), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)), (x, z) -> groundY - 8);
assertEquals(originalBuried, buried);
assertEquals(originalSubmerged, submerged);
}
@Test
public void flattenExemptsUndergroundAndSubmergedStructureTypes() {
Structure.StructureSettings surface = new Structure.StructureSettings(
HolderSet.empty(), Map.of(), GenerationStep.Decoration.SURFACE_STRUCTURES, TerrainAdjustment.NONE);
Structure.StructureSettings underground = new Structure.StructureSettings(
HolderSet.empty(), Map.of(), GenerationStep.Decoration.UNDERGROUND_STRUCTURES, TerrainAdjustment.NONE);
List<Structure> exempt = List.of(new StrongholdStructure(surface), new OceanMonumentStructure(surface),
new ShipwreckStructure(surface, false), new DesertPyramidStructure(underground));
for (Structure structure : exempt) {
StructureStart start = new StructureStart(structure, new ChunkPos(0, 0), 0,
new PiecesContainer(desertStart().getPieces()));
assertFalse(NativeStructureSurfaceFitter.requiresFlattenTerrain(
surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)));
}
assertFalse(NativeStructureSurfaceFitter.requiresFlattenTerrain(
surfaceTarget(desertStart(TerrainAdjustment.BURY), IrisStructureTerrainMode.FLATTEN)));
assertFalse(NativeStructureSurfaceFitter.requiresFlattenTerrain(
surfaceTarget(desertStart(TerrainAdjustment.ENCAPSULATE), IrisStructureTerrainMode.FLATTEN)));
}
@Test
public void flattenFoundationReadsAndWritesStayInsideConfiguredVerticalRange() throws Exception {
PoolElementStructurePiece piece = rigidTemplatePiece(new InlineSinglePoolElement(template(List.of(
block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))),
new BoundingBox(0, 128, 0, 0, 136, 0), 0, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(piece));
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, IrisStructureTerrainMode.FLATTEN);
target.terrain().setFlattenRange(32);
BoundingBox area = new BoundingBox(0, 0, 0, 0, 255, 0);
Map<BlockPos, BlockState> blocks = new HashMap<>();
put(blocks, 0, 94, 0, Blocks.GOLD_BLOCK.defaultBlockState());
WorldGenLevel delegate = world(blocks);
WorldGenLevel bounded = (WorldGenLevel) Proxy.newProxyInstance(WorldGenLevel.class.getClassLoader(),
new Class<?>[]{WorldGenLevel.class}, (proxy, method, arguments) -> {
if (method.getName().equals("getBlockState") || method.getName().equals("setBlock")) {
BlockPos position = (BlockPos) arguments[0];
assertTrue(position.toString(), position.getY() >= 95 && position.getY() <= 159);
}
return method.invoke(delegate, arguments);
});
NativeStructureSurfaceFitter.prepareSurfaceStructures(bounded, area, List.of(target), (x, z) -> 127);
assertEquals(Blocks.GOLD_BLOCK.defaultBlockState(), state(blocks, 0, 94, 0));
assertEquals(Blocks.STONE.defaultBlockState(), state(blocks, 0, 95, 0));
assertEquals(Blocks.STONE.defaultBlockState(), state(blocks, 0, 127, 0));
assertTrue(state(blocks, 0, 128, 0).isAir());
}
@Test
public void flattenReferencesIncludeTheMaximumConfiguredFeather() throws Exception {
PoolElementStructurePiece piece = rigidTemplatePiece(new InlineSinglePoolElement(template(List.of(
block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))),
new BoundingBox(0, 64, 0, 0, 70, 0), 0, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(piece));
IrisStructureTerrain terrain = new IrisStructureTerrain().setMode(IrisStructureTerrainMode.FLATTEN)
.setHorizontalPadding(128).setFlattenRange(128);
BoundingBox references = NativeStructureReferenceEnvelope.referenceBounds(start, start.getStructure(), terrain);
assertEquals(-128, references.minX());
assertEquals(128, references.maxX());
assertEquals(-128, references.minZ());
assertEquals(128, references.maxZ());
NativeStructureSurfaceFitter.FlattenAnchor anchor = new NativeStructureSurfaceFitter.FlattenAnchor(
new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 64, 2), 128, 128);
assertTrue(NativeStructureSurfaceFitter.resolveFlattenSurface(List.of(anchor), 127, 0, 100).range() > 0);
assertEquals(0, NativeStructureSurfaceFitter.resolveFlattenSurface(List.of(anchor), 128, 0, 100).range());
}
@Test
public void flattenPreservesNativeVillageSupportBridges() throws Exception {
PoolElementStructurePiece lower = rigidTemplatePiece(new InlineLegacyPoolElement(template(List.of(
block(0, 0, 0, Blocks.AIR.defaultBlockState())))),
new BoundingBox(0, 62, 0, 0, 65, 0), 1, Rotation.NONE);
PoolElementStructurePiece upper = rigidTemplatePiece(new InlineSinglePoolElement(template(List.of(
block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))),
new BoundingBox(0, 64, 0, 0, 70, 0), 0, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(lower, upper));
BoundingBox area = new BoundingBox(0, 58, 0, 0, 72, 0);
Map<BlockPos, BlockState> blocks = supportTerrain(0, 0);
Set<Long> written = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport(world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)),
NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager);
assertEquals(Set.of(BlockPos.asLong(0, 63, 0)), written);
assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 0, 63, 0));
}
@Test
public void flattenDoesNotBridgeACompletelyBuriedVillage() throws Exception {
PoolElementStructurePiece lower = rigidTemplatePiece(new InlineLegacyPoolElement(template(List.of(
block(0, 0, 0, Blocks.AIR.defaultBlockState())))),
new BoundingBox(0, 62, 0, 0, 65, 0), 1, Rotation.NONE);
PoolElementStructurePiece upper = rigidTemplatePiece(new InlineSinglePoolElement(template(List.of(
block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))),
new BoundingBox(0, 64, 0, 0, 70, 0), 0, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(lower, upper));
BoundingBox area = new BoundingBox(0, 58, 0, 0, 90, 0);
Map<BlockPos, BlockState> blocks = supportTerrain(0, 0);
put(blocks, 0, 80, 0, Blocks.STONE.defaultBlockState());
Set<Long> written = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport(world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)),
NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager);
assertTrue(written.isEmpty());
assertTrue(state(blocks, 0, 63, 0).isAir());
}
@Test
public void flattenKeepsPyramidPlacementAboveItsPreparedGround() {
FlattenDesertPiece piece = new FlattenDesertPiece();
StructureStart start = new StructureStart(desertStart().getStructure(), new ChunkPos(0, 0), 0,
new PiecesContainer(List.of(piece)));
BoundingBox footprint = piece.getBoundingBox();
NativeStructureVerticalPlacer.applyVerticalPlacement(start, "minecraft:desert_pyramid", 0,
63, 0, 256, false, false, null, (x, z) -> x == footprint.minX() ? 80 : 112);
BoundingBox area = new BoundingBox(footprint.minX(), 0, footprint.minZ(),
footprint.maxX(), 160, footprint.maxZ());
Map<BlockPos, BlockState> blocks = new HashMap<>();
for (int x = footprint.minX(); x <= footprint.maxX(); x++) {
for (int z = footprint.minZ(); z <= footprint.maxZ(); z++) {
int surfaceY = x == footprint.minX() ? 80 : 112;
put(blocks, x, surfaceY - 1, z, Blocks.DIRT.defaultBlockState());
put(blocks, x, surfaceY, z, Blocks.GRASS_BLOCK.defaultBlockState());
}
}
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)),
(x, z) -> x == footprint.minX() ? 80 : 112);
assertEquals(81, piece.getBoundingBox().minY());
assertTrue(piece.attemptVanillaAlignment());
assertEquals(81, piece.getBoundingBox().minY());
assertTrue(state(blocks, footprint.maxX(), 80, footprint.maxZ()).isSolid());
assertTrue(state(blocks, footprint.maxX(), 81, footprint.maxZ()).isAir());
}
@Test
public void flattenLetsTerrainMatchingPathsFollowThePreparedVillageGrade() throws Exception {
StructureTemplate foundation = template(List.of(block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())));
PoolElementStructurePiece left = rigidTemplatePiece(new InlineSinglePoolElement(foundation),
new BoundingBox(0, 65, 0, 0, 75, 0), 0, Rotation.NONE);
PoolElementStructurePiece right = rigidTemplatePiece(new InlineSinglePoolElement(foundation),
new BoundingBox(8, 65, 0, 8, 75, 0), 0, Rotation.NONE);
PoolElementStructurePiece path = rigidTemplatePiece(new InlineSinglePoolElement(
template(List.of(block(0, 0, 0, Blocks.DIRT_PATH.defaultBlockState()))),
List.of(), StructureTemplatePool.Projection.TERRAIN_MATCHING),
new BoundingBox(4, 65, 0, 4, 75, 0), 0, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(left, path, right));
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, IrisStructureTerrainMode.FLATTEN);
target.terrain().setHorizontalPadding(24);
BoundingBox area = new BoundingBox(0, 0, 0, 8, 128, 0);
Map<BlockPos, BlockState> blocks = flatTerrain(area, 96);
for (int x : List.of(0, 8)) {
blocks.remove(new BlockPos(x, 95, 0));
blocks.remove(new BlockPos(x, 96, 0));
put(blocks, x, 63, 0, Blocks.DIRT.defaultBlockState());
put(blocks, x, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState());
}
WorldGenLevel world = world(blocks);
NativeStructureSurfaceFitter.prepareSurfaceStructures(world, area, List.of(target),
(x, z) -> x == 0 || x == 8 ? 64 : 96);
NativeStructureTemplateOccupancy.OccupancyResult occupancy = NativeStructureTemplateOccupancy.resolve(
world, path, BlockPos.ZERO, area,
NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager,
area::isInside, cells -> {});
Map<Long, NativeStructureTemplateOccupancy.LowestCell> pathGround =
NativeStructureTemplateOccupancy.lowestProcessedSolidCells(occupancy.cells());
assertTrue(state(blocks, 4, 96, 0).isAir());
assertEquals(1, pathGround.size());
int pathY = pathGround.values().iterator().next().y();
assertTrue("Path Y " + pathY, pathY >= 64 && pathY <= 67);
}
@Test
public void flattenPublishesCutAndFillHeightsBeforeNativePathProjectionAtFeatures() throws Exception {
ProtoChunk chunk = new ProtoChunk(new ChunkPos(0, 0), UpgradeData.EMPTY,
LevelHeightAccessor.create(0, 256), containerFactory(), null);
chunk.setPersistedStatus(ChunkStatus.CARVERS);
for (int x = 0; x <= 8; x++) {
int originalY = x < 4 ? 96 : 32;
write(chunk, x, originalY - 1, 0, Blocks.DIRT.defaultBlockState());
write(chunk, x, originalY, 0, Blocks.GRASS_BLOCK.defaultBlockState());
}
WorldgenTerrainHeightmaps.primeTerrain(chunk,
(x, z) -> x == 15 ? 81 : x < 4 ? 97 : 33,
(x, z) -> x == 15 ? 61 : x < 4 ? 97 : 33);
StructureTemplate foundation = template(List.of(block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())));
PoolElementStructurePiece left = rigidTemplatePiece(new InlineSinglePoolElement(foundation),
new BoundingBox(0, 65, 0, 0, 75, 0), 0, Rotation.NONE);
PoolElementStructurePiece right = rigidTemplatePiece(new InlineSinglePoolElement(foundation),
new BoundingBox(8, 65, 0, 8, 75, 0), 0, Rotation.NONE);
PoolElementStructurePiece path = rigidTemplatePiece(new InlineSinglePoolElement(
template(List.of(block(0, 0, 0, Blocks.DIRT_PATH.defaultBlockState()),
block(8, 0, 0, Blocks.DIRT_PATH.defaultBlockState()))),
List.of(), StructureTemplatePool.Projection.TERRAIN_MATCHING),
new BoundingBox(0, 65, 0, 8, 75, 0), 0, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(left, path, right));
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, IrisStructureTerrainMode.FLATTEN);
target.terrain().setHorizontalPadding(24);
BoundingBox area = new BoundingBox(0, 0, 0, 8, 127, 0);
WorldGenLevel world = world(chunk);
NativeStructureSurfaceFitter.SurfaceTerrainPlan plan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
world, area, List.of(target), (x, z) -> x < 4 ? 96 : 32);
assertEquals(96, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 0, 0));
assertEquals(32, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 8, 0));
plan.primeHeightmaps(chunk);
chunk.setBlockState(new BlockPos(0, 85, 0), Blocks.OAK_PLANKS.defaultBlockState(), 2);
NativeStructureTemplateOccupancy.OccupancyResult occupancy = NativeStructureTemplateOccupancy.resolve(
world, path, BlockPos.ZERO, area,
NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager,
area::isInside, cells -> {});
Map<Long, NativeStructureTemplateOccupancy.LowestCell> pathGround =
NativeStructureTemplateOccupancy.lowestProcessedSolidCells(occupancy.cells());
assertEquals(2, pathGround.size());
for (NativeStructureTemplateOccupancy.LowestCell cell : pathGround.values()) {
assertEquals(64, cell.y());
chunk.setBlockState(new BlockPos(cell.x(), cell.y(), cell.z()), cell.occupancy().state(), 2);
assertTrue(chunk.getBlockState(new BlockPos(cell.x(), 63, cell.z())).isSolid());
assertTrue(chunk.getBlockState(new BlockPos(cell.x(), 65, cell.z())).isAir());
assertTrue(chunk.getBlockState(new BlockPos(cell.x(), 66, cell.z())).isAir());
assertEquals(64, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, cell.x(), cell.z()));
assertEquals(64, chunk.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, cell.x(), cell.z()));
}
assertEquals(80, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 15, 0));
assertEquals(60, chunk.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, 15, 0));
}
@Test
public void flattenSupportsProjectedRoadsAcrossRoofCavitiesWithoutFillingAdjacentColumns() throws Exception {
ProtoChunk chunk = new ProtoChunk(new ChunkPos(0, 0), UpgradeData.EMPTY,
LevelHeightAccessor.create(0, 256), containerFactory(), null);
chunk.setPersistedStatus(ChunkStatus.CARVERS);
for (int x = 0; x <= 8; x++) {
int originalY = x == 0 || x == 8 ? 64 : 96;
write(chunk, x, 20, 0, Blocks.STONE.defaultBlockState());
write(chunk, x, originalY - 1, 0, Blocks.DIRT.defaultBlockState());
write(chunk, x, originalY, 0, Blocks.GRASS_BLOCK.defaultBlockState());
}
write(chunk, 6, 50, 0, Blocks.WATER.defaultBlockState());
WorldgenTerrainHeightmaps.primeTerrain(chunk,
(x, z) -> x == 0 || x == 8 ? 65 : 97,
(x, z) -> x == 0 || x == 8 ? 65 : 97);
StructureTemplate foundation = template(List.of(block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())));
PoolElementStructurePiece left = rigidTemplatePiece(new InlineSinglePoolElement(foundation),
new BoundingBox(0, 65, 0, 0, 75, 0), 0, Rotation.NONE);
PoolElementStructurePiece right = rigidTemplatePiece(new InlineSinglePoolElement(foundation),
new BoundingBox(8, 65, 0, 8, 75, 0), 0, Rotation.NONE);
PoolElementStructurePiece path = rigidTemplatePiece(new InlineSinglePoolElement(
template(List.of(block(0, 0, 0, Blocks.DIRT_PATH.defaultBlockState()),
block(1, 0, 0, Blocks.AIR.defaultBlockState()),
block(2, 0, 0, Blocks.DIRT_PATH.defaultBlockState()))),
List.of(), StructureTemplatePool.Projection.TERRAIN_MATCHING),
new BoundingBox(4, 65, 0, 6, 75, 0), 0, Rotation.NONE);
StructureStart start = rigidSurfaceStart(List.of(left, path, right));
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, IrisStructureTerrainMode.FLATTEN);
target.terrain().setHorizontalPadding(24).setFlattenRange(32);
BoundingBox area = new BoundingBox(0, 0, 0, 8, 127, 0);
WorldGenLevel world = world(chunk);
NativeStructureSurfaceFitter.SurfaceTerrainPlan plan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
world, area, List.of(target), (x, z) -> x == 0 || x == 8 ? 64 : 96);
plan.primeHeightmaps(chunk);
NativeStructureTemplateOccupancy.OccupancyResult occupancy = NativeStructureTemplateOccupancy.resolve(
world, path, BlockPos.ZERO, area,
NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager,
area::isInside, cells -> {});
Map<Long, NativeStructureTemplateOccupancy.LowestCell> pathGround =
NativeStructureTemplateOccupancy.lowestProcessedSolidCells(occupancy.cells());
assertEquals(2, pathGround.size());
for (NativeStructureTemplateOccupancy.LowestCell cell : pathGround.values()) {
write(chunk, cell.x(), cell.y(), cell.z(), cell.occupancy().state());
assertTrue(cell.y() > 64 && cell.y() < 72);
assertTrue(chunk.getBlockState(new BlockPos(cell.x(), cell.y() - 1, cell.z())).isSolid());
assertTrue(chunk.getBlockState(new BlockPos(cell.x(), cell.y() + 1, cell.z())).isAir());
}
int roadY = chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 4, 0);
for (int y = roadY - 32; y < roadY; y++) {
assertTrue("Missing road support at " + y, chunk.getBlockState(new BlockPos(4, y, 0)).isSolid());
}
assertTrue(chunk.getBlockState(new BlockPos(4, roadY - 33, 0)).isAir());
int adjacentY = chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 5, 0);
assertTrue(chunk.getBlockState(new BlockPos(5, adjacentY - 1, 0)).isAir());
assertEquals(Blocks.WATER.defaultBlockState(), chunk.getBlockState(new BlockPos(6, 50, 0)));
assertTrue(chunk.getBlockState(new BlockPos(6, 49, 0)).isAir());
}
@Test
public void sourceBeardAdjustmentsPrepareOnlyAtTheSurfaceStructuresStep() {
assertTrue(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain(
@@ -474,7 +884,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
BoundingBox area = new BoundingBox(0, 54, 0, 3, 74, 3);
Map<BlockPos, BlockState> blocks = flatTerrain(area, 60);
NativeStructureSurfaceFitter.VacuumFoundationPlan plan =
NativeStructureSurfaceFitter.SurfaceTerrainPlan plan =
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
@@ -514,7 +924,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
BoundingBox area = new BoundingBox(0, 54, 0, 3, 72, 3);
Map<BlockPos, BlockState> blocks = flatTerrain(area, 60);
NativeStructureSurfaceFitter.VacuumFoundationPlan plan =
NativeStructureSurfaceFitter.SurfaceTerrainPlan plan =
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(blocks), area,
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
@@ -546,13 +956,13 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
Map<BlockPos, BlockState> wideBlocks = flatTerrain(wideArea, 60);
Map<BlockPos, BlockState> splitBlocks = flatTerrain(wideArea, 60);
NativeStructureSurfaceFitter.VacuumFoundationPlan widePlan =
NativeStructureSurfaceFitter.SurfaceTerrainPlan widePlan =
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(wideBlocks), wideArea, List.of(target), (x, z) -> 60);
NativeStructureSurfaceFitter.VacuumFoundationPlan westPlan =
NativeStructureSurfaceFitter.SurfaceTerrainPlan westPlan =
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(splitBlocks), westArea, List.of(target), (x, z) -> 60);
NativeStructureSurfaceFitter.VacuumFoundationPlan eastPlan =
NativeStructureSurfaceFitter.SurfaceTerrainPlan eastPlan =
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world(splitBlocks), eastArea, List.of(target), (x, z) -> 60);
for (int x = 15; x <= 16; x++) {
@@ -2203,6 +2613,16 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
return template;
}
private static final class FlattenDesertPiece extends DesertPyramidPiece {
private FlattenDesertPiece() {
super(RandomSource.create(29L), 0, 0);
}
private boolean attemptVanillaAlignment() {
return updateHeightPositionToLowestGroundHeight((LevelAccessor) null, -2);
}
}
private static final class InlineSinglePoolElement extends SinglePoolElement {
private InlineSinglePoolElement(StructureTemplate template) {
this(template, List.of());
@@ -2319,6 +2739,9 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
if (methodName.equals("getSeed")) {
return TEST_SEED;
}
if (methodName.equals("getHeight") && arguments != null && arguments.length == 3) {
return chunk.getHeight((Heightmap.Types) arguments[0], (int) arguments[1], (int) arguments[2]) + 1;
}
if (methodName.equals("getLevel")) {
return null;
}
@@ -0,0 +1,129 @@
package art.arcane.iris.nativegen;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.levelgen.Heightmap;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.OptionalInt;
import java.util.function.IntSupplier;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public final class NativeTerrainHeightCacheTest {
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void coordinatesRuntimePredicatesAndBoundsHaveSeparateEntries() {
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
AtomicInteger calls = new AtomicInteger();
NativeTerrainHeightCache.Query[] queries = {
query(1, -1, 1),
query(1, 1, -1),
query(2, -1, 1),
new NativeTerrainHeightCache.Query(1, -1, 1, Heightmap.Types.OCEAN_FLOOR, -256, 768),
new NativeTerrainHeightCache.Query(1, -1, 1, Heightmap.Types.WORLD_SURFACE, -64, 768),
new NativeTerrainHeightCache.Query(1, -1, 1, Heightmap.Types.WORLD_SURFACE, -256, 256)
};
for (int index = 0; index < queries.length; index++) {
assertEquals(index + 1, cached(cache, queries[index], calls::incrementAndGet));
}
for (int index = 0; index < queries.length; index++) {
assertEquals(index + 1, cached(cache, queries[index], calls::incrementAndGet));
}
assertEquals(queries.length, calls.get());
cache.evictRuntime(1);
assertEquals(3, cached(cache, queries[2], calls::incrementAndGet));
assertEquals(7, cached(cache, queries[0], calls::incrementAndGet));
}
@Test
public void allIntegerResultsAreCacheable() {
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
NativeTerrainHeightCache.Query query = query(1, 0, 0);
assertEquals(Integer.MIN_VALUE, cached(cache, query, () -> Integer.MIN_VALUE));
assertEquals(Integer.MIN_VALUE, cached(cache, query, () -> 0));
}
@Test
public void failedResolutionCanBeRetried() {
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
NativeTerrainHeightCache.Query query = query(1, 0, 0);
assertThrows(IllegalStateException.class, () -> cached(cache, query, () -> {
throw new IllegalStateException("terrain unavailable");
}));
assertEquals(42, cached(cache, query, () -> 42));
}
@Test
public void coldResolutionDoesNotHoldCacheMonitor() throws Exception {
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
Future<Integer> slow = executor.submit(() -> cached(cache, query(1, 0, 0), () -> {
entered.countDown();
try {
assertTrue(release.await(5, TimeUnit.SECONDS));
} catch (InterruptedException interruption) {
Thread.currentThread().interrupt();
throw new IllegalStateException(interruption);
}
return 9;
}));
assertTrue(entered.await(5, TimeUnit.SECONDS));
Future<Integer> independent = executor.submit(() -> cached(cache, query(1, 1, 0), () -> 17));
assertEquals(Integer.valueOf(17), independent.get(1, TimeUnit.SECONDS));
release.countDown();
assertEquals(Integer.valueOf(9), slow.get(5, TimeUnit.SECONDS));
} finally {
release.countDown();
executor.shutdownNow();
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
}
}
@Test
public void boundedCacheKeepsRecentlyReadCoordinates() {
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
for (int x = 0; x < 65_536; x++) {
cached(cache, query(1, x, 0), () -> 8);
}
assertEquals(8, cached(cache, query(1, 0, 0), () -> 99));
cached(cache, query(1, 65_536, 0), () -> 9);
assertEquals(8, cached(cache, query(1, 0, 0), () -> 99));
assertEquals(99, cached(cache, query(1, 1, 0), () -> 99));
}
@Test
public void unavailableResolvedHeightIsNotCached() {
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
NativeTerrainHeightCache.Query query = query(1, 0, 0);
assertTrue(cache.resolvedHeight(query, OptionalInt::empty).isEmpty());
assertEquals(42, cache.resolvedHeight(query, () -> OptionalInt.of(42)).orElseThrow());
assertEquals(42, cache.resolvedHeight(query, OptionalInt::empty).orElseThrow());
}
private static int cached(NativeTerrainHeightCache cache, NativeTerrainHeightCache.Query query,
IntSupplier resolver) {
return cache.resolvedHeight(query, () -> OptionalInt.of(resolver.getAsInt())).orElseThrow();
}
private static NativeTerrainHeightCache.Query query(int runtimeId, int x, int z) {
return new NativeTerrainHeightCache.Query(runtimeId, x, z, Heightmap.Types.WORLD_SURFACE, -256, 768);
}
}
@@ -63,6 +63,7 @@ public final class NativeStructureReferenceEnvelope {
boolean usesEnvelope = mode == IrisStructureTerrainMode.BORE
|| mode == IrisStructureTerrainMode.FORCE_CARVE
|| mode == IrisStructureTerrainMode.VACUUM
|| mode == IrisStructureTerrainMode.FLATTEN
|| mode == IrisStructureTerrainMode.ENCASE;
int horizontalPadding = mode == IrisStructureTerrainMode.VACUUM
? NativeStructureSurfaceFitter.surfaceTerrainRadius()
@@ -12,6 +12,7 @@ import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
import net.minecraft.world.level.levelgen.structure.structures.RuinedPortalPiece;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
@@ -52,6 +53,32 @@ final class NativeStructureReflection {
return resolved;
}
static RuinedPortalPiece.VerticalPlacement ruinedPortalVerticalPlacement(RuinedPortalPiece piece) {
try {
return (RuinedPortalPiece.VerticalPlacement) RuinedPortalPlacementAccess.FIELD.get(piece);
} catch (IllegalAccessException error) {
throw new IllegalStateException("Cannot read native ruined portal vertical placement", error);
}
}
private static Field resolveRuinedPortalPlacementField() {
Field resolved = null;
for (Field field : RuinedPortalPiece.class.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers())
|| field.getType() != RuinedPortalPiece.VerticalPlacement.class) {
continue;
}
if (resolved != null) {
throw new IllegalStateException("RuinedPortalPiece has multiple vertical-placement fields");
}
resolved = field;
}
if (resolved == null || !resolved.trySetAccessible()) {
throw new IllegalStateException("RuinedPortalPiece vertical-placement field is inaccessible");
}
return resolved;
}
private static Field resolveMonumentChildPiecesField() {
Field resolved = null;
for (Field field : OceanMonumentPieces.MonumentBuilding.class.getDeclaredFields()) {
@@ -308,6 +335,13 @@ final class NativeStructureReflection {
}
}
private static final class RuinedPortalPlacementAccess {
private static final Field FIELD = resolveRuinedPortalPlacementField();
private RuinedPortalPlacementAccess() {
}
}
static final class ScatteredHeightPositionAccess {
static final Field FIELD = resolveScatteredHeightPositionField();
@@ -8,7 +8,9 @@ import net.minecraft.core.BlockPos;
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.chunk.ChunkAccess;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
@@ -16,6 +18,15 @@ import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
import net.minecraft.world.level.levelgen.structure.pools.JigsawJunction;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import net.minecraft.world.level.levelgen.structure.structures.IglooPieces;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure;
import net.minecraft.world.level.levelgen.structure.structures.OceanRuinStructure;
import net.minecraft.world.level.levelgen.structure.structures.RuinedPortalPiece;
import net.minecraft.world.level.levelgen.structure.structures.ShipwreckStructure;
import net.minecraft.world.level.levelgen.structure.structures.StrongholdStructure;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
import net.minecraft.world.level.levelgen.structure.structures.WoodlandMansionPieces;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import java.util.ArrayList;
import java.util.Collections;
@@ -39,11 +50,13 @@ public final class NativeStructureSurfaceFitter {
ConcurrentHashMap.newKeySet();
private static final Set<String> WARNED_VACUUM_BUDGET =
ConcurrentHashMap.newKeySet();
private static final Set<String> WARNED_FLATTEN_BUDGET =
ConcurrentHashMap.newKeySet();
private NativeStructureSurfaceFitter() {
}
public static VacuumFoundationPlan prepareSurfaceStructures(
public static SurfaceTerrainPlan prepareSurfaceStructures(
WorldGenLevel world, BoundingBox area,
List<NativeStructureTerrainIntegrator.TerrainTarget> targets,
IntBinaryOperator surfaceHeight) {
@@ -51,12 +64,12 @@ public final class NativeStructureSurfaceFitter {
world, area, targets, surfaceHeight, MAX_SURFACE_TEMPLATE_CELLS);
}
static VacuumFoundationPlan prepareSurfaceStructures(
static SurfaceTerrainPlan prepareSurfaceStructures(
WorldGenLevel world, BoundingBox area,
List<NativeStructureTerrainIntegrator.TerrainTarget> targets,
IntBinaryOperator surfaceHeight, int maximumTemplateCells) {
if (targets == null || targets.isEmpty()) {
return VacuumFoundationPlan.empty();
return SurfaceTerrainPlan.empty();
}
Objects.requireNonNull(surfaceHeight, "Surface structure terrain fitting requires an Iris height resolver");
List<SurfaceAnchor> anchors = collectSourceSurfaceAnchors(
@@ -64,18 +77,22 @@ public final class NativeStructureSurfaceFitter {
if (!anchors.isEmpty()) {
fitSurfaceTerrain(world, area, anchors, surfaceHeight);
}
FlattenFootprint flatten = collectFlattenFootprint(
world, area, targets, surfaceHeight, maximumTemplateCells);
Map<Long, Integer> flattenedHeights = flatten.anchors().isEmpty() && flatten.projectedSupport().isEmpty() ? Map.of()
: fitFlattenTerrain(world, area, flatten, surfaceHeight);
VacuumFootprint vacuum = collectVacuumFootprint(
world, area, targets, maximumTemplateCells);
if (!vacuum.anchors().isEmpty()) {
fitVacuumTerrain(world, area, vacuum, surfaceHeight);
}
return VacuumFoundationPlan.create(
vacuum.foundationBases(), vacuum.occupiedCells(), area);
return SurfaceTerrainPlan.create(
vacuum, area, flattenedHeights);
}
public static void repairVacuumFoundations(
WorldGenLevel world, BoundingBox area,
VacuumFoundationPlan plan) {
SurfaceTerrainPlan plan) {
if (plan == null || plan.foundationBases.isEmpty()) {
return;
}
@@ -484,6 +501,7 @@ public final class NativeStructureSurfaceFitter {
}
IrisStructureTerrainMode mode = target.terrain().resolvedMode();
return mode == IrisStructureTerrainMode.VACUUM
|| requiresFlattenTerrain(target)
|| requiresSourceSurfaceTerrain(target);
}
@@ -528,6 +546,268 @@ public final class NativeStructureSurfaceFitter {
}
}
static boolean requiresFlattenTerrain(NativeStructureTerrainIntegrator.TerrainTarget target) {
if (target == null || target.terrain() == null
|| target.terrain().resolvedMode() != IrisStructureTerrainMode.FLATTEN
|| target.terrain().resolvedFlattenRange() == 0
|| target.start() == null || !target.start().isValid()) {
return false;
}
StructureStart start = target.start();
return start.getStructure().step() == GenerationStep.Decoration.SURFACE_STRUCTURES
&& start.getStructure().terrainAdaptation() != TerrainAdjustment.BURY
&& start.getStructure().terrainAdaptation() != TerrainAdjustment.ENCAPSULATE
&& !(start.getStructure() instanceof StrongholdStructure)
&& !(start.getStructure() instanceof OceanMonumentStructure)
&& !(start.getStructure() instanceof OceanRuinStructure)
&& (!(start.getStructure() instanceof ShipwreckStructure)
|| "minecraft:shipwreck_beached".equals(target.structureId()));
}
private static FlattenFootprint collectFlattenFootprint(
WorldGenLevel world, BoundingBox area,
List<NativeStructureTerrainIntegrator.TerrainTarget> targets,
IntBinaryOperator surfaceHeight, int maximumTemplateCells) {
List<FlattenAnchor> anchors = new ArrayList<>();
Map<Long, Integer> projectedSupport = new HashMap<>();
Set<StructureStart> seen = Collections.newSetFromMap(new IdentityHashMap<>());
for (NativeStructureTerrainIntegrator.TerrainTarget target : targets) {
if (!requiresFlattenTerrain(target) || !seen.add(target.start())) {
continue;
}
int radius = Math.max(0, Math.min(128, target.terrain().getHorizontalPadding()));
int range = target.terrain().resolvedFlattenRange();
BoundingBox influenceArea = new BoundingBox(
area.minX() - radius, area.minY(), area.minZ() - radius,
area.maxX() + radius, area.maxY(), area.maxZ() + radius);
StructureStart start = target.start();
if (!hasExposedFlattenPiece(start, surfaceHeight, range)) {
continue;
}
BoundingBox first = start.getPieces().getFirst().getBoundingBox();
BlockPos reference = new BlockPos(first.getCenter().getX(), first.minY(), first.getCenter().getZ());
TemplateCellBudget budget = new TemplateCellBudget(maximumTemplateCells);
List<SurfaceAnchor> sources = new ArrayList<>();
Set<Long> projectedColumns = new HashSet<>();
try {
for (StructurePiece piece : start.getPieces()) {
BoundingBox bounds = piece.getBoundingBox();
if (!intersectsHorizontally(bounds, influenceArea) || !flattenPieceAllowed(start, piece)) {
continue;
}
if (piece instanceof PoolElementStructurePiece poolPiece) {
if (poolPiece.getElement().getProjection() == StructureTemplatePool.Projection.RIGID) {
addProcessedSurfaceAnchors(world, influenceArea, poolPiece, reference,
StructureTemplatePool.Projection.RIGID, budget, sources);
} else {
addProjectedFlattenSupport(world, area, poolPiece, reference, budget, projectedColumns);
}
for (JigsawJunction junction : poolPiece.getJunctions()) {
sources.add(new SurfaceAnchor(junction.getSourceX(), junction.getSourceX(),
junction.getSourceZ(), junction.getSourceZ(), junction.getSourceGroundY() - 1, 1));
}
} else {
sources.add(new SurfaceAnchor(bounds.minX(), bounds.maxX(), bounds.minZ(), bounds.maxZ(),
flattenGroundY(start, piece, surfaceHeight), 2));
}
}
} catch (TemplateBudgetExceeded ignored) {
String structureId = target.structureId() == null
? start.getStructure().getClass().getName() : target.structureId();
if (WARNED_FLATTEN_BUDGET.add(structureId)) {
IrisLogging.warn("Native structure FLATTEN fitting for '" + structureId
+ "' exceeded its bounded template budget; skipping this start");
}
continue;
}
for (SurfaceAnchor source : sources) {
anchors.add(new FlattenAnchor(source, radius, range));
}
for (long column : projectedColumns) {
projectedSupport.merge(column, range, Math::max);
}
}
return new FlattenFootprint(List.copyOf(anchors), Map.copyOf(projectedSupport));
}
private static void addProjectedFlattenSupport(
WorldGenLevel world, BoundingBox area, PoolElementStructurePiece piece,
BlockPos reference, TemplateCellBudget budget, Set<Long> columns) {
NativeStructureTemplateOccupancy.OccupancyResult occupancy = NativeStructureTemplateOccupancy.resolve(
world, piece, reference, area, () -> world.getLevel().getStructureManager(),
area::isInside, budget::consume);
for (Map.Entry<Long, NativeStructureTemplateOccupancy.OccupancyCell> entry : occupancy.cells().entrySet()) {
NativeStructureTemplateOccupancy.OccupancyCell cell = entry.getValue();
if (cell.blocker() || !NativeStructureTemplateOccupancy.isSolidBase(cell.state())) {
continue;
}
BlockPos position = BlockPos.of(entry.getKey());
columns.add(NativeStructureTemplateOccupancy.columnKey(position.getX(), position.getZ()));
}
}
private static boolean flattenPieceAllowed(StructureStart start, StructurePiece piece) {
if (piece instanceof RuinedPortalPiece portal) {
RuinedPortalPiece.VerticalPlacement placement = NativeStructureReflection.ruinedPortalVerticalPlacement(portal);
return placement == RuinedPortalPiece.VerticalPlacement.ON_LAND_SURFACE
|| placement == RuinedPortalPiece.VerticalPlacement.IN_NETHER;
}
if (piece instanceof IglooPieces.IglooPiece) {
int highest = Integer.MIN_VALUE;
for (StructurePiece candidate : start.getPieces()) {
if (candidate instanceof IglooPieces.IglooPiece) {
highest = Math.max(highest, candidate.getBoundingBox().minY());
}
}
return piece.getBoundingBox().minY() == highest;
}
return true;
}
static boolean hasExposedFlattenPiece(
StructureStart start, IntBinaryOperator surfaceHeight, int range) {
for (StructurePiece piece : start.getPieces()) {
if (!flattenPieceAllowed(start, piece)) {
continue;
}
BoundingBox bounds = piece.getBoundingBox();
int groundY = flattenGroundY(start, piece, surfaceHeight);
int roofY = piece instanceof IglooPieces.IglooPiece || piece instanceof SwampHutPiece
? groundY + bounds.getYSpan() : bounds.maxY();
for (int x = bounds.minX(); x <= bounds.maxX(); x++) {
for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) {
int surfaceY = surfaceHeight.applyAsInt(x, z);
if (surfaceY <= roofY && (long) groundY - surfaceY <= range) {
return true;
}
}
}
}
return false;
}
private static int flattenGroundY(
StructureStart start, StructurePiece piece, IntBinaryOperator surfaceHeight) {
BoundingBox bounds = piece.getBoundingBox();
if (piece instanceof PoolElementStructurePiece poolPiece) {
return bounds.minY() + poolPiece.getGroundLevelDelta() - 1;
}
if (piece instanceof IglooPieces.IglooPiece igloo) {
BlockPos entrance = igloo.templatePosition().offset(
StructureTemplate.calculateRelativePosition(igloo.placeSettings(), new BlockPos(3, 0, 0)));
return surfaceHeight.applyAsInt(entrance.getX(), entrance.getZ());
}
if (piece instanceof SwampHutPiece || start.getStructure() instanceof ShipwreckStructure) {
long sum = 0;
int minimum = Integer.MAX_VALUE;
for (int x = bounds.minX(); x <= bounds.maxX(); x++) {
for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) {
int surfaceY = surfaceHeight.applyAsInt(x, z);
sum += surfaceY;
minimum = Math.min(minimum, surfaceY);
}
}
return piece instanceof SwampHutPiece
? (int) Math.floorDiv(sum, (long) bounds.getXSpan() * bounds.getZSpan()) : minimum;
}
if (piece instanceof WoodlandMansionPieces.WoodlandMansionPiece) {
return start.getBoundingBox().minY() - 1;
}
return bounds.minY() - 1;
}
static FlattenResolution resolveFlattenSurface(
List<FlattenAnchor> anchors, int x, int z, int originalY) {
FlattenAnchor selected = null;
long weightedY = 0;
long totalWeight = 0;
long maximumInfluence = 0;
int range = 0;
for (FlattenAnchor anchor : anchors) {
SurfaceAnchor source = anchor.source();
int outX = IrisObjectVacuum.outset(x, source.minX(), source.maxX());
int outZ = IrisObjectVacuum.outset(z, source.minZ(), source.maxZ());
if (outX == 0 && outZ == 0) {
if (selected == null || precedes(source, selected.source())) {
selected = anchor;
}
continue;
}
double distance = Math.sqrt((double) outX * outX + (double) outZ * outZ);
if (anchor.radius() == 0 || distance >= anchor.radius()) {
continue;
}
double progress = distance / anchor.radius();
double factor = 1D - progress * progress * (3D - 2D * progress);
long influence = Math.round(factor * SURFACE_TERRAIN_INFLUENCE_SCALE);
long weight = influence * source.strength();
int targetY = originalY + Math.max(-anchor.range(), Math.min(anchor.range(), source.meetY() - originalY));
weightedY += weight * targetY;
totalWeight += weight;
maximumInfluence = Math.max(maximumInfluence, influence);
range = Math.max(range, anchor.range());
}
if (selected != null) {
int targetY = originalY + Math.max(-selected.range(),
Math.min(selected.range(), selected.source().meetY() - originalY));
return new FlattenResolution(targetY, selected.range(), selected.source().strength() > 1);
}
if (totalWeight == 0) {
return new FlattenResolution(originalY, 0, false);
}
int targetY = blendSurfaceTarget(originalY, weightedY / (double) totalWeight,
maximumInfluence / (double) SURFACE_TERRAIN_INFLUENCE_SCALE);
return new FlattenResolution(targetY, range, false);
}
private static Map<Long, Integer> fitFlattenTerrain(
WorldGenLevel world, BoundingBox area, FlattenFootprint footprint,
IntBinaryOperator surfaceHeight) {
Map<Long, Integer> heights = new HashMap<>();
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
for (int x = area.minX(); x <= area.maxX(); x++) {
for (int z = area.minZ(); z <= area.maxZ(); z++) {
int originalY = Math.max(area.minY(), Math.min(area.maxY(), surfaceHeight.applyAsInt(x, z)));
long column = NativeStructureTemplateOccupancy.columnKey(x, z);
FlattenResolution resolution = resolveFlattenSurface(footprint.anchors(), x, z, originalY);
int projectedRange = footprint.projectedSupport().getOrDefault(column, 0);
int range = Math.max(resolution.range(), projectedRange);
if (range == 0 || originalY < area.maxY()
&& !world.getBlockState(position.set(x, originalY + 1, z)).getFluidState().isEmpty()) {
continue;
}
int targetY = Math.max(area.minY(), Math.min(area.maxY(), resolution.targetY()));
int minimumY = Math.max(area.minY(), targetY - range);
applySurfaceColumn(world, position, x, z, originalY, targetY,
minimumY, Math.min(area.maxY(), originalY + range));
if (resolution.foundation() || projectedRange > 0) {
fillFlattenFoundation(world, position, x, z, targetY, minimumY);
}
heights.put(column, targetY + 1);
}
}
return Map.copyOf(heights);
}
private static void fillFlattenFoundation(
WorldGenLevel world, BlockPos.MutableBlockPos position,
int x, int z, int targetY, int minimumY) {
SurfaceMaterials materials = resolveSurfaceMaterials(world, position, x, z, targetY, minimumY);
if (!isTerrainBlock(world.getBlockState(position.set(x, targetY, z)))) {
world.setBlock(position, materials.surface(), 2);
}
for (int y = targetY - 1; y >= minimumY; y--) {
BlockState existing = world.getBlockState(position.set(x, y, z));
if (!existing.getFluidState().isEmpty()
|| NativeStructureVegetationClearer.isTreeBlock(existing)) {
break;
}
if (!isTerrainBlock(existing)) {
world.setBlock(position, materials.subsurface(), 2);
}
}
}
private static void fitVacuumTerrain(
WorldGenLevel world, BoundingBox area, VacuumFootprint footprint,
IntBinaryOperator surfaceHeight) {
@@ -821,6 +1101,15 @@ public final class NativeStructureSurfaceFitter {
record SurfaceAnchor(int minX, int maxX, int minZ, int maxZ, int meetY, int strength) {
}
record FlattenAnchor(SurfaceAnchor source, int radius, int range) {
}
record FlattenResolution(int targetY, int range, boolean foundation) {
}
private record FlattenFootprint(List<FlattenAnchor> anchors, Map<Long, Integer> projectedSupport) {
}
private record SurfaceMaterials(BlockState surface, BlockState subsurface) {
}
@@ -835,27 +1124,25 @@ public final class NativeStructureSurfaceFitter {
private record VacuumAnchor(int surfaceY, int strength) {
}
public static final class VacuumFoundationPlan {
private static final VacuumFoundationPlan EMPTY =
new VacuumFoundationPlan(List.of(), Set.of());
public record SurfaceTerrainPlan(
List<Long> foundationBases, Set<Long> occupiedCells, Map<Long, Integer> flattenedHeights) {
private static final SurfaceTerrainPlan EMPTY =
new SurfaceTerrainPlan(List.of(), Set.of(), Map.of());
private final List<Long> foundationBases;
private final Set<Long> occupiedCells;
private VacuumFoundationPlan(
List<Long> foundationBases, Set<Long> occupiedCells) {
this.foundationBases = foundationBases;
this.occupiedCells = occupiedCells;
public SurfaceTerrainPlan {
foundationBases = List.copyOf(foundationBases);
occupiedCells = Set.copyOf(occupiedCells);
flattenedHeights = Map.copyOf(flattenedHeights);
}
private static VacuumFoundationPlan empty() {
private static SurfaceTerrainPlan empty() {
return EMPTY;
}
private static VacuumFoundationPlan create(
Set<Long> packedBases, Set<Long> packedOccupancy,
BoundingBox area) {
if (packedBases.isEmpty()) {
private static SurfaceTerrainPlan create(
VacuumFootprint vacuum, BoundingBox area, Map<Long, Integer> flattenedHeights) {
Set<Long> packedBases = vacuum.foundationBases();
if (packedBases.isEmpty() && flattenedHeights.isEmpty()) {
return EMPTY;
}
List<Long> bases = new ArrayList<>(packedBases.size());
@@ -869,7 +1156,7 @@ public final class NativeStructureSurfaceFitter {
columns.add(NativeStructureTemplateOccupancy.columnKey(
base.getX(), base.getZ()));
}
if (bases.isEmpty()) {
if (bases.isEmpty() && flattenedHeights.isEmpty()) {
return EMPTY;
}
bases.sort((first, second) -> {
@@ -878,15 +1165,28 @@ public final class NativeStructureSurfaceFitter {
return yOrder == 0 ? Long.compare(first, second) : yOrder;
});
Set<Long> occupancy = new HashSet<>();
for (long packedCell : packedOccupancy) {
for (long packedCell : vacuum.occupiedCells()) {
BlockPos cell = BlockPos.of(packedCell);
if (columns.contains(NativeStructureTemplateOccupancy.columnKey(
cell.getX(), cell.getZ()))) {
occupancy.add(packedCell);
}
}
return new VacuumFoundationPlan(
List.copyOf(bases), Set.copyOf(occupancy));
return new SurfaceTerrainPlan(
bases, occupancy, flattenedHeights);
}
public void primeHeightmaps(ChunkAccess chunk) {
if (flattenedHeights.isEmpty()) {
return;
}
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
WorldgenTerrainHeightmaps.primeTerrain(chunk,
(x, z) -> flattenedHeights.getOrDefault(NativeStructureTemplateOccupancy.columnKey(x, z),
surface.getFirstAvailable(x & 15, z & 15)),
(x, z) -> flattenedHeights.getOrDefault(NativeStructureTemplateOccupancy.columnKey(x, z),
floor.getFirstAvailable(x & 15, z & 15)));
}
}
@@ -1,8 +1,10 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.engine.object.IrisStructureTerrainMode;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
@@ -44,6 +46,10 @@ final class NativeStructureSurfaceSupportBuilder {
for (NativeStructureTerrainIntegrator.TerrainTarget target : targets) {
if (target == null
|| !NativeStructureSurfaceFitter.requiresSurfaceTerrain(target)
|| target.terrain().resolvedMode() == IrisStructureTerrainMode.FLATTEN
&& !NativeStructureSurfaceFitter.hasExposedFlattenPiece(
target.start(), (x, z) -> world.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z) - 1,
target.terrain().resolvedFlattenRange())
|| !seenStarts.add(target.start())) {
continue;
}
@@ -118,7 +118,7 @@ public final class NativeStructureTerrainIntegrator {
if (mode == IrisStructureTerrainMode.PRESERVE) {
return;
}
if (mode == IrisStructureTerrainMode.VACUUM) {
if (mode == IrisStructureTerrainMode.VACUUM || mode == IrisStructureTerrainMode.FLATTEN) {
return;
}
if (mode == IrisStructureTerrainMode.ENCASE) {
@@ -0,0 +1,42 @@
package art.arcane.iris.nativegen;
import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap;
import net.minecraft.world.level.levelgen.Heightmap;
import java.util.OptionalInt;
import java.util.function.Supplier;
public final class NativeTerrainHeightCache {
private static final int MAXIMUM_CACHED_QUERIES = 65_536;
private final Object2IntLinkedOpenHashMap<Query> heights = new Object2IntLinkedOpenHashMap<>();
public OptionalInt resolvedHeight(Query query, Supplier<OptionalInt> resolver) {
synchronized (heights) {
if (heights.containsKey(query)) {
return OptionalInt.of(heights.getAndMoveToFirst(query));
}
}
OptionalInt resolved = resolver.get();
if (resolved.isEmpty()) {
return resolved;
}
int height = resolved.getAsInt();
synchronized (heights) {
heights.putAndMoveToFirst(query, height);
while (heights.size() > MAXIMUM_CACHED_QUERIES) {
heights.removeLastInt();
}
}
return resolved;
}
public void evictRuntime(int runtimeId) {
synchronized (heights) {
heights.keySet().removeIf(query -> query.runtimeId() == runtimeId);
}
}
public record Query(int runtimeId, int x, int z, Heightmap.Types type, int minimumY, int height) {
}
}
@@ -19,6 +19,7 @@
package art.arcane.iris.modded;
import art.arcane.iris.nativegen.NativeTransitionColumn;
import art.arcane.iris.nativegen.NativeTerrainHeightCache;
import art.arcane.iris.engine.history.TerrainBoundarySignature;
import art.arcane.iris.engine.history.NativeBiomeSpawnSelection;
import java.util.Optional;
@@ -107,6 +108,7 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.OptionalInt;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CancellationException;
@@ -145,6 +147,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
private final ModdedNativeStructureStage nativeStructures = new ModdedNativeStructureStage(this);
private final ModdedSpawnTableMerger spawnTables = new ModdedSpawnTableMerger(this);
private final ModdedImportedFeatureStage importedFeatures;
private final NativeTerrainHeightCache terrainHeights = new NativeTerrainHeightCache();
private final AtomicBoolean announced = new AtomicBoolean(false);
private final IntConsumer generationRuntimeRetirementListener = this::retireGenerationRuntimeCaches;
private volatile boolean unloading;
@@ -1381,15 +1384,18 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
@Override
public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor heightAccessor, RandomState randomState) {
Engine current = requireDataQueryEngine("base height");
boolean ignoreFluid = !type.isOpaque().test(Blocks.WATER.defaultBlockState());
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
current, x, z, "modded_base_height");
GenerationSessionLease lease = current.acquireGenerationLease("modded_base_height");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
Optional<TerrainBoundarySignature> resolved = current.getComplex().resolvedTerrainColumn(x, z);
NativeTerrainHeightCache.Query query = new NativeTerrainHeightCache.Query(
current.getCacheID(), x, z, type, heightAccessor.getMinY(), heightAccessor.getHeight());
OptionalInt resolved = terrainHeights.resolvedHeight(query,
() -> resolvedBaseHeight(current, x, z, type, heightAccessor));
if (resolved.isPresent()) {
return NativeTransitionColumn.height(resolved.get(), type, heightAccessor);
return resolved.getAsInt();
}
boolean ignoreFluid = !type.isOpaque().test(Blocks.WATER.defaultBlockState());
int height = current.getDimensionStackContext() == null
? current.getHeight(x, z, ignoreFluid)
: Engine.hostHeight(current, x, z, ignoreFluid);
@@ -1399,6 +1405,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
}
private OptionalInt resolvedBaseHeight(Engine current, int x, int z, Heightmap.Types type, LevelHeightAccessor heightAccessor) {
Optional<TerrainBoundarySignature> resolved = current.getComplex().resolvedTerrainColumn(x, z);
return resolved.isPresent()
? OptionalInt.of(NativeTransitionColumn.height(resolved.get(), type, heightAccessor))
: OptionalInt.empty();
}
@Override
public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor heightAccessor, RandomState randomState) {
int minY = heightAccessor.getMinY();
@@ -1482,6 +1495,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
private void retireGenerationRuntimeCaches(int runtimeIdentity) {
terrainHeights.evictRuntime(runtimeIdentity);
structureBiomeSource.evictRuntime(runtimeIdentity);
importedFeatures.evictRuntime(runtimeIdentity);
spawnTables.evictRuntime(runtimeIdentity);
@@ -46,6 +46,7 @@ import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.Difficulty;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntitySpawnReason;
import net.minecraft.world.entity.EntityType;
@@ -120,13 +121,20 @@ public final class ModdedEntitySpawner {
return null;
}
Entity entity = type.spawn(level, BlockPos.containing(x, y, z), reasonFor(irisEntity.getReason()));
Entity entity = spawnNative(type, level, BlockPos.containing(x, y, z), reasonFor(irisEntity.getReason()));
if (entity != null) {
entity.snapTo(x, y, z, 0F, 0F);
}
return entity;
}
static Entity spawnNative(EntityType<?> type, ServerLevel level, BlockPos position, EntitySpawnReason reason) {
if (level.getDifficulty() == Difficulty.PEACEFUL && !type.isAllowedInPeaceful()) {
return null;
}
return type.spawn(level, position, reason);
}
private static void applyConfig(Engine engine, IrisEntity irisEntity, Entity entity, ServerLevel level, int blockX, int blockY, int blockZ, RNG rng, boolean riseEffectActive) {
String customName = irisEntity.getCustomName();
if (customName != null) {
@@ -398,10 +398,7 @@ final class ModdedNativeStructureStage {
}
IrisStaticObjectLayer staticObjects = current.getDimension().getStaticObjectLayer(current.getData());
Predicate<BlockPos> protectedPosition = nativeStructureProtection(current, staticObjects);
WorldGenLevel boundedWorld = staticObjects.isEmpty()
&& current.getDimensionStackContext() == null
? world
: ModdedNativeStructureWorldgenAccess.create(
WorldGenLevel boundedWorld = ModdedNativeStructureWorldgenAccess.create(
world, chunkPos, worldgenSurfaceHeight(current, world.getMinY()), worldgenFloorHeight(current, world.getMinY()),
current.getDimensionStackContext() != null,
protectedPosition);
@@ -413,11 +410,12 @@ final class ModdedNativeStructureStage {
"vegetation cleanup", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
NativeStructureSurfaceFitter.VacuumFoundationPlan vacuumFoundationPlan;
NativeStructureSurfaceFitter.SurfaceTerrainPlan surfaceTerrainPlan;
try {
vacuumFoundationPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
surfaceTerrainPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
boundedWorld, area, terrainTargets,
(x, z) -> Engine.hostHeight(current, x, z, true) + current.getMinHeight());
surfaceTerrainPlan.primeHeightmaps(chunk);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"terrain integration", nativeStructureBatchContext(placementGroups),
@@ -445,7 +443,7 @@ final class ModdedNativeStructureStage {
}
try {
NativeStructureSurfaceFitter.repairVacuumFoundations(
boundedWorld, area, vacuumFoundationPlan);
boundedWorld, area, surfaceTerrainPlan);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"foundation repair", nativeStructureBatchContext(placementGroups),
@@ -210,7 +210,7 @@ public final class ModdedPlatform implements IrisPlatform {
}
EntityType<?> type = (EntityType<?>) resolved.nativeHandle();
BlockPos pos = BlockPos.containing(x, y, z);
Entity entity = type.spawn(level, pos, EntitySpawnReason.COMMAND);
Entity entity = ModdedEntitySpawner.spawnNative(type, level, pos, EntitySpawnReason.COMMAND);
return entity != null;
}
@@ -0,0 +1,132 @@
package art.arcane.iris.modded;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisEntity;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ServerChunkCache;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.Difficulty;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntitySpawnReason;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.level.chunk.LevelChunk;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ModdedEntitySpawnDifficultyTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void sharedPlatformSpawnReturnsFalseForPeacefulHostiles() {
ServerLevel level = mock(ServerLevel.class);
when(level.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
ModdedPlatform platform = new ModdedPlatform(mock(ModdedLoader.class));
assertFalse(platform.spawnEntity(new ModdedPlatformWorld(level), "minecraft:zombie", 0.5, 80, 0.5));
verify(level, never()).addFreshEntity(any(Entity.class));
verify(level, never()).enabledFeatures();
}
@Test
public void ambientSpawnReturnsNullForPeacefulHostilesBeforeApplyingConfiguration() {
ServerLevel level = mock(ServerLevel.class);
ServerChunkCache chunks = mock(ServerChunkCache.class);
when(level.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
when(level.getChunkSource()).thenReturn(chunks);
when(chunks.getChunkNow(anyInt(), anyInt())).thenReturn(mock(LevelChunk.class));
IrisEntity configured = new IrisEntity().setType("zombie").setSpawnEffectRiseOutOfGround(false);
assertNull(ModdedEntitySpawner.spawn(mock(Engine.class), configured, level, 0, 80, 0, new RNG(1L)));
verify(level, never()).addFreshEntity(any(Entity.class));
verify(level, never()).enabledFeatures();
}
@Test
public void peacefulRejectsEveryNativeForbiddenTypeBeforeCreation() {
ServerLevel level = mock(ServerLevel.class);
when(level.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
int rejected = 0;
for (EntityType<?> registered : BuiltInRegistries.ENTITY_TYPE) {
if (registered.isAllowedInPeaceful()) {
continue;
}
EntityType<?> type = spy(registered);
assertNull(ModdedEntitySpawner.spawnNative(type, level, BlockPos.ZERO, EntitySpawnReason.NATURAL));
verify(type, never()).spawn(level, BlockPos.ZERO, EntitySpawnReason.NATURAL);
rejected++;
}
assertTrue(rejected > 20);
}
@Test
public void peacefulPreservesPassiveAndNativeMonsterCategoryExceptions() {
ServerLevel level = mock(ServerLevel.class);
when(level.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
Entity created = mock(Entity.class);
for (String key : List.of("cow", "villager", "wolf", "piglin", "shulker")) {
EntityType<?> type = spy(BuiltInRegistries.ENTITY_TYPE.getValue(Identifier.parse("minecraft:" + key)));
doReturn(created).when(type).spawn(level, BlockPos.ZERO, EntitySpawnReason.NATURAL);
assertSame(key, created, ModdedEntitySpawner.spawnNative(type, level, BlockPos.ZERO, EntitySpawnReason.NATURAL));
verify(type).spawn(level, BlockPos.ZERO, EntitySpawnReason.NATURAL);
}
}
@Test
public void nonPeacefulDifficultiesPreserveHostileSpawns() {
ServerLevel level = mock(ServerLevel.class);
Entity created = mock(Entity.class);
for (Difficulty difficulty : List.of(Difficulty.EASY, Difficulty.NORMAL, Difficulty.HARD)) {
EntityType<?> type = spy(BuiltInRegistries.ENTITY_TYPE.getValue(Identifier.parse("minecraft:zombie")));
when(level.getDifficulty()).thenReturn(difficulty);
doReturn(created).when(type).spawn(level, BlockPos.ZERO, EntitySpawnReason.NATURAL);
assertSame(created, ModdedEntitySpawner.spawnNative(type, level, BlockPos.ZERO, EntitySpawnReason.NATURAL));
verify(type).spawn(level, BlockPos.ZERO, EntitySpawnReason.NATURAL);
}
}
@Test
public void unrelatedNativeSpawnFailuresStillPropagate() {
ServerLevel level = mock(ServerLevel.class);
when(level.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
EntityType<?> type = spy(BuiltInRegistries.ENTITY_TYPE.getValue(Identifier.parse("minecraft:cow")));
IllegalStateException failure = new IllegalStateException("Spawn rejected by platform");
doThrow(failure).when(type).spawn(level, BlockPos.ZERO, EntitySpawnReason.NATURAL);
assertSame(failure, assertThrows(IllegalStateException.class,
() -> ModdedEntitySpawner.spawnNative(type, level, BlockPos.ZERO, EntitySpawnReason.NATURAL)));
}
}
@@ -1,9 +1,14 @@
package art.arcane.iris.modded;
import art.arcane.iris.engine.object.IrisStructureTerrain;
import art.arcane.iris.engine.object.IrisStructureTerrainMode;
import art.arcane.iris.nativegen.NativeStructurePostProcessor;
import art.arcane.iris.nativegen.NativeStructureTerrainIntegrator;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.ChunkPos;
@@ -12,10 +17,25 @@ import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.block.Block;
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.chunk.status.ChunkStatus;
import net.minecraft.world.level.entity.EntityTypeTest;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
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.StructurePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure;
import net.minecraft.world.level.levelgen.structure.templatesystem.GravityProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.material.Fluid;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.phys.AABB;
@@ -30,11 +50,13 @@ import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class ModdedNativeStructureWorldgenAccessTest {
@@ -49,6 +71,69 @@ public class ModdedNativeStructureWorldgenAccessTest {
Bootstrap.bootStrap();
}
@Test
public void ordinaryWorldFlattenPreparationHandlesReferencedVillagePiecesBeyondEightChunks() {
int farX = (GENERATION_CENTER_X - 9) << 4;
int farZ = (GENERATION_CENTER_Z + 4) << 4;
BoundingBox bounds = new BoundingBox(farX, 64, farZ, farX + 4, 90, farZ + 4);
PoolElementStructurePiece piece = new PoolElementStructurePiece(
null, StructurePoolElement.single("minecraft:empty").apply(StructureTemplatePool.Projection.RIGID),
new BlockPos(farX, 64, farZ), 1, Rotation.NONE, bounds, LiquidSettings.APPLY_WATERLOGGING);
Structure structure = new DesertPyramidStructure(new Structure.StructureSettings(
HolderSet.empty(), Map.of(), GenerationStep.Decoration.SURFACE_STRUCTURES, TerrainAdjustment.BEARD_BOX));
StructureStart start = new StructureStart(structure, generationCenter(), 0, new PiecesContainer(List.of(piece)));
List<NativeStructureTerrainIntegrator.TerrainTarget> targets = List.of(
new NativeStructureTerrainIntegrator.TerrainTarget("minecraft:village_plains", start,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.FLATTEN)));
BoundingBox area = new BoundingBox(generationCenter().getMinBlockX(), -64, generationCenter().getMinBlockZ(),
generationCenter().getMaxBlockX(), 319, generationCenter().getMaxBlockZ());
RecordingDelegate unbounded = new RecordingDelegate();
unbounded.rejectDistantTerrainReads = true;
assertThrows(IllegalStateException.class, () -> NativeStructurePostProcessor.prepareTerrain(
unbounded.world(), area, targets, (palette, rng, x, y, z) -> Blocks.STONE.defaultBlockState()));
RecordingDelegate recording = new RecordingDelegate();
recording.rejectDistantTerrainReads = true;
ModdedNativeStructureWorldgenAccess access = access(recording);
NativeStructurePostProcessor.prepareTerrain(
access, area, targets, (palette, rng, x, y, z) -> Blocks.STONE.defaultBlockState());
assertEquals(0, recording.heightReads);
assertEquals(0, recording.chunkReads);
assertEquals(0, recording.terrainReads);
assertEquals(0, recording.mutations);
assertEquals(91, access.getHeight(Heightmap.Types.WORLD_SURFACE_WG,
generationCenter().getMinBlockX(), generationCenter().getMinBlockZ()));
assertEquals(1, recording.heightReads);
}
@Test
public void projectedTemplatePrefiltersUseRemoteTerrainAndRetainLocalHeightmaps() {
RecordingDelegate recording = new RecordingDelegate();
recording.rejectDistantTerrainReads = true;
ModdedNativeStructureWorldgenAccess access = access(recording);
GravityProcessor gravity = new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, -1);
BlockPos remote = new BlockPos((GENERATION_CENTER_X - 9) << 4, 64, (GENERATION_CENTER_Z + 4) << 4);
StructureTemplate.StructureBlockInfo raw = new StructureTemplate.StructureBlockInfo(
new BlockPos(0, 4, 0), Blocks.DIRT_PATH.defaultBlockState(), null);
StructurePlaceSettings settings = new StructurePlaceSettings().addProcessor(gravity);
StructureTemplate.StructureBlockInfo projected = StructureTemplate.processBlockInfos(
access, remote, BlockPos.ZERO, settings, List.of(raw)).getFirst();
assertEquals(remote.atY(SURFACE_FIRST_FREE_Y + 3), projected.pos());
assertEquals(0, recording.heightReads);
BlockPos local = generationCenter().getMiddleBlockPosition(64);
StructureTemplate.StructureBlockInfo localBlock = new StructureTemplate.StructureBlockInfo(
BlockPos.ZERO, Blocks.DIRT_PATH.defaultBlockState(), null);
StructureTemplate.StructureBlockInfo localProjection = StructureTemplate.processBlockInfos(
access, local, BlockPos.ZERO, settings, List.of(localBlock)).getFirst();
assertEquals(local.atY(90), localProjection.pos());
assertEquals(1, recording.heightReads);
assertEquals(0, recording.chunkReads);
}
@Test
public void distanceTwoTerrainReadsNeverReachTheDelegate() {
RecordingDelegate recording = new RecordingDelegate();
@@ -253,6 +338,7 @@ public class ModdedNativeStructureWorldgenAccessTest {
private int statusChunkReads;
private int mutations;
private int events;
private boolean rejectDistantTerrainReads;
private RecordingDelegate() {
this.biome = Holder.direct((Biome) null);
@@ -293,6 +379,11 @@ public class ModdedNativeStructureWorldgenAccessTest {
return 384;
}
if (name.equals("getHeight")) {
if (rejectDistantTerrainReads
&& (Math.abs(((int) arguments[1] >> 4) - GENERATION_CENTER_X) > 1
|| Math.abs(((int) arguments[2] >> 4) - GENERATION_CENTER_Z) > 1)) {
throw new IllegalStateException("Requested chunk unavailable during world generation");
}
heightReads++;
return 91;
}