mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 13:30:08 +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:
1 parent
98ddb6644d
commit
ca9f28c9b0
142 files changed
+11159
-773
No files matched your search
+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());
|
||||
|
||||
Reference in new issue
Block a user