mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-12 03:24:14 +00:00
Expanded terrain generation tooling
- Standalone Minecraft 26.3 generation uses native registries, biome planning and structure planning with bounded concurrency - Offline export publishes fresh TERRAIN checkpoints for native server completion - Checkpoints retain mantle data, immutable generation history and captured server configuration - Output publication uses staged directories, durability checks and atomic moves - Offline distribution tasks provide an installed launcher and reusable native fixtures - Dimension settings control terrain, biome-bound and cave-density sampling with unchanged defaults - Pack validation and schemas enforce the supported sampling intervals - Biome layer caches isolate data, engine and seed identities - Floating biome palettes keep stable thickness patterns across generation orders - Hydrology batches terrain samples within the owning generation runtime - Hydrology planners share bounded basis caches and reuse raster geometry and distance samples - Demand planning admits new tiles as active plans finish - Volumetric height queries avoid unnecessary density evaluation and bound cached anchors - Cave generation reuses chunk-scoped inputs and semantic capture - Object replay indexes mutations by destination and reuses resolved records - Source-plan caches coordinate concurrent builds and reduce overlay allocation - Prerequisite carving reads preserve the underlying hydrology before object placement - Tree material decoding preserves stored native block identities - Engine shutdown drains generation outside the lifecycle lock and coordinates runtime retirement - Pregeneration sizes temporary worker boosts from processor and heap capacity - Pregeneration restores native worker counts after drain and failure paths - Native structure queries retain per-origin coordination without broad window locks - Saved biome writes drain pending batches under their write stripe - Boundary column lookups avoid repeated voxel decoding - Modded registry consumers use explicit VolmLib providers and version-specific native source overlays - Regression tests cover generation parity, cache isolation, concurrency, shutdown, persistence and offline checkpoints
This commit is contained in:
+59
-23
@@ -103,6 +103,21 @@ public class NativeStructureVolumeIndexTest {
|
||||
assertEquals(coldResolutions, resolver.resolutions());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completedOriginsRemainBoundedAndPendingBuildsAreReleased() throws Exception {
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(
|
||||
(engine, chunkX, chunkZ) -> NativeStructureVolume.NONE);
|
||||
for (int chunkX = -8_192; chunkX <= 8_192; chunkX++) {
|
||||
index.originVolumes(null, chunkX, -17);
|
||||
}
|
||||
assertEquals(16_384, cache(index, "originCache").size());
|
||||
assertTrue(cache(index, "originBuilds").isEmpty());
|
||||
index.resolve(null, -16, -16, -1, -1);
|
||||
assertEquals(16_384, cache(index, "originCache").size());
|
||||
assertTrue(cache(index, "originBuilds").isEmpty());
|
||||
assertTrue(cache(index, "queryBuilds").isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runtimeBucketsStayIsolatedAndRetirementEvictsOnlyTheirEntries() {
|
||||
AtomicInteger runtimeId = new AtomicInteger(1);
|
||||
@@ -282,7 +297,7 @@ public class NativeStructureVolumeIndexTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void overlappingColdQueriesCoordinateBeforeResolvingOrigins() throws Exception {
|
||||
public void overlappingColdQueriesResolveIndependentOriginsConcurrently() throws Exception {
|
||||
BlockingResolver resolver = new BlockingResolver();
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
@@ -291,7 +306,8 @@ public class NativeStructureVolumeIndexTest {
|
||||
assertTrue(resolver.awaitFirstEntry());
|
||||
Future<KList<NativeStructureVolume>> adjacent = executor.submit(() -> index.resolve(null, 16, 0, 31, 15));
|
||||
|
||||
assertFalse(resolver.awaitSecondEntry(250));
|
||||
assertTrue(resolver.awaitSecondEntry(5_000));
|
||||
assertTrue(adjacent.get(5, TimeUnit.SECONDS).isEmpty());
|
||||
resolver.release();
|
||||
assertTrue(first.get(5, TimeUnit.SECONDS).isEmpty());
|
||||
assertTrue(adjacent.get(5, TimeUnit.SECONDS).isEmpty());
|
||||
@@ -303,12 +319,42 @@ public class NativeStructureVolumeIndexTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contendedNativeWindowsAllowQueuedTerrainWorkToRun() throws Exception {
|
||||
public void overlappingNegativeQueriesPreserveEveryVolumeAndOriginOrder() throws Exception {
|
||||
BlockingResolver coordination = new BlockingResolver();
|
||||
NativeStructureVolumeIndex.OriginResolver volumes = (engine, chunkX, chunkZ) ->
|
||||
new KList<>(new NativeStructureVolume(chunkX + ":" + chunkZ,
|
||||
-256, 0, -256, 256, 255, 256));
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting((engine, chunkX, chunkZ) -> {
|
||||
coordination.volumesAt(engine, chunkX, chunkZ);
|
||||
return volumes.volumesAt(engine, chunkX, chunkZ);
|
||||
});
|
||||
NativeStructureVolumeIndex sequential = NativeStructureVolumeIndex.forTesting(volumes);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
Future<KList<NativeStructureVolume>> negative = executor.submit(
|
||||
() -> index.resolve(null, -16, 0, -1, 15));
|
||||
assertTrue(coordination.awaitFirstEntry());
|
||||
Future<KList<NativeStructureVolume>> positive = executor.submit(
|
||||
() -> index.resolve(null, 0, 0, 15, 15));
|
||||
assertTrue(coordination.awaitSecondEntry(5_000));
|
||||
assertEquals(sequential.resolve(null, 0, 0, 15, 15), positive.get(5, TimeUnit.SECONDS));
|
||||
coordination.release();
|
||||
assertEquals(sequential.resolve(null, -16, 0, -1, 15), negative.get(5, TimeUnit.SECONDS));
|
||||
assertEquals(306, coordination.resolutions());
|
||||
} finally {
|
||||
coordination.release();
|
||||
executor.shutdownNow();
|
||||
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sharedOriginWaitAllowsQueuedTerrainWorkToRun() throws Exception {
|
||||
assertQueuedTerrainCanComplete(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void interruptedNativeWindowWaitStillAcquiresAndReleasesOnce() throws Exception {
|
||||
public void interruptedOriginWaitPreservesInterruptAndResult() throws Exception {
|
||||
assertQueuedTerrainCanComplete(true);
|
||||
}
|
||||
|
||||
@@ -331,7 +377,7 @@ public class NativeStructureVolumeIndexTest {
|
||||
AtomicReference<Thread> secondWaiter = new AtomicReference<>();
|
||||
try {
|
||||
Future<KList<NativeStructureVolume>> original = owner.submit(
|
||||
() -> index.resolve(null, 0, 0, 15, 15));
|
||||
() -> index.originVolumes(null, -6, -8));
|
||||
assertTrue(ownerEntered.await(5, TimeUnit.SECONDS));
|
||||
Future<KList<NativeStructureVolume>> adjacent = workers.submit(() -> {
|
||||
firstWaiter.set(Thread.currentThread());
|
||||
@@ -343,15 +389,15 @@ public class NativeStructureVolumeIndexTest {
|
||||
secondWaiter.set(Thread.currentThread());
|
||||
return index.resolve(null, 32, 0, 47, 15);
|
||||
});
|
||||
awaitOriginWindowWaiter(firstWaiter);
|
||||
awaitOriginWindowWaiter(secondWaiter);
|
||||
awaitOriginWaiter(firstWaiter);
|
||||
awaitOriginWaiter(secondWaiter);
|
||||
if (interruptWaiter) {
|
||||
firstWaiter.get().interrupt();
|
||||
}
|
||||
|
||||
Future<?> terrainTask = workers.submit(() -> terrain.complete(null));
|
||||
terrainTask.get(3, TimeUnit.SECONDS);
|
||||
assertEquals(289, original.get(3, TimeUnit.SECONDS).size());
|
||||
assertEquals(1, original.get(3, TimeUnit.SECONDS).size());
|
||||
KList<NativeStructureVolume> adjacentVolumes = adjacent.get(3, TimeUnit.SECONDS);
|
||||
assertEquals(289, adjacentVolumes.size());
|
||||
assertEquals(adjacentVolumes, index.resolve(null, 16, 0, 31, 15));
|
||||
@@ -366,28 +412,28 @@ public class NativeStructureVolumeIndexTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static void awaitOriginWindowWaiter(AtomicReference<Thread> reference) {
|
||||
private static void awaitOriginWaiter(AtomicReference<Thread> reference) {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
|
||||
while (System.nanoTime() < deadline) {
|
||||
Thread thread = reference.get();
|
||||
if (thread != null && thread.getState() == Thread.State.WAITING) {
|
||||
for (StackTraceElement frame : thread.getStackTrace()) {
|
||||
if (frame.getClassName().equals(NativeStructureVolumeIndex.class.getName())
|
||||
&& frame.getMethodName().equals("lockOriginWindow")) {
|
||||
if (frame.getClassName().equals("art.arcane.volmlib.nativelib.terrain.NativeBuildFutures")
|
||||
&& frame.getMethodName().equals("awaitBuild")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
|
||||
}
|
||||
throw new AssertionError("Native query did not wait for the occupied origin window");
|
||||
throw new AssertionError("Native query did not wait for the shared origin");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void distantColdQueriesCanResolveOriginsConcurrently() throws Exception {
|
||||
BlockingResolver resolver = new BlockingResolver();
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
|
||||
int distantChunkX = findDisjointWindow(0, 0);
|
||||
int distantChunkX = 64;
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
Future<KList<NativeStructureVolume>> first = executor.submit(() -> index.resolve(null, 0, 0, 15, 15));
|
||||
@@ -478,16 +524,6 @@ public class NativeStructureVolumeIndexTest {
|
||||
);
|
||||
}
|
||||
|
||||
private static int findDisjointWindow(int chunkX, int chunkZ) {
|
||||
long sourceMask = NativeStructureVolumeIndex.originWindowStripeMask(chunkX, chunkZ);
|
||||
for (int candidate = 64; candidate < 16_384; candidate += 17) {
|
||||
if ((sourceMask & NativeStructureVolumeIndex.originWindowStripeMask(candidate, chunkZ)) == 0L) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("Unable to find a disjoint native structure window stripe");
|
||||
}
|
||||
|
||||
private static final class CountingResolver implements NativeStructureVolumeIndex.OriginResolver {
|
||||
private final AtomicInteger resolutions = new AtomicInteger();
|
||||
private final int originChunkX;
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
import art.arcane.iris.platform.bukkit.nms.BukkitBiomePolicy;
|
||||
import art.arcane.iris.platform.bukkit.nms.BukkitTerrainColumnPolicy;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeBiomeRegistryImpl;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeBiomeSourceImpl;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeTerrainColumns;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeManager;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.blending.Blender;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
final class HeadlessChunkGenerator extends ChunkGenerator {
|
||||
private final NativeTerrainColumns columns;
|
||||
private final int minimumY;
|
||||
private final int height;
|
||||
private final int seaLevel;
|
||||
|
||||
HeadlessChunkGenerator(Options options) {
|
||||
super(biomes(options));
|
||||
Engine engine = options.engine();
|
||||
columns = new NativeTerrainColumns(new BukkitTerrainColumnPolicy(engine, null));
|
||||
minimumY = engine.getMinHeight();
|
||||
height = engine.getHeight();
|
||||
seaLevel = minimumY + engine.getDimension().getFluidHeight();
|
||||
if (engine instanceof IrisEngine irisEngine) {
|
||||
irisEngine.addGenerationRuntimeRetirementListener(columns::evictRuntime);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<? extends ChunkGenerator> codec() {
|
||||
return MapCodec.unit(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getGenDepth() {
|
||||
return height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSeaLevel() {
|
||||
return seaLevel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinY() {
|
||||
return minimumY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor accessor, RandomState random) {
|
||||
return columns.baseHeight(x, z, type, accessor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor accessor, RandomState random) {
|
||||
return columns.baseColumn(x, z, accessor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ChunkAccess> buildTerrain(ChunkAccess chunk, Blender blender, RandomState random,
|
||||
StructureManager structures, BiomeManager biomes, WorldGenRegion region, Set<Holder<Biome>> possibleBiomes) {
|
||||
throw new UnsupportedOperationException("Headless terrain must be generated through the Iris region pipeline");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spawnOriginalMobs(WorldGenRegion region) {
|
||||
throw new UnsupportedOperationException("Headless native population is not installed");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addDebugScreenInfo(List<String> result, RandomState random, BlockPos position, SamplerContext context) {
|
||||
result.add("Iris native structure terrain");
|
||||
}
|
||||
|
||||
private static NativeBiomeSourceImpl biomes(Options options) {
|
||||
NativeBiomeRegistryImpl registry = new NativeBiomeRegistryImpl(options.registries().lookupOrThrow(Registries.BIOME));
|
||||
BukkitBiomePolicy<Holder<Biome>, Biome> policy = new BukkitBiomePolicy<>(
|
||||
new BukkitBiomePolicy.RuntimeOptions<>(options.engine().getSeedManager().getSeed(), options.engine(),
|
||||
null, () -> registry), registry);
|
||||
return new NativeBiomeSourceImpl(policy);
|
||||
}
|
||||
|
||||
record Options(Engine engine, RegistryAccess registries) {
|
||||
Options {
|
||||
Objects.requireNonNull(engine, "engine");
|
||||
Objects.requireNonNull(registries, "registries");
|
||||
}
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk;
|
||||
import ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray;
|
||||
import ca.spottedleaf.moonrise.patches.starlight.storage.StarlightSectionData;
|
||||
import it.unimi.dsi.fastutil.longs.LongSet;
|
||||
import it.unimi.dsi.fastutil.shorts.ShortArrayList;
|
||||
import it.unimi.dsi.fastutil.shorts.ShortList;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.world.level.chunk.DataLayer;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
import net.minecraft.world.level.chunk.PalettedContainerFactory;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkType;
|
||||
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
final class HeadlessChunkSerialization {
|
||||
private HeadlessChunkSerialization() {
|
||||
}
|
||||
|
||||
static SerializableChunkData copyOf(ProtoChunk chunk, Context context) {
|
||||
if (!chunk.canBeSerialized() || chunk.getPersistedStatus().getChunkType() != ChunkType.PROTOCHUNK) {
|
||||
throw new IllegalArgumentException("Headless serialization requires a serializable proto chunk");
|
||||
}
|
||||
List<CompoundTag> blockEntities = new ArrayList<>(chunk.getBlockEntitiesPos().size());
|
||||
for (BlockPos position : chunk.getBlockEntitiesPos()) {
|
||||
CompoundTag tag = chunk.getBlockEntityNbtForSaving(position, context.structures().registryAccess());
|
||||
if (tag != null) {
|
||||
blockEntities.add(tag.copy());
|
||||
}
|
||||
}
|
||||
List<CompoundTag> entities = new ArrayList<>(chunk.getEntities().size());
|
||||
for (CompoundTag entity : chunk.getEntities()) {
|
||||
entities.add(entity.copy());
|
||||
}
|
||||
Map<Heightmap.Types, long[]> heightmaps = new EnumMap<>(Heightmap.Types.class);
|
||||
for (Map.Entry<Heightmap.Types, Heightmap> entry : chunk.getHeightmaps()) {
|
||||
heightmaps.put(entry.getKey(), entry.getValue().getRawData().clone());
|
||||
}
|
||||
ShortList[] postProcessing = new ShortList[chunk.getPostProcessing().length];
|
||||
for (int i = 0; i < postProcessing.length; i++) {
|
||||
ShortList source = chunk.getPostProcessing()[i];
|
||||
if (source != null && !source.isEmpty()) {
|
||||
postProcessing[i] = new ShortArrayList(source);
|
||||
}
|
||||
}
|
||||
CompoundTag persistent = chunk.persistentDataContainer.isEmpty()
|
||||
? null : chunk.persistentDataContainer.toTagCompound().copy();
|
||||
return new SerializableChunkData(context.containers(), chunk.getPos(), chunk.getMinSectionY(),
|
||||
context.gameTime(), chunk.getInhabitedTime(), chunk.getPersistedStatus(),
|
||||
chunk.getBlendingData() == null ? null : chunk.getBlendingData().pack(),
|
||||
chunk.getBelowZeroRetrogen(), chunk.getUpgradeData().copy(), heightmaps,
|
||||
chunk.getTicksForSerialization(context.gameTime()), postProcessing, chunk.isLightCorrect(),
|
||||
sections(chunk), entities, blockEntities, structures(chunk, context.structures()), persistent);
|
||||
}
|
||||
|
||||
private static List<SerializableChunkData.SectionData> sections(ProtoChunk chunk) {
|
||||
LevelChunkSection[] blocks = chunk.getSections();
|
||||
StarlightChunk light = (StarlightChunk) chunk;
|
||||
SWMRNibbleArray[] blockLight = light.starlight$getBlockNibbles();
|
||||
SWMRNibbleArray[] skyLight = light.starlight$getSkyNibbles();
|
||||
List<SerializableChunkData.SectionData> sections = new ArrayList<>(blocks.length + 2);
|
||||
for (int i = 0; i < blocks.length + 2; i++) {
|
||||
LevelChunkSection blocksCopy = i > 0 && i <= blocks.length ? blocks[i - 1].copy() : null;
|
||||
SWMRNibbleArray.SaveState blockState = blockLight[i].getSaveState();
|
||||
SWMRNibbleArray.SaveState skyState = skyLight[i].getSaveState();
|
||||
if (blocksCopy == null && blockState == null && skyState == null) {
|
||||
continue;
|
||||
}
|
||||
SerializableChunkData.SectionData section = new SerializableChunkData.SectionData(
|
||||
chunk.getMinSectionY() + i - 1, blocksCopy, layer(blockState), layer(skyState));
|
||||
if (blockState != null) {
|
||||
((StarlightSectionData) (Object) section).starlight$setBlockLightState(blockState.state);
|
||||
}
|
||||
if (skyState != null) {
|
||||
((StarlightSectionData) (Object) section).starlight$setSkyLightState(skyState.state);
|
||||
}
|
||||
sections.add(section);
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
private static DataLayer layer(SWMRNibbleArray.SaveState state) {
|
||||
return state == null || state.data == null ? null : new DataLayer(state.data);
|
||||
}
|
||||
|
||||
private static CompoundTag structures(ProtoChunk chunk, StructurePieceSerializationContext context) {
|
||||
Registry<Structure> registry = context.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
CompoundTag starts = new CompoundTag();
|
||||
for (Map.Entry<Structure, StructureStart> entry : chunk.getAllStarts().entrySet()) {
|
||||
starts.put(Objects.requireNonNull(registry.getKey(entry.getKey()), "structure key").toString(),
|
||||
entry.getValue().createTag(context, chunk.getPos()));
|
||||
}
|
||||
CompoundTag references = new CompoundTag();
|
||||
for (Map.Entry<Structure, LongSet> entry : chunk.getAllReferences().entrySet()) {
|
||||
if (!entry.getValue().isEmpty()) {
|
||||
references.putLongArray(Objects.requireNonNull(registry.getKey(entry.getKey()), "structure key").toString(),
|
||||
entry.getValue().toLongArray());
|
||||
}
|
||||
}
|
||||
CompoundTag result = new CompoundTag();
|
||||
result.put("starts", starts);
|
||||
result.put("References", references);
|
||||
return result;
|
||||
}
|
||||
|
||||
record Context(StructurePieceSerializationContext structures, PalettedContainerFactory containers, long gameTime) {
|
||||
Context {
|
||||
Objects.requireNonNull(structures, "structures");
|
||||
Objects.requireNonNull(containers, "containers");
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.spi.PlatformBiomeWriter;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBiome;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
record HeadlessNativeBiomeWriter(Supplier<HolderLookup.Provider> registries) implements PlatformBiomeWriter {
|
||||
@Override
|
||||
public int biomeIdFor(String key) {
|
||||
Registry<Biome> registry = registry();
|
||||
Biome biome = registry.getOrThrow(ResourceKey.create(Registries.BIOME, Identifier.parse(key))).value();
|
||||
return registry.getId(biome);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NativeBiome> allBiomes() {
|
||||
return registry().listElements().map(holder -> (NativeBiome) ModdedBiome.of(
|
||||
holder.value(), holder.key().identifier().toString())).toList();
|
||||
}
|
||||
|
||||
private Registry<Biome> registry() {
|
||||
return (Registry<Biome>) registries.get().lookupOrThrow(Registries.BIOME);
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.component.DataComponentInitializers;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.packs.PackType;
|
||||
import net.minecraft.server.packs.repository.ServerPacksSource;
|
||||
import net.minecraft.server.packs.resources.MultiPackResourceManager;
|
||||
import net.minecraft.tags.TagLoader;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
final class HeadlessNativeBootstrap {
|
||||
private static boolean initialized;
|
||||
|
||||
private HeadlessNativeBootstrap() {
|
||||
}
|
||||
|
||||
static synchronized void bindComponents(HolderLookup.Provider registries) {
|
||||
List<DataComponentInitializers.PendingComponents<?>> pending =
|
||||
BuiltInRegistries.DATA_COMPONENT_INITIALIZERS.build(registries);
|
||||
for (DataComponentInitializers.PendingComponents<?> components : pending) {
|
||||
components.apply();
|
||||
}
|
||||
}
|
||||
|
||||
static synchronized void initialize() {
|
||||
if (initialized) {
|
||||
return;
|
||||
}
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
try (MultiPackResourceManager resources = new MultiPackResourceManager(PackType.SERVER_DATA,
|
||||
List.of(ServerPacksSource.createVanillaPackSource().fullResources()))) {
|
||||
List<Registry.PendingTags<?>> pending = TagLoader.loadTagsForExistingRegistries(resources,
|
||||
RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY));
|
||||
for (Registry.PendingTags<?> tags : pending) {
|
||||
tags.apply();
|
||||
}
|
||||
}
|
||||
initialized = true;
|
||||
}
|
||||
}
|
||||
+273
@@ -0,0 +1,273 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.platform.bukkit.nms.BukkitGenerationRegistry;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.PlatformGenerationRegistry;
|
||||
import art.arcane.iris.spi.LogLevel;
|
||||
import art.arcane.iris.spi.PlatformBiomeWriter;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.spi.PlatformScheduler;
|
||||
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||
import art.arcane.volmlib.nativelib.entity.NativeEntityType;
|
||||
import art.arcane.volmlib.nativelib.item.NativeItem;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockResolver;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeRegistryAccess;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeGenerationRegistryImpl;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeWorldGenerationImpl;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockProperty;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
final class HeadlessNativePlatform implements IrisPlatform, PlatformRegistries, NativeBlockResolver.Policy {
|
||||
private final StubPlatform services;
|
||||
private final NativeRegistryAccess nativeAccess;
|
||||
private final PlatformBiomeWriter biomeWriter;
|
||||
private final PlatformGenerationRegistry generationRegistry;
|
||||
private final NativeBlockResolver blocks = new NativeBlockResolver(this);
|
||||
private final ConcurrentHashMap<String, NativeBlockState> states = new ConcurrentHashMap<>();
|
||||
|
||||
HeadlessNativePlatform(File root, Supplier<HolderLookup.Provider> registries) {
|
||||
services = new StubPlatform(root);
|
||||
biomeWriter = new HeadlessNativeBiomeWriter(registries);
|
||||
generationRegistry = new BukkitGenerationRegistry(new NativeGenerationRegistryImpl(() -> {
|
||||
HolderLookup.Provider lookup = registries.get();
|
||||
if (lookup instanceof RegistryAccess access) {
|
||||
return access;
|
||||
}
|
||||
throw new IllegalStateException("Generation registry contract requires loaded native registries");
|
||||
}), "bukkit-generation-registry-v1", new NativeWorldGenerationImpl().generationRendererIdentity());
|
||||
nativeAccess = new NativeRegistryAccess(new NativeRegistryAccess.Configuration(registries,
|
||||
() -> { throw new UnsupportedOperationException("Headless reloadable registries are not loaded"); },
|
||||
message -> { throw new IllegalStateException("Unavailable native registry: " + message); }));
|
||||
}
|
||||
|
||||
NativeBlockResolver blocks() {
|
||||
return blocks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformRegistries registries() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformGenerationRegistry generationRegistry() {
|
||||
return generationRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String platformName() {
|
||||
return "headless-native-core";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String minecraftVersion() {
|
||||
return SharedConstants.getCurrentVersion().name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState decodeBlockState(String key) {
|
||||
return states.computeIfAbsent(key, NativeBlockResolver::strictParse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState block(String key) {
|
||||
return states.computeIfAbsent(key, candidate -> Objects.requireNonNull(blocks.getOrNull(candidate, true),
|
||||
"Unresolved native block: " + candidate));
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState blockOrNull(String key) {
|
||||
return blocks.getOrNull(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState blockOrNull(String key, boolean warn) {
|
||||
return blocks.getOrNull(key, warn);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState air() {
|
||||
return blocks.getAir();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModdedBlockState resolveCustomBlock(String key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean preventLeafDecay() {
|
||||
return IrisSettings.get().getGenerator().isPreventLeafDecay();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportError(String key, Throwable failure) {
|
||||
services.reportError(failure);
|
||||
failure.printStackTrace(System.err);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warnUnresolved(String key, String message) {
|
||||
throw new IllegalArgumentException(message + ": " + key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void debug(String message) {
|
||||
services.log(LogLevel.DEBUG, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState deepSlateOre(NativeBlockState block, NativeBlockState ore) {
|
||||
return nativeAccess.deepSlateOre(block, ore);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBiome biome(String key) {
|
||||
return nativeAccess.biome(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeItem item(String key) {
|
||||
return nativeAccess.item(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeEntityType entity(String key) {
|
||||
return nativeAccess.entity(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> blockKeys() {
|
||||
return nativeAccess.blockKeys();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> biomeKeys() {
|
||||
return nativeAccess.biomeKeys();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> structureKeys() {
|
||||
return nativeAccess.structureKeys();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> itemKeys() {
|
||||
return nativeAccess.itemKeys();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> entityKeys() {
|
||||
return nativeAccess.entityKeys();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> enchantmentKeys() {
|
||||
return nativeAccess.enchantmentKeys();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> potionEffectKeys() {
|
||||
return nativeAccess.potionEffectKeys();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> lootTableKeys() {
|
||||
return nativeAccess.lootTableKeys();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<NativeBlockProperty>> blockStateProperties() {
|
||||
return nativeAccess.blockStateProperties();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> blockTypeKeys() {
|
||||
return blockKeys();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformScheduler scheduler() {
|
||||
return services.scheduler();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformStructureHooks structureHooks() {
|
||||
return services.structureHooks();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBiomeWriter biomeWriter() {
|
||||
return biomeWriter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File dataFolder() {
|
||||
return services.dataFolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public File dataFile(String... path) {
|
||||
return services.dataFile(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public File pluginJar() {
|
||||
return services.pluginJar();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int irisVersionNumber() {
|
||||
return services.irisVersionNumber();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int minecraftVersionNumber() {
|
||||
return Integer.parseInt(minecraftVersion().replace(".", ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void callEvent(Object event) {
|
||||
services.callEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispatchConsoleCommand(String command) {
|
||||
services.dispatchConsoleCommand(command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean spawnEntity(NativeWorld world, String entityKey, double x, double y, double z) {
|
||||
return services.spawnEntity(world, entityKey, x, y, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(LogLevel level, String message) {
|
||||
services.log(level, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void msg(String message) {
|
||||
services.msg(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportError(Throwable error) {
|
||||
services.reportError(error);
|
||||
}
|
||||
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.pack.datapack.IrisDatapackCompiler;
|
||||
import art.arcane.iris.pack.datapack.v263.DataFixerV263;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.RegistryDataLoader;
|
||||
import net.minecraft.server.packs.PackLocationInfo;
|
||||
import net.minecraft.server.packs.PackType;
|
||||
import net.minecraft.server.packs.PathPackResources;
|
||||
import net.minecraft.server.packs.repository.PackSource;
|
||||
import net.minecraft.server.packs.repository.ServerPacksSource;
|
||||
import net.minecraft.server.packs.resources.MultiPackResourceManager;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
final class HeadlessNativeRegistries implements AutoCloseable {
|
||||
private final Path directory;
|
||||
private final MultiPackResourceManager resources;
|
||||
private final RegistryAccess.Frozen registries;
|
||||
|
||||
private HeadlessNativeRegistries(Path directory, MultiPackResourceManager resources, RegistryAccess.Frozen registries) {
|
||||
this.directory = directory;
|
||||
this.resources = resources;
|
||||
this.registries = registries;
|
||||
}
|
||||
|
||||
static HeadlessNativeRegistries load(Map<String, String> definitions) throws IOException {
|
||||
HeadlessNativeBootstrap.initialize();
|
||||
Path directory = Files.createTempDirectory("iris-native-registry-");
|
||||
try {
|
||||
for (Map.Entry<String, String> definition : definitions.entrySet()) {
|
||||
Path file = directory.resolve(definition.getKey()).normalize();
|
||||
if (!file.startsWith(directory.resolve("data")) || !file.toString().endsWith(".json")) {
|
||||
throw new IllegalArgumentException("Invalid generated registry resource " + definition.getKey());
|
||||
}
|
||||
Files.createDirectories(file.getParent());
|
||||
Files.writeString(file, definition.getValue());
|
||||
}
|
||||
return load(directory);
|
||||
} catch (IOException | RuntimeException failure) {
|
||||
delete(directory);
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
static HeadlessNativeRegistries compile(File pack) throws IOException {
|
||||
HeadlessNativeBootstrap.initialize();
|
||||
Path directory = Files.createTempDirectory("iris-native-pack-");
|
||||
try {
|
||||
IrisDatapackCompiler.compile(List.of(pack), new KList<>(directory.toFile()), List.of(),
|
||||
new DataFixerV263(), false);
|
||||
return load(directory);
|
||||
} catch (IOException | RuntimeException failure) {
|
||||
delete(directory);
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
private static HeadlessNativeRegistries load(Path directory) {
|
||||
PackLocationInfo location = new PackLocationInfo("iris-headless", Component.literal("Iris"),
|
||||
PackSource.WORLD, Optional.empty());
|
||||
MultiPackResourceManager resources = new MultiPackResourceManager(PackType.SERVER_DATA, List.of(
|
||||
ServerPacksSource.createVanillaPackSource().fullResources(), new PathPackResources(location, directory)));
|
||||
try {
|
||||
RegistryAccess.Frozen builtins = RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY);
|
||||
RegistryAccess.Frozen loaded;
|
||||
try (ExecutorService executor = Executors.newFixedThreadPool(4)) {
|
||||
loaded = RegistryDataLoader.load(resources, builtins.listRegistries().toList(),
|
||||
RegistryDataLoader.WORLD_REGISTRIES, executor).join();
|
||||
}
|
||||
RegistryAccess.Frozen registries = new RegistryAccess.ImmutableRegistryAccess(
|
||||
Stream.concat(builtins.registries(), loaded.registries())).freeze();
|
||||
HeadlessNativeBootstrap.bindComponents(registries);
|
||||
return new HeadlessNativeRegistries(directory, resources, registries);
|
||||
} catch (RuntimeException failure) {
|
||||
resources.close();
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
RegistryAccess.Frozen registries() {
|
||||
return registries;
|
||||
}
|
||||
|
||||
MultiPackResourceManager resources() {
|
||||
return resources;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
resources.close();
|
||||
delete(directory);
|
||||
}
|
||||
|
||||
private static void delete(Path directory) throws IOException {
|
||||
try (Stream<Path> paths = Files.walk(directory)) {
|
||||
for (Path path : paths.sorted(Comparator.reverseOrder()).toList()) {
|
||||
Files.delete(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.block.BlockDataMergeSupport;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureVolumeIndex;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.generation.decoration.DecoratorPlatformHooks;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.modded.ModdedDecoratorHooks;
|
||||
import art.arcane.iris.modded.ModdedStateRotator;
|
||||
import art.arcane.iris.modded.ModdedTileData;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeTileReader;
|
||||
import art.arcane.iris.pack.datapack.v263.DataFixerV263;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.world.history.GenerationHistory;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.structure.object.IrisObjectRotation;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeStateMerger;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.data.registries.VanillaRegistries;
|
||||
import net.minecraft.server.packs.resources.ResourceManager;
|
||||
|
||||
final class HeadlessNativeRuntime implements RealPackProbeSupport.RuntimeBindings {
|
||||
private NativeRegionTerrainWriter writer;
|
||||
private HeadlessNativeRegistries nativeRegistries;
|
||||
private HolderLookup.Provider registries;
|
||||
|
||||
@Override
|
||||
public IrisPlatform create(File root) {
|
||||
HeadlessNativeBootstrap.initialize();
|
||||
registries = VanillaRegistries.createWorldLookup();
|
||||
HeadlessNativePlatform platform = new HeadlessNativePlatform(root, () -> registries);
|
||||
ModdedDecoratorHooks decorators = new ModdedDecoratorHooks();
|
||||
DecoratorPlatformHooks.bind(decorators, decorators);
|
||||
IrisObjectRotation.bindPlatformRotator(new ModdedStateRotator());
|
||||
BlockDataMergeSupport.bindPlatformMerger(new NativeStateMerger(platform.blocks())::merge);
|
||||
NativeTileReader tiles = new NativeTileReader(() -> registries);
|
||||
TileData.bindPlatformReader(input -> ModdedTileData.wrap(tiles.read(input)));
|
||||
TileData.bindPlatformFactory(ModdedTileData::fromProperties);
|
||||
return platform;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare(IrisData data, IrisDimension dimension) {
|
||||
try {
|
||||
if (nativeRegistries != null) {
|
||||
throw new IllegalStateException("Headless native runtime supports one immutable pack per process");
|
||||
}
|
||||
nativeRegistries = HeadlessNativeRegistries.compile(data.getDataFolder());
|
||||
registries = nativeRegistries.registries();
|
||||
writer = new NativeRegionTerrainWriter(nativeRegistries.registries());
|
||||
} catch (IOException failure) {
|
||||
throw new UncheckedIOException("Cannot load native generation registries", failure);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public KList<NativeStructureVolume> nativeStructureVolumes(Engine engine, int minX, int minZ, int maxX, int maxZ) {
|
||||
return NativeStructureVolumeIndex.volumes(engine, minX, minZ, maxX, maxZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GenerationHistory createHistory(RealPackProbeSupport.HistoryRequest request) throws IOException {
|
||||
return HeadlessGenerationHistorySession.create(request, new DataFixerV263());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (nativeRegistries != null) {
|
||||
nativeRegistries.close();
|
||||
nativeRegistries = null;
|
||||
}
|
||||
}
|
||||
|
||||
RegistryAccess.Frozen registries() {
|
||||
return nativeRegistries.registries();
|
||||
}
|
||||
|
||||
ResourceManager resources() {
|
||||
return nativeRegistries.resources();
|
||||
}
|
||||
|
||||
NativeRegionTerrainWriter writer() {
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.context.IrisContext;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
||||
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
|
||||
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.iris.world.history.NativeTerrainReceipt;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBiomeSourcePolicy;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeTerrainPipeline;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.WorldgenTerrainHeightmaps;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
final class HeadlessRegionTerrain {
|
||||
private static final NamespacedKey STRUCTURE_ACTIVATION = new NamespacedKey("iris", "structure_activation");
|
||||
private static final NamespacedKey NATURAL_TERRAIN = new NamespacedKey("iris", "natural_terrain");
|
||||
|
||||
private HeadlessRegionTerrain() {
|
||||
}
|
||||
|
||||
static final class Session {
|
||||
private final Map<Long, HeadlessStructurePlanner.PlannedChunk<NativeStructureOwnershipRecord>> chunks = new HashMap<>();
|
||||
private final RegionGenerationWindow.Policy workerPolicy;
|
||||
private HeadlessTerrainContext context;
|
||||
|
||||
Session(RegionGenerationWindow.Policy workerPolicy) {
|
||||
this.workerPolicy = Objects.requireNonNull(workerPolicy, "workerPolicy");
|
||||
}
|
||||
|
||||
Result generate(Request request, Consumer<String> progress) throws Exception {
|
||||
if (context != null && context != request.context()) {
|
||||
throw new IllegalArgumentException("Rolling regions require one native terrain context");
|
||||
}
|
||||
context = request.context();
|
||||
List<NativeRegionPlan.PlannedChunk> plan = NativeRegionPlan.plan(
|
||||
request.chunkX(), request.chunkZ(), request.width());
|
||||
Set<Long> retained = new HashSet<>(plan.size());
|
||||
for (NativeRegionPlan.PlannedChunk planned : plan) {
|
||||
retained.add(ChunkPos.pack(planned.x(), planned.z()));
|
||||
}
|
||||
chunks.keySet().retainAll(retained);
|
||||
Map<Long, ChunkStatus> previous = new HashMap<>(chunks.size());
|
||||
for (Map.Entry<Long, HeadlessStructurePlanner.PlannedChunk<NativeStructureOwnershipRecord>> entry : chunks.entrySet()) {
|
||||
previous.put(entry.getKey(), entry.getValue().chunk().getPersistedStatus());
|
||||
}
|
||||
for (ChunkStatus status : List.of(ChunkStatus.STRUCTURE_STARTS, ChunkStatus.STRUCTURE_REFERENCES,
|
||||
ChunkStatus.BIOMES, ChunkStatus.TERRAIN)) {
|
||||
List<NativeRegionPlan.PlannedChunk> eligible = new ArrayList<>();
|
||||
for (NativeRegionPlan.PlannedChunk planned : plan) {
|
||||
HeadlessStructurePlanner.PlannedChunk<NativeStructureOwnershipRecord> existing =
|
||||
chunks.get(ChunkPos.pack(planned.x(), planned.z()));
|
||||
if (planned.status().isOrAfter(status)
|
||||
&& (status != ChunkStatus.BIOMES || planned.status() == ChunkStatus.BIOMES)
|
||||
&& (existing == null || existing.chunk().getPersistedStatus().isBefore(status))) {
|
||||
eligible.add(planned);
|
||||
}
|
||||
}
|
||||
if (eligible.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
long started = System.nanoTime();
|
||||
AtomicInteger completed = new AtomicInteger();
|
||||
RegionGenerationWindow.process(new RegionGenerationWindow.Request<>(eligible.size(), request.parallelism(),
|
||||
index -> apply(request.context(), eligible.get(index), status, chunks),
|
||||
(index, result) -> {
|
||||
if (status == ChunkStatus.STRUCTURE_STARTS) {
|
||||
chunks.put(result.chunk().getPos().pack(), result);
|
||||
}
|
||||
int count = completed.incrementAndGet();
|
||||
if (count % 128 == 0 || count == eligible.size()) {
|
||||
progress.accept("stage=" + status.getName() + " chunks=" + count + "/" + eligible.size()
|
||||
+ " seconds=" + (System.nanoTime() - started) / 1_000_000_000.0);
|
||||
}
|
||||
}), workerPolicy);
|
||||
failReported();
|
||||
}
|
||||
List<ProtoChunk> ordered = new ArrayList<>(plan.size());
|
||||
List<ProtoChunk> updated = new ArrayList<>();
|
||||
for (NativeRegionPlan.PlannedChunk planned : plan) {
|
||||
ProtoChunk chunk = chunks.get(ChunkPos.pack(planned.x(), planned.z())).chunk();
|
||||
if (chunk.getPersistedStatus().isBefore(planned.status())) {
|
||||
throw new IllegalStateException("Incomplete native stage at " + chunk.getPos());
|
||||
}
|
||||
ordered.add(chunk);
|
||||
ChunkStatus prior = previous.get(chunk.getPos().pack());
|
||||
if (prior == null || chunk.getPersistedStatus().isAfter(prior)) {
|
||||
updated.add(chunk);
|
||||
}
|
||||
}
|
||||
return new Result(List.copyOf(ordered), List.copyOf(updated), request.width() * request.width());
|
||||
}
|
||||
}
|
||||
|
||||
private static HeadlessStructurePlanner.PlannedChunk<NativeStructureOwnershipRecord> apply(
|
||||
HeadlessTerrainContext context, NativeRegionPlan.PlannedChunk planned, ChunkStatus status,
|
||||
Map<Long, HeadlessStructurePlanner.PlannedChunk<NativeStructureOwnershipRecord>> chunks) throws Exception {
|
||||
Engine engine = context.engine();
|
||||
GenerationHistoryRuntimeRouter router = ((IrisEngine) engine).getGenerationHistoryRuntimeRouter().orElseThrow();
|
||||
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = router.openRoute(planned.x(), planned.z());
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope scope = route.openRuntimeScope();
|
||||
GenerationSessionLease lease = engine.acquireGenerationLease("headless_native_terrain");
|
||||
IrisContext.Scope generation = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (status == ChunkStatus.STRUCTURE_STARTS) {
|
||||
HeadlessStructurePlanner.PlannedChunk<NativeStructureOwnershipRecord> result = HeadlessStructurePlanner.plan(
|
||||
context.structures(), context.policies(), new ChunkPos(planned.x(), planned.z()));
|
||||
result.chunk().persistentDataContainer.set(STRUCTURE_ACTIVATION,
|
||||
PersistentDataType.LONG, route.activation().activationId());
|
||||
return result;
|
||||
}
|
||||
HeadlessStructurePlanner.PlannedChunk<NativeStructureOwnershipRecord> result = Objects.requireNonNull(
|
||||
chunks.get(ChunkPos.pack(planned.x(), planned.z())), "planned structure chunk");
|
||||
if (status == ChunkStatus.STRUCTURE_REFERENCES) {
|
||||
HeadlessStructurePlanner.references(context.policies(), result, chunks);
|
||||
} else if (status == ChunkStatus.BIOMES) {
|
||||
biomes(context, result.chunk());
|
||||
} else if (status == ChunkStatus.TERRAIN) {
|
||||
if (result.chunk().getPersistedStatus().isBefore(ChunkStatus.BIOMES)) {
|
||||
biomes(context, result.chunk());
|
||||
}
|
||||
terrain(context, result.chunk(), route);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported headless native stage " + status);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private static void biomes(HeadlessTerrainContext context, ProtoChunk chunk) {
|
||||
context.biomes().prepareVisibleBiomeBatch();
|
||||
NativeBiomeSourcePolicy.VisibleResolver<Holder<Biome>> resolver = context.biomes().visibleResolver();
|
||||
chunk.fillBiomesFromNoise(resolver::biome);
|
||||
chunk.setPersistedStatus(ChunkStatus.BIOMES);
|
||||
}
|
||||
|
||||
private static void terrain(HeadlessTerrainContext context, ProtoChunk target,
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route) throws Exception {
|
||||
Engine engine = context.engine();
|
||||
ChunkPos position = target.getPos();
|
||||
Hunk<NativeBlockState> blocks = Hunk.newArrayHunk(16, engine.getHeight(), 16);
|
||||
Hunk<NativeBiome> biomes = Hunk.newArrayHunk(16, engine.getHeight(), 16);
|
||||
engine.generate(position.x() << 4, position.z() << 4, blocks, biomes, true);
|
||||
context.writer().fill(target, new NativeRegionTerrainWriter.TerrainInput(position.x(), position.z(),
|
||||
engine.getMinHeight(), engine.getHeight(),
|
||||
(x, y, z) -> blockState(blocks.getRaw(x, y, z)),
|
||||
(x, y, z) -> Objects.requireNonNull(biomes.getRaw(x, y, z), "Generated native biome").key(), List.of()));
|
||||
NativeTerrainPipeline.claimGeneratedSemantics(route, target, engine.getMinHeight());
|
||||
target.persistentDataContainer.set(NATURAL_TERRAIN, PersistentDataType.BYTE_ARRAY,
|
||||
NativeTerrainReceipt.encode(route.naturalTerrain().orElseThrow(),
|
||||
route.activation().activationId(), route.epoch().epochId()));
|
||||
WorldgenTerrainHeightmaps.primeTerrain(target,
|
||||
(x, z) -> engine.getHeight(x, z, false) + engine.getMinHeight() + 1,
|
||||
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight() + 1);
|
||||
Heightmap.primeHeightmaps(target, EnumSet.of(Heightmap.Types.MOTION_BLOCKING,
|
||||
Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, Heightmap.Types.OCEAN_FLOOR, Heightmap.Types.WORLD_SURFACE));
|
||||
target.setPersistedStatus(ChunkStatus.TERRAIN);
|
||||
target.markUnsaved();
|
||||
}
|
||||
|
||||
private static BlockState blockState(NativeBlockState state) {
|
||||
return state == null ? Blocks.AIR.defaultBlockState() : ((ModdedBlockState) state).handle();
|
||||
}
|
||||
|
||||
private static void failReported() {
|
||||
List<Throwable> reports = RealPackProbeSupport.drainReported();
|
||||
if (!reports.isEmpty()) {
|
||||
IllegalStateException failure = new IllegalStateException("Native terrain generation reported errors");
|
||||
for (Throwable report : reports) {
|
||||
failure.addSuppressed(report);
|
||||
}
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
record Request(HeadlessTerrainContext context, int chunkX, int chunkZ, int width, int parallelism) {
|
||||
Request {
|
||||
Objects.requireNonNull(context, "context");
|
||||
if (parallelism < 1 || parallelism > 32) {
|
||||
throw new IllegalArgumentException("Headless native parallelism must be 1..32");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
record Result(List<ProtoChunk> chunks, List<ProtoChunk> updated, int targetChunks) {
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.Level;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.InvalidConfigurationException;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.spigotmc.SpigotConfig;
|
||||
import org.spigotmc.SpigotWorldConfig;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Objects;
|
||||
|
||||
final class HeadlessStructureConfiguration {
|
||||
private static final int CONFIG_VERSION = 13;
|
||||
private static final Field CONFIG_FILE = nativeField("CONFIG_FILE");
|
||||
private static final Field VERSION = nativeField("version");
|
||||
|
||||
private HeadlessStructureConfiguration() {
|
||||
}
|
||||
|
||||
static SpigotWorldConfig load(Options options) throws IOException {
|
||||
if (Bukkit.getServer() != null) {
|
||||
throw new IllegalStateException("Headless structure configuration cannot initialize inside a Bukkit server");
|
||||
}
|
||||
Files.createDirectories(options.stagingDirectory());
|
||||
Path staged = Files.createTempFile(options.stagingDirectory(), "spigot-structures-", ".yml");
|
||||
try {
|
||||
Files.copy(options.source(), staged, StandardCopyOption.REPLACE_EXISTING);
|
||||
YamlConfiguration configuration = new YamlConfiguration();
|
||||
configuration.load(staged.toFile());
|
||||
configuration.options().copyDefaults(true);
|
||||
int version = configuration.getInt("config-version", CONFIG_VERSION);
|
||||
String worldName = options.levelKey().identifier().toString();
|
||||
prepareEnvironment(configuration, options.legacyWorldName(), worldName, version);
|
||||
configuration.set("config-version", CONFIG_VERSION);
|
||||
return initialize(options, staged, configuration);
|
||||
} catch (InvalidConfigurationException | ReflectiveOperationException | RuntimeException failure) {
|
||||
throw new IOException("Could not initialize native structure configuration from " + options.source(), failure);
|
||||
} finally {
|
||||
Files.deleteIfExists(staged);
|
||||
}
|
||||
}
|
||||
|
||||
private static void prepareEnvironment(YamlConfiguration configuration, String legacyWorldName,
|
||||
String worldName, int version) {
|
||||
if (version <= 12) {
|
||||
ConfigurationSection legacy = configuration.getConfigurationSection("world-settings." + legacyWorldName);
|
||||
if (legacy != null) {
|
||||
configuration.set("world-settings." + legacyWorldName, null);
|
||||
configuration.set("world-settings." + worldName, legacy);
|
||||
}
|
||||
}
|
||||
String worldPath = "world-settings." + worldName + ".";
|
||||
configuration.set(worldPath + "verbose", false);
|
||||
configuration.set(worldPath + "view-distance", 3);
|
||||
configuration.set(worldPath + "simulation-distance", 3);
|
||||
}
|
||||
|
||||
private static SpigotWorldConfig initialize(Options options, Path staged, YamlConfiguration configuration)
|
||||
throws ReflectiveOperationException {
|
||||
synchronized (SpigotConfig.class) {
|
||||
YamlConfiguration previousConfiguration = SpigotConfig.config;
|
||||
File previousFile = (File) CONFIG_FILE.get(null);
|
||||
int previousVersion = VERSION.getInt(null);
|
||||
try {
|
||||
SpigotConfig.config = configuration;
|
||||
CONFIG_FILE.set(null, staged.toFile());
|
||||
VERSION.setInt(null, CONFIG_VERSION);
|
||||
return new SpigotWorldConfig(options.legacyWorldName(), Key.key(options.levelKey().identifier().toString()));
|
||||
} finally {
|
||||
SpigotConfig.config = previousConfiguration;
|
||||
CONFIG_FILE.set(null, previousFile);
|
||||
VERSION.setInt(null, previousVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Field nativeField(String name) {
|
||||
try {
|
||||
Field field = SpigotConfig.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
return field;
|
||||
} catch (ReflectiveOperationException failure) {
|
||||
throw new ExceptionInInitializerError(failure);
|
||||
}
|
||||
}
|
||||
|
||||
record Options(Path source, String legacyWorldName, ResourceKey<Level> levelKey, Path stagingDirectory) {
|
||||
Options {
|
||||
Objects.requireNonNull(source, "source");
|
||||
Objects.requireNonNull(legacyWorldName, "legacyWorldName");
|
||||
Objects.requireNonNull(levelKey, "levelKey");
|
||||
Objects.requireNonNull(stagingDirectory, "stagingDirectory");
|
||||
}
|
||||
}
|
||||
}
|
||||
+333
@@ -0,0 +1,333 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeGenerationScope;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.StructureInjectionPolicy;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.StructureOwnershipRecordView;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.StructurePlacementDecision;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.StructureReferencePolicy;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.StructureStartPlan;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.StructureTerrainSettings;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureFactory;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureOwnershipFingerprint;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureReferenceEnvelope;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureReferenceRepair;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureTerrainIntegrator;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureVegetationClearer;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureVerticalPlacer;
|
||||
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||
import it.unimi.dsi.fastutil.longs.LongSet;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.biome.Climate;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.PalettedContainerFactory;
|
||||
import net.minecraft.world.level.chunk.UpgradeData;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.LegacyRandomSource;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.WorldgenRandom;
|
||||
import net.minecraft.world.level.levelgen.WorldOptions;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
final class HeadlessStructurePlanner {
|
||||
private HeadlessStructurePlanner() {
|
||||
}
|
||||
|
||||
static <P extends StructureStartPlan, O extends StructureOwnershipRecordView<O>> PlannedChunk<O> plan(
|
||||
NativeContext context, Policies<P, O> policies, ChunkPos position) {
|
||||
ProtoChunk chunk = new ProtoChunk(position, UpgradeData.EMPTY,
|
||||
LevelHeightAccessor.create(context.generator().getMinY(), context.generator().getGenDepth()),
|
||||
PalettedContainerFactory.create(context.registries()), null);
|
||||
if (!context.enabled()) {
|
||||
chunk.setPersistedStatus(ChunkStatus.STRUCTURE_STARTS);
|
||||
return new PlannedChunk<O>(context, chunk, Map.of(), Map.of());
|
||||
}
|
||||
try (NativeGenerationScope scope = policies.references().openOriginScope(chunk.getPos().x(), chunk.getPos().z())) {
|
||||
generateNaturalStarts(context, chunk);
|
||||
Map<Structure, O> ownership = inject(context, policies, chunk);
|
||||
Map<Structure, StructureTerrainSettings> terrain = adjust(context, policies, chunk, ownership);
|
||||
chunk.setPersistedStatus(ChunkStatus.STRUCTURE_STARTS);
|
||||
return new PlannedChunk<>(context, chunk, Map.copyOf(ownership), Map.copyOf(terrain));
|
||||
}
|
||||
}
|
||||
|
||||
static <P extends StructureStartPlan, O extends StructureOwnershipRecordView<O>> void references(
|
||||
Policies<P, O> policies, PlannedChunk<O> target, Map<Long, PlannedChunk<O>> grid) {
|
||||
ChunkPos position = target.chunk().getPos();
|
||||
List<PlannedChunk<O>> origins = new ArrayList<>(289);
|
||||
if (target.chunk().getPersistedStatus() != ChunkStatus.STRUCTURE_STARTS) {
|
||||
throw new IllegalArgumentException("Structure references require an unreferenced planned chunk: " + position);
|
||||
}
|
||||
if (!target.context().adapterEnabled()) {
|
||||
target.chunk().setPersistedStatus(ChunkStatus.STRUCTURE_REFERENCES);
|
||||
return;
|
||||
}
|
||||
for (int x = position.x() - 8; x <= position.x() + 8; x++) {
|
||||
for (int z = position.z() - 8; z <= position.z() + 8; z++) {
|
||||
PlannedChunk<O> origin = grid.get(ChunkPos.pack(x, z));
|
||||
if (origin == null || origin.context() != target.context()
|
||||
|| origin.chunk().getPos().x() != x || origin.chunk().getPos().z() != z
|
||||
|| !origin.chunk().getPersistedStatus().isOrAfter(ChunkStatus.STRUCTURE_STARTS)) {
|
||||
throw new IllegalArgumentException("Missing matching structure origin at " + x + "," + z);
|
||||
}
|
||||
origins.add(origin);
|
||||
}
|
||||
}
|
||||
Map<Structure, LongSet> references = new HashMap<>();
|
||||
Registry<Structure> registry = target.context().registries().lookupOrThrow(Registries.STRUCTURE);
|
||||
for (PlannedChunk<O> origin : origins) {
|
||||
ChunkPos source = origin.chunk().getPos();
|
||||
try (NativeGenerationScope scope = policies.references().openOriginScope(source.x(), source.z())) {
|
||||
for (Map.Entry<Structure, StructureStart> entry : origin.chunk().getAllStarts().entrySet()) {
|
||||
StructureStart start = entry.getValue();
|
||||
if (!start.isValid()) {
|
||||
continue;
|
||||
}
|
||||
String key = Objects.requireNonNull(registry.getKey(entry.getKey()), "structure key").toString();
|
||||
if (!NativeStructureReferenceEnvelope.contentFitsReferenceRange(start)) {
|
||||
throw new IllegalStateException("Planned structure exceeds its reference range: " + key);
|
||||
}
|
||||
O ownership = origin.ownership().get(entry.getKey());
|
||||
boolean relevant = ownership == null
|
||||
? NativeStructureReferenceRepair.requiresNaturalReference(policies.references(), position,
|
||||
key, entry.getKey(), start)
|
||||
: NativeStructureReferenceRepair.requiresReference(position, key, start, ownership);
|
||||
if (relevant) {
|
||||
references.computeIfAbsent(entry.getKey(), ignored -> new LongOpenHashSet()).add(source.pack());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
target.chunk().setAllReferences(references);
|
||||
target.chunk().setPersistedStatus(ChunkStatus.STRUCTURE_REFERENCES);
|
||||
}
|
||||
|
||||
private static <P extends StructureStartPlan, O extends StructureOwnershipRecordView<O>> Map<Structure, O> inject(
|
||||
NativeContext context, Policies<P, O> policies, ProtoChunk chunk) {
|
||||
Registry<Structure> registry = context.registries().lookupOrThrow(Registries.STRUCTURE);
|
||||
Map<Structure, O> ownership = new LinkedHashMap<>();
|
||||
for (P plan : policies.injection().plansAt(chunk.getPos().x(), chunk.getPos().z())) {
|
||||
Holder.Reference<Structure> holder = registry.getOrThrow(ResourceKey.create(Registries.STRUCTURE,
|
||||
Identifier.parse(plan.structureKey())));
|
||||
Structure structure = holder.value();
|
||||
if (ownership.containsKey(structure)) {
|
||||
policies.injection().duplicate(plan.structureKey());
|
||||
continue;
|
||||
}
|
||||
StructureStart existing = chunk.getStartForStructure(structure);
|
||||
boolean replace = plan.replacesSource() || existing != null && existing.isValid()
|
||||
&& policies.injection().sourceReplaced(plan.structureKey(),
|
||||
NativeStructureVegetationClearer.isUndergroundStep(structure.step()));
|
||||
if (!replace && existing != null && existing.isValid()) {
|
||||
continue;
|
||||
}
|
||||
int references = existing != null && existing.isValid() ? existing.getReferences() : 0;
|
||||
StructureStart generated = NativeStructureFactory.generate(new NativeStructureFactory.GenerationContext(
|
||||
context.registries(), context.generator(), context.generator().getBiomeSource(),
|
||||
context.structureState().randomState(), context.templates(), context.structureState().getLevelSeed(),
|
||||
context.levelKey(), chunk, biome -> true, context.generator().getSeaLevel(),
|
||||
policies.injection()::surfaceHeight), holder, plan, references);
|
||||
if (!generated.isValid()) {
|
||||
if (replace) {
|
||||
chunk.setStartForStructure(structure, StructureStart.INVALID_START);
|
||||
}
|
||||
policies.injection().discard(plan.structureKey(), chunk.getPos().x(), chunk.getPos().z());
|
||||
continue;
|
||||
}
|
||||
BoundingBox envelope = NativeStructureReferenceEnvelope.referenceBounds(generated, structure,
|
||||
plan.terrain(), plan.structureKey());
|
||||
policies.injection().record(plan, NativeStructureOwnershipFingerprint.capture(plan.structureKey(), generated, envelope));
|
||||
O recorded;
|
||||
try {
|
||||
recorded = policies.references().findPersisted(plan.structureKey(), chunk.getPos().x(), chunk.getPos().z());
|
||||
if (!NativeStructureOwnershipFingerprint.matches(recorded, generated)) {
|
||||
throw new IllegalStateException("Configured structure ownership was not retained: " + plan.structureKey());
|
||||
}
|
||||
chunk.setStartForStructure(structure, generated);
|
||||
} catch (RuntimeException | Error failure) {
|
||||
try {
|
||||
policies.injection().discard(plan.structureKey(), chunk.getPos().x(), chunk.getPos().z());
|
||||
} catch (RuntimeException | Error cleanupFailure) {
|
||||
failure.addSuppressed(cleanupFailure);
|
||||
}
|
||||
throw failure;
|
||||
}
|
||||
ownership.put(structure, recorded);
|
||||
}
|
||||
return ownership;
|
||||
}
|
||||
|
||||
private static <P extends StructureStartPlan, O extends StructureOwnershipRecordView<O>> Map<Structure, StructureTerrainSettings> adjust(
|
||||
NativeContext context, Policies<P, O> policies, ProtoChunk chunk, Map<Structure, O> ownership) {
|
||||
Registry<Structure> registry = context.registries().lookupOrThrow(Registries.STRUCTURE);
|
||||
Map<Structure, StructureTerrainSettings> terrain = new HashMap<>();
|
||||
for (Map.Entry<Structure, StructureStart> entry : chunk.getAllStarts().entrySet()) {
|
||||
Structure structure = entry.getKey();
|
||||
StructureStart start = entry.getValue();
|
||||
if (!start.isValid()) {
|
||||
continue;
|
||||
}
|
||||
String key = Objects.requireNonNull(registry.getKey(structure), "structure key").toString();
|
||||
if (!policies.allowsFootprint().test(start.getBoundingBox())) {
|
||||
chunk.setStartForStructure(structure, StructureStart.INVALID_START);
|
||||
if (ownership.remove(structure) != null) {
|
||||
policies.references().discard(key, chunk.getPos().x(), chunk.getPos().z());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
O recorded = ownership.get(structure);
|
||||
if (recorded != null) {
|
||||
terrain.put(structure, NativeStructureTerrainIntegrator.resolveNativeTerrain(start, recorded.terrain()));
|
||||
continue;
|
||||
}
|
||||
boolean underground = NativeStructureVegetationClearer.isUndergroundStep(structure.step());
|
||||
StructurePlacementDecision decision = policies.references().resolve(key, underground);
|
||||
if (!decision.generate()) {
|
||||
chunk.setStartForStructure(structure, StructureStart.INVALID_START);
|
||||
continue;
|
||||
}
|
||||
NativeStructureVerticalPlacer.applyVerticalPlacement(start, key, decision.yShift(),
|
||||
context.generator().getSeaLevel(), chunk.getMinY(), chunk.getMinY() + chunk.getHeight(),
|
||||
underground, decision.preserveSourceY(), decision.yBand(), policies.injection()::surfaceHeight);
|
||||
StructureTerrainSettings resolved = NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain());
|
||||
StructureStart wrapped = NativeStructureReferenceEnvelope.wrapForPublication(start, structure,
|
||||
start.getReferences(), resolved, key);
|
||||
chunk.setStartForStructure(structure, wrapped);
|
||||
if (wrapped.isValid()) {
|
||||
terrain.put(structure, resolved);
|
||||
}
|
||||
}
|
||||
return terrain;
|
||||
}
|
||||
|
||||
static void generateNaturalStarts(NativeContext context, ChunkAccess chunk) {
|
||||
if (!context.enabled() || SharedConstants.DEBUG_DISABLE_STRUCTURES) {
|
||||
return;
|
||||
}
|
||||
RandomState random = context.structureState().randomState();
|
||||
Climate.Sampler climate = random.createClimateSampler(SamplerContext.builder().enableCaches().build());
|
||||
for (Holder<StructureSet> holder : context.structureState().possibleStructureSets()) {
|
||||
generateSet(context, chunk, holder.value(), climate);
|
||||
}
|
||||
}
|
||||
|
||||
private static void generateSet(NativeContext context, ChunkAccess chunk, StructureSet set, Climate.Sampler climate) {
|
||||
List<StructureSet.StructureSelectionEntry> entries = set.structures();
|
||||
for (StructureSet.StructureSelectionEntry entry : entries) {
|
||||
StructureStart existing = chunk.getStartForStructure(entry.structure().value());
|
||||
if (existing != null && existing.isValid()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
StructurePlacement placement = set.placement();
|
||||
ChunkPos position = chunk.getPos();
|
||||
ResourceKey<StructureSet> key = placement instanceof ChunkGeneratorStructureState.KeyedRandomSpreadStructurePlacement keyed
|
||||
? keyed.key : null;
|
||||
if (!placement.isStructureChunk(context.structureState(), position.x(), position.z(), key)) {
|
||||
return;
|
||||
}
|
||||
if (entries.size() == 1) {
|
||||
generate(context, chunk, entries.getFirst(), climate);
|
||||
return;
|
||||
}
|
||||
ArrayList<StructureSet.StructureSelectionEntry> remaining = new ArrayList<>(entries);
|
||||
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(0L));
|
||||
random.setLargeFeatureSeed(context.structureState().getLevelSeed(), position.x(), position.z());
|
||||
int weight = 0;
|
||||
for (StructureSet.StructureSelectionEntry entry : remaining) {
|
||||
weight += entry.weight();
|
||||
}
|
||||
while (!remaining.isEmpty()) {
|
||||
int selection = random.nextInt(weight);
|
||||
int index = 0;
|
||||
for (StructureSet.StructureSelectionEntry entry : remaining) {
|
||||
selection -= entry.weight();
|
||||
if (selection < 0) {
|
||||
break;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
StructureSet.StructureSelectionEntry selected = remaining.get(index);
|
||||
if (generate(context, chunk, selected, climate)) {
|
||||
return;
|
||||
}
|
||||
remaining.remove(index);
|
||||
weight -= selected.weight();
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean generate(NativeContext context, ChunkAccess chunk,
|
||||
StructureSet.StructureSelectionEntry entry, Climate.Sampler climate) {
|
||||
Structure structure = entry.structure().value();
|
||||
StructureStart existing = chunk.getStartForStructure(structure);
|
||||
int references = existing == null ? 0 : existing.getReferences();
|
||||
StructureStart generated = structure.generate(entry.structure(), context.levelKey(), context.registries(),
|
||||
context.generator(), context.generator().getBiomeSource(), climate, context.structureState().randomState(),
|
||||
context.templates(), context.structureState().getLevelSeed(), chunk.getPos(), references, chunk,
|
||||
structure.biomes()::contains);
|
||||
if (!generated.isValid()) {
|
||||
return false;
|
||||
}
|
||||
chunk.setStartForStructure(structure, generated);
|
||||
return true;
|
||||
}
|
||||
|
||||
record NativeContext(RegistryAccess registries, ChunkGenerator generator,
|
||||
ChunkGeneratorStructureState structureState, StructureTemplateManager templates,
|
||||
ResourceKey<Level> levelKey, WorldOptions worldOptions, boolean adapterEnabled) {
|
||||
NativeContext {
|
||||
Objects.requireNonNull(registries, "registries");
|
||||
Objects.requireNonNull(generator, "generator");
|
||||
Objects.requireNonNull(structureState, "structureState");
|
||||
Objects.requireNonNull(templates, "templates");
|
||||
Objects.requireNonNull(levelKey, "levelKey");
|
||||
Objects.requireNonNull(worldOptions, "worldOptions");
|
||||
if (worldOptions.seed() != structureState.getLevelSeed()) {
|
||||
throw new IllegalArgumentException("Structure state seed differs from world options");
|
||||
}
|
||||
}
|
||||
|
||||
boolean enabled() {
|
||||
return worldOptions.generateStructures() && adapterEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
record Policies<P extends StructureStartPlan, O extends StructureOwnershipRecordView<O>>(
|
||||
StructureInjectionPolicy<P> injection, StructureReferencePolicy<P, O> references,
|
||||
Predicate<BoundingBox> allowsFootprint) {
|
||||
Policies {
|
||||
Objects.requireNonNull(injection, "injection");
|
||||
Objects.requireNonNull(references, "references");
|
||||
Objects.requireNonNull(allowsFootprint, "allowsFootprint");
|
||||
}
|
||||
}
|
||||
|
||||
record PlannedChunk<O extends StructureOwnershipRecordView<O>>(NativeContext context, ProtoChunk chunk,
|
||||
Map<Structure, O> ownership, Map<Structure, StructureTerrainSettings> terrain) {
|
||||
}
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.platform.bukkit.nms.BukkitBiomePolicy;
|
||||
import art.arcane.iris.structure.nativegen.IrisStructurePolicy;
|
||||
import art.arcane.iris.structure.nativegen.IrisStructureVolumePolicy;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureVolumeIndex;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeBiomeRegistryImpl;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureVolumeSource;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.util.datafix.DataFixers;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
import net.minecraft.world.level.chunk.PalettedContainerFactory;
|
||||
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.WorldOptions;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||
import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
import org.spigotmc.SpigotWorldConfig;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
final class HeadlessTerrainContext implements AutoCloseable {
|
||||
private final Engine engine;
|
||||
private final LevelStorageSource.LevelStorageAccess storage;
|
||||
private final HeadlessStructurePlanner.NativeContext structures;
|
||||
private final HeadlessStructurePlanner.Policies<NativeStructureStartPlan, NativeStructureOwnershipRecord> policies;
|
||||
private final BukkitBiomePolicy<Holder<Biome>, Biome> biomes;
|
||||
private final HeadlessChunkSerialization.Context serialization;
|
||||
private final NativeRegionTerrainWriter writer;
|
||||
|
||||
private HeadlessTerrainContext(Options options, LevelStorageSource.LevelStorageAccess storage) {
|
||||
engine = options.engine();
|
||||
this.storage = storage;
|
||||
HeadlessNativeRuntime runtime = options.runtime();
|
||||
writer = runtime.writer();
|
||||
HeadlessChunkGenerator generator = new HeadlessChunkGenerator(new HeadlessChunkGenerator.Options(
|
||||
engine, runtime.registries()));
|
||||
long seed = engine.getSeedManager().getSeed();
|
||||
NoiseGeneratorSettings settings = runtime.registries().lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||
.getOrThrow(options.noiseSettings()).value();
|
||||
RandomState random = RandomState.create(runtime.registries().lookupOrThrow(Registries.NOISE), seed, settings);
|
||||
ChunkGeneratorStructureState state = generator.createState(
|
||||
runtime.registries().lookupOrThrow(Registries.STRUCTURE_SET), random, seed, options.configuration());
|
||||
initializeStructureState(state);
|
||||
StructureTemplateManager templates = new StructureTemplateManager(runtime.resources(), storage,
|
||||
DataFixers.getDataFixer(), BuiltInRegistries.BLOCK);
|
||||
structures = new HeadlessStructurePlanner.NativeContext(runtime.registries(), generator, state,
|
||||
templates, options.levelKey(), new WorldOptions(seed, true, false), true);
|
||||
IrisStructurePolicy policy = new IrisStructurePolicy(engine);
|
||||
policies = new HeadlessStructurePlanner.Policies<>(policy, policy,
|
||||
bounds -> engine.getComplex().allowsNewGenerationFootprint(
|
||||
bounds.minX(), bounds.minZ(), bounds.maxX(), bounds.maxZ()));
|
||||
NativeBiomeRegistryImpl registry = new NativeBiomeRegistryImpl(runtime.registries().lookupOrThrow(Registries.BIOME));
|
||||
biomes = new BukkitBiomePolicy<>(new BukkitBiomePolicy.RuntimeOptions<>(seed, engine, null, () -> registry), registry);
|
||||
serialization = new HeadlessChunkSerialization.Context(new StructurePieceSerializationContext(
|
||||
runtime.resources(), runtime.registries(), templates), PalettedContainerFactory.create(runtime.registries()), 0L);
|
||||
NativeStructureVolumeSource<Engine, NativeStructureStartPlan> volumes = new NativeStructureVolumeSource<>(
|
||||
new NativeStructureVolumeSource.Context(runtime.registries(), templates, options.levelKey(),
|
||||
LevelHeightAccessor.create(engine.getMinHeight(), engine.getHeight()),
|
||||
() -> generator, generator::getBiomeSource, () -> state), IrisStructureVolumePolicy.INSTANCE);
|
||||
NativeStructureVolumeIndex.install(engine, volumes::volumesAt);
|
||||
}
|
||||
|
||||
static HeadlessTerrainContext create(Options options) throws IOException {
|
||||
LevelStorageSource.LevelStorageAccess storage = LevelStorageSource.createDefault(options.templateStorage())
|
||||
.createAccess("native-templates");
|
||||
try {
|
||||
return new HeadlessTerrainContext(options, storage);
|
||||
} catch (RuntimeException | Error failure) {
|
||||
try {
|
||||
storage.close();
|
||||
} catch (IOException closeFailure) {
|
||||
failure.addSuppressed(closeFailure);
|
||||
}
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
private static void initializeStructureState(ChunkGeneratorStructureState state) {
|
||||
state.ensureStructuresGenerated();
|
||||
Throwable failure = null;
|
||||
for (Holder<StructureSet> structureSet : state.possibleStructureSets()) {
|
||||
if (!(structureSet.value().placement() instanceof ConcentricRingsStructurePlacement rings)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
state.getRingPositionsFor(rings);
|
||||
} catch (RuntimeException | Error problem) {
|
||||
if (failure == null) {
|
||||
failure = problem;
|
||||
} else {
|
||||
failure.addSuppressed(problem);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (failure instanceof RuntimeException exception) {
|
||||
throw exception;
|
||||
}
|
||||
if (failure instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
NativeStructureVolumeIndex.uninstall(engine);
|
||||
storage.close();
|
||||
}
|
||||
|
||||
Engine engine() {
|
||||
return engine;
|
||||
}
|
||||
|
||||
HeadlessStructurePlanner.NativeContext structures() {
|
||||
return structures;
|
||||
}
|
||||
|
||||
HeadlessStructurePlanner.Policies<NativeStructureStartPlan, NativeStructureOwnershipRecord> policies() {
|
||||
return policies;
|
||||
}
|
||||
|
||||
BukkitBiomePolicy<Holder<Biome>, Biome> biomes() {
|
||||
return biomes;
|
||||
}
|
||||
|
||||
HeadlessChunkSerialization.Context serialization() {
|
||||
return serialization;
|
||||
}
|
||||
|
||||
NativeRegionTerrainWriter writer() {
|
||||
return writer;
|
||||
}
|
||||
|
||||
record Options(Engine engine, HeadlessNativeRuntime runtime, Path templateStorage, ResourceKey<Level> levelKey,
|
||||
ResourceKey<NoiseGeneratorSettings> noiseSettings, SpigotWorldConfig configuration) {
|
||||
Options {
|
||||
Objects.requireNonNull(engine, "engine");
|
||||
Objects.requireNonNull(runtime, "runtime");
|
||||
Objects.requireNonNull(templateStorage, "templateStorage");
|
||||
Objects.requireNonNull(levelKey, "levelKey");
|
||||
Objects.requireNonNull(noiseSettings, "noiseSettings");
|
||||
Objects.requireNonNull(configuration, "configuration");
|
||||
}
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.chunk.status.ChunkPyramid;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStep;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
final class NativeRegionPlan {
|
||||
private NativeRegionPlan() {
|
||||
}
|
||||
|
||||
static List<PlannedChunk> plan(int chunkX, int chunkZ, int width) {
|
||||
if (width < 1 || width > 32) {
|
||||
throw new IllegalArgumentException("Native terrain tile width must be 1..32 chunks");
|
||||
}
|
||||
ChunkStep target = ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FULL);
|
||||
int halo = target.accumulatedDependencies().size() - 1;
|
||||
int maximumX = Math.addExact(chunkX, width - 1);
|
||||
int maximumZ = Math.addExact(chunkZ, width - 1);
|
||||
if (!new ChunkPos(Math.subtractExact(chunkX, halo), Math.subtractExact(chunkZ, halo)).isValid()
|
||||
|| !new ChunkPos(Math.addExact(maximumX, halo), Math.addExact(maximumZ, halo)).isValid()) {
|
||||
throw new IllegalArgumentException("Native terrain tile exceeds supported chunk bounds");
|
||||
}
|
||||
List<PlannedChunk> result = new ArrayList<>((width + halo * 2) * (width + halo * 2));
|
||||
for (int z = Math.subtractExact(chunkZ, halo); z <= Math.addExact(maximumZ, halo); z++) {
|
||||
for (int x = Math.subtractExact(chunkX, halo); x <= Math.addExact(maximumX, halo); x++) {
|
||||
int distance = Math.max(distance(x, chunkX, maximumX), distance(z, chunkZ, maximumZ));
|
||||
ChunkStatus required = distance == 0 ? ChunkStatus.FULL : target.accumulatedDependencies().get(distance);
|
||||
ChunkStatus status = required.isAfter(ChunkStatus.TERRAIN) ? ChunkStatus.TERRAIN : required;
|
||||
result.add(new PlannedChunk(x, z, status, distance == 0));
|
||||
}
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
static int terrainHalo() {
|
||||
ChunkStep target = ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FULL);
|
||||
int radius = 0;
|
||||
for (int distance = 1; distance < target.accumulatedDependencies().size(); distance++) {
|
||||
if (target.accumulatedDependencies().get(distance).isOrAfter(ChunkStatus.TERRAIN)) {
|
||||
radius = distance;
|
||||
}
|
||||
}
|
||||
return radius;
|
||||
}
|
||||
|
||||
private static int distance(int value, int minimum, int maximum) {
|
||||
return Math.max(0, Math.max(minimum - value, value - maximum));
|
||||
}
|
||||
|
||||
record PlannedChunk(int x, int z, ChunkStatus status, boolean target) {
|
||||
}
|
||||
}
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import it.unimi.dsi.fastutil.shorts.ShortList;
|
||||
import net.minecraft.commands.arguments.blocks.BlockStateParser;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.NbtIo;
|
||||
import net.minecraft.nbt.NbtOps;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.EntityBlock;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.level.storage.TagValueInput;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
import net.minecraft.world.level.chunk.PalettedContainer;
|
||||
import net.minecraft.world.level.chunk.PalettedContainerFactory;
|
||||
import net.minecraft.world.level.chunk.UpgradeData;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.chunk.storage.RegionFile;
|
||||
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
|
||||
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
final class NativeRegionTerrainWriter {
|
||||
private final Registry<Biome> biomes;
|
||||
private final RegistryAccess registries;
|
||||
private final PalettedContainerFactory containers;
|
||||
private final Map<String, BlockState> tileStates = new ConcurrentHashMap<>();
|
||||
private final Map<String, Holder<Biome>> biomeHolders = new ConcurrentHashMap<>();
|
||||
|
||||
NativeRegionTerrainWriter(RegistryAccess registries) {
|
||||
this.registries = Objects.requireNonNull(registries, "registries");
|
||||
biomes = registries.lookupOrThrow(Registries.BIOME);
|
||||
containers = PalettedContainerFactory.create(registries);
|
||||
}
|
||||
|
||||
RegistryAccess registries() {
|
||||
return registries;
|
||||
}
|
||||
|
||||
CompoundTag encode(TerrainInput input) {
|
||||
TerrainData terrain = terrain(input, null);
|
||||
SerializableChunkData data = new SerializableChunkData(containers,
|
||||
new ChunkPos(input.chunkX(), input.chunkZ()), input.minimumY() >> 4, 0L, 0L,
|
||||
ChunkStatus.EMPTY, null, null, UpgradeData.EMPTY, Map.of(),
|
||||
new ChunkAccess.PackedTicks(List.of(), List.of()), new ShortList[input.height() >> 4], false,
|
||||
terrain.sections(), List.of(), terrain.blockEntities(), new CompoundTag(), null);
|
||||
CompoundTag tag = data.write();
|
||||
tag.putString("iris:checkpoint", "iris:terrain_checkpoint");
|
||||
return tag;
|
||||
}
|
||||
|
||||
|
||||
void fill(ProtoChunk chunk, TerrainInput input) {
|
||||
if (chunk.getPos().x() != input.chunkX() || chunk.getPos().z() != input.chunkZ()
|
||||
|| chunk.getMinY() != input.minimumY() || chunk.getHeight() != input.height()) {
|
||||
throw new IllegalArgumentException("Generated terrain does not match its planned chunk");
|
||||
}
|
||||
TerrainData terrain = terrain(input, chunk.getSections());
|
||||
for (int i = 0; i < terrain.sections().size(); i++) {
|
||||
chunk.getSections()[i] = terrain.sections().get(i).chunkSection();
|
||||
}
|
||||
for (CompoundTag entity : terrain.blockEntities()) {
|
||||
chunk.setBlockEntityNbt(entity);
|
||||
}
|
||||
chunk.markUnsaved();
|
||||
}
|
||||
|
||||
private TerrainData terrain(TerrainInput input, LevelChunkSection[] existingBiomes) {
|
||||
Int2ObjectOpenHashMap<TileInput> tiles = new Int2ObjectOpenHashMap<>(input.tiles().size());
|
||||
for (TileInput tile : input.tiles()) {
|
||||
if (tile.y() >= input.height() || tiles.put(tile.x() | tile.z() << 4 | tile.y() << 8, tile) != null) {
|
||||
throw new IllegalArgumentException("Duplicate or out-of-bounds tile data");
|
||||
}
|
||||
}
|
||||
List<CompoundTag> blockEntities = new ArrayList<>();
|
||||
List<SerializableChunkData.SectionData> sections = new ArrayList<>(input.height() >> 4);
|
||||
for (int sectionY = 0; sectionY < input.height(); sectionY += 16) {
|
||||
PalettedContainer<BlockState> blocks = containers.createForBlockStates();
|
||||
PalettedContainer<Holder<Biome>> sectionBiomes = existingBiomes == null
|
||||
? containers.createForBiomes() : existingBiomes[sectionY >> 4].getBiomes().copy();
|
||||
for (int y = 0; y < 16; y++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
BlockState state = Objects.requireNonNull(input.blocks().state(x, sectionY + y, z), "block state");
|
||||
TileInput tile = tiles.isEmpty() ? null : tiles.get(x | z << 4 | (sectionY + y) << 8);
|
||||
if (tile == null && state == Blocks.AIR.defaultBlockState()) {
|
||||
continue;
|
||||
}
|
||||
if (tile != null) {
|
||||
state = tileStates.computeIfAbsent(tile.stateKey(), NativeRegionTerrainWriter::resolveState);
|
||||
}
|
||||
blocks.set(x, y, z, state);
|
||||
if (state.hasBlockEntity()) {
|
||||
BlockPos position = new BlockPos((input.chunkX() << 4) + x,
|
||||
input.minimumY() + sectionY + y, (input.chunkZ() << 4) + z);
|
||||
CompoundTag entity = encodeBlockEntity(state, position, tile);
|
||||
if (entity != null) {
|
||||
blockEntities.add(entity);
|
||||
}
|
||||
} else if (tile != null) {
|
||||
throw new IllegalArgumentException("Tile data has no native block entity at " + x + "," + y + "," + z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (existingBiomes == null) {
|
||||
for (int y = 0; y < 4; y++) {
|
||||
for (int z = 0; z < 4; z++) {
|
||||
for (int x = 0; x < 4; x++) {
|
||||
String key = Objects.requireNonNull(input.biomes().key(x << 2, sectionY + (y << 2), z << 2), "biome key");
|
||||
sectionBiomes.set(x, y, z, biomeHolders.computeIfAbsent(key, this::resolveBiome));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sections.add(new SerializableChunkData.SectionData((input.minimumY() + sectionY) >> 4,
|
||||
new LevelChunkSection(blocks, sectionBiomes), null, null));
|
||||
}
|
||||
return new TerrainData(sections, blockEntities);
|
||||
}
|
||||
|
||||
private CompoundTag encodeBlockEntity(BlockState state, BlockPos position, TileInput tile) {
|
||||
BlockEntity entity = ((EntityBlock) state.getBlock()).newBlockEntity(position, state);
|
||||
if (entity == null) {
|
||||
if (tile != null) {
|
||||
throw new IllegalStateException("Native block did not create a tile entity at " + position);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (tile != null) {
|
||||
CompoundTag merged = entity.saveWithoutMetadata(registries);
|
||||
merged.merge(tile.payload());
|
||||
entity.loadWithComponents(TagValueInput.create(ProblemReporter.DISCARDING, registries, merged));
|
||||
}
|
||||
return entity.saveWithFullMetadata(registries);
|
||||
}
|
||||
|
||||
BlockState readBlock(CompoundTag tag, int x, int y, int z) {
|
||||
for (CompoundTag section : tag.getListOrEmpty("sections").compoundStream().toList()) {
|
||||
if (section.getByteOr("Y", (byte) 0) == y >> 4) {
|
||||
return containers.blockStatesContainerCodec().parse(NbtOps.INSTANCE,
|
||||
section.getCompoundOrEmpty("block_states")).getOrThrow().get(x & 15, y & 15, z & 15);
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Missing section at " + y);
|
||||
}
|
||||
|
||||
static void write(RegionFile region, CompoundTag tag) throws IOException {
|
||||
ChunkPos position = new ChunkPos(tag.getIntOr("xPos", 0), tag.getIntOr("zPos", 0));
|
||||
try (DataOutputStream output = region.getChunkDataOutputStream(position)) {
|
||||
NbtIo.write(tag, output);
|
||||
}
|
||||
}
|
||||
|
||||
private static BlockState resolveState(String key) {
|
||||
try {
|
||||
return BlockStateParser.parseForBlock(BuiltInRegistries.BLOCK, key, false).blockState();
|
||||
} catch (CommandSyntaxException failure) {
|
||||
throw new IllegalArgumentException("Unsupported native block state: " + key, failure);
|
||||
}
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveBiome(String key) {
|
||||
return biomes.get(ResourceKey.create(Registries.BIOME, Identifier.parse(key)))
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unsupported native biome: " + key));
|
||||
}
|
||||
|
||||
record TerrainInput(int chunkX, int chunkZ, int minimumY, int height, BlockLookup blocks, KeyLookup biomes, List<TileInput> tiles) {
|
||||
TerrainInput {
|
||||
if ((minimumY & 15) != 0 || (height & 15) != 0 || height < 16
|
||||
|| minimumY < -2032 || (long) minimumY + height > 2032) {
|
||||
throw new IllegalArgumentException("Terrain bounds must be section aligned within the native height range");
|
||||
}
|
||||
Objects.requireNonNull(blocks, "blocks");
|
||||
Objects.requireNonNull(biomes, "biomes");
|
||||
tiles = List.copyOf(tiles);
|
||||
}
|
||||
}
|
||||
|
||||
record TileInput(int x, int y, int z, String stateKey, CompoundTag payload) {
|
||||
TileInput {
|
||||
if (x < 0 || x > 15 || z < 0 || z > 15 || y < 0) {
|
||||
throw new IllegalArgumentException("Tile position is outside its chunk");
|
||||
}
|
||||
Objects.requireNonNull(stateKey, "stateKey");
|
||||
payload = Objects.requireNonNull(payload, "payload").copy();
|
||||
}
|
||||
}
|
||||
|
||||
private record TerrainData(List<SerializableChunkData.SectionData> sections, List<CompoundTag> blockEntities) {
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface BlockLookup {
|
||||
BlockState state(int x, int y, int z);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface KeyLookup {
|
||||
String key(int x, int y, int z);
|
||||
}
|
||||
}
|
||||
+258
@@ -0,0 +1,258 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
|
||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.world.pregen.PregenPerformanceProfile;
|
||||
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
|
||||
import org.spigotmc.SpigotWorldConfig;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class OfflineRegionGenerator {
|
||||
private OfflineRegionGenerator() {
|
||||
}
|
||||
|
||||
public static void main(String[] arguments) {
|
||||
PrintStream diagnostics = System.err;
|
||||
try {
|
||||
execute(arguments);
|
||||
} catch (Throwable failure) {
|
||||
failure.printStackTrace(diagnostics);
|
||||
diagnostics.flush();
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void execute(String[] arguments) throws Exception {
|
||||
if (arguments.length == 1 && arguments[0].equals("--help")) {
|
||||
System.out.println("Usage: iris-offline pack dimension seed chunkX chunkZ width regionsX regionsZ parallelism spigot worldName levelKey noiseSettings output");
|
||||
System.out.println("Java 25; width 1..32 chunks; parallelism 1..32. Output must be a new absolute directory.");
|
||||
System.out.println("Generates fresh TERRAIN checkpoints and immutable Iris history. Native server completion is required for features, population and lighting.");
|
||||
return;
|
||||
}
|
||||
if (Bukkit.getServer() != null) {
|
||||
throw new IllegalStateException("Offline generation requires a separate process without a running Bukkit server");
|
||||
}
|
||||
long processStarted = System.nanoTime();
|
||||
HeadlessNativeBootstrap.initialize();
|
||||
Configuration configuration = Configuration.parse(arguments);
|
||||
Files.createDirectory(configuration.output());
|
||||
force(configuration.output());
|
||||
try (FileChannel channel = FileChannel.open(configuration.output().resolve("checkpoint.lock"),
|
||||
StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
|
||||
FileLock lock = channel.lock()) {
|
||||
if (!lock.isValid()) {
|
||||
throw new IllegalStateException("Headless checkpoint lock is invalid");
|
||||
}
|
||||
run(configuration);
|
||||
System.out.println("processSeconds=" + (System.nanoTime() - processStarted) / 1_000_000_000.0);
|
||||
}
|
||||
}
|
||||
|
||||
private static void run(Configuration configuration) throws Exception {
|
||||
Path staged = Files.createDirectory(configuration.output().resolve("incomplete"));
|
||||
byte[] configurationBytes = Files.readAllBytes(configuration.spigot());
|
||||
Path capturedConfiguration = staged.resolve("spigot.yml");
|
||||
Files.write(capturedConfiguration, configurationBytes, StandardOpenOption.CREATE_NEW);
|
||||
String configurationHash = HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(configurationBytes));
|
||||
HeadlessNativeRuntime runtime = new HeadlessNativeRuntime();
|
||||
long started = System.nanoTime();
|
||||
long writes = 0;
|
||||
long terrainWrites = 0;
|
||||
double generationSeconds = 0;
|
||||
double serializationSeconds = 0;
|
||||
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(new RealPackProbeSupport.WorkspaceOptions(configuration.pack(),
|
||||
configuration.dimension(), "[headless-native-region]", runtime, staged));
|
||||
RealPackProbeSupport.EngineSession session = workspace.openEngine(configuration.seed(), false, "native-region")) {
|
||||
session.retainWorld(staged.resolve("world"));
|
||||
SpigotWorldConfig spigot = HeadlessStructureConfiguration.load(new HeadlessStructureConfiguration.Options(
|
||||
capturedConfiguration, configuration.worldName(), configuration.levelKey(), staged.resolve("configuration")));
|
||||
PregenPerformanceProfile.apply(session.engine());
|
||||
System.out.println("generationProfile=pregen noiseCacheSize=" + IrisSettings.get().getPerformance().getNoiseCacheSize()
|
||||
+ " burstParallelism=" + MultiBurst.burst.parallelism() + " multicore=true");
|
||||
IrisHydrologyRuntime hydrology = session.engine().getComplex().getHydrologyRuntime();
|
||||
try (HydrologyTileCache.PregenerationScope hydrologyScope = hydrology == null ? null
|
||||
: hydrology.preparePregeneration(configuration.hydrologyArea());
|
||||
HeadlessTerrainContext context = HeadlessTerrainContext.create(new HeadlessTerrainContext.Options(
|
||||
session.engine(), runtime, staged.resolve("templates"), configuration.levelKey(),
|
||||
configuration.noiseSettings(), spigot))) {
|
||||
HeadlessRegionTerrain.Session rolling = new HeadlessRegionTerrain.Session(RegionGenerationWindow.Policy.STANDALONE);
|
||||
Path regionDirectory = session.engine().getTarget().getWorld().worldFolder().toPath().resolve("region");
|
||||
Files.createDirectory(regionDirectory);
|
||||
OfflineRegionWriter writer = new OfflineRegionWriter(new OfflineRegionWriter.Options(
|
||||
context, regionDirectory, configuration.parallelism(), RegionGenerationWindow.Policy.STANDALONE));
|
||||
for (int row = 0; row < configuration.regionsZ(); row++) {
|
||||
for (int column = 0; column < configuration.regionsX(); column++) {
|
||||
int regionX = row % 2 == 0 ? column : configuration.regionsX() - column - 1;
|
||||
int chunkX = Math.addExact(configuration.chunkX(), Math.multiplyExact(regionX, configuration.width()));
|
||||
int chunkZ = Math.addExact(configuration.chunkZ(), Math.multiplyExact(row, configuration.width()));
|
||||
RegionResult generated = generateRegion(new RegionRequest(context, rolling, writer, chunkX, chunkZ,
|
||||
configuration.width(), configuration.parallelism(),
|
||||
staged.resolve("receipts.properties")));
|
||||
writes += generated.written().chunks();
|
||||
terrainWrites += generated.written().terrainChunks();
|
||||
generationSeconds += generated.generationSeconds();
|
||||
serializationSeconds += generated.serializationSeconds();
|
||||
System.out.println("region=" + regionX + "," + row + " residentChunks=" + generated.residentChunks()
|
||||
+ " upgradedChunks=" + generated.upgradedChunks() + " writtenChunks=" + generated.written().chunks());
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
PregenPerformanceProfile.restore();
|
||||
}
|
||||
}
|
||||
double wallSeconds = (System.nanoTime() - started) / 1_000_000_000.0;
|
||||
String report = String.format(Locale.ROOT,
|
||||
"format=iris-native-terrain-checkpoint\nplatform=headless-native\nstatus=minecraft:terrain\ncomplete=false\n"
|
||||
+ "targetChunks=%d\nchunkWrites=%d\nterrainWrites=%d\nparallelism=%d\nseed=%d\nchunkX=%d\nchunkZ=%d\nwidth=%d\nregionsX=%d\nregionsZ=%d\n"
|
||||
+ "levelKey=%s\nworldName=%s\nnoiseSettings=%s\nspigotSha256=%s\npipelineWallSeconds=%.3f\ngenerationSeconds=%.3f\nserializationSeconds=%.3f\n"
|
||||
+ "limitations=Fresh offline world checkpoint only. Native features, population, lighting, POI, entities and scheduled ticks require native completion. No live plugin events are dispatched. Authored tile payloads remain in retained mantle for native completion. No transitions or existing chunks are imported.\n",
|
||||
Math.multiplyExact(Math.multiplyExact(configuration.width(), configuration.width()),
|
||||
Math.multiplyExact(configuration.regionsX(), configuration.regionsZ())), writes, terrainWrites,
|
||||
configuration.parallelism(), configuration.seed(), configuration.chunkX(), configuration.chunkZ(),
|
||||
configuration.width(), configuration.regionsX(), configuration.regionsZ(), configuration.levelKey().identifier(),
|
||||
configuration.worldName(), configuration.noiseSettings().identifier(), configurationHash,
|
||||
wallSeconds, generationSeconds, serializationSeconds);
|
||||
Files.writeString(staged.resolve("checkpoint.properties"), report, StandardOpenOption.CREATE_NEW);
|
||||
long publicationStarted = System.nanoTime();
|
||||
forceTree(staged);
|
||||
Files.move(staged, configuration.output().resolve("native-terrain-checkpoint"), StandardCopyOption.ATOMIC_MOVE);
|
||||
force(configuration.output());
|
||||
System.out.println(report);
|
||||
System.out.println("publicationSeconds=" + (System.nanoTime() - publicationStarted) / 1_000_000_000.0);
|
||||
System.out.println("totalSeconds=" + (System.nanoTime() - started) / 1_000_000_000.0);
|
||||
}
|
||||
|
||||
private static RegionResult generateRegion(RegionRequest request) throws Exception {
|
||||
long generationStarted = System.nanoTime();
|
||||
HeadlessRegionTerrain.Result generated = request.rolling().generate(new HeadlessRegionTerrain.Request(
|
||||
request.context(), request.chunkX(), request.chunkZ(), request.width(), request.parallelism()), System.out::println);
|
||||
double generationSeconds = (System.nanoTime() - generationStarted) / 1_000_000_000.0;
|
||||
long serializationStarted = System.nanoTime();
|
||||
OfflineRegionWriter.Result written = request.writer().write(generated);
|
||||
writeReceipts(generated, request.receipts());
|
||||
return new RegionResult(written, generated.chunks().size(), generated.updated().size(), generationSeconds,
|
||||
(System.nanoTime() - serializationStarted) / 1_000_000_000.0);
|
||||
}
|
||||
|
||||
private record RegionRequest(HeadlessTerrainContext context, HeadlessRegionTerrain.Session rolling, OfflineRegionWriter writer,
|
||||
int chunkX, int chunkZ, int width, int parallelism, Path receipts) {
|
||||
}
|
||||
|
||||
private record RegionResult(OfflineRegionWriter.Result written, int residentChunks, int upgradedChunks,
|
||||
double generationSeconds, double serializationSeconds) {
|
||||
}
|
||||
|
||||
private static void writeReceipts(HeadlessRegionTerrain.Result generated, Path path) throws Exception {
|
||||
NamespacedKey key = new NamespacedKey("iris", "natural_terrain");
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
StringBuilder receipts = new StringBuilder();
|
||||
for (ProtoChunk chunk : generated.updated()) {
|
||||
if (!chunk.getPersistedStatus().isOrAfter(ChunkStatus.TERRAIN)) {
|
||||
continue;
|
||||
}
|
||||
byte[] receipt = chunk.persistentDataContainer.get(key, PersistentDataType.BYTE_ARRAY);
|
||||
if (receipt == null) {
|
||||
throw new IllegalStateException("Native TERRAIN checkpoint has no receipt at " + chunk.getPos());
|
||||
}
|
||||
receipts.append(chunk.getPos().x()).append(',').append(chunk.getPos().z()).append('=')
|
||||
.append(HexFormat.of().formatHex(digest.digest(receipt))).append('\n');
|
||||
}
|
||||
Files.writeString(path, receipts, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
|
||||
}
|
||||
|
||||
private static void forceTree(Path directory) throws IOException {
|
||||
Files.walkFileTree(directory, new SimpleFileVisitor<>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException {
|
||||
force(file);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path path, IOException failure) throws IOException {
|
||||
if (failure != null) {
|
||||
throw failure;
|
||||
}
|
||||
force(path);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void force(Path path) throws IOException {
|
||||
try (FileChannel channel = FileChannel.open(path, Files.isDirectory(path)
|
||||
? StandardOpenOption.READ : StandardOpenOption.WRITE)) {
|
||||
channel.force(true);
|
||||
}
|
||||
}
|
||||
|
||||
record Configuration(File pack, String dimension, long seed, int chunkX, int chunkZ, int width,
|
||||
int regionsX, int regionsZ, int parallelism, Path spigot, String worldName, ResourceKey<Level> levelKey,
|
||||
ResourceKey<NoiseGeneratorSettings> noiseSettings, Path output) {
|
||||
Configuration {
|
||||
if (regionsX < 1 || regionsZ < 1) {
|
||||
throw new IllegalArgumentException("Region dimensions must be positive");
|
||||
}
|
||||
Math.multiplyExact(Math.multiplyExact(width, width), Math.multiplyExact(regionsX, regionsZ));
|
||||
NativeRegionPlan.plan(chunkX, chunkZ, width);
|
||||
NativeRegionPlan.plan(Math.addExact(chunkX, Math.multiplyExact(regionsX - 1, width)),
|
||||
Math.addExact(chunkZ, Math.multiplyExact(regionsZ - 1, width)), width);
|
||||
if (parallelism < 1 || parallelism > 32 || !Files.isRegularFile(spigot)) {
|
||||
throw new IllegalArgumentException("Parallelism must be 1..32 and Spigot configuration must exist");
|
||||
}
|
||||
if (!output.isAbsolute() || Files.exists(output) || !Files.isDirectory(output.getParent())) {
|
||||
throw new IllegalArgumentException("Output must be a new absolute directory under an existing parent");
|
||||
}
|
||||
}
|
||||
|
||||
HydrologyTileCache.PregenerationArea hydrologyArea() {
|
||||
int halo = NativeRegionPlan.terrainHalo();
|
||||
long minimumX = ((long) chunkX - halo) * 16L;
|
||||
long minimumZ = ((long) chunkZ - halo) * 16L;
|
||||
long maximumX = ((long) chunkX + (long) width * regionsX + halo) * 16L - 1L;
|
||||
long maximumZ = ((long) chunkZ + (long) width * regionsZ + halo) * 16L - 1L;
|
||||
return new HydrologyTileCache.PregenerationArea(Math.toIntExact((minimumX + maximumX) / 2L),
|
||||
Math.toIntExact((minimumZ + maximumZ) / 2L), minimumX, minimumZ, maximumX, maximumZ);
|
||||
}
|
||||
|
||||
static Configuration parse(String[] arguments) throws IOException {
|
||||
if (arguments.length != 14) {
|
||||
throw new IllegalArgumentException("Expected pack dimension seed chunkX chunkZ width regionsX regionsZ parallelism spigot worldName levelKey noiseSettings output");
|
||||
}
|
||||
return new Configuration(new File(arguments[0]).getCanonicalFile(), arguments[1], Long.parseLong(arguments[2]),
|
||||
Integer.parseInt(arguments[3]), Integer.parseInt(arguments[4]), Integer.parseInt(arguments[5]),
|
||||
Integer.parseInt(arguments[6]), Integer.parseInt(arguments[7]), Integer.parseInt(arguments[8]),
|
||||
Path.of(arguments[9]), arguments[10],
|
||||
ResourceKey.create(Registries.DIMENSION, Identifier.parse(arguments[11])),
|
||||
ResourceKey.create(Registries.NOISE_SETTINGS, Identifier.parse(arguments[12])), Path.of(arguments[13]));
|
||||
}
|
||||
}
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.IORunnable;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.NbtIo;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.chunk.storage.RegionFile;
|
||||
import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
final class OfflineRegionWriter {
|
||||
private static final int MAXIMUM_PREPARATION_WORKERS = 8;
|
||||
private static final int MAXIMUM_STATUS_REGIONS = 64;
|
||||
|
||||
private final Options options;
|
||||
private final Map<Long, int[]> published = new LinkedHashMap<>(MAXIMUM_STATUS_REGIONS, 0.75F, true) {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<Long, int[]> eldest) {
|
||||
return size() > MAXIMUM_STATUS_REGIONS;
|
||||
}
|
||||
};
|
||||
|
||||
OfflineRegionWriter(Options options) {
|
||||
this.options = Objects.requireNonNull(options, "options");
|
||||
}
|
||||
|
||||
Result write(HeadlessRegionTerrain.Result generated) throws Exception {
|
||||
try (OpenRegions regions = new OpenRegions(options)) {
|
||||
List<ChunkWrite> pending = new ArrayList<>(generated.updated().size());
|
||||
for (ProtoChunk chunk : generated.updated()) {
|
||||
ChunkPos position = chunk.getPos();
|
||||
long regionKey = ChunkPos.pack(position.x() >> 5, position.z() >> 5);
|
||||
int[] statuses = published.computeIfAbsent(regionKey, ignored -> new int[1024]);
|
||||
int index = (position.x() & 31) + ((position.z() & 31) << 5);
|
||||
int status = chunk.getPersistedStatus().getIndex() + 2;
|
||||
if (statuses[index] >= status) {
|
||||
continue;
|
||||
}
|
||||
RegionFile region = regions.get(position);
|
||||
if (statuses[index] == 0) {
|
||||
statuses[index] = readStatus(region, position);
|
||||
if (statuses[index] >= status) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
pending.add(new ChunkWrite(chunk, region, statuses, index, status));
|
||||
}
|
||||
if (pending.isEmpty()) {
|
||||
return new Result(0, 0);
|
||||
}
|
||||
int[] counts = new int[2];
|
||||
RegionGenerationWindow.process(new RegionGenerationWindow.Request<>(pending.size(),
|
||||
Math.min(options.parallelism(), MAXIMUM_PREPARATION_WORKERS),
|
||||
index -> prepare(pending.get(index)),
|
||||
(index, prepared) -> {
|
||||
ChunkWrite chunk = prepared.chunk();
|
||||
prepared.commit().run(chunk.region());
|
||||
chunk.statuses()[chunk.index()] = chunk.status();
|
||||
counts[0]++;
|
||||
if (chunk.chunk().getPersistedStatus().isOrAfter(ChunkStatus.TERRAIN)) {
|
||||
counts[1]++;
|
||||
}
|
||||
}), options.policy());
|
||||
return new Result(counts[0], counts[1]);
|
||||
}
|
||||
}
|
||||
|
||||
private PreparedWrite prepare(ChunkWrite chunk) throws IOException {
|
||||
CompoundTag tag = HeadlessChunkSerialization.copyOf(chunk.chunk(), options.context().serialization()).write();
|
||||
WriteData prepared = chunk.region().moonrise$startWrite(tag, chunk.chunk().getPos());
|
||||
try (DataOutputStream output = prepared.output()) {
|
||||
NbtIo.write(tag, output);
|
||||
}
|
||||
return new PreparedWrite(chunk, prepared.write());
|
||||
}
|
||||
|
||||
private static int readStatus(RegionFile region, ChunkPos position) throws IOException {
|
||||
try (DataInputStream input = region.getChunkDataInputStream(position)) {
|
||||
if (input == null) {
|
||||
return 1;
|
||||
}
|
||||
ChunkStatus status = ChunkStatus.byName(NbtIo.read(input).getStringOr("Status", ""));
|
||||
if (status == null) {
|
||||
throw new IOException("Invalid native chunk status at " + position);
|
||||
}
|
||||
return status.getIndex() + 2;
|
||||
}
|
||||
}
|
||||
|
||||
record Options(HeadlessTerrainContext context, Path directory, int parallelism, RegionGenerationWindow.Policy policy) {
|
||||
Options {
|
||||
Objects.requireNonNull(context, "context");
|
||||
Objects.requireNonNull(directory, "directory");
|
||||
Objects.requireNonNull(policy, "policy");
|
||||
if (parallelism < 1 || parallelism > 32) {
|
||||
throw new IllegalArgumentException("Offline writer parallelism must be 1..32");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
record Result(int chunks, int terrainChunks) {
|
||||
}
|
||||
|
||||
private record ChunkWrite(ProtoChunk chunk, RegionFile region, int[] statuses, int index, int status) {
|
||||
}
|
||||
|
||||
private record PreparedWrite(ChunkWrite chunk, IORunnable commit) {
|
||||
}
|
||||
|
||||
private static final class OpenRegions implements AutoCloseable {
|
||||
private final Options options;
|
||||
private final Map<Long, RegionFile> regions = new HashMap<>();
|
||||
|
||||
private OpenRegions(Options options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
private RegionFile get(ChunkPos position) throws IOException {
|
||||
long key = ChunkPos.pack(position.x() >> 5, position.z() >> 5);
|
||||
RegionFile region = regions.get(key);
|
||||
if (region != null) {
|
||||
return region;
|
||||
}
|
||||
Path path = options.directory().resolve("r." + (position.x() >> 5) + "." + (position.z() >> 5) + ".mca");
|
||||
region = new RegionFile(new RegionStorageInfo("headless-native-terrain",
|
||||
options.context().structures().levelKey(), "chunk"), path, options.directory(), false);
|
||||
regions.put(key, region);
|
||||
return region;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
IOException failure = null;
|
||||
for (RegionFile region : regions.values()) {
|
||||
try {
|
||||
region.close();
|
||||
} catch (IOException exception) {
|
||||
if (failure == null) {
|
||||
failure = exception;
|
||||
} else {
|
||||
failure.addSuppressed(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (failure != null) {
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutorCompletionService;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
final class RegionGenerationWindow {
|
||||
private static final int UNRESPONSIVE_WORKER_EXIT = 70;
|
||||
|
||||
private RegionGenerationWindow() {
|
||||
}
|
||||
|
||||
static <T> void process(Request<T> request) throws Exception {
|
||||
process(request, Policy.EMBEDDED);
|
||||
}
|
||||
|
||||
static <T> void process(Request<T> request, Policy policy) throws Exception {
|
||||
ExecutorService workers = Executors.newFixedThreadPool(request.parallelism());
|
||||
ExecutorCompletionService<Completed<T>> completions = new ExecutorCompletionService<>(workers);
|
||||
Set<Future<Completed<T>>> active = new HashSet<>(request.parallelism());
|
||||
try {
|
||||
int submitted = 0;
|
||||
for (; submitted < Math.min(request.count(), request.parallelism()); submitted++) {
|
||||
active.add(submit(completions, request.generator(), submitted));
|
||||
}
|
||||
for (int completed = 0; completed < request.count(); completed++) {
|
||||
Future<Completed<T>> pending = completions.poll(policy.completionTimeout().toNanos(), TimeUnit.NANOSECONDS);
|
||||
if (pending == null) {
|
||||
throw new TimeoutException("No generation worker completed within " + policy.completionTimeout());
|
||||
}
|
||||
active.remove(pending);
|
||||
Completed<T> result = pending.get();
|
||||
if (submitted < request.count()) {
|
||||
active.add(submit(completions, request.generator(), submitted++));
|
||||
}
|
||||
request.sink().accept(result.index(), result.value());
|
||||
}
|
||||
} catch (Exception | Error failure) {
|
||||
for (Future<Completed<T>> future : active) {
|
||||
future.cancel(true);
|
||||
}
|
||||
workers.shutdownNow();
|
||||
drain(workers, policy, failure);
|
||||
throw failure;
|
||||
}
|
||||
workers.shutdown();
|
||||
drain(workers, policy, null);
|
||||
}
|
||||
|
||||
private static void drain(ExecutorService workers, Policy policy, Throwable failure) {
|
||||
boolean interrupted = Thread.interrupted() || failure instanceof InterruptedException;
|
||||
long deadline = System.nanoTime() + policy.drainTimeout().toNanos();
|
||||
try {
|
||||
while (!workers.isTerminated()) {
|
||||
long remaining = deadline - System.nanoTime();
|
||||
if (remaining <= 0L) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
workers.awaitTermination(remaining, TimeUnit.NANOSECONDS);
|
||||
} catch (InterruptedException interruption) {
|
||||
interrupted = true;
|
||||
}
|
||||
}
|
||||
if (workers.isTerminated()) {
|
||||
return;
|
||||
}
|
||||
IllegalStateException stalled = new IllegalStateException(
|
||||
"Generation workers did not stop within " + policy.drainTimeout()
|
||||
+ "; generation resources remain open and no checkpoint will be published", failure);
|
||||
stalled.printStackTrace(System.err);
|
||||
if (policy.haltOnUnresponsiveWorkers()) {
|
||||
System.err.println("Terminating offline generation; incomplete output is retained for inspection");
|
||||
System.err.flush();
|
||||
Runtime.getRuntime().halt(UNRESPONSIVE_WORKER_EXIT);
|
||||
}
|
||||
System.err.println("Waiting for embedded generation workers to stop before releasing their resources");
|
||||
while (!workers.isTerminated()) {
|
||||
try {
|
||||
workers.awaitTermination(1L, TimeUnit.DAYS);
|
||||
} catch (InterruptedException interruption) {
|
||||
interrupted = true;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> Future<Completed<T>> submit(ExecutorCompletionService<Completed<T>> completions,
|
||||
Generator<T> generator, int index) {
|
||||
return completions.submit(() -> new Completed<>(index, generator.generate(index)));
|
||||
}
|
||||
|
||||
record Policy(Duration completionTimeout, Duration drainTimeout, boolean haltOnUnresponsiveWorkers) {
|
||||
static final Policy EMBEDDED = new Policy(Duration.ofMinutes(5), Duration.ofSeconds(30), false);
|
||||
static final Policy STANDALONE = new Policy(Duration.ofMinutes(5), Duration.ofSeconds(30), true);
|
||||
|
||||
Policy {
|
||||
Objects.requireNonNull(completionTimeout, "completionTimeout");
|
||||
Objects.requireNonNull(drainTimeout, "drainTimeout");
|
||||
if (completionTimeout.isNegative() || completionTimeout.isZero()
|
||||
|| drainTimeout.isNegative() || drainTimeout.isZero()) {
|
||||
throw new IllegalArgumentException("Worker completion and drain timeouts must be positive");
|
||||
}
|
||||
completionTimeout.toNanos();
|
||||
drainTimeout.toNanos();
|
||||
}
|
||||
}
|
||||
|
||||
record Request<T>(int count, int parallelism, Generator<T> generator, Sink<T> sink) {
|
||||
Request {
|
||||
if (count < 1 || count > 4096 || parallelism < 1 || parallelism > 32) {
|
||||
throw new IllegalArgumentException("Generation window requires 1..4096 tasks and 1..32 workers");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record Completed<T>(int index, T value) {
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface Generator<T> {
|
||||
T generate(int index) throws Exception;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface Sink<T> {
|
||||
void accept(int index, T value) throws Exception;
|
||||
}
|
||||
}
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.probe.NativeRegionPlan.PlannedChunk;
|
||||
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import net.minecraft.server.level.ChunkMap;
|
||||
import net.minecraft.server.level.GenerationChunkHolder;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.StaticCache2D;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.UpgradeData;
|
||||
import net.minecraft.world.level.chunk.status.ChunkPyramid;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStep;
|
||||
import net.minecraft.world.level.chunk.status.WorldGenContext;
|
||||
import net.minecraft.world.level.chunk.storage.RegionFile;
|
||||
import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
|
||||
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HexFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
final class DetachedNativeTerrain {
|
||||
private static final NamespacedKey NATURAL_TERRAIN = new NamespacedKey("iris", "natural_terrain");
|
||||
|
||||
private DetachedNativeTerrain() {
|
||||
}
|
||||
|
||||
static Result generate(Request request, Consumer<String> progress) throws Exception {
|
||||
List<PlannedChunk> plan = NativeRegionPlan.plan(request.chunkX(), request.chunkZ(), request.width());
|
||||
Path output = Files.createDirectory(request.output());
|
||||
WorldGenContext context = context(request.level().getChunkSource().chunkMap);
|
||||
Map<Long, DetachedHolder> holders = new HashMap<>(plan.size());
|
||||
for (PlannedChunk planned : plan) {
|
||||
ChunkPos position = new ChunkPos(planned.x(), planned.z());
|
||||
holders.put(position.pack(), new DetachedHolder(new ProtoChunk(position, UpgradeData.EMPTY,
|
||||
request.level(), request.level().palettedContainerFactory(), null)));
|
||||
}
|
||||
long started = System.nanoTime();
|
||||
for (ChunkStatus status : ChunkStatus.getStatusList()) {
|
||||
if (status == ChunkStatus.EMPTY) {
|
||||
continue;
|
||||
}
|
||||
if (status.isAfter(ChunkStatus.TERRAIN)) {
|
||||
break;
|
||||
}
|
||||
ChunkStep step = ChunkPyramid.GENERATION_PYRAMID.getStepTo(status);
|
||||
executeStage(new Stage(context, step, plan, holders, request.parallelism()), progress);
|
||||
}
|
||||
long serializationStarted = System.nanoTime();
|
||||
write(request.level(), output, plan, holders);
|
||||
progress.accept("stage=native_serialization seconds=" + (System.nanoTime() - serializationStarted) / 1_000_000_000.0);
|
||||
double seconds = (System.nanoTime() - started) / 1_000_000_000.0;
|
||||
return new Result(request.width() * request.width(), plan.size(), seconds);
|
||||
}
|
||||
|
||||
private static void executeStage(Stage stage, Consumer<String> progress) throws Exception {
|
||||
long started = System.nanoTime();
|
||||
List<PlannedChunk> eligible = new ArrayList<>();
|
||||
for (PlannedChunk planned : stage.plan()) {
|
||||
if (planned.status().isOrAfter(stage.step().targetStatus())) {
|
||||
eligible.add(planned);
|
||||
}
|
||||
}
|
||||
AtomicInteger completed = new AtomicInteger();
|
||||
AtomicInteger targetCompleted = new AtomicInteger();
|
||||
RegionGenerationWindow.process(new RegionGenerationWindow.Request<>(eligible.size(), stage.parallelism(),
|
||||
index -> {
|
||||
PlannedChunk planned = eligible.get(index);
|
||||
apply(stage, planned);
|
||||
return planned;
|
||||
}, (index, planned) -> {
|
||||
int count = completed.incrementAndGet();
|
||||
if (planned.target()) {
|
||||
targetCompleted.incrementAndGet();
|
||||
}
|
||||
if (count % 128 == 0) {
|
||||
progress.accept("stage=" + stage.step().targetStatus().getName() + " completed=" + count
|
||||
+ " elapsedSeconds=" + (System.nanoTime() - started) / 1_000_000_000.0);
|
||||
}
|
||||
}));
|
||||
progress.accept("stage=" + stage.step().targetStatus().getName() + " chunks=" + completed.get()
|
||||
+ " targetChunks=" + targetCompleted.get() + " haloChunks=" + (completed.get() - targetCompleted.get())
|
||||
+ " seconds=" + (System.nanoTime() - started) / 1_000_000_000.0);
|
||||
}
|
||||
|
||||
private static void apply(Stage stage, PlannedChunk planned) {
|
||||
DetachedHolder center = requireHolder(stage.holders(), planned.x(), planned.z());
|
||||
StaticCache2D<GenerationChunkHolder> cache = StaticCache2D.create(planned.x(), planned.z(),
|
||||
stage.step().directDependencies().size() - 1, (x, z) -> requireHolder(stage.holders(), x, z));
|
||||
ChunkAccess result = stage.step().apply(stage.context(), cache, center.chunk).join();
|
||||
if (result != center.chunk || result.getPersistedStatus() != stage.step().targetStatus()) {
|
||||
throw new IllegalStateException("Native stage did not retain its detached chunk: " + stage.step().targetStatus());
|
||||
}
|
||||
}
|
||||
|
||||
private static DetachedHolder requireHolder(Map<Long, DetachedHolder> holders, int x, int z) {
|
||||
DetachedHolder holder = holders.get(ChunkPos.pack(x, z));
|
||||
if (holder == null) {
|
||||
throw new IllegalStateException("Native dependency escaped the terrain halo at " + x + "," + z);
|
||||
}
|
||||
return holder;
|
||||
}
|
||||
|
||||
private static WorldGenContext context(ChunkMap map) throws ReflectiveOperationException {
|
||||
for (Field field : ChunkMap.class.getDeclaredFields()) {
|
||||
if (field.getType() == WorldGenContext.class) {
|
||||
field.setAccessible(true);
|
||||
return (WorldGenContext) field.get(map);
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("Native world generation context is unavailable");
|
||||
}
|
||||
|
||||
private static void write(ServerLevel level, Path output, List<PlannedChunk> plan,
|
||||
Map<Long, DetachedHolder> holders) throws IOException {
|
||||
List<PlannedChunk> ordered = new ArrayList<>(plan);
|
||||
ordered.sort(Comparator.comparingInt((PlannedChunk chunk) -> chunk.z() >> 5)
|
||||
.thenComparingInt(chunk -> chunk.x() >> 5).thenComparingInt(PlannedChunk::z).thenComparingInt(PlannedChunk::x));
|
||||
StringBuilder receipts = new StringBuilder();
|
||||
RegionFile region = null;
|
||||
long currentRegion = Long.MIN_VALUE;
|
||||
try {
|
||||
for (PlannedChunk planned : ordered) {
|
||||
if (planned.status() == ChunkStatus.EMPTY) {
|
||||
continue;
|
||||
}
|
||||
long key = ChunkPos.pack(planned.x() >> 5, planned.z() >> 5);
|
||||
if (key != currentRegion) {
|
||||
if (region != null) {
|
||||
region.close();
|
||||
}
|
||||
Path file = output.resolve("r." + (planned.x() >> 5) + "." + (planned.z() >> 5) + ".mca");
|
||||
region = new RegionFile(new RegionStorageInfo(level.serverLevelData.getLevelName(),
|
||||
level.dimension(), "chunk"), file, output, false);
|
||||
currentRegion = key;
|
||||
}
|
||||
ChunkAccess chunk = holders.get(ChunkPos.pack(planned.x(), planned.z())).chunk;
|
||||
if (planned.status() == ChunkStatus.TERRAIN) {
|
||||
receipts.append(planned.x()).append(",").append(planned.z()).append("=")
|
||||
.append(receiptHash(chunk)).append("\n");
|
||||
}
|
||||
CompoundTag tag = SerializableChunkData.copyOf(level, chunk).write();
|
||||
NativeRegionTerrainWriter.write(region, tag);
|
||||
}
|
||||
Files.writeString(output.resolve("receipts.properties"), receipts.toString(), StandardCharsets.UTF_8);
|
||||
} finally {
|
||||
if (region != null) {
|
||||
region.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static String receiptHash(ChunkAccess chunk) {
|
||||
byte[] receipt = chunk.persistentDataContainer.get(NATURAL_TERRAIN,
|
||||
PersistentDataType.BYTE_ARRAY);
|
||||
if (receipt == null || receipt.length == 0) {
|
||||
throw new IllegalStateException("Missing Iris natural terrain receipt at " + chunk.getPos());
|
||||
}
|
||||
try {
|
||||
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(receipt));
|
||||
} catch (NoSuchAlgorithmException failure) {
|
||||
throw new IllegalStateException(failure);
|
||||
}
|
||||
}
|
||||
|
||||
record Request(ServerLevel level, int chunkX, int chunkZ, int width, int parallelism, Path output) {
|
||||
Request {
|
||||
if (parallelism < 1 || parallelism > 16) {
|
||||
throw new IllegalArgumentException("Native terrain worker count must be 1..16");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record Stage(WorldGenContext context, ChunkStep step, List<PlannedChunk> plan,
|
||||
Map<Long, DetachedHolder> holders, int parallelism) {
|
||||
}
|
||||
|
||||
record Result(int terrainChunks, int dependencyChunks, double seconds) {
|
||||
}
|
||||
|
||||
|
||||
private static final class DetachedHolder extends GenerationChunkHolder {
|
||||
private final ProtoChunk chunk;
|
||||
|
||||
private DetachedHolder(ProtoChunk chunk) {
|
||||
super(chunk.getPos());
|
||||
this.chunk = chunk;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChunkAccess getChunkIfPresentUnchecked(ChunkStatus status) {
|
||||
if (chunk.getPersistedStatus().isBefore(status)) {
|
||||
throw new IllegalStateException("Native dependency is not ready: " + getPos() + " needs " + status);
|
||||
}
|
||||
return chunk;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addSaveDependency(CompletableFuture<?> dependency) {
|
||||
throw new UnsupportedOperationException("Detached chunks have no live save queue");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTicketLevel() {
|
||||
throw new UnsupportedOperationException("Detached chunks have no live tickets");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getQueueLevel() {
|
||||
throw new UnsupportedOperationException("Detached chunks have no live queue");
|
||||
}
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.chunk.status.ChunkPyramid;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStep;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class DetachedNativeTerrainTest {
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fullRegionPlanSatisfiesEveryNativeStageDependency() {
|
||||
List<NativeRegionPlan.PlannedChunk> plan = NativeRegionPlan.plan(-32, 64, 32);
|
||||
Map<Long, ChunkStatus> statuses = new HashMap<>();
|
||||
int terrain = 0;
|
||||
for (NativeRegionPlan.PlannedChunk chunk : plan) {
|
||||
statuses.put(ChunkPos.pack(chunk.x(), chunk.z()), chunk.status());
|
||||
if (chunk.status() == ChunkStatus.TERRAIN) {
|
||||
terrain++;
|
||||
}
|
||||
}
|
||||
assertEquals(1296, terrain);
|
||||
assertEquals(2916, plan.size());
|
||||
assertTrue(plan.size() < 4096);
|
||||
for (NativeRegionPlan.PlannedChunk chunk : plan) {
|
||||
for (ChunkStatus status : ChunkStatus.getStatusList()) {
|
||||
if (status == ChunkStatus.EMPTY || status.isAfter(chunk.status())) {
|
||||
continue;
|
||||
}
|
||||
ChunkStep step = ChunkPyramid.GENERATION_PYRAMID.getStepTo(status);
|
||||
int radius = step.directDependencies().size() - 1;
|
||||
for (int z = -radius; z <= radius; z++) {
|
||||
for (int x = -radius; x <= radius; x++) {
|
||||
ChunkStatus required = step.directDependencies().get(Math.max(Math.abs(x), Math.abs(z)));
|
||||
ChunkStatus available = statuses.get(ChunkPos.pack(chunk.x() + x, chunk.z() + z));
|
||||
assertTrue("Missing native stage dependency", available != null && available.isOrAfter(required));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeBiomeSourceImpl;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.packs.PackType;
|
||||
import net.minecraft.server.packs.repository.ServerPacksSource;
|
||||
import net.minecraft.server.packs.resources.MultiPackResourceManager;
|
||||
import net.minecraft.util.datafix.DataFixers;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.chunk.PalettedContainerFactory;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.UpgradeData;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.bukkit.craftbukkit.generator.CustomChunkGenerator;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.clearInvocations;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
public class HeadlessChunkGeneratorTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporary = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void nativeStructureStateAndOriginMatchThePaperGeneratorDefaults() throws Exception {
|
||||
int checked = 0;
|
||||
for (Method method : CustomChunkGenerator.class.getMethods()) {
|
||||
if (!method.getName().equals("getOrigin") && !method.getName().equals("createState")) {
|
||||
continue;
|
||||
}
|
||||
assertEquals(ChunkGenerator.class, method.getDeclaringClass());
|
||||
assertEquals(method, HeadlessChunkGenerator.class.getMethod(method.getName(), method.getParameterTypes()));
|
||||
checked++;
|
||||
}
|
||||
assertEquals(2, checked);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativeSwampHutQueriesRealPackHeightsAndBiomesWithoutAServer() throws Exception {
|
||||
long seed = 69420L;
|
||||
HeadlessNativeRuntime runtime = new HeadlessNativeRuntime();
|
||||
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(new RealPackProbeSupport.WorkspaceOptions(
|
||||
pack().toFile(), "main", "[native-column-test]", runtime, temporary.getRoot().toPath()));
|
||||
RealPackProbeSupport.EngineSession session = workspace.openEngine(seed, false, "columns");
|
||||
MultiPackResourceManager resources = new MultiPackResourceManager(PackType.SERVER_DATA,
|
||||
List.of(ServerPacksSource.createVanillaPackSource().fullResources()));
|
||||
LevelStorageSource.LevelStorageAccess storage = LevelStorageSource.createDefault(
|
||||
temporary.newFolder().toPath()).createAccess("native")) {
|
||||
Engine engine = session.engine();
|
||||
HeadlessChunkGenerator generator = spy(new HeadlessChunkGenerator(
|
||||
new HeadlessChunkGenerator.Options(engine, runtime.registries())));
|
||||
assertTrue(generator.getBiomeSource() instanceof NativeBiomeSourceImpl);
|
||||
assertEquals(engine.getMinHeight(), generator.getMinY());
|
||||
assertEquals(engine.getHeight(), generator.getGenDepth());
|
||||
assertEquals(engine.getMinHeight() + engine.getDimension().getFluidHeight(), generator.getSeaLevel());
|
||||
Holder<NoiseGeneratorSettings> settings = runtime.registries().lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||
.getOrThrow(NoiseGeneratorSettings.OVERWORLD);
|
||||
RandomState random = RandomState.create(runtime.registries().lookupOrThrow(Registries.NOISE), seed, settings.value());
|
||||
LevelHeightAccessor height = LevelHeightAccessor.create(generator.getMinY(), generator.getGenDepth());
|
||||
Holder<Biome> swamp = runtime.registries().lookupOrThrow(Registries.BIOME).getOrThrow(
|
||||
ResourceKey.create(Registries.BIOME, Identifier.parse("minecraft:swamp")));
|
||||
assertTrue(generator.getBiomeSource().possibleBiomes().contains(swamp));
|
||||
for (int x : new int[]{-33, 0, 49}) {
|
||||
int z = -x - 7;
|
||||
int expected = engine.getMinHeight() + engine.getHeight(x, z, true) + 1;
|
||||
assertTrue(expected > generator.getSeaLevel());
|
||||
assertEquals(expected, generator.getBaseHeight(x, z, Heightmap.Types.OCEAN_FLOOR_WG, height, random));
|
||||
NoiseColumn column = generator.getBaseColumn(x, z, height, random);
|
||||
assertFalse(column.getBlock(expected - 1).isAir());
|
||||
assertTrue(column.getBlock(expected).isAir());
|
||||
assertEquals(swamp, generator.getBiomeSource().createResolver(
|
||||
random.createClimateSampler(SamplerContext.builder().enableCaches().build()))
|
||||
.getNoiseBiome(x >> 2, expected >> 2, z >> 2));
|
||||
}
|
||||
Holder.Reference<Structure> hut = runtime.registries().lookupOrThrow(Registries.STRUCTURE).getOrThrow(
|
||||
ResourceKey.create(Registries.STRUCTURE, Identifier.parse("minecraft:swamp_hut")));
|
||||
StructureTemplateManager templates = new StructureTemplateManager(resources, storage,
|
||||
DataFixers.getDataFixer(), BuiltInRegistries.BLOCK);
|
||||
ChunkPos position = new ChunkPos(-3, 2);
|
||||
ProtoChunk chunk = new ProtoChunk(position, UpgradeData.EMPTY, height,
|
||||
PalettedContainerFactory.create(runtime.registries()), null);
|
||||
clearInvocations(generator);
|
||||
StructureStart start = hut.value().generate(hut, Level.OVERWORLD, runtime.registries(), generator,
|
||||
generator.getBiomeSource(), random.createClimateSampler(SamplerContext.builder().enableCaches().build()),
|
||||
random, templates, seed, position, 0, chunk, hut.value().biomes()::contains);
|
||||
assertTrue(start.isValid());
|
||||
assertFalse(start.getPieces().isEmpty());
|
||||
verify(generator, atLeastOnce()).getBaseHeight(anyInt(), anyInt(), any(Heightmap.Types.class),
|
||||
any(LevelHeightAccessor.class), any(RandomState.class));
|
||||
StructureStart excluded = hut.value().generate(hut, Level.OVERWORLD, runtime.registries(), generator,
|
||||
generator.getBiomeSource(), random.createClimateSampler(SamplerContext.builder().enableCaches().build()),
|
||||
random, templates, seed, position, 0, chunk, selected -> !selected.equals(swamp));
|
||||
assertFalse(excluded.isValid());
|
||||
assertTrue(RealPackProbeSupport.drainReported().isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
private Path pack() throws Exception {
|
||||
Path pack = temporary.newFolder().toPath();
|
||||
for (String directory : List.of("dimensions", "regions", "biomes", "generators")) {
|
||||
Files.createDirectories(pack.resolve(directory));
|
||||
}
|
||||
Files.writeString(pack.resolve("dimensions/main.json"), """
|
||||
{"name":"Native columns","focus":"swamp","regions":["owned"],"fluidHeight":40,"logicalHeight":256,
|
||||
"landChance":1,"carvingEnabled":false,"hydrology":{"rivers":{"enabled":false}}}
|
||||
""");
|
||||
Files.writeString(pack.resolve("regions/owned.json"), "{\"landBiomes\":[\"swamp\"]}");
|
||||
Files.writeString(pack.resolve("generators/flat.json"), "{\"composite\":[]}");
|
||||
Files.writeString(pack.resolve("biomes/swamp.json"), """
|
||||
{"derivative":"minecraft:swamp","generators":[{"generator":"flat","min":35,"max":35}]}
|
||||
""");
|
||||
return pack;
|
||||
}
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk;
|
||||
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||
import it.unimi.dsi.fastutil.shorts.ShortArrayList;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.packs.PackType;
|
||||
import net.minecraft.server.packs.repository.ServerPacksSource;
|
||||
import net.minecraft.server.packs.resources.MultiPackResourceManager;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.util.datafix.DataFixers;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.chunk.PalettedContainerFactory;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.UpgradeData;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
import net.minecraft.world.level.material.Fluids;
|
||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
import net.minecraft.world.ticks.ScheduledTick;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class HeadlessChunkSerializationTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporary = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void snapshotMatchesNativeCopyOfIncludingStructuresLightAndReceipts() throws Exception {
|
||||
RegistryAccess registries = HeadlessNativeTestRegistries.get();
|
||||
PalettedContainerFactory containers = PalettedContainerFactory.create(registries);
|
||||
LevelHeightAccessor height = LevelHeightAccessor.create(-64, 384);
|
||||
ProtoChunk chunk = new ProtoChunk(new ChunkPos(-33, 17), UpgradeData.EMPTY, height, containers, null);
|
||||
BlockPos block = new BlockPos(-528, -40, 272);
|
||||
chunk.setBlockState(block, Blocks.CHEST.defaultBlockState(), 0);
|
||||
CompoundTag chest = new CompoundTag();
|
||||
chest.putString("id", "minecraft:chest");
|
||||
chest.putInt("x", block.getX());
|
||||
chest.putInt("y", block.getY());
|
||||
chest.putInt("z", block.getZ());
|
||||
chest.putString("LootTable", "minecraft:chests/simple_dungeon");
|
||||
chunk.setBlockEntityNbt(chest);
|
||||
CompoundTag entity = new CompoundTag();
|
||||
entity.putString("id", "minecraft:pig");
|
||||
chunk.addEntity(entity);
|
||||
chunk.getBlockTicks().schedule(new ScheduledTick<>(Blocks.CHEST, block, 37L, 2L));
|
||||
chunk.getFluidTicks().schedule(new ScheduledTick<>(Fluids.WATER, block.above(), 41L, 3L));
|
||||
chunk.addPackedPostProcess(new ShortArrayList(new short[]{17, 2049}), 1);
|
||||
chunk.setInhabitedTime(192L);
|
||||
Heightmap.primeHeightmaps(chunk, EnumSet.allOf(Heightmap.Types.class));
|
||||
chunk.setPersistedStatus(ChunkStatus.TERRAIN);
|
||||
chunk.persistentDataContainer.set(new NamespacedKey("iris", "natural_terrain"),
|
||||
PersistentDataType.BYTE_ARRAY, new byte[]{1, -8, 92, 0});
|
||||
StarlightChunk light = (StarlightChunk) chunk;
|
||||
light.starlight$getBlockNibbles()[1].setFull();
|
||||
light.starlight$getBlockNibbles()[1].updateVisible();
|
||||
light.starlight$getSkyNibbles()[0].setFull();
|
||||
light.starlight$getSkyNibbles()[0].updateVisible();
|
||||
chunk.setLightCorrect(true);
|
||||
Structure hut = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(
|
||||
ResourceKey.create(Registries.STRUCTURE, Identifier.parse("minecraft:swamp_hut"))).value();
|
||||
StructureStart start = new StructureStart(hut, chunk.getPos(), 2,
|
||||
new PiecesContainer(List.of(new SwampHutPiece(RandomSource.create(69420L), block.getX(), block.getZ()))));
|
||||
start.persistentDataContainer.set(new NamespacedKey("iris", "structure_activation"), PersistentDataType.LONG, 73L);
|
||||
chunk.setAllStarts(Map.of(hut, start));
|
||||
chunk.setAllReferences(Map.of(hut, new LongOpenHashSet(new long[]{ChunkPos.pack(-32, 17)})));
|
||||
try (MultiPackResourceManager resources = new MultiPackResourceManager(PackType.SERVER_DATA,
|
||||
List.of(ServerPacksSource.createVanillaPackSource().fullResources()));
|
||||
LevelStorageSource.LevelStorageAccess storage = LevelStorageSource.createDefault(
|
||||
temporary.newFolder().toPath()).createAccess("serialization")) {
|
||||
StructureTemplateManager templates = new StructureTemplateManager(resources, storage,
|
||||
DataFixers.getDataFixer(), BuiltInRegistries.BLOCK);
|
||||
StructurePieceSerializationContext structures = new StructurePieceSerializationContext(resources, registries, templates);
|
||||
HeadlessChunkSerialization.Context context = new HeadlessChunkSerialization.Context(structures, containers, 29L);
|
||||
MinecraftServer server = mock(MinecraftServer.class);
|
||||
when(server.getResourceManager()).thenReturn(resources);
|
||||
when(server.registryAccess()).thenReturn(registries.freeze());
|
||||
when(server.getStructureTemplateManager()).thenReturn(templates);
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
when(level.getServer()).thenReturn(server);
|
||||
when(level.registryAccess()).thenReturn(registries);
|
||||
when(level.palettedContainerFactory()).thenReturn(containers);
|
||||
when(level.getGameTime()).thenReturn(29L);
|
||||
when(level.getMinSectionY()).thenReturn(height.getMinSectionY());
|
||||
when(level.getMaxSectionY()).thenReturn(height.getMaxSectionY());
|
||||
CompoundTag nativeTag = SerializableChunkData.copyOf(level, chunk).write();
|
||||
SerializableChunkData snapshot = HeadlessChunkSerialization.copyOf(chunk, context);
|
||||
CompoundTag headless = snapshot.write();
|
||||
assertEquals(nativeTag, headless);
|
||||
assertEquals("minecraft:terrain", headless.getStringOr("Status", ""));
|
||||
assertEquals(29L, headless.getLongOr("LastUpdate", -1L));
|
||||
assertEquals(192L, headless.getLongOr("InhabitedTime", -1L));
|
||||
assertArrayEquals(new byte[]{1, -8, 92, 0}, headless.getCompoundOrEmpty("ChunkBukkitValues")
|
||||
.getByteArray("iris:natural_terrain").orElseThrow());
|
||||
assertFalse(headless.getListOrEmpty("block_entities").isEmpty());
|
||||
assertFalse(headless.getListOrEmpty("block_ticks").isEmpty());
|
||||
assertFalse(headless.getListOrEmpty("fluid_ticks").isEmpty());
|
||||
assertTrue(headless.getCompoundOrEmpty("structures").getCompoundOrEmpty("starts").contains("minecraft:swamp_hut"));
|
||||
CompoundTag serializedStart = headless.getCompoundOrEmpty("structures").getCompoundOrEmpty("starts")
|
||||
.getCompoundOrEmpty("minecraft:swamp_hut");
|
||||
StructureStart restored = StructureStart.loadStaticStart(structures, serializedStart, 69420L);
|
||||
assertTrue(restored.isValid());
|
||||
assertEquals(start.getBoundingBox(), restored.getBoundingBox());
|
||||
assertEquals(73L, serializedStart.getCompoundOrEmpty("StructureBukkitValues")
|
||||
.getLongOr("iris:structure_activation", -1L));
|
||||
chunk.getSection(1).setBlockState(0, 8, 0, Blocks.AIR.defaultBlockState());
|
||||
chunk.getPostProcessing()[1].clear();
|
||||
chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE).getRawData()[0] = 0L;
|
||||
assertEquals(headless, snapshot.write());
|
||||
for (ChunkStatus status : List.of(ChunkStatus.EMPTY, ChunkStatus.STRUCTURE_STARTS,
|
||||
ChunkStatus.STRUCTURE_REFERENCES, ChunkStatus.BIOMES, ChunkStatus.TERRAIN)) {
|
||||
ProtoChunk fresh = new ProtoChunk(new ChunkPos(0, -1), UpgradeData.EMPTY, height, containers, null);
|
||||
fresh.setPersistedStatus(status);
|
||||
CompoundTag freshTag = HeadlessChunkSerialization.copyOf(fresh, context).write();
|
||||
assertEquals(SerializableChunkData.copyOf(level, fresh).write(), freshTag);
|
||||
assertEquals(status, SerializableChunkData.getChunkStatusFromTag(freshTag));
|
||||
assertFalse(freshTag.contains("ChunkBukkitValues"));
|
||||
assertTrue(freshTag.getListOrEmpty("entities").isEmpty());
|
||||
assertTrue(freshTag.getListOrEmpty("block_entities").isEmpty());
|
||||
}
|
||||
chunk.setPersistedStatus(ChunkStatus.FULL);
|
||||
assertThrows(IllegalArgumentException.class, () -> HeadlessChunkSerialization.copyOf(chunk, context));
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.spi.PlatformGenerationRegistry;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeWorldGenerationImpl;
|
||||
import net.minecraft.data.registries.VanillaRegistries;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
public final class HeadlessGenerationRegistryTest {
|
||||
@Test
|
||||
public void capturesGeneratedDefinitionsThroughNativeCodecs() throws Exception {
|
||||
HeadlessNativeBootstrap.initialize();
|
||||
String biome = HeadlessNativeTestRegistries.generatedBiome();
|
||||
String dimension = HeadlessNativeTestRegistries.generatedDimension();
|
||||
HeadlessNativePlatform platform = new HeadlessNativePlatform(new File("unused-native-platform"),
|
||||
HeadlessNativeTestRegistries::get);
|
||||
PlatformGenerationRegistry registry = platform.generationRegistry();
|
||||
assertEquals("bukkit-generation-registry-v1", registry.runtimeIdentity());
|
||||
assertEquals(new NativeWorldGenerationImpl().generationRendererIdentity(),
|
||||
registry.generatedDefinitionRendererIdentity());
|
||||
assertEquals(registry.canonicalDefinition("minecraft:worldgen/biome", "iris:native_test", biome),
|
||||
registry.generatedDefinition("minecraft:worldgen/biome", "iris:native_test"));
|
||||
assertEquals(registry.canonicalDefinition("minecraft:dimension_type", "iris:native_test", dimension),
|
||||
registry.generatedDefinition("minecraft:dimension_type", "iris:native_test"));
|
||||
assertEquals(PlatformGenerationRegistry.Definition.resourceIdentity("minecraft:block", "minecraft:stone"),
|
||||
registry.definition("minecraft:block", "minecraft:stone"));
|
||||
assertThrows(IllegalStateException.class,
|
||||
() -> registry.generatedDefinition("minecraft:worldgen/biome", "iris:absent"));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> registry.canonicalDefinition("minecraft:worldgen/biome", "iris:invalid", "{}"));
|
||||
assertNull(Bukkit.getServer());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsContractCaptureBeforeCoherentRegistriesLoad() {
|
||||
HeadlessNativeBootstrap.initialize();
|
||||
HeadlessNativePlatform platform = new HeadlessNativePlatform(new File("unused-native-platform"),
|
||||
VanillaRegistries::createWorldLookup);
|
||||
assertThrows(IllegalStateException.class,
|
||||
() -> platform.generationRegistry().definition("minecraft:block", "minecraft:stone"));
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.world.storage.matter.TileWrapper;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.IrisEngineMantle;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Comparator;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class HeadlessMantleInspection {
|
||||
private HeadlessMantleInspection() {
|
||||
}
|
||||
|
||||
public static void main(String[] arguments) throws Exception {
|
||||
if (arguments.length != 6) {
|
||||
throw new IllegalArgumentException("Expected pack, dimension, seed, world, regionX, regionZ");
|
||||
}
|
||||
Path temporary = Files.createTempDirectory("iris-mantle-inspection-");
|
||||
Path copied = temporary.resolve("mantle");
|
||||
try {
|
||||
Process copy = new ProcessBuilder("cp", "-Rc", Path.of(arguments[3]).resolve("mantle-hydrology").toString(),
|
||||
copied.toString()).inheritIO().start();
|
||||
if (copy.waitFor() != 0) {
|
||||
throw new IllegalStateException("Could not clone retained mantle for read-only inspection");
|
||||
}
|
||||
inspect(arguments, copied);
|
||||
} finally {
|
||||
try (Stream<Path> paths = Files.walk(temporary)) {
|
||||
for (Path path : paths.sorted(Comparator.reverseOrder()).toList()) {
|
||||
Files.delete(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void inspect(String[] arguments, Path copied) throws Exception {
|
||||
HeadlessNativeRuntime runtime = new HeadlessNativeRuntime();
|
||||
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(new RealPackProbeSupport.WorkspaceOptions(
|
||||
new File(arguments[0]), arguments[1], "[mantle-inspection]", runtime,
|
||||
Path.of(System.getProperty("java.io.tmpdir"))));
|
||||
RealPackProbeSupport.EngineSession session = workspace.openEngine(Long.parseLong(arguments[2]), false, "inspect")) {
|
||||
Engine engine = session.engine();
|
||||
Mantle<Matter> mantle = IrisEngineMantle.createMantle(engine, copied, engine::getData);
|
||||
try {
|
||||
AtomicInteger tiles = new AtomicInteger();
|
||||
Map<String, Integer> materials = new TreeMap<>();
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
int minimumX = Integer.parseInt(arguments[4]) << 5;
|
||||
int minimumZ = Integer.parseInt(arguments[5]) << 5;
|
||||
for (int z = minimumZ; z < minimumZ + 32; z++) {
|
||||
for (int x = minimumX; x < minimumX + 32; x++) {
|
||||
mantle.iterateChunk(x, z, TileWrapper.class, (localX, y, localZ, wrapper) -> {
|
||||
TileData tile = wrapper.getData();
|
||||
tiles.incrementAndGet();
|
||||
materials.merge(tile.getMaterialKey(), 1, Integer::sum);
|
||||
digest.update(encoded(tile));
|
||||
});
|
||||
}
|
||||
}
|
||||
System.out.println("retainedMantleTiles=" + tiles.get() + " materials=" + materials
|
||||
+ " sha256=" + HexFormat.of().formatHex(digest.digest()));
|
||||
} finally {
|
||||
mantle.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] encoded(TileData tile) {
|
||||
try (ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
DataOutputStream output = new DataOutputStream(bytes)) {
|
||||
tile.toBinary(output);
|
||||
return bytes.toByteArray();
|
||||
} catch (Exception failure) {
|
||||
throw new IllegalStateException("Cannot serialize retained tile " + tile.getMaterialKey(), failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockProperties;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.times;
|
||||
|
||||
public final class HeadlessNativeBlockPredicateTest {
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
HeadlessNativeBootstrap.initialize();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void memoizedPredicatesMatchEveryNativeBlockStateIncludingStateProperties() {
|
||||
for (Block block : BuiltInRegistries.BLOCK) {
|
||||
for (BlockState state : block.getStateDefinition().getPossibleStates()) {
|
||||
ModdedBlockState wrapped = ModdedBlockState.deferred(state, Map.of(), "iris:test");
|
||||
for (int iteration = 0; iteration < 2; iteration++) {
|
||||
assertEquals(NativeBlockProperties.isStorage(state), wrapped.isStorage());
|
||||
assertEquals(NativeBlockProperties.isUpdatable(state), wrapped.isUpdatable());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void repeatedWritesClassifyImmutableStateOnlyOnce() {
|
||||
BlockState state = Blocks.CHEST.defaultBlockState();
|
||||
ModdedBlockState wrapped = ModdedBlockState.deferred(state, Map.of(), "iris:chest");
|
||||
boolean storage = NativeBlockProperties.isStorage(state);
|
||||
boolean updatable = NativeBlockProperties.isUpdatable(state);
|
||||
try (MockedStatic<NativeBlockProperties> properties = mockStatic(NativeBlockProperties.class, CALLS_REAL_METHODS)) {
|
||||
for (int iteration = 0; iteration < 512; iteration++) {
|
||||
assertEquals(storage, wrapped.isStorage());
|
||||
assertEquals(updatable, wrapped.isUpdatable());
|
||||
}
|
||||
properties.verify(() -> NativeBlockProperties.isUpdatable(state), times(1));
|
||||
properties.verify(() -> NativeBlockProperties.isStorage(state), times(2));
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
|
||||
import java.util.Map;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import org.junit.Test;
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class HeadlessNativeBootstrapTest {
|
||||
@Test
|
||||
public void bindsNativeItemComponentsWithoutStartingServer() throws Exception {
|
||||
HeadlessNativeBootstrap.bindComponents(HeadlessNativeTestRegistries.get());
|
||||
for (Item item : BuiltInRegistries.ITEM) {
|
||||
ItemStack stack = new ItemStack(item);
|
||||
if (item != Items.AIR) {
|
||||
assertTrue(stack.getMaxStackSize() > 0);
|
||||
}
|
||||
}
|
||||
assertNotNull(new ItemStack(Items.APPLE).get(DataComponents.FOOD));
|
||||
assertEquals(64, new ItemStack(Items.TRIAL_KEY).getMaxStackSize());
|
||||
assertTrue(Bukkit.getServer() == null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadsActualNativeTagsWithoutStartingServer() throws Exception {
|
||||
HeadlessNativeBootstrap.initialize();
|
||||
assertTrue(Blocks.OAK_LOG.defaultBlockState().is(BlockTags.LOGS));
|
||||
assertTrue(Blocks.OAK_LEAVES.defaultBlockState().is(BlockTags.LEAVES));
|
||||
assertTrue(Blocks.STONE.defaultBlockState().is(BlockTags.BLOCKS_MOTION));
|
||||
assertFalse(Blocks.WATER.defaultBlockState().is(BlockTags.BLOCKS_MOTION));
|
||||
assertFalse(Blocks.SHORT_GRASS.defaultBlockState().is(BlockTags.BLOCKS_MOTION));
|
||||
assertTrue(Bukkit.getServer() == null);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import org.junit.Test;
|
||||
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class HeadlessNativeRegistriesTest {
|
||||
@Test
|
||||
public void nativeStructureBiomeTagsUseTheLoadedBiomeHolders() throws Exception {
|
||||
RegistryAccess registries = HeadlessNativeTestRegistries.get();
|
||||
Structure swampHut = registries.lookupOrThrow(Registries.STRUCTURE)
|
||||
.getValue(Identifier.parse("minecraft:swamp_hut"));
|
||||
Holder<Biome> swamp = registries.lookupOrThrow(Registries.BIOME)
|
||||
.getOrThrow(ResourceKey.create(Registries.BIOME, Identifier.parse("minecraft:swamp")));
|
||||
assertTrue(swampHut.biomes().contains(swamp));
|
||||
Holder<Biome> custom = registries.lookupOrThrow(Registries.BIOME)
|
||||
.getOrThrow(ResourceKey.create(Registries.BIOME, Identifier.parse("iris:coherent_swamp")));
|
||||
assertTrue(swampHut.biomes().contains(custom));
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeRegistryAccess;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Map;
|
||||
import java.io.File;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
public final class HeadlessNativeRegistryTest {
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
HeadlessNativeBootstrap.initialize();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void corePlatformUsesNativeResolutionWithoutBiomeFallback() throws Exception {
|
||||
NativeRegionTerrainWriter writer = new NativeRegionTerrainWriter(HeadlessNativeTestRegistries.get());
|
||||
HeadlessNativePlatform platform = new HeadlessNativePlatform(new File("unused-native-platform"), writer::registries);
|
||||
assertEquals("headless-native-core", platform.platformName());
|
||||
assertTrue(platform.block("minecraft:stone").isSolid());
|
||||
assertFalse(platform.block("minecraft:short_grass").isSolid());
|
||||
assertTrue(platform.block("minecraft:oak_log").isTreeBlock());
|
||||
assertTrue(platform.biomeWriter().biomeIdFor("minecraft:plains") >= 0);
|
||||
assertThrows(IllegalStateException.class, () -> platform.biomeWriter().biomeIdFor("iris:unknown"));
|
||||
assertThrows(UnsupportedOperationException.class, platform::lootTableKeys);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolvesNativeDynamicAndStaticRegistriesWithoutServer() throws Exception {
|
||||
NativeRegionTerrainWriter writer = new NativeRegionTerrainWriter(HeadlessNativeTestRegistries.get());
|
||||
NativeRegistryAccess access = new NativeRegistryAccess(new NativeRegistryAccess.Configuration(
|
||||
writer::registries, writer::registries, message -> {
|
||||
throw new AssertionError("Unavailable registry: " + message);
|
||||
}));
|
||||
assertNull(Bukkit.getServer());
|
||||
assertEquals("iris:headless/test", access.biome("iris:headless/test").key());
|
||||
assertNotNull(access.biome("minecraft:plains"));
|
||||
assertNull(access.biome("iris:unknown"));
|
||||
assertTrue(access.structureKeys().contains("minecraft:mineshaft"));
|
||||
assertTrue(access.enchantmentKeys().contains("minecraft:fortune"));
|
||||
assertEquals("minecraft:diamond_sword", access.item("DIAMOND SWORD").key());
|
||||
assertEquals("creature", access.entity("minecraft:cow").spawnCategory());
|
||||
assertTrue(access.blockStateProperties().containsKey("minecraft:oak_log"));
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockResolver;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class HeadlessNativeStateTest {
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
HeadlessNativeBootstrap.initialize();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativeMaterialsDistinguishDecorationsFluidsGlassAndBlockEntities() {
|
||||
assertFalse(NativeBlockResolver.strictParse("minecraft:short_grass").isSolid());
|
||||
assertTrue(NativeBlockResolver.strictParse("minecraft:glass").isSolid());
|
||||
assertFalse(NativeBlockResolver.strictParse("minecraft:glass").isOccluding());
|
||||
assertTrue(NativeBlockResolver.strictParse("minecraft:stone").isOccluding());
|
||||
assertTrue(NativeBlockResolver.strictParse("minecraft:oak_log").isTreeBlock());
|
||||
assertTrue(NativeBlockResolver.strictParse("minecraft:oak_leaves").isTreeBlock());
|
||||
assertTrue(NativeBlockResolver.strictParse("minecraft:chest").hasTileEntity());
|
||||
NativeBlockState slab = NativeBlockResolver.strictParse("minecraft:oak_slab[waterlogged=true]");
|
||||
assertTrue(slab.isWaterLogged());
|
||||
assertFalse(slab.isFluid());
|
||||
assertTrue(NativeBlockResolver.strictParse("minecraft:water").isFluid());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canonicalStateKeysRoundTripEveryNativeDefaultState() {
|
||||
for (Block block : BuiltInRegistries.BLOCK) {
|
||||
BlockState state = block.defaultBlockState();
|
||||
ModdedBlockState decoded = NativeBlockResolver.strictParse(ModdedBlockState.serialize(state));
|
||||
assertEquals(state, decoded.handle());
|
||||
}
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> NativeBlockResolver.strictParse("minecraft:oak_log[axis=invalid]"));
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.biome.IrisBiomeCustom;
|
||||
import art.arcane.iris.generation.terrain.IrisDimensionType;
|
||||
import art.arcane.iris.generation.terrain.IrisDimensionTypeOptions;
|
||||
import art.arcane.iris.pack.datapack.IDataFixer;
|
||||
import art.arcane.iris.pack.datapack.v263.DataFixerV263;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.data.registries.VanillaRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.Map;
|
||||
|
||||
final class HeadlessNativeTestRegistries {
|
||||
private static RegistryAccess registries;
|
||||
|
||||
private HeadlessNativeTestRegistries() {
|
||||
}
|
||||
|
||||
static synchronized RegistryAccess get() {
|
||||
if (registries != null) {
|
||||
return registries;
|
||||
}
|
||||
HeadlessNativeBootstrap.initialize();
|
||||
HolderLookup.Provider vanilla = VanillaRegistries.createWorldLookup();
|
||||
String plains = biome(vanilla, "minecraft:plains");
|
||||
String swamp = biome(vanilla, "minecraft:swamp");
|
||||
Map<String, String> definitions = Map.of(
|
||||
"data/iris/worldgen/biome/native_test.json", generatedBiome(),
|
||||
"data/iris/dimension_type/native_test.json", generatedDimension(),
|
||||
"data/iris/worldgen/biome/biomes/test.json", plains,
|
||||
"data/iris/worldgen/biome/headless/test.json", plains,
|
||||
"data/iris/worldgen/biome/coherent_swamp.json", swamp,
|
||||
"data/minecraft/tags/worldgen/biome/has_structure/swamp_hut.json",
|
||||
"{\"replace\":false,\"values\":[\"iris:coherent_swamp\"]}");
|
||||
try (HeadlessNativeRegistries loaded = HeadlessNativeRegistries.load(definitions)) {
|
||||
registries = loaded.registries();
|
||||
return registries;
|
||||
} catch (IOException failure) {
|
||||
throw new UncheckedIOException(failure);
|
||||
}
|
||||
}
|
||||
|
||||
static String generatedBiome() {
|
||||
return new IrisBiomeCustom().setId("native_test").generateJson(new DataFixerV263());
|
||||
}
|
||||
|
||||
static String generatedDimension() {
|
||||
return new IrisDimensionType(IDataFixer.Dimension.OVERWORLD,
|
||||
new IrisDimensionTypeOptions(), 768, 768, -256).toJson(new DataFixerV263());
|
||||
}
|
||||
|
||||
private static String biome(HolderLookup.Provider vanilla, String key) {
|
||||
Biome biome = vanilla.lookupOrThrow(Registries.BIOME).getOrThrow(
|
||||
ResourceKey.create(Registries.BIOME, Identifier.parse(key))).value();
|
||||
return Biome.DIRECT_CODEC.encodeStart(vanilla.createSerializationContext(JsonOps.INSTANCE), biome)
|
||||
.getOrThrow().toString();
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeTileData;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
|
||||
import art.arcane.iris.modded.ModdedTileData;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeTileReader;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.data.registries.VanillaRegistries;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
public final class HeadlessNativeTileTest {
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
HeadlessNativeBootstrap.initialize();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativeTileBinaryRetainsTypedPayloadWithoutWorld() throws Exception {
|
||||
NativeTileData captured = NativeTileData.capture("minecraft:player_head",
|
||||
"{id:\"minecraft:skull\",profile:{id:[I;1,2,3,4]},note:[B;5b,6b]}",
|
||||
message -> { throw new AssertionError(message); });
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
try (DataOutputStream output = new DataOutputStream(bytes)) {
|
||||
captured.toBinary(output);
|
||||
}
|
||||
NativeTileReader reader = new NativeTileReader(VanillaRegistries::createWorldLookup);
|
||||
NativeTileData decoded;
|
||||
try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(bytes.toByteArray()))) {
|
||||
decoded = reader.read(input);
|
||||
}
|
||||
CompoundTag payload = decoded.payload();
|
||||
assertEquals(captured.payload(), payload);
|
||||
assertArrayEquals(new int[]{1, 2, 3, 4}, payload.getCompoundOrEmpty("profile").getIntArray("id").orElseThrow());
|
||||
assertArrayEquals(new byte[]{5, 6}, payload.getByteArray("note").orElseThrow());
|
||||
assertEquals(captured, decoded);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writesNativeTilePayloadIntoChunkAndRejectsMismatchedBlocks() throws Exception {
|
||||
ModdedTileData tile = ModdedTileData.capture("minecraft:chest",
|
||||
"{LootTable:'minecraft:chests/simple_dungeon',LootTableSeed:17L}");
|
||||
HeadlessNativeTiles.Placement placement = new HeadlessNativeTiles.Placement(2, 4, 6,
|
||||
new BlockPos(-32, -64, 48), ModdedBlockState.of(Blocks.CHEST.defaultBlockState(), Map.of()), tile);
|
||||
NativeRegionTerrainWriter.TileInput input = HeadlessNativeTiles.prepare(placement);
|
||||
NativeRegionTerrainWriter writer = new NativeRegionTerrainWriter(HeadlessNativeTestRegistries.get());
|
||||
CompoundTag chunk = writer.encode(new NativeRegionTerrainWriter.TerrainInput(-2, 3, -64, 16,
|
||||
(x, y, z) -> x == 2 && y == 4 && z == 6 ? Blocks.CHEST.defaultBlockState() : Blocks.AIR.defaultBlockState(),
|
||||
(x, y, z) -> "minecraft:plains", List.of(input)));
|
||||
CompoundTag entity = chunk.getListOrEmpty("block_entities").getCompoundOrEmpty(0);
|
||||
assertEquals("minecraft:chest", entity.getStringOr("id", ""));
|
||||
assertEquals(17L, entity.getLongOr("LootTableSeed", 0));
|
||||
assertEquals(-60, entity.getIntOr("y", 0));
|
||||
assertThrows(IllegalStateException.class, () -> HeadlessNativeTiles.prepare(new HeadlessNativeTiles.Placement(
|
||||
2, 4, 6, placement.origin(), ModdedBlockState.of(Blocks.BARREL.defaultBlockState(), Map.of()), tile)));
|
||||
}
|
||||
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.modded.ModdedTileData;
|
||||
import art.arcane.iris.world.storage.matter.TileWrapper;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeTileData;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.world.level.block.EntityBlock;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
final class HeadlessNativeTiles {
|
||||
private HeadlessNativeTiles() {
|
||||
}
|
||||
|
||||
static List<NativeRegionTerrainWriter.TileInput> capture(Engine engine,
|
||||
RealPackProbeSupport.GeneratedChunk chunk,
|
||||
BlockPos origin) {
|
||||
List<NativeRegionTerrainWriter.TileInput> tiles = new ArrayList<>();
|
||||
engine.getMantle().getMantle().iterateChunk(origin.getX() >> 4, origin.getZ() >> 4,
|
||||
TileWrapper.class, (x, y, z, wrapper) -> tiles.add(prepare(new Placement(x, y, z, origin,
|
||||
chunk.blocks().get(x, y, z), wrapper.getData()))));
|
||||
return List.copyOf(tiles);
|
||||
}
|
||||
|
||||
static NativeRegionTerrainWriter.TileInput prepare(Placement placement) {
|
||||
if (!(placement.tile() instanceof ModdedTileData tile)
|
||||
|| !(placement.state() instanceof ModdedBlockState original)) {
|
||||
throw new IllegalStateException("Native tile export requires native block and tile data");
|
||||
}
|
||||
NativeTileData data = tile.nativeData();
|
||||
BlockState state = data.adjustBlockState(original.handle());
|
||||
BlockPos position = placement.origin().offset(placement.x(), placement.y(), placement.z());
|
||||
if (!(state.getBlock() instanceof EntityBlock block)) {
|
||||
throw new IllegalStateException("Tile data has no native block entity at " + position);
|
||||
}
|
||||
BlockEntity entity = block.newBlockEntity(position, state);
|
||||
if (entity == null || !data.isApplicable(state, entity)) {
|
||||
throw new IllegalStateException("Tile data does not match native block at " + position);
|
||||
}
|
||||
try {
|
||||
CompoundTag payload = data.payload();
|
||||
if (payload == null) {
|
||||
throw new IllegalStateException("Tile data has no native payload at " + position);
|
||||
}
|
||||
return new NativeRegionTerrainWriter.TileInput(placement.x(), placement.y(), placement.z(),
|
||||
ModdedBlockState.serialize(state), payload);
|
||||
} catch (Exception failure) {
|
||||
throw new IllegalStateException("Cannot export native tile payload at " + position, failure);
|
||||
}
|
||||
}
|
||||
|
||||
record Placement(int x, int y, int z, BlockPos origin, NativeBlockState state, TileData tile) {
|
||||
}
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureVolumeIndex;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.NbtIo;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.chunk.storage.RegionFile;
|
||||
import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
|
||||
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.spigotmc.SpigotWorldConfig;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class HeadlessRegionTerrainTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporary = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void producesNativeTerrainAndFullCompletionHaloWithoutAServer() throws Exception {
|
||||
Path root = temporary.newFolder().toPath();
|
||||
Path configuration = root.resolve("spigot.yml");
|
||||
Files.writeString(configuration, "config-version: 13\nworld-settings:\n default:\n verbose: false\n");
|
||||
HeadlessNativeRuntime runtime = new HeadlessNativeRuntime();
|
||||
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(new RealPackProbeSupport.WorkspaceOptions(
|
||||
pack().toFile(), "main", "[headless-native-grid-test]", runtime, temporary.getRoot().toPath()));
|
||||
RealPackProbeSupport.EngineSession session = workspace.openEngine(69420L, false, "native-grid")) {
|
||||
NativeStructureVolume volume = NativeStructureVolume.of("minecraft:swamp_hut", 0, 20, 0, 7, 30, 7);
|
||||
NativeStructureVolumeIndex.install(session.engine(), (engine, x, z) ->
|
||||
x == 0 && z == 0 ? new KList<>(List.of(volume)) : NativeStructureVolume.NONE);
|
||||
assertEquals(List.of(volume), session.engine().getNativeStructureVolumes(0, 0, 15, 15));
|
||||
SpigotWorldConfig spigot = HeadlessStructureConfiguration.load(new HeadlessStructureConfiguration.Options(
|
||||
configuration, "probe", Level.OVERWORLD, root.resolve("configuration")));
|
||||
try (HeadlessTerrainContext context = HeadlessTerrainContext.create(new HeadlessTerrainContext.Options(
|
||||
session.engine(), runtime, root.resolve("templates"), Level.OVERWORLD,
|
||||
NoiseGeneratorSettings.OVERWORLD, spigot))) {
|
||||
HeadlessRegionTerrain.Session rolling = new HeadlessRegionTerrain.Session(RegionGenerationWindow.Policy.EMBEDDED);
|
||||
HeadlessRegionTerrain.Result result = rolling.generate(
|
||||
new HeadlessRegionTerrain.Request(context, -3, 2, 1, 4), System.out::println);
|
||||
OfflineRegionGenerator.Configuration export = new OfflineRegionGenerator.Configuration(
|
||||
workspace.pack(), "main", 69420L, -3, 2, 1, 2, 3, 4, configuration,
|
||||
"probe", Level.OVERWORLD, NoiseGeneratorSettings.OVERWORLD, root.resolve("export"));
|
||||
HydrologyTileCache.PregenerationArea bounds = export.hydrologyArea();
|
||||
assertEquals(-80L, bounds.minimumBlockX());
|
||||
assertEquals(0L, bounds.minimumBlockZ());
|
||||
assertEquals(15L, bounds.maximumBlockX());
|
||||
assertEquals(111L, bounds.maximumBlockZ());
|
||||
assertEquals(1, result.targetChunks());
|
||||
assertEquals(529, result.chunks().size());
|
||||
long terrain = result.chunks().stream().filter(chunk -> chunk.getPersistedStatus() == ChunkStatus.TERRAIN).count();
|
||||
long biomes = result.chunks().stream().filter(chunk -> chunk.getPersistedStatus() == ChunkStatus.BIOMES).count();
|
||||
assertEquals(25L, terrain);
|
||||
assertEquals(24L, biomes);
|
||||
ProtoChunk target = result.chunks().stream().filter(chunk -> chunk.getPos().equals(new ChunkPos(-3, 2)))
|
||||
.findFirst().orElseThrow();
|
||||
CompoundTag tag = HeadlessChunkSerialization.copyOf(target, context.serialization()).write();
|
||||
assertEquals("minecraft:terrain", tag.getStringOr("Status", ""));
|
||||
assertFalse(tag.getBooleanOr("isLightOn", false));
|
||||
assertTrue(tag.getCompoundOrEmpty("Heightmaps").contains("WORLD_SURFACE_WG"));
|
||||
assertTrue(tag.getCompoundOrEmpty("Heightmaps").contains("OCEAN_FLOOR_WG"));
|
||||
assertEquals(6, tag.getCompoundOrEmpty("Heightmaps").size());
|
||||
assertTrue(tag.getCompoundOrEmpty("ChunkBukkitValues").contains("iris:natural_terrain"));
|
||||
assertTrue(tag.getCompoundOrEmpty("ChunkBukkitValues").contains("iris:structure_activation"));
|
||||
assertFalse(tag.getListOrEmpty("sections").isEmpty());
|
||||
assertTrue(RealPackProbeSupport.drainReported().isEmpty());
|
||||
assertNull(Bukkit.getServer());
|
||||
HeadlessRegionTerrain.Result repeated = rolling.generate(
|
||||
new HeadlessRegionTerrain.Request(context, -3, 2, 1, 4), System.out::println);
|
||||
assertEquals(0, repeated.updated().size());
|
||||
HeadlessRegionTerrain.Result adjacent = rolling.generate(
|
||||
new HeadlessRegionTerrain.Request(context, -2, 2, 1, 4), System.out::println);
|
||||
assertEquals(529, adjacent.chunks().size());
|
||||
assertTrue(adjacent.updated().size() < result.updated().size());
|
||||
ProtoChunk retained = adjacent.chunks().stream().filter(chunk -> chunk.getPos().equals(target.getPos()))
|
||||
.findFirst().orElseThrow();
|
||||
assertSame(target, retained);
|
||||
assertEquals(ChunkStatus.TERRAIN, retained.getPersistedStatus());
|
||||
Path regionDirectory = Files.createDirectory(root.resolve("region"));
|
||||
OfflineRegionWriter writer = new OfflineRegionWriter(new OfflineRegionWriter.Options(
|
||||
context, regionDirectory, 4, RegionGenerationWindow.Policy.EMBEDDED));
|
||||
OfflineRegionWriter.Result firstWrite = writer.write(result);
|
||||
assertEquals(result.updated().size(), firstWrite.chunks());
|
||||
assertEquals(tag, read(regionDirectory, target.getPos()));
|
||||
OfflineRegionWriter.Result duplicateWrite = writer.write(result);
|
||||
assertEquals(0, duplicateWrite.chunks());
|
||||
OfflineRegionWriter reopened = new OfflineRegionWriter(new OfflineRegionWriter.Options(
|
||||
context, regionDirectory, 4, RegionGenerationWindow.Policy.EMBEDDED));
|
||||
assertEquals(0, reopened.write(result).chunks());
|
||||
target.setPersistedStatus(ChunkStatus.STRUCTURE_STARTS);
|
||||
try {
|
||||
assertEquals(0, writer.write(
|
||||
new HeadlessRegionTerrain.Result(List.of(target), List.of(target), 1)).chunks());
|
||||
} finally {
|
||||
target.setPersistedStatus(ChunkStatus.TERRAIN);
|
||||
}
|
||||
HeadlessRegionTerrain.Result nextRow = rolling.generate(
|
||||
new HeadlessRegionTerrain.Request(context, -2, 3, 1, 4), System.out::println);
|
||||
assertEquals(529, nextRow.chunks().size());
|
||||
assertSame(target, nextRow.chunks().stream().filter(chunk -> chunk.getPos().equals(target.getPos()))
|
||||
.findFirst().orElseThrow());
|
||||
assertTrue(nextRow.updated().size() < result.updated().size());
|
||||
HeadlessRegionTerrain.Result distant = rolling.generate(
|
||||
new HeadlessRegionTerrain.Request(context, 20, 2, 1, 4), System.out::println);
|
||||
assertEquals(529, distant.chunks().size());
|
||||
ProtoChunk previousHalo = distant.chunks().getFirst();
|
||||
HeadlessRegionTerrain.Result back = rolling.generate(
|
||||
new HeadlessRegionTerrain.Request(context, -3, 2, 1, 4), System.out::println);
|
||||
assertFalse(back.chunks().contains(previousHalo));
|
||||
ProtoChunk regenerated = back.chunks().stream().filter(chunk -> chunk.getPos().equals(target.getPos()))
|
||||
.findFirst().orElseThrow();
|
||||
CompoundTag regeneratedTag = HeadlessChunkSerialization.copyOf(regenerated, context.serialization()).write();
|
||||
for (String key : List.of("sections", "Heightmaps", "structures", "ChunkBukkitValues")) {
|
||||
assertEquals(key, tag.get(key), regeneratedTag.get(key));
|
||||
}
|
||||
assertEquals(0, writer.write(back).chunks());
|
||||
Path oversizedDirectory = Files.createDirectory(root.resolve("oversized"));
|
||||
OfflineRegionWriter oversizedWriter = new OfflineRegionWriter(new OfflineRegionWriter.Options(
|
||||
context, oversizedDirectory, 4, RegionGenerationWindow.Policy.EMBEDDED));
|
||||
NamespacedKey payloadKey = new NamespacedKey("iris", "serialization_test_payload");
|
||||
byte[] payload = new byte[2 * 1024 * 1024];
|
||||
new Random(69420L).nextBytes(payload);
|
||||
regenerated.persistentDataContainer.set(payloadKey, PersistentDataType.BYTE_ARRAY, payload);
|
||||
HeadlessRegionTerrain.Result singleton = new HeadlessRegionTerrain.Result(List.of(regenerated), List.of(regenerated), 1);
|
||||
assertEquals(1, oversizedWriter.write(singleton).chunks());
|
||||
assertTrue(Files.isRegularFile(oversizedDirectory.resolve("c.-3.2.mcc")));
|
||||
assertArrayEquals(payload, read(oversizedDirectory, regenerated.getPos())
|
||||
.getCompoundOrEmpty("ChunkBukkitValues").getByteArray("iris:serialization_test_payload").orElseThrow());
|
||||
regenerated.persistentDataContainer.remove(payloadKey);
|
||||
Path failedDirectory = Files.createDirectory(root.resolve("failed"));
|
||||
OfflineRegionWriter failedWriter = new OfflineRegionWriter(new OfflineRegionWriter.Options(
|
||||
context, failedDirectory, 4, RegionGenerationWindow.Policy.EMBEDDED));
|
||||
regenerated.setPersistedStatus(ChunkStatus.FULL);
|
||||
try {
|
||||
assertThrows(Exception.class, () -> failedWriter.write(singleton));
|
||||
} finally {
|
||||
regenerated.setPersistedStatus(ChunkStatus.TERRAIN);
|
||||
}
|
||||
assertEquals(1, failedWriter.write(singleton).chunks());
|
||||
assertEquals("minecraft:terrain", read(failedDirectory, regenerated.getPos()).getStringOr("Status", ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static CompoundTag read(Path directory, ChunkPos position) throws Exception {
|
||||
Path path = directory.resolve("r." + (position.x() >> 5) + "." + (position.z() >> 5) + ".mca");
|
||||
try (RegionFile region = new RegionFile(new RegionStorageInfo("test", Level.OVERWORLD, "chunk"), path, directory, false);
|
||||
DataInputStream input = region.getChunkDataInputStream(position)) {
|
||||
return NbtIo.read(input);
|
||||
}
|
||||
}
|
||||
|
||||
private Path pack() throws Exception {
|
||||
Path pack = temporary.newFolder().toPath();
|
||||
for (String directory : List.of("dimensions", "regions", "biomes", "generators")) {
|
||||
Files.createDirectories(pack.resolve(directory));
|
||||
}
|
||||
Files.writeString(pack.resolve("dimensions/main.json"), """
|
||||
{"name":"Native region","focus":"swamp","regions":["owned"],"fluidHeight":40,"logicalHeight":256,
|
||||
"landChance":1,"carvingEnabled":false,"hydrology":{"rivers":{"enabled":false}}}
|
||||
""");
|
||||
Files.writeString(pack.resolve("regions/owned.json"), "{\"landBiomes\":[\"swamp\"]}");
|
||||
Files.writeString(pack.resolve("generators/flat.json"), "{\"composite\":[]}");
|
||||
Files.writeString(pack.resolve("biomes/swamp.json"), """
|
||||
{"derivative":"minecraft:swamp","generators":[{"generator":"flat","min":35,"max":35}]}
|
||||
""");
|
||||
return pack;
|
||||
}
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import net.minecraft.world.level.Level;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.junit.Rule;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.spigotmc.SpigotConfig;
|
||||
import org.spigotmc.SpigotWorldConfig;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Comparator;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HeadlessStructureConfigurationTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporary = new TemporaryFolder();
|
||||
|
||||
@BeforeClass
|
||||
public static void initializeNative() {
|
||||
HeadlessNativeBootstrap.initialize();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativeConstructorInitializesEveryDefaultWithoutChangingTheSourceOrGlobals() throws Exception {
|
||||
Path source = temporary.newFile("spigot.yml").toPath();
|
||||
Path staging = temporary.newFolder().toPath();
|
||||
byte[] original = Files.readAllBytes(source);
|
||||
YamlConfiguration previous = SpigotConfig.config;
|
||||
Object previousFile = nativeField("CONFIG_FILE").get(null);
|
||||
int previousVersion = nativeField("version").getInt(null);
|
||||
SpigotWorldConfig configuration = HeadlessStructureConfiguration.load(options(source, staging));
|
||||
assertEquals(SpigotWorldConfig.class, configuration.getClass());
|
||||
int checked = 0;
|
||||
for (Field field : seedFields()) {
|
||||
if (field.getType() == int.class) {
|
||||
assertTrue(field.getName(), field.getInt(configuration) > 0);
|
||||
} else {
|
||||
assertNull(field.getName(), field.get(configuration));
|
||||
}
|
||||
checked++;
|
||||
}
|
||||
assertTrue(checked >= 22);
|
||||
assertEquals(100, configuration.cactusModifier);
|
||||
assertArrayEquals(original, Files.readAllBytes(source));
|
||||
assertSame(previous, SpigotConfig.config);
|
||||
assertEquals(previousFile, nativeField("CONFIG_FILE").get(null));
|
||||
assertEquals(previousVersion, nativeField("version").getInt(null));
|
||||
assertEmpty(staging);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everyNativeSeedHonorsDefaultAndNamespacedOverrides() throws Exception {
|
||||
Path source = temporary.newFile("overrides.yml").toPath();
|
||||
Path staging = temporary.newFolder().toPath();
|
||||
YamlConfiguration yaml = new YamlConfiguration();
|
||||
yaml.set("config-version", 13);
|
||||
int ordinal = 0;
|
||||
for (Field field : seedFields()) {
|
||||
String path = seedPath(field);
|
||||
yaml.set("world-settings.default." + path, 12000 + ordinal);
|
||||
yaml.set("world-settings.minecraft:overworld." + path,
|
||||
field.getType() == Long.class ? 9000000000L + ordinal : 24000L + ordinal);
|
||||
ordinal++;
|
||||
}
|
||||
yaml.set("world-settings.default.verbose", true);
|
||||
yaml.set("world-settings.legacy.seed-swamp", 9);
|
||||
yaml.save(source.toFile());
|
||||
byte[] original = Files.readAllBytes(source);
|
||||
SpigotWorldConfig configured = HeadlessStructureConfiguration.load(options(source, staging));
|
||||
ordinal = 0;
|
||||
for (Field field : seedFields()) {
|
||||
assertEquals(field.getName(), field.getType() == Long.class ? 9000000000L + ordinal : 24000L + ordinal,
|
||||
((Number) field.get(configured)).longValue());
|
||||
ordinal++;
|
||||
}
|
||||
assertArrayEquals(original, Files.readAllBytes(source));
|
||||
yaml.set("world-settings.minecraft:overworld", null);
|
||||
yaml.save(source.toFile());
|
||||
SpigotWorldConfig defaults = HeadlessStructureConfiguration.load(options(source, staging));
|
||||
ordinal = 0;
|
||||
for (Field field : seedFields()) {
|
||||
assertEquals(field.getName(), 12000L + ordinal++, ((Number) field.get(defaults)).longValue());
|
||||
}
|
||||
assertEmpty(staging);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void legacyMigrationRetainsNativePrecedenceAndFailuresRestoreState() throws Exception {
|
||||
Path source = temporary.newFile("legacy.yml").toPath();
|
||||
Path staging = temporary.newFolder().toPath();
|
||||
Files.writeString(source, """
|
||||
config-version: 12
|
||||
world-settings:
|
||||
default:
|
||||
seed-swamp: 11
|
||||
legacy:
|
||||
seed-swamp: 73
|
||||
verbose: true
|
||||
minecraft:overworld:
|
||||
seed-swamp: 99
|
||||
""");
|
||||
byte[] original = Files.readAllBytes(source);
|
||||
assertEquals(73, HeadlessStructureConfiguration.load(options(source, staging)).swampSeed);
|
||||
assertArrayEquals(original, Files.readAllBytes(source));
|
||||
assertEquals(99, HeadlessStructureConfiguration.load(new HeadlessStructureConfiguration.Options(
|
||||
source, "minecraft:overworld", Level.OVERWORLD, staging)).swampSeed);
|
||||
assertArrayEquals(original, Files.readAllBytes(source));
|
||||
YamlConfiguration previous = SpigotConfig.config;
|
||||
Object previousFile = nativeField("CONFIG_FILE").get(null);
|
||||
int previousVersion = nativeField("version").getInt(null);
|
||||
for (String invalid : List.of("world-settings: [broken", "world-settings:\n default:\n seed-stronghold: '2.5'\n")) {
|
||||
Files.writeString(source, invalid);
|
||||
IOException failure = assertThrows(IOException.class,
|
||||
() -> HeadlessStructureConfiguration.load(options(source, staging)));
|
||||
assertNotNull(failure.getCause());
|
||||
assertEquals(invalid, Files.readString(source));
|
||||
assertSame(previous, SpigotConfig.config);
|
||||
assertEquals(previousFile, nativeField("CONFIG_FILE").get(null));
|
||||
assertEquals(previousVersion, nativeField("version").getInt(null));
|
||||
assertEmpty(staging);
|
||||
}
|
||||
}
|
||||
|
||||
private static HeadlessStructureConfiguration.Options options(Path source, Path staging) {
|
||||
return new HeadlessStructureConfiguration.Options(source, "legacy", Level.OVERWORLD, staging);
|
||||
}
|
||||
|
||||
private static List<Field> seedFields() {
|
||||
return Stream.of(SpigotWorldConfig.class.getFields()).filter(field -> field.getName().endsWith("Seed"))
|
||||
.sorted(Comparator.comparing(Field::getName)).toList();
|
||||
}
|
||||
|
||||
private static String seedPath(Field field) {
|
||||
return "seed-" + field.getName().substring(0, field.getName().length() - 4).toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private static Field nativeField(String name) throws Exception {
|
||||
Field field = SpigotConfig.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
return field;
|
||||
}
|
||||
|
||||
private static void assertEmpty(Path directory) throws IOException {
|
||||
try (Stream<Path> entries = Files.list(directory)) {
|
||||
assertEquals(0L, entries.count());
|
||||
}
|
||||
}
|
||||
}
|
||||
+366
@@ -0,0 +1,366 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.structure.nativegen.IrisNativeStructure;
|
||||
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureGenerationStatus;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
|
||||
import art.arcane.iris.structure.placement.IrisStructurePlacement;
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeGenerationScope;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.StructureFingerprint;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.StructureInjectionPolicy;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.StructurePlacementDecision;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.StructureReferencePolicy;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureFactory;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureOwnershipFingerprint;
|
||||
import art.arcane.volmlib.util.structure.StructureTerrainMode;
|
||||
import com.mojang.serialization.Lifecycle;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.MappedRegistry;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.RegistrationInfo;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.data.registries.VanillaRegistries;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.packs.PackType;
|
||||
import net.minecraft.server.packs.repository.ServerPacksSource;
|
||||
import net.minecraft.server.packs.resources.MultiPackResourceManager;
|
||||
import net.minecraft.util.datafix.DataFixers;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.FixedBiomeSource;
|
||||
import net.minecraft.world.level.biome.MultiNoiseBiomeSource;
|
||||
import net.minecraft.world.level.biome.MultiNoiseBiomeSourceParameterLists;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
import net.minecraft.world.level.chunk.PalettedContainerFactory;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.UpgradeData;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.WorldOptions;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureType;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
|
||||
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadType;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.spigotmc.SpigotWorldConfig;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
public class HeadlessStructurePlannerTest {
|
||||
private static final long SEED = 69420L;
|
||||
@Rule
|
||||
public TemporaryFolder temporary = new TemporaryFolder();
|
||||
private HolderLookup.Provider lookup;
|
||||
private MappedRegistry<Biome> biomes;
|
||||
private Holder.Reference<Biome> biome;
|
||||
private RandomState random;
|
||||
private ChunkGenerator generator;
|
||||
|
||||
@Before
|
||||
public void initialize() {
|
||||
HeadlessNativeBootstrap.initialize();
|
||||
lookup = VanillaRegistries.createWorldLookup();
|
||||
biomes = new MappedRegistry<>(Registries.BIOME, Lifecycle.stable());
|
||||
Biome value = lookup.lookupOrThrow(Registries.BIOME).getOrThrow(
|
||||
ResourceKey.create(Registries.BIOME, Identifier.parse("minecraft:plains"))).value();
|
||||
biome = biomes.register(ResourceKey.create(Registries.BIOME, Identifier.parse("minecraft:plains")),
|
||||
value, RegistrationInfo.BUILT_IN);
|
||||
biomes.freeze();
|
||||
Holder.Reference<NoiseGeneratorSettings> settings = lookup.lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||
.getOrThrow(NoiseGeneratorSettings.OVERWORLD);
|
||||
random = RandomState.create(lookup.lookupOrThrow(Registries.NOISE), SEED, settings.value());
|
||||
generator = new NoiseBasedChunkGenerator(new FixedBiomeSource(biome), settings);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void weightedFailedTrialsMatchNativeSelectionAndClimateAtNegativeCoordinates() throws Exception {
|
||||
generator = new NoiseBasedChunkGenerator(MultiNoiseBiomeSource.createFromPreset(
|
||||
lookup.lookupOrThrow(Registries.MULTI_NOISE_BIOME_SOURCE_PARAMETER_LIST)
|
||||
.getOrThrow(MultiNoiseBiomeSourceParameterLists.OVERWORLD)),
|
||||
lookup.lookupOrThrow(Registries.NOISE_SETTINGS).getOrThrow(NoiseGeneratorSettings.OVERWORLD));
|
||||
List<String> attempts = new ArrayList<>();
|
||||
Set<String> climates = new HashSet<>();
|
||||
MappedRegistry<Structure> structures = new MappedRegistry<>(Registries.STRUCTURE, Lifecycle.stable());
|
||||
List<StructureSet.StructureSelectionEntry> entries = new ArrayList<>();
|
||||
for (int index = 0; index < 4; index++) {
|
||||
String key = "probe:rejected_" + index;
|
||||
Holder.Reference<Structure> holder = structures.register(ResourceKey.create(Registries.STRUCTURE,
|
||||
Identifier.parse(key)), new RejectedStructure(
|
||||
HolderSet.direct(List.copyOf(generator.getBiomeSource().possibleBiomes())), key, attempts), RegistrationInfo.BUILT_IN);
|
||||
entries.add(new StructureSet.StructureSelectionEntry(holder, index + 1));
|
||||
}
|
||||
structures.freeze();
|
||||
RegistryAccess registries = new RegistryAccess.ImmutableRegistryAccess(List.of(biomes, structures));
|
||||
ChunkGeneratorStructureState state = state(entries, "probe:weighted", 18373);
|
||||
try (MultiPackResourceManager resources = resources();
|
||||
LevelStorageSource.LevelStorageAccess storage = LevelStorageSource.createDefault(temporary.newFolder().toPath()).createAccess("world")) {
|
||||
StructureTemplateManager templates = new StructureTemplateManager(resources, storage,
|
||||
DataFixers.getDataFixer(), BuiltInRegistries.BLOCK);
|
||||
HeadlessStructurePlanner.NativeContext context = new HeadlessStructurePlanner.NativeContext(
|
||||
registries, generator, state, templates, Level.OVERWORLD, new WorldOptions(SEED, true, false), true);
|
||||
for (ChunkPos position : List.of(new ChunkPos(-17, 31), new ChunkPos(0, 0), new ChunkPos(17, -31))) {
|
||||
attempts.clear();
|
||||
generator.createStructures(registries, state,
|
||||
new StructureManager(null, new WorldOptions(SEED, true, false), null), chunk(position, registries),
|
||||
templates, Level.OVERWORLD);
|
||||
List<String> nativeAttempts = List.copyOf(attempts);
|
||||
attempts.clear();
|
||||
HeadlessStructurePlanner.generateNaturalStarts(context, chunk(position, registries));
|
||||
assertEquals(nativeAttempts, attempts);
|
||||
assertEquals(4, attempts.size());
|
||||
climates.add(attempts.getFirst().split("\\|", 3)[1]);
|
||||
}
|
||||
assertTrue(climates.size() > 1);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativePiecesAndConfiguredPlacementSaltArePreserved() throws Exception {
|
||||
MappedRegistry<Structure> structures = new MappedRegistry<>(Registries.STRUCTURE, Lifecycle.stable());
|
||||
Holder.Reference<Structure> hut = structures.register(ResourceKey.create(Registries.STRUCTURE,
|
||||
Identifier.parse("probe:hut")), new SwampHutStructure(new Structure.StructureSettings(HolderSet.direct(biome))),
|
||||
RegistrationInfo.BUILT_IN);
|
||||
structures.freeze();
|
||||
RegistryAccess registries = new RegistryAccess.ImmutableRegistryAccess(List.of(biomes, structures));
|
||||
ChunkGeneratorStructureState state = state(List.of(new StructureSet.StructureSelectionEntry(hut, 1)),
|
||||
"minecraft:villages", 18373);
|
||||
RandomSpreadStructurePlacement placement = (RandomSpreadStructurePlacement) state.possibleStructureSets().getFirst().value().placement();
|
||||
try (MultiPackResourceManager resources = resources();
|
||||
LevelStorageSource.LevelStorageAccess storage = LevelStorageSource.createDefault(temporary.newFolder().toPath()).createAccess("world")) {
|
||||
StructureTemplateManager templates = new StructureTemplateManager(resources, storage,
|
||||
DataFixers.getDataFixer(), BuiltInRegistries.BLOCK);
|
||||
HeadlessStructurePlanner.NativeContext context = new HeadlessStructurePlanner.NativeContext(
|
||||
registries, generator, state, templates, Level.OVERWORLD, new WorldOptions(SEED, true, false), true);
|
||||
ChunkPos position = placement.getPotentialStructureChunk(SEED, -33, 17);
|
||||
ProtoChunk chunk = chunk(position, registries);
|
||||
HeadlessStructurePlanner.generateNaturalStarts(context, chunk);
|
||||
StructureStart expected = hut.value().generate(hut, Level.OVERWORLD, registries, generator,
|
||||
generator.getBiomeSource(), random.createClimateSampler(SamplerContext.builder().enableCaches().build()),
|
||||
random, templates, SEED, position, 0, chunk, hut.value().biomes()::contains);
|
||||
assertTrue(expected.isValid());
|
||||
StructurePieceSerializationContext serialization = new StructurePieceSerializationContext(resources, registries, templates);
|
||||
CompoundTag expectedTag = expected.createTag(serialization, position);
|
||||
assertEquals(expectedTag, chunk.getStartForStructure(hut.value()).createTag(serialization, position));
|
||||
RandomSpreadStructurePlacement configured = new RandomSpreadStructurePlacement(7, 1, RandomSpreadType.LINEAR, 17);
|
||||
assertEquals(configured.getPotentialStructureChunk(SEED, -33, 17), position);
|
||||
HeadlessStructurePlanner.NativeContext disabled = new HeadlessStructurePlanner.NativeContext(
|
||||
registries, generator, state, templates, Level.OVERWORLD, new WorldOptions(SEED, false, false), true);
|
||||
ProtoChunk empty = chunk(position, registries);
|
||||
HeadlessStructurePlanner.generateNaturalStarts(disabled, empty);
|
||||
assertTrue(empty.getAllStarts().isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
private ChunkGeneratorStructureState state(List<StructureSet.StructureSelectionEntry> entries, String key, int salt) {
|
||||
MappedRegistry<StructureSet> sets = new MappedRegistry<>(Registries.STRUCTURE_SET, Lifecycle.stable());
|
||||
sets.register(ResourceKey.create(Registries.STRUCTURE_SET, Identifier.parse(key)), new StructureSet(entries,
|
||||
new RandomSpreadStructurePlacement(key.equals("minecraft:villages") ? 7 : 1,
|
||||
key.equals("minecraft:villages") ? 1 : 0, RandomSpreadType.LINEAR, salt)), RegistrationInfo.BUILT_IN);
|
||||
sets.freeze();
|
||||
return ChunkGeneratorStructureState.createForNormal(random, SEED, generator.getOrigin(random),
|
||||
generator.getBiomeSource(), sets, configuration());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configuredStartsKeepOwnershipTerrainAndReferencesBeyondPieceBounds() throws Exception {
|
||||
MappedRegistry<Structure> structures = new MappedRegistry<>(Registries.STRUCTURE, Lifecycle.stable());
|
||||
Holder.Reference<Structure> hut = structures.register(ResourceKey.create(Registries.STRUCTURE,
|
||||
Identifier.parse("probe:hut")), new SwampHutStructure(new Structure.StructureSettings(HolderSet.direct(biome))),
|
||||
RegistrationInfo.BUILT_IN);
|
||||
structures.freeze();
|
||||
RegistryAccess registries = new RegistryAccess.ImmutableRegistryAccess(List.of(biomes, structures));
|
||||
MappedRegistry<StructureSet> sets = new MappedRegistry<>(Registries.STRUCTURE_SET, Lifecycle.stable());
|
||||
sets.freeze();
|
||||
ChunkGeneratorStructureState state = ChunkGeneratorStructureState.createForNormal(random, SEED,
|
||||
generator.getOrigin(random), generator.getBiomeSource(), sets, configuration());
|
||||
IrisStructureTerrain terrain = new IrisStructureTerrain().setMode(StructureTerrainMode.BORE).setHorizontalPadding(24);
|
||||
NativeStructureStartPlan plan = new NativeStructureStartPlan(
|
||||
new IrisStructurePlacement().setUnderground(true).setTerrain(terrain),
|
||||
new IrisNativeStructure().setStructure("probe:hut"), -1, 0, 30);
|
||||
TestPolicy policy = new TestPolicy(plan);
|
||||
HeadlessStructurePlanner.Policies<NativeStructureStartPlan, NativeStructureOwnershipRecord> policies =
|
||||
new HeadlessStructurePlanner.Policies<>(policy, policy, footprint -> true);
|
||||
try (MultiPackResourceManager resources = resources();
|
||||
LevelStorageSource.LevelStorageAccess storage = LevelStorageSource.createDefault(temporary.newFolder().toPath()).createAccess("world")) {
|
||||
StructureTemplateManager templates = new StructureTemplateManager(resources, storage,
|
||||
DataFixers.getDataFixer(), BuiltInRegistries.BLOCK);
|
||||
HeadlessStructurePlanner.NativeContext context = new HeadlessStructurePlanner.NativeContext(
|
||||
registries, generator, state, templates, Level.OVERWORLD, new WorldOptions(SEED, true, false), true);
|
||||
Map<Long, HeadlessStructurePlanner.PlannedChunk<NativeStructureOwnershipRecord>> grid = new HashMap<>();
|
||||
for (int x = -10; x <= 6; x++) {
|
||||
for (int z = -8; z <= 8; z++) {
|
||||
ChunkPos position = new ChunkPos(x, z);
|
||||
grid.put(position.pack(), HeadlessStructurePlanner.plan(context, policies, position));
|
||||
}
|
||||
}
|
||||
HeadlessStructurePlanner.PlannedChunk<NativeStructureOwnershipRecord> origin = grid.get(new ChunkPos(-1, 0).pack());
|
||||
StructureStart actual = origin.chunk().getStartForStructure(hut.value());
|
||||
StructureStart expected = NativeStructureFactory.generate(new NativeStructureFactory.GenerationContext(
|
||||
registries, generator, generator.getBiomeSource(), random, templates, SEED, Level.OVERWORLD,
|
||||
origin.chunk(), selected -> true, generator.getSeaLevel(), policy::surfaceHeight), hut, plan, 0);
|
||||
StructurePieceSerializationContext serialization = new StructurePieceSerializationContext(resources, registries, templates);
|
||||
assertEquals(expected.createTag(serialization, origin.chunk().getPos()), actual.createTag(serialization, origin.chunk().getPos()));
|
||||
assertEquals(30, actual.getBoundingBox().minY());
|
||||
assertTrue(NativeStructureOwnershipFingerprint.matches(origin.ownership().get(hut.value()), actual));
|
||||
assertEquals(StructureTerrainMode.BORE, origin.terrain().get(hut.value()).resolvedMode());
|
||||
HeadlessStructurePlanner.PlannedChunk<NativeStructureOwnershipRecord> target = grid.get(new ChunkPos(-2, 0).pack());
|
||||
assertFalse(actual.getBoundingBox().intersects(-32, 0, -17, 15));
|
||||
Map<Long, HeadlessStructurePlanner.PlannedChunk<NativeStructureOwnershipRecord>> incomplete = new HashMap<>(grid);
|
||||
incomplete.remove(new ChunkPos(-10, -8).pack());
|
||||
assertThrows(IllegalArgumentException.class, () -> HeadlessStructurePlanner.references(policies, target, incomplete));
|
||||
assertTrue(target.chunk().getAllReferences().isEmpty());
|
||||
HeadlessStructurePlanner.references(policies, target, grid);
|
||||
assertTrue(target.chunk().getAllReferences().get(hut.value()).contains(new ChunkPos(-1, 0).pack()));
|
||||
assertEquals(ChunkStatus.STRUCTURE_REFERENCES, target.chunk().getPersistedStatus());
|
||||
HeadlessStructurePlanner.Policies<NativeStructureStartPlan, NativeStructureOwnershipRecord> excluded =
|
||||
new HeadlessStructurePlanner.Policies<>(policy, policy, footprint -> false);
|
||||
HeadlessStructurePlanner.PlannedChunk<NativeStructureOwnershipRecord> rejected = HeadlessStructurePlanner.plan(
|
||||
context, excluded, new ChunkPos(-1, 0));
|
||||
assertFalse(rejected.chunk().getStartForStructure(hut.value()).isValid());
|
||||
assertTrue(rejected.ownership().isEmpty());
|
||||
assertTrue(rejected.terrain().isEmpty());
|
||||
assertEquals(1, policy.discards);
|
||||
policy.hideOwnership = true;
|
||||
assertThrows(IllegalStateException.class, () -> HeadlessStructurePlanner.plan(context, policies, new ChunkPos(-1, 0)));
|
||||
assertEquals(2, policy.discards);
|
||||
for (boolean adapterEnabled : new boolean[]{true, false}) {
|
||||
HeadlessStructurePlanner.NativeContext disabled = new HeadlessStructurePlanner.NativeContext(
|
||||
registries, generator, state, templates, Level.OVERWORLD,
|
||||
new WorldOptions(SEED, !adapterEnabled, false), adapterEnabled);
|
||||
HeadlessStructurePlanner.PlannedChunk<NativeStructureOwnershipRecord> empty = HeadlessStructurePlanner.plan(
|
||||
disabled, policies, new ChunkPos(-1, 0));
|
||||
assertTrue(empty.chunk().getAllStarts().isEmpty());
|
||||
assertTrue(empty.ownership().isEmpty());
|
||||
if (!adapterEnabled) {
|
||||
int scopes = policy.scopes;
|
||||
HeadlessStructurePlanner.references(policies, empty, Map.of());
|
||||
assertEquals(scopes, policy.scopes);
|
||||
assertTrue(empty.chunk().getAllReferences().isEmpty());
|
||||
assertEquals(ChunkStatus.STRUCTURE_REFERENCES, empty.chunk().getPersistedStatus());
|
||||
}
|
||||
}
|
||||
assertEquals(2, policy.discards);
|
||||
assertThrows(IllegalArgumentException.class, () -> new HeadlessStructurePlanner.NativeContext(
|
||||
registries, generator, state, templates, Level.OVERWORLD,
|
||||
new WorldOptions(SEED + 1, true, false), true));
|
||||
}
|
||||
}
|
||||
|
||||
private static SpigotWorldConfig configuration() {
|
||||
return new SpigotWorldConfig("probe", Key.key("probe:world")) {
|
||||
@Override
|
||||
public void init() {
|
||||
villageSeed = 17;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static final class TestPolicy implements StructureInjectionPolicy<NativeStructureStartPlan>,
|
||||
StructureReferencePolicy<NativeStructureStartPlan, NativeStructureOwnershipRecord> {
|
||||
private final NativeStructureStartPlan plan;
|
||||
private NativeStructureOwnershipRecord ownership;
|
||||
private boolean hideOwnership;
|
||||
private int discards;
|
||||
private int scopes;
|
||||
|
||||
private TestPolicy(NativeStructureStartPlan plan) { this.plan = plan; }
|
||||
@Override public List<NativeStructureStartPlan> plansAt(int x, int z) {
|
||||
return x == plan.chunkX() && z == plan.chunkZ() ? List.of(plan) : List.of();
|
||||
}
|
||||
@Override public boolean sourceReplaced(String key, boolean underground) { return false; }
|
||||
@Override public int surfaceHeight(int x, int z) { return 80; }
|
||||
@Override public void record(NativeStructureStartPlan selected, StructureFingerprint fingerprint) {
|
||||
ownership = NativeStructureOwnershipRecord.capture(fingerprint, selected);
|
||||
}
|
||||
@Override public void discard(String key, int x, int z) { ownership = null; discards++; }
|
||||
@Override public void duplicate(String key) { throw new AssertionError(key); }
|
||||
@Override public NativeStructureStartPlan matchingPlan(String key, int x, int z) {
|
||||
return plansAt(x, z).stream().findFirst().orElse(null);
|
||||
}
|
||||
@Override public NativeStructureOwnershipRecord findPersisted(String key, int x, int z) {
|
||||
return !hideOwnership && ownership != null && ownership.structureKey().equals(key) && ownership.originChunkX() == x
|
||||
&& ownership.originChunkZ() == z ? ownership : null;
|
||||
}
|
||||
@Override public NativeStructureOwnershipRecord capture(StructureFingerprint fingerprint, NativeStructureStartPlan selected) {
|
||||
return NativeStructureOwnershipRecord.capture(fingerprint, selected);
|
||||
}
|
||||
@Override public void record(NativeStructureOwnershipRecord record) { ownership = record; }
|
||||
@Override public NativeGenerationScope openOriginScope(int x, int z) { scopes++; return () -> {}; }
|
||||
@Override public StructurePlacementDecision resolve(String key, boolean underground) {
|
||||
return new IrisNativeStructureDecision(NativeStructureGenerationStatus.GENERATE_NATIVE, 0, null,
|
||||
true, null, new IrisStructureTerrain());
|
||||
}
|
||||
@Override public void invalidated(String key) { throw new AssertionError(key); }
|
||||
}
|
||||
|
||||
private static ProtoChunk chunk(ChunkPos position, RegistryAccess registries) {
|
||||
return new ProtoChunk(position, UpgradeData.EMPTY, LevelHeightAccessor.create(-64, 384),
|
||||
PalettedContainerFactory.create(registries), null);
|
||||
}
|
||||
|
||||
private static MultiPackResourceManager resources() {
|
||||
return new MultiPackResourceManager(PackType.SERVER_DATA,
|
||||
List.of(ServerPacksSource.createVanillaPackSource().fullResources()));
|
||||
}
|
||||
|
||||
private static final class RejectedStructure extends Structure {
|
||||
private final String key;
|
||||
private final List<String> attempts;
|
||||
|
||||
private RejectedStructure(HolderSet<Biome> biomes, String key, List<String> attempts) {
|
||||
super(new StructureSettings(biomes));
|
||||
this.key = key;
|
||||
this.attempts = attempts;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
||||
int x = context.chunkPos().getMinBlockX() >> 2;
|
||||
int z = context.chunkPos().getMinBlockZ() >> 2;
|
||||
attempts.add(key + "|" + context.climateSampler().sample(x, 20, z)
|
||||
+ "|" + context.biomeResolver().getNoiseBiome(x, 20, z).unwrapKey().orElseThrow());
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StructureType<?> type() {
|
||||
return StructureType.SWAMP_HUT;
|
||||
}
|
||||
}
|
||||
}
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.NbtIo;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.EntityBlock;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.biome.Biomes;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.UpgradeData;
|
||||
import net.minecraft.world.level.chunk.PalettedContainerFactory;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.ChestBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.storage.RegionFile;
|
||||
import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class NativeRegionTerrainWriterTest {
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativeRoundTripPreservesNegativeCoordinatesAndPartialStatus() throws Exception {
|
||||
NativeRegionTerrainWriter codec = new NativeRegionTerrainWriter(HeadlessNativeTestRegistries.get());
|
||||
CompoundTag tag = codec.encode(new NativeRegionTerrainWriter.TerrainInput(-33, -1, -64, 32,
|
||||
(x, y, z) -> y < 17 ? Blocks.STONE.defaultBlockState() : Blocks.AIR.defaultBlockState(),
|
||||
(x, y, z) -> "minecraft:plains", List.of()));
|
||||
assertEquals("minecraft:empty", tag.getStringOr("Status", ""));
|
||||
assertEquals(SharedConstants.getCurrentVersion().dataVersion().version(), tag.getIntOr("DataVersion", -1));
|
||||
assertFalse(tag.contains("Level"));
|
||||
assertFalse(tag.getBooleanOr("isLightOn", false));
|
||||
assertEquals("iris:terrain_checkpoint", tag.getStringOr("iris:checkpoint", ""));
|
||||
assertEquals(Blocks.STONE.defaultBlockState(), codec.readBlock(tag, 0, -48, 0));
|
||||
assertEquals(Blocks.AIR.defaultBlockState(), codec.readBlock(tag, 0, -47, 0));
|
||||
Path directory = Files.createTempDirectory("iris-native-region-test-");
|
||||
Path file = directory.resolve("r.-2.-1.mca");
|
||||
try {
|
||||
try (RegionFile region = new RegionFile(new RegionStorageInfo("checkpoint", Level.OVERWORLD, "chunk"), file, directory, false)) {
|
||||
NativeRegionTerrainWriter.write(region, tag);
|
||||
}
|
||||
try (RegionFile region = new RegionFile(new RegionStorageInfo("checkpoint", Level.OVERWORLD, "chunk"), file, directory, false);
|
||||
DataInputStream input = region.getChunkDataInputStream(new ChunkPos(-33, -1))) {
|
||||
assertTrue(input != null);
|
||||
assertEquals(tag, NbtIo.read(input));
|
||||
}
|
||||
} finally {
|
||||
Files.deleteIfExists(file);
|
||||
Files.delete(directory);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preservesDistinctAirStatesInInitiallyEmptyContainers() throws Exception {
|
||||
NativeRegionTerrainWriter codec = new NativeRegionTerrainWriter(HeadlessNativeTestRegistries.get());
|
||||
assertEquals(Blocks.AIR.defaultBlockState(),
|
||||
PalettedContainerFactory.create(codec.registries()).createForBlockStates().get(0, 0, 0));
|
||||
CompoundTag tag = codec.encode(new NativeRegionTerrainWriter.TerrainInput(0, 0, 0, 16,
|
||||
(x, y, z) -> x == 1 ? Blocks.CAVE_AIR.defaultBlockState()
|
||||
: x == 2 ? Blocks.VOID_AIR.defaultBlockState() : Blocks.AIR.defaultBlockState(),
|
||||
(x, y, z) -> "minecraft:plains", List.of()));
|
||||
assertEquals(Blocks.AIR.defaultBlockState(), codec.readBlock(tag, 0, 0, 0));
|
||||
assertEquals(Blocks.CAVE_AIR.defaultBlockState(), codec.readBlock(tag, 1, 0, 0));
|
||||
assertEquals(Blocks.VOID_AIR.defaultBlockState(), codec.readBlock(tag, 2, 0, 0));
|
||||
assertTrue(tag.getListOrEmpty("block_entities").isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serializesNativeBlockEntityDefaultsAndCustomPayload() throws Exception {
|
||||
NativeRegionTerrainWriter codec = new NativeRegionTerrainWriter(HeadlessNativeTestRegistries.get());
|
||||
CompoundTag payload = new CompoundTag();
|
||||
payload.putString("LootTable", "minecraft:chests/simple_dungeon");
|
||||
payload.putLong("LootTableSeed", 91L);
|
||||
NativeRegionTerrainWriter.TileInput tile = new NativeRegionTerrainWriter.TileInput(3, 5, 7,
|
||||
"minecraft:chest[facing=north,type=single,waterlogged=false]", payload);
|
||||
CompoundTag tag = codec.encode(new NativeRegionTerrainWriter.TerrainInput(-2, 3, -64, 16,
|
||||
(x, y, z) -> x == 3 && y == 5 && z == 7 || x == 1 && y == 2 && z == 4
|
||||
? Blocks.CHEST.defaultBlockState() : Blocks.AIR.defaultBlockState(),
|
||||
(x, y, z) -> "minecraft:plains", List.of(tile)));
|
||||
List<CompoundTag> entities = tag.getListOrEmpty("block_entities").compoundStream().toList();
|
||||
assertEquals(2, entities.size());
|
||||
CompoundTag custom = entities.stream().filter(entity -> entity.getIntOr("x", 0) == -29)
|
||||
.findFirst().orElseThrow();
|
||||
assertEquals("minecraft:chest", custom.getStringOr("id", ""));
|
||||
assertEquals(-59, custom.getIntOr("y", 0));
|
||||
assertEquals(55, custom.getIntOr("z", 0));
|
||||
assertEquals("minecraft:chests/simple_dungeon", custom.getStringOr("LootTable", ""));
|
||||
assertEquals(91L, custom.getLongOr("LootTableSeed", 0));
|
||||
assertEquals("minecraft:empty", tag.getStringOr("Status", ""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fillsPlannedChunkWithoutDiscardingStatusOrBlockEntities() throws Exception {
|
||||
NativeRegionTerrainWriter codec = new NativeRegionTerrainWriter(HeadlessNativeTestRegistries.get());
|
||||
ProtoChunk chunk = new ProtoChunk(new ChunkPos(-2, 3), UpgradeData.EMPTY,
|
||||
LevelHeightAccessor.create(-64, 32), PalettedContainerFactory.create(codec.registries()), null);
|
||||
chunk.setPersistedStatus(ChunkStatus.BIOMES);
|
||||
chunk.fillBiomesFromNoise((x, y, z) -> codec.registries().lookupOrThrow(Registries.BIOME)
|
||||
.getOrThrow(Biomes.SWAMP));
|
||||
BlockState chest = Blocks.CHEST.defaultBlockState().setValue(ChestBlock.FACING, Direction.EAST);
|
||||
NativeRegionTerrainWriter.TerrainInput input = new NativeRegionTerrainWriter.TerrainInput(-2, 3, -64, 32,
|
||||
(x, y, z) -> x == 3 && y == 5 && z == 7 ? chest : Blocks.STONE.defaultBlockState(),
|
||||
(x, y, z) -> "minecraft:plains", List.of());
|
||||
codec.fill(chunk, input);
|
||||
assertEquals(ChunkStatus.BIOMES, chunk.getPersistedStatus());
|
||||
assertEquals(Biomes.SWAMP,
|
||||
chunk.getNoiseBiome(-8, -16, 12).unwrapKey().orElseThrow());
|
||||
assertEquals(Blocks.STONE.defaultBlockState(), chunk.getBlockState(new BlockPos(-32, -33, 48)));
|
||||
assertEquals(chest, chunk.getBlockState(new BlockPos(-29, -59, 55)));
|
||||
assertEquals("minecraft:chest", chunk.getBlockEntityNbt(new BlockPos(-29, -59, 55))
|
||||
.getStringOr("id", ""));
|
||||
assertThrows(IllegalArgumentException.class, () -> codec.fill(chunk,
|
||||
new NativeRegionTerrainWriter.TerrainInput(0, 0, -64, 32,
|
||||
input.blocks(), input.biomes(), List.of())));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serializesEveryNativeDefaultBlockEntityWithoutWorld() throws Exception {
|
||||
NativeRegionTerrainWriter codec = new NativeRegionTerrainWriter(HeadlessNativeTestRegistries.get());
|
||||
int tested = 0;
|
||||
for (Block block : BuiltInRegistries.BLOCK) {
|
||||
if (!block.defaultBlockState().hasBlockEntity()) {
|
||||
continue;
|
||||
}
|
||||
String key = BuiltInRegistries.BLOCK.getKey(block).toString();
|
||||
CompoundTag tag = codec.encode(new NativeRegionTerrainWriter.TerrainInput(0, 0, 0, 16,
|
||||
(x, y, z) -> x == 0 && y == 0 && z == 0 ? block.defaultBlockState() : Blocks.AIR.defaultBlockState(),
|
||||
(x, y, z) -> "minecraft:plains", List.of()));
|
||||
int expected = ((EntityBlock) block).newBlockEntity(BlockPos.ZERO, block.defaultBlockState()) == null ? 0 : 1;
|
||||
assertEquals(key, expected, tag.getListOrEmpty("block_entities").size());
|
||||
tested++;
|
||||
}
|
||||
assertTrue(tested > 50);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsMissingNativeStatesAndUnregisteredBiomes() throws Exception {
|
||||
NativeRegionTerrainWriter codec = new NativeRegionTerrainWriter(HeadlessNativeTestRegistries.get());
|
||||
assertThrows(NullPointerException.class, () -> codec.encode(new NativeRegionTerrainWriter.TerrainInput(
|
||||
0, 0, 0, 16, (x, y, z) -> null, (x, y, z) -> "minecraft:plains", List.of())));
|
||||
assertThrows(IllegalArgumentException.class, () -> codec.encode(new NativeRegionTerrainWriter.TerrainInput(
|
||||
0, 0, 0, 16, (x, y, z) -> Blocks.STONE.defaultBlockState(), (x, y, z) -> "iris:custom_biome", List.of())));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validatesSectionBounds() throws Exception {
|
||||
assertThrows(IllegalArgumentException.class, () -> new NativeRegionTerrainWriter.TerrainInput(
|
||||
0, 0, -63, 16, (x, y, z) -> Blocks.AIR.defaultBlockState(), (x, y, z) -> "minecraft:plains", List.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preservesDeclaredCustomBiomeKeys() throws Exception {
|
||||
NativeRegionTerrainWriter codec = new NativeRegionTerrainWriter(HeadlessNativeTestRegistries.get());
|
||||
CompoundTag tag = codec.encode(new NativeRegionTerrainWriter.TerrainInput(0, 0, 0, 16,
|
||||
(x, y, z) -> Blocks.STONE.defaultBlockState(), (x, y, z) -> "iris:biomes/test", List.of()));
|
||||
assertEquals("iris:biomes/test", tag.getListOrEmpty("sections").getCompoundOrEmpty(0)
|
||||
.getCompoundOrEmpty("biomes").getListOrEmpty("palette").getStringOr(0, ""));
|
||||
}
|
||||
}
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class RegionGenerationWindowTest {
|
||||
@Test
|
||||
public void replenishesWorkersWhileEarlierChunksRemainBlocked() throws Exception {
|
||||
CountDownLatch replacementStarted = new CountDownLatch(1);
|
||||
AtomicInteger active = new AtomicInteger();
|
||||
AtomicInteger maximum = new AtomicInteger();
|
||||
List<Integer> completed = new ArrayList<>();
|
||||
RegionGenerationWindow.process(new RegionGenerationWindow.Request<>(16, 2, index -> {
|
||||
int running = active.incrementAndGet();
|
||||
maximum.accumulateAndGet(running, Math::max);
|
||||
try {
|
||||
if (index == 0 && !replacementStarted.await(5, TimeUnit.SECONDS)) {
|
||||
throw new IllegalStateException("A completed worker was not replenished");
|
||||
}
|
||||
if (index == 2) {
|
||||
replacementStarted.countDown();
|
||||
}
|
||||
return index;
|
||||
} finally {
|
||||
active.decrementAndGet();
|
||||
}
|
||||
}, (index, value) -> completed.add(value)));
|
||||
assertEquals(16, completed.size());
|
||||
assertTrue(completed.indexOf(1) < completed.indexOf(0));
|
||||
assertTrue(maximum.get() <= 2);
|
||||
assertEquals(0, active.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void thirtyTwoWorkersRemainBoundedWhileReplenishing() throws Exception {
|
||||
CountDownLatch initialWorkers = new CountDownLatch(32);
|
||||
AtomicInteger active = new AtomicInteger();
|
||||
AtomicInteger maximum = new AtomicInteger();
|
||||
AtomicInteger completed = new AtomicInteger();
|
||||
RegionGenerationWindow.process(new RegionGenerationWindow.Request<>(97, 32, index -> {
|
||||
maximum.accumulateAndGet(active.incrementAndGet(), Math::max);
|
||||
try {
|
||||
if (index < 32) {
|
||||
initialWorkers.countDown();
|
||||
assertTrue(initialWorkers.await(5, TimeUnit.SECONDS));
|
||||
}
|
||||
return index;
|
||||
} finally {
|
||||
active.decrementAndGet();
|
||||
}
|
||||
}, (index, value) -> completed.incrementAndGet()));
|
||||
assertEquals(32, maximum.get());
|
||||
assertEquals(97, completed.get());
|
||||
assertEquals(0, active.get());
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new RegionGenerationWindow.Request<>(97, 33, index -> index, (index, value) -> { }));
|
||||
}
|
||||
|
||||
@Test(timeout = 5000)
|
||||
public void interruptsAndDrainsOtherWorkersAfterFailure() {
|
||||
CountDownLatch blocked = new CountDownLatch(1);
|
||||
AtomicInteger interrupted = new AtomicInteger();
|
||||
assertThrows(Exception.class, () -> RegionGenerationWindow.process(
|
||||
new RegionGenerationWindow.Request<>(2, 2, index -> {
|
||||
if (index == 0) {
|
||||
assertTrue(blocked.await(5, TimeUnit.SECONDS));
|
||||
throw new IllegalStateException("Expected fixture failure");
|
||||
}
|
||||
blocked.countDown();
|
||||
try {
|
||||
new CountDownLatch(1).await();
|
||||
} catch (InterruptedException failure) {
|
||||
interrupted.incrementAndGet();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
return index;
|
||||
}, (index, value) -> { })));
|
||||
assertEquals(1, interrupted.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void drainsStartedWorkBeforeReportingFailure() {
|
||||
CountDownLatch secondStarted = new CountDownLatch(1);
|
||||
AtomicInteger finished = new AtomicInteger();
|
||||
assertThrows(Exception.class, () -> RegionGenerationWindow.process(
|
||||
new RegionGenerationWindow.Request<>(4, 2, index -> {
|
||||
if (index == 0) {
|
||||
assertTrue(secondStarted.await(5, TimeUnit.SECONDS));
|
||||
throw new IllegalStateException("Expected fixture failure");
|
||||
}
|
||||
secondStarted.countDown();
|
||||
finished.incrementAndGet();
|
||||
return index;
|
||||
}, (index, value) -> { })));
|
||||
assertTrue(finished.get() >= 1);
|
||||
}
|
||||
|
||||
@Test(timeout = 5000)
|
||||
public void completionTimeoutInterruptsAndDrainsActiveWork() {
|
||||
AtomicInteger completed = new AtomicInteger();
|
||||
RegionGenerationWindow.Policy policy = new RegionGenerationWindow.Policy(
|
||||
Duration.ofMillis(50), Duration.ofSeconds(1), false);
|
||||
assertThrows(TimeoutException.class, () -> RegionGenerationWindow.process(
|
||||
new RegionGenerationWindow.Request<>(1, 1, index -> {
|
||||
try {
|
||||
new CountDownLatch(1).await();
|
||||
} finally {
|
||||
completed.incrementAndGet();
|
||||
}
|
||||
return index;
|
||||
}, (index, value) -> { }), policy));
|
||||
assertEquals(1, completed.get());
|
||||
}
|
||||
|
||||
@Test(timeout = 10000)
|
||||
public void standaloneStalledWorkerStopsProcessBeforeResourceCleanup() throws Exception {
|
||||
Path directory = Files.createTempDirectory("iris-worker-shutdown-");
|
||||
Path cleanup = directory.resolve("cleanup");
|
||||
String executable = System.getProperty("os.name").startsWith("Windows") ? "java.exe" : "java";
|
||||
String classpath = Path.of(RegionGenerationWindowTest.class.getProtectionDomain().getCodeSource().getLocation().toURI())
|
||||
+ File.pathSeparator
|
||||
+ Path.of(RegionGenerationWindow.class.getProtectionDomain().getCodeSource().getLocation().toURI());
|
||||
Process process = new ProcessBuilder(Path.of(System.getProperty("java.home"), "bin", executable).toString(),
|
||||
"-cp", classpath, UnresponsiveProcess.class.getName(), cleanup.toString())
|
||||
.redirectErrorStream(true).start();
|
||||
try {
|
||||
assertTrue("Offline failure did not terminate", process.waitFor(5, TimeUnit.SECONDS));
|
||||
String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||
assertEquals(output, 70, process.exitValue());
|
||||
assertTrue(output, output.contains("Generation workers did not stop"));
|
||||
assertTrue(output, output.contains("Expected process fixture failure"));
|
||||
assertFalse("Resources were closed while generation was active", Files.exists(cleanup));
|
||||
} finally {
|
||||
process.destroyForcibly();
|
||||
process.waitFor(2, TimeUnit.SECONDS);
|
||||
Files.deleteIfExists(cleanup);
|
||||
Files.delete(directory);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class UnresponsiveProcess {
|
||||
public static void main(String[] arguments) throws Exception {
|
||||
CountDownLatch entered = new CountDownLatch(1);
|
||||
CountDownLatch never = new CountDownLatch(1);
|
||||
RegionGenerationWindow.Policy policy = new RegionGenerationWindow.Policy(
|
||||
Duration.ofSeconds(1), Duration.ofMillis(25), true);
|
||||
try {
|
||||
RegionGenerationWindow.process(new RegionGenerationWindow.Request<>(2, 2, index -> {
|
||||
if (index == 0) {
|
||||
if (!entered.await(1, TimeUnit.SECONDS)) {
|
||||
throw new IllegalStateException("Worker did not start");
|
||||
}
|
||||
throw new IllegalStateException("Expected process fixture failure");
|
||||
}
|
||||
entered.countDown();
|
||||
while (true) {
|
||||
try {
|
||||
never.await();
|
||||
} catch (InterruptedException ignored) {
|
||||
continue;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
}, (index, value) -> { }), policy);
|
||||
} finally {
|
||||
Files.writeString(Path.of(arguments[0]), "closed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.ConsoleCommandSender;
|
||||
import org.bukkit.craftbukkit.CraftWorld;
|
||||
import org.bukkit.craftbukkit.CraftChunk;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.io.Reader;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class RegionNativeFixture extends JavaPlugin {
|
||||
private final AtomicBoolean running = new AtomicBoolean();
|
||||
private final LoadCompletions pendingLoads = new LoadCompletions();
|
||||
private final ExecutorService worker = Executors.newSingleThreadExecutor();
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
getLogger().info("Native TERRAIN export fixture ready; console: mcaterrain <world> <chunkX> <chunkZ> <width> <workers>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
pendingLoads.stop();
|
||||
worker.shutdown();
|
||||
try {
|
||||
if (!worker.awaitTermination(60, TimeUnit.SECONDS)) {
|
||||
getLogger().severe("Native terrain export worker did not stop before shutdown");
|
||||
}
|
||||
} catch (InterruptedException failure) {
|
||||
Thread.currentThread().interrupt();
|
||||
getLogger().log(Level.SEVERE, "Interrupted while stopping native terrain export", failure);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
|
||||
boolean loading = command.getName().equals("mcaload");
|
||||
int expectedArguments = 5;
|
||||
if (!(sender instanceof ConsoleCommandSender) || arguments.length != expectedArguments) {
|
||||
sender.sendMessage("Console only: " + command.getName() + " <world> <chunkX> <chunkZ> <width>"
|
||||
+ " <workers>");
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
if (!Bukkit.getWorldContainer().getCanonicalFile().getName().startsWith("mca-")) {
|
||||
throw new IllegalStateException("The fixture requires a disposable mca-* test instance");
|
||||
}
|
||||
if (!Bukkit.getOnlinePlayers().isEmpty() || Bukkit.getName().toLowerCase(Locale.ROOT).contains("folia")) {
|
||||
throw new IllegalStateException("The fixture requires an empty Paper test server");
|
||||
}
|
||||
World world = Bukkit.getWorld(arguments[0]);
|
||||
if (world == null || world.getGenerator() == null) {
|
||||
throw new IllegalArgumentException("A loaded Iris world is required");
|
||||
}
|
||||
int chunkX = Integer.parseInt(arguments[1]);
|
||||
int chunkZ = Integer.parseInt(arguments[2]);
|
||||
int width = Integer.parseInt(arguments[3]);
|
||||
List<NativeRegionPlan.PlannedChunk> plan = NativeRegionPlan.plan(chunkX, chunkZ, width);
|
||||
if (loading) {
|
||||
load(new LoadRequest(world, chunkX, chunkZ, width, Integer.parseInt(arguments[4])));
|
||||
return true;
|
||||
}
|
||||
Path worldRoot = world.getWorldFolder().toPath();
|
||||
for (NativeRegionPlan.PlannedChunk chunk : plan) {
|
||||
if (world.isChunkLoaded(chunk.x(), chunk.z()) || Files.exists(worldRoot.resolve("region")
|
||||
.resolve("r." + (chunk.x() >> 5) + "." + (chunk.z() >> 5) + ".mca"))) {
|
||||
throw new IllegalStateException("Native export requires untouched chunk and halo regions");
|
||||
}
|
||||
}
|
||||
Files.createDirectories(getDataFolder().toPath());
|
||||
Path output = getDataFolder().toPath().resolve("terrain-" + chunkX + "-" + chunkZ + "-" + width);
|
||||
ServerLevel level = ((CraftWorld) world).getHandle();
|
||||
DetachedNativeTerrain.Request request = new DetachedNativeTerrain.Request(level, chunkX, chunkZ, width,
|
||||
Integer.parseInt(arguments[4]), output);
|
||||
if (Files.exists(output) || !running.compareAndSet(false, true)) {
|
||||
throw new IllegalStateException("An export is active or the output already exists");
|
||||
}
|
||||
worker.execute(() -> export(request));
|
||||
sender.sendMessage("Native TERRAIN export started: " + output);
|
||||
} catch (Exception failure) {
|
||||
getLogger().log(Level.SEVERE, "Native TERRAIN export could not start", failure);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void load(LoadRequest request) throws Exception {
|
||||
Path completed = getDataFolder().toPath().resolve("terrain-" + request.chunkX() + "-" + request.chunkZ()
|
||||
+ "-" + request.width()).resolve("terrain.properties");
|
||||
if (!Files.isRegularFile(completed)) {
|
||||
throw new IllegalStateException("No completed native TERRAIN export exists for these coordinates");
|
||||
}
|
||||
Properties receipts = new Properties();
|
||||
try (Reader reader = Files.newBufferedReader(completed.getParent().resolve("receipts.properties"))) {
|
||||
receipts.load(reader);
|
||||
}
|
||||
if (!running.compareAndSet(false, true)) {
|
||||
throw new IllegalStateException("A native fixture operation is active");
|
||||
}
|
||||
worker.execute(() -> loadRegion(request, receipts));
|
||||
}
|
||||
|
||||
private void loadRegion(LoadRequest request, Properties receipts) {
|
||||
long started = System.nanoTime();
|
||||
int chunks = request.width() * request.width();
|
||||
getLogger().info("NATIVE_LOAD_STARTED chunks=" + chunks + " chunkX=" + request.chunkX()
|
||||
+ " chunkZ=" + request.chunkZ() + " workers=" + request.parallelism());
|
||||
try {
|
||||
RegionGenerationWindow.process(new RegionGenerationWindow.Request<>(chunks, request.parallelism(),
|
||||
index -> {
|
||||
int x = request.chunkX() + index % request.width();
|
||||
int z = request.chunkZ() + index / request.width();
|
||||
requestChunk(new ChunkLoad(request.world(), x, z, receipts.getProperty(x + "," + z))).join();
|
||||
return index;
|
||||
}, (index, completed) -> {}));
|
||||
getLogger().info("NATIVE_LOAD_COMPLETE chunks=" + chunks + " workers=" + request.parallelism()
|
||||
+ " status=full light=true receiptSha256Matches=true seconds="
|
||||
+ (System.nanoTime() - started) / 1_000_000_000.0);
|
||||
} catch (Exception failure) {
|
||||
getLogger().log(Level.SEVERE, "Native terrain reload failed after draining started requests", failure);
|
||||
} finally {
|
||||
running.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> requestChunk(ChunkLoad request) {
|
||||
CompletableFuture<Void> completion = pendingLoads.register();
|
||||
if (completion.isDone()) {
|
||||
return completion;
|
||||
}
|
||||
try {
|
||||
Bukkit.getScheduler().runTask(this, () -> {
|
||||
if (completion.isDone()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
request.world().getChunkAtAsync(request.x(), request.z(), true)
|
||||
.whenComplete((chunk, failure) -> completeLoad(new LoadedChunk(request, chunk, failure), completion));
|
||||
} catch (RuntimeException failure) {
|
||||
completion.completeExceptionally(failure);
|
||||
}
|
||||
});
|
||||
} catch (RuntimeException failure) {
|
||||
completion.completeExceptionally(failure);
|
||||
}
|
||||
return completion;
|
||||
}
|
||||
|
||||
private void completeLoad(LoadedChunk loaded, CompletableFuture<Void> completion) {
|
||||
if (completion.isDone()) {
|
||||
return;
|
||||
}
|
||||
if (loaded.failure() != null) {
|
||||
completion.completeExceptionally(loaded.failure());
|
||||
return;
|
||||
}
|
||||
if (!Bukkit.isPrimaryThread()) {
|
||||
try {
|
||||
Bukkit.getScheduler().runTask(this, () -> completeLoad(loaded, completion));
|
||||
} catch (RuntimeException failure) {
|
||||
completion.completeExceptionally(failure);
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ChunkAccess nativeChunk = ((CraftChunk) loaded.chunk()).getHandle(ChunkStatus.FULL);
|
||||
if (nativeChunk.getPersistedStatus() != ChunkStatus.FULL || !nativeChunk.isLightCorrect()
|
||||
|| !DetachedNativeTerrain.receiptHash(nativeChunk).equals(loaded.request().receipt())) {
|
||||
throw new IllegalStateException("Native load did not finish status, light and receipt at "
|
||||
+ loaded.request().x() + "," + loaded.request().z());
|
||||
}
|
||||
completion.complete(null);
|
||||
} catch (RuntimeException failure) {
|
||||
completion.completeExceptionally(failure);
|
||||
}
|
||||
}
|
||||
|
||||
private void export(DetachedNativeTerrain.Request request) {
|
||||
try {
|
||||
DetachedNativeTerrain.Result result = DetachedNativeTerrain.generate(request, getLogger()::info);
|
||||
String report = String.format(Locale.ROOT,
|
||||
"stage=minecraft:terrain%ncomplete=false%nterrainChunks=%d%ndependencyChunks=%d%nseconds=%.3f%nworkers=%d%n"
|
||||
+ "remaining=features,lighting,spawn,full%n"
|
||||
+ "world=%s%nseed=%d%n"
|
||||
+ "installation=Stop this test server, retain its world history and mantle, recheck that EVERY destination MCA file is absent, then copy all MCA files without replacement into this same untouched world region directory. Never copy while running.%n",
|
||||
result.terrainChunks(), result.dependencyChunks(), result.seconds(), request.parallelism(),
|
||||
request.level().getWorld().getName(), request.level().getSeed());
|
||||
Files.writeString(request.output().resolve("terrain.properties"), report);
|
||||
getLogger().info("NATIVE_TERRAIN_COMPLETE " + report.replace('\n', ' '));
|
||||
} catch (Exception failure) {
|
||||
getLogger().log(Level.SEVERE, "Native TERRAIN export failed; output is incomplete", failure);
|
||||
} finally {
|
||||
running.set(false);
|
||||
}
|
||||
}
|
||||
static final class LoadCompletions {
|
||||
private final Set<CompletableFuture<Void>> pending = ConcurrentHashMap.newKeySet();
|
||||
private final AtomicBoolean stopping = new AtomicBoolean();
|
||||
|
||||
CompletableFuture<Void> register() {
|
||||
CompletableFuture<Void> completion = new CompletableFuture<>();
|
||||
pending.add(completion);
|
||||
completion.whenComplete((result, failure) -> pending.remove(completion));
|
||||
if (stopping.get()) {
|
||||
cancel(completion);
|
||||
}
|
||||
return completion;
|
||||
}
|
||||
|
||||
void stop() {
|
||||
stopping.set(true);
|
||||
for (CompletableFuture<Void> completion : pending) {
|
||||
cancel(completion);
|
||||
}
|
||||
}
|
||||
|
||||
int pendingCount() {
|
||||
return pending.size();
|
||||
}
|
||||
|
||||
private static void cancel(CompletableFuture<Void> completion) {
|
||||
completion.completeExceptionally(new CancellationException("Native fixture is stopping"));
|
||||
}
|
||||
}
|
||||
|
||||
private record LoadRequest(World world, int chunkX, int chunkZ, int width, int parallelism) {
|
||||
private LoadRequest {
|
||||
if (parallelism < 1 || parallelism > 16) {
|
||||
throw new IllegalArgumentException("Native load worker count must be 1..16");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record ChunkLoad(World world, int x, int z, String receipt) {
|
||||
}
|
||||
|
||||
private record LoadedChunk(ChunkLoad request, Chunk chunk, Throwable failure) {
|
||||
}
|
||||
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
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 static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class RegionNativeFixtureTest {
|
||||
@Test
|
||||
public void stoppingReleasesPendingAndRejectsLaterAdmissions() {
|
||||
RegionNativeFixture.LoadCompletions requests = new RegionNativeFixture.LoadCompletions();
|
||||
CompletableFuture<Void> pending = requests.register();
|
||||
CompletableFuture<Void> completed = requests.register();
|
||||
completed.complete(null);
|
||||
assertEquals(1, requests.pendingCount());
|
||||
requests.stop();
|
||||
assertTrue(pending.isCompletedExceptionally());
|
||||
assertFalse(completed.isCompletedExceptionally());
|
||||
assertTrue(requests.register().isCompletedExceptionally());
|
||||
assertEquals(0, requests.pendingCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentShutdownLeavesNoUncompletedAdmissions() throws Exception {
|
||||
for (int round = 0; round < 32; round++) {
|
||||
RegionNativeFixture.LoadCompletions requests = new RegionNativeFixture.LoadCompletions();
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
List<Future<CompletableFuture<Void>>> admissions = new ArrayList<>();
|
||||
try (ExecutorService workers = Executors.newFixedThreadPool(8)) {
|
||||
for (int index = 0; index < 32; index++) {
|
||||
admissions.add(workers.submit(() -> {
|
||||
start.await();
|
||||
return requests.register();
|
||||
}));
|
||||
}
|
||||
start.countDown();
|
||||
requests.stop();
|
||||
for (Future<CompletableFuture<Void>> admission : admissions) {
|
||||
assertTrue(admission.get(5, TimeUnit.SECONDS).isCompletedExceptionally());
|
||||
}
|
||||
}
|
||||
assertEquals(0, requests.pendingCount());
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
-23
@@ -103,6 +103,21 @@ public class NativeStructureVolumeIndexTest {
|
||||
assertEquals(coldResolutions, resolver.resolutions());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completedOriginsRemainBoundedAndPendingBuildsAreReleased() throws Exception {
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(
|
||||
(engine, chunkX, chunkZ) -> NativeStructureVolume.NONE);
|
||||
for (int chunkX = -8_192; chunkX <= 8_192; chunkX++) {
|
||||
index.originVolumes(null, chunkX, -17);
|
||||
}
|
||||
assertEquals(16_384, cache(index, "originCache").size());
|
||||
assertTrue(cache(index, "originBuilds").isEmpty());
|
||||
index.resolve(null, -16, -16, -1, -1);
|
||||
assertEquals(16_384, cache(index, "originCache").size());
|
||||
assertTrue(cache(index, "originBuilds").isEmpty());
|
||||
assertTrue(cache(index, "queryBuilds").isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runtimeBucketsStayIsolatedAndRetirementEvictsOnlyTheirEntries() {
|
||||
AtomicInteger runtimeId = new AtomicInteger(1);
|
||||
@@ -282,7 +297,7 @@ public class NativeStructureVolumeIndexTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void overlappingColdQueriesCoordinateBeforeResolvingOrigins() throws Exception {
|
||||
public void overlappingColdQueriesResolveIndependentOriginsConcurrently() throws Exception {
|
||||
BlockingResolver resolver = new BlockingResolver();
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
@@ -291,7 +306,8 @@ public class NativeStructureVolumeIndexTest {
|
||||
assertTrue(resolver.awaitFirstEntry());
|
||||
Future<KList<NativeStructureVolume>> adjacent = executor.submit(() -> index.resolve(null, 16, 0, 31, 15));
|
||||
|
||||
assertFalse(resolver.awaitSecondEntry(250));
|
||||
assertTrue(resolver.awaitSecondEntry(5_000));
|
||||
assertTrue(adjacent.get(5, TimeUnit.SECONDS).isEmpty());
|
||||
resolver.release();
|
||||
assertTrue(first.get(5, TimeUnit.SECONDS).isEmpty());
|
||||
assertTrue(adjacent.get(5, TimeUnit.SECONDS).isEmpty());
|
||||
@@ -303,12 +319,42 @@ public class NativeStructureVolumeIndexTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contendedNativeWindowsAllowQueuedTerrainWorkToRun() throws Exception {
|
||||
public void overlappingNegativeQueriesPreserveEveryVolumeAndOriginOrder() throws Exception {
|
||||
BlockingResolver coordination = new BlockingResolver();
|
||||
NativeStructureVolumeIndex.OriginResolver volumes = (engine, chunkX, chunkZ) ->
|
||||
new KList<>(new NativeStructureVolume(chunkX + ":" + chunkZ,
|
||||
-256, 0, -256, 256, 255, 256));
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting((engine, chunkX, chunkZ) -> {
|
||||
coordination.volumesAt(engine, chunkX, chunkZ);
|
||||
return volumes.volumesAt(engine, chunkX, chunkZ);
|
||||
});
|
||||
NativeStructureVolumeIndex sequential = NativeStructureVolumeIndex.forTesting(volumes);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
Future<KList<NativeStructureVolume>> negative = executor.submit(
|
||||
() -> index.resolve(null, -16, 0, -1, 15));
|
||||
assertTrue(coordination.awaitFirstEntry());
|
||||
Future<KList<NativeStructureVolume>> positive = executor.submit(
|
||||
() -> index.resolve(null, 0, 0, 15, 15));
|
||||
assertTrue(coordination.awaitSecondEntry(5_000));
|
||||
assertEquals(sequential.resolve(null, 0, 0, 15, 15), positive.get(5, TimeUnit.SECONDS));
|
||||
coordination.release();
|
||||
assertEquals(sequential.resolve(null, -16, 0, -1, 15), negative.get(5, TimeUnit.SECONDS));
|
||||
assertEquals(306, coordination.resolutions());
|
||||
} finally {
|
||||
coordination.release();
|
||||
executor.shutdownNow();
|
||||
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sharedOriginWaitAllowsQueuedTerrainWorkToRun() throws Exception {
|
||||
assertQueuedTerrainCanComplete(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void interruptedNativeWindowWaitStillAcquiresAndReleasesOnce() throws Exception {
|
||||
public void interruptedOriginWaitPreservesInterruptAndResult() throws Exception {
|
||||
assertQueuedTerrainCanComplete(true);
|
||||
}
|
||||
|
||||
@@ -331,7 +377,7 @@ public class NativeStructureVolumeIndexTest {
|
||||
AtomicReference<Thread> secondWaiter = new AtomicReference<>();
|
||||
try {
|
||||
Future<KList<NativeStructureVolume>> original = owner.submit(
|
||||
() -> index.resolve(null, 0, 0, 15, 15));
|
||||
() -> index.originVolumes(null, -6, -8));
|
||||
assertTrue(ownerEntered.await(5, TimeUnit.SECONDS));
|
||||
Future<KList<NativeStructureVolume>> adjacent = workers.submit(() -> {
|
||||
firstWaiter.set(Thread.currentThread());
|
||||
@@ -343,15 +389,15 @@ public class NativeStructureVolumeIndexTest {
|
||||
secondWaiter.set(Thread.currentThread());
|
||||
return index.resolve(null, 32, 0, 47, 15);
|
||||
});
|
||||
awaitOriginWindowWaiter(firstWaiter);
|
||||
awaitOriginWindowWaiter(secondWaiter);
|
||||
awaitOriginWaiter(firstWaiter);
|
||||
awaitOriginWaiter(secondWaiter);
|
||||
if (interruptWaiter) {
|
||||
firstWaiter.get().interrupt();
|
||||
}
|
||||
|
||||
Future<?> terrainTask = workers.submit(() -> terrain.complete(null));
|
||||
terrainTask.get(3, TimeUnit.SECONDS);
|
||||
assertEquals(289, original.get(3, TimeUnit.SECONDS).size());
|
||||
assertEquals(1, original.get(3, TimeUnit.SECONDS).size());
|
||||
KList<NativeStructureVolume> adjacentVolumes = adjacent.get(3, TimeUnit.SECONDS);
|
||||
assertEquals(289, adjacentVolumes.size());
|
||||
assertEquals(adjacentVolumes, index.resolve(null, 16, 0, 31, 15));
|
||||
@@ -366,28 +412,28 @@ public class NativeStructureVolumeIndexTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static void awaitOriginWindowWaiter(AtomicReference<Thread> reference) {
|
||||
private static void awaitOriginWaiter(AtomicReference<Thread> reference) {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
|
||||
while (System.nanoTime() < deadline) {
|
||||
Thread thread = reference.get();
|
||||
if (thread != null && thread.getState() == Thread.State.WAITING) {
|
||||
for (StackTraceElement frame : thread.getStackTrace()) {
|
||||
if (frame.getClassName().equals(NativeStructureVolumeIndex.class.getName())
|
||||
&& frame.getMethodName().equals("lockOriginWindow")) {
|
||||
if (frame.getClassName().equals("art.arcane.volmlib.nativelib.terrain.NativeBuildFutures")
|
||||
&& frame.getMethodName().equals("awaitBuild")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
|
||||
}
|
||||
throw new AssertionError("Native query did not wait for the occupied origin window");
|
||||
throw new AssertionError("Native query did not wait for the shared origin");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void distantColdQueriesCanResolveOriginsConcurrently() throws Exception {
|
||||
BlockingResolver resolver = new BlockingResolver();
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
|
||||
int distantChunkX = findDisjointWindow(0, 0);
|
||||
int distantChunkX = 64;
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
Future<KList<NativeStructureVolume>> first = executor.submit(() -> index.resolve(null, 0, 0, 15, 15));
|
||||
@@ -478,16 +524,6 @@ public class NativeStructureVolumeIndexTest {
|
||||
);
|
||||
}
|
||||
|
||||
private static int findDisjointWindow(int chunkX, int chunkZ) {
|
||||
long sourceMask = NativeStructureVolumeIndex.originWindowStripeMask(chunkX, chunkZ);
|
||||
for (int candidate = 64; candidate < 16_384; candidate += 17) {
|
||||
if ((sourceMask & NativeStructureVolumeIndex.originWindowStripeMask(candidate, chunkZ)) == 0L) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("Unable to find a disjoint native structure window stripe");
|
||||
}
|
||||
|
||||
private static final class CountingResolver implements NativeStructureVolumeIndex.OriginResolver {
|
||||
private final AtomicInteger resolutions = new AtomicInteger();
|
||||
private final int originChunkX;
|
||||
|
||||
+4
-1
@@ -391,7 +391,10 @@ public final class ModdedEngineBootstrap {
|
||||
BlockDataMergeSupport.StateMerger previousMerger = BlockDataMergeSupport.bindPlatformMerger(new NativeStateMerger(ModdedBlockResolution.BLOCKS)::merge);
|
||||
rollback.add(() -> BlockDataMergeSupport.restorePlatformMerger(previousMerger));
|
||||
|
||||
NativeTileReader nativeTileReader = new NativeTileReader(boundLoader::currentServer);
|
||||
NativeTileReader nativeTileReader = new NativeTileReader(() -> {
|
||||
NativeModdedServer server = boundLoader.currentServer();
|
||||
return server == null ? null : server.registryAccess();
|
||||
});
|
||||
TileData.TileReader previousTileReader = TileData.bindPlatformReader(in -> ModdedTileData.wrap(nativeTileReader.read(in)));
|
||||
rollback.add(() -> TileData.restorePlatformReader(previousTileReader));
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ import art.arcane.iris.spi.PlatformScheduler;
|
||||
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||
|
||||
import net.minecraft.core.HolderLookup;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -61,13 +63,25 @@ public final class ModdedPlatform implements IrisPlatform {
|
||||
public ModdedPlatform(NativeModdedLoader loader) {
|
||||
this.loader = loader;
|
||||
this.registries = new ModdedRegistries(
|
||||
new NativeRegistryAccess(ModdedEngineBootstrap::currentServer, ModdedRegistries::warnNotReady),
|
||||
new NativeRegistryAccess(new NativeRegistryAccess.Configuration(
|
||||
ModdedPlatform::nativeRegistries, ModdedPlatform::reloadableRegistries,
|
||||
ModdedRegistries::warnNotReady)),
|
||||
ModdedPlatform::generationRegistry);
|
||||
this.scheduler = new ModdedScheduler();
|
||||
this.structureHooks = new ModdedStructureHooks(ModdedEngineBootstrap::currentServer);
|
||||
this.biomeWriter = new ModdedBiomeWriter(new NativeBiomeRegistry(ModdedEngineBootstrap::currentServer, "minecraft:plains"));
|
||||
}
|
||||
|
||||
private static HolderLookup.Provider nativeRegistries() {
|
||||
NativeModdedServer server = ModdedEngineBootstrap.currentServer();
|
||||
return server == null ? null : server.registryAccess();
|
||||
}
|
||||
|
||||
private static HolderLookup.Provider reloadableRegistries() {
|
||||
NativeModdedServer server = ModdedEngineBootstrap.currentServer();
|
||||
return server == null ? null : server.reloadableRegistries();
|
||||
}
|
||||
|
||||
public static void errorSink(Consumer<Throwable> sink) {
|
||||
ERROR_SINK = sink;
|
||||
}
|
||||
|
||||
+2
-2
@@ -25,7 +25,7 @@ public class NativeRegistryAccessTest {
|
||||
|
||||
@Test
|
||||
public void statePropertiesPreserveTypesDefaultsAndSharedGroups() {
|
||||
NativeRegistryAccess access = new NativeRegistryAccess(() -> null, message -> {});
|
||||
NativeRegistryAccess access = new NativeRegistryAccess(new NativeRegistryAccess.Configuration(() -> null, () -> null, message -> {}));
|
||||
Map<String, List<NativeBlockProperty>> properties = access.blockStateProperties();
|
||||
|
||||
assertSame(properties.get("minecraft:stone"), properties.get("minecraft:dirt"));
|
||||
@@ -41,7 +41,7 @@ public class NativeRegistryAccessTest {
|
||||
@Test
|
||||
public void staticRegistriesResolveWithoutAnActiveServerAndDynamicReadsStayEmpty() {
|
||||
List<String> warnings = new ArrayList<>();
|
||||
NativeRegistryAccess access = new NativeRegistryAccess(() -> null, warnings::add);
|
||||
NativeRegistryAccess access = new NativeRegistryAccess(new NativeRegistryAccess.Configuration(() -> null, () -> null, warnings::add));
|
||||
|
||||
assertEquals("minecraft:diamond_sword", access.item("DIAMOND SWORD").key());
|
||||
assertEquals("creature", access.entity("minecraft:cow").spawnCategory());
|
||||
|
||||
+157
@@ -163,6 +163,9 @@ nmsBindings.each { String key ->
|
||||
compileOnly(rootProject.libs.byteBuddy.core)
|
||||
testImplementation('junit:junit:4.13.2')
|
||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
||||
if (key == 'v26_3_R1') {
|
||||
testImplementation(project(':probe'))
|
||||
}
|
||||
testImplementation(project(':core'))
|
||||
testImplementation(testFixtures(project(':core')))
|
||||
testImplementation("com.github.VolmitSoftware.VolmLib:native-common:${volmLibCoordinate.tokenize(':').last()}") {
|
||||
@@ -955,3 +958,157 @@ pluginPackaging {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
project(':adapters:bukkit:nms:v26_3_R1') {
|
||||
Configuration headlessNativeSources = configurations.create('headlessNativeSources') {
|
||||
canBeConsumed = false
|
||||
canBeResolved = true
|
||||
}
|
||||
dependencies.add(headlessNativeSources.name,
|
||||
"com.github.VolmitSoftware.VolmLib:native-minecraft26_2-sources:${volmLibCoordinate.tokenize(':').last()}")
|
||||
List<String> headlessNativeTypes = ['ModdedBlockState', 'ModdedBiome', 'NativeBlockProperties', 'NativeBlockResolver',
|
||||
'NativeBlockFaces', 'NativeStateRotator', 'NativeStateMerger',
|
||||
'NativeRegistryAccess', 'ModdedItem', 'ModdedEntityType', 'NativeTileReader', 'NativeTileData']
|
||||
TaskProvider<Sync> unpackHeadlessNativeSources = tasks.register('unpackHeadlessNativeSources', Sync) {
|
||||
dependsOn(headlessNativeSources)
|
||||
inputs.property('nativeTypes', headlessNativeTypes)
|
||||
inputs.property('nativeVersion', '26.3')
|
||||
from(provider { headlessNativeSources.collect { File artifact -> zipTree(artifact) } }) {
|
||||
headlessNativeTypes.each { String type -> include("art/arcane/volmlib/nativelib/minecraft26_2/modded/${type}.java") }
|
||||
include('versions/26.3/**')
|
||||
eachFile { FileCopyDetails source ->
|
||||
if (source.path.startsWith('versions/26.3/')) {
|
||||
source.path = source.path.substring('versions/26.3/'.length())
|
||||
}
|
||||
}
|
||||
}
|
||||
from(rootProject.file('adapters/modded-common/src/main/java')) {
|
||||
include('art/arcane/iris/modded/ModdedDecoratorHooks.java',
|
||||
'art/arcane/iris/modded/ModdedStateRotator.java', 'art/arcane/iris/modded/ModdedTileData.java')
|
||||
}
|
||||
into(layout.buildDirectory.dir('generated/sources/headless-native'))
|
||||
}
|
||||
sourceSets {
|
||||
offline {
|
||||
java.srcDir(unpackHeadlessNativeSources)
|
||||
compileClasspath += sourceSets.main.compileClasspath
|
||||
runtimeClasspath += sourceSets.main.compileClasspath + sourceSets.main.output
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
offlineImplementation(project(':probe'))
|
||||
offlineImplementation(project(':core'))
|
||||
offlineImplementation("com.github.VolmitSoftware.VolmLib:native-common:${volmLibCoordinate.tokenize(':').last()}") {
|
||||
transitive = false
|
||||
}
|
||||
offlineImplementation(volmLibCoordinate) {
|
||||
transitive = false
|
||||
}
|
||||
}
|
||||
sourceSets.test.compileClasspath += sourceSets.offline.output
|
||||
sourceSets.test.runtimeClasspath += sourceSets.offline.output
|
||||
tasks.named('test', Test).configure {
|
||||
forkEvery = 1
|
||||
}
|
||||
|
||||
TaskProvider<Jar> offlineJar = tasks.register('offlineJar', Jar) {
|
||||
dependsOn(offlineClasses)
|
||||
archiveClassifier = 'offline'
|
||||
from(sourceSets.offline.runtimeClasspath.filter { File file -> file.isDirectory() })
|
||||
}
|
||||
FileCollection offlineLibraries = sourceSets.offline.runtimeClasspath.filter { File file -> file.isFile() }
|
||||
TaskProvider<CreateStartScripts> offlineStartScripts = tasks.register('offlineStartScripts', CreateStartScripts) {
|
||||
dependsOn(offlineJar)
|
||||
mainClass = 'art.arcane.iris.probe.OfflineRegionGenerator'
|
||||
applicationName = 'iris-offline'
|
||||
outputDir = layout.buildDirectory.dir('offline-scripts').get().asFile
|
||||
classpath = files(offlineJar) + offlineLibraries
|
||||
defaultJvmOpts = ['-Xmx8G', '--add-modules=jdk.incubator.vector', '-Djava.awt.headless=true']
|
||||
}
|
||||
tasks.register('installOffline', Sync) {
|
||||
group = 'distribution'
|
||||
description = 'Installs the standalone native region generator and its runtime libraries.'
|
||||
dependsOn(offlineJar, offlineStartScripts)
|
||||
into(layout.buildDirectory.dir('install/iris-offline'))
|
||||
into('lib') { from(offlineJar, offlineLibraries) }
|
||||
into('bin') {
|
||||
from(offlineStartScripts)
|
||||
filePermissions { unix('rwxr-xr-x') }
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('regionMantleInspection', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn(testClasses)
|
||||
mainClass = 'art.arcane.iris.probe.HeadlessMantleInspection'
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
maxHeapSize = '4G'
|
||||
jvmArgs('--add-modules', 'jdk.incubator.vector', '-Djava.awt.headless=true')
|
||||
doFirst {
|
||||
args(['probePack', 'probeDimension', 'probeSeed', 'probeWorld', 'probeRegionX', 'probeRegionZ']
|
||||
.collect { String name -> providers.gradleProperty(name).get() })
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('generateOfflineRegions', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Writes fresh native TERRAIN checkpoints with exact prerequisites for subsequent native FULL completion.'
|
||||
dependsOn(offlineClasses)
|
||||
mainClass = 'art.arcane.iris.probe.OfflineRegionGenerator'
|
||||
classpath = sourceSets.offline.runtimeClasspath
|
||||
maxHeapSize = '8G'
|
||||
jvmArgs('--add-modules', 'jdk.incubator.vector', '-Djava.awt.headless=true')
|
||||
doFirst {
|
||||
List<String> names = ['probePack', 'probeDimension', 'probeSeed', 'probeChunkX', 'probeChunkZ',
|
||||
'probeWidth', 'probeRegionsX', 'probeRegionsZ', 'probeParallelism', 'probeSpigot', 'probeWorldName', 'probeLevelKey',
|
||||
'probeNoiseSettings', 'probeOutput']
|
||||
args(names.collect { String name -> providers.gradleProperty(name).get() })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
project(':adapters:bukkit:nms:v26_3_R1') {
|
||||
Provider<Directory> fixtureResources = layout.buildDirectory.dir('generated/region-native-fixture')
|
||||
TaskProvider<Task> fixtureDescriptor = tasks.register('regionNativeFixtureDescriptor') {
|
||||
outputs.dir(fixtureResources)
|
||||
doLast {
|
||||
File descriptor = fixtureResources.get().file('plugin.yml').asFile
|
||||
descriptor.parentFile.mkdirs()
|
||||
descriptor.text = '''name: RegionNativeFixture
|
||||
version: 1.0
|
||||
main: art.arcane.iris.probe.RegionNativeFixture
|
||||
api-version: '26.3'
|
||||
depend: [Iris]
|
||||
commands:
|
||||
mcaterrain:
|
||||
description: Export native terrain checkpoints on an isolated test server
|
||||
mcaload:
|
||||
description: Verify native terrain checkpoints finish generation on reload
|
||||
'''
|
||||
}
|
||||
}
|
||||
TaskProvider<JavaCompile> compileRegionNativeFixture = tasks.register('compileRegionNativeFixture', JavaCompile) {
|
||||
source(fileTree('src/test/java') {
|
||||
include('art/arcane/iris/probe/RegionNativeFixture.java',
|
||||
'art/arcane/iris/probe/DetachedNativeTerrain.java')
|
||||
})
|
||||
source(fileTree('src/offline/java') {
|
||||
include('art/arcane/iris/probe/RegionGenerationWindow.java',
|
||||
'art/arcane/iris/probe/NativeRegionTerrainWriter.java',
|
||||
'art/arcane/iris/probe/NativeRegionPlan.java')
|
||||
})
|
||||
classpath = sourceSets.test.compileClasspath
|
||||
destinationDirectory = layout.buildDirectory.dir('classes/region-native-fixture')
|
||||
javaCompiler = javaToolchains.compilerFor { languageVersion = JavaLanguageVersion.of(25) }
|
||||
options.release = 25
|
||||
options.compilerArgs.add('-parameters')
|
||||
}
|
||||
tasks.register('regionNativeFixtureJar', Jar) {
|
||||
dependsOn(compileRegionNativeFixture, fixtureDescriptor)
|
||||
archiveFileName = 'mca-native-fixture.jar'
|
||||
from(compileRegionNativeFixture.flatMap { JavaCompile task -> task.destinationDirectory })
|
||||
from(fixtureResources)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,9 +115,9 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
|
||||
private final transient AtomicCache<Integer> maxWithObjectHeight = new AtomicCache<>();
|
||||
private final transient AtomicCache<IrisBiome> realCarveBiome = new AtomicCache<>();
|
||||
private final transient AtomicCache<KList<IrisBiome>> realChildren = new AtomicCache<>();
|
||||
private final transient AtomicCache<KList<CNG>> layerHeightGenerators = new AtomicCache<>();
|
||||
private final transient AtomicCache<KList<CNG>> layerCeilingHeightGenerators = new AtomicCache<>();
|
||||
private final transient AtomicCache<KList<CNG>> layerSeaHeightGenerators = new AtomicCache<>();
|
||||
private final transient IrisBiomeLayerHeightCache layerHeightGenerators = new IrisBiomeLayerHeightCache();
|
||||
private final transient IrisBiomeLayerHeightCache layerCeilingHeightGenerators = new IrisBiomeLayerHeightCache();
|
||||
private final transient IrisBiomeLayerHeightCache layerSeaHeightGenerators = new IrisBiomeLayerHeightCache();
|
||||
private final transient AtomicCache<KList<IrisOreGenerator>> surfaceOreCache = new AtomicCache<>();
|
||||
private final transient AtomicCache<KList<IrisOreGenerator>> undergroundOreCache = new AtomicCache<>();
|
||||
private final transient AtomicCache<IrisOreGeneratorBounds> surfaceOreBoundsCache = new AtomicCache<>();
|
||||
|
||||
@@ -25,7 +25,6 @@ import art.arcane.iris.structure.object.IrisObjectPlacement;
|
||||
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.volmlib.util.cache.AtomicCache;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
@@ -316,27 +315,10 @@ final class IrisBiomeLayerGenerator {
|
||||
return getHeightGenerators(biome.getLayerSeaHeightGenerators(), biome.getSeaLayers(), 7735, rng, data);
|
||||
}
|
||||
|
||||
private static KList<CNG> getHeightGenerators(AtomicCache<KList<CNG>> cache,
|
||||
private static KList<CNG> getHeightGenerators(IrisBiomeLayerHeightCache cache,
|
||||
KList<IrisBiomePaletteLayer> layers,
|
||||
int seedOffset, RNG rng, IrisData data) {
|
||||
KList<CNG> cached = cache.getIfPresent();
|
||||
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
return cache.aquire(() ->
|
||||
{
|
||||
KList<CNG> layerHeightGenerators = new KList<>();
|
||||
|
||||
int m = seedOffset;
|
||||
|
||||
for (IrisBiomePaletteLayer layer : layers) {
|
||||
layerHeightGenerators.add(layer.getHeightGenerator(rng.nextParallelRNG((m++) * m * m * m), data));
|
||||
}
|
||||
|
||||
return layerHeightGenerators;
|
||||
});
|
||||
return cache.get(layers, seedOffset, rng, data);
|
||||
}
|
||||
|
||||
private static void appendLayer(KList<NativeBlockState> blocks, IrisBiomePaletteLayer layer,
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package art.arcane.iris.generation.biome;
|
||||
|
||||
import art.arcane.iris.generation.cache.LazyBoundedCache;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.noise.CNG;
|
||||
|
||||
final class IrisBiomeLayerHeightCache {
|
||||
private final LazyBoundedCache<GeneratorKey, KList<CNG>> generators = new LazyBoundedCache<>(32);
|
||||
private volatile CachedGenerators recent;
|
||||
|
||||
KList<CNG> get(KList<IrisBiomePaletteLayer> layers, int seedOffset, RNG rng, IrisData data) {
|
||||
Engine engine = data == null ? null : data.getEngine();
|
||||
long seed = rng.getSeed();
|
||||
CachedGenerators cached = recent;
|
||||
if (cached != null && cached.key.matches(data, engine, seed)) {
|
||||
return cached.generators;
|
||||
}
|
||||
|
||||
GeneratorKey key = new GeneratorKey(data, engine, seed);
|
||||
KList<CNG> result = generators.computeIfAbsent(key, ignored -> create(layers, seedOffset, rng, data));
|
||||
recent = new CachedGenerators(key, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static KList<CNG> create(KList<IrisBiomePaletteLayer> layers, int seedOffset, RNG rng, IrisData data) {
|
||||
KList<CNG> result = new KList<>();
|
||||
int offset = seedOffset;
|
||||
for (IrisBiomePaletteLayer layer : layers) {
|
||||
result.add(layer.getHeightGenerator(rng.nextParallelRNG((offset++) * offset * offset * offset), data));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private record CachedGenerators(GeneratorKey key, KList<CNG> generators) {
|
||||
}
|
||||
|
||||
private static final class GeneratorKey {
|
||||
private final IrisData data;
|
||||
private final Engine engine;
|
||||
private final long seed;
|
||||
|
||||
private GeneratorKey(IrisData data, Engine engine, long seed) {
|
||||
this.data = data;
|
||||
this.engine = engine;
|
||||
this.seed = seed;
|
||||
}
|
||||
|
||||
private boolean matches(IrisData data, Engine engine, long seed) {
|
||||
return this.data == data && this.engine == engine && this.seed == seed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
return this == object || object instanceof GeneratorKey other
|
||||
&& matches(other.data, other.engine, other.seed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = System.identityHashCode(data);
|
||||
result = 31 * result + System.identityHashCode(engine);
|
||||
return 31 * result + Long.hashCode(seed);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,13 @@ public class IrisBiomePaletteLayer {
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private final transient AtomicReference<CachedLayerGenerator> recentLayerGenerator = new AtomicReference<>();
|
||||
private final transient AtomicCache<CNG> heightGenerator = new AtomicCache<>();
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private final transient LazyBoundedCache<LayerGeneratorKey, CNG> heightGenerators =
|
||||
new LazyBoundedCache<>(LAYER_GENERATOR_CACHE_SIZE);
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private final transient AtomicReference<CachedLayerGenerator> recentHeightGenerator = new AtomicReference<>();
|
||||
@Description("The style of noise")
|
||||
private IrisGeneratorStyle style = NoiseStyle.STATIC.style();
|
||||
@DependsOn({"minHeight", "maxHeight"})
|
||||
@@ -91,13 +97,18 @@ public class IrisBiomePaletteLayer {
|
||||
private KList<IrisBlockData> palette = new KList<IrisBlockData>().qadd(new IrisBlockData("GRASS_BLOCK"));
|
||||
|
||||
public CNG getHeightGenerator(RNG rng, IrisData data) {
|
||||
CNG cached = heightGenerator.getIfPresent();
|
||||
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
Engine engine = data == null ? null : data.getEngine();
|
||||
long generatorSeed = rng.getSeed() + minHeight * maxHeight + getBlockData(data).size();
|
||||
CachedLayerGenerator recent = recentHeightGenerator.get();
|
||||
if (recent != null && recent.key.matches(data, engine, generatorSeed)) {
|
||||
return recent.generator;
|
||||
}
|
||||
|
||||
return heightGenerator.aquire(() -> CNG.signature(rng.nextParallelRNG(minHeight * maxHeight + getBlockData(data).size())));
|
||||
LayerGeneratorKey key = new LayerGeneratorKey(data, engine, generatorSeed);
|
||||
CNG generator = heightGenerators.computeIfAbsent(key,
|
||||
ignored -> CNG.signature(new RNG(generatorSeed)));
|
||||
recentHeightGenerator.set(new CachedLayerGenerator(key, generator));
|
||||
return generator;
|
||||
}
|
||||
|
||||
public NativeBlockState get(RNG rng, double x, double y, double z, IrisData data) {
|
||||
|
||||
@@ -35,7 +35,8 @@ public class LinkedTerrainChunk implements TerrainChunk {
|
||||
private final int minHeight;
|
||||
private final int maxHeight;
|
||||
private final int biomeHeight;
|
||||
private final Biome[] biomes;
|
||||
private final NativeBiome[] biomes;
|
||||
private volatile NativeBiome defaultBiome;
|
||||
|
||||
public LinkedTerrainChunk(World world) {
|
||||
this(Bukkit.createChunkData(world));
|
||||
@@ -46,19 +47,27 @@ public class LinkedTerrainChunk implements TerrainChunk {
|
||||
minHeight = data.getMinHeight();
|
||||
maxHeight = data.getMaxHeight();
|
||||
biomeHeight = Math.max(1, maxHeight - minHeight);
|
||||
biomes = new Biome[CHUNK_SIZE * biomeHeight * CHUNK_SIZE];
|
||||
biomes = new NativeBiome[CHUNK_SIZE * biomeHeight * CHUNK_SIZE];
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBiome getBiome(int x, int y, int z) {
|
||||
int index = biomeIndex(x, y, z);
|
||||
Biome biome = biomes[index];
|
||||
return BukkitBiome.of(biome == null ? Biome.PLAINS : biome);
|
||||
NativeBiome biome = biomes[index];
|
||||
if (biome != null) {
|
||||
return biome;
|
||||
}
|
||||
NativeBiome fallback = defaultBiome;
|
||||
if (fallback == null) {
|
||||
fallback = BukkitBiome.of(Biome.PLAINS);
|
||||
defaultBiome = fallback;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBiome(int x, int y, int z, NativeBiome bio) {
|
||||
biomes[biomeIndex(x, y, z)] = (Biome) bio.nativeHandle();
|
||||
biomes[biomeIndex(x, y, z)] = canonicalBiome(bio);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,7 +75,7 @@ public class LinkedTerrainChunk implements TerrainChunk {
|
||||
* calling setBiome for every y in [minHeight, maxHeight).
|
||||
*/
|
||||
public void fillBiomeColumn(int x, int z, NativeBiome bio) {
|
||||
Biome handle = (Biome) bio.nativeHandle();
|
||||
NativeBiome handle = canonicalBiome(bio);
|
||||
int stride = CHUNK_SIZE * CHUNK_SIZE;
|
||||
int index = (z & (CHUNK_SIZE - 1)) * CHUNK_SIZE + (x & (CHUNK_SIZE - 1));
|
||||
|
||||
@@ -120,4 +129,12 @@ public class LinkedTerrainChunk implements TerrainChunk {
|
||||
int clampedY = Math.max(minHeight, Math.min(maxHeight - 1, y)) - minHeight;
|
||||
return (clampedY * CHUNK_SIZE + clampedZ) * CHUNK_SIZE + clampedX;
|
||||
}
|
||||
|
||||
private static NativeBiome canonicalBiome(NativeBiome biome) {
|
||||
if (biome instanceof BukkitBiome) {
|
||||
return biome;
|
||||
}
|
||||
Biome handle = (Biome) biome.nativeHandle();
|
||||
return handle == null ? null : BukkitBiome.of(handle);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -13,7 +13,9 @@ public record TreeBlockMaterial(String materialKey) {
|
||||
}
|
||||
|
||||
public static TreeBlockMaterial of(NativeBlockState state) {
|
||||
return of(Objects.requireNonNull(state, "state").key());
|
||||
NativeBlockState resolved = Objects.requireNonNull(state, "state");
|
||||
String materialKey = resolved.materialKey();
|
||||
return materialKey == null ? of(resolved.key()) : new TreeBlockMaterial(materialKey);
|
||||
}
|
||||
|
||||
public static TreeBlockMaterial of(String blockStateKey) {
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.volmlib.util.cache.CacheKey;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
||||
|
||||
final class HydrologyBasisCache {
|
||||
private static final int MAXIMUM_ENTRIES = 32_768;
|
||||
private static final int MAXIMUM_STRIPES = 64;
|
||||
private final Stripe[] full;
|
||||
private final Stripe[] withoutSlope;
|
||||
|
||||
HydrologyBasisCache() {
|
||||
this(MAXIMUM_ENTRIES);
|
||||
}
|
||||
|
||||
HydrologyBasisCache(int maximumEntries) {
|
||||
if (maximumEntries < 1 || Integer.bitCount(maximumEntries) != 1) {
|
||||
throw new IllegalArgumentException("Terrain basis cache size must be a positive power of two.");
|
||||
}
|
||||
int stripeCount = Math.min(MAXIMUM_STRIPES, maximumEntries);
|
||||
full = new Stripe[stripeCount];
|
||||
withoutSlope = new Stripe[stripeCount];
|
||||
for (int index = 0; index < stripeCount; index++) {
|
||||
full[index] = new Stripe(maximumEntries / stripeCount);
|
||||
withoutSlope[index] = new Stripe(maximumEntries / stripeCount);
|
||||
}
|
||||
}
|
||||
|
||||
HydrologyTerrainSample sample(int x, int z, HydrologyNaturalTerrainSampler sampler) {
|
||||
return sample(x, z, sampler, false);
|
||||
}
|
||||
|
||||
HydrologyTerrainSample sampleWithoutSlope(int x, int z, HydrologyNaturalTerrainSampler sampler) {
|
||||
return sample(x, z, sampler, true);
|
||||
}
|
||||
|
||||
void seed(Long2ObjectMap<HydrologyTerrainSample> samples) {
|
||||
seed(samples, full);
|
||||
}
|
||||
|
||||
boolean findWithoutSlope(long packed, HydrologyTerrainSample[] samples, int index) {
|
||||
Stripe stripe = stripe(withoutSlope, packed);
|
||||
synchronized (stripe) {
|
||||
samples[index] = stripe.samples.getAndMoveToFirst(packed);
|
||||
return samples[index] != null || stripe.samples.containsKey(packed);
|
||||
}
|
||||
}
|
||||
|
||||
HydrologyTerrainSample rememberWithoutSlope(long packed, HydrologyTerrainSample sample) {
|
||||
Stripe stripe = stripe(withoutSlope, packed);
|
||||
synchronized (stripe) {
|
||||
if (stripe.samples.containsKey(packed)) {
|
||||
return stripe.samples.getAndMoveToFirst(packed);
|
||||
}
|
||||
stripe.put(packed, sample);
|
||||
return sample;
|
||||
}
|
||||
}
|
||||
|
||||
void seedWithoutSlope(Long2ObjectMap<HydrologyTerrainSample> samples) {
|
||||
seed(samples, withoutSlope);
|
||||
}
|
||||
|
||||
private HydrologyTerrainSample sample(int x, int z, HydrologyNaturalTerrainSampler sampler, boolean omitSlope) {
|
||||
long packed = RiverFootprint.pack(x, z);
|
||||
Stripe stripe = stripe(omitSlope ? withoutSlope : full, packed);
|
||||
synchronized (stripe) {
|
||||
HydrologyTerrainSample cached = stripe.samples.getAndMoveToFirst(packed);
|
||||
if (cached != null || stripe.samples.containsKey(packed)) {
|
||||
return cached;
|
||||
}
|
||||
HydrologyTerrainSample sampled = omitSlope ? sampler.sampleBasisWithoutSlope(x, z) : sampler.sampleBasis(x, z);
|
||||
stripe.put(packed, sampled);
|
||||
return sampled;
|
||||
}
|
||||
}
|
||||
|
||||
private void seed(Long2ObjectMap<HydrologyTerrainSample> samples, Stripe[] stripes) {
|
||||
for (Long2ObjectMap.Entry<HydrologyTerrainSample> entry : samples.long2ObjectEntrySet()) {
|
||||
Stripe stripe = stripe(stripes, entry.getLongKey());
|
||||
synchronized (stripe) {
|
||||
stripe.put(entry.getLongKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Stripe stripe(Stripe[] stripes, long packed) {
|
||||
return stripes[(int) CacheKey.mix(packed) & (stripes.length - 1)];
|
||||
}
|
||||
|
||||
private static final class Stripe {
|
||||
private final int maximumEntries;
|
||||
private final Long2ObjectLinkedOpenHashMap<HydrologyTerrainSample> samples;
|
||||
|
||||
private Stripe(int maximumEntries) {
|
||||
this.maximumEntries = maximumEntries;
|
||||
samples = new Long2ObjectLinkedOpenHashMap<>(Math.min(16, maximumEntries));
|
||||
}
|
||||
|
||||
private void put(long packed, HydrologyTerrainSample value) {
|
||||
if (samples.size() >= maximumEntries && !samples.containsKey(packed)) {
|
||||
samples.removeLast();
|
||||
}
|
||||
samples.putAndMoveToFirst(packed, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-3
@@ -269,6 +269,7 @@ final class HydrologyCrossTileResolver {
|
||||
}
|
||||
HydrologyCaveCourseFilter.Result result = publication.result();
|
||||
List<HydrologyDiagnosticCandidate> uniqueDiagnostics = planner.sourcePlanner.uniqueDiagnostics(diagnostics);
|
||||
footprintCompiler.sharedBasis = null;
|
||||
HydrologyOwnerDraft draft = new HydrologyOwnerDraft(
|
||||
key,
|
||||
result,
|
||||
@@ -295,9 +296,15 @@ final class HydrologyCrossTileResolver {
|
||||
HydrologyPlanner.PlanningSamples ownerSamples = planner.planningSamples.get();
|
||||
HydrologyLandHeightCache sharedLandHeights = planner.naturalSampler == null ? null
|
||||
: ownerSamples == null ? new HydrologyLandHeightCache() : ownerSamples.fallbackLandHeights();
|
||||
HydrologyBasisCache sharedBasis = null;
|
||||
if (planner.naturalSampler != null) {
|
||||
sharedBasis = ownerSamples == null ? new HydrologyBasisCache()
|
||||
: ownerSamples.fallbackBasis(context.footprintCompiler().terrainBases);
|
||||
}
|
||||
FallbackSamples shared = new FallbackSamples(sharedLandHeights, sharedBasis);
|
||||
for (int index = start; index < end; index++) {
|
||||
OutletCandidate outlet = outlets.get(index);
|
||||
tasks.add(() -> compileFallbackTrial(context, outlet, sharedLandHeights));
|
||||
tasks.add(() -> compileFallbackTrial(context, outlet, shared));
|
||||
}
|
||||
List<FallbackTrial> trials = HydrologyForkJoin.invokeAll(tasks,
|
||||
IrisPlatforms.isBound() ? MultiBurst.hydrology : null);
|
||||
@@ -318,7 +325,7 @@ final class HydrologyCrossTileResolver {
|
||||
private FallbackTrial compileFallbackTrial(
|
||||
FallbackContext context,
|
||||
OutletCandidate outlet,
|
||||
HydrologyLandHeightCache sharedLandHeights
|
||||
FallbackSamples shared
|
||||
) {
|
||||
HydrologyPlanner.PlanningSamples previousSamples = planner.planningSamples.get();
|
||||
DraftProfile previousProfile = planner.draftProfiles.get();
|
||||
@@ -328,12 +335,13 @@ final class HydrologyCrossTileResolver {
|
||||
HashMap<SourceCompilationKey, SourceCompilation> sourceCompilations = new HashMap<>(context.sourceCompilations());
|
||||
HydrologyFootprintCompiler footprints = null;
|
||||
try {
|
||||
planner.planningSamples.set(new HydrologyPlanner.PlanningSamples(sharedLandHeights));
|
||||
planner.planningSamples.set(new HydrologyPlanner.PlanningSamples(shared.landHeights(), shared.basis()));
|
||||
planner.draftProfiles.set(profile);
|
||||
footprints = new HydrologyFootprintCompiler(
|
||||
planner.settings,
|
||||
new HydrologyFootprintCompiler.Sampling(planner.sampler, planner.geometrySampler, planner.naturalSampler)
|
||||
);
|
||||
footprints.sharedBasis = shared.basis();
|
||||
footprints.regionalNetwork = context.footprintCompiler().regionalNetwork;
|
||||
footprints.regionalSurfaceFootprints.putAll(context.footprintCompiler().regionalSurfaceFootprints);
|
||||
footprints.regionalDropRasters.putAll(context.footprintCompiler().regionalDropRasters);
|
||||
@@ -368,6 +376,9 @@ final class HydrologyCrossTileResolver {
|
||||
}
|
||||
}
|
||||
|
||||
private record FallbackSamples(HydrologyLandHeightCache landHeights, HydrologyBasisCache basis) {
|
||||
}
|
||||
|
||||
private record FallbackContext(
|
||||
HydrologyTileKey key,
|
||||
HydrologySampledGrid grid,
|
||||
|
||||
+60
-3
@@ -46,6 +46,7 @@ final class HydrologyFootprintCompiler {
|
||||
final Map<Long, SurfaceFootprint> regionalSurfaceFootprints;
|
||||
final Map<Long, HydrologySurfaceDropRaster> regionalDropRasters;
|
||||
HydrologyRegionalNetwork regionalNetwork;
|
||||
HydrologyBasisCache sharedBasis;
|
||||
|
||||
int fullMaterializationCount;
|
||||
final HydrologyFootprintRasterizer rasterizer;
|
||||
@@ -86,7 +87,17 @@ final class HydrologyFootprintCompiler {
|
||||
this.courseFootprints = new LinkedHashMap<>(COURSE_FOOTPRINT_CACHE_SIZE, 1F, true);
|
||||
this.validationCourseRasters = new LinkedHashMap<>(VALIDATION_RASTER_CACHE_SIZE, 1F, true);
|
||||
this.rasterStencils = new HashMap<>();
|
||||
this.surfaceCompiler = new SurfaceFootprintCompiler(settings, this::sampleTerrainBasis, this.geometrySampler);
|
||||
this.surfaceCompiler = new SurfaceFootprintCompiler(settings, new HydrologyTerrainSampler() {
|
||||
@Override
|
||||
public HydrologyTerrainSample sample(int x, int z) {
|
||||
return sampleTerrainBasis(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HydrologyTerrainSample[] sampleBatch(long[] coordinates, int count) {
|
||||
return sampleTerrainBases(coordinates, count);
|
||||
}
|
||||
}, this.geometrySampler);
|
||||
this.surfaceFootprints = new LinkedHashMap<>(COURSE_FOOTPRINT_CACHE_SIZE, 1F, true);
|
||||
this.regionalSurfaceFootprints = new LinkedHashMap<>();
|
||||
this.regionalDropRasters = new LinkedHashMap<>();
|
||||
@@ -101,6 +112,14 @@ final class HydrologyFootprintCompiler {
|
||||
}
|
||||
|
||||
SurfaceFootprint surfaceFootprint(RiverCourse course) {
|
||||
return surfaceFootprint(course, false);
|
||||
}
|
||||
|
||||
SurfaceFootprint surfaceFootprintForPublication(RiverCourse course) {
|
||||
return surfaceFootprint(course, true);
|
||||
}
|
||||
|
||||
private SurfaceFootprint surfaceFootprint(RiverCourse course, boolean publication) {
|
||||
SurfaceFootprint regional = regionalSurfaceFootprints.get(course.id());
|
||||
if (regional != null) {
|
||||
return regional;
|
||||
@@ -115,7 +134,10 @@ final class HydrologyFootprintCompiler {
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
SurfaceFootprint compiled = surfaceCompiler.compile(course);
|
||||
SurfaceFootprint compiled = publication ? surfaceCompiler.compileForPublication(course) : surfaceCompiler.compile(course);
|
||||
if (publication && !compiled.accepted()) {
|
||||
return compiled;
|
||||
}
|
||||
surfaceFootprints.put(rasterKey, compiled);
|
||||
if (surfaceFootprints.size() > COURSE_FOOTPRINT_CACHE_SIZE) {
|
||||
surfaceFootprints.remove(surfaceFootprints.sequencedKeySet().getFirst());
|
||||
@@ -906,13 +928,48 @@ final class HydrologyFootprintCompiler {
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
HydrologyTerrainSample sampled = naturalSampler.sampleBasisWithoutSlope(x, z);
|
||||
HydrologyTerrainSample sampled = sharedBasis == null ? naturalSampler.sampleBasisWithoutSlope(x, z)
|
||||
: sharedBasis.sampleWithoutSlope(x, z, naturalSampler);
|
||||
if (sampled != null) {
|
||||
terrainBases.put(packed, sampled);
|
||||
}
|
||||
return sampled;
|
||||
}
|
||||
|
||||
private HydrologyTerrainSample[] sampleTerrainBases(long[] coordinates, int count) {
|
||||
HydrologyTerrainSample[] samples = new HydrologyTerrainSample[count];
|
||||
long[] missing = new long[count];
|
||||
int[] positions = new int[count];
|
||||
int missingCount = 0;
|
||||
for (int index = 0; index < count; index++) {
|
||||
long key = coordinates[index];
|
||||
HydrologyTerrainSample cached = terrainBases.get(key);
|
||||
if (cached != null) {
|
||||
samples[index] = cached;
|
||||
} else if (sharedBasis != null && sharedBasis.findWithoutSlope(key, samples, index)) {
|
||||
continue;
|
||||
} else if (naturalSampler == null) {
|
||||
samples[index] = sampleTerrainBasis(RiverFootprint.unpackX(key), RiverFootprint.unpackZ(key));
|
||||
} else {
|
||||
missing[missingCount] = key;
|
||||
positions[missingCount++] = index;
|
||||
}
|
||||
}
|
||||
if (missingCount > 0) {
|
||||
HydrologyTerrainSample[] loaded = naturalSampler.sampleBasisWithoutSlopeBatch(missing, missingCount);
|
||||
for (int index = 0; index < missingCount; index++) {
|
||||
samples[positions[index]] = sharedBasis == null ? loaded[index]
|
||||
: sharedBasis.rememberWithoutSlope(missing[index], loaded[index]);
|
||||
}
|
||||
}
|
||||
for (int index = 0; index < count; index++) {
|
||||
if (samples[index] != null) {
|
||||
terrainBases.put(coordinates[index], samples[index]);
|
||||
}
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
|
||||
HydrologyRoutingTerrainSampler.NaturalClassification classifyNatural(int x, int z) {
|
||||
if (naturalSampler == null) {
|
||||
return HydrologyRoutingTerrainSampler.NaturalClassification.UNAVAILABLE;
|
||||
|
||||
+8
@@ -11,4 +11,12 @@ public interface HydrologyNaturalTerrainSampler extends HydrologyRoutingTerrainS
|
||||
HydrologyTerrainSample terrain = sampleBasisWithoutSlope(blockX, blockZ);
|
||||
return terrain == null || terrain.ocean() ? Double.NaN : terrain.naturalHeight();
|
||||
}
|
||||
|
||||
default HydrologyTerrainSample[] sampleBasisWithoutSlopeBatch(long[] coordinates, int count) {
|
||||
HydrologyTerrainSample[] samples = new HydrologyTerrainSample[count];
|
||||
for (int index = 0; index < count; index++) {
|
||||
samples[index] = sampleBasisWithoutSlope(RiverFootprint.unpackX(coordinates[index]), RiverFootprint.unpackZ(coordinates[index]));
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,11 @@ public final class HydrologyOceanReceiver implements HydrologyTerrainSampler {
|
||||
return source.sample(blockX, blockZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HydrologyTerrainSample[] sampleBatch(long[] coordinates, int count) {
|
||||
return source.sampleBatch(coordinates, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean receivingWater(int blockX, int blockZ, int queriedSeaLevel) {
|
||||
return queriedSeaLevel == seaLevel && receiving.contains(RiverFootprint.pack(blockX, blockZ))
|
||||
|
||||
@@ -11,6 +11,7 @@ import art.arcane.volmlib.util.noise.SimplexNoise;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2DoubleOpenHashMap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -549,7 +550,8 @@ public final class HydrologyPlanner {
|
||||
}
|
||||
HydrologyTerrainSample sampled = naturalSampler == null
|
||||
? sampler.sample(blockX, blockZ)
|
||||
: naturalSampler.sampleBasis(blockX, blockZ);
|
||||
: samples.sharedBasis == null ? naturalSampler.sampleBasis(blockX, blockZ)
|
||||
: samples.sharedBasis.sample(blockX, blockZ, naturalSampler);
|
||||
samples.basis.put(packed, sampled);
|
||||
return sampled;
|
||||
}
|
||||
@@ -576,7 +578,9 @@ public final class HydrologyPlanner {
|
||||
if (cached != null || samples.basisWithoutSlope.containsKey(packed)) {
|
||||
return cached;
|
||||
}
|
||||
HydrologyTerrainSample sampled = naturalSampler.sampleBasisWithoutSlope(blockX, blockZ);
|
||||
HydrologyTerrainSample sampled = samples.sharedBasis == null
|
||||
? naturalSampler.sampleBasisWithoutSlope(blockX, blockZ)
|
||||
: samples.sharedBasis.sampleWithoutSlope(blockX, blockZ, naturalSampler);
|
||||
samples.basisWithoutSlope.put(packed, sampled);
|
||||
return sampled;
|
||||
}
|
||||
@@ -692,6 +696,7 @@ public final class HydrologyPlanner {
|
||||
private final Long2ObjectOpenHashMap<HydrologyTerrainSample> basisWithoutSlope;
|
||||
private final Long2DoubleOpenHashMap landHeights;
|
||||
private HydrologyLandHeightCache sharedLandHeights;
|
||||
private HydrologyBasisCache sharedBasis;
|
||||
final Long2ObjectOpenHashMap<HydrologyTerrainSample> detailed;
|
||||
private final Long2ObjectOpenHashMap<HydrologyRoutingTerrainSampler.NaturalClassification> classifications;
|
||||
private final HashMap<HydrologyGeometrySampler.Request, Integer> geometry;
|
||||
@@ -704,11 +709,12 @@ public final class HydrologyPlanner {
|
||||
final HydrologyCaveContainmentPlanner.ValidationCache caveValidations;
|
||||
|
||||
PlanningSamples() {
|
||||
this(null);
|
||||
this(null, null);
|
||||
}
|
||||
|
||||
PlanningSamples(HydrologyLandHeightCache sharedLandHeights) {
|
||||
PlanningSamples(HydrologyLandHeightCache sharedLandHeights, HydrologyBasisCache sharedBasis) {
|
||||
this.sharedLandHeights = sharedLandHeights;
|
||||
this.sharedBasis = sharedBasis;
|
||||
this.basis = new Long2ObjectOpenHashMap<>();
|
||||
this.basisWithoutSlope = new Long2ObjectOpenHashMap<>();
|
||||
this.landHeights = new Long2DoubleOpenHashMap();
|
||||
@@ -725,6 +731,16 @@ public final class HydrologyPlanner {
|
||||
this.caveValidations = new HydrologyCaveContainmentPlanner.ValidationCache();
|
||||
}
|
||||
|
||||
HydrologyBasisCache fallbackBasis(Long2ObjectMap<HydrologyTerrainSample> footprintBasis) {
|
||||
if (sharedBasis == null) {
|
||||
sharedBasis = new HydrologyBasisCache();
|
||||
sharedBasis.seed(basis);
|
||||
sharedBasis.seedWithoutSlope(basisWithoutSlope);
|
||||
sharedBasis.seedWithoutSlope(footprintBasis);
|
||||
}
|
||||
return sharedBasis;
|
||||
}
|
||||
|
||||
HydrologyLandHeightCache fallbackLandHeights() {
|
||||
if (sharedLandHeights == null) {
|
||||
sharedLandHeights = new HydrologyLandHeightCache();
|
||||
|
||||
+1
-1
@@ -89,7 +89,7 @@ final class HydrologyRegionalTributaries {
|
||||
if (tributary == null || !confined(tributary)) {
|
||||
return null;
|
||||
}
|
||||
SurfaceFootprint footprint = footprints.surfaceFootprint(tributary);
|
||||
SurfaceFootprint footprint = footprints.surfaceFootprintForPublication(tributary);
|
||||
if (!footprint.accepted() || !joinsWater(tributary, footprint, receiver.course())) {
|
||||
if (!footprint.accepted()) {
|
||||
planner.tributaries.addTributaryDiagnostic(local.id(), local.segments().getFirst().start(),
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ final class HydrologySurfaceProfiles {
|
||||
if (course.type() != RiverCourseType.SURFACE) {
|
||||
continue;
|
||||
}
|
||||
SurfaceFootprint footprint = compiler.surfaceFootprint(course);
|
||||
SurfaceFootprint footprint = compiler.surfaceFootprintForPublication(course);
|
||||
if (!footprint.accepted()) {
|
||||
iterator.remove();
|
||||
rejectedOutlets.add(course.outletId().orElseThrow());
|
||||
|
||||
@@ -2,8 +2,18 @@ package art.arcane.iris.generation.hydrology;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface HydrologyTerrainSampler {
|
||||
int MAXIMUM_BATCH_SIZE = 4096;
|
||||
|
||||
HydrologyTerrainSample sample(int blockX, int blockZ);
|
||||
|
||||
default HydrologyTerrainSample[] sampleBatch(long[] coordinates, int count) {
|
||||
HydrologyTerrainSample[] samples = new HydrologyTerrainSample[count];
|
||||
for (int index = 0; index < count; index++) {
|
||||
samples[index] = sample(RiverFootprint.unpackX(coordinates[index]), RiverFootprint.unpackZ(coordinates[index]));
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
|
||||
default boolean receivingWater(int blockX, int blockZ, int seaLevel) {
|
||||
HydrologyTerrainSample terrain = sample(blockX, blockZ);
|
||||
return terrain != null && terrain.ocean() && terrain.naturalHeight() < seaLevel;
|
||||
|
||||
@@ -44,6 +44,8 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
.build();
|
||||
|
||||
private final HydrologyPlanner planner;
|
||||
private final int tileSize;
|
||||
private final int publicationRadius;
|
||||
private final int maximumEntries;
|
||||
private final Cache<HydrologyTileKey, HydrologyTile> tiles;
|
||||
private final Cache<Long, ChunkColumns> composedChunks;
|
||||
@@ -107,6 +109,8 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
SharedCacheScope sharedCacheScope
|
||||
) {
|
||||
this.planner = Objects.requireNonNull(planner, "planner");
|
||||
this.tileSize = planner.settings().routing().tileSize();
|
||||
this.publicationRadius = planner.settings().publicationRadius();
|
||||
this.prefetchExecutor = prefetchExecutor;
|
||||
this.waitingForbidden = waitingForbidden;
|
||||
this.sharedCacheScope = sharedCacheScope;
|
||||
@@ -560,8 +564,6 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
|
||||
public PregenerationScope preparePregeneration(PregenerationArea area) {
|
||||
Objects.requireNonNull(area, "area");
|
||||
int tileSize = planner.settings().routing().tileSize();
|
||||
int publicationRadius = planner.settings().publicationRadius();
|
||||
TileBounds bounds = new TileBounds(
|
||||
tileCoordinate(Math.subtractExact(area.minimumBlockX(), publicationRadius), tileSize),
|
||||
tileCoordinate(Math.subtractExact(area.minimumBlockZ(), publicationRadius), tileSize),
|
||||
@@ -590,7 +592,7 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
sharedCacheScope = Objects.requireNonNull(scope, "scope");
|
||||
persistentStore = persistentRoot == null
|
||||
? null
|
||||
: new PreparedHydrologyTileStore(persistentRoot, scope, planner.settings().routing().tileSize());
|
||||
: new PreparedHydrologyTileStore(persistentRoot, scope, tileSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -670,7 +672,7 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
return tile.key().equals(sharedKey.tileKey())
|
||||
&& tile.worldSeed() == scope.worldSeed()
|
||||
&& tile.settingsFingerprint() == scope.settingsFingerprint()
|
||||
&& tile.tileSize() == planner.settings().routing().tileSize();
|
||||
&& tile.tileSize() == tileSize;
|
||||
}
|
||||
|
||||
private ChunkColumns chunkColumns(int blockX, int blockZ) {
|
||||
@@ -765,8 +767,6 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
private ArrayList<HydrologyTileKey> relevantKeys(int chunkX, int chunkZ) {
|
||||
int minimumBlockX = Math.multiplyExact(chunkX, CHUNK_SIZE);
|
||||
int minimumBlockZ = Math.multiplyExact(chunkZ, CHUNK_SIZE);
|
||||
int tileSize = planner.settings().routing().tileSize();
|
||||
int publicationRadius = planner.settings().publicationRadius();
|
||||
int minimumTileX = tileCoordinate((long) minimumBlockX - publicationRadius, tileSize);
|
||||
int maximumTileX = tileCoordinate((long) minimumBlockX + CHUNK_SIZE - 1L + publicationRadius, tileSize);
|
||||
int minimumTileZ = tileCoordinate((long) minimumBlockZ - publicationRadius, tileSize);
|
||||
@@ -854,8 +854,6 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
|
||||
private List<HydrologyTileKey> prefetchAreaKeys(long minimumBlockX, long minimumBlockZ, long maximumBlockX,
|
||||
long maximumBlockZ, int centreBlockX, int centreBlockZ) {
|
||||
int tileSize = planner.settings().routing().tileSize();
|
||||
int publicationRadius = planner.settings().publicationRadius();
|
||||
int minimumTileX = tileCoordinate((long) minimumBlockX - publicationRadius, tileSize);
|
||||
int maximumTileX = tileCoordinate((long) maximumBlockX + publicationRadius, tileSize);
|
||||
int minimumTileZ = tileCoordinate((long) minimumBlockZ - publicationRadius, tileSize);
|
||||
@@ -1127,23 +1125,42 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
return List.of(loaded);
|
||||
}
|
||||
int batch = Math.max(1, Math.min(maximumEntries / 2, HydrologyPlanningAdmission.maximumRoots()));
|
||||
ArrayList<CompletableFuture<HydrologyTile>> futures = new ArrayList<>(Math.min(batch, loaded.length));
|
||||
for (int start = 0; start < loaded.length; start += batch) {
|
||||
int end = Math.min(loaded.length, start + batch);
|
||||
futures.clear();
|
||||
for (int index = start; index < end; index++) {
|
||||
futures.add(planDemandAsync(keys.get(index)));
|
||||
}
|
||||
for (int index = start; index < end; index++) {
|
||||
loaded[index] = awaitPlan(futures.get(index - start));
|
||||
}
|
||||
}
|
||||
loadDemandTiles(keys, loaded, batch);
|
||||
return List.of(loaded);
|
||||
} finally {
|
||||
finishDemandBatch(demandEpoch);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadDemandTiles(List<HydrologyTileKey> keys, HydrologyTile[] loaded, int maximumPending) {
|
||||
ArrayList<PendingTile> pending = new ArrayList<>(Math.min(maximumPending, loaded.length));
|
||||
int next = 0;
|
||||
while (next < loaded.length || !pending.isEmpty()) {
|
||||
while (next < loaded.length && pending.size() < maximumPending) {
|
||||
if (loaded[next] == null) {
|
||||
pending.add(new PendingTile(next, planDemandAsync(keys.get(next))));
|
||||
}
|
||||
next++;
|
||||
}
|
||||
boolean completed = false;
|
||||
for (int index = pending.size() - 1; index >= 0; index--) {
|
||||
PendingTile tile = pending.get(index);
|
||||
if (tile.future().isDone()) {
|
||||
loaded[tile.index()] = awaitPlan(tile.future());
|
||||
pending.remove(index);
|
||||
completed = true;
|
||||
}
|
||||
}
|
||||
if (!completed && !pending.isEmpty()) {
|
||||
CompletableFuture<?>[] futures = new CompletableFuture<?>[pending.size()];
|
||||
for (int index = 0; index < pending.size(); index++) {
|
||||
futures[index] = pending.get(index).future();
|
||||
}
|
||||
awaitPlan(CompletableFuture.anyOf(futures));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private long beginDemandBatch() {
|
||||
synchronized (prefetchQueue) {
|
||||
demandBatches++;
|
||||
@@ -1283,10 +1300,8 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
List<HydrologyTileKey> relevantKeys,
|
||||
List<HydrologyTile> relevantTiles
|
||||
) {
|
||||
ArrayList<HydrologyColumnLayer> layers = new ArrayList<>();
|
||||
ArrayList<HydrologyColumnLayer> layers = null;
|
||||
HydrologyColumnSample template = null;
|
||||
int tileSize = planner.settings().routing().tileSize();
|
||||
int publicationRadius = planner.settings().publicationRadius();
|
||||
int minimumTileX = tileCoordinate((long) blockX - publicationRadius, tileSize);
|
||||
int maximumTileX = tileCoordinate((long) blockX + publicationRadius, tileSize);
|
||||
int minimumTileZ = tileCoordinate((long) blockZ - publicationRadius, tileSize);
|
||||
@@ -1307,19 +1322,28 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
template = sample;
|
||||
} else {
|
||||
requireMatchingTerrain(template, sample);
|
||||
if (layers == null) {
|
||||
layers = new ArrayList<>(template.layers());
|
||||
}
|
||||
layers.addAll(sample.layers());
|
||||
}
|
||||
}
|
||||
if (template == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
if (layers == null && template.layers().size() < 2) {
|
||||
return Optional.of(template);
|
||||
}
|
||||
LinkedHashMap<Long, HydrologyColumnLayer> unique = new LinkedHashMap<>();
|
||||
for (HydrologyColumnLayer layer : layers) {
|
||||
for (HydrologyColumnLayer layer : layers == null ? template.layers() : layers) {
|
||||
HydrologyColumnLayer existing = unique.putIfAbsent(layer.feature().id(), layer);
|
||||
if (existing != null && !existing.equals(layer)) {
|
||||
throw new IllegalStateException("Hydrology feature id collision at " + blockX + "," + blockZ + ".");
|
||||
}
|
||||
}
|
||||
if (layers == null && unique.size() == template.layers().size()) {
|
||||
return Optional.of(template);
|
||||
}
|
||||
return Optional.of(new HydrologyColumnSample(
|
||||
blockX,
|
||||
blockZ,
|
||||
@@ -1384,6 +1408,9 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
private record PendingTile(int index, CompletableFuture<HydrologyTile> future) {
|
||||
}
|
||||
|
||||
private record TileBounds(int minimumX, int minimumZ, int maximumX, int maximumZ) {
|
||||
private boolean contains(HydrologyTileKey key) {
|
||||
return key.tileX() >= minimumX && key.tileX() <= maximumX
|
||||
|
||||
+57
-3
@@ -5,6 +5,9 @@ import art.arcane.volmlib.util.cache.CacheKey;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyNaturalTerrainSampler;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyRoutingTerrainSampler;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTerrainSample;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTerrainSampler;
|
||||
import art.arcane.iris.generation.hydrology.RiverFootprint;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeGenerationScope;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyForkJoin;
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import it.unimi.dsi.fastutil.longs.Long2DoubleLinkedOpenHashMap;
|
||||
@@ -15,6 +18,8 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrainSampler, AutoCloseable {
|
||||
private static final int MINIMUM_PARALLEL_BASIS_SAMPLES = 64;
|
||||
@@ -130,6 +135,44 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
||||
return basis(blockX, blockZ).terrain();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HydrologyTerrainSample[] sampleBasisWithoutSlopeBatch(long[] coordinates, int count) {
|
||||
Objects.checkFromIndexSize(0, count, coordinates.length);
|
||||
if (count > HydrologyTerrainSampler.MAXIMUM_BATCH_SIZE) {
|
||||
throw new IllegalArgumentException("Terrain basis batch exceeds its sample limit");
|
||||
}
|
||||
int workers = Math.min(Math.min(samplingOptions.maximumWorkers(), Runtime.getRuntime().availableProcessors()),
|
||||
Math.ceilDiv(count, MINIMUM_PARALLEL_BASIS_SAMPLES));
|
||||
Supplier<NativeGenerationScope> scope = workers > 1 ? samplingOptions.batchScopes().capture() : null;
|
||||
if (scope == null) {
|
||||
return HydrologyNaturalTerrainSampler.super.sampleBasisWithoutSlopeBatch(coordinates, count);
|
||||
}
|
||||
HydrologyTerrainSample[] samples = new HydrologyTerrainSample[count];
|
||||
int perWorker = Math.ceilDiv(count, workers);
|
||||
ArrayList<Callable<Void>> tasks = new ArrayList<>(workers);
|
||||
for (int worker = 0; worker < workers; worker++) {
|
||||
int start = worker * perWorker;
|
||||
int end = Math.min(count, start + perWorker);
|
||||
if (start >= end) {
|
||||
break;
|
||||
}
|
||||
tasks.add(() -> {
|
||||
try (NativeGenerationScope ignored = scope.get()) {
|
||||
for (int index = start; index < end; index++) {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
throw new CancellationException("Terrain basis sampling interrupted");
|
||||
}
|
||||
samples[index] = sampleBasisWithoutSlope(RiverFootprint.unpackX(coordinates[index]),
|
||||
RiverFootprint.unpackZ(coordinates[index]));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
HydrologyForkJoin.invokeAll(tasks, samplingOptions.executor());
|
||||
return samples;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double sampleLandHeight(int blockX, int blockZ) {
|
||||
long packed = pack(blockX, blockZ);
|
||||
@@ -415,8 +458,13 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
||||
record SamplingOptions(
|
||||
int maximumEntries,
|
||||
int maximumWorkers,
|
||||
Executor executor
|
||||
Executor executor,
|
||||
BatchScopes batchScopes
|
||||
) {
|
||||
SamplingOptions(int maximumEntries, int maximumWorkers, Executor executor) {
|
||||
this(maximumEntries, maximumWorkers, executor, () -> () -> () -> {});
|
||||
}
|
||||
|
||||
SamplingOptions {
|
||||
if (maximumEntries < 1) {
|
||||
throw new IllegalArgumentException("maximumEntries must be positive");
|
||||
@@ -425,16 +473,18 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
||||
throw new IllegalArgumentException("maximumWorkers must be positive");
|
||||
}
|
||||
Objects.requireNonNull(executor, "executor");
|
||||
Objects.requireNonNull(batchScopes, "batchScopes");
|
||||
}
|
||||
|
||||
static SamplingOptions production(int maximumEntries) {
|
||||
static SamplingOptions production(int maximumEntries, BatchScopes batchScopes) {
|
||||
int configuredWorkers = IrisSettings.getThreadCount(
|
||||
IrisSettings.get().getConcurrency().getParallelism()
|
||||
);
|
||||
return new SamplingOptions(
|
||||
maximumEntries,
|
||||
Math.max(1, configuredWorkers),
|
||||
MultiBurst.burst
|
||||
MultiBurst.burst,
|
||||
batchScopes
|
||||
);
|
||||
}
|
||||
|
||||
@@ -443,6 +493,10 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
||||
}
|
||||
}
|
||||
|
||||
interface BatchScopes {
|
||||
Supplier<NativeGenerationScope> capture();
|
||||
}
|
||||
|
||||
record TerrainBasis(double naturalHeight, HydrologyTerrainSample terrain) {
|
||||
TerrainBasis {
|
||||
if (!Double.isFinite(naturalHeight)) {
|
||||
|
||||
+36
-1
@@ -1,6 +1,9 @@
|
||||
package art.arcane.iris.generation.hydrology.runtime;
|
||||
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.generation.context.IrisContext;
|
||||
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeGenerationScope;
|
||||
import art.arcane.iris.pack.loading.ResourceLoader;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
||||
@@ -53,6 +56,7 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.IntConsumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
private static final int MAXIMUM_CACHE_TILES = 64;
|
||||
@@ -102,7 +106,7 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
);
|
||||
this.routingTerrainSampler = new IrisHydrologyRoutingTerrainSampler(
|
||||
terrainSources,
|
||||
IrisHydrologyRoutingTerrainSampler.SamplingOptions.production(MAXIMUM_TERRAIN_SAMPLES)
|
||||
IrisHydrologyRoutingTerrainSampler.SamplingOptions.production(MAXIMUM_TERRAIN_SAMPLES, this::captureBasisScope)
|
||||
);
|
||||
HydrologyPlanner planner = new HydrologyPlanner(
|
||||
context.seed(),
|
||||
@@ -532,6 +536,37 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
+ " (raw=" + rawNaturalHeight + ", sampled=" + sampledNaturalHeight + "; " + breakdown + ")";
|
||||
}
|
||||
|
||||
private Supplier<NativeGenerationScope> captureBasisScope() {
|
||||
if (!(context.data().getEngine() instanceof IrisEngine engine)) {
|
||||
return null;
|
||||
}
|
||||
IrisEngine.GenerationRuntimeBinding binding = engine.captureGenerationRuntimeBinding();
|
||||
try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope(binding)) {
|
||||
if (engine.getComplex().getHydrologyRuntime() != this) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
IrisContext current = IrisContext.get();
|
||||
long session = current != null && current.getEngine() == engine ? current.getGenerationSessionId() : 0L;
|
||||
return () -> {
|
||||
IrisEngine.GenerationRuntimeScope runtime = engine.openGenerationRuntimeScope(binding);
|
||||
IrisContext.Scope execution;
|
||||
try {
|
||||
execution = IrisContext.open(engine, session, null);
|
||||
} catch (RuntimeException | Error failure) {
|
||||
runtime.close();
|
||||
throw failure;
|
||||
}
|
||||
return () -> {
|
||||
try {
|
||||
execution.close();
|
||||
} finally {
|
||||
runtime.close();
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
private IrisHydrologyRoutingTerrainSampler.TerrainBasis createTerrainBasis(
|
||||
int x,
|
||||
int z,
|
||||
|
||||
+118
-29
@@ -13,6 +13,7 @@ import it.unimi.dsi.fastutil.longs.Long2DoubleOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.LongArrayList;
|
||||
import it.unimi.dsi.fastutil.longs.LongIterator;
|
||||
import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
|
||||
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||
|
||||
@@ -105,9 +106,33 @@ public final class ErosionFieldCompiler {
|
||||
return new PreparedField(count, basin, bendOffsets, blendWidth, radii, inletStart);
|
||||
}
|
||||
|
||||
ErosionField compileForPublication(long courseSeed, SurfaceCenterline centerline, ChannelProfile channel,
|
||||
ValleyProfile valley, SurfaceTerminal terminal, int apronLimit,
|
||||
HydrologyPlannerSettings.Ponds ponds, SurfaceRasterContext context,
|
||||
PreparedField prepared) {
|
||||
RasterGeometry geometry = new RasterGeometry();
|
||||
if (context.bounds() == null) {
|
||||
ErosionField wet = compile(courseSeed, centerline, channel, valley, terminal, apronLimit,
|
||||
ponds, context, prepared, geometry, true);
|
||||
if (wet.rejection() != null) {
|
||||
return wet;
|
||||
}
|
||||
}
|
||||
return compile(courseSeed, centerline, channel, valley, terminal, apronLimit,
|
||||
ponds, context, prepared, geometry, false);
|
||||
}
|
||||
|
||||
ErosionField compile(long courseSeed, SurfaceCenterline centerline, ChannelProfile channel, ValleyProfile valley,
|
||||
SurfaceTerminal terminal, int apronLimit, HydrologyPlannerSettings.Ponds ponds,
|
||||
SurfaceRasterContext context, PreparedField prepared) {
|
||||
return compile(courseSeed, centerline, channel, valley, terminal, apronLimit,
|
||||
ponds, context, prepared, new RasterGeometry(), false);
|
||||
}
|
||||
|
||||
private ErosionField compile(long courseSeed, SurfaceCenterline centerline, ChannelProfile channel,
|
||||
ValleyProfile valley, SurfaceTerminal terminal, int apronLimit,
|
||||
HydrologyPlannerSettings.Ponds ponds, SurfaceRasterContext context,
|
||||
PreparedField prepared, RasterGeometry geometry, boolean wetOnly) {
|
||||
Objects.requireNonNull(context, "context");
|
||||
SurfaceBounds bounds = context.bounds();
|
||||
SurfaceBounds samplingBounds = bounds == null ? null : bounds.expand(2);
|
||||
@@ -124,9 +149,9 @@ public final class ErosionFieldCompiler {
|
||||
boolean[] basin = prepared.basin();
|
||||
double[] bendOffsets = prepared.bendOffsets();
|
||||
double[][] blendWidth = prepared.blendWidth();
|
||||
Long2IntOpenHashMap nearest = new Long2IntOpenHashMap();
|
||||
nearest.defaultReturnValue(-1);
|
||||
Long2DoubleOpenHashMap distance = new Long2DoubleOpenHashMap();
|
||||
Long2IntOpenHashMap nearest = geometry.nearest;
|
||||
Long2DoubleOpenHashMap distance = geometry.distance;
|
||||
if (!geometry.prepared) {
|
||||
for (int station = 0; station < count; station++) {
|
||||
int radius = prepared.radii()[station];
|
||||
int stationX = centerline.x()[station];
|
||||
@@ -142,7 +167,7 @@ public final class ErosionFieldCompiler {
|
||||
for (int deltaX = minimumX; deltaX <= maximumX; deltaX++) {
|
||||
int cellX = stationX + deltaX;
|
||||
int cellZ = stationZ + deltaZ;
|
||||
double cellDistance = centerline.distanceToSegment(station, cellX, cellZ);
|
||||
double cellDistance = centerline.distanceToSegment(station, cellX, cellZ, SurfaceDistanceTable.SHARED);
|
||||
if (cellDistance > radius) {
|
||||
continue;
|
||||
}
|
||||
@@ -159,11 +184,20 @@ public final class ErosionFieldCompiler {
|
||||
}
|
||||
}
|
||||
}
|
||||
geometry.prepared = true;
|
||||
}
|
||||
int oceanStart = oceanStart(centerline, valley, terminal);
|
||||
int inletStart = prepared.inletStart();
|
||||
Long2ObjectOpenHashMap<SurfaceColumn> columns = new Long2ObjectOpenHashMap<>(nearest.size());
|
||||
Long2ObjectOpenHashMap<SurfaceColumn> columns = new Long2ObjectOpenHashMap<>(wetOnly
|
||||
? Math.min(nearest.size(), Math.max(16, count * 8)) : nearest.size());
|
||||
LongArrayList wetKeys = new LongArrayList();
|
||||
for (long key : nearest.keySet()) {
|
||||
LongIterator candidates = nearest.keySet().iterator();
|
||||
long[] coordinates = new long[Math.min(nearest.size(), HydrologyTerrainSampler.MAXIMUM_BATCH_SIZE)];
|
||||
double[] outlines = new double[coordinates.length];
|
||||
while (candidates.hasNext()) {
|
||||
int pageSize = 0;
|
||||
while (candidates.hasNext() && pageSize < coordinates.length) {
|
||||
long key = candidates.nextLong();
|
||||
int station = nearest.get(key);
|
||||
double cellDistance = distance.get(key);
|
||||
int cellX = RiverFootprint.unpackX(key);
|
||||
@@ -180,12 +214,32 @@ public final class ErosionFieldCompiler {
|
||||
Math.min(channelSettings.outlineMaximumRatio() * halfWidth, outline));
|
||||
boolean wet = cellDistance <= outline + 0.25D;
|
||||
double bankDistance = cellDistance - outline;
|
||||
if (wetOnly && !wet) {
|
||||
continue;
|
||||
}
|
||||
if (!wet && cellDistance > outline + maximumShoreWidth + 0.25D
|
||||
&& (bankDistance > erosion.excavation().maximumWidth() || !erosion.enabled()
|
||||
|| bankDistance - maximumShoreWidth >= blendWidth[station][side(centerline, station, cellX, cellZ) < 0D ? 0 : 1])) {
|
||||
continue;
|
||||
}
|
||||
HydrologyTerrainSample terrain = sampler.sample(cellX, cellZ);
|
||||
coordinates[pageSize] = key;
|
||||
outlines[pageSize++] = outline;
|
||||
}
|
||||
if (pageSize == 0) {
|
||||
continue;
|
||||
}
|
||||
HydrologyTerrainSample[] samples = sampler.sampleBatch(coordinates, pageSize);
|
||||
for (int index = 0; index < pageSize; index++) {
|
||||
long key = coordinates[index];
|
||||
int station = nearest.get(key);
|
||||
double cellDistance = distance.get(key);
|
||||
int cellX = RiverFootprint.unpackX(key);
|
||||
int cellZ = RiverFootprint.unpackZ(key);
|
||||
int head = valley.head()[station];
|
||||
double outline = outlines[index];
|
||||
boolean wet = cellDistance <= outline + 0.25D;
|
||||
double bankDistance = cellDistance - outline;
|
||||
HydrologyTerrainSample terrain = samples[index];
|
||||
boolean mouthLand = terminal == SurfaceTerminal.OCEAN_MOUTH && head == seaLevel
|
||||
&& SurfaceCellAdmission.mouthLand(terrain, seaLevel);
|
||||
if (!SurfaceCellAdmission.writable(terrain, seaLevel) && !mouthLand) {
|
||||
@@ -260,11 +314,25 @@ public final class ErosionFieldCompiler {
|
||||
height = boundedBankHeight(terrain, height, erosion.excavation());
|
||||
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, dryRole, height, height, false));
|
||||
}
|
||||
if (count > 0) {
|
||||
pond(columns, wetKeys, courseSeed ^ SOURCE_POND_SALT, ponds.source(), centerline, 0, valley.head()[0], samplingContext);
|
||||
if (terminal == SurfaceTerminal.SINKHOLE) {
|
||||
pond(columns, wetKeys, courseSeed ^ TERMINAL_POND_SALT, ponds.terminal(), centerline, count - 1, valley.head()[count - 1], samplingContext);
|
||||
}
|
||||
if (count > 0) {
|
||||
pond(columns, wetKeys, courseSeed ^ SOURCE_POND_SALT, ponds.source(), centerline, 0, valley.head()[0], samplingContext, wetOnly);
|
||||
if (terminal == SurfaceTerminal.SINKHOLE) {
|
||||
pond(columns, wetKeys, courseSeed ^ TERMINAL_POND_SALT, ponds.terminal(), centerline, count - 1, valley.head()[count - 1], samplingContext, wetOnly);
|
||||
}
|
||||
}
|
||||
if (wetOnly) {
|
||||
connectSteps(columns, wetKeys);
|
||||
for (SurfaceColumn column : columns.values()) {
|
||||
if (!column.apron()) {
|
||||
int cut = unsupportedIncisionCut(column, inletStart);
|
||||
if (cut > 0) {
|
||||
return new ErosionField(columns, 0,
|
||||
HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, cut, 0L);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new ErosionField(columns, 0, null, 0, 0L);
|
||||
}
|
||||
int uncontained = contain(columns, wetKeys, sink, bounds, context.drops());
|
||||
connectSteps(columns, wetKeys);
|
||||
@@ -286,7 +354,8 @@ public final class ErosionFieldCompiler {
|
||||
SurfaceCenterline centerline,
|
||||
int station,
|
||||
int head,
|
||||
SurfaceRasterContext context
|
||||
SurfaceRasterContext context,
|
||||
boolean wetOnly
|
||||
) {
|
||||
SurfaceBounds bounds = context.bounds();
|
||||
if (!pond.enabled()) {
|
||||
@@ -315,6 +384,8 @@ public final class ErosionFieldCompiler {
|
||||
}
|
||||
int bankTop = head + sink;
|
||||
int shoreTop = bankTop + (int) StrictMath.round(shoreRise);
|
||||
double maximumBlendWidth = Math.min(banks.maximumBlendWidth(),
|
||||
Math.max(0.25D, erosion.excavation().maximumWidth() - shore));
|
||||
int reach = (int) StrictMath.ceil(radius * (1D + roughness) + shore + banks.maximumBlendWidth()) + 1;
|
||||
for (int deltaZ = -reach; deltaZ <= reach; deltaZ++) {
|
||||
for (int deltaX = -reach; deltaX <= reach; deltaX++) {
|
||||
@@ -329,11 +400,6 @@ public final class ErosionFieldCompiler {
|
||||
|| context.drops().connects(cellX, cellZ, head)) {
|
||||
continue;
|
||||
}
|
||||
HydrologyTerrainSample terrain = sampler.sample(cellX, cellZ);
|
||||
if (!SurfaceCellAdmission.writable(terrain, seaLevel)) {
|
||||
continue;
|
||||
}
|
||||
int natural = terrain.naturalHeight();
|
||||
long key = RiverFootprint.pack(cellX, cellZ);
|
||||
SurfaceColumn existing = columns.get(key);
|
||||
if (existing != null && existing.apron()) {
|
||||
@@ -344,7 +410,22 @@ public final class ErosionFieldCompiler {
|
||||
outline = Math.max(
|
||||
Math.max(0.5D, channelSettings.outlineMinimumRatio() * radius),
|
||||
Math.min(channelSettings.outlineMaximumRatio() * radius, outline));
|
||||
if (distance <= outline + 0.25D) {
|
||||
boolean wet = distance <= outline + 0.25D;
|
||||
if (wetOnly && !wet) {
|
||||
continue;
|
||||
}
|
||||
if (!wet && (existing != null && existing.role() == SurfaceRole.CHANNEL
|
||||
|| distance - outline > erosion.excavation().maximumWidth()
|
||||
|| distance > outline + shore + 0.25D && (!erosion.enabled() || !erode
|
||||
|| (distance - outline - shore) / maximumBlendWidth >= 1D))) {
|
||||
continue;
|
||||
}
|
||||
HydrologyTerrainSample terrain = sampler.sample(cellX, cellZ);
|
||||
if (!SurfaceCellAdmission.writable(terrain, seaLevel)) {
|
||||
continue;
|
||||
}
|
||||
int natural = terrain.naturalHeight();
|
||||
if (wet) {
|
||||
double normalized = Math.min(1D, distance / Math.max(0.5D, outline));
|
||||
double local = 1D + (pond.depth() - 1D) * bedFactor(normalized, erosion)
|
||||
+ erosion.bedNoise() * roughness * SurfaceNoise.signed(seed ^ BED_SALT, cellX, cellZ, banks.roughnessWavelength());
|
||||
@@ -363,12 +444,6 @@ public final class ErosionFieldCompiler {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (existing != null && existing.role() == SurfaceRole.CHANNEL) {
|
||||
continue;
|
||||
}
|
||||
if (distance - outline > erosion.excavation().maximumWidth()) {
|
||||
continue;
|
||||
}
|
||||
if (distance <= outline + shore + 0.25D) {
|
||||
int height = bankTop + (int) StrictMath.round(shoreRise * benchProgress(distance, outline, shore));
|
||||
height = boundedBankHeight(terrain, height, erosion.excavation());
|
||||
@@ -376,7 +451,7 @@ public final class ErosionFieldCompiler {
|
||||
continue;
|
||||
}
|
||||
int cut = natural - shoreTop;
|
||||
if (cut == 0 || !erosion.enabled() || !erode) {
|
||||
if (cut == 0) {
|
||||
continue;
|
||||
}
|
||||
double width = Math.max(banks.minimumBlendWidth(),
|
||||
@@ -624,6 +699,12 @@ public final class ErosionFieldCompiler {
|
||||
return Math.min(incision, (int) StrictMath.floor(incision * terrain.incisionMultiplier()));
|
||||
}
|
||||
|
||||
private int unsupportedIncisionCut(SurfaceColumn column, int inletStart) {
|
||||
int incision = maximumIncision(column.terrain(), column.station() >= inletStart);
|
||||
int cut = column.terrain().naturalHeight() - column.height();
|
||||
return cut > incision ? cut : 0;
|
||||
}
|
||||
|
||||
private ErosionField validate(Long2ObjectOpenHashMap<SurfaceColumn> columns, LongArrayList wetKeys,
|
||||
SurfaceCenterline centerline, SurfaceTerminal terminal,
|
||||
SurfaceBounds bounds, int uncontained, int inletStart) {
|
||||
@@ -635,10 +716,8 @@ public final class ErosionFieldCompiler {
|
||||
continue;
|
||||
}
|
||||
if (column.role() == SurfaceRole.CHANNEL) {
|
||||
HydrologyTerrainSample terrain = column.terrain();
|
||||
int incision = maximumIncision(terrain, column.station() >= inletStart);
|
||||
int cut = terrain.naturalHeight() - column.height();
|
||||
if (cut > incision) {
|
||||
int cut = unsupportedIncisionCut(column, inletStart);
|
||||
if (cut > 0) {
|
||||
return new ErosionField(columns, uncontained,
|
||||
HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED,
|
||||
cut, excavation);
|
||||
@@ -818,6 +897,16 @@ public final class ErosionFieldCompiler {
|
||||
return deltaX * centerline.normalX(station) + deltaZ * centerline.normalZ(station);
|
||||
}
|
||||
|
||||
private static final class RasterGeometry {
|
||||
private final Long2IntOpenHashMap nearest = new Long2IntOpenHashMap();
|
||||
private final Long2DoubleOpenHashMap distance = new Long2DoubleOpenHashMap();
|
||||
private boolean prepared;
|
||||
|
||||
private RasterGeometry() {
|
||||
nearest.defaultReturnValue(-1);
|
||||
}
|
||||
}
|
||||
|
||||
record PreparedField(int count, boolean[] basin, double[] bendOffsets, double[][] blendWidth, int[] radii,
|
||||
int inletStart) {
|
||||
}
|
||||
|
||||
+8
-2
@@ -75,10 +75,15 @@ public record SurfaceCenterline(int[] x, int[] z, double[] tangentX, double[] ta
|
||||
}
|
||||
|
||||
public double distanceToSegment(int station, double pointX, double pointZ) {
|
||||
return distanceToSegment(station, pointX, pointZ, null);
|
||||
}
|
||||
|
||||
double distanceToSegment(int station, double pointX, double pointZ, SurfaceDistanceTable distances) {
|
||||
double startX = x[station];
|
||||
double startZ = z[station];
|
||||
if (station + 1 >= x.length) {
|
||||
return StrictMath.hypot(pointX - startX, pointZ - startZ);
|
||||
return distances == null ? StrictMath.hypot(pointX - startX, pointZ - startZ)
|
||||
: distances.hypot(pointX - startX, pointZ - startZ);
|
||||
}
|
||||
double endX = x[station + 1];
|
||||
double endZ = z[station + 1];
|
||||
@@ -91,7 +96,8 @@ public record SurfaceCenterline(int[] x, int[] z, double[] tangentX, double[] ta
|
||||
progress = Math.max(0D, Math.min(1D, progress));
|
||||
double closestX = startX + segmentX * progress;
|
||||
double closestZ = startZ + segmentZ * progress;
|
||||
return StrictMath.hypot(pointX - closestX, pointZ - closestZ);
|
||||
return distances == null ? StrictMath.hypot(pointX - closestX, pointZ - closestZ)
|
||||
: distances.hypot(pointX - closestX, pointZ - closestZ);
|
||||
}
|
||||
|
||||
public SurfaceCenterline truncate(int stations) {
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package art.arcane.iris.generation.hydrology.surface;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.DoubleBinaryOperator;
|
||||
|
||||
final class SurfaceDistanceTable {
|
||||
static final SurfaceDistanceTable SHARED = new SurfaceDistanceTable(256, StrictMath::hypot);
|
||||
|
||||
private final int maximumResidual;
|
||||
private final int width;
|
||||
private final double[] integers;
|
||||
private final double[] diagonals;
|
||||
private final DoubleBinaryOperator calculator;
|
||||
|
||||
SurfaceDistanceTable(int maximumResidual, DoubleBinaryOperator calculator) {
|
||||
if (maximumResidual < 0 || maximumResidual > 256) {
|
||||
throw new IllegalArgumentException("Surface distance table residual must be between zero and 256.");
|
||||
}
|
||||
this.maximumResidual = maximumResidual;
|
||||
this.width = maximumResidual + 1;
|
||||
this.calculator = Objects.requireNonNull(calculator, "calculator");
|
||||
this.integers = new double[width * width];
|
||||
this.diagonals = new double[maximumResidual];
|
||||
for (int x = 0; x <= maximumResidual; x++) {
|
||||
for (int z = 0; z <= maximumResidual; z++) {
|
||||
integers[x * width + z] = calculator.applyAsDouble(x, z);
|
||||
}
|
||||
}
|
||||
for (int offset = 0; offset < maximumResidual; offset++) {
|
||||
diagonals[offset] = calculator.applyAsDouble(offset + 0.5D, offset + 0.5D);
|
||||
}
|
||||
}
|
||||
|
||||
double hypot(double x, double z) {
|
||||
double positiveX = Math.abs(x);
|
||||
double positiveZ = Math.abs(z);
|
||||
int integerX = (int) positiveX;
|
||||
int integerZ = (int) positiveZ;
|
||||
if (integerX <= maximumResidual && integerZ <= maximumResidual
|
||||
&& positiveX == integerX && positiveZ == integerZ) {
|
||||
return integers[integerX * width + integerZ];
|
||||
}
|
||||
if (positiveX == positiveZ && positiveX < maximumResidual && positiveX == integerX + 0.5D) {
|
||||
return diagonals[integerX];
|
||||
}
|
||||
return calculator.applyAsDouble(x, z);
|
||||
}
|
||||
}
|
||||
+17
-5
@@ -54,11 +54,19 @@ public final class SurfaceFootprintCompiler {
|
||||
return compile(prepare(course), bounds);
|
||||
}
|
||||
|
||||
public SurfaceFootprint compileForPublication(RiverCourse course) {
|
||||
return compile(prepare(course), null, true);
|
||||
}
|
||||
|
||||
public PreparedCourse prepare(RiverCourse course) {
|
||||
return new PreparedCourse(this, Objects.requireNonNull(course, "course"));
|
||||
}
|
||||
|
||||
public SurfaceFootprint compile(PreparedCourse prepared, SurfaceBounds bounds) {
|
||||
return compile(prepared, bounds, false);
|
||||
}
|
||||
|
||||
private SurfaceFootprint compile(PreparedCourse prepared, SurfaceBounds bounds, boolean publication) {
|
||||
if (prepared.compiler != this) {
|
||||
throw new IllegalArgumentException("Prepared course belongs to another surface compiler");
|
||||
}
|
||||
@@ -89,7 +97,7 @@ public final class SurfaceFootprintCompiler {
|
||||
while (after < course.segments().size() && exposedSegment(course.segments().get(after))) {
|
||||
after++;
|
||||
}
|
||||
SurfaceFootprint run = compileRun(prepared, first, after, prepared.inlet.offset(first), bounds, drops, pool, receivingSampler);
|
||||
SurfaceFootprint run = compileRun(prepared, first, after, prepared.inlet.offset(first), bounds, drops, pool, receivingSampler, publication);
|
||||
columns.addAll(run.columns());
|
||||
uncontained += run.uncontainedWetCells();
|
||||
excavation += run.bankExcavation();
|
||||
@@ -147,7 +155,7 @@ public final class SurfaceFootprintCompiler {
|
||||
|
||||
private SurfaceFootprint compileRun(PreparedCourse prepared, int first, int after, int stationOffset,
|
||||
SurfaceBounds bounds, HydrologySurfaceDropRaster drops, boolean pool,
|
||||
HydrologyTerrainSampler receivingSampler) {
|
||||
HydrologyTerrainSampler receivingSampler, boolean publication) {
|
||||
RiverCourse course = prepared.course;
|
||||
PreparedRun run = prepared.runs[first];
|
||||
if (run == null) {
|
||||
@@ -159,9 +167,13 @@ public final class SurfaceFootprintCompiler {
|
||||
SurfaceCenterline centerline = run.centerline();
|
||||
boolean coastalChannel = run.coastalChannel();
|
||||
String poolBiome = run.poolBiome();
|
||||
ErosionField field = run.erosion().compile(
|
||||
HydrologyHash.mix(course.id(), COURSE_SEED_SALT), centerline, run.channel(), run.valley(), run.terminal(),
|
||||
settings.outlets().maximumOceanApron(), run.ponds(), new SurfaceRasterContext(bounds, drops, run.boundary()),
|
||||
SurfaceRasterContext context = new SurfaceRasterContext(bounds, drops, run.boundary());
|
||||
long seed = HydrologyHash.mix(course.id(), COURSE_SEED_SALT);
|
||||
ErosionField field = publication ? run.erosion().compileForPublication(
|
||||
seed, centerline, run.channel(), run.valley(), run.terminal(),
|
||||
settings.outlets().maximumOceanApron(), run.ponds(), context, run.field()) : run.erosion().compile(
|
||||
seed, centerline, run.channel(), run.valley(), run.terminal(),
|
||||
settings.outlets().maximumOceanApron(), run.ponds(), context,
|
||||
run.field());
|
||||
ArrayList<SurfaceColumn> ordered = new ArrayList<>(field.columns().values());
|
||||
ordered.sort(Comparator.comparingInt(SurfaceColumn::station)
|
||||
|
||||
@@ -40,6 +40,12 @@ final class CaveCarveScratch {
|
||||
final double[] planeThresholdLimit = new double[256];
|
||||
final boolean[] planeCarve = new boolean[256];
|
||||
final double[] adaptivePlaneDensity = new double[81];
|
||||
double[] verticalDensityLower = new double[9];
|
||||
double[] verticalDensityUpper = new double[9];
|
||||
int verticalDensityLowerY = Integer.MIN_VALUE;
|
||||
int verticalDensityUpperY = Integer.MIN_VALUE;
|
||||
int verticalDensityX;
|
||||
int verticalDensityZ;
|
||||
final double[] adaptivePlanePrediction = new double[256];
|
||||
final double[] adaptivePlaneAmbiguity = new double[256];
|
||||
final int[] adaptivePlaneSampleBounds = new int[4];
|
||||
|
||||
@@ -91,6 +91,9 @@ public class IrisCaveCarver3D {
|
||||
private final int fluidHeight;
|
||||
private final int aquiferCeilingY;
|
||||
private final boolean parallelDensity;
|
||||
private final int verticalDensityStep;
|
||||
private final int worldMinimumY;
|
||||
private final int densityMaximumY;
|
||||
private final ThreadLocal<Boolean> carving = ThreadLocal.withInitial(() -> false);
|
||||
private final ThreadLocal<CaveCarveScratch> scratchCache = ThreadLocal.withInitial(CaveCarveScratch::new);
|
||||
|
||||
@@ -139,6 +142,9 @@ public class IrisCaveCarver3D {
|
||||
detailMinContribution = -detailWeight;
|
||||
detailMaxContribution = detailWeight;
|
||||
parallelDensity = fixedDensityStyles(profile);
|
||||
verticalDensityStep = parallelDensity ? Math.max(1, engine.getDimension().getCaveDensitySamplingStep()) : 1;
|
||||
worldMinimumY = engine.getWorld().minHeight();
|
||||
densityMaximumY = engine.getWorld().getHeight() - 1;
|
||||
}
|
||||
|
||||
public int carve(MantleWriter writer, int chunkX, int chunkZ) {
|
||||
@@ -1354,6 +1360,13 @@ public class IrisCaveCarver3D {
|
||||
return;
|
||||
}
|
||||
|
||||
if (verticalDensityStep > 1 && adaptiveSampleStep == ADAPTIVE_DEEP_SAMPLE_STEP) {
|
||||
prepareVerticalDensityPlane(scratch, x0, z0, y);
|
||||
classifyDeepAdaptivePlaneFromSamples(scratch, planeColumnIndices, planeThresholdLimit,
|
||||
planeCount, planeCarve, ADAPTIVE_DEEP_SAMPLE_STEP, scratch.adaptivePlaneDensity, 2, 3);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasWarp) {
|
||||
if (!hasModules) {
|
||||
classifyDensityPlaneAdaptiveNoWarpNoModules(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve, adaptiveSampleStep, adaptiveThresholdMargin);
|
||||
@@ -1372,6 +1385,65 @@ public class IrisCaveCarver3D {
|
||||
classifyDensityPlaneAdaptiveWarpModules(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve, adaptiveSampleStep, adaptiveThresholdMargin);
|
||||
}
|
||||
|
||||
private void prepareVerticalDensityPlane(CaveCarveScratch scratch, int x0, int z0, int y) {
|
||||
int lower = (int) (Math.floorDiv((long) y + worldMinimumY, verticalDensityStep)
|
||||
* verticalDensityStep - worldMinimumY);
|
||||
int upper = Math.min(densityMaximumY, lower + verticalDensityStep);
|
||||
lower = Math.max(0, lower);
|
||||
for (CaveFieldModuleState module : modules) {
|
||||
if (y < module.minY) {
|
||||
upper = Math.min(upper, module.minY - 1);
|
||||
} else if (y > module.maxY) {
|
||||
lower = Math.max(lower, module.maxY + 1);
|
||||
} else {
|
||||
lower = Math.max(lower, module.minY);
|
||||
upper = Math.min(upper, module.maxY);
|
||||
}
|
||||
}
|
||||
if (scratch.verticalDensityX != x0 || scratch.verticalDensityZ != z0) {
|
||||
scratch.verticalDensityX = x0;
|
||||
scratch.verticalDensityZ = z0;
|
||||
scratch.verticalDensityLowerY = Integer.MIN_VALUE;
|
||||
scratch.verticalDensityUpperY = Integer.MIN_VALUE;
|
||||
}
|
||||
if (scratch.verticalDensityUpperY == lower) {
|
||||
double[] previousLower = scratch.verticalDensityLower;
|
||||
scratch.verticalDensityLower = scratch.verticalDensityUpper;
|
||||
scratch.verticalDensityUpper = previousLower;
|
||||
scratch.verticalDensityUpperY = scratch.verticalDensityLowerY;
|
||||
scratch.verticalDensityLowerY = lower;
|
||||
}
|
||||
if (scratch.verticalDensityLowerY != lower) {
|
||||
scratch.verticalDensityLowerY = Integer.MIN_VALUE;
|
||||
sampleVerticalDensityPlane(scratch, x0, z0, lower, scratch.verticalDensityLower);
|
||||
scratch.verticalDensityLowerY = lower;
|
||||
}
|
||||
if (y == lower) {
|
||||
System.arraycopy(scratch.verticalDensityLower, 0, scratch.adaptivePlaneDensity, 0, 9);
|
||||
return;
|
||||
}
|
||||
if (scratch.verticalDensityUpperY != upper) {
|
||||
scratch.verticalDensityUpperY = Integer.MIN_VALUE;
|
||||
sampleVerticalDensityPlane(scratch, x0, z0, upper, scratch.verticalDensityUpper);
|
||||
scratch.verticalDensityUpperY = upper;
|
||||
}
|
||||
double fraction = (y - lower) / (double) (upper - lower);
|
||||
for (int index = 0; index < 9; index++) {
|
||||
double lowerDensity = scratch.verticalDensityLower[index];
|
||||
scratch.adaptivePlaneDensity[index] = lowerDensity
|
||||
+ (scratch.verticalDensityUpper[index] - lowerDensity) * fraction;
|
||||
}
|
||||
}
|
||||
|
||||
private void sampleVerticalDensityPlane(CaveCarveScratch scratch, int x0, int z0, int y, double[] samples) {
|
||||
for (int x = 0; x < 3; x++) {
|
||||
for (int z = 0; z < 3; z++) {
|
||||
samples[x * 3 + z] = sampleDensityOptimized(scratch,
|
||||
x0 + x * ADAPTIVE_DEEP_SAMPLE_STEP, y, z0 + z * ADAPTIVE_DEEP_SAMPLE_STEP);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void classifyDensityPlaneNoWarpNoModules(int x0, int z0, int y, int[] planeColumnIndices, double[] planeThresholdLimit, int planeCount, boolean[] planeCarve) {
|
||||
for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) {
|
||||
int columnIndex = planeColumnIndices[planeIndex];
|
||||
|
||||
@@ -590,29 +590,25 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
if (matter == null) {
|
||||
continue;
|
||||
}
|
||||
MatterSlice<PreObjectMatterCell> journalSlice = matter.hasSlice(PreObjectMatterCell.class)
|
||||
? matter.getSlice(PreObjectMatterCell.class)
|
||||
: null;
|
||||
MatterSlice<MatterCavern> cavernSlice = matter.hasSlice(MatterCavern.class)
|
||||
? matter.getSlice(MatterCavern.class)
|
||||
: null;
|
||||
MatterSlice<HydrologyCaveCell> hydrologySlice = matter.hasSlice(HydrologyCaveCell.class)
|
||||
? matter.getSlice(HydrologyCaveCell.class)
|
||||
: null;
|
||||
MatterSlice<PreObjectMatterCell> journalSlice = matter.getSlice(PreObjectMatterCell.class);
|
||||
MatterSlice<MatterCavern> cavernSlice = matter.getSlice(MatterCavern.class);
|
||||
MatterSlice<HydrologyCaveCell> hydrologySlice = matter.getSlice(HydrologyCaveCell.class);
|
||||
int sectionBaseY = section << 4;
|
||||
int sectionMaxY = Math.min(cappedHeight, sectionBaseY + 16);
|
||||
for (int y = sectionBaseY; y < sectionMaxY; y++) {
|
||||
int localY = y & 15;
|
||||
HydrologyCaveCell hydrology = hydrologySlice == null
|
||||
PreObjectMatterCell cell = journalSlice == null
|
||||
? null
|
||||
: journalSlice.get(x & 15, localY, z & 15);
|
||||
HydrologyCaveCell hydrology = cell != null && cell.hydrologyCaptured()
|
||||
? cell.hydrology()
|
||||
: hydrologySlice == null
|
||||
? null
|
||||
: hydrologySlice.get(x & 15, localY, z & 15);
|
||||
if (hydrology != null) {
|
||||
carvedColumn[y] = hydrology.carves() ? (byte) 1 : 0;
|
||||
continue;
|
||||
}
|
||||
PreObjectMatterCell cell = journalSlice == null
|
||||
? null
|
||||
: journalSlice.get(x & 15, localY, z & 15);
|
||||
MatterCavern cavern = cell != null && cell.cavernCaptured()
|
||||
? cell.cavern()
|
||||
: cavernSlice == null ? null : cavernSlice.get(x & 15, localY, z & 15);
|
||||
@@ -799,10 +795,12 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
}
|
||||
|
||||
private static <T> T prerequisiteValue(Matter matter, int x, int y, int z, Class<T> type) {
|
||||
if (isPreObjectType(type) || type == HydrologyCaveCell.class) {
|
||||
PreObjectMatterCell cell = preObjectCell(matter, x, y, z);
|
||||
if (cell != null && cell.captures(type)) {
|
||||
return cell.original(type);
|
||||
}
|
||||
}
|
||||
MatterSlice<T> slice = matter.getSlice(type);
|
||||
return slice == null ? null : slice.get(x & 15, y & 15, z & 15);
|
||||
}
|
||||
|
||||
+131
-35
@@ -3,6 +3,7 @@ package art.arcane.iris.generation.mantle;
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.structure.object.IObjectPlacer;
|
||||
@@ -11,6 +12,9 @@ import art.arcane.iris.generation.block.B;
|
||||
import art.arcane.iris.world.storage.matter.TileWrapper;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
@@ -26,7 +30,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
private final int destinationChunkZ;
|
||||
private final int worldHeight;
|
||||
private final List<Mutation> mutations;
|
||||
private final Map<DataKey, Object> overlay;
|
||||
private final Long2ObjectOpenHashMap<Int2ObjectOpenHashMap<OverlayCell>> overlay;
|
||||
|
||||
ObjectDestinationTransaction(MantleWriter writer, int destinationChunkX, int destinationChunkZ) {
|
||||
this.writer = writer;
|
||||
@@ -34,7 +38,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
this.destinationChunkZ = destinationChunkZ;
|
||||
this.worldHeight = writer.getMantle().getWorldHeight();
|
||||
this.mutations = new ArrayList<>();
|
||||
this.overlay = new HashMap<>();
|
||||
this.overlay = new Long2ObjectOpenHashMap<>();
|
||||
}
|
||||
|
||||
void commit() {
|
||||
@@ -53,9 +57,10 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
}
|
||||
|
||||
void apply(ObjectSourcePlan plan) {
|
||||
for (Mutation mutation : plan.mutations()) {
|
||||
if (isDestination(mutation.x(), mutation.z())) {
|
||||
mutation.apply(this);
|
||||
for (Mutation mutation : plan.mutationsFor(destinationChunkX, destinationChunkZ)) {
|
||||
switch (mutation) {
|
||||
case SetMutation set -> setData(set.key().x(), set.key().y(), set.key().z(), set.value(), set);
|
||||
case CustomBlockMutation custom -> set(custom.key().x(), custom.key().y(), custom.key().z(), custom.state(), custom);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,6 +107,10 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
|
||||
@Override
|
||||
public void set(int x, int y, int z, NativeBlockState state) {
|
||||
set(x, y, z, state, null);
|
||||
}
|
||||
|
||||
private void set(int x, int y, int z, NativeBlockState state, CustomBlockMutation replayed) {
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
@@ -111,11 +120,12 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
if (!canSetBlock(x, y, z)) {
|
||||
return;
|
||||
}
|
||||
DataKey blockKey = new DataKey(x, y, z, NativeBlockState.class);
|
||||
Identifier identifier = Identifier.fromString(placementKey);
|
||||
overlay.put(blockKey, baseState);
|
||||
overlay.put(new DataKey(x, y, z, Identifier.class), identifier);
|
||||
mutations.add(new CustomBlockMutation(blockKey, state));
|
||||
OverlayCell cell = writableCell(x, y, z);
|
||||
cell.block = baseState;
|
||||
cell.identifier = identifier;
|
||||
mutations.add(replayed != null
|
||||
? replayed : new CustomBlockMutation(new DataKey(x, y, z, NativeBlockState.class), state));
|
||||
return;
|
||||
}
|
||||
setData(x, y, z, state);
|
||||
@@ -123,8 +133,8 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
|
||||
@Override
|
||||
public NativeBlockState get(int x, int y, int z) {
|
||||
DataKey key = new DataKey(x, y, z, NativeBlockState.class);
|
||||
Object value = overlay.get(key);
|
||||
OverlayCell cell = overlayCell(x, y, z);
|
||||
Object value = cell == null ? null : cell.block;
|
||||
if (value == CLEARED) {
|
||||
return EngineMantle.AIR.get();
|
||||
}
|
||||
@@ -140,15 +150,19 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
|
||||
@Override
|
||||
public boolean isCarved(int x, int y, int z) {
|
||||
Object block = overlay.get(new DataKey(x, y, z, NativeBlockState.class));
|
||||
OverlayCell cell = overlayCell(x, y, z);
|
||||
if (cell == null) {
|
||||
return writer.isPrerequisiteCarved(x, y, z);
|
||||
}
|
||||
Object block = cell.block;
|
||||
if (block instanceof NativeBlockState state && !state.isAir() && !state.isFluid()) {
|
||||
return false;
|
||||
}
|
||||
Object hydrologyValue = overlay.get(new DataKey(x, y, z, HydrologyCaveCell.class));
|
||||
Object hydrologyValue = cell.hydrology;
|
||||
if (hydrologyValue instanceof HydrologyCaveCell hydrology) {
|
||||
return hydrology.carves();
|
||||
}
|
||||
Object cavern = overlay.get(new DataKey(x, y, z, MatterCavern.class));
|
||||
Object cavern = cell.cavern;
|
||||
return cavern == null ? writer.isPrerequisiteCarved(x, y, z) : cavern != CLEARED;
|
||||
}
|
||||
|
||||
@@ -186,6 +200,10 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
|
||||
@Override
|
||||
public <T> void setData(int x, int y, int z, T data) {
|
||||
setData(x, y, z, data, null);
|
||||
}
|
||||
|
||||
private void setData(int x, int y, int z, Object data, SetMutation replayed) {
|
||||
if (data == null || y < 0 || y >= worldHeight) {
|
||||
return;
|
||||
}
|
||||
@@ -196,18 +214,21 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
return;
|
||||
}
|
||||
Class<?> type = data instanceof NativeBlockState ? NativeBlockState.class : data.getClass();
|
||||
OverlayCell cell = writableCell(x, y, z);
|
||||
if (data instanceof NativeBlockState) {
|
||||
overlay.put(new DataKey(x, y, z, Identifier.class), CLEARED);
|
||||
cell.block = data;
|
||||
cell.identifier = CLEARED;
|
||||
} else {
|
||||
cell.put(type, data);
|
||||
}
|
||||
DataKey key = new DataKey(x, y, z, type);
|
||||
overlay.put(key, data);
|
||||
mutations.add(new SetMutation(key, data));
|
||||
mutations.add(replayed != null
|
||||
? replayed : new SetMutation(new DataKey(x, y, z, type), data));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> @Nullable T getDataIfPresent(int x, int y, int z, Class<T> type) {
|
||||
DataKey key = new DataKey(x, y, z, type);
|
||||
Object value = overlay.get(key);
|
||||
OverlayCell cell = overlayCell(x, y, z);
|
||||
Object value = cell == null ? null : cell.get(type);
|
||||
if (value == CLEARED) {
|
||||
return null;
|
||||
}
|
||||
@@ -228,22 +249,22 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
} else {
|
||||
carved = carved.clone();
|
||||
}
|
||||
for (int y = 0; y < cappedHeight; y++) {
|
||||
Object block = overlay.get(new DataKey(x, y, z, NativeBlockState.class));
|
||||
if (block instanceof NativeBlockState state && !state.isAir() && !state.isFluid()) {
|
||||
Int2ObjectOpenHashMap<OverlayCell> column = overlay.get(columnKey(x, z));
|
||||
if (column == null) {
|
||||
return carved;
|
||||
}
|
||||
for (Int2ObjectMap.Entry<OverlayCell> entry : column.int2ObjectEntrySet()) {
|
||||
int y = entry.getIntKey();
|
||||
if (y >= cappedHeight) {
|
||||
continue;
|
||||
}
|
||||
OverlayCell cell = entry.getValue();
|
||||
if (cell.block instanceof NativeBlockState state && !state.isAir() && !state.isFluid()) {
|
||||
carved[y] = 0;
|
||||
continue;
|
||||
}
|
||||
DataKey hydrologyKey = new DataKey(x, y, z, HydrologyCaveCell.class);
|
||||
Object hydrologyValue = overlay.get(hydrologyKey);
|
||||
if (hydrologyValue instanceof HydrologyCaveCell hydrology) {
|
||||
} else if (cell.hydrology instanceof HydrologyCaveCell hydrology) {
|
||||
carved[y] = hydrology.carves() ? (byte) 1 : 0;
|
||||
continue;
|
||||
}
|
||||
DataKey cavernKey = new DataKey(x, y, z, MatterCavern.class);
|
||||
Object cavernValue = overlay.get(cavernKey);
|
||||
if (cavernValue != null) {
|
||||
carved[y] = cavernValue == CLEARED ? (byte) 0 : (byte) 1;
|
||||
} else if (cell.cavern != null) {
|
||||
carved[y] = cell.cavern == CLEARED ? (byte) 0 : (byte) 1;
|
||||
}
|
||||
}
|
||||
return carved;
|
||||
@@ -254,6 +275,30 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
return writer.getEngine();
|
||||
}
|
||||
|
||||
private static long columnKey(int x, int z) {
|
||||
return ((long) x << 32) | (z & 0xffffffffL);
|
||||
}
|
||||
|
||||
private OverlayCell overlayCell(int x, int y, int z) {
|
||||
Int2ObjectOpenHashMap<OverlayCell> column = overlay.get(columnKey(x, z));
|
||||
return column == null ? null : column.get(y);
|
||||
}
|
||||
|
||||
private OverlayCell writableCell(int x, int y, int z) {
|
||||
long coordinate = columnKey(x, z);
|
||||
Int2ObjectOpenHashMap<OverlayCell> column = overlay.get(coordinate);
|
||||
if (column == null) {
|
||||
column = new Int2ObjectOpenHashMap<>(4);
|
||||
overlay.put(coordinate, column);
|
||||
}
|
||||
OverlayCell cell = column.get(y);
|
||||
if (cell == null) {
|
||||
cell = new OverlayCell();
|
||||
column.put(y, cell);
|
||||
}
|
||||
return cell;
|
||||
}
|
||||
|
||||
private Object prerequisiteOrCleared(int x, int y, int z, Class<?> type) {
|
||||
Object value = writer.getPrerequisiteDataIfPresent(x, y, z, type);
|
||||
return value == null ? CLEARED : value;
|
||||
@@ -317,10 +362,61 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class OverlayCell {
|
||||
private Object block;
|
||||
private Object hydrology;
|
||||
private Object cavern;
|
||||
private Object identifier;
|
||||
private Object string;
|
||||
private Object treeMaterial;
|
||||
private Map<Class<?>, Object> other;
|
||||
|
||||
private Object get(Class<?> type) {
|
||||
if (type == NativeBlockState.class) {
|
||||
return block;
|
||||
}
|
||||
if (type == HydrologyCaveCell.class) {
|
||||
return hydrology;
|
||||
}
|
||||
if (type == MatterCavern.class) {
|
||||
return cavern;
|
||||
}
|
||||
if (type == Identifier.class) {
|
||||
return identifier;
|
||||
}
|
||||
if (type == String.class) {
|
||||
return string;
|
||||
}
|
||||
if (type == TreeBlockMaterial.class) {
|
||||
return treeMaterial;
|
||||
}
|
||||
return other == null ? null : other.get(type);
|
||||
}
|
||||
|
||||
private void put(Class<?> type, Object value) {
|
||||
if (type == HydrologyCaveCell.class) {
|
||||
hydrology = value;
|
||||
} else if (type == MatterCavern.class) {
|
||||
cavern = value;
|
||||
} else if (type == Identifier.class) {
|
||||
identifier = value;
|
||||
} else if (type == String.class) {
|
||||
string = value;
|
||||
} else if (type == TreeBlockMaterial.class) {
|
||||
treeMaterial = value;
|
||||
} else {
|
||||
if (other == null) {
|
||||
other = new HashMap<>();
|
||||
}
|
||||
other.put(type, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
record DataKey(int x, int y, int z, Class<?> type) {
|
||||
}
|
||||
|
||||
interface Mutation {
|
||||
sealed interface Mutation permits SetMutation, CustomBlockMutation {
|
||||
int x();
|
||||
|
||||
int z();
|
||||
|
||||
@@ -1,25 +1,47 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
final class ObjectSourcePlan {
|
||||
private final List<ObjectDestinationTransaction.Mutation> mutations;
|
||||
private static final int DESTINATION_INDEX_WEIGHT = 8;
|
||||
private final Long2ObjectOpenHashMap<List<ObjectDestinationTransaction.Mutation>> destinations;
|
||||
private final int mutationWeight;
|
||||
|
||||
ObjectSourcePlan(List<ObjectDestinationTransaction.Mutation> mutations) {
|
||||
this.mutations = List.copyOf(mutations);
|
||||
Long2ObjectOpenHashMap<ArrayList<ObjectDestinationTransaction.Mutation>> grouped = new Long2ObjectOpenHashMap<>(2);
|
||||
long weight = 1L;
|
||||
for (ObjectDestinationTransaction.Mutation mutation : this.mutations) {
|
||||
for (ObjectDestinationTransaction.Mutation mutation : mutations) {
|
||||
long key = destinationKey(mutation.x() >> 4, mutation.z() >> 4);
|
||||
ArrayList<ObjectDestinationTransaction.Mutation> local = grouped.get(key);
|
||||
if (local == null) {
|
||||
local = new ArrayList<>();
|
||||
grouped.put(key, local);
|
||||
weight += DESTINATION_INDEX_WEIGHT;
|
||||
}
|
||||
local.add(mutation);
|
||||
weight += mutation.weight();
|
||||
}
|
||||
destinations = new Long2ObjectOpenHashMap<>(grouped.size());
|
||||
for (Long2ObjectMap.Entry<ArrayList<ObjectDestinationTransaction.Mutation>> entry : grouped.long2ObjectEntrySet()) {
|
||||
destinations.put(entry.getLongKey(), List.copyOf(entry.getValue()));
|
||||
}
|
||||
this.mutationWeight = weight >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) weight;
|
||||
}
|
||||
|
||||
List<ObjectDestinationTransaction.Mutation> mutations() {
|
||||
return mutations;
|
||||
List<ObjectDestinationTransaction.Mutation> mutationsFor(int chunkX, int chunkZ) {
|
||||
List<ObjectDestinationTransaction.Mutation> mutations = destinations.get(destinationKey(chunkX, chunkZ));
|
||||
return mutations == null ? List.of() : mutations;
|
||||
}
|
||||
|
||||
int mutationWeight() {
|
||||
return mutationWeight;
|
||||
}
|
||||
|
||||
private static long destinationKey(int chunkX, int chunkZ) {
|
||||
return ((long) chunkX << 32) | (chunkZ & 0xffffffffL);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,21 @@ package art.arcane.iris.generation.mantle;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import art.arcane.volmlib.util.cache.CacheKey;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static art.arcane.iris.generation.cache.Cache.key;
|
||||
|
||||
final class ObjectSourcePlanCache {
|
||||
static final long MAXIMUM_MUTATION_WEIGHT = 1_048_576L;
|
||||
static final long MAXIMUM_MUTATION_WEIGHT = 4_194_304L;
|
||||
|
||||
private final Cache<Long, ObjectSourcePlan> plans;
|
||||
private final long maximumMutationWeight;
|
||||
private volatile State state;
|
||||
|
||||
ObjectSourcePlanCache() {
|
||||
this(MAXIMUM_MUTATION_WEIGHT);
|
||||
@@ -21,23 +26,88 @@ final class ObjectSourcePlanCache {
|
||||
if (maximumMutationWeight <= 0L) {
|
||||
throw new IllegalArgumentException("Maximum mutation weight must be positive");
|
||||
}
|
||||
this.plans = Caffeine.newBuilder()
|
||||
.maximumWeight(maximumMutationWeight)
|
||||
.weigher((Long key, ObjectSourcePlan plan) -> plan.mutationWeight())
|
||||
.build();
|
||||
this.maximumMutationWeight = maximumMutationWeight;
|
||||
state = new State(maximumMutationWeight);
|
||||
}
|
||||
|
||||
ObjectSourcePlan get(int sourceChunkX, int sourceChunkZ, Supplier<ObjectSourcePlan> builder) {
|
||||
Objects.requireNonNull(builder, "Source plan builder");
|
||||
return plans.get(key(sourceChunkX, sourceChunkZ), ignored -> builder.get());
|
||||
State current = state;
|
||||
long source = CacheKey.mix(key(sourceChunkX, sourceChunkZ));
|
||||
ObjectSourcePlan cached = current.plans.getIfPresent(source);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
Pending created = new Pending(Thread.currentThread());
|
||||
Pending pending = current.pending.putIfAbsent(source, created);
|
||||
if (pending != null) {
|
||||
return pending.await();
|
||||
}
|
||||
try {
|
||||
ObjectSourcePlan plan = current.plans.getIfPresent(source);
|
||||
if (plan == null) {
|
||||
plan = builder.get();
|
||||
if (plan != null) {
|
||||
current.plans.put(source, plan);
|
||||
}
|
||||
}
|
||||
created.result.complete(plan);
|
||||
return plan;
|
||||
} catch (RuntimeException | Error failure) {
|
||||
created.failure = failure;
|
||||
created.result.completeExceptionally(failure);
|
||||
throw failure;
|
||||
} finally {
|
||||
current.pending.remove(source, created);
|
||||
}
|
||||
}
|
||||
|
||||
void clear() {
|
||||
plans.invalidateAll();
|
||||
state = new State(maximumMutationWeight);
|
||||
}
|
||||
|
||||
long estimatedSize() {
|
||||
plans.cleanUp();
|
||||
return plans.estimatedSize();
|
||||
State current = state;
|
||||
current.plans.cleanUp();
|
||||
return current.plans.estimatedSize();
|
||||
}
|
||||
|
||||
private static final class State {
|
||||
private final Cache<Long, ObjectSourcePlan> plans;
|
||||
private final ConcurrentHashMap<Long, Pending> pending = new ConcurrentHashMap<>();
|
||||
|
||||
private State(long maximumMutationWeight) {
|
||||
plans = Caffeine.newBuilder()
|
||||
.maximumWeight(maximumMutationWeight)
|
||||
.weigher((Long key, ObjectSourcePlan plan) -> plan.mutationWeight())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Pending {
|
||||
private final Thread owner;
|
||||
private final CompletableFuture<ObjectSourcePlan> result = new CompletableFuture<>();
|
||||
private Throwable failure;
|
||||
|
||||
private Pending(Thread owner) {
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
private ObjectSourcePlan await() {
|
||||
if (owner == Thread.currentThread()) {
|
||||
throw new IllegalStateException("Recursive source plan construction");
|
||||
}
|
||||
try {
|
||||
return result.join();
|
||||
} catch (CompletionException completion) {
|
||||
if (failure instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
if (failure instanceof RuntimeException runtime) {
|
||||
throw runtime;
|
||||
}
|
||||
throw completion;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import art.arcane.volmlib.util.matter.MatterSlice;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.ArrayList;
|
||||
@@ -117,6 +118,7 @@ public final class TerrainMatterView {
|
||||
}
|
||||
|
||||
private static <T> T raw(Matter matter, int x, int y, int z, Class<T> type) {
|
||||
return !matter.hasSlice(type) ? null : matter.<T>getSlice(type).get(x & 15, y & 15, z & 15);
|
||||
MatterSlice<T> slice = matter.getSlice(type);
|
||||
return slice == null ? null : slice.get(x & 15, y & 15, z & 15);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
@@ -52,6 +54,7 @@ final class EngineShutdownSequence {
|
||||
Collections.newSetFromMap(new IdentityHashMap<>()));
|
||||
private final Set<EngineTarget> incompleteTargets = Collections.synchronizedSet(
|
||||
Collections.newSetFromMap(new IdentityHashMap<>()));
|
||||
private final Map<GenerationRuntime, CompletableFuture<Throwable>> detachedRuntimeCloses = new IdentityHashMap<>();
|
||||
private boolean runtimeReleased;
|
||||
private boolean worldManagerStopped;
|
||||
private boolean targetReleased;
|
||||
@@ -66,7 +69,7 @@ final class EngineShutdownSequence {
|
||||
generationAdmissions.add(history.retainRuntime());
|
||||
}
|
||||
|
||||
void close() {
|
||||
synchronized void close() {
|
||||
if (!engine.beginShutdown()) {
|
||||
return;
|
||||
}
|
||||
@@ -91,10 +94,11 @@ final class EngineShutdownSequence {
|
||||
}
|
||||
engine.lifecycleState = LifecycleState.CLOSING;
|
||||
engine.getClosing().set(true);
|
||||
}
|
||||
engine.backgroundTasks.closeBackgroundTaskAdmission();
|
||||
EngineTickRegistry.unregisterTicking(engine);
|
||||
// Best-effort like every other step: a pregen join timeout must not escape the
|
||||
// synchronized block before anything is saved or released. On failure, save what
|
||||
// shutdown before anything is saved or released. On failure, save what
|
||||
// can be saved and leave the close incomplete-but-retryable (a live pregen writer
|
||||
// may still hold the mantle, so the releases are skipped).
|
||||
Throwable pregenFailure = runCleanup(null, () -> engine.getPlatformHooks().shutdownPregenerator(engine));
|
||||
@@ -172,10 +176,11 @@ final class EngineShutdownSequence {
|
||||
&& preservationReleased) {
|
||||
failure = releaseGenerationAdmissions(failure);
|
||||
if (failure == null) {
|
||||
synchronized (engine.lifecycleLock) {
|
||||
engine.closed = true;
|
||||
engine.lifecycleState = LifecycleState.CLOSED;
|
||||
IrisLogging.debug("Engine Fully Shutdown!");
|
||||
}
|
||||
IrisLogging.debug("Engine Fully Shutdown!");
|
||||
}
|
||||
}
|
||||
if (failure != null) {
|
||||
@@ -359,6 +364,38 @@ final class EngineShutdownSequence {
|
||||
return failure;
|
||||
}
|
||||
|
||||
DetachedRuntimeClose claimDetachedRuntimeClose(GenerationRuntime runtime, boolean rejectConcurrent) {
|
||||
synchronized (engine.lifecycleLock) {
|
||||
if (!engine.detachedGenerationRuntimes.contains(runtime)) {
|
||||
return new DetachedRuntimeClose(CompletableFuture.completedFuture(null), false);
|
||||
}
|
||||
CompletableFuture<Throwable> existing = detachedRuntimeCloses.get(runtime);
|
||||
if (existing != null) {
|
||||
if (rejectConcurrent) {
|
||||
throw new IllegalStateException("Detached Iris generation runtime is already retiring.");
|
||||
}
|
||||
return new DetachedRuntimeClose(existing, false);
|
||||
}
|
||||
CompletableFuture<Throwable> completion = new CompletableFuture<>();
|
||||
detachedRuntimeCloses.put(runtime, completion);
|
||||
if (rejectConcurrent) {
|
||||
engine.retiringGenerationRuntimes.add(runtime);
|
||||
}
|
||||
return new DetachedRuntimeClose(completion, true);
|
||||
}
|
||||
}
|
||||
|
||||
void finishDetachedRuntimeClose(GenerationRuntime runtime, DetachedRuntimeClose close, Throwable failure) {
|
||||
synchronized (engine.lifecycleLock) {
|
||||
if (failure == null) {
|
||||
engine.detachedGenerationRuntimes.remove(runtime);
|
||||
}
|
||||
engine.retiringGenerationRuntimes.remove(runtime);
|
||||
close.completion().complete(failure);
|
||||
detachedRuntimeCloses.remove(runtime);
|
||||
}
|
||||
}
|
||||
|
||||
Throwable closeDetachedGenerationRuntime(GenerationRuntime generationRuntime, Throwable failure) {
|
||||
IrisEngine.GenerationRuntimeBinding binding = new IrisEngine.GenerationRuntimeBinding(
|
||||
engine,
|
||||
@@ -509,15 +546,28 @@ final class EngineShutdownSequence {
|
||||
private Throwable closeDetachedGenerationRuntimes(Throwable failure) {
|
||||
GenerationRuntime[] detachedRuntimes = engine.detachedGenerationRuntimes.toArray(new GenerationRuntime[0]);
|
||||
for (GenerationRuntime detached : detachedRuntimes) {
|
||||
Throwable detachedFailure = closeDetachedGenerationRuntime(detached, null);
|
||||
failure = appendFailure(failure, detachedFailure);
|
||||
if (detachedFailure == null) {
|
||||
engine.detachedGenerationRuntimes.remove(detached);
|
||||
DetachedRuntimeClose close = claimDetachedRuntimeClose(detached, false);
|
||||
Throwable detachedFailure;
|
||||
if (close.owner()) {
|
||||
detachedFailure = null;
|
||||
try {
|
||||
detachedFailure = closeDetachedGenerationRuntime(detached, null);
|
||||
} catch (Throwable closeFailure) {
|
||||
detachedFailure = closeFailure;
|
||||
} finally {
|
||||
finishDetachedRuntimeClose(detached, close, detachedFailure);
|
||||
}
|
||||
} else {
|
||||
detachedFailure = close.completion().join();
|
||||
}
|
||||
failure = appendFailure(failure, detachedFailure);
|
||||
}
|
||||
return failure;
|
||||
}
|
||||
|
||||
record DetachedRuntimeClose(CompletableFuture<Throwable> completion, boolean owner) {
|
||||
}
|
||||
|
||||
private Throwable releaseTarget(Throwable failure) {
|
||||
if (targetReleased) {
|
||||
return failure;
|
||||
|
||||
@@ -63,6 +63,7 @@ import art.arcane.iris.generation.hydrology.IrisRiverProfile;
|
||||
import art.arcane.iris.generation.noise.IrisShapedGeneratorStyle;
|
||||
import art.arcane.iris.generation.terrain.Terrain3DColumn;
|
||||
import art.arcane.iris.generation.terrain.Terrain3DRuntime;
|
||||
import art.arcane.iris.generation.terrain.ProceduralTerrainHeightSampler;
|
||||
import art.arcane.iris.generation.terrain.HydrologyBankTerrainRuntime;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
@@ -113,9 +114,9 @@ public class IrisComplex implements DataProvider {
|
||||
private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L);
|
||||
private static final int GRID_BOUNDS_CACHE_SIZE = 8192;
|
||||
private static final int STUDIO_NOISE_CACHE_SIZE = 32_768;
|
||||
private static final int TERRAIN_COLUMN_CACHE_SIZE = 65_536;
|
||||
/** One million corners: about 16 MB, roughly a 4000 by 4000 block area at the 4-block grid. */
|
||||
private static final int SHARED_CORNER_BOUNDS_CAPACITY = 1 << 20;
|
||||
private static final int HEIGHT_BOUNDS_GRID = 4;
|
||||
/** The slope streams measure the rise across a run of this many blocks, so gradient is slope over run. */
|
||||
private static final double SLOPE_RUN = 3D;
|
||||
/** A sheer coast would let shoreMinimumWidth buy unbounded height, so the shore band stops climbing here. */
|
||||
@@ -135,6 +136,8 @@ public class IrisComplex implements DataProvider {
|
||||
};
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient ThreadLocal<GridBoundsCache> gridBoundsCache = ThreadLocal.withInitial(GridBoundsCache::new);
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final int biomeBoundsSamplingStep;
|
||||
private transient volatile SharedCornerBounds sharedCornerBounds = new SharedCornerBounds(SHARED_CORNER_BOUNDS_CAPACITY);
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient IrisInterpolator[] frozenInterpolators;
|
||||
@@ -178,6 +181,8 @@ public class IrisComplex implements DataProvider {
|
||||
private ProceduralStream<Double> naturalHeightStream;
|
||||
private ProceduralStream<Double> baseTerrainHeightStream;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final ProceduralTerrainHeightSampler proceduralTerrainHeight;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient Engine terrainEngine;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private transient Terrain3DRuntime terrain3D;
|
||||
@@ -240,6 +245,7 @@ public class IrisComplex implements DataProvider {
|
||||
|
||||
IrisComplex(Engine engine, boolean simple, TransitionGenerationPlan transitionGenerationPlan, boolean detached) {
|
||||
terrainEngine = engine;
|
||||
biomeBoundsSamplingStep = engine.getDimension().getBiomeBoundsSamplingStep();
|
||||
this.detached = detached;
|
||||
this.transitionGenerationPlan = transitionGenerationPlan;
|
||||
this.resolvedTerrain = new ResolvedTerrainProvider(engine);
|
||||
@@ -379,8 +385,11 @@ public class IrisComplex implements DataProvider {
|
||||
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
||||
return mapped == null ? biome : mapped;
|
||||
}), "imageMappedBaseBiomeStream", engine, cacheSize);
|
||||
proceduralTerrainHeight = new ProceduralTerrainHeightSampler(
|
||||
(x, z) -> getHeight(engine, x, z, engine.getSeedManager().getHeight()),
|
||||
engine.getDimension().getTerrainSamplingStep());
|
||||
baseTerrainHeightStream = GenerationStreams.cache2DDouble(ProceduralStream.of(
|
||||
(x, z) -> sampleUnblendedNaturalTerrainHeight(engine, x, z),
|
||||
this::sampleUnblendedNaturalTerrainHeight,
|
||||
Interpolated.DOUBLE
|
||||
), "baseTerrainHeightStream", engine, cacheSize);
|
||||
boolean terrain3DEnabled = false;
|
||||
@@ -395,7 +404,7 @@ public class IrisComplex implements DataProvider {
|
||||
(x, z) -> baseTerrainHeightStream.getDouble(x, z),
|
||||
this::sampleTerrain3DBiome),
|
||||
new Terrain3DRuntime.Options(engine.getSeedManager().getTerrain(), engine.getHeight(),
|
||||
fluidHeight, data, terrain3DEnabled, Math.max(4_096, cacheSize)));
|
||||
fluidHeight, data, terrain3DEnabled, TERRAIN_COLUMN_CACHE_SIZE));
|
||||
unblendedNaturalHeightStream = terrain3DEnabled
|
||||
? GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::sampleTerrain3DHeight), "unblendedNaturalHeightStream", engine, cacheSize)
|
||||
: baseTerrainHeightStream;
|
||||
@@ -634,12 +643,11 @@ public class IrisComplex implements DataProvider {
|
||||
private double sampleNaturalTerrainHeight(Engine engine, double x, double z) {
|
||||
return terrain3D != null && terrain3D.active()
|
||||
? sampleTerrain3DHeight(x, z)
|
||||
: sampleUnblendedNaturalTerrainHeight(engine, x, z);
|
||||
: sampleUnblendedNaturalTerrainHeight(x, z);
|
||||
}
|
||||
|
||||
private double sampleTerrain3DHeight(double x, double z) {
|
||||
Terrain3DColumn column = terrain3D.column(blockCoordinate(x), blockCoordinate(z));
|
||||
return column.shaped() ? column.topY() : column.baseHeight();
|
||||
return terrain3D.height(blockCoordinate(x), blockCoordinate(z));
|
||||
}
|
||||
|
||||
private IrisBiome focusedBiomeAt(double x, double z) {
|
||||
@@ -735,8 +743,8 @@ public class IrisComplex implements DataProvider {
|
||||
return column == null ? getPlacementHeightStream().getDouble(x, z) : column.nearestSurfaceY(surfaceY);
|
||||
}
|
||||
|
||||
private double sampleUnblendedNaturalTerrainHeight(Engine engine, double x, double z) {
|
||||
double proceduralHeight = getHeight(engine, x, z, engine.getSeedManager().getHeight());
|
||||
private double sampleUnblendedNaturalTerrainHeight(double x, double z) {
|
||||
double proceduralHeight = proceduralTerrainHeight.sample(x, z);
|
||||
return imageMapRuntime.sampleTerrainHeight(x, z, proceduralHeight);
|
||||
}
|
||||
|
||||
@@ -1279,11 +1287,8 @@ public class IrisComplex implements DataProvider {
|
||||
return null;
|
||||
}
|
||||
|
||||
public double sampleProceduralTerrainHeight(Engine engine, double worldX, double worldZ) {
|
||||
if (engine == null) {
|
||||
throw new IllegalArgumentException("Engine is required to sample procedural terrain height");
|
||||
}
|
||||
return getHeight(engine, worldX, worldZ, engine.getSeedManager().getHeight());
|
||||
public double sampleProceduralTerrainHeight(double worldX, double worldZ) {
|
||||
return proceduralTerrainHeight.sample(worldX, worldZ);
|
||||
}
|
||||
|
||||
private IrisDecorator decorateFor(IrisBiome b, double x, double z, IrisDecorationPart part) {
|
||||
@@ -1439,7 +1444,7 @@ public class IrisComplex implements DataProvider {
|
||||
if (biomeBuffet != null && !biomeBuffet.cells().isEmpty()) {
|
||||
return generatorBounds.get(interpolator).get(focusedBiomeAt(x, z)).noiseBounds;
|
||||
}
|
||||
int grid = HEIGHT_BOUNDS_GRID;
|
||||
int grid = biomeBoundsSamplingStep;
|
||||
GridBoundsCache cache = gridBoundsCache.get();
|
||||
if (grid <= 1) {
|
||||
return sampleBoundsRaw(cache, engine, interpolator, generators, x, z);
|
||||
|
||||
@@ -1166,6 +1166,7 @@ public class IrisEngine implements Engine {
|
||||
public void closeDetachedGenerationRuntime(GenerationRuntimeBinding binding) {
|
||||
GenerationRuntimeBinding required = Objects.requireNonNull(binding, "detached generation runtime binding");
|
||||
GenerationRuntime retired;
|
||||
EngineShutdownSequence.DetachedRuntimeClose retirement;
|
||||
synchronized (lifecycleLock) {
|
||||
if (required.engine != this) {
|
||||
throw new IllegalArgumentException("Generation runtime binding belongs to a different Iris engine.");
|
||||
@@ -1177,19 +1178,17 @@ public class IrisEngine implements Engine {
|
||||
if (!detachedGenerationRuntimes.contains(required.runtime)) {
|
||||
return;
|
||||
}
|
||||
if (!retiringGenerationRuntimes.add(required.runtime)) {
|
||||
throw new IllegalStateException("Detached Iris generation runtime is already retiring.");
|
||||
}
|
||||
retired = required.runtime;
|
||||
retirement = shutdownSequence.claimDetachedRuntimeClose(retired, true);
|
||||
}
|
||||
Throwable failure = retireGenerationRuntimeCaches(retired.cacheId(), null);
|
||||
Throwable failure = null;
|
||||
try {
|
||||
failure = retireGenerationRuntimeCaches(retired.cacheId(), null);
|
||||
failure = shutdownSequence.closeDetachedGenerationRuntime(retired, failure);
|
||||
} catch (Throwable closeFailure) {
|
||||
failure = EngineShutdownSequence.appendFailure(failure, closeFailure);
|
||||
} finally {
|
||||
synchronized (lifecycleLock) {
|
||||
detachedGenerationRuntimes.remove(retired);
|
||||
retiringGenerationRuntimes.remove(retired);
|
||||
}
|
||||
shutdownSequence.finishDetachedRuntimeClose(retired, retirement, failure);
|
||||
}
|
||||
if (failure != null) {
|
||||
throw new IllegalStateException("Failed to close a detached Iris generation runtime.", failure);
|
||||
|
||||
@@ -24,6 +24,10 @@ import art.arcane.iris.generation.runtime.DimensionStackLayout;
|
||||
import art.arcane.iris.generation.runtime.UpperDimensionContext;
|
||||
import art.arcane.iris.generation.terrain.Terrain3DColumn;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
import art.arcane.iris.generation.runtime.EngineAssignedModifier;
|
||||
import art.arcane.iris.generation.mantle.TerrainMatterView;
|
||||
import art.arcane.iris.generation.terrain.InferredType;
|
||||
@@ -37,6 +41,7 @@ import art.arcane.iris.generation.hydrology.IrisRiverMaterialConfig;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
|
||||
import art.arcane.iris.generation.context.ChunkContext;
|
||||
import art.arcane.iris.generation.context.IrisContext;
|
||||
import art.arcane.iris.generation.block.B;
|
||||
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
@@ -83,7 +88,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
public void onModify(int x, int z, Hunk<NativeBlockState> output, boolean multicore, ChunkContext context) {
|
||||
PrecisionStopwatch caveStopwatch = PrecisionStopwatch.start();
|
||||
Mantle<Matter> mantle = getEngine().getMantle().getMantle();
|
||||
IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State();
|
||||
CaveInputs resolverState = new CaveInputs(getEngine());
|
||||
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache = new Long2ObjectOpenHashMap<>(2048);
|
||||
IrisCarveScratch scratch = SCRATCH.get();
|
||||
scratch.reset();
|
||||
@@ -97,7 +102,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
int[] upperSurfaceHeights = protectUpper ? scratch.getOrCreateUpperSurfaceHeights() : null;
|
||||
DimensionStackContext stackContext = getEngine().getDimensionStackContext();
|
||||
DimensionStackLayout[] stackLayouts = stackContext == null ? null : new DimensionStackLayout[256];
|
||||
Terrain3DColumn[] terrainColumns = getComplex().hasTerrain3D() ? new Terrain3DColumn[256] : null;
|
||||
Terrain3DColumn[] terrainColumns = resolverState.complex.hasTerrain3D() ? new Terrain3DColumn[256] : null;
|
||||
int chunkBlockX = PowerOfTwoCoordinates.chunkToBlock(x);
|
||||
int chunkBlockZ = PowerOfTwoCoordinates.chunkToBlock(z);
|
||||
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
|
||||
@@ -105,7 +110,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
int localZ = columnIndex & 15;
|
||||
surfaceHeights[columnIndex] = context.getRoundedHeight(localX, localZ);
|
||||
if (terrainColumns != null) {
|
||||
terrainColumns[columnIndex] = getComplex().terrainColumn(chunkBlockX + localX, chunkBlockZ + localZ);
|
||||
terrainColumns[columnIndex] = resolverState.complex.terrainColumn(chunkBlockX + localX, chunkBlockZ + localZ);
|
||||
}
|
||||
if (protectUpper) {
|
||||
int worldX = localX + chunkBlockX;
|
||||
@@ -594,7 +599,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
int columnIndex,
|
||||
int chunkX,
|
||||
int chunkZ,
|
||||
IrisDimensionCarvingResolver.State resolverState,
|
||||
CaveInputs resolverState,
|
||||
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
|
||||
Map<String, IrisBiome> customBiomeCache
|
||||
) {
|
||||
@@ -617,12 +622,12 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
int y = firstHeight;
|
||||
|
||||
while (y >= 0) {
|
||||
if (y <= getEngine().getHeight()) {
|
||||
if (y <= resolverState.height) {
|
||||
if (y == buf + 1) {
|
||||
buf = y;
|
||||
zone.ceiling = buf;
|
||||
} else {
|
||||
if (zone.isValid(getEngine())) {
|
||||
if (zone.isValid(resolverState.height)) {
|
||||
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState,
|
||||
caveBiomeCache, customBiomeCache);
|
||||
}
|
||||
@@ -635,7 +640,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
y = columnMask.nextSetBit(y + 1);
|
||||
}
|
||||
|
||||
if (zone.isValid(getEngine())) {
|
||||
if (zone.isValid(resolverState.height)) {
|
||||
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState,
|
||||
caveBiomeCache, customBiomeCache);
|
||||
}
|
||||
@@ -649,7 +654,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
int columnIndex,
|
||||
int chunkX,
|
||||
int chunkZ,
|
||||
IrisDimensionCarvingResolver.State resolverState,
|
||||
CaveInputs resolverState,
|
||||
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
|
||||
Map<String, IrisBiome> customBiomeCache
|
||||
) {
|
||||
@@ -692,7 +697,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
int worldZ,
|
||||
int zoneFloor,
|
||||
int zoneCeiling,
|
||||
IrisDimensionCarvingResolver.State resolverState,
|
||||
CaveInputs resolverState,
|
||||
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
|
||||
Map<String, IrisBiome> customBiomeCache
|
||||
) {
|
||||
@@ -711,7 +716,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
mantleChunk, rx, zoneFloor, rz, HydrologyCaveCell.class);
|
||||
IrisRiverMaterialConfig bedMaterial = undergroundBedMaterial();
|
||||
KList<NativeBlockState> floorLayers = floorBiome.generateLayers(
|
||||
getDimension(), worldX, worldZ, rng, 3, zoneFloor, getData(), getComplex());
|
||||
resolverState.dimension, worldX, worldZ, rng, 3, zoneFloor, resolverState.data, resolverState.complex);
|
||||
for (int i = 0; i < zoneFloor - 1; i++) {
|
||||
if (!floorLayers.hasIndex(i)) {
|
||||
break;
|
||||
@@ -732,7 +737,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
output, rx, floorY, rz,
|
||||
paintUndergroundBedMaterial(
|
||||
floorLayers.get(i), bedMaterial, floorHydrology, i,
|
||||
rng, worldX, floorY, worldZ, getData()),
|
||||
rng, worldX, floorY, worldZ, resolverState.data),
|
||||
floorHydrology);
|
||||
if (!B.isSolid(existing)
|
||||
|| !canReplaceHydrologyGuard(hydrology, layer, false)
|
||||
@@ -750,7 +755,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
if (ceilingBiome != null) {
|
||||
int worldMaxY = getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight();
|
||||
KList<NativeBlockState> ceilingLayers = ceilingBiome.generateCeilingLayers(
|
||||
getDimension(), worldX, worldZ, rng, 3, zoneCeiling, getData(), getComplex());
|
||||
resolverState.dimension, worldX, worldZ, rng, 3, zoneCeiling, resolverState.data, resolverState.complex);
|
||||
for (int i = 0; i < ceilingLayers.size(); i++) {
|
||||
int ceilingY = zoneCeiling + i + 1;
|
||||
if (ceilingY >= worldMaxY) {
|
||||
@@ -794,7 +799,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
|
||||
private void processZone(Hunk<NativeBlockState> output, MantleChunk<Matter> mc, Mantle<Matter> mantle,
|
||||
CaveZone zone, int rx, int rz, int xx, int zz,
|
||||
IrisDimensionCarvingResolver.State resolverState,
|
||||
CaveInputs resolverState,
|
||||
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
|
||||
Map<String, IrisBiome> customBiomeCache) {
|
||||
int maxY = output.getHeight();
|
||||
@@ -818,7 +823,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
HydrologyCaveCell floorHydrology = dataIfPresent(
|
||||
mc, rx, zone.floor, rz, HydrologyCaveCell.class);
|
||||
IrisRiverMaterialConfig bedMaterial = undergroundBedMaterial();
|
||||
KList<NativeBlockState> floorBlocks = floorBiome.generateLayers(getDimension(), xx, zz, rng, 3, zone.floor, getData(), getComplex());
|
||||
KList<NativeBlockState> floorBlocks = floorBiome.generateLayers(resolverState.dimension, xx, zz, rng, 3, zone.floor, resolverState.data, resolverState.complex);
|
||||
for (int i = 0; i < zone.floor - 1; i++) {
|
||||
if (!floorBlocks.hasIndex(i)) {
|
||||
break;
|
||||
@@ -834,7 +839,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
output, rx, y, rz,
|
||||
paintUndergroundBedMaterial(
|
||||
floorBlocks.get(i), bedMaterial, floorHydrology, i,
|
||||
rng, xx, y, zz, getData()),
|
||||
rng, xx, y, zz, resolverState.data),
|
||||
floorHydrology);
|
||||
NativeBlockState existing = output.getRaw(rx, y, rz);
|
||||
if (!B.isSolid(existing)
|
||||
@@ -851,7 +856,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
}
|
||||
|
||||
if (ceilingBiome != null) {
|
||||
KList<NativeBlockState> ceilingBlocks = ceilingBiome.generateCeilingLayers(getDimension(), xx, zz, rng, 3, zone.ceiling, getData(), getComplex());
|
||||
KList<NativeBlockState> ceilingBlocks = ceilingBiome.generateCeilingLayers(resolverState.dimension, xx, zz, rng, 3, zone.ceiling, resolverState.data, resolverState.complex);
|
||||
for (int i = 0; i < ceilingBlocks.size(); i++) {
|
||||
int cy = zone.ceiling + i + 1;
|
||||
if (cy >= maxY) {
|
||||
@@ -882,7 +887,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
public void decorateNaturalCaves(int blockX, int blockZ, Hunk<NativeBlockState> output) {
|
||||
Mantle<Matter> mantle = getEngine().getMantle().getMantle();
|
||||
MantleChunk<Matter> chunk = mantle.getChunk(blockX >> 4, blockZ >> 4).use();
|
||||
IrisDimensionCarvingResolver.State resolver = new IrisDimensionCarvingResolver.State();
|
||||
CaveInputs resolver = new CaveInputs(getEngine());
|
||||
Long2ObjectOpenHashMap<IrisBiome> caveBiomes = new Long2ObjectOpenHashMap<>(256);
|
||||
Map<String, IrisBiome> customBiomes = new HashMap<>();
|
||||
try {
|
||||
@@ -901,7 +906,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
CaveZone zone = new CaveZone();
|
||||
zone.setFloor(floor);
|
||||
zone.setCeiling(y - 1);
|
||||
if (zone.isValid(getEngine())
|
||||
if (zone.isValid(resolver.height)
|
||||
&& !getComplex().isTerrain3DOpening(worldX, floor, worldZ)) {
|
||||
if (markerRoll(worldX, zone.ceiling, worldZ, 0x9E3779B97F4A7C15L)) {
|
||||
mantle.set(worldX, zone.ceiling, worldZ, MarkerMatter.CAVE_CEILING);
|
||||
@@ -985,15 +990,15 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
}
|
||||
}
|
||||
|
||||
IrisBiome resolveCaveBoundaryBiome(MantleChunk<Matter> mantleChunk, int x, int y, int z, int worldX, int worldZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Map<String, IrisBiome> customBiomeCache) {
|
||||
IrisBiome resolveCaveBoundaryBiome(MantleChunk<Matter> mantleChunk, int x, int y, int z, int worldX, int worldZ, CaveInputs resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Map<String, IrisBiome> customBiomeCache) {
|
||||
MatterCavern cavern = composedCavernAt(mantleChunk, x, y, z);
|
||||
return resolveCaveBoundaryBiome(
|
||||
cavern, worldX, y, worldZ, resolverState, caveBiomeCache, customBiomeCache);
|
||||
}
|
||||
|
||||
IrisBiome resolveCaveBoundaryBiome(MatterCavern cavern, int worldX, int y, int worldZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Map<String, IrisBiome> customBiomeCache) {
|
||||
if (getComplex().isTerrain3DOpening(worldX, y, worldZ)) {
|
||||
return getComplex().getTrueBiomeStream().get(worldX, worldZ);
|
||||
IrisBiome resolveCaveBoundaryBiome(MatterCavern cavern, int worldX, int y, int worldZ, CaveInputs resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Map<String, IrisBiome> customBiomeCache) {
|
||||
if (resolverState.complex.isTerrain3DOpening(worldX, y, worldZ)) {
|
||||
return resolverState.complex.getTrueBiomeStream().get(worldX, worldZ);
|
||||
}
|
||||
if (cavern != null && !cavern.getCustomBiome().isEmpty()) {
|
||||
return resolveCustomBiome(customBiomeCache, cavern.getCustomBiome());
|
||||
@@ -1105,7 +1110,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
return IrisProceduralBlocks.isGravityAffected(state);
|
||||
}
|
||||
|
||||
private IrisBiome resolveCaveBiome(Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, int x, int y, int z, IrisDimensionCarvingResolver.State resolverState) {
|
||||
private IrisBiome resolveCaveBiome(Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, int x, int y, int z, CaveInputs resolverState) {
|
||||
IrisBiome center = sampleCaveBiome(caveBiomeCache, x, y, z, resolverState);
|
||||
if (center == null) {
|
||||
return null;
|
||||
@@ -1125,14 +1130,14 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
return neighbor != null ? neighbor : center;
|
||||
}
|
||||
|
||||
private IrisBiome sampleCaveBiome(Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, int x, int y, int z, IrisDimensionCarvingResolver.State resolverState) {
|
||||
private IrisBiome sampleCaveBiome(Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, int x, int y, int z, CaveInputs resolverState) {
|
||||
long key = BlockPosition.toLong(x, y, z);
|
||||
IrisBiome cachedBiome = caveBiomeCache.get(key);
|
||||
if (cachedBiome != null) {
|
||||
return cachedBiome;
|
||||
}
|
||||
|
||||
IrisBiome resolvedBiome = getEngine().getCaveBiome(x, y, z, resolverState);
|
||||
IrisBiome resolvedBiome = resolverState.resolve(x, y, z);
|
||||
if (resolvedBiome != null) {
|
||||
caveBiomeCache.put(key, resolvedBiome);
|
||||
}
|
||||
@@ -1149,6 +1154,79 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
return loaded;
|
||||
}
|
||||
|
||||
static final class CaveInputs {
|
||||
private final Engine engine;
|
||||
private final IrisComplex complex;
|
||||
private final IrisDimension dimension;
|
||||
private final IrisData data;
|
||||
private final int height;
|
||||
private final IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State();
|
||||
private final IrisDimensionCarvingResolver.Snapshot snapshot;
|
||||
private final int minimumY;
|
||||
private final DimensionStackContext stack;
|
||||
private final ProceduralStream<IrisBiome> surfaceBiomes;
|
||||
private final ProceduralStream<IrisBiome> caveBiomes;
|
||||
private final ProceduralStream<Double> heights;
|
||||
private final CaveColumn[] columns;
|
||||
|
||||
CaveInputs(Engine engine) {
|
||||
this.engine = engine;
|
||||
complex = engine.getComplex();
|
||||
dimension = engine.getDimension();
|
||||
data = engine.getData();
|
||||
height = engine.getHeight();
|
||||
IrisContext context = IrisContext.get();
|
||||
boolean scoped = engine instanceof IrisEngine irisEngine
|
||||
&& irisEngine.hasGenerationRuntimeScope() && !engine.getPlatformHooks().isMainThread()
|
||||
&& context != null && context.getChunkContext() != null
|
||||
&& context.getChunkContext().getComplex() == complex
|
||||
&& context.getChunkContext().isNaturalTerrain();
|
||||
snapshot = scoped ? IrisDimensionCarvingResolver.snapshot(engine) : null;
|
||||
minimumY = scoped ? engine.getWorld().minHeight() : 0;
|
||||
stack = scoped ? engine.getDimensionStackContext() : null;
|
||||
surfaceBiomes = scoped ? complex.getTrueBiomeStream() : null;
|
||||
caveBiomes = scoped ? complex.getCaveBiomeStream() : null;
|
||||
heights = scoped ? complex.getHeightStream() : null;
|
||||
columns = scoped ? new CaveColumn[1024] : null;
|
||||
}
|
||||
|
||||
IrisBiome resolve(int x, int y, int z) {
|
||||
if (snapshot == null) {
|
||||
return engine.getCaveBiome(x, y, z, state);
|
||||
}
|
||||
IrisBiome configured = snapshot.resolveBiome(x, y + minimumY, z);
|
||||
if (configured != null) {
|
||||
return configured;
|
||||
}
|
||||
long key = ((long) x << 32) | (z & 0xffffffffL);
|
||||
int index = (x * 31 + z) & (columns.length - 1);
|
||||
CaveColumn column = columns[index];
|
||||
if (column == null || column.key != key) {
|
||||
IrisBiome surface = surfaceBiomes.get(x, z);
|
||||
IrisBiome cave = caveBiomes.get(x, z);
|
||||
int surfaceY = heights.get(x, z).intValue();
|
||||
if (cave == null || cave.getLoadKey() == null) {
|
||||
cave = surface;
|
||||
if (stack != null) {
|
||||
DimensionStackLayout.Layer layer = stack.getLayout(x, z).surfaceLayer();
|
||||
if (layer != null && layer.biome() != null) {
|
||||
cave = layer.biome();
|
||||
}
|
||||
}
|
||||
}
|
||||
column = new CaveColumn(key, surface, cave, surfaceY,
|
||||
cave == null ? 0 : Math.max(0, cave.getCaveMinDepthBelowSurface()));
|
||||
columns[index] = column;
|
||||
}
|
||||
int depth = column.surfaceY - y;
|
||||
return column.cave == null || depth <= 0 || depth < column.minimumDepth
|
||||
? column.surface : column.cave;
|
||||
}
|
||||
}
|
||||
|
||||
private record CaveColumn(long key, IrisBiome surface, IrisBiome cave, int surfaceY, int minimumDepth) {
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class CaveZone {
|
||||
private int ceiling = -1;
|
||||
@@ -1158,8 +1236,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
return (ceiling - floor) - 1;
|
||||
}
|
||||
|
||||
public boolean isValid(Engine engine) {
|
||||
return floor < ceiling && ceiling - floor >= 1 && floor >= 0 && ceiling <= engine.getHeight() && airThickness() > 0;
|
||||
public boolean isValid(int height) {
|
||||
return floor < ceiling && ceiling - floor >= 1 && floor >= 0 && ceiling <= height && airThickness() > 0;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
|
||||
+8
-5
@@ -176,10 +176,9 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
|
||||
return block == null ? fallbackSolid : block;
|
||||
}
|
||||
|
||||
private PaletteContext createPaletteContext(IrisBiome parent, IrisFloatingChildBiomes entry, IrisDimension dimension, int wx, int wz, long colSeed, int paletteDepth, IrisData data, IrisComplex complex) {
|
||||
private PaletteContext createPaletteContext(IrisBiome parent, IrisFloatingChildBiomes entry, IrisDimension dimension, int wx, int wz, int paletteDepth, IrisData data, IrisComplex complex) {
|
||||
IrisBiome target = entry == null ? parent : entry.getRealBiome(parent, data);
|
||||
int entrySeed = entry == null || entry.getBiome() == null ? 0 : entry.getBiome().hashCode();
|
||||
RNG layerRng = rng.nextParallelRNG((int) (colSeed ^ 0x7A4E ^ entrySeed));
|
||||
RNG layerRng = paletteRng(rng, entry);
|
||||
KList<NativeBlockState> topBlocks = target == null ? null : target.generateLayers(dimension, wx, wz, layerRng, paletteDepth, paletteDepth, data, complex);
|
||||
if (topBlocks == null || topBlocks.isEmpty()) {
|
||||
topBlocks = parent.generateLayers(dimension, wx, wz, layerRng, paletteDepth, paletteDepth, data, complex);
|
||||
@@ -188,6 +187,11 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
|
||||
return new PaletteContext(topBlocks, bottomBlocks, B.getState("minecraft:stone"));
|
||||
}
|
||||
|
||||
static RNG paletteRng(RNG worldRng, IrisFloatingChildBiomes entry) {
|
||||
int entrySeed = entry == null || entry.getBiome() == null ? 0 : entry.getBiome().hashCode();
|
||||
return worldRng.nextParallelRNG(0x7A4E ^ entrySeed);
|
||||
}
|
||||
|
||||
private static final class PaletteContext {
|
||||
private final KList<NativeBlockState> topBlocks;
|
||||
private final KList<NativeBlockState> bottomBlocks;
|
||||
@@ -230,7 +234,6 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
|
||||
continue;
|
||||
}
|
||||
|
||||
long colSeed = FloatingIslandSample.columnSeed(baseSeed, wx, wz);
|
||||
int paletteDepth = Math.max(4, sample.solidCount + 4);
|
||||
IdentityHashMap<IrisFloatingChildBiomes, PaletteContext> paletteContexts = new IdentityHashMap<>();
|
||||
IdentityHashMap<IrisFloatingChildBiomes, Integer> topDepthByEntry = new IdentityHashMap<>();
|
||||
@@ -246,7 +249,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
|
||||
IrisFloatingChildBiomes entry = sample.entryAt(k);
|
||||
PaletteContext paletteContext = paletteContexts.get(entry);
|
||||
if (paletteContext == null) {
|
||||
paletteContext = createPaletteContext(parent, entry, dimension, wx, wz, colSeed, paletteDepth, data, complex);
|
||||
paletteContext = createPaletteContext(parent, entry, dimension, wx, wz, paletteDepth, data, complex);
|
||||
paletteContexts.put(entry, paletteContext);
|
||||
}
|
||||
int depth = topDepthByEntry.getOrDefault(entry, 0);
|
||||
|
||||
@@ -71,6 +71,7 @@ import art.arcane.iris.pack.schema.annotation.ArrayType;
|
||||
import art.arcane.volmlib.util.documentation.Description;
|
||||
import art.arcane.iris.pack.schema.annotation.MaxNumber;
|
||||
import art.arcane.iris.pack.schema.annotation.MinNumber;
|
||||
import art.arcane.iris.pack.schema.annotation.IntegerOptions;
|
||||
import art.arcane.iris.pack.schema.annotation.RegistryListFunction;
|
||||
import art.arcane.iris.pack.schema.annotation.RegistryListResource;
|
||||
import art.arcane.iris.pack.schema.annotation.Required;
|
||||
@@ -278,6 +279,21 @@ public class IrisDimension extends IrisRegistrant {
|
||||
@MaxNumber(512)
|
||||
@Description("Zoom in or out the biome size. Higher = bigger biomes")
|
||||
private double biomeZoom = 1D;
|
||||
@MinNumber(1)
|
||||
@MaxNumber(8)
|
||||
@IntegerOptions({1, 2, 4, 8})
|
||||
@Description("Procedural terrain height sampling interval in blocks: 1, 2, 4, or 8. 1 retains full detail. Larger intervals interpolate between shared samples, reducing generation work and smoothing small height features. Image maps retain their configured sampling. Set before creating a world.")
|
||||
private int terrainSamplingStep = 1;
|
||||
@MinNumber(4)
|
||||
@MaxNumber(32)
|
||||
@IntegerOptions({4, 8, 16, 32})
|
||||
@Description("Biome height-bound sampling interval in blocks: 4, 8, 16, or 32. 4 retains the original biome blending. Larger intervals interpolate the configured generator bounds between shared anchors, smoothing small biome-height features. Generator interpolation styles and radii remain active at the anchors. Set before creating a world.")
|
||||
private int biomeBoundsSamplingStep = 4;
|
||||
@MinNumber(1)
|
||||
@MaxNumber(8)
|
||||
@IntegerOptions({1, 2, 4, 8})
|
||||
@Description("Vertical cave density sampling interval in blocks: 1, 2, 4, or 8. Larger intervals interpolate density while retaining cave module bounds, surface protection, and fluid rules. Changes cave geometry. Set before creating a world.")
|
||||
private int caveDensitySamplingStep = 1;
|
||||
@MinNumber(0)
|
||||
@MaxNumber(360)
|
||||
@Description("You can rotate the input coordinates by an angle. This can make terrain appear more natural (less sharp corners and lines). This literally rotates the entire dimension by an angle. Hint: Try 12 degrees or something not on a 90 or 45 degree angle.")
|
||||
@@ -513,6 +529,14 @@ public class IrisDimension extends IrisRegistrant {
|
||||
});
|
||||
}
|
||||
|
||||
public int getBiomeBoundsSamplingStep() {
|
||||
if (biomeBoundsSamplingStep != 4 && biomeBoundsSamplingStep != 8
|
||||
&& biomeBoundsSamplingStep != 16 && biomeBoundsSamplingStep != 32) {
|
||||
throw new IllegalArgumentException("Biome bounds sampling step must be 4, 8, 16, or 32");
|
||||
}
|
||||
return biomeBoundsSamplingStep;
|
||||
}
|
||||
|
||||
public int getFluidHeight() {
|
||||
return fluidHeight - (int) dimensionHeight.getMin();
|
||||
}
|
||||
|
||||
+69
-1
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.generation.terrain;
|
||||
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||
import art.arcane.iris.pack.value.IrisRange;
|
||||
import art.arcane.iris.pack.value.IrisRaritySelection;
|
||||
|
||||
@@ -28,6 +29,10 @@ public final class IrisDimensionCarvingResolver {
|
||||
|
||||
}
|
||||
|
||||
public static Snapshot snapshot(Engine engine) {
|
||||
return new Snapshot(engine);
|
||||
}
|
||||
|
||||
public static IrisBiome resolveBiome(Engine engine, int worldX, int worldY, int worldZ, State state) {
|
||||
State resolvedState = state == null ? threadState() : state;
|
||||
IrisData data = resolvedState.bind(engine);
|
||||
@@ -80,7 +85,10 @@ public final class IrisDimensionCarvingResolver {
|
||||
return resolvedState.rootEntriesByWorldY.get(worldY);
|
||||
}
|
||||
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
return selectBoundRootEntry(engine.getDimension(), worldY, data, resolvedState);
|
||||
}
|
||||
|
||||
private static IrisDimensionCarvingEntry selectBoundRootEntry(IrisDimension dimension, int worldY, IrisData data, State resolvedState) {
|
||||
List<IrisDimensionCarvingEntry> entries = dimension.getCarving();
|
||||
if (entries == null || entries.isEmpty()) {
|
||||
resolvedState.rootEntriesByWorldY.put(worldY, null);
|
||||
@@ -115,10 +123,19 @@ public final class IrisDimensionCarvingResolver {
|
||||
return rootEntry;
|
||||
}
|
||||
|
||||
ColumnResolutions columns = resolvedState.columns;
|
||||
int slot = ((worldX & 31) << 5) | (worldZ & 31);
|
||||
long coordinate = ((long) worldX << 32) | (worldZ & 0xffffffffL);
|
||||
if (columns != null && columns.roots[slot] == rootEntry && columns.coordinates[slot] == coordinate) {
|
||||
return columns.entries[slot];
|
||||
}
|
||||
|
||||
Map<String, IrisDimensionCarvingEntry> entryIndex = resolveEntryIndex(engine, resolvedState);
|
||||
IrisDimensionCarvingEntry current = rootEntry;
|
||||
int depth = remainingDepth;
|
||||
boolean cacheable = true;
|
||||
while (depth > 0) {
|
||||
cacheable &= fixedChildStyle(current);
|
||||
IrisDimensionCarvingEntry selected = selectChild(engine, data, current, worldX, worldZ, entryIndex, resolvedState);
|
||||
if (selected == null || selected == current) {
|
||||
break;
|
||||
@@ -132,9 +149,27 @@ public final class IrisDimensionCarvingResolver {
|
||||
current = selected;
|
||||
}
|
||||
|
||||
if (cacheable) {
|
||||
if (columns == null) {
|
||||
columns = new ColumnResolutions();
|
||||
resolvedState.columns = columns;
|
||||
}
|
||||
columns.coordinates[slot] = coordinate;
|
||||
columns.entries[slot] = current;
|
||||
columns.roots[slot] = rootEntry;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
private static boolean fixedChildStyle(IrisDimensionCarvingEntry entry) {
|
||||
for (IrisGeneratorStyle style = entry.getChildStyle(); style != null; style = style.getFracture()) {
|
||||
if (style.getExpression() != null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static IrisBiome resolveBoundEntryBiome(IrisData data, IrisDimensionCarvingEntry entry, State state) {
|
||||
if (state.biomeCache.containsKey(entry)) {
|
||||
return state.biomeCache.get(entry);
|
||||
@@ -275,6 +310,31 @@ public final class IrisDimensionCarvingResolver {
|
||||
return replacement;
|
||||
}
|
||||
|
||||
public static final class Snapshot {
|
||||
private final Engine engine;
|
||||
private final State state = new State();
|
||||
private final IrisDimension dimension;
|
||||
private final IrisData data;
|
||||
|
||||
private Snapshot(Engine engine) {
|
||||
this.engine = engine;
|
||||
this.dimension = engine.getDimension();
|
||||
this.data = engine.getData();
|
||||
state.entryIndex = dimension.getCarvingEntryIndex();
|
||||
}
|
||||
|
||||
public IrisBiome resolveBiome(int worldX, int worldY, int worldZ) {
|
||||
IrisDimensionCarvingEntry root = state.rootEntriesByWorldY.containsKey(worldY)
|
||||
? state.rootEntriesByWorldY.get(worldY)
|
||||
: selectBoundRootEntry(dimension, worldY, data, state);
|
||||
if (root == null) {
|
||||
return null;
|
||||
}
|
||||
IrisDimensionCarvingEntry resolved = resolveBoundFromRoot(engine, root, worldX, worldZ, data, state);
|
||||
return resolved == null ? null : resolveBoundEntryBiome(data, resolved, state);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class State {
|
||||
private final Map<Integer, IrisDimensionCarvingEntry> rootEntriesByWorldY = new HashMap<>();
|
||||
private final Map<IrisDimensionCarvingEntry, IrisRaritySelection<CarvingChoice>> selectionPlans = new IdentityHashMap<>();
|
||||
@@ -284,6 +344,7 @@ public final class IrisDimensionCarvingResolver {
|
||||
private WeakReference<IrisData> dataIdentity;
|
||||
private Map<String, IrisDimensionCarvingEntry> entryIndex;
|
||||
private Long childSeed;
|
||||
private ColumnResolutions columns;
|
||||
|
||||
private IrisData bind(Engine engine) {
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
@@ -301,6 +362,7 @@ public final class IrisDimensionCarvingResolver {
|
||||
biomeCache.clear();
|
||||
entryIndex = null;
|
||||
childSeed = null;
|
||||
columns = null;
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -309,6 +371,12 @@ public final class IrisDimensionCarvingResolver {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ColumnResolutions {
|
||||
private final long[] coordinates = new long[1024];
|
||||
private final IrisDimensionCarvingEntry[] roots = new IrisDimensionCarvingEntry[1024];
|
||||
private final IrisDimensionCarvingEntry[] entries = new IrisDimensionCarvingEntry[1024];
|
||||
}
|
||||
|
||||
private static final class CarvingChoice implements Rarity {
|
||||
private final IrisDimensionCarvingEntry entry;
|
||||
private final int rarity;
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package art.arcane.iris.generation.terrain;
|
||||
|
||||
import art.arcane.volmlib.util.cache.CacheKey;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicReferenceArray;
|
||||
import java.util.function.DoubleBinaryOperator;
|
||||
|
||||
public final class ProceduralTerrainHeightSampler {
|
||||
private static final int MAXIMUM_NODES = 262_144;
|
||||
|
||||
private final DoubleBinaryOperator source;
|
||||
private final int step;
|
||||
private final AtomicReferenceArray<Node> nodes;
|
||||
|
||||
public ProceduralTerrainHeightSampler(DoubleBinaryOperator source, int step) {
|
||||
if (step != 1 && step != 2 && step != 4 && step != 8) {
|
||||
throw new IllegalArgumentException("Terrain sampling step must be 1, 2, 4, or 8");
|
||||
}
|
||||
this.source = Objects.requireNonNull(source, "Procedural terrain height source");
|
||||
this.step = step;
|
||||
nodes = step == 1 ? null : new AtomicReferenceArray<>(MAXIMUM_NODES);
|
||||
}
|
||||
|
||||
public double sample(double x, double z) {
|
||||
if (step == 1) {
|
||||
return source.applyAsDouble(x, z);
|
||||
}
|
||||
double lowerX = Math.floor(x / step) * step;
|
||||
double lowerZ = Math.floor(z / step) * step;
|
||||
double dx = (x - lowerX) / step;
|
||||
double dz = (z - lowerZ) / step;
|
||||
double northWest = node(lowerX, lowerZ);
|
||||
double north = dx == 0D ? northWest
|
||||
: northWest + (node(lowerX + step, lowerZ) - northWest) * dx;
|
||||
if (dz == 0D) {
|
||||
return north;
|
||||
}
|
||||
double southWest = node(lowerX, lowerZ + step);
|
||||
double south = dx == 0D ? southWest
|
||||
: southWest + (node(lowerX + step, lowerZ + step) - southWest) * dx;
|
||||
return north + (south - north) * dz;
|
||||
}
|
||||
|
||||
int cachedNodeCount() {
|
||||
if (nodes == null) {
|
||||
return 0;
|
||||
}
|
||||
int count = 0;
|
||||
for (int index = 0; index < nodes.length(); index++) {
|
||||
if (nodes.get(index) != null) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private double node(double x, double z) {
|
||||
long xBits = Double.doubleToLongBits(x);
|
||||
long zBits = Double.doubleToLongBits(z);
|
||||
int index = (int) CacheKey.mix(xBits ^ Long.rotateLeft(zBits, 32)) & (MAXIMUM_NODES - 1);
|
||||
Node cached = nodes.get(index);
|
||||
if (cached != null && cached.xBits() == xBits && cached.zBits() == zBits) {
|
||||
return cached.height();
|
||||
}
|
||||
double height = source.applyAsDouble(x, z);
|
||||
if (!Double.isFinite(height)) {
|
||||
throw new IllegalStateException("Nonfinite procedural terrain height at " + x + "," + z);
|
||||
}
|
||||
nodes.set(index, new Node(xBits, zBits, height));
|
||||
return height;
|
||||
}
|
||||
|
||||
private record Node(long xBits, long zBits, double height) {
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,27 @@ final class Terrain3DFragmentFilter {
|
||||
this.source = Objects.requireNonNull(source, "Raw density columns");
|
||||
}
|
||||
|
||||
double height(int x, int z) {
|
||||
DensityColumn density = source.column(x, z);
|
||||
Terrain3DColumn raw = density.raw;
|
||||
if (!raw.shaped()) {
|
||||
return raw.baseHeight();
|
||||
}
|
||||
Terrain3DColumn resolved = density.resolved;
|
||||
if (resolved != null) {
|
||||
return resolved.topY();
|
||||
}
|
||||
for (int span = raw.spanCount() - 1; span > 0; span--) {
|
||||
if (density.state(span) == UNKNOWN) {
|
||||
TRAVERSALS.get().classify(this, x, z, density, span);
|
||||
}
|
||||
if (density.state(span) == KEEP) {
|
||||
return raw.floor(span);
|
||||
}
|
||||
}
|
||||
return raw.floor(0);
|
||||
}
|
||||
|
||||
Terrain3DColumn column(int x, int z) {
|
||||
DensityColumn density = source.column(x, z);
|
||||
Terrain3DColumn resolved = density.resolved;
|
||||
|
||||
@@ -4,6 +4,7 @@ import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||
import art.arcane.volmlib.util.noise.CNG;
|
||||
import art.arcane.volmlib.util.cache.CacheKey;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
||||
|
||||
@@ -16,6 +17,8 @@ import java.util.concurrent.atomic.AtomicReferenceArray;
|
||||
public final class Terrain3DRuntime {
|
||||
private static final int STEP = 4;
|
||||
private static final int CACHE_STRIPES = 16;
|
||||
private static final int MAXIMUM_ANCHORS = 32_768;
|
||||
private static final long ANCHOR_CACHE_BYTES = 64L * 1024 * 1024;
|
||||
private static final long DENSITY_SALT = 0x536E4A11C924B3D7L;
|
||||
private static final long CRACK_SALT = 0x7839A16DC4052EFBL;
|
||||
private static final NodeSample EMPTY_SAMPLE = new NodeSample(0D, 0D, 0D);
|
||||
@@ -44,7 +47,7 @@ public final class Terrain3DRuntime {
|
||||
this.options = Objects.requireNonNull(options, "Terrain options");
|
||||
this.noiseFactory = Objects.requireNonNull(noiseFactory, "Terrain noise factory");
|
||||
columns = new BoundedCache<>(options.maximumColumns());
|
||||
anchors = new BoundedCache<>(Math.max(CACHE_STRIPES, options.maximumColumns() / 4));
|
||||
anchors = new BoundedCache<>(anchorCacheCapacity(options.height(), options.maximumColumns()));
|
||||
fragments = new Terrain3DFragmentFilter(this::rawColumn);
|
||||
}
|
||||
|
||||
@@ -52,6 +55,24 @@ public final class Terrain3DRuntime {
|
||||
return options.enabled();
|
||||
}
|
||||
|
||||
static int anchorCacheCapacity(int height, int maximumColumns) {
|
||||
long bytesPerAnchor = 160L + 48L * (Math.ceilDiv((long) height, STEP) + 1);
|
||||
long maximumEntries = Math.min(MAXIMUM_ANCHORS,
|
||||
Math.min((long) maximumColumns * 8, ANCHOR_CACHE_BYTES / bytesPerAnchor));
|
||||
return Math.max(CACHE_STRIPES, (int) (maximumEntries / CACHE_STRIPES) * CACHE_STRIPES);
|
||||
}
|
||||
|
||||
public double height(int x, int z) {
|
||||
if (!active()) {
|
||||
return baseHeight(x, z);
|
||||
}
|
||||
ColumnMemo memo = localColumn.get();
|
||||
if (memo.generation == cacheGeneration && memo.key == pack(x, z)) {
|
||||
return memo.column.shaped() ? memo.column.topY() : memo.column.baseHeight();
|
||||
}
|
||||
return fragments.height(x, z);
|
||||
}
|
||||
|
||||
public Terrain3DColumn column(int x, int z) {
|
||||
if (!active()) {
|
||||
return Terrain3DColumn.unshaped(baseHeight(x, z), options.height());
|
||||
@@ -80,6 +101,10 @@ public final class Terrain3DRuntime {
|
||||
localColumn.remove();
|
||||
}
|
||||
|
||||
int cachedColumnCount() {
|
||||
return columns.size();
|
||||
}
|
||||
|
||||
private Terrain3DFragmentFilter.DensityColumn rawColumn(int x, int z) {
|
||||
long key = pack(x, z);
|
||||
Terrain3DFragmentFilter.DensityColumn cached = columns.get(key);
|
||||
@@ -408,11 +433,16 @@ public final class Terrain3DRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
private int size() {
|
||||
int size = 0;
|
||||
for (CacheStripe<T> stripe : stripes) {
|
||||
size += stripe.size();
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
private CacheStripe<T> stripe(long key) {
|
||||
long mixed = key ^ (key >>> 33);
|
||||
mixed *= 0xff51afd7ed558ccdL;
|
||||
mixed ^= mixed >>> 33;
|
||||
return stripes[(int) mixed & (CACHE_STRIPES - 1)];
|
||||
return stripes[(int) CacheKey.mix(key) & (CACHE_STRIPES - 1)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,5 +473,9 @@ public final class Terrain3DRuntime {
|
||||
private synchronized void clear() {
|
||||
entries.clear();
|
||||
}
|
||||
|
||||
private synchronized int size() {
|
||||
return entries.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,9 @@ final class PackDimensionValidator {
|
||||
|
||||
validateImportedStructurePolicy(dimensionKey, dimJson, blockingErrors, warnings);
|
||||
validateDimensionHeights(packFolder, dimensionKey, dimJson, blockingErrors);
|
||||
validateSamplingStep(dimensionKey, dimJson, "terrainSamplingStep", 1, blockingErrors);
|
||||
validateSamplingStep(dimensionKey, dimJson, "caveDensitySamplingStep", 1, blockingErrors);
|
||||
validateSamplingStep(dimensionKey, dimJson, "biomeBoundsSamplingStep", 4, blockingErrors);
|
||||
validateWorldBoundary(dimensionKey, dimJson, blockingErrors);
|
||||
validateStaticObjects(packFolder, dimensionKey, dimJson, blockingErrors);
|
||||
validateObjectScaleFactor(dimensionKey, dimJson, blockingErrors);
|
||||
@@ -109,6 +112,21 @@ final class PackDimensionValidator {
|
||||
IrisObjectScale.MAXIMUM_FACTOR, "Dimension '" + dimensionKey + "'", errors);
|
||||
}
|
||||
|
||||
private static void validateSamplingStep(String dimensionKey, JSONObject dimension, String field, int minimum, List<String> errors) {
|
||||
if (!dimension.has(field)) {
|
||||
return;
|
||||
}
|
||||
Object value = dimension.opt(field);
|
||||
if (value instanceof Number number) {
|
||||
double step = number.doubleValue();
|
||||
if (step == minimum || step == minimum * 2D || step == minimum * 4D || step == minimum * 8D) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
errors.add("Dimension '" + dimensionKey + "' " + field + " must be one of " + minimum + ", "
|
||||
+ minimum * 2 + ", " + minimum * 4 + ", or " + minimum * 8 + ".");
|
||||
}
|
||||
|
||||
static void validateDimensionStack(File packFolder, String dimensionKey, JSONObject dimension, Set<String> dimensionKeys,
|
||||
List<String> blockingErrors) {
|
||||
if (!dimension.has("dimensionStack")) {
|
||||
|
||||
@@ -22,11 +22,13 @@ import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.pack.schema.annotation.ArrayType;
|
||||
import art.arcane.iris.pack.schema.annotation.MaxNumber;
|
||||
import art.arcane.iris.pack.schema.annotation.MinNumber;
|
||||
import art.arcane.iris.pack.schema.annotation.IntegerOptions;
|
||||
import art.arcane.iris.pack.schema.annotation.RegistryMapBlockState;
|
||||
import art.arcane.iris.pack.schema.annotation.Required;
|
||||
import art.arcane.iris.pack.schema.annotation.Snippet;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
@@ -67,6 +69,13 @@ final class SchemaPropertyBuilder {
|
||||
}
|
||||
|
||||
private String integerProperty(Field k, JSONObject prop, KList<String> description) {
|
||||
if (k.isAnnotationPresent(IntegerOptions.class)) {
|
||||
JSONArray options = new JSONArray();
|
||||
for (int value : k.getDeclaredAnnotation(IntegerOptions.class).value()) {
|
||||
options.put(value);
|
||||
}
|
||||
prop.put("enum", options);
|
||||
}
|
||||
if (k.isAnnotationPresent(MinNumber.class)) {
|
||||
int min = (int) k.getDeclaredAnnotation(MinNumber.class).value();
|
||||
prop.put("minimum", min);
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package art.arcane.iris.pack.schema.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.FIELD)
|
||||
public @interface IntegerOptions {
|
||||
int[] value();
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import art.arcane.volmlib.nativelib.terrain.NativeBiomeRegistry;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBiomeSourcePolicy;
|
||||
import java.util.function.IntUnaryOperator;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.generation.runtime.DimensionStackContext;
|
||||
import art.arcane.iris.generation.runtime.DimensionStackLayout;
|
||||
@@ -22,7 +21,6 @@ import art.arcane.iris.platform.generation.BukkitChunkGenerator;
|
||||
import art.arcane.iris.generation.context.IrisContext;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Biome;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -37,6 +35,8 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H> {
|
||||
private static final int NOISE_BIOME_CACHE_MAX = 262144;
|
||||
@@ -49,6 +49,7 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
private final BukkitChunkGenerator platformGenerator;
|
||||
private final NativeBiomeRegistry<H, V> biomeCustomRegistry;
|
||||
private final NativeBiomeRegistry<H, V> biomeRegistry;
|
||||
private final Supplier<NativeBiomeRegistry<H, V>> possibleBiomeRegistry;
|
||||
private final H fallbackBiome;
|
||||
private final ConcurrentHashMap<RuntimeNoiseKey, H> noiseBiomeCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<RuntimeNoiseKey, H> structureBiomeCache = new ConcurrentHashMap<>();
|
||||
@@ -56,13 +57,11 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
private final ConcurrentHashMap<RuntimeColumnKey, H> naturalSurfaceStructureBiomeCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Integer, RuntimeBiomeState<H, V>> runtimeBiomeStates = new ConcurrentHashMap<>();
|
||||
|
||||
public BukkitBiomePolicy(RuntimeOptions options, NativeBiomeRegistry<H, V> registry) {
|
||||
public BukkitBiomePolicy(RuntimeOptions<H, V> options, NativeBiomeRegistry<H, V> registry) {
|
||||
Engine engine = options.engine();
|
||||
World world = options.world();
|
||||
this.engine = engine;
|
||||
this.platformGenerator = world.getGenerator() instanceof BukkitChunkGenerator generator
|
||||
? generator
|
||||
: null;
|
||||
this.platformGenerator = options.platformGenerator();
|
||||
this.possibleBiomeRegistry = options.possibleBiomeRegistry();
|
||||
this.seed = options.seed();
|
||||
this.biomeCustomRegistry = registry;
|
||||
this.biomeRegistry = biomeCustomRegistry;
|
||||
@@ -179,12 +178,8 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
throw new IllegalStateException("Iris possible biome lookup has no active engine runtime");
|
||||
}
|
||||
runtimeBiomeState();
|
||||
World world = BukkitWorldBinding.world(engine.getWorld());
|
||||
if (world == null) {
|
||||
throw new IllegalStateException("Iris biome source has no bound Bukkit world");
|
||||
}
|
||||
NativeBiomeRegistry<H, V> customRegistry = biomeCustomRegistry;
|
||||
NativeBiomeRegistry<H, V> worldRegistry = biomeRegistry.forWorld(world);
|
||||
NativeBiomeRegistry<H, V> worldRegistry = Objects.requireNonNull(possibleBiomeRegistry.get(), "possible biome registry");
|
||||
return Set.copyOf(getAllBiomes(
|
||||
customRegistry, worldRegistry, engine, includeDimensionStack));
|
||||
}
|
||||
@@ -1105,7 +1100,12 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
Map<V, H> vanillaSpawnBiomes
|
||||
) {
|
||||
}
|
||||
public record RuntimeOptions(long seed, Engine engine, World world) {
|
||||
public record RuntimeOptions<H, V>(long seed, Engine engine, BukkitChunkGenerator platformGenerator,
|
||||
Supplier<NativeBiomeRegistry<H, V>> possibleBiomeRegistry) {
|
||||
public RuntimeOptions {
|
||||
Objects.requireNonNull(engine, "engine");
|
||||
Objects.requireNonNull(possibleBiomeRegistry, "possible biome registry");
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public VisibleResolver<H> visibleResolver() {
|
||||
|
||||
@@ -5,6 +5,7 @@ import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
import art.arcane.iris.generation.runtime.EngineLifecycleTasks;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.platform.generation.BukkitChunkGenerator;
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.structure.nativegen.IrisStructurePolicy;
|
||||
import art.arcane.iris.structure.nativegen.ImportedFeaturePolicy;
|
||||
import art.arcane.iris.structure.nativegen.IrisStructureVolumePolicy;
|
||||
@@ -50,7 +51,16 @@ public final class BukkitGeneratorContext extends BukkitGenerationPolicy impleme
|
||||
|
||||
@Override
|
||||
public <H, V> NativeBiomeSourcePolicy<H> biomes(NativeBiomeRegistry<H, V> registry) {
|
||||
return new BukkitBiomePolicy<>(new BukkitBiomePolicy.RuntimeOptions(options.seed(), current(), options.world()), registry);
|
||||
return new BukkitBiomePolicy<>(new BukkitBiomePolicy.RuntimeOptions<>(options.seed(), current(), generator,
|
||||
() -> worldBiomeRegistry(registry)), registry);
|
||||
}
|
||||
|
||||
private <H, V> NativeBiomeRegistry<H, V> worldBiomeRegistry(NativeBiomeRegistry<H, V> registry) {
|
||||
World world = BukkitWorldBinding.world(current().getWorld());
|
||||
if (world == null) {
|
||||
throw new IllegalStateException("Iris biome source has no bound Bukkit world");
|
||||
}
|
||||
return registry.forWorld(world);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
-112
@@ -18,10 +18,6 @@ import java.util.Objects;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.ForkJoinTask;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.IntConsumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@@ -35,8 +31,6 @@ import java.util.function.Supplier;
|
||||
*/
|
||||
public final class NativeStructureVolumeIndex {
|
||||
private static final int ORIGIN_REACH_CHUNKS = StructureOwnershipRecordView.MAX_REFERENCE_DISTANCE_CHUNKS;
|
||||
private static final int ORIGIN_WINDOW_DIAMETER = ORIGIN_REACH_CHUNKS * 2 + 1;
|
||||
private static final int ORIGIN_WINDOW_STRIPE_COUNT = Long.SIZE;
|
||||
private static final int MAX_CACHED_ORIGIN_CHUNKS = 16_384;
|
||||
private static final int MAX_CACHED_QUERY_CHUNKS = 4_096;
|
||||
private static final Map<Engine, NativeStructureVolumeIndex> INDEXES =
|
||||
@@ -49,7 +43,6 @@ public final class NativeStructureVolumeIndex {
|
||||
new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<RuntimeChunkKey, CompletableFuture<KList<NativeStructureVolume>>> queryBuilds =
|
||||
new ConcurrentHashMap<>();
|
||||
private final ReentrantLock[] originWindowStripes = createOriginWindowStripes();
|
||||
private final IntConsumer retirementListener = this::evictRuntime;
|
||||
|
||||
private NativeStructureVolumeIndex(OriginResolver origins) {
|
||||
@@ -147,16 +140,6 @@ public final class NativeStructureVolumeIndex {
|
||||
}
|
||||
|
||||
private KList<NativeStructureVolume> buildChunkVolumes(Engine engine, int chunkX, int chunkZ) {
|
||||
long stripeMask = originWindowStripeMask(chunkX, chunkZ);
|
||||
lockOriginWindow(stripeMask);
|
||||
try {
|
||||
return buildChunkVolumesCoordinated(engine, chunkX, chunkZ);
|
||||
} finally {
|
||||
unlockOriginWindow(stripeMask);
|
||||
}
|
||||
}
|
||||
|
||||
private KList<NativeStructureVolume> buildChunkVolumesCoordinated(Engine engine, int chunkX, int chunkZ) {
|
||||
int minX = chunkX << 4;
|
||||
int minZ = chunkZ << 4;
|
||||
int maxX = minX + 15;
|
||||
@@ -207,20 +190,6 @@ public final class NativeStructureVolumeIndex {
|
||||
return originVolumes(engine, chunkX, chunkZ);
|
||||
}
|
||||
|
||||
static long originWindowStripeMask(int chunkX, int chunkZ) {
|
||||
int minCellX = Math.floorDiv(chunkX - ORIGIN_REACH_CHUNKS, ORIGIN_WINDOW_DIAMETER);
|
||||
int maxCellX = Math.floorDiv(chunkX + ORIGIN_REACH_CHUNKS, ORIGIN_WINDOW_DIAMETER);
|
||||
int minCellZ = Math.floorDiv(chunkZ - ORIGIN_REACH_CHUNKS, ORIGIN_WINDOW_DIAMETER);
|
||||
int maxCellZ = Math.floorDiv(chunkZ + ORIGIN_REACH_CHUNKS, ORIGIN_WINDOW_DIAMETER);
|
||||
long mask = 0L;
|
||||
for (int cellX = minCellX; cellX <= maxCellX; cellX++) {
|
||||
for (int cellZ = minCellZ; cellZ <= maxCellZ; cellZ++) {
|
||||
mask |= 1L << originWindowStripe(cellX, cellZ);
|
||||
}
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
KList<NativeStructureVolume> originVolumes(Engine engine, int chunkX, int chunkZ) {
|
||||
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
|
||||
openHistoryCoordinateScope(engine, chunkX, chunkZ)) {
|
||||
@@ -301,87 +270,6 @@ public final class NativeStructureVolumeIndex {
|
||||
return ((long) chunkX << 32) ^ (chunkZ & 0xffffffffL);
|
||||
}
|
||||
|
||||
private static int originWindowStripe(int cellX, int cellZ) {
|
||||
long mixed = chunkKey(cellX, cellZ);
|
||||
mixed ^= mixed >>> 33;
|
||||
mixed *= 0xff51afd7ed558ccdL;
|
||||
mixed ^= mixed >>> 33;
|
||||
mixed *= 0xc4ceb9fe1a85ec53L;
|
||||
mixed ^= mixed >>> 33;
|
||||
return (int) mixed & (ORIGIN_WINDOW_STRIPE_COUNT - 1);
|
||||
}
|
||||
|
||||
private static ReentrantLock[] createOriginWindowStripes() {
|
||||
ReentrantLock[] stripes = new ReentrantLock[ORIGIN_WINDOW_STRIPE_COUNT];
|
||||
for (int i = 0; i < stripes.length; i++) {
|
||||
stripes[i] = new ReentrantLock();
|
||||
}
|
||||
return stripes;
|
||||
}
|
||||
|
||||
private void lockOriginWindow(long stripeMask) {
|
||||
long pending = stripeMask;
|
||||
while (pending != 0L) {
|
||||
int stripe = Long.numberOfTrailingZeros(pending);
|
||||
lockOriginStripe(originWindowStripes[stripe]);
|
||||
pending &= pending - 1L;
|
||||
}
|
||||
}
|
||||
|
||||
private static void lockOriginStripe(ReentrantLock lock) {
|
||||
if (lock.tryLock()) {
|
||||
return;
|
||||
}
|
||||
if (!ForkJoinTask.inForkJoinPool()) {
|
||||
lock.lock();
|
||||
return;
|
||||
}
|
||||
OriginLockBlocker blocker = new OriginLockBlocker(lock);
|
||||
try {
|
||||
ForkJoinPool.managedBlock(blocker);
|
||||
} catch (InterruptedException interruption) {
|
||||
Thread.currentThread().interrupt();
|
||||
blocker.block();
|
||||
} catch (RejectedExecutionException exhaustedPool) {
|
||||
blocker.block();
|
||||
}
|
||||
}
|
||||
|
||||
private void unlockOriginWindow(long stripeMask) {
|
||||
long pending = stripeMask;
|
||||
while (pending != 0L) {
|
||||
int stripe = Long.SIZE - 1 - Long.numberOfLeadingZeros(pending);
|
||||
originWindowStripes[stripe].unlock();
|
||||
pending &= ~(1L << stripe);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class OriginLockBlocker implements ForkJoinPool.ManagedBlocker {
|
||||
private final ReentrantLock lock;
|
||||
private boolean acquired;
|
||||
|
||||
private OriginLockBlocker(ReentrantLock lock) {
|
||||
this.lock = lock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean block() {
|
||||
if (!acquired) {
|
||||
lock.lock();
|
||||
acquired = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReleasable() {
|
||||
if (!acquired) {
|
||||
acquired = lock.tryLock();
|
||||
}
|
||||
return acquired;
|
||||
}
|
||||
}
|
||||
|
||||
private static GenerationHistoryRuntimeRouter.CoordinateScope openHistoryCoordinateScope(
|
||||
Engine engine,
|
||||
int chunkX,
|
||||
|
||||
@@ -583,7 +583,7 @@ public final class ImageMapStudioGUI {
|
||||
&& !engine.isClosed()
|
||||
&& engine.getComplex() != null) {
|
||||
proceduralHeightSampler = (worldX, worldZ) -> engine.getComplex()
|
||||
.sampleProceduralTerrainHeight(engine, worldX, worldZ);
|
||||
.sampleProceduralTerrainHeight(worldX, worldZ);
|
||||
}
|
||||
DoubleBinaryOperator currentProceduralHeightSampler = proceduralHeightSampler;
|
||||
new SwingWorker<ImageMapStudioExporter.PreviewResult, Void>() {
|
||||
|
||||
@@ -163,13 +163,14 @@ public final class BoundaryColumnGeometry implements NativeBlockColumn {
|
||||
}
|
||||
int nearest = -1;
|
||||
double nearestDistance = Double.POSITIVE_INFINITY;
|
||||
for (int offset = 0; offset + 1 < height(); offset++) {
|
||||
Voxel current = voxelAt(minimumY + offset);
|
||||
Voxel above = voxelAt(minimumY + offset + 1);
|
||||
for (int run = 0; run + 1 < runEnds.length; run++) {
|
||||
Voxel current = palette.get(paletteIndices[run]);
|
||||
Voxel above = palette.get(paletteIndices[run + 1]);
|
||||
if (current.phase() != Phase.SOLID || current.protectedContent()
|
||||
|| above.phase() == Phase.SOLID && !above.protectedContent()) {
|
||||
continue;
|
||||
}
|
||||
int offset = runEnds[run] - 1;
|
||||
double distance = Math.abs(offset - expectedOffset);
|
||||
if (distance < nearestDistance) {
|
||||
nearest = offset;
|
||||
@@ -179,10 +180,10 @@ public final class BoundaryColumnGeometry implements NativeBlockColumn {
|
||||
if (nearest >= 0) {
|
||||
return nearest;
|
||||
}
|
||||
for (int offset = height() - 1; offset >= 0; offset--) {
|
||||
Voxel current = voxelAt(minimumY + offset);
|
||||
for (int run = runEnds.length - 1; run >= 0; run--) {
|
||||
Voxel current = palette.get(paletteIndices[run]);
|
||||
if (current.phase() == Phase.SOLID && !current.protectedContent()) {
|
||||
return offset;
|
||||
return runEnds[run] - 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
|
||||
+1
-1
@@ -1415,7 +1415,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
|
||||
|| router.history.resolveActivation(chunkX(), chunkZ()).activationId() != stage.activation().activationId()) {
|
||||
return false;
|
||||
}
|
||||
ChunkGenerationSemantics semantics = GenerationSemanticCapture.capture(
|
||||
ChunkGenerationSemantics semantics = GenerationSemanticCapture.captureScoped(
|
||||
router.engine,
|
||||
stage,
|
||||
caveSpace
|
||||
|
||||
@@ -3,6 +3,10 @@ package art.arcane.iris.world.history;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockPositionPredicate;
|
||||
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
import art.arcane.iris.generation.runtime.DimensionStackContext;
|
||||
import art.arcane.iris.generation.runtime.DimensionStackLayout;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.structure.placement.IrisStructureLocator;
|
||||
import art.arcane.iris.structure.placement.StructurePlacementMarker;
|
||||
@@ -12,6 +16,7 @@ import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.generation.terrain.IrisDimensionCarvingResolver;
|
||||
import art.arcane.iris.structure.placement.IrisStructurePlacement;
|
||||
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
@@ -28,6 +33,14 @@ public final class GenerationSemanticCapture {
|
||||
private GenerationSemanticCapture() {
|
||||
}
|
||||
|
||||
static ChunkGenerationSemantics captureScoped(
|
||||
Engine engine,
|
||||
GenerationHistory.GenerationStage stage,
|
||||
NativeBlockPositionPredicate caveSpace
|
||||
) {
|
||||
return capture(engine, stage.chunkX(), stage.chunkZ(), stage.activation().activationId(), caveSpace, true);
|
||||
}
|
||||
|
||||
public static ChunkGenerationSemantics capture(
|
||||
Engine engine,
|
||||
GenerationHistory.GenerationStage stage
|
||||
@@ -47,7 +60,8 @@ public final class GenerationSemanticCapture {
|
||||
requiredStage.chunkX(),
|
||||
requiredStage.chunkZ(),
|
||||
requiredStage.activation().activationId(),
|
||||
caveSpace
|
||||
caveSpace,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,7 +71,7 @@ public final class GenerationSemanticCapture {
|
||||
int chunkZ,
|
||||
long activationId
|
||||
) {
|
||||
return capture(engine, chunkX, chunkZ, activationId, (x, y, z) -> true);
|
||||
return capture(engine, chunkX, chunkZ, activationId, (x, y, z) -> true, false);
|
||||
}
|
||||
|
||||
private static ChunkGenerationSemantics capture(
|
||||
@@ -65,7 +79,8 @@ public final class GenerationSemanticCapture {
|
||||
int chunkX,
|
||||
int chunkZ,
|
||||
long activationId,
|
||||
NativeBlockPositionPredicate caveSpace
|
||||
NativeBlockPositionPredicate caveSpace,
|
||||
boolean scoped
|
||||
) {
|
||||
Objects.requireNonNull(caveSpace, "cave space");
|
||||
Engine requiredEngine = Objects.requireNonNull(engine, "engine");
|
||||
@@ -75,7 +90,7 @@ public final class GenerationSemanticCapture {
|
||||
activationId
|
||||
);
|
||||
captureColumns(requiredEngine, chunkX, chunkZ, semantics);
|
||||
captureMantleFacts(requiredEngine, chunkX, chunkZ, semantics, caveSpace);
|
||||
captureMantleFacts(requiredEngine, chunkX, chunkZ, semantics, caveSpace, scoped);
|
||||
captureStructures(requiredEngine, chunkX, chunkZ, semantics);
|
||||
return semantics.seal().build();
|
||||
}
|
||||
@@ -133,18 +148,20 @@ public final class GenerationSemanticCapture {
|
||||
int chunkX,
|
||||
int chunkZ,
|
||||
ChunkGenerationSemantics.Builder semantics,
|
||||
NativeBlockPositionPredicate caveSpace
|
||||
NativeBlockPositionPredicate caveSpace,
|
||||
boolean scoped
|
||||
) {
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
int minimumX = Math.multiplyExact(chunkX, CHUNK_SIZE);
|
||||
int minimumZ = Math.multiplyExact(chunkZ, CHUNK_SIZE);
|
||||
BitSet resolvedCaves = new BitSet();
|
||||
CaveBiomes caveBiomes = new CaveBiomes(engine, scoped);
|
||||
mantle.iterateChunk(chunkX, chunkZ, MatterCavern.class, (localX, y, localZ, cavern) -> {
|
||||
if (!caveSpace.test(localX, y, localZ)) {
|
||||
return;
|
||||
}
|
||||
captureCave(
|
||||
engine,
|
||||
caveBiomes,
|
||||
semantics,
|
||||
resolvedCaves,
|
||||
minimumX + localX,
|
||||
@@ -163,7 +180,7 @@ public final class GenerationSemanticCapture {
|
||||
return;
|
||||
}
|
||||
captureCave(
|
||||
engine,
|
||||
caveBiomes,
|
||||
semantics,
|
||||
resolvedCaves,
|
||||
blockX,
|
||||
@@ -196,7 +213,7 @@ public final class GenerationSemanticCapture {
|
||||
}
|
||||
|
||||
private static void captureCave(
|
||||
Engine engine,
|
||||
CaveBiomes caveBiomes,
|
||||
ChunkGenerationSemantics.Builder semantics,
|
||||
BitSet resolvedCaves,
|
||||
int blockX,
|
||||
@@ -213,7 +230,7 @@ public final class GenerationSemanticCapture {
|
||||
semantics.addCaveBiome(explicitBiomeKey);
|
||||
return;
|
||||
}
|
||||
IrisBiome caveBiome = engine.getCaveBiome(blockX, y, blockZ);
|
||||
IrisBiome caveBiome = caveBiomes.resolve(blockX, y, blockZ);
|
||||
if (caveBiome != null && caveBiome.getLoadKey() != null) {
|
||||
semantics.addCaveBiome(caveBiome.getLoadKey());
|
||||
}
|
||||
@@ -256,4 +273,80 @@ public final class GenerationSemanticCapture {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class CaveBiomes {
|
||||
private final Engine engine;
|
||||
private final boolean scoped;
|
||||
private final CaveFallback fallback;
|
||||
private final IrisDimensionCarvingResolver.State fallbackState = new IrisDimensionCarvingResolver.State();
|
||||
private IrisDimensionCarvingResolver.Snapshot snapshot;
|
||||
private int minimumY;
|
||||
|
||||
private CaveBiomes(Engine engine, boolean scoped) {
|
||||
this.engine = engine;
|
||||
this.scoped = scoped;
|
||||
this.fallback = scoped && engine instanceof IrisEngine irisEngine
|
||||
&& irisEngine.hasGenerationRuntimeScope() && !engine.getPlatformHooks().isMainThread()
|
||||
? new CaveFallback(engine.getComplex(), engine.getDimensionStackContext())
|
||||
: null;
|
||||
}
|
||||
|
||||
private IrisBiome resolve(int x, int y, int z) {
|
||||
if (!scoped) {
|
||||
return engine.getCaveBiome(x, y, z);
|
||||
}
|
||||
if (snapshot == null) {
|
||||
snapshot = IrisDimensionCarvingResolver.snapshot(engine);
|
||||
minimumY = engine.getWorld().minHeight();
|
||||
}
|
||||
IrisBiome configured = snapshot.resolveBiome(x, y + minimumY, z);
|
||||
if (configured != null) {
|
||||
return configured;
|
||||
}
|
||||
return fallback == null ? engine.getCaveBiome(x, y, z, fallbackState) : fallback.resolve(x, y, z);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class CaveFallback {
|
||||
private final ProceduralStream<IrisBiome> surfaceBiomes;
|
||||
private final ProceduralStream<IrisBiome> caveBiomes;
|
||||
private final ProceduralStream<Double> heights;
|
||||
private final DimensionStackContext stack;
|
||||
private final CaveColumn[] columns = new CaveColumn[CHUNK_SIZE * CHUNK_SIZE];
|
||||
|
||||
private CaveFallback(IrisComplex complex, DimensionStackContext stack) {
|
||||
this.surfaceBiomes = complex.getTrueBiomeStream();
|
||||
this.caveBiomes = complex.getCaveBiomeStream();
|
||||
this.heights = complex.getHeightStream();
|
||||
this.stack = stack;
|
||||
}
|
||||
|
||||
private IrisBiome resolve(int x, int y, int z) {
|
||||
int index = ((x & 15) << 4) | (z & 15);
|
||||
CaveColumn column = columns[index];
|
||||
if (column == null) {
|
||||
IrisBiome surfaceBiome = surfaceBiomes.get(x, z);
|
||||
int surfaceY = heights.get(x, z).intValue();
|
||||
IrisBiome caveBiome = caveBiomes.get(x, z);
|
||||
if (caveBiome == null || caveBiome.getLoadKey() == null) {
|
||||
caveBiome = surfaceBiome;
|
||||
if (stack != null) {
|
||||
DimensionStackLayout.Layer layer = stack.getLayout(x, z).surfaceLayer();
|
||||
if (layer != null && layer.biome() != null) {
|
||||
caveBiome = layer.biome();
|
||||
}
|
||||
}
|
||||
}
|
||||
column = new CaveColumn(surfaceBiome, caveBiome, surfaceY,
|
||||
caveBiome == null ? 0 : Math.max(0, caveBiome.getCaveMinDepthBelowSurface()));
|
||||
columns[index] = column;
|
||||
}
|
||||
int depth = column.surfaceY() - y;
|
||||
return column.caveBiome() == null || depth <= 0 || depth < column.minimumDepth()
|
||||
? column.surfaceBiome() : column.caveBiome();
|
||||
}
|
||||
}
|
||||
|
||||
private record CaveColumn(IrisBiome surfaceBiome, IrisBiome caveBiome, int surfaceY, int minimumDepth) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -106,7 +106,8 @@ public final class SavedBiomeStore {
|
||||
public boolean claimAndPersist(SavedBiomeChunk chunk) throws IOException {
|
||||
SavedBiomeChunk required = Objects.requireNonNull(chunk, "chunk");
|
||||
WriteStripe lock = regionLock(required.chunkX(), required.chunkZ());
|
||||
if (!lock.writing) {
|
||||
requireWritable();
|
||||
if (cached(required.chunkX(), required.chunkZ()).isPresent()) {
|
||||
synchronized (lock) {
|
||||
if (alreadyClaimed(required)) {
|
||||
return false;
|
||||
@@ -119,16 +120,9 @@ public final class SavedBiomeStore {
|
||||
PendingWrite pending = new PendingWrite(required, record);
|
||||
lock.pending.add(pending);
|
||||
synchronized (lock) {
|
||||
if (!pending.completed) {
|
||||
lock.writing = true;
|
||||
try {
|
||||
while (!pending.completed) {
|
||||
persistPending(lock);
|
||||
}
|
||||
} finally {
|
||||
lock.writing = false;
|
||||
}
|
||||
}
|
||||
if (pending.failure != null) {
|
||||
throw pending.failure;
|
||||
}
|
||||
@@ -788,7 +782,6 @@ public final class SavedBiomeStore {
|
||||
|
||||
private static final class WriteStripe {
|
||||
private final ConcurrentLinkedQueue<PendingWrite> pending = new ConcurrentLinkedQueue<>();
|
||||
private volatile boolean writing;
|
||||
}
|
||||
|
||||
private static final class PendingWrite {
|
||||
|
||||
@@ -79,6 +79,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
private final int workerPoolThreads;
|
||||
private final int runtimeCpuThreads;
|
||||
private final int effectiveWorkerThreads;
|
||||
private final int pregenWorkerThreads;
|
||||
private final int recommendedRuntimeConcurrencyCap;
|
||||
private final Method directChunkAtAsyncUrgentMethod;
|
||||
private final Method directChunkAtAsyncMethod;
|
||||
@@ -141,10 +142,11 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
this.chunkAccessMode = chunkAsyncMethodSelection.mode();
|
||||
int detectedWorkerPoolThreads = resolveWorkerPoolThreads();
|
||||
int detectedCpuThreads = Math.max(1, Runtime.getRuntime().availableProcessors());
|
||||
int configuredWorldGenThreads = Math.max(1, IrisSettings.get().getConcurrency().getWorldGenThreads());
|
||||
this.pregenWorkerThreads = pregenWorkerThreadTarget(detectedCpuThreads, Runtime.getRuntime().maxMemory(),
|
||||
!foliaRuntime && !strictSerial && detectedWorkerPoolThreads > 0);
|
||||
int workerThreadsForCap = foliaRuntime
|
||||
? resolveFoliaConcurrencyWorkerThreads(detectedWorkerPoolThreads, detectedCpuThreads, configuredWorldGenThreads)
|
||||
: resolvePaperLikeConcurrencyWorkerThreads(detectedWorkerPoolThreads, detectedCpuThreads, configuredWorldGenThreads);
|
||||
? resolveFoliaConcurrencyWorkerThreads(detectedWorkerPoolThreads, detectedCpuThreads, pregenWorkerThreads)
|
||||
: resolvePaperLikeConcurrencyWorkerThreads(detectedWorkerPoolThreads, detectedCpuThreads, pregenWorkerThreads);
|
||||
if (foliaRuntime) {
|
||||
this.paperLikeBackendMode = IrisPaperLikeBackendMode.AUTO;
|
||||
this.backendMode = "folia-region";
|
||||
@@ -650,14 +652,22 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
}
|
||||
|
||||
static int computeInitialInFlightLimit(int concurrencyCap, int workerThreads) {
|
||||
int initial = Math.max(4, Math.max(1, workerThreads));
|
||||
return Math.min(Math.max(1, concurrencyCap), initial);
|
||||
long initial = Math.max(1L, workerThreads) * 8L;
|
||||
return (int) Math.min(Math.max(1, concurrencyCap), initial);
|
||||
}
|
||||
|
||||
static int nextAdaptiveInFlightLimit(int current, int concurrencyCap) {
|
||||
return Math.min(Math.max(1, concurrencyCap), Math.max(1, current) + 1);
|
||||
}
|
||||
|
||||
static int pregenWorkerThreadTarget(int availableProcessors, long maximumHeapBytes, boolean expanded) {
|
||||
int baseline = Math.max(2, availableProcessors);
|
||||
if (!expanded || maximumHeapBytes < (16L << 30)) {
|
||||
return baseline;
|
||||
}
|
||||
return Math.max(baseline, (int) Math.min(32L, (long) baseline * 2L));
|
||||
}
|
||||
|
||||
static int resolvePaperLikeConcurrencyWorkerThreads(int detectedWorkerPoolThreads, int detectedCpuThreads, int configuredWorldGenThreads) {
|
||||
int provisionedWorkerThreads = Math.max(1, configuredWorldGenThreads);
|
||||
if (detectedWorkerPoolThreads > 0) {
|
||||
@@ -804,8 +814,10 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
+ ", recommendedCap=" + recommendedRuntimeConcurrencyCap
|
||||
+ ", urgent=" + urgent
|
||||
+ ", slowWarning=" + slowRequestWarningSeconds + "s");
|
||||
if (workerPoolThreads > 0 && holdsWorkerBoost.compareAndSet(false, true)) {
|
||||
acquireWorkerThreadBoost();
|
||||
synchronized (this) {
|
||||
if (!closing.get() && workerPoolThreads > 0 && holdsWorkerBoost.compareAndSet(false, true)) {
|
||||
acquireWorkerThreadBoost(pregenWorkerThreads);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -837,20 +849,28 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
flushAllRemainingChunks();
|
||||
chunkFlush.close(CHUNK_FLUSH_DRAIN_SECONDS, TimeUnit.SECONDS);
|
||||
executor.shutdown();
|
||||
if (holdsWorkerBoost.compareAndSet(true, false)) {
|
||||
releaseWorkerThreadBoost();
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
HydrologyTileCache.PregenerationScope scope = hydrologyPrefetchScope;
|
||||
if (scope != null) {
|
||||
scope.close();
|
||||
hydrologyPrefetchScope = null;
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
synchronized (this) {
|
||||
if (holdsWorkerBoost.compareAndSet(true, false)) {
|
||||
releaseWorkerThreadBoost();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isCancelled() {
|
||||
return closing.get() || Thread.currentThread().isInterrupted();
|
||||
@@ -1007,62 +1027,51 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void acquireWorkerThreadBoost() {
|
||||
if (BOOST_HOLDERS.getAndIncrement() > 0) {
|
||||
return;
|
||||
public static synchronized void acquireWorkerThreadBoost(int targetThreads) {
|
||||
if (targetThreads < 1) {
|
||||
throw new IllegalArgumentException("Worker thread target must be positive");
|
||||
}
|
||||
increaseWorkerThreads();
|
||||
BOOST_HOLDERS.incrementAndGet();
|
||||
increaseWorkerThreads(targetThreads);
|
||||
}
|
||||
|
||||
public static void releaseWorkerThreadBoost() {
|
||||
if (BOOST_HOLDERS.decrementAndGet() > 0) {
|
||||
public static synchronized void releaseWorkerThreadBoost() {
|
||||
if (BOOST_HOLDERS.get() == 0 || BOOST_HOLDERS.decrementAndGet() > 0) {
|
||||
return;
|
||||
}
|
||||
resetWorkerThreads();
|
||||
}
|
||||
|
||||
private static void increaseWorkerThreads() {
|
||||
THREAD_COUNT.updateAndGet(i -> {
|
||||
if (i > 0) {
|
||||
return i;
|
||||
}
|
||||
|
||||
int adjusted = IrisSettings.get().getConcurrency().getWorldGenThreads();
|
||||
private static void increaseWorkerThreads(int targetThreads) {
|
||||
try {
|
||||
NativeWorkerPool pool = workerPool();
|
||||
int threads = pool.threadCount();
|
||||
if (threads >= adjusted) {
|
||||
return 0;
|
||||
if (threads >= targetThreads) {
|
||||
return;
|
||||
}
|
||||
|
||||
pool.adjustThreadCount(adjusted);
|
||||
return threads;
|
||||
if (THREAD_COUNT.get() == 0) {
|
||||
THREAD_COUNT.set(threads);
|
||||
}
|
||||
pool.adjustThreadCount(targetThreads);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.warn("Failed to increase worker threads, if you are on paper or a fork of it please increase it manually to " + adjusted);
|
||||
IrisLogging.warn("For more information see https://docs.papermc.io/paper/reference/global-configuration#chunk_system_worker_threads");
|
||||
if (e instanceof InvocationTargetException) {
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.warn("Failed to increase worker threads, if you are on paper or a fork of it please increase it manually to " + targetThreads);
|
||||
IrisLogging.warn("For more information see https://docs.papermc.io/paper/reference/global-configuration#chunk_system_worker_threads");
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
private static void resetWorkerThreads() {
|
||||
THREAD_COUNT.updateAndGet(i -> {
|
||||
if (i == 0) {
|
||||
return 0;
|
||||
int original = THREAD_COUNT.get();
|
||||
if (original == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
workerPool().adjustThreadCount(i);
|
||||
return 0;
|
||||
workerPool().adjustThreadCount(original);
|
||||
THREAD_COUNT.set(0);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.error("Failed to reset worker threads");
|
||||
}
|
||||
return i;
|
||||
});
|
||||
}
|
||||
|
||||
private static NativeWorkerPool workerPool() {
|
||||
|
||||
@@ -11,7 +11,6 @@ import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
@@ -28,7 +27,6 @@ public class IrisBiomeCeilingLayerTest {
|
||||
new IrisDimension(), 13, -27, new RNG(1), 8, 64, null, null);
|
||||
|
||||
assertEquals(List.of(first, first, second, third, third), result);
|
||||
assertNull(biome.getLayerHeightGenerators().getIfPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -45,8 +43,8 @@ public class IrisBiomeCeilingLayerTest {
|
||||
assertEquals(firstCeiling.size(), secondCeiling.size());
|
||||
}
|
||||
first.getLayerHeightGenerators(rng, null);
|
||||
assertNotSame(first.getLayerHeightGenerators().getIfPresent().get(0),
|
||||
first.getLayerCeilingHeightGenerators().getIfPresent().get(0));
|
||||
assertNotSame(first.getLayerHeightGenerators(rng, null).get(0),
|
||||
first.getLayerCeilingHeightGenerators().get(first.getCaveCeilingLayers(), 7235, rng, null).get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
package art.arcane.iris.generation.biome;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.noise.CNG;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
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 static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisBiomeLayerHeightCacheTest {
|
||||
private static final long TERRAIN_SEED = 7191L;
|
||||
private static final long CARVE_SEED = -872315L;
|
||||
private static final long FLOATING_SEED = (TERRAIN_SEED ^ 0x7EB0A73F1DCE514DL)
|
||||
+ (0x7A4E ^ "magnetics/glass-shard".hashCode());
|
||||
|
||||
@Test
|
||||
public void layerHeightUsesEachCallerSeedInEitherOrder() {
|
||||
IrisBiomePaletteLayer layer = layer();
|
||||
assertNoise(layer().getHeightGenerator(new RNG(CARVE_SEED), null),
|
||||
layer.getHeightGenerator(new RNG(CARVE_SEED), null));
|
||||
assertNoise(layer().getHeightGenerator(new RNG(FLOATING_SEED), null),
|
||||
layer.getHeightGenerator(new RNG(FLOATING_SEED), null));
|
||||
IrisBiomePaletteLayer reversed = layer();
|
||||
assertNoise(layer.getHeightGenerator(new RNG(FLOATING_SEED), null),
|
||||
reversed.getHeightGenerator(new RNG(FLOATING_SEED), null));
|
||||
assertNoise(layer.getHeightGenerator(new RNG(CARVE_SEED), null),
|
||||
reversed.getHeightGenerator(new RNG(CARVE_SEED), null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sharedSurfaceCarveAndFloatingBiomeIgnoresWarmOrder() {
|
||||
IrisBiome biome = biome();
|
||||
long[] seeds = {FLOATING_SEED, CARVE_SEED, TERRAIN_SEED, FLOATING_SEED + 733L};
|
||||
for (long seed : seeds) {
|
||||
assertGenerators(biome().getLayerHeightGenerators(new RNG(seed), null),
|
||||
biome.getLayerHeightGenerators(new RNG(seed), null));
|
||||
}
|
||||
IrisBiome reversed = biome();
|
||||
for (int i = seeds.length - 1; i >= 0; i--) {
|
||||
assertGenerators(biome.getLayerHeightGenerators(new RNG(seeds[i]), null),
|
||||
reversed.getLayerHeightGenerators(new RNG(seeds[i]), null));
|
||||
}
|
||||
assertNotEquals(biome.getLayerHeightGenerators(new RNG(TERRAIN_SEED), null).get(0).noise(13, -29),
|
||||
biome.getLayerHeightGenerators(new RNG(FLOATING_SEED), null).get(0).noise(13, -29), 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentSurfaceCarveAndFloatingCallersMatchIsolatedBiomes() throws Exception {
|
||||
IrisBiome shared = biome();
|
||||
ExecutorService executor = Executors.newFixedThreadPool(8);
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
List<Future<?>> tasks = new ArrayList<>();
|
||||
long[] seeds = {TERRAIN_SEED, CARVE_SEED, FLOATING_SEED, FLOATING_SEED + 733L};
|
||||
try {
|
||||
for (int worker = 0; worker < 8; worker++) {
|
||||
long seed = seeds[worker % seeds.length];
|
||||
KList<CNG> expected = biome().getLayerHeightGenerators(new RNG(seed), null);
|
||||
tasks.add(executor.submit(() -> {
|
||||
assertTrue(start.await(5, TimeUnit.SECONDS));
|
||||
for (int sample = 0; sample < 30; sample++) {
|
||||
assertGenerators(expected, shared.getLayerHeightGenerators(new RNG(seed), null));
|
||||
}
|
||||
return null;
|
||||
}));
|
||||
}
|
||||
start.countDown();
|
||||
for (Future<?> task : tasks) {
|
||||
task.get(10, TimeUnit.SECONDS);
|
||||
}
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runtimeAndDataIdentitiesDoNotReuseHeightGenerators() {
|
||||
IrisBiome biome = biome();
|
||||
IrisData data = mock(IrisData.class);
|
||||
IrisData otherData = mock(IrisData.class);
|
||||
Engine first = mock(Engine.class);
|
||||
Engine second = mock(Engine.class);
|
||||
when(data.getEngine()).thenReturn(first);
|
||||
when(otherData.getEngine()).thenReturn(first);
|
||||
RNG rng = new RNG(TERRAIN_SEED);
|
||||
KList<CNG> original = biome.getLayerHeightGenerators(rng, data);
|
||||
assertSame(original, biome.getLayerHeightGenerators(rng, data));
|
||||
KList<CNG> other = biome.getLayerHeightGenerators(rng, otherData);
|
||||
assertNotSame(original, other);
|
||||
assertNotSame(original.get(0), other.get(0));
|
||||
assertGenerators(original, other);
|
||||
when(data.getEngine()).thenReturn(second);
|
||||
KList<CNG> replacement = biome.getLayerHeightGenerators(rng, data);
|
||||
assertNotSame(original, replacement);
|
||||
assertNotSame(original.get(0), replacement.get(0));
|
||||
assertGenerators(original, replacement);
|
||||
when(data.getEngine()).thenReturn(first);
|
||||
assertSame(original, biome.getLayerHeightGenerators(rng, data));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void materialBandsMatchIsolatedGenerationAcrossSurfaceSeaAndCeiling() {
|
||||
NativeBlockState first = mock(NativeBlockState.class);
|
||||
NativeBlockState second = mock(NativeBlockState.class);
|
||||
IrisBiome shared = materialBiome(first, second);
|
||||
IrisDimension dimension = new IrisDimension();
|
||||
long[] seeds = {FLOATING_SEED, TERRAIN_SEED, CARVE_SEED, FLOATING_SEED + 733L};
|
||||
for (long seed : seeds) {
|
||||
IrisBiome isolated = materialBiome(first, second);
|
||||
RNG rng = new RNG(seed);
|
||||
for (int x = -65; x <= 65; x += 7) {
|
||||
assertEquals(isolated.generateLayers(dimension, x, -x, rng, 64, 120, null, null),
|
||||
shared.generateLayers(dimension, x, -x, rng, 64, 120, null, null));
|
||||
assertEquals(isolated.generateCeilingLayers(dimension, x, -x, rng, 64, 120, null, null),
|
||||
shared.generateCeilingLayers(dimension, x, -x, rng, 64, 120, null, null));
|
||||
assertEquals(isolated.generateSeaLayers(x, -x, rng, 64, null),
|
||||
shared.generateSeaLayers(x, -x, rng, 64, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void evictionRecomputesTheSameNoise() {
|
||||
IrisBiome biome = biome();
|
||||
KList<CNG> original = biome.getLayerHeightGenerators(new RNG(TERRAIN_SEED), null);
|
||||
for (int seed = 0; seed < 96; seed++) {
|
||||
biome.getLayerHeightGenerators(new RNG(seed), null);
|
||||
}
|
||||
KList<CNG> recomputed = biome.getLayerHeightGenerators(new RNG(TERRAIN_SEED), null);
|
||||
assertNotSame(original, recomputed);
|
||||
assertNotSame(original.get(0), recomputed.get(0));
|
||||
assertGenerators(original, recomputed);
|
||||
}
|
||||
|
||||
private static IrisBiomePaletteLayer layer() {
|
||||
return new IrisBiomePaletteLayer().zero().setMinHeight(1).setMaxHeight(12);
|
||||
}
|
||||
|
||||
private static IrisBiome biome() {
|
||||
return new IrisBiome().setLayers(new KList<>(layer(), layer().setMinHeight(3).setMaxHeight(5)));
|
||||
}
|
||||
|
||||
private static IrisBiome materialBiome(NativeBlockState first, NativeBlockState second) {
|
||||
IrisBiomePaletteLayer upper = IrisBiomeCeilingLayerTest.layer(first, 1).setMaxHeight(5);
|
||||
IrisBiomePaletteLayer lower = IrisBiomeCeilingLayerTest.layer(second, 3).setMaxHeight(12);
|
||||
KList<IrisBiomePaletteLayer> layers = new KList<>(upper, lower);
|
||||
return new IrisBiome().setLayers(layers).setCaveCeilingLayers(layers).setSeaLayers(layers);
|
||||
}
|
||||
|
||||
private static void assertGenerators(KList<CNG> expected, KList<CNG> actual) {
|
||||
assertEquals(expected.size(), actual.size());
|
||||
for (int index = 0; index < expected.size(); index++) {
|
||||
assertNoise(expected.get(index), actual.get(index));
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertNoise(CNG expected, CNG actual) {
|
||||
for (int x = -97; x <= 97; x += 13) {
|
||||
assertEquals(expected.noise(x, x * -3.25), actual.noise(x, x * -3.25), 0D);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package art.arcane.iris.generation.chunk;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitBiome;
|
||||
import art.arcane.iris.testsupport.BukkitTestServer;
|
||||
import art.arcane.iris.world.history.BoundaryColumnGeometry;
|
||||
import art.arcane.iris.world.history.NativeTerrainReceipt;
|
||||
import art.arcane.iris.world.history.SavedTerrainChunk;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
|
||||
import io.papermc.paper.InternalAPIBridge;
|
||||
import io.papermc.paper.registry.RegistryAccess;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Registry;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.generator.ChunkGenerator.ChunkData;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.RETURNS_DEFAULTS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class LinkedTerrainChunkTest {
|
||||
@BeforeClass
|
||||
public static void initializeBiomes() throws Exception {
|
||||
BukkitTestServer.install();
|
||||
AtomicReference<Object> registered = new AtomicReference<>();
|
||||
RegistryAccess access = mock(RegistryAccess.class, invocation -> {
|
||||
if (invocation.getMethod().getName().equals("getRegistry")) {
|
||||
if (registered.get() == null) {
|
||||
registered.set(Proxy.newProxyInstance(Registry.class.getClassLoader(), new Class<?>[]{Registry.class},
|
||||
(proxy, method, arguments) -> method.getName().equals("getOrThrow")
|
||||
? biome((Key) arguments[0]) : null));
|
||||
}
|
||||
return registered.get();
|
||||
}
|
||||
return RETURNS_DEFAULTS.answer(invocation);
|
||||
});
|
||||
InternalAPIBridge bridge = mock(InternalAPIBridge.class, invocation ->
|
||||
invocation.getMethod().getName().equals("constructLegacyCustomBiome")
|
||||
? biome(Key.key("minecraft:custom")) : RETURNS_DEFAULTS.answer(invocation));
|
||||
try (MockedStatic<RegistryAccess> registries = mockStatic(RegistryAccess.class);
|
||||
MockedStatic<InternalAPIBridge> internals = mockStatic(InternalAPIBridge.class)) {
|
||||
registries.when(RegistryAccess::registryAccess).thenReturn(access);
|
||||
internals.when(InternalAPIBridge::get).thenReturn(bridge);
|
||||
Class.forName(Registry.class.getName());
|
||||
Class.forName(Biome.class.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void biomeReadsReuseCanonicalHandlesAndPreserveReceiptBytes() throws Exception {
|
||||
LinkedTerrainChunk chunk = chunk();
|
||||
NativeBiome plains = BukkitBiome.of(Biome.PLAINS);
|
||||
NativeBiome desert = BukkitBiome.of(Biome.DESERT);
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
chunk.fillBiomeColumn(x, z, plains);
|
||||
}
|
||||
}
|
||||
NativeBiome alternate = mock(NativeBiome.class);
|
||||
when(alternate.nativeHandle()).thenReturn(Biome.DESERT);
|
||||
when(alternate.key()).thenReturn("ignored:alias");
|
||||
chunk.setBiome(0, -12, 0, alternate);
|
||||
chunk.setBiome(17, 100, -1, desert);
|
||||
AtomicInteger conversions = new AtomicInteger();
|
||||
try (MockedStatic<BukkitBiome> wrappers = mockStatic(BukkitBiome.class, invocation -> {
|
||||
conversions.incrementAndGet();
|
||||
return CALLS_REAL_METHODS.answer(invocation);
|
||||
})) {
|
||||
assertSame(desert, chunk.getBiome(0, -12, 0));
|
||||
assertSame(desert, chunk.getBiome(1, -1, 15));
|
||||
assertSame(plains, chunk.getBiome(0, -100, 0));
|
||||
for (boolean boundary : new boolean[]{false, true}) {
|
||||
SavedTerrainChunk.VoxelSource actual = source((x, y, z) -> chunk.getBiome(x, y, z).key());
|
||||
SavedTerrainChunk.VoxelSource expected = source((x, y, z) ->
|
||||
x == 0 && y == -12 && z == 0 || x == 1 && y == -1 && z == 15
|
||||
? "minecraft:desert" : "minecraft:plains");
|
||||
SavedTerrainChunk captured = capture(actual, boundary);
|
||||
assertArrayEquals(NativeTerrainReceipt.encode(capture(expected, boundary), 7L, "epoch"),
|
||||
NativeTerrainReceipt.encode(captured, 7L, "epoch"));
|
||||
}
|
||||
assertEquals(0, conversions.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyColumnsResolveTheDefaultBiomeOnlyOnce() {
|
||||
LinkedTerrainChunk chunk = chunk();
|
||||
NativeBiome plains = BukkitBiome.of(Biome.PLAINS);
|
||||
AtomicInteger conversions = new AtomicInteger();
|
||||
try (MockedStatic<BukkitBiome> wrappers = mockStatic(BukkitBiome.class, invocation -> {
|
||||
conversions.incrementAndGet();
|
||||
return CALLS_REAL_METHODS.answer(invocation);
|
||||
})) {
|
||||
for (int y = -32; y < 16; y++) {
|
||||
assertSame(plains, chunk.getBiome(0, y, 0));
|
||||
}
|
||||
assertEquals(1, conversions.get());
|
||||
}
|
||||
}
|
||||
|
||||
private static LinkedTerrainChunk chunk() {
|
||||
ChunkData data = mock(ChunkData.class);
|
||||
when(data.getMinHeight()).thenReturn(-16);
|
||||
when(data.getMaxHeight()).thenReturn(0);
|
||||
return new LinkedTerrainChunk(data);
|
||||
}
|
||||
|
||||
private static SavedTerrainChunk capture(SavedTerrainChunk.VoxelSource source, boolean boundary) throws Exception {
|
||||
return boundary ? SavedTerrainChunk.captureBoundary(-1, 2, -16, 16, "minecraft:noise", source)
|
||||
: SavedTerrainChunk.capture(-1, 2, -16, 16, "minecraft:noise", source);
|
||||
}
|
||||
|
||||
private static SavedTerrainChunk.VoxelSource source(BiomeKey source) {
|
||||
BoundaryColumnGeometry.Voxel stone = new BoundaryColumnGeometry.Voxel(
|
||||
"minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false);
|
||||
return new SavedTerrainChunk.VoxelSource() {
|
||||
@Override
|
||||
public BoundaryColumnGeometry.Voxel voxel(int x, int y, int z) {
|
||||
return stone;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String biome(int x, int y, int z) {
|
||||
return source.get(x, y, z);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static Biome biome(Key key) {
|
||||
NamespacedKey namespaced = new NamespacedKey(key.namespace(), key.value());
|
||||
return (Biome) Proxy.newProxyInstance(Biome.class.getClassLoader(), new Class<?>[]{Biome.class},
|
||||
(proxy, method, arguments) -> switch (method.getName()) {
|
||||
case "getKey" -> namespaced;
|
||||
case "hashCode" -> namespaced.hashCode();
|
||||
case "equals" -> proxy == arguments[0];
|
||||
case "toString", "name" -> key.value();
|
||||
default -> null;
|
||||
});
|
||||
}
|
||||
|
||||
private interface BiomeKey {
|
||||
String get(int x, int y, int z);
|
||||
}
|
||||
}
|
||||
+31
@@ -1,11 +1,42 @@
|
||||
package art.arcane.iris.generation.decoration.tree;
|
||||
|
||||
import org.junit.Test;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class TreeBlockMaterialTest {
|
||||
@Test
|
||||
public void nativeMaterialUsesTheMemoizedStringWithoutReadingTheStateKey() {
|
||||
NativeBlockState state = mock(NativeBlockState.class);
|
||||
String material = new String("minecraft:oak_log");
|
||||
when(state.materialKey()).thenReturn(material);
|
||||
|
||||
TreeBlockMaterial first = TreeBlockMaterial.of(state);
|
||||
TreeBlockMaterial second = TreeBlockMaterial.of(state);
|
||||
|
||||
assertSame(material, first.materialKey());
|
||||
assertSame(first.materialKey(), second.materialKey());
|
||||
verify(state, never()).key();
|
||||
assertEquals(TreeBlockMaterial.of("minecraft:oak_log[axis=x]"), first);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativeMaterialFallsBackToTheStateKeyWhenNotMemoized() {
|
||||
NativeBlockState state = mock(NativeBlockState.class);
|
||||
when(state.key()).thenReturn("minecraft:oak_log[axis=z]");
|
||||
|
||||
assertEquals(TreeBlockMaterial.of("minecraft:oak_log"), TreeBlockMaterial.of(state));
|
||||
verify(state).key();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void materialComparisonIgnoresBlockStateProperties() {
|
||||
TreeBlockMaterial expected = TreeBlockMaterial.of("minecraft:oak_log[axis=y]");
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
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.assertNull;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyBasisCacheTest {
|
||||
@Test
|
||||
public void batchLookupPreservesNullAndFirstPublishedSample() {
|
||||
HydrologyBasisCache cache = new HydrologyBasisCache(1);
|
||||
HydrologyTerrainSample[] samples = new HydrologyTerrainSample[1];
|
||||
long key = RiverFootprint.pack(-17, Integer.MAX_VALUE);
|
||||
assertTrue(!cache.findWithoutSlope(key, samples, 0));
|
||||
assertNull(cache.rememberWithoutSlope(key, null));
|
||||
assertTrue(cache.findWithoutSlope(key, samples, 0));
|
||||
assertNull(samples[0]);
|
||||
HydrologyTerrainSample later = HydrologyTerrainSample.openLand(73, 0D, "land");
|
||||
assertNull(cache.rememberWithoutSlope(key, later));
|
||||
cache.rememberWithoutSlope(key + 1, later);
|
||||
assertTrue(!cache.findWithoutSlope(key, samples, 0));
|
||||
assertEquals(later, cache.rememberWithoutSlope(key + 1, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void channelsNullsFailuresAndEvictionPreserveSamples() {
|
||||
CountingSampler sampler = new CountingSampler();
|
||||
HydrologyBasisCache cache = new HydrologyBasisCache(1);
|
||||
assertEquals(3D, cache.sample(1, -2, sampler).slope(), 0D);
|
||||
assertEquals(0D, cache.sampleWithoutSlope(1, -2, sampler).slope(), 0D);
|
||||
assertEquals(3D, cache.sample(1, -2, sampler).slope(), 0D);
|
||||
assertEquals(0D, cache.sampleWithoutSlope(1, -2, sampler).slope(), 0D);
|
||||
assertEquals(1, sampler.fullCalls.get());
|
||||
assertEquals(1, sampler.slopeFreeCalls.get());
|
||||
assertNull(cache.sampleWithoutSlope(Integer.MIN_VALUE, Integer.MAX_VALUE, sampler));
|
||||
assertNull(cache.sampleWithoutSlope(Integer.MIN_VALUE, Integer.MAX_VALUE, sampler));
|
||||
assertEquals(2, sampler.slopeFreeCalls.get());
|
||||
assertThrows(IllegalStateException.class, () -> cache.sampleWithoutSlope(99, 0, sampler));
|
||||
assertEquals(87, cache.sampleWithoutSlope(99, 0, sampler).naturalHeight());
|
||||
assertEquals(87, cache.sampleWithoutSlope(99, 0, sampler).naturalHeight());
|
||||
assertEquals(4, sampler.slopeFreeCalls.get());
|
||||
assertEquals(87, cache.sampleWithoutSlope(1, -2, sampler).naturalHeight());
|
||||
assertEquals(5, sampler.slopeFreeCalls.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plannerTrialsPreserveLocalSlopePrecedenceAndOwnerIsolation() {
|
||||
CountingSampler natural = new CountingSampler();
|
||||
HydrologyTerrainSampler detailed = (x, z) -> {
|
||||
throw new AssertionError("Basis caching must not sample caves");
|
||||
};
|
||||
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
|
||||
HydrologyPlanner planner = new HydrologyPlanner(23L, settings, detailed, natural,
|
||||
HydrologyGeometrySampler.deterministic(detailed), -4096,
|
||||
footprint -> new HydrologyTerrainCaveVoxelView(detailed, settings.seaLevel(), -4096, 4096));
|
||||
planner.planningSamples.set(new HydrologyPlanner.PlanningSamples());
|
||||
try {
|
||||
assertEquals(3D, planner.sampleBasis(1, 1).slope(), 0D);
|
||||
assertEquals(0D, planner.sampleBasisWithoutSlope(2, 1).slope(), 0D);
|
||||
HydrologyBasisCache shared = planner.planningSamples.get()
|
||||
.fallbackBasis(new Long2ObjectOpenHashMap<>());
|
||||
for (int trial = 0; trial < 4; trial++) {
|
||||
planner.planningSamples.set(new HydrologyPlanner.PlanningSamples(null, shared));
|
||||
assertEquals(0D, planner.sampleBasisWithoutSlope(1, 1).slope(), 0D);
|
||||
assertEquals(3D, planner.sampleBasis(1, 1).slope(), 0D);
|
||||
assertEquals(3D, planner.sampleBasisWithoutSlope(1, 1).slope(), 0D);
|
||||
assertEquals(0D, planner.sampleBasisWithoutSlope(2, 1).slope(), 0D);
|
||||
}
|
||||
assertEquals(1, natural.fullCalls.get());
|
||||
assertEquals(2, natural.slopeFreeCalls.get());
|
||||
planner.planningSamples.set(new HydrologyPlanner.PlanningSamples());
|
||||
assertEquals(0D, planner.sampleBasisWithoutSlope(1, 1).slope(), 0D);
|
||||
assertEquals(3, natural.slopeFreeCalls.get());
|
||||
} finally {
|
||||
planner.planningSamples.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parallelTrialsComputeEachCoordinateOnce() throws Exception {
|
||||
CountingSampler sampler = new CountingSampler();
|
||||
HydrologyBasisCache cache = new HydrologyBasisCache();
|
||||
CountDownLatch ready = new CountDownLatch(4);
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
ExecutorService workers = Executors.newFixedThreadPool(4);
|
||||
try {
|
||||
List<Future<List<HydrologyTerrainSample>>> results = new ArrayList<>();
|
||||
for (int trial = 0; trial < 4; trial++) {
|
||||
results.add(workers.submit(() -> {
|
||||
ready.countDown();
|
||||
assertTrue(start.await(5, TimeUnit.SECONDS));
|
||||
List<HydrologyTerrainSample> values = new ArrayList<>();
|
||||
for (int x = -128; x < 0; x++) {
|
||||
values.add(cache.sampleWithoutSlope(x, -17, sampler));
|
||||
}
|
||||
return values;
|
||||
}));
|
||||
}
|
||||
assertTrue(ready.await(5, TimeUnit.SECONDS));
|
||||
start.countDown();
|
||||
List<HydrologyTerrainSample> expected = results.getFirst().get(5, TimeUnit.SECONDS);
|
||||
for (Future<List<HydrologyTerrainSample>> result : results) {
|
||||
assertEquals(expected, result.get(5, TimeUnit.SECONDS));
|
||||
}
|
||||
assertEquals(128, sampler.slopeFreeCalls.get());
|
||||
} finally {
|
||||
start.countDown();
|
||||
workers.shutdownNow();
|
||||
assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
private static final class CountingSampler implements HydrologyNaturalTerrainSampler {
|
||||
private final AtomicInteger fullCalls = new AtomicInteger();
|
||||
private final AtomicInteger slopeFreeCalls = new AtomicInteger();
|
||||
private final AtomicInteger failures = new AtomicInteger();
|
||||
|
||||
@Override
|
||||
public HydrologyTerrainSample sampleBasis(int x, int z) {
|
||||
fullCalls.incrementAndGet();
|
||||
return HydrologyTerrainSample.openLand(87, 3D, "land");
|
||||
}
|
||||
|
||||
@Override
|
||||
public HydrologyTerrainSample sampleBasisWithoutSlope(int x, int z) {
|
||||
slopeFreeCalls.incrementAndGet();
|
||||
if (x == Integer.MIN_VALUE) {
|
||||
return null;
|
||||
}
|
||||
if (x == 99 && failures.getAndIncrement() == 0) {
|
||||
throw new IllegalStateException("retry");
|
||||
}
|
||||
return HydrologyTerrainSample.openLand(87, 0D, "land");
|
||||
}
|
||||
|
||||
@Override
|
||||
public HydrologyTerrainSample[] sampleGrid(GridRequest request) {
|
||||
throw new AssertionError("Basis caching must not sample grids");
|
||||
}
|
||||
|
||||
@Override
|
||||
public NaturalClassification classifyNatural(int x, int z) {
|
||||
throw new AssertionError("Basis caching must not classify separately");
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -34,6 +34,7 @@ public class HydrologyDiagnosticCacheTest {
|
||||
@Test
|
||||
public void terrainLoadsLeaveDiagnosticsLazyAndQueriesRetainImmutableResults() {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
when(planner.settings()).thenReturn(HydrologyPlannerSettings.defaults());
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
HydrologyTileKey key = new HydrologyTileKey(0, 0);
|
||||
HydrologyDiagnosticCandidate candidate = mock(HydrologyDiagnosticCandidate.class);
|
||||
@@ -59,6 +60,7 @@ public class HydrologyDiagnosticCacheTest {
|
||||
@Test
|
||||
public void forbiddenThreadsCanReadOnlyCompleteCachedDiagnostics() {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
when(planner.settings()).thenReturn(HydrologyPlannerSettings.defaults());
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
HydrologyTileKey key = new HydrologyTileKey(0, 0);
|
||||
AtomicBoolean forbidden = new AtomicBoolean(true);
|
||||
@@ -77,6 +79,7 @@ public class HydrologyDiagnosticCacheTest {
|
||||
@Test
|
||||
public void interruptedWaiterLeavesTheSharedDiagnosticLoadRunning() throws Exception {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
when(planner.settings()).thenReturn(HydrologyPlannerSettings.defaults());
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
HydrologyTileKey key = new HydrologyTileKey(0, 0);
|
||||
List<HydrologyDiagnosticCandidate> expected = List.of(mock(HydrologyDiagnosticCandidate.class));
|
||||
@@ -127,6 +130,7 @@ public class HydrologyDiagnosticCacheTest {
|
||||
@Test
|
||||
public void clearDoesNotPublishAnOlderDiagnosticResultIntoTheNewEpoch() throws Exception {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
when(planner.settings()).thenReturn(HydrologyPlannerSettings.defaults());
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
HydrologyTileKey key = new HydrologyTileKey(0, 0);
|
||||
List<HydrologyDiagnosticCandidate> old = List.of(mock(HydrologyDiagnosticCandidate.class));
|
||||
@@ -166,6 +170,7 @@ public class HydrologyDiagnosticCacheTest {
|
||||
@Test
|
||||
public void closeCancelsQueuedDiagnosticsAndDrainsActiveDiagnostics() throws Exception {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
when(planner.settings()).thenReturn(HydrologyPlannerSettings.defaults());
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
HydrologyTileKey firstKey = new HydrologyTileKey(0, 0);
|
||||
HydrologyTileKey secondKey = new HydrologyTileKey(1, 0);
|
||||
@@ -213,6 +218,7 @@ public class HydrologyDiagnosticCacheTest {
|
||||
@Test
|
||||
public void aDiagnosticLoadCannotCloseItsOwnCache() {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
when(planner.settings()).thenReturn(HydrologyPlannerSettings.defaults());
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
HydrologyTileKey key = new HydrologyTileKey(0, 0);
|
||||
when(planner.plan(key)).thenReturn(tile);
|
||||
@@ -229,6 +235,7 @@ public class HydrologyDiagnosticCacheTest {
|
||||
@Test
|
||||
public void diagnosticFailureIsRetriedAndCompletedListsRemainBounded() throws Exception {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
when(planner.settings()).thenReturn(HydrologyPlannerSettings.defaults());
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
|
||||
when(planner.diagnosticCandidates(tile)).thenThrow(new IllegalStateException("expected")).thenReturn(List.of());
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user