mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 21:40:05 +00:00
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:
1 parent
4de1aea8b9
commit
62e61970f8
150 files changed
+7892
-1341
No files matched your search
+24
-27
@@ -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();
|
||||
|
||||
+11
-7
@@ -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
|
||||
|
||||
+129
-45
@@ -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) {
|
||||
|
||||
+91
@@ -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;
|
||||
}
|
||||
|
||||
+100
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+103
@@ -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)));
|
||||
}
|
||||
}
|
||||
+223
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+130
@@ -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) {
|
||||
}
|
||||
}
|
||||
+428
-5
@@ -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;
|
||||
}
|
||||
|
||||
+129
@@ -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);
|
||||
}
|
||||
}
|
||||
+1
@@ -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()
|
||||
|
||||
+34
@@ -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();
|
||||
|
||||
|
||||
+325
-25
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
@@ -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;
|
||||
}
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+42
@@ -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) {
|
||||
}
|
||||
}
|
||||
+17
-3
@@ -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);
|
||||
|
||||
+9
-1
@@ -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) {
|
||||
|
||||
+5
-7
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+132
@@ -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)));
|
||||
}
|
||||
}
|
||||
+91
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user