mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-11 19:14:11 +00:00
Test- 26.3
This commit is contained in:
+1
-1
@@ -958,7 +958,7 @@ public class NMSBinding implements INMSBinding {
|
|||||||
if (global == null) {
|
if (global == null) {
|
||||||
throw new IllegalStateException("Iris cannot build the global block state palette on this server version");
|
throw new IllegalStateException("Iris cannot build the global block state palette on this server version");
|
||||||
}
|
}
|
||||||
java.util.Map<CompoundTag, BlockState> innerDecodeCache = new java.util.concurrent.ConcurrentHashMap<>(64);
|
java.util.Map<art.arcane.volmlib.util.nbt.tag.Tag<?>, BlockState> innerDecodeCache = new java.util.concurrent.ConcurrentHashMap<>(64);
|
||||||
java.util.Map<CompoundTag, BlockState> outerDecodeCache = new java.util.concurrent.ConcurrentHashMap<>(64);
|
java.util.Map<CompoundTag, BlockState> outerDecodeCache = new java.util.concurrent.ConcurrentHashMap<>(64);
|
||||||
MCAPalettedContainer<BlockState> container = new MCAPalettedContainer<>(global, registry,
|
MCAPalettedContainer<BlockState> container = new MCAPalettedContainer<>(global, registry,
|
||||||
i -> innerDecodeCache.computeIfAbsent(i, t -> ((CraftBlockData) NBTWorld.getBlockData(t)).getState()),
|
i -> innerDecodeCache.computeIfAbsent(i, t -> ((CraftBlockData) NBTWorld.getBlockData(t)).getState()),
|
||||||
|
|||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||||
|
import java.util.Set;
|
||||||
|
import com.mojang.serialization.MapCodec;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
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.biome.FixedBiomeSource;
|
||||||
|
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 java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
final class ForcedStructureChunkGenerator extends ChunkGenerator {
|
||||||
|
private final ChunkGenerator delegate;
|
||||||
|
|
||||||
|
ForcedStructureChunkGenerator(ChunkGenerator delegate, Holder<Biome> sourceBiome) {
|
||||||
|
super(new FixedBiomeSource(sourceBiome));
|
||||||
|
this.delegate = delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected MapCodec<? extends ChunkGenerator> codec() {
|
||||||
|
return MapCodec.unit(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void spawnOriginalMobs(WorldGenRegion worldGenRegion) {
|
||||||
|
delegate.spawnOriginalMobs(worldGenRegion);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getGenDepth() {
|
||||||
|
return delegate.getGenDepth();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<ChunkAccess> buildTerrain(ChunkAccess centerChunk, Blender blender, RandomState randomState,
|
||||||
|
StructureManager structureManager, BiomeManager biomeManager,
|
||||||
|
WorldGenRegion carverBiomeRegion, Set<Holder<Biome>> possibleBiomes) {
|
||||||
|
return delegate.buildTerrain(
|
||||||
|
centerChunk, blender, randomState, structureManager, biomeManager, carverBiomeRegion, possibleBiomes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSeaLevel() {
|
||||||
|
return delegate.getSeaLevel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getMinY() {
|
||||||
|
return delegate.getMinY();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getBaseHeight(int x, int z, Heightmap.Types type,
|
||||||
|
LevelHeightAccessor heightAccessor, RandomState randomState) {
|
||||||
|
return delegate.getBaseHeight(x, z, type, heightAccessor, randomState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor heightAccessor,
|
||||||
|
RandomState randomState) {
|
||||||
|
return delegate.getBaseColumn(x, z, heightAccessor, randomState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addDebugScreenInfo(List<String> result, RandomState randomState,
|
||||||
|
BlockPos feetPos, SamplerContext samplerContext) {
|
||||||
|
delegate.addDebugScreenInfo(result, randomState, feetPos, samplerContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CompletionException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared waiter for the racy build caches in this package: the loser of a putIfAbsent race
|
||||||
|
* must surface the same exception the builder threw, not a CompletionException wrapper.
|
||||||
|
*/
|
||||||
|
final class NativeBuildFutures {
|
||||||
|
private NativeBuildFutures() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static <T> T awaitBuild(CompletableFuture<T> future, String what) {
|
||||||
|
try {
|
||||||
|
return future.join();
|
||||||
|
} catch (CompletionException error) {
|
||||||
|
Throwable cause = error.getCause();
|
||||||
|
if (cause instanceof RuntimeException runtime) {
|
||||||
|
throw runtime;
|
||||||
|
}
|
||||||
|
if (cause instanceof Error fatal) {
|
||||||
|
throw fatal;
|
||||||
|
}
|
||||||
|
if (cause == null) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException(what + " failed", cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.runtime.DimensionStackContext;
|
||||||
|
import art.arcane.iris.generation.runtime.DimensionStackLayout;
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||||
|
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||||
|
import art.arcane.iris.structure.object.IrisStaticObjectLayer;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.status.ChunkPyramid;
|
||||||
|
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
public final class NativeGenerationWriteGuard {
|
||||||
|
private NativeGenerationWriteGuard() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean allowsDecoration(Engine engine, WorldGenLevel region, ChunkPos center) {
|
||||||
|
int radius = ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FEATURES).blockStateWriteRadius();
|
||||||
|
for (int offsetX = -radius; offsetX <= radius; offsetX++) {
|
||||||
|
for (int offsetZ = -radius; offsetZ <= radius; offsetZ++) {
|
||||||
|
int chunkX = Math.addExact(center.x(), offsetX);
|
||||||
|
int chunkZ = Math.addExact(center.z(), offsetZ);
|
||||||
|
ChunkAccess target = region.getChunk(chunkX, chunkZ);
|
||||||
|
if (target.getPersistedStatus().isOrAfter(ChunkStatus.FULL)
|
||||||
|
&& !engine.getComplex().allowsMantleChunkWrite(chunkX, chunkZ)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isHistoricalStructure(Engine engine, long activation) {
|
||||||
|
if (activation <= 0 || !(engine instanceof IrisEngine irisEngine)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null);
|
||||||
|
return router != null && activation < router.history().activeActivation().activationId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean allowsPendingStage(Engine engine, ChunkAccess chunk, ChunkStatus stage) {
|
||||||
|
return stage.isOrAfter(ChunkStatus.FEATURES)
|
||||||
|
&& chunk.getPersistedStatus().isOrAfter(ChunkStatus.TERRAIN)
|
||||||
|
&& !chunk.getPersistedStatus().isOrAfter(stage)
|
||||||
|
&& !engine.getComplex().allowsMantleChunkWrite(chunk.getPos().x(), chunk.getPos().z());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Predicate<BlockPos> protectedPositions(
|
||||||
|
IrisStaticObjectLayer staticObjects,
|
||||||
|
DimensionStackContext stackContext,
|
||||||
|
int minimumY
|
||||||
|
) {
|
||||||
|
Map<Long, DimensionStackLayout> layouts = new ConcurrentHashMap<>();
|
||||||
|
return position -> {
|
||||||
|
if (!staticObjects.isEmpty() && staticObjects.contains(
|
||||||
|
position.getX(), position.getY() - minimumY, position.getZ())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stackContext == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
long columnKey = ((long) position.getX() << 32)
|
||||||
|
^ (position.getZ() & 0xFFFFFFFFL);
|
||||||
|
DimensionStackLayout layout = layouts.computeIfAbsent(
|
||||||
|
columnKey,
|
||||||
|
ignored -> stackContext.sample(position.getX(), position.getZ())
|
||||||
|
);
|
||||||
|
return layout.isHostFeatureProtectedY(position.getY() - minimumY);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+286
@@ -0,0 +1,286 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
|
||||||
|
import art.arcane.iris.structure.jigsaw.IrisJigsawConfiguration;
|
||||||
|
import art.arcane.iris.structure.jigsaw.IrisJigsawHeightmap;
|
||||||
|
import art.arcane.iris.structure.jigsaw.IrisJigsawLiquidSettings;
|
||||||
|
import art.arcane.iris.spi.PlatformStructureHooks.JigsawSourceMetadata;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.RegistryAccess;
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
import net.minecraft.nbt.NbtOps;
|
||||||
|
import net.minecraft.nbt.Tag;
|
||||||
|
import net.minecraft.resources.RegistryOps;
|
||||||
|
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.Biome;
|
||||||
|
import net.minecraft.world.level.biome.BiomeSource;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
|
import net.minecraft.world.level.levelgen.RandomState;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.IntBinaryOperator;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
public final class NativeStructureFactory {
|
||||||
|
private NativeStructureFactory() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StructureStart generate(GenerationContext context, Holder<Structure> sourceHolder,
|
||||||
|
NativeStructureStartPlan plan, int references) {
|
||||||
|
Structure source = sourceHolder.value();
|
||||||
|
Structure configured = configure(
|
||||||
|
context.registryAccess(), source, plan.source().getJigsaw(),
|
||||||
|
plan.placement().isUnderground(), plan.baseY());
|
||||||
|
Holder<Biome> sourceBiome = source.biomes().stream().findFirst()
|
||||||
|
.or(() -> context.biomeSource().possibleBiomes().stream().findFirst())
|
||||||
|
.orElseThrow(() -> new IllegalStateException("Configured native structure '"
|
||||||
|
+ plan.source().getStructure() + "' has no usable generation biome"));
|
||||||
|
ChunkGenerator forcedGenerator = new ForcedStructureChunkGenerator(
|
||||||
|
context.generator(), sourceBiome);
|
||||||
|
StructureStart generated = configured.generate(
|
||||||
|
sourceHolder,
|
||||||
|
context.levelKey(),
|
||||||
|
context.registryAccess(),
|
||||||
|
forcedGenerator,
|
||||||
|
forcedGenerator.getBiomeSource(),
|
||||||
|
context.randomState().createClimateSampler(SamplerContext.EMPTY_UNCACHED),
|
||||||
|
context.randomState(),
|
||||||
|
context.templateManager(),
|
||||||
|
context.levelSeed(),
|
||||||
|
new ChunkPos(plan.chunkX(), plan.chunkZ()),
|
||||||
|
references,
|
||||||
|
context.heightAccessor(),
|
||||||
|
context.biomePredicate()
|
||||||
|
);
|
||||||
|
if (generated == null || !generated.isValid()) {
|
||||||
|
return StructureStart.INVALID_START;
|
||||||
|
}
|
||||||
|
StructureStart positioned;
|
||||||
|
if (plan.placement().isUnderground()) {
|
||||||
|
positioned = NativeStructureVerticalPlacer.relocateToMinY(
|
||||||
|
generated, source, plan.baseY(), context.heightAccessor());
|
||||||
|
} else {
|
||||||
|
NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||||
|
generated,
|
||||||
|
plan.source().getStructure(),
|
||||||
|
0,
|
||||||
|
context.seaLevel(),
|
||||||
|
context.heightAccessor().getMinY(),
|
||||||
|
Math.addExact(context.heightAccessor().getMinY(), context.heightAccessor().getHeight()),
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
context.surfaceHeight());
|
||||||
|
positioned = generated;
|
||||||
|
}
|
||||||
|
if (!positioned.isValid()) {
|
||||||
|
return StructureStart.INVALID_START;
|
||||||
|
}
|
||||||
|
return NativeStructureReferenceEnvelope.wrapForPublication(
|
||||||
|
positioned,
|
||||||
|
source,
|
||||||
|
references,
|
||||||
|
plan.placement().resolvedTerrain(),
|
||||||
|
plan.source().getStructure()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JigsawSourceMetadata sourceMetadata(RegistryAccess registryAccess,
|
||||||
|
StructureTemplateManager templateManager,
|
||||||
|
JigsawStructure source) {
|
||||||
|
CompoundTag sourceTag = encodeJigsaw(registryAccess, source);
|
||||||
|
return new JigsawSourceMetadata(
|
||||||
|
sourceDistance(sourceTag, "horizontal"),
|
||||||
|
horizontalReferenceExpansion(source),
|
||||||
|
NativeStructureTemplatePoolBounds.sourceHorizontalSpan(
|
||||||
|
registryAccess, templateManager, source));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int templatePoolHorizontalSpan(RegistryAccess registryAccess,
|
||||||
|
StructureTemplateManager templateManager,
|
||||||
|
String templatePoolKey) {
|
||||||
|
return NativeStructureTemplatePoolBounds.horizontalSpan(
|
||||||
|
registryAccess, templateManager, templatePoolKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int jigsawStartPoolHorizontalSpan(RegistryAccess registryAccess,
|
||||||
|
StructureTemplateManager templateManager,
|
||||||
|
JigsawStructure source,
|
||||||
|
String templatePoolKey) {
|
||||||
|
return NativeStructureTemplatePoolBounds.horizontalSpan(
|
||||||
|
registryAccess, templateManager, source, templatePoolKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Structure configure(RegistryAccess registryAccess, Structure source,
|
||||||
|
IrisJigsawConfiguration configuration,
|
||||||
|
boolean underground, int baseY) {
|
||||||
|
Objects.requireNonNull(source, "Native structure source must not be null");
|
||||||
|
if (!(source instanceof JigsawStructure sourceJigsaw)) {
|
||||||
|
if (configuration != null) {
|
||||||
|
throw new IllegalStateException("Jigsaw options require a registered jigsaw structure, found "
|
||||||
|
+ source.getClass().getName());
|
||||||
|
}
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
RegistryOps<Tag> registryOps = RegistryOps.create(NbtOps.INSTANCE, registryAccess);
|
||||||
|
CompoundTag structureTag = encodeJigsaw(registryOps, sourceJigsaw);
|
||||||
|
CompoundTag configuredTag = structureTag.copy();
|
||||||
|
if (underground) {
|
||||||
|
CompoundTag height = new CompoundTag();
|
||||||
|
height.putInt("absolute", baseY);
|
||||||
|
configuredTag.put("start_height", height);
|
||||||
|
configuredTag.remove("project_start_to_heightmap");
|
||||||
|
}
|
||||||
|
applyConfiguration(configuredTag, configuration);
|
||||||
|
Structure decoded = Structure.DIRECT_CODEC.parse(registryOps, configuredTag).getOrThrow();
|
||||||
|
if (!(decoded instanceof JigsawStructure configured)) {
|
||||||
|
throw new IllegalStateException("Configured native jigsaw codec produced "
|
||||||
|
+ decoded.getClass().getName());
|
||||||
|
}
|
||||||
|
return configured;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CompoundTag encodeJigsaw(RegistryAccess registryAccess,
|
||||||
|
JigsawStructure source) {
|
||||||
|
return encodeJigsaw(RegistryOps.create(NbtOps.INSTANCE, registryAccess), source);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CompoundTag encodeJigsaw(RegistryOps<Tag> registryOps,
|
||||||
|
JigsawStructure source) {
|
||||||
|
Tag encoded = Structure.DIRECT_CODEC.encodeStart(registryOps, source).getOrThrow();
|
||||||
|
if (!(encoded instanceof CompoundTag structureTag)) {
|
||||||
|
throw new IllegalStateException("Native jigsaw codec did not produce a compound");
|
||||||
|
}
|
||||||
|
return structureTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void applyConfiguration(CompoundTag tag, IrisJigsawConfiguration configuration) {
|
||||||
|
if (configuration == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (configuration.getStartPool() != null && !configuration.getStartPool().isBlank()) {
|
||||||
|
tag.putString("start_pool", configuration.getStartPool().trim());
|
||||||
|
}
|
||||||
|
if (configuration.getStartJigsawName() != null
|
||||||
|
&& !configuration.getStartJigsawName().isBlank()) {
|
||||||
|
if ("NONE".equalsIgnoreCase(configuration.getStartJigsawName().trim())) {
|
||||||
|
tag.remove("start_jigsaw_name");
|
||||||
|
} else {
|
||||||
|
tag.putString("start_jigsaw_name", configuration.getStartJigsawName().trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (configuration.getMaxDepth() != null) {
|
||||||
|
tag.putInt("size", configuration.getMaxDepth());
|
||||||
|
}
|
||||||
|
applyDistance(tag, configuration);
|
||||||
|
if (configuration.getUseExpansionHack() != null) {
|
||||||
|
tag.putBoolean("use_expansion_hack", configuration.getUseExpansionHack());
|
||||||
|
}
|
||||||
|
applyHeightmap(tag, configuration.getProjectStartToHeightmap());
|
||||||
|
applyDimensionPadding(tag, configuration);
|
||||||
|
applyLiquidSettings(tag, configuration.getLiquidSettings());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void applyDistance(CompoundTag tag, IrisJigsawConfiguration configuration) {
|
||||||
|
Integer horizontal = configuration.getMaxDistanceHorizontal();
|
||||||
|
Integer vertical = configuration.getMaxDistanceVertical();
|
||||||
|
if (horizontal == null && vertical == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int resolvedHorizontal = horizontal == null ? sourceDistance(tag, "horizontal") : horizontal;
|
||||||
|
int resolvedVertical = vertical == null ? sourceDistance(tag, "vertical") : vertical;
|
||||||
|
if (resolvedHorizontal == resolvedVertical) {
|
||||||
|
tag.putInt("max_distance_from_center", resolvedHorizontal);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CompoundTag distance = new CompoundTag();
|
||||||
|
distance.putInt("horizontal", resolvedHorizontal);
|
||||||
|
distance.putInt("vertical", resolvedVertical);
|
||||||
|
tag.put("max_distance_from_center", distance);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int sourceDistance(CompoundTag tag, String axis) {
|
||||||
|
Tag raw = tag.get("max_distance_from_center");
|
||||||
|
if (raw instanceof CompoundTag compound) {
|
||||||
|
return compound.getIntOr(axis, 128);
|
||||||
|
}
|
||||||
|
return tag.getIntOr("max_distance_from_center", 128);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int horizontalReferenceExpansion(Structure structure) {
|
||||||
|
BoundingBox content = new BoundingBox(0, 0, 0, 0, 0, 0);
|
||||||
|
BoundingBox adjusted = structure.adjustBoundingBox(content);
|
||||||
|
int expansion = Math.max(content.minX() - adjusted.minX(), adjusted.maxX() - content.maxX());
|
||||||
|
expansion = Math.max(expansion, content.minZ() - adjusted.minZ());
|
||||||
|
return Math.max(0, Math.max(expansion, adjusted.maxZ() - content.maxZ()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void applyHeightmap(CompoundTag tag, IrisJigsawHeightmap heightmap) {
|
||||||
|
if (heightmap == null || heightmap == IrisJigsawHeightmap.SOURCE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (heightmap == IrisJigsawHeightmap.NONE) {
|
||||||
|
tag.remove("project_start_to_heightmap");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tag.putString("project_start_to_heightmap", heightmap.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void applyDimensionPadding(CompoundTag tag, IrisJigsawConfiguration configuration) {
|
||||||
|
Integer bottom = configuration.getDimensionPaddingBottom();
|
||||||
|
Integer top = configuration.getDimensionPaddingTop();
|
||||||
|
if (bottom == null && top == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int resolvedBottom = bottom == null ? sourcePadding(tag, "bottom") : bottom;
|
||||||
|
int resolvedTop = top == null ? sourcePadding(tag, "top") : top;
|
||||||
|
if (resolvedBottom == resolvedTop) {
|
||||||
|
tag.putInt("dimension_padding", resolvedBottom);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CompoundTag padding = new CompoundTag();
|
||||||
|
padding.putInt("bottom", resolvedBottom);
|
||||||
|
padding.putInt("top", resolvedTop);
|
||||||
|
tag.put("dimension_padding", padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int sourcePadding(CompoundTag tag, String side) {
|
||||||
|
Tag raw = tag.get("dimension_padding");
|
||||||
|
if (raw instanceof CompoundTag compound) {
|
||||||
|
return compound.getIntOr(side, 0);
|
||||||
|
}
|
||||||
|
return tag.getIntOr("dimension_padding", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void applyLiquidSettings(CompoundTag tag, IrisJigsawLiquidSettings settings) {
|
||||||
|
if (settings == null || settings == IrisJigsawLiquidSettings.SOURCE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tag.putString("liquid_settings", settings.name().toLowerCase(Locale.ROOT));
|
||||||
|
}
|
||||||
|
|
||||||
|
public record GenerationContext(
|
||||||
|
RegistryAccess registryAccess,
|
||||||
|
ChunkGenerator generator,
|
||||||
|
BiomeSource biomeSource,
|
||||||
|
RandomState randomState,
|
||||||
|
StructureTemplateManager templateManager,
|
||||||
|
long levelSeed,
|
||||||
|
ResourceKey<Level> levelKey,
|
||||||
|
LevelHeightAccessor heightAccessor,
|
||||||
|
Predicate<Holder<Biome>> biomePredicate,
|
||||||
|
int seaLevel,
|
||||||
|
IntBinaryOperator surfaceHeight
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+212
@@ -0,0 +1,212 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.structure.placement.IrisStructureStiltSettings;
|
||||||
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
|
import net.minecraft.tags.BlockTags;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.SupportType;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.BitSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.IntBinaryOperator;
|
||||||
|
|
||||||
|
public final class NativeStructureFoundationBuilder {
|
||||||
|
private static final int FOUNDATION_VERTICAL_TOLERANCE = 1;
|
||||||
|
|
||||||
|
private NativeStructureFoundationBuilder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<FoundationColumn> foundationEnvelope(BoundingBox area, StructureStart start) {
|
||||||
|
BoundingBox structure = NativeStructureReferenceEnvelope.contentBounds(start);
|
||||||
|
int minX = Math.max(area.minX(), structure.minX());
|
||||||
|
int minZ = Math.max(area.minZ(), structure.minZ());
|
||||||
|
int maxX = Math.min(area.maxX(), structure.maxX());
|
||||||
|
int maxZ = Math.min(area.maxZ(), structure.maxZ());
|
||||||
|
if (minX > maxX || minZ > maxZ) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<StructurePiece> pieces = start.getPieces();
|
||||||
|
List<FoundationColumn> columns = new ArrayList<>((maxX - minX + 1) * (maxZ - minZ + 1));
|
||||||
|
BitSet envelope = new BitSet(area.getYSpan());
|
||||||
|
for (int z = minZ; z <= maxZ; z++) {
|
||||||
|
for (int x = minX; x <= maxX; x++) {
|
||||||
|
envelope.clear();
|
||||||
|
markFoundationEnvelope(envelope, pieces, area, x, z);
|
||||||
|
int cellCount = envelope.cardinality();
|
||||||
|
if (cellCount == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int[] ys = new int[cellCount];
|
||||||
|
int cell = 0;
|
||||||
|
for (int bit = envelope.nextSetBit(0); bit >= 0; bit = envelope.nextSetBit(bit + 1)) {
|
||||||
|
ys[cell++] = area.minY() + bit;
|
||||||
|
}
|
||||||
|
columns.add(new FoundationColumn(x, z, ys));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void markFoundationEnvelope(BitSet envelope, List<StructurePiece> pieces, BoundingBox area,
|
||||||
|
int x, int z) {
|
||||||
|
for (StructurePiece piece : pieces) {
|
||||||
|
BoundingBox bounds = piece.getBoundingBox();
|
||||||
|
if (x < bounds.minX() || x > bounds.maxX() || z < bounds.minZ() || z > bounds.maxZ()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int groundY = bounds.minY();
|
||||||
|
if (piece instanceof PoolElementStructurePiece poolPiece) {
|
||||||
|
groundY += poolPiece.getGroundLevelDelta();
|
||||||
|
if (groundY < bounds.minY()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int minY = Math.max(area.minY(), bounds.minY());
|
||||||
|
int maxY = Math.min(area.maxY(), Math.min(bounds.maxY(), groundY + FOUNDATION_VERTICAL_TOLERANCE));
|
||||||
|
if (minY <= maxY) {
|
||||||
|
envelope.set(minY - area.minY(), maxY - area.minY() + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void placeStilts(WorldGenLevel world, BoundingBox area, String structureId,
|
||||||
|
StructureStart start, IrisStructureStiltSettings settings,
|
||||||
|
NativeStructurePostProcessor.PaletteBlockResolver paletteBlockResolver,
|
||||||
|
IntBinaryOperator surfaceHeight,
|
||||||
|
boolean surfaceStructure) {
|
||||||
|
Objects.requireNonNull(surfaceHeight, "Structure stilts require an Iris terrain height resolver");
|
||||||
|
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
|
||||||
|
int structureHash = structureId == null ? 0 : structureId.hashCode();
|
||||||
|
RNG rng = new RNG(world.getSeed() ^ structureHash);
|
||||||
|
for (FoundationColumn column : foundationEnvelope(area, start)) {
|
||||||
|
if (!isStiltColumn(column.x(), column.z(), settings.getSpacing())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int foundationY = findFoundationY(world, column, position);
|
||||||
|
if (foundationY == Integer.MIN_VALUE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int terrainY = surfaceStructure
|
||||||
|
? Math.max(area.minY(), Math.min(
|
||||||
|
area.maxY(), surfaceHeight.applyAsInt(column.x(), column.z())))
|
||||||
|
: area.minY() - 1;
|
||||||
|
int anchorY = findStiltAnchorY(
|
||||||
|
world, column.x(), column.z(), foundationY,
|
||||||
|
Math.max(1, settings.getMaxDepth()), terrainY, area.minY(), position);
|
||||||
|
if (anchorY == Integer.MIN_VALUE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int y = foundationY - 1; y > anchorY; y--) {
|
||||||
|
position.set(column.x(), y, column.z());
|
||||||
|
BlockState stilt = settings.getPalette() == null
|
||||||
|
? Blocks.COBBLESTONE.defaultBlockState()
|
||||||
|
: Objects.requireNonNull(
|
||||||
|
paletteBlockResolver.resolve(
|
||||||
|
settings.getPalette(), rng, column.x(), y, column.z()),
|
||||||
|
"Stilt palette returned no block for " + structureId + " at "
|
||||||
|
+ column.x() + "," + y + "," + column.z());
|
||||||
|
world.setBlock(position, stilt, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isStiltColumn(int x, int z, int spacing) {
|
||||||
|
int resolvedSpacing = Math.max(1, spacing);
|
||||||
|
return resolvedSpacing == 1
|
||||||
|
|| Math.floorMod(x, resolvedSpacing) == 0
|
||||||
|
&& Math.floorMod(z, resolvedSpacing) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int findStiltAnchorY(
|
||||||
|
WorldGenLevel world, int x, int z, int foundationY, int maxDepth,
|
||||||
|
int terrainY, int areaMinY, BlockPos.MutableBlockPos position) {
|
||||||
|
int minimumAnchorY = Math.max(
|
||||||
|
areaMinY, Math.max(terrainY, foundationY - maxDepth - 1));
|
||||||
|
for (int y = foundationY - 1; y >= minimumAnchorY; y--) {
|
||||||
|
BlockState state = world.getBlockState(position.set(x, y, z));
|
||||||
|
boolean vegetation = state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES);
|
||||||
|
if (!vegetation && state.isFaceSturdy(
|
||||||
|
world, position, Direction.UP, SupportType.FULL)) {
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Integer.MIN_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StiltSupportAudit auditStiltSupport(WorldGenLevel world, BoundingBox area,
|
||||||
|
StructureStart start, BlockState expectedStilt,
|
||||||
|
IntBinaryOperator surfaceHeight) {
|
||||||
|
Objects.requireNonNull(expectedStilt, "Expected stilt state must not be null");
|
||||||
|
Objects.requireNonNull(surfaceHeight, "Structure stilt audit requires an Iris terrain height resolver");
|
||||||
|
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
|
||||||
|
int baseColumns = 0;
|
||||||
|
int stiltBlocks = 0;
|
||||||
|
int stiltColumns = 0;
|
||||||
|
int unsupportedColumns = 0;
|
||||||
|
for (FoundationColumn column : foundationEnvelope(area, start)) {
|
||||||
|
int foundationY = findFoundationY(world, column, position);
|
||||||
|
if (foundationY == Integer.MIN_VALUE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
baseColumns++;
|
||||||
|
int terrainY = Math.max(area.minY(), Math.min(
|
||||||
|
area.maxY(), surfaceHeight.applyAsInt(column.x(), column.z())));
|
||||||
|
boolean grounded = foundationY <= terrainY + 1;
|
||||||
|
boolean stiltColumn = false;
|
||||||
|
for (int y = foundationY - 1; y >= area.minY(); y--) {
|
||||||
|
if (y <= terrainY) {
|
||||||
|
grounded = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
BlockState state = world.getBlockState(position.set(column.x(), y, column.z()));
|
||||||
|
if (state.is(expectedStilt.getBlock())) {
|
||||||
|
stiltBlocks++;
|
||||||
|
stiltColumn = true;
|
||||||
|
if (y == area.minY()) {
|
||||||
|
grounded = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
boolean vegetation = state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES);
|
||||||
|
grounded = state.isSolid() && !vegetation;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (stiltColumn) {
|
||||||
|
stiltColumns++;
|
||||||
|
}
|
||||||
|
if (!grounded) {
|
||||||
|
unsupportedColumns++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new StiltSupportAudit(baseColumns, stiltBlocks, stiltColumns, unsupportedColumns);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int findFoundationY(WorldGenLevel world, FoundationColumn column,
|
||||||
|
BlockPos.MutableBlockPos position) {
|
||||||
|
for (int cell = 0; cell < column.ys().length; cell++) {
|
||||||
|
int y = column.ys()[cell];
|
||||||
|
BlockState state = world.getBlockState(position.set(column.x(), y, column.z()));
|
||||||
|
if (state.isSolid()) {
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Integer.MIN_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record FoundationColumn(int x, int z, int[] ys) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record StiltSupportAudit(int baseColumns, int stiltBlocks, int stiltColumns,
|
||||||
|
int unsupportedColumns) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
public final class NativeStructureGenerationException extends IllegalStateException {
|
||||||
|
private NativeStructureGenerationException(FailureContext context, Throwable cause) {
|
||||||
|
super(message(context), cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
private NativeStructureGenerationException(FailureContext context) {
|
||||||
|
super(message(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NativeStructureGenerationException failure(String operation, String structureContext,
|
||||||
|
int chunkX, int chunkZ, Throwable cause) {
|
||||||
|
return new NativeStructureGenerationException(
|
||||||
|
new FailureContext(operation, structureContext, chunkX, chunkZ), cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NativeStructureGenerationException failure(String operation, String structureContext,
|
||||||
|
int chunkX, int chunkZ) {
|
||||||
|
return new NativeStructureGenerationException(
|
||||||
|
new FailureContext(operation, structureContext, chunkX, chunkZ));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String message(FailureContext context) {
|
||||||
|
String resolvedContext = context.structureContext() == null || context.structureContext().isBlank()
|
||||||
|
? "<unregistered structure>"
|
||||||
|
: context.structureContext();
|
||||||
|
return "Iris native structure " + context.operation() + " failed for " + resolvedContext
|
||||||
|
+ " in chunk " + context.chunkX() + "," + context.chunkZ()
|
||||||
|
+ "; chunk generation was aborted to prevent partial structure state";
|
||||||
|
}
|
||||||
|
|
||||||
|
private record FailureContext(String operation, String structureContext, int chunkX, int chunkZ) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Servers
|
||||||
|
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.placement.AbstractSpreadingStructurePlacement;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class NativeStructureGenerationKeys {
|
||||||
|
private static final Method FREQUENCY_METHOD = resolveFrequencyMethod();
|
||||||
|
|
||||||
|
private NativeStructureGenerationKeys() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<String> reachable(ServerLevel level, Set<String> possibleBiomeKeys) {
|
||||||
|
ServerLevel activeLevel = Objects.requireNonNull(level, "Native structure reachability requires a level");
|
||||||
|
Set<String> possibleBiomes = Objects.requireNonNull(
|
||||||
|
possibleBiomeKeys, "Native structure reachability requires possible biome keys");
|
||||||
|
if (!activeLevel.getServer().getWorldGenSettings().options().generateStructures()) {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> keys = new LinkedHashSet<>();
|
||||||
|
List<Holder<StructureSet>> structureSets =
|
||||||
|
activeLevel.getChunkSource().getGeneratorState().possibleStructureSets();
|
||||||
|
for (Holder<StructureSet> structureSetHolder : structureSets) {
|
||||||
|
StructureSet structureSet = structureSetHolder.value();
|
||||||
|
if (!isEnabledPlacement(structureSet.placement())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (StructureSet.StructureSelectionEntry entry : structureSet.structures()) {
|
||||||
|
if (entry.weight() <= 0 || !hasPossibleBiome(entry.structure().value(), possibleBiomes)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Optional<ResourceKey<Structure>> key = entry.structure().unwrapKey();
|
||||||
|
key.ifPresent(resourceKey -> keys.add(resourceKey.identifier().toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableSet(keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean hasPossibleBiome(Structure structure, Set<String> possibleBiomeKeys) {
|
||||||
|
for (Holder<Biome> holder : structure.biomes()) {
|
||||||
|
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
|
||||||
|
if (key.isPresent() && possibleBiomeKeys.contains(key.get().identifier().toString())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isEnabledPlacement(StructurePlacement placement) {
|
||||||
|
return placementFrequency(placement) > 0.0F;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float placementFrequency(StructurePlacement placement) {
|
||||||
|
StructurePlacement activePlacement = Objects.requireNonNull(
|
||||||
|
placement, "Native structure placement frequency requires a placement");
|
||||||
|
if (!(activePlacement instanceof AbstractSpreadingStructurePlacement)) {
|
||||||
|
return 1F;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return ((Float) FREQUENCY_METHOD.invoke(activePlacement)).floatValue();
|
||||||
|
} catch (ReflectiveOperationException error) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Unable to read native structure placement frequency from "
|
||||||
|
+ activePlacement.getClass().getName(), error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Method resolveFrequencyMethod() {
|
||||||
|
Method frequencyMethod = null;
|
||||||
|
for (Method method : AbstractSpreadingStructurePlacement.class.getDeclaredMethods()) {
|
||||||
|
if (Modifier.isStatic(method.getModifiers())
|
||||||
|
|| method.getParameterCount() != 0
|
||||||
|
|| method.getReturnType() != float.class) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (frequencyMethod != null) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Native structure placement exposes multiple zero-argument float accessors");
|
||||||
|
}
|
||||||
|
frequencyMethod = method;
|
||||||
|
}
|
||||||
|
if (frequencyMethod == null || !frequencyMethod.trySetAccessible()) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Native structure placement frequency accessor is unavailable");
|
||||||
|
}
|
||||||
|
return frequencyMethod;
|
||||||
|
}
|
||||||
|
}
|
||||||
+351
@@ -0,0 +1,351 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.structure.placement.IrisStructureLocator;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.Registry;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
import net.minecraft.nbt.IntTag;
|
||||||
|
import net.minecraft.nbt.NbtUtils;
|
||||||
|
import net.minecraft.nbt.StreamTagVisitor;
|
||||||
|
import net.minecraft.nbt.Tag;
|
||||||
|
import net.minecraft.nbt.visitors.CollectFields;
|
||||||
|
import net.minecraft.nbt.visitors.FieldSelector;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.util.datafix.DataFixTypes;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.storage.SimpleRegionStorage;
|
||||||
|
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public final class NativeStructureLocatePersistence {
|
||||||
|
public static final int MAX_SELECTED_CANDIDATE_RETRIES = 64;
|
||||||
|
private static final int MAX_STORAGE_PROBES = 512;
|
||||||
|
private static final Method PAPER_LEVEL_TYPE_KEY = paperLevelTypeKey();
|
||||||
|
|
||||||
|
private NativeStructureLocatePersistence() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Probe probe(ServerLevel level, Structure structure, boolean requireUnreferenced) {
|
||||||
|
return probe(level, structure, requireUnreferenced, new ProbeBudget(MAX_STORAGE_PROBES));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ProbeBudget probeBudget() {
|
||||||
|
return new ProbeBudget(MAX_STORAGE_PROBES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Probe probe(ServerLevel level, Structure structure, boolean requireUnreferenced,
|
||||||
|
ProbeBudget budget) {
|
||||||
|
return new Probe(level, structure, requireUnreferenced, budget);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Search search(Engine engine, String structureKey,
|
||||||
|
int blockX, int blockZ, int radius, Probe probe) {
|
||||||
|
return new Search(engine, structureKey, blockX, blockZ, radius, probe);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Probe {
|
||||||
|
private final ServerLevel level;
|
||||||
|
private final Structure structure;
|
||||||
|
private final String structureKey;
|
||||||
|
private final boolean requireUnreferenced;
|
||||||
|
private final ProbeBudget budget;
|
||||||
|
private final Map<Long, Boolean> storedDecisions = new HashMap<>();
|
||||||
|
|
||||||
|
Probe(ServerLevel level, Structure structure, boolean requireUnreferenced,
|
||||||
|
ProbeBudget budget) {
|
||||||
|
this.level = level;
|
||||||
|
this.structure = structure;
|
||||||
|
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||||
|
Identifier identifier = registry.getKey(structure);
|
||||||
|
if (identifier == null) {
|
||||||
|
throw new IllegalStateException("Cannot probe an unregistered native structure");
|
||||||
|
}
|
||||||
|
this.structureKey = identifier.toString();
|
||||||
|
this.requireUnreferenced = requireUnreferenced;
|
||||||
|
this.budget = budget;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean accepts(int chunkX, int chunkZ) {
|
||||||
|
ChunkAccess loaded = level.getChunkSource().getChunk(
|
||||||
|
chunkX, chunkZ, ChunkStatus.STRUCTURE_STARTS, false);
|
||||||
|
if (loaded != null) {
|
||||||
|
StructureStart start = loaded.getStartForStructure(structure);
|
||||||
|
return start != null && start.isValid()
|
||||||
|
&& (!requireUnreferenced || start.canBeReferenced());
|
||||||
|
}
|
||||||
|
|
||||||
|
ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
|
||||||
|
Boolean storedDecision = storedDecisions.get(chunkPos.pack());
|
||||||
|
if (storedDecision != null) {
|
||||||
|
return storedDecision;
|
||||||
|
}
|
||||||
|
boolean accepted = budget.acceptsStored(
|
||||||
|
level, chunkPos, structureKey, requireUnreferenced);
|
||||||
|
storedDecisions.put(chunkPos.pack(), accepted);
|
||||||
|
return accepted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StructureStart verifySelected(int chunkX, int chunkZ) {
|
||||||
|
ChunkAccess chunk = level.getChunk(chunkX, chunkZ, ChunkStatus.STRUCTURE_STARTS);
|
||||||
|
StructureStart start = chunk.getStartForStructure(structure);
|
||||||
|
if (start == null || !start.isValid()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (requireUnreferenced) {
|
||||||
|
if (!start.canBeReferenced()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reference(StructureStart start) {
|
||||||
|
if (requireUnreferenced) {
|
||||||
|
level.structureManager().addReference(start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class ProbeBudget {
|
||||||
|
private final int maximum;
|
||||||
|
private final Map<Long, StoredChunkState> storedChunks = new HashMap<>();
|
||||||
|
private final ChunkStorageScanner scanner;
|
||||||
|
private final StoredChunkDatafixer datafixer;
|
||||||
|
private int used;
|
||||||
|
private boolean scanFailureReported;
|
||||||
|
|
||||||
|
private ProbeBudget(int maximum) {
|
||||||
|
this(maximum, null, ProbeBudget::datafix);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProbeBudget(int maximum, ChunkStorageScanner scanner) {
|
||||||
|
this(maximum, scanner, ProbeBudget::datafix);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProbeBudget(int maximum, ChunkStorageScanner scanner,
|
||||||
|
StoredChunkDatafixer datafixer) {
|
||||||
|
this.maximum = maximum;
|
||||||
|
this.scanner = scanner;
|
||||||
|
this.datafixer = Objects.requireNonNull(
|
||||||
|
datafixer, "Stored chunk datafixer must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void claim() {
|
||||||
|
if (used >= maximum) {
|
||||||
|
throw new IrisStructureLocator.CandidateSearchLimitException();
|
||||||
|
}
|
||||||
|
used++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private StoredChunkState storedState(ServerLevel level, ChunkPos chunkPos) {
|
||||||
|
StoredChunkState cached = storedChunks.get(chunkPos.pack());
|
||||||
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
claim();
|
||||||
|
CollectFields fields = new CollectFields(
|
||||||
|
new FieldSelector(IntTag.TYPE, "DataVersion"),
|
||||||
|
new FieldSelector("Level", "Structures", CompoundTag.TYPE, "Starts"),
|
||||||
|
new FieldSelector("structures", CompoundTag.TYPE, "starts"));
|
||||||
|
ChunkStorageScanner activeScanner = scanner == null
|
||||||
|
? level.getChunkSource().chunkMap.chunkScanner()::scanChunk : scanner;
|
||||||
|
try {
|
||||||
|
activeScanner.scan(chunkPos, fields).join();
|
||||||
|
} catch (RuntimeException error) {
|
||||||
|
if (!scanFailureReported) {
|
||||||
|
scanFailureReported = true;
|
||||||
|
IrisLogging.reportError(
|
||||||
|
"Native structure locate could not scan stored chunk state; candidates will be verified by loading their structure starts.",
|
||||||
|
error);
|
||||||
|
}
|
||||||
|
StoredChunkState unresolved = StoredChunkState.missing();
|
||||||
|
storedChunks.put(chunkPos.pack(), unresolved);
|
||||||
|
return unresolved;
|
||||||
|
}
|
||||||
|
Tag result = fields.getResult();
|
||||||
|
StoredChunkState resolved;
|
||||||
|
try {
|
||||||
|
resolved = result instanceof CompoundTag storedChunk
|
||||||
|
? StoredChunkState.parse(datafixer.datafix(level, storedChunk))
|
||||||
|
: StoredChunkState.missing();
|
||||||
|
} catch (RuntimeException error) {
|
||||||
|
if (!scanFailureReported) {
|
||||||
|
scanFailureReported = true;
|
||||||
|
IrisLogging.reportError(
|
||||||
|
"Native structure locate could not datafix stored chunk state; candidates will be verified by loading their structure starts.",
|
||||||
|
error);
|
||||||
|
}
|
||||||
|
resolved = StoredChunkState.missing();
|
||||||
|
}
|
||||||
|
storedChunks.put(chunkPos.pack(), resolved);
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CompoundTag datafix(ServerLevel level, CompoundTag storedChunk) {
|
||||||
|
int dataVersion = NbtUtils.getDataVersion(storedChunk);
|
||||||
|
int currentDataVersion = SharedConstants.getCurrentVersion().dataVersion().version();
|
||||||
|
if (dataVersion >= currentDataVersion) {
|
||||||
|
return storedChunk;
|
||||||
|
}
|
||||||
|
if (level == null) {
|
||||||
|
throw new IllegalStateException("Stored chunk datafix requires an active level");
|
||||||
|
}
|
||||||
|
CompoundTag context = chunkDataFixContext(level);
|
||||||
|
SimpleRegionStorage.injectDatafixingContext(storedChunk, context);
|
||||||
|
return DataFixTypes.CHUNK.updateToCurrentVersion(
|
||||||
|
level.getServer().getFixerUpper(), storedChunk, dataVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CompoundTag chunkDataFixContext(ServerLevel level) {
|
||||||
|
CompoundTag context = new CompoundTag();
|
||||||
|
String levelIdentifier = level.dimension().identifier().toString();
|
||||||
|
if (PAPER_LEVEL_TYPE_KEY == null) {
|
||||||
|
context.putString("dimension", levelIdentifier);
|
||||||
|
} else {
|
||||||
|
context.putString("dimension", paperLevelTypeKey(level).identifier().toString());
|
||||||
|
context.putString("level_identifier", levelIdentifier);
|
||||||
|
}
|
||||||
|
level.getChunkSource().getGenerator().getTypeNameForDataFixer()
|
||||||
|
.ifPresent(identifier -> context.putString("generator", identifier.toString()));
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean acceptsStored(ServerLevel level, ChunkPos chunkPos,
|
||||||
|
String structureKey, boolean requireUnreferenced) {
|
||||||
|
return storedState(level, chunkPos).accepts(structureKey, requireUnreferenced);
|
||||||
|
}
|
||||||
|
|
||||||
|
int used() {
|
||||||
|
return used;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Search {
|
||||||
|
private final Engine engine;
|
||||||
|
private final String structureKey;
|
||||||
|
private final int blockX;
|
||||||
|
private final int blockZ;
|
||||||
|
private final int radius;
|
||||||
|
private final Probe probe;
|
||||||
|
private final Set<Long> rejectedChunks = new HashSet<>();
|
||||||
|
|
||||||
|
private Search(Engine engine, String structureKey, int blockX, int blockZ,
|
||||||
|
int radius, Probe probe) {
|
||||||
|
this.engine = engine;
|
||||||
|
this.structureKey = structureKey;
|
||||||
|
this.blockX = blockX;
|
||||||
|
this.blockZ = blockZ;
|
||||||
|
this.radius = radius;
|
||||||
|
this.probe = probe;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IrisStructureLocator.LocateResult predict() {
|
||||||
|
if (rejectedChunks.size() >= MAX_SELECTED_CANDIDATE_RETRIES) {
|
||||||
|
return new IrisStructureLocator.LocateResult(
|
||||||
|
IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED, 0, 0, 0);
|
||||||
|
}
|
||||||
|
return IrisStructureLocator.locateInPlacementRings(
|
||||||
|
engine, structureKey, blockX, blockZ, radius,
|
||||||
|
(chunkX, chunkZ) -> !rejectedChunks.contains(ChunkPos.pack(chunkX, chunkZ))
|
||||||
|
&& probe.accepts(chunkX, chunkZ));
|
||||||
|
}
|
||||||
|
|
||||||
|
public VerifiedStart verify(IrisStructureLocator.LocateResult result) {
|
||||||
|
int chunkX = result.originX() >> 4;
|
||||||
|
int chunkZ = result.originZ() >> 4;
|
||||||
|
StructureStart start = probe.verifySelected(chunkX, chunkZ);
|
||||||
|
if (start == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipRecovery.resolve(
|
||||||
|
engine, probe.level, structureKey, probe.structure, start);
|
||||||
|
if (ownership == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new VerifiedStart(start, ownership);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reject(IrisStructureLocator.LocateResult result) {
|
||||||
|
rejectedChunks.add(ChunkPos.pack(result.originX() >> 4, result.originZ() >> 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reference(VerifiedStart verified) {
|
||||||
|
probe.reference(verified.start());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Method paperLevelTypeKey() {
|
||||||
|
try {
|
||||||
|
return ServerLevel.class.getMethod("getTypeKey");
|
||||||
|
} catch (NoSuchMethodException ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ResourceKey<?> paperLevelTypeKey(ServerLevel level) {
|
||||||
|
try {
|
||||||
|
Object value = PAPER_LEVEL_TYPE_KEY.invoke(level);
|
||||||
|
if (value instanceof ResourceKey<?> key) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Paper level type key has an unexpected value");
|
||||||
|
} catch (IllegalAccessException | InvocationTargetException error) {
|
||||||
|
throw new IllegalStateException("Cannot read Paper level type key", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record VerifiedStart(StructureStart start,
|
||||||
|
NativeStructureOwnershipRecord ownership) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
interface ChunkStorageScanner {
|
||||||
|
CompletableFuture<Void> scan(ChunkPos chunkPos, StreamTagVisitor visitor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
interface StoredChunkDatafixer {
|
||||||
|
CompoundTag datafix(ServerLevel level, CompoundTag storedChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record StoredChunkState(boolean stored, boolean startsPresent, CompoundTag starts) {
|
||||||
|
private static StoredChunkState missing() {
|
||||||
|
return new StoredChunkState(false, false, new CompoundTag());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StoredChunkState parse(CompoundTag chunk) {
|
||||||
|
CompoundTag structures = chunk.getCompoundOrEmpty("structures");
|
||||||
|
return structures.getCompound("starts")
|
||||||
|
.map(starts -> new StoredChunkState(true, true, starts))
|
||||||
|
.orElseGet(() -> new StoredChunkState(true, false, new CompoundTag()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean accepts(String structureKey, boolean requireUnreferenced) {
|
||||||
|
if (!stored || !startsPresent) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return starts.getCompound(structureKey).map(start -> {
|
||||||
|
String id = start.getStringOr("id", "");
|
||||||
|
return !StructureStart.INVALID_START_ID.equals(id)
|
||||||
|
&& (!requireUnreferenced || start.getIntOr("references", 0) == 0);
|
||||||
|
}).orElse(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Servers
|
||||||
|
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import com.mojang.datafixers.util.Pair;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
|
||||||
|
public final class NativeStructureLocateResults {
|
||||||
|
private NativeStructureLocateResults() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> Pair<BlockPos, T> nearest(BlockPos origin, Pair<BlockPos, T> replacement,
|
||||||
|
Pair<BlockPos, T> nativeResult) {
|
||||||
|
if (replacement == null) {
|
||||||
|
return nativeResult;
|
||||||
|
}
|
||||||
|
if (nativeResult == null) {
|
||||||
|
return replacement;
|
||||||
|
}
|
||||||
|
return horizontalDistanceSquared(origin, nativeResult.getFirst())
|
||||||
|
<= horizontalDistanceSquared(origin, replacement.getFirst()) ? nativeResult : replacement;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> Pair<BlockPos, T> selectAndReference(
|
||||||
|
BlockPos origin,
|
||||||
|
Pair<BlockPos, T> replacement, Runnable replacementReference,
|
||||||
|
Pair<BlockPos, T> nativeResult, Runnable nativeReference) {
|
||||||
|
Pair<BlockPos, T> selected = nearest(origin, replacement, nativeResult);
|
||||||
|
if (selected == replacement && replacement != null) {
|
||||||
|
replacementReference.run();
|
||||||
|
} else if (selected == nativeResult && nativeResult != null) {
|
||||||
|
nativeReference.run();
|
||||||
|
}
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long horizontalDistanceSquared(BlockPos origin, BlockPos target) {
|
||||||
|
long dx = (long) target.getX() - origin.getX();
|
||||||
|
long dz = (long) target.getZ() - origin.getZ();
|
||||||
|
return dx * dx + dz * dz;
|
||||||
|
}
|
||||||
|
}
|
||||||
+239
@@ -0,0 +1,239 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructurePlacementPlanner;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
|
||||||
|
import art.arcane.iris.structure.placement.StructurePlacementGrid;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.EmptyPoolElement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HexFormat;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class NativeStructureOwnershipFingerprint {
|
||||||
|
private static final Comparator<PieceIdentity> PIECE_ORDER = Comparator
|
||||||
|
.comparing(PieceIdentity::type)
|
||||||
|
.thenComparingInt(PieceIdentity::minX)
|
||||||
|
.thenComparingInt(PieceIdentity::minY)
|
||||||
|
.thenComparingInt(PieceIdentity::minZ)
|
||||||
|
.thenComparingInt(PieceIdentity::maxX)
|
||||||
|
.thenComparingInt(PieceIdentity::maxY)
|
||||||
|
.thenComparingInt(PieceIdentity::maxZ)
|
||||||
|
.thenComparingInt(PieceIdentity::orientation)
|
||||||
|
.thenComparingInt(PieceIdentity::generationDepth)
|
||||||
|
.thenComparing(PieceIdentity::detail);
|
||||||
|
|
||||||
|
private NativeStructureOwnershipFingerprint() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NativeStructureOwnershipRecord capture(String structureKey,
|
||||||
|
StructureStart start,
|
||||||
|
NativeStructureStartPlan plan,
|
||||||
|
BoundingBox referenceEnvelope) {
|
||||||
|
StructureStart resolvedStart = requireValid(start);
|
||||||
|
NativeStructureStartPlan resolvedPlan = Objects.requireNonNull(
|
||||||
|
plan, "Native structure start plan must not be null");
|
||||||
|
BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(resolvedStart);
|
||||||
|
BoundingBox references = Objects.requireNonNull(
|
||||||
|
referenceEnvelope, "Native structure reference envelope must not be null");
|
||||||
|
ChunkPos origin = resolvedStart.getChunkPos();
|
||||||
|
return NativeStructureOwnershipRecord.create(
|
||||||
|
structureKey,
|
||||||
|
origin.x(),
|
||||||
|
origin.z(),
|
||||||
|
StructurePlacementGrid.placementIdentity(resolvedPlan.placement()),
|
||||||
|
resolvedPlan.baseY(),
|
||||||
|
content.minX(),
|
||||||
|
content.minY(),
|
||||||
|
content.minZ(),
|
||||||
|
content.maxX(),
|
||||||
|
content.maxY(),
|
||||||
|
content.maxZ(),
|
||||||
|
locatorY(resolvedStart),
|
||||||
|
references.minX() >> 4,
|
||||||
|
references.maxX() >> 4,
|
||||||
|
references.minZ() >> 4,
|
||||||
|
references.maxZ() >> 4,
|
||||||
|
fingerprint(structureKey, resolvedStart),
|
||||||
|
NativeStructurePlacementPlanner.decisionFor(resolvedPlan)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean matches(NativeStructureOwnershipRecord record, StructureStart start) {
|
||||||
|
if (record == null || start == null || !start.isValid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ChunkPos origin = start.getChunkPos();
|
||||||
|
if (origin.x() != record.originChunkX()
|
||||||
|
|| origin.z() != record.originChunkZ()
|
||||||
|
|| !fingerprint(record.structureKey(), start).equals(record.contentFingerprint())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(start);
|
||||||
|
if (content.getXSpan() != (record.contentMaxX() - record.contentMinX()) + 1
|
||||||
|
|| content.getYSpan() != (record.contentMaxY() - record.contentMinY()) + 1
|
||||||
|
|| content.getZSpan() != (record.contentMaxZ() - record.contentMinZ()) + 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return start.getStructure() instanceof OceanMonumentStructure
|
||||||
|
|| content.minX() == record.contentMinX()
|
||||||
|
&& content.minY() == record.contentMinY()
|
||||||
|
&& content.minZ() == record.contentMinZ()
|
||||||
|
&& content.maxX() == record.contentMaxX()
|
||||||
|
&& content.maxY() == record.contentMaxY()
|
||||||
|
&& content.maxZ() == record.contentMaxZ();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String fingerprint(String structureKey, StructureStart start) {
|
||||||
|
StructureStart resolvedStart = requireValid(start);
|
||||||
|
MessageDigest digest = sha256();
|
||||||
|
updateString(digest, Objects.requireNonNull(structureKey,
|
||||||
|
"Native structure key must not be null").trim().toLowerCase(Locale.ROOT));
|
||||||
|
updateInt(digest, resolvedStart.getChunkPos().x());
|
||||||
|
updateInt(digest, resolvedStart.getChunkPos().z());
|
||||||
|
BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(resolvedStart);
|
||||||
|
List<PieceIdentity> pieces = contentPieces(resolvedStart, content.minY());
|
||||||
|
updateInt(digest, pieces.size());
|
||||||
|
for (PieceIdentity piece : pieces) {
|
||||||
|
updateString(digest, piece.type());
|
||||||
|
updateInt(digest, piece.minX());
|
||||||
|
updateInt(digest, piece.minY());
|
||||||
|
updateInt(digest, piece.minZ());
|
||||||
|
updateInt(digest, piece.maxX());
|
||||||
|
updateInt(digest, piece.maxY());
|
||||||
|
updateInt(digest, piece.maxZ());
|
||||||
|
updateInt(digest, piece.orientation());
|
||||||
|
updateInt(digest, piece.generationDepth());
|
||||||
|
updateString(digest, piece.detail());
|
||||||
|
}
|
||||||
|
return HexFormat.of().formatHex(digest.digest());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int locatorY(StructureStart start) {
|
||||||
|
StructureStart resolvedStart = requireValid(start);
|
||||||
|
if (!resolvedStart.getPieces().isEmpty()) {
|
||||||
|
return resolvedStart.getPieces().getFirst().getLocatorPosition().getY();
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Native structure contains no locatable content pieces");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<PieceIdentity> contentPieces(StructureStart start, int contentMinY) {
|
||||||
|
List<PieceIdentity> pieces = new ArrayList<>();
|
||||||
|
for (StructurePiece piece : start.getPieces()) {
|
||||||
|
BoundingBox bounds = piece.getBoundingBox();
|
||||||
|
Identifier type = BuiltInRegistries.STRUCTURE_PIECE.getKey(piece.getType());
|
||||||
|
Direction orientation = piece.getOrientation();
|
||||||
|
String detail = pieceDetail(piece, contentMinY);
|
||||||
|
pieces.add(new PieceIdentity(
|
||||||
|
type == null ? "" : type.toString(),
|
||||||
|
bounds.minX(), bounds.minY() - contentMinY, bounds.minZ(),
|
||||||
|
bounds.maxX(), bounds.maxY() - contentMinY, bounds.maxZ(),
|
||||||
|
orientation == null ? -1 : orientation.get2DDataValue(),
|
||||||
|
piece.getGenDepth(),
|
||||||
|
detail
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (pieces.isEmpty()) {
|
||||||
|
throw new IllegalStateException("Native structure contains no content pieces");
|
||||||
|
}
|
||||||
|
pieces.sort(PIECE_ORDER);
|
||||||
|
return List.copyOf(pieces);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String pieceDetail(StructurePiece piece, int contentMinY) {
|
||||||
|
if (!(piece instanceof PoolElementStructurePiece poolPiece)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return poolElementIdentity(poolPiece.getElement()) + "|"
|
||||||
|
+ poolPiece.getPosition().getX() + ","
|
||||||
|
+ (poolPiece.getPosition().getY() - contentMinY) + ","
|
||||||
|
+ poolPiece.getPosition().getZ() + "|"
|
||||||
|
+ poolPiece.getGroundLevelDelta() + "|"
|
||||||
|
+ poolPiece.getRotation().getSerializedName();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String poolElementIdentity(StructurePoolElement element) {
|
||||||
|
Identifier type = BuiltInRegistries.STRUCTURE_POOL_ELEMENT.getKey(element.getType());
|
||||||
|
StringBuilder identity = new StringBuilder(element.getClass().getName())
|
||||||
|
.append('|')
|
||||||
|
.append(type == null ? "" : type)
|
||||||
|
.append('|')
|
||||||
|
.append(element.getProjection().getSerializedName())
|
||||||
|
.append('|')
|
||||||
|
.append(element.getGroundLevelDelta());
|
||||||
|
if (element == EmptyPoolElement.INSTANCE) {
|
||||||
|
return identity.append("|empty").toString();
|
||||||
|
}
|
||||||
|
if (element instanceof SinglePoolElement single) {
|
||||||
|
try {
|
||||||
|
return identity.append("|template=")
|
||||||
|
.append(single.getTemplateLocation()).toString();
|
||||||
|
} catch (RuntimeException inlineTemplate) {
|
||||||
|
return identity.append("|runtime-template").toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (element instanceof ListPoolElement list) {
|
||||||
|
identity.append("|list[");
|
||||||
|
for (StructurePoolElement child : list.getElements()) {
|
||||||
|
String childIdentity = poolElementIdentity(child);
|
||||||
|
identity.append(childIdentity.length()).append(':').append(childIdentity);
|
||||||
|
}
|
||||||
|
return identity.append(']').toString();
|
||||||
|
}
|
||||||
|
return identity.append("|bounded-custom").toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureStart requireValid(StructureStart start) {
|
||||||
|
StructureStart resolved = Objects.requireNonNull(
|
||||||
|
start, "Native structure start must not be null");
|
||||||
|
if (!resolved.isValid()) {
|
||||||
|
throw new IllegalArgumentException("Native structure start must be valid");
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MessageDigest sha256() {
|
||||||
|
try {
|
||||||
|
return MessageDigest.getInstance("SHA-256");
|
||||||
|
} catch (NoSuchAlgorithmException error) {
|
||||||
|
throw new IllegalStateException("SHA-256 is unavailable", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void updateString(MessageDigest digest, String value) {
|
||||||
|
byte[] encoded = value.getBytes(StandardCharsets.UTF_8);
|
||||||
|
updateInt(digest, encoded.length);
|
||||||
|
digest.update(encoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void updateInt(MessageDigest digest, int value) {
|
||||||
|
digest.update((byte) (value >>> 24));
|
||||||
|
digest.update((byte) (value >>> 16));
|
||||||
|
digest.update((byte) (value >>> 8));
|
||||||
|
digest.update((byte) value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record PieceIdentity(String type,
|
||||||
|
int minX, int minY, int minZ,
|
||||||
|
int maxX, int maxY, int maxZ,
|
||||||
|
int orientation, int generationDepth, String detail) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+195
@@ -0,0 +1,195 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipStore;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructurePlacementPlanner;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
|
||||||
|
import art.arcane.iris.structure.placement.StructurePlacementGrid;
|
||||||
|
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||||
|
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureGenerationStatus;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureSuppression;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.Registry;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.biome.BiomeSource;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class NativeStructureOwnershipRecovery {
|
||||||
|
private NativeStructureOwnershipRecovery() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NativeStructureOwnershipRecord resolve(
|
||||||
|
Engine engine, ServerLevel level, String structureKey,
|
||||||
|
Structure structure, StructureStart start) {
|
||||||
|
Objects.requireNonNull(engine, "Native structure ownership recovery requires an engine");
|
||||||
|
ServerLevel activeLevel = Objects.requireNonNull(
|
||||||
|
level, "Native structure ownership recovery requires a level");
|
||||||
|
Structure activeStructure = Objects.requireNonNull(
|
||||||
|
structure, "Native structure ownership recovery requires a structure");
|
||||||
|
if (start == null || !start.isValid() || start.getStructure() != activeStructure) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ChunkPos origin = start.getChunkPos();
|
||||||
|
NativeStructureOwnershipRecord persisted = NativeStructureOwnershipStore.findPersisted(
|
||||||
|
engine, structureKey, origin.x(), origin.z());
|
||||||
|
if (persisted != null) {
|
||||||
|
NativeStructureOwnershipRecord refreshed = refreshReferenceEnvelope(
|
||||||
|
structureKey, activeStructure, start, persisted);
|
||||||
|
if (refreshed != null) {
|
||||||
|
if (refreshed != persisted) {
|
||||||
|
NativeStructureOwnershipStore.record(engine, refreshed);
|
||||||
|
}
|
||||||
|
return refreshed;
|
||||||
|
}
|
||||||
|
NativeStructureOwnershipStore.discard(
|
||||||
|
engine, structureKey, origin.x(), origin.z());
|
||||||
|
}
|
||||||
|
NativeStructureStartPlan plan = NativeStructurePlacementPlanner.matchingPlan(
|
||||||
|
engine, structureKey, origin.x(), origin.z());
|
||||||
|
if (!matchesPlan(structureKey, origin, plan)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Registry<Structure> registry = activeLevel.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||||
|
Holder<Structure> holder = registry.wrapAsHolder(activeStructure);
|
||||||
|
ChunkGeneratorStructureState state = activeLevel.getChunkSource().getGeneratorState();
|
||||||
|
if (naturalStartIsAmbiguous(engine, structureKey, activeStructure, holder, state, plan)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
StructureStart expected = generateExpected(
|
||||||
|
engine, activeLevel, holder, plan, start.getReferences());
|
||||||
|
NativeStructureOwnershipRecord recovered = proveCandidate(
|
||||||
|
structureKey, activeStructure, start, plan, expected, false);
|
||||||
|
if (recovered == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
NativeStructureOwnershipStore.record(engine, recovered);
|
||||||
|
return recovered;
|
||||||
|
}
|
||||||
|
|
||||||
|
static NativeStructureOwnershipRecord refreshReferenceEnvelope(
|
||||||
|
String structureKey, Structure structure, StructureStart start,
|
||||||
|
NativeStructureOwnershipRecord ownership) {
|
||||||
|
if (structure == null || start == null || !start.isValid()
|
||||||
|
|| start.getStructure() != structure || ownership == null
|
||||||
|
|| !ownership.structureKey().equals(normalize(structureKey))
|
||||||
|
|| !NativeStructureOwnershipFingerprint.matches(ownership, start)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
IrisStructureTerrain terrain = NativeStructureTerrainIntegrator.resolveNativeTerrain(
|
||||||
|
start, ownership.restoredDecision().terrain());
|
||||||
|
if (terrain.resolvedMode() != IrisStructureTerrainMode.VACUUM) {
|
||||||
|
return ownership;
|
||||||
|
}
|
||||||
|
BoundingBox expected = NativeStructureReferenceEnvelope.referenceBounds(
|
||||||
|
start, structure, terrain, structureKey);
|
||||||
|
int referenceMinChunkX = expected.minX() >> 4;
|
||||||
|
int referenceMaxChunkX = expected.maxX() >> 4;
|
||||||
|
int referenceMinChunkZ = expected.minZ() >> 4;
|
||||||
|
int referenceMaxChunkZ = expected.maxZ() >> 4;
|
||||||
|
if (ownership.referenceMinChunkX() == referenceMinChunkX
|
||||||
|
&& ownership.referenceMaxChunkX() == referenceMaxChunkX
|
||||||
|
&& ownership.referenceMinChunkZ() == referenceMinChunkZ
|
||||||
|
&& ownership.referenceMaxChunkZ() == referenceMaxChunkZ) {
|
||||||
|
return ownership;
|
||||||
|
}
|
||||||
|
return ownership.withReferenceEnvelope(
|
||||||
|
referenceMinChunkX, referenceMaxChunkX,
|
||||||
|
referenceMinChunkZ, referenceMaxChunkZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
static NativeStructureOwnershipRecord proveCandidate(
|
||||||
|
String structureKey, Structure structure, StructureStart persisted,
|
||||||
|
NativeStructureStartPlan plan, StructureStart expected,
|
||||||
|
boolean naturalStartAmbiguous) {
|
||||||
|
if (naturalStartAmbiguous || structure == null
|
||||||
|
|| persisted == null || !persisted.isValid()
|
||||||
|
|| expected == null || !expected.isValid()
|
||||||
|
|| persisted.getStructure() != structure
|
||||||
|
|| expected.getStructure() != structure
|
||||||
|
|| !matchesPlan(structureKey, persisted.getChunkPos(), plan)
|
||||||
|
|| !persisted.getChunkPos().equals(expected.getChunkPos())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
BoundingBox referenceEnvelope = NativeStructureReferenceEnvelope.referenceBounds(
|
||||||
|
expected, structure, plan.placement().resolvedTerrain(), structureKey);
|
||||||
|
NativeStructureOwnershipRecord candidate = NativeStructureOwnershipFingerprint.capture(
|
||||||
|
structureKey, expected, plan, referenceEnvelope);
|
||||||
|
if (candidate.placementIdentity()
|
||||||
|
!= StructurePlacementGrid.placementIdentity(plan.placement())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return NativeStructureOwnershipFingerprint.matches(candidate, persisted)
|
||||||
|
? candidate : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean matchesPlan(String structureKey, ChunkPos origin,
|
||||||
|
NativeStructureStartPlan plan) {
|
||||||
|
if (structureKey == null || structureKey.isBlank() || plan == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return plan.chunkX() == origin.x()
|
||||||
|
&& plan.chunkZ() == origin.z()
|
||||||
|
&& normalize(structureKey).equals(normalize(plan.source().getStructure()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureStart generateExpected(
|
||||||
|
Engine engine, ServerLevel level, Holder<Structure> holder,
|
||||||
|
NativeStructureStartPlan plan, int references) {
|
||||||
|
ChunkGeneratorStructureState state = level.getChunkSource().getGeneratorState();
|
||||||
|
ChunkGenerator generator = level.getChunkSource().getGenerator();
|
||||||
|
BiomeSource biomeSource = generator.getBiomeSource();
|
||||||
|
NativeStructureFactory.GenerationContext generationContext =
|
||||||
|
new NativeStructureFactory.GenerationContext(
|
||||||
|
level.registryAccess(),
|
||||||
|
generator,
|
||||||
|
biomeSource,
|
||||||
|
state.randomState(),
|
||||||
|
level.getStructureTemplateManager(),
|
||||||
|
state.getLevelSeed(),
|
||||||
|
level.dimension(),
|
||||||
|
level,
|
||||||
|
biome -> true,
|
||||||
|
generator.getSeaLevel(),
|
||||||
|
(x, z) -> Engine.hostHeight(engine, x, z, true) + engine.getMinHeight()
|
||||||
|
);
|
||||||
|
return NativeStructureFactory.generate(generationContext, holder, plan, references);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean naturalStartIsAmbiguous(
|
||||||
|
Engine engine, String structureKey, Structure structure,
|
||||||
|
Holder<Structure> holder, ChunkGeneratorStructureState state,
|
||||||
|
NativeStructureStartPlan plan) {
|
||||||
|
if (plan.placement().getNativeSuppression() == NativeStructureSuppression.REPLACE_SOURCE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
NativeStructureGenerationStatus sourceStatus = NativeStructureGenerationPolicy.resolve(
|
||||||
|
engine, structureKey,
|
||||||
|
NativeStructureVegetationClearer.isUndergroundStep(structure.step())).status();
|
||||||
|
if (sourceStatus == NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (StructurePlacement placement : state.getPlacementsForStructure(holder)) {
|
||||||
|
if (placement.isStructureChunk(state, plan.chunkX(), plan.chunkZ())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalize(String structureKey) {
|
||||||
|
return structureKey == null ? "" : structureKey.trim().toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
}
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
|
||||||
|
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
|
||||||
|
import art.arcane.iris.structure.placement.IrisStructureStiltSettings;
|
||||||
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.StructureManager;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
|
import net.minecraft.world.level.levelgen.WorldgenRandom;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.IntBinaryOperator;
|
||||||
|
|
||||||
|
public final class NativeStructurePostProcessor {
|
||||||
|
private NativeStructurePostProcessor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void place(WorldGenLevel world, StructureManager structureManager, ChunkGenerator generator,
|
||||||
|
WorldgenRandom random, BoundingBox area, ChunkPos chunkPos, String structureId,
|
||||||
|
StructureStart start, IrisNativeStructureDecision decision,
|
||||||
|
PaletteBlockResolver paletteBlockResolver,
|
||||||
|
IntBinaryOperator surfaceHeight) {
|
||||||
|
IrisStructureStiltSettings stilt = decision.stilt();
|
||||||
|
NativeStructureVerticalPlacer.ensureMonumentSeaLevelAlignment(start, structureId, decision.yShift(),
|
||||||
|
generator.getSeaLevel(), area.minY(), area.maxY() + 1);
|
||||||
|
start.placeInChunk(world, structureManager, generator, random, area, chunkPos);
|
||||||
|
if (stilt != null) {
|
||||||
|
NativeStructureFoundationBuilder.placeStilts(world, area, structureId, start, stilt,
|
||||||
|
paletteBlockResolver, surfaceHeight,
|
||||||
|
!NativeStructureVegetationClearer.isUndergroundStep(start.getStructure().step()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void prepareTerrain(WorldGenLevel world, BoundingBox area,
|
||||||
|
List<NativeStructureTerrainIntegrator.TerrainTarget> targets,
|
||||||
|
PaletteBlockResolver paletteBlockResolver) {
|
||||||
|
if (targets == null || targets.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceTerrain =
|
||||||
|
NativeStructureTerrainIntegrator.captureSourceTerrain(world, area, targets);
|
||||||
|
for (NativeStructureTerrainIntegrator.TerrainTarget target : targets) {
|
||||||
|
NativeStructureTerrainIntegrator.integrateTerrain(world, area, target.structureId(), target.start(),
|
||||||
|
target.terrain(), paletteBlockResolver, sourceTerrain);
|
||||||
|
}
|
||||||
|
for (NativeStructureTerrainIntegrator.TerrainTarget target : targets) {
|
||||||
|
if (NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
|
||||||
|
target.start(), target.terrain())) {
|
||||||
|
NativeStructureTerrainIntegrator.clearLegacyTemplateAir(
|
||||||
|
world, area, target.start(), () -> world.getLevel().getStructureTemplateManager());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport(
|
||||||
|
world, area, targets, () -> world.getLevel().getStructureTemplateManager());
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface PaletteBlockResolver {
|
||||||
|
BlockState resolve(IrisMaterialPalette palette, RNG rng, int x, int y, int z);
|
||||||
|
}
|
||||||
|
}
|
||||||
+239
@@ -0,0 +1,239 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||||
|
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public final class NativeStructureReferenceEnvelope {
|
||||||
|
private static final int MAX_REFERENCE_DISTANCE_CHUNKS = 8;
|
||||||
|
private static final Set<String> WARNED_CLIPPED_TERRAIN = ConcurrentHashMap.newKeySet();
|
||||||
|
private static final Set<String> WARNED_SKIPPED_CONTENT = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
|
private NativeStructureReferenceEnvelope() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StructureStart wrap(StructureStart generated, Structure source, int references,
|
||||||
|
IrisStructureTerrain terrain) {
|
||||||
|
return wrap(generated, source, references, terrain, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StructureStart wrap(StructureStart generated, Structure source, int references,
|
||||||
|
IrisStructureTerrain terrain, String structureKey) {
|
||||||
|
referenceBounds(generated, source, terrain, structureKey);
|
||||||
|
return new StructureStart(
|
||||||
|
source,
|
||||||
|
generated.getChunkPos(),
|
||||||
|
references,
|
||||||
|
new PiecesContainer(List.copyOf(generated.getPieces()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StructureStart wrapForPublication(
|
||||||
|
StructureStart generated, Structure source, int references,
|
||||||
|
IrisStructureTerrain terrain, String structureKey) {
|
||||||
|
try {
|
||||||
|
return wrap(generated, source, references, terrain, structureKey);
|
||||||
|
} catch (UnrepresentableContentException exception) {
|
||||||
|
warnSkippedContent(generated, source, structureKey);
|
||||||
|
return StructureStart.INVALID_START;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BoundingBox referenceBounds(StructureStart start, Structure source,
|
||||||
|
IrisStructureTerrain terrain) {
|
||||||
|
return referenceBounds(start, source, terrain, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BoundingBox referenceBounds(StructureStart start, Structure source,
|
||||||
|
IrisStructureTerrain terrain,
|
||||||
|
String structureKey) {
|
||||||
|
IrisStructureTerrainMode mode = terrain == null
|
||||||
|
? IrisStructureTerrainMode.PRESERVE : terrain.resolvedMode();
|
||||||
|
boolean usesEnvelope = mode == IrisStructureTerrainMode.BORE
|
||||||
|
|| mode == IrisStructureTerrainMode.FORCE_CARVE
|
||||||
|
|| mode == IrisStructureTerrainMode.VACUUM
|
||||||
|
|| mode == IrisStructureTerrainMode.FLATTEN
|
||||||
|
|| mode == IrisStructureTerrainMode.ENCASE;
|
||||||
|
int horizontalPadding = mode == IrisStructureTerrainMode.VACUUM
|
||||||
|
? NativeStructureSurfaceFitter.surfaceTerrainRadius()
|
||||||
|
: usesEnvelope ? Math.max(0, terrain.getHorizontalPadding()) : 0;
|
||||||
|
BoundingBox content = contentBounds(start);
|
||||||
|
if (!fitsReferenceRange(start.getChunkPos(), content)) {
|
||||||
|
throw new UnrepresentableContentException("Native structure content at "
|
||||||
|
+ start.getChunkPos().x() + "," + start.getChunkPos().z()
|
||||||
|
+ " exceeds Minecraft's " + MAX_REFERENCE_DISTANCE_CHUNKS
|
||||||
|
+ "-chunk structure reference range");
|
||||||
|
}
|
||||||
|
BoundingBox adjustedEnvelope;
|
||||||
|
boolean arithmeticClipped = false;
|
||||||
|
try {
|
||||||
|
BoundingBox envelope = horizontalPadding == 0
|
||||||
|
? content
|
||||||
|
: new BoundingBox(
|
||||||
|
Math.subtractExact(content.minX(), horizontalPadding),
|
||||||
|
content.minY(),
|
||||||
|
Math.subtractExact(content.minZ(), horizontalPadding),
|
||||||
|
Math.addExact(content.maxX(), horizontalPadding),
|
||||||
|
content.maxY(),
|
||||||
|
Math.addExact(content.maxZ(), horizontalPadding)
|
||||||
|
);
|
||||||
|
adjustedEnvelope = mode == IrisStructureTerrainMode.SOURCE
|
||||||
|
? source.adjustBoundingBox(content) : envelope;
|
||||||
|
adjustedEnvelope = encapsulating(adjustedEnvelope, content);
|
||||||
|
} catch (ArithmeticException exception) {
|
||||||
|
adjustedEnvelope = maximumReferenceBounds(start.getChunkPos(), content);
|
||||||
|
arithmeticClipped = true;
|
||||||
|
}
|
||||||
|
BoundingBox referencedEnvelope = clampReferenceRange(start.getChunkPos(), adjustedEnvelope);
|
||||||
|
if (arithmeticClipped || !sameHorizontalBounds(adjustedEnvelope, referencedEnvelope)) {
|
||||||
|
warnClippedTerrain(start, source, structureKey);
|
||||||
|
return referencedEnvelope;
|
||||||
|
}
|
||||||
|
return adjustedEnvelope;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean contentFitsReferenceRange(StructureStart start) {
|
||||||
|
try {
|
||||||
|
return fitsReferenceRange(start.getChunkPos(), contentBounds(start));
|
||||||
|
} catch (UnrepresentableContentException exception) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BoundingBox contentBounds(StructureStart start) {
|
||||||
|
return contentBounds(start.getPieces());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BoundingBox contentBounds(List<StructurePiece> pieces) {
|
||||||
|
BoundingBox bounds = null;
|
||||||
|
for (StructurePiece piece : pieces) {
|
||||||
|
bounds = bounds == null
|
||||||
|
? copy(piece.getBoundingBox())
|
||||||
|
: bounds.encapsulate(piece.getBoundingBox());
|
||||||
|
}
|
||||||
|
if (bounds == null) {
|
||||||
|
throw new IllegalStateException("Native jigsaw generated no content pieces");
|
||||||
|
}
|
||||||
|
return bounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BoundingBox clampReferenceRange(ChunkPos startChunk, BoundingBox envelope) {
|
||||||
|
int minX = minimumReferenceBlock(startChunk.x());
|
||||||
|
int maxX = maximumReferenceBlock(startChunk.x());
|
||||||
|
int minZ = minimumReferenceBlock(startChunk.z());
|
||||||
|
int maxZ = maximumReferenceBlock(startChunk.z());
|
||||||
|
return new BoundingBox(
|
||||||
|
Math.max(envelope.minX(), minX),
|
||||||
|
envelope.minY(),
|
||||||
|
Math.max(envelope.minZ(), minZ),
|
||||||
|
Math.min(envelope.maxX(), maxX),
|
||||||
|
envelope.maxY(),
|
||||||
|
Math.min(envelope.maxZ(), maxZ)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BoundingBox maximumReferenceBounds(ChunkPos startChunk, BoundingBox content) {
|
||||||
|
return new BoundingBox(
|
||||||
|
minimumReferenceBlock(startChunk.x()), content.minY(),
|
||||||
|
minimumReferenceBlock(startChunk.z()),
|
||||||
|
maximumReferenceBlock(startChunk.x()), content.maxY(),
|
||||||
|
maximumReferenceBlock(startChunk.z())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean fitsReferenceRange(ChunkPos startChunk, BoundingBox content) {
|
||||||
|
return content.minX() >= minimumReferenceBlock(startChunk.x())
|
||||||
|
&& content.maxX() <= maximumReferenceBlock(startChunk.x())
|
||||||
|
&& content.minZ() >= minimumReferenceBlock(startChunk.z())
|
||||||
|
&& content.maxZ() <= maximumReferenceBlock(startChunk.z());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int minimumReferenceBlock(int originChunk) {
|
||||||
|
return checkedReferenceBlock(
|
||||||
|
((long) originChunk - MAX_REFERENCE_DISTANCE_CHUNKS) << 4, originChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int maximumReferenceBlock(int originChunk) {
|
||||||
|
return checkedReferenceBlock(
|
||||||
|
(((long) originChunk + MAX_REFERENCE_DISTANCE_CHUNKS) << 4) + 15L, originChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int checkedReferenceBlock(long block, int originChunk) {
|
||||||
|
if (block < Integer.MIN_VALUE || block > Integer.MAX_VALUE) {
|
||||||
|
throw new UnrepresentableContentException("Native structure origin chunk " + originChunk
|
||||||
|
+ " exceeds Minecraft's block coordinate range");
|
||||||
|
}
|
||||||
|
return (int) block;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean sameHorizontalBounds(BoundingBox left, BoundingBox right) {
|
||||||
|
return left.minX() == right.minX()
|
||||||
|
&& left.minZ() == right.minZ()
|
||||||
|
&& left.maxX() == right.maxX()
|
||||||
|
&& left.maxZ() == right.maxZ();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BoundingBox copy(BoundingBox bounds) {
|
||||||
|
return new BoundingBox(
|
||||||
|
bounds.minX(), bounds.minY(), bounds.minZ(),
|
||||||
|
bounds.maxX(), bounds.maxY(), bounds.maxZ());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BoundingBox encapsulating(BoundingBox left, BoundingBox right) {
|
||||||
|
return new BoundingBox(
|
||||||
|
Math.min(left.minX(), right.minX()),
|
||||||
|
Math.min(left.minY(), right.minY()),
|
||||||
|
Math.min(left.minZ(), right.minZ()),
|
||||||
|
Math.max(left.maxX(), right.maxX()),
|
||||||
|
Math.max(left.maxY(), right.maxY()),
|
||||||
|
Math.max(left.maxZ(), right.maxZ())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void warnClippedTerrain(
|
||||||
|
StructureStart start, Structure source, String structureKey) {
|
||||||
|
String key = warningKey(source, structureKey);
|
||||||
|
if (WARNED_CLIPPED_TERRAIN.add(key)) {
|
||||||
|
IrisLogging.warn("Clipping optional terrain envelope for native structure '"
|
||||||
|
+ key + "' at " + start.getChunkPos().x() + "," + start.getChunkPos().z()
|
||||||
|
+ " to Minecraft's " + MAX_REFERENCE_DISTANCE_CHUNKS
|
||||||
|
+ "-chunk reference range; all generated content remains exactly referenced");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void warnSkippedContent(
|
||||||
|
StructureStart start, Structure source, String structureKey) {
|
||||||
|
String key = warningKey(source, structureKey);
|
||||||
|
if (WARNED_SKIPPED_CONTENT.add(key)) {
|
||||||
|
IrisLogging.warn("Skipping native structure '" + key + "' at "
|
||||||
|
+ start.getChunkPos().x() + "," + start.getChunkPos().z()
|
||||||
|
+ " because its generated content exceeds Minecraft's "
|
||||||
|
+ MAX_REFERENCE_DISTANCE_CHUNKS
|
||||||
|
+ "-chunk reference range; no structure start, ownership, references, or blocks were published");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String warningKey(Structure source, String structureKey) {
|
||||||
|
if (structureKey != null && !structureKey.isBlank()) {
|
||||||
|
return structureKey.trim().toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
return source.getClass().getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class UnrepresentableContentException extends IllegalStateException {
|
||||||
|
private UnrepresentableContentException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+246
@@ -0,0 +1,246 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipStore;
|
||||||
|
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||||
|
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||||
|
import it.unimi.dsi.fastutil.longs.LongSet;
|
||||||
|
import net.minecraft.core.Registry;
|
||||||
|
import net.minecraft.core.SectionPos;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.StructureManager;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public final class NativeStructureReferenceRepair {
|
||||||
|
private static final int REFERENCE_DISTANCE_CHUNKS =
|
||||||
|
NativeStructureOwnershipRecord.MAX_REFERENCE_DISTANCE_CHUNKS;
|
||||||
|
private static final Set<String> WARNED_POLICY_INVALIDATIONS = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
|
private NativeStructureReferenceRepair() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void createReferences(Engine engine, WorldGenLevel level,
|
||||||
|
StructureManager structureManager, ChunkAccess targetChunk) {
|
||||||
|
ChunkPos target = targetChunk.getPos();
|
||||||
|
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||||
|
ServerLevel serverLevel = level.getLevel();
|
||||||
|
List<ScannedStart> starts = new ArrayList<>();
|
||||||
|
for (int originChunkX = target.x() - REFERENCE_DISTANCE_CHUNKS;
|
||||||
|
originChunkX <= target.x() + REFERENCE_DISTANCE_CHUNKS; originChunkX++) {
|
||||||
|
for (int originChunkZ = target.z() - REFERENCE_DISTANCE_CHUNKS;
|
||||||
|
originChunkZ <= target.z() + REFERENCE_DISTANCE_CHUNKS; originChunkZ++) {
|
||||||
|
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
|
||||||
|
openOriginRuntimeScope(engine, originChunkX, originChunkZ)) {
|
||||||
|
ChunkAccess originChunk = level.getChunk(
|
||||||
|
originChunkX, originChunkZ, ChunkStatus.STRUCTURE_STARTS);
|
||||||
|
for (Map.Entry<Structure, StructureStart> entry : originChunk.getAllStarts().entrySet()) {
|
||||||
|
ScannedStart start = scanStart(
|
||||||
|
engine, serverLevel, structureManager, targetChunk, registry,
|
||||||
|
originChunk, entry.getKey(), entry.getValue());
|
||||||
|
if (start != null && isTargetRelevant(engine, targetChunk, start)) {
|
||||||
|
starts.add(start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (ScannedStart start : starts) {
|
||||||
|
boolean valid;
|
||||||
|
synchronized (start.originChunk()) {
|
||||||
|
StructureStart current = start.originChunk().getStartForStructure(start.structure());
|
||||||
|
valid = current == start.start() && current.isValid();
|
||||||
|
}
|
||||||
|
if (!valid) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
structureManager.addReferenceForStructure(
|
||||||
|
start.structure(),
|
||||||
|
start.origin().pack(), targetChunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GenerationHistoryRuntimeRouter.CoordinateScope openOriginRuntimeScope(
|
||||||
|
Engine engine,
|
||||||
|
int chunkX,
|
||||||
|
int chunkZ
|
||||||
|
) {
|
||||||
|
if (!(engine instanceof IrisEngine irisEngine)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return irisEngine.openGenerationHistoryCoordinateScope(chunkX << 4, chunkZ << 4);
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw new IllegalStateException("Unable to resolve native structure references for origin "
|
||||||
|
+ chunkX + "," + chunkZ + " through generation history.", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isTargetRelevant(
|
||||||
|
Engine engine, ChunkAccess targetChunk, ScannedStart scanned) {
|
||||||
|
StructureStart start = scanned.start();
|
||||||
|
Structure structure = scanned.structure();
|
||||||
|
ChunkPos target = targetChunk.getPos();
|
||||||
|
if (scanned.ownership() != null) {
|
||||||
|
return requiresReference(target, scanned.structureKey(), start, scanned.ownership());
|
||||||
|
}
|
||||||
|
if (scanned.registered()) {
|
||||||
|
return requiresNaturalReference(
|
||||||
|
engine, target, scanned.structureKey(), structure, start);
|
||||||
|
}
|
||||||
|
return NativeStructureReferenceEnvelope.contentBounds(start).intersects(
|
||||||
|
target.getMinBlockX(), target.getMinBlockZ(),
|
||||||
|
target.getMaxBlockX(), target.getMaxBlockZ());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ScannedStart scanStart(
|
||||||
|
Engine engine, ServerLevel level, StructureManager structureManager,
|
||||||
|
ChunkAccess targetChunk, Registry<Structure> registry,
|
||||||
|
ChunkAccess originChunk, Structure structure, StructureStart start) {
|
||||||
|
Identifier identifier = registry.getKey(structure);
|
||||||
|
String structureKey = identifier == null
|
||||||
|
? structure.getClass().getName() : identifier.toString();
|
||||||
|
if (start == null || !start.isValid()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!NativeStructureReferenceEnvelope.contentFitsReferenceRange(start)) {
|
||||||
|
if (identifier != null) {
|
||||||
|
NativeStructureOwnershipStore.discard(
|
||||||
|
engine, structureKey, start.getChunkPos().x(), start.getChunkPos().z());
|
||||||
|
}
|
||||||
|
invalidateStart(structureManager, originChunk, structure, start, targetChunk);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ChunkPos startOrigin = start.getChunkPos();
|
||||||
|
NativeStructureOwnershipRecord ownership = identifier == null ? null
|
||||||
|
: NativeStructureOwnershipRecovery.resolve(
|
||||||
|
engine, level, structureKey, structure, start);
|
||||||
|
if (ownership != null) {
|
||||||
|
return new ScannedStart(
|
||||||
|
originChunk, structure, start, structureKey, ownership, true);
|
||||||
|
}
|
||||||
|
if (identifier != null && !naturalPolicyAllows(engine, structureKey, structure)) {
|
||||||
|
invalidateStart(structureManager, originChunk, structure, start, targetChunk);
|
||||||
|
if (WARNED_POLICY_INVALIDATIONS.add(structureKey)) {
|
||||||
|
IrisLogging.warn("Invalidating persisted natural structure start '"
|
||||||
|
+ structureKey + "' because the current Iris dimension policy disables it");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new ScannedStart(
|
||||||
|
originChunk, structure, start, structureKey, null, identifier != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean naturalPolicyAllows(
|
||||||
|
Engine engine, String structureKey, Structure structure) {
|
||||||
|
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(
|
||||||
|
engine, structureKey,
|
||||||
|
NativeStructureVegetationClearer.isUndergroundStep(structure.step()));
|
||||||
|
return naturalDecisionAllows(decision);
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean naturalDecisionAllows(IrisNativeStructureDecision decision) {
|
||||||
|
return Objects.requireNonNull(
|
||||||
|
decision, "Native structure decision must not be null").generate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void invalidateStart(
|
||||||
|
StructureManager structureManager, ChunkAccess originChunk,
|
||||||
|
Structure structure, StructureStart start, ChunkAccess targetChunk) {
|
||||||
|
synchronized (originChunk) {
|
||||||
|
StructureStart current = originChunk.getStartForStructure(structure);
|
||||||
|
if (current == start && current.isValid()) {
|
||||||
|
structureManager.setStartForStructure(
|
||||||
|
structure,
|
||||||
|
StructureStart.INVALID_START, originChunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetChunk != null) {
|
||||||
|
removeTargetReference(targetChunk, structure, start.getChunkPos().pack());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void removeTargetReference(
|
||||||
|
ChunkAccess targetChunk, Structure structure, long origin) {
|
||||||
|
LongSet current = targetChunk.getAllReferences().get(structure);
|
||||||
|
if (current == null || !current.contains(origin)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<Structure, LongSet> updated = new HashMap<>(targetChunk.getAllReferences());
|
||||||
|
LongSet retained = new LongOpenHashSet(current);
|
||||||
|
retained.remove(origin);
|
||||||
|
if (retained.isEmpty()) {
|
||||||
|
updated.remove(structure);
|
||||||
|
} else {
|
||||||
|
updated.put(structure, retained);
|
||||||
|
}
|
||||||
|
targetChunk.setAllReferences(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean requiresNaturalReference(Engine engine, ChunkPos target, String structureKey,
|
||||||
|
Structure structure, StructureStart start) {
|
||||||
|
if (target == null || start == null || !start.isValid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(
|
||||||
|
engine, structureKey,
|
||||||
|
NativeStructureVegetationClearer.isUndergroundStep(structure.step()));
|
||||||
|
if (!decision.generate()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
BoundingBox referenceBounds = NativeStructureReferenceEnvelope.referenceBounds(
|
||||||
|
start, structure,
|
||||||
|
NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain()),
|
||||||
|
structureKey);
|
||||||
|
return referenceBounds.intersects(
|
||||||
|
target.getMinBlockX(), target.getMinBlockZ(),
|
||||||
|
target.getMaxBlockX(), target.getMaxBlockZ());
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean requiresReference(ChunkPos target, String structureKey,
|
||||||
|
StructureStart start,
|
||||||
|
NativeStructureOwnershipRecord ownership) {
|
||||||
|
if (target == null || start == null || !start.isValid()
|
||||||
|
|| ownership == null || !ownership.structureKey().equals(structureKey)
|
||||||
|
|| !ownership.covers(target.x(), target.z())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return NativeStructureOwnershipFingerprint.matches(ownership, start);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ScannedStart(
|
||||||
|
ChunkAccess originChunk,
|
||||||
|
Structure structure,
|
||||||
|
StructureStart start,
|
||||||
|
String structureKey,
|
||||||
|
NativeStructureOwnershipRecord ownership,
|
||||||
|
boolean registered
|
||||||
|
) {
|
||||||
|
private ChunkPos origin() {
|
||||||
|
return ownership == null
|
||||||
|
? start.getChunkPos()
|
||||||
|
: new ChunkPos(ownership.originChunkX(), ownership.originChunkZ());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+386
@@ -0,0 +1,386 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import com.mojang.datafixers.util.Either;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.world.level.ServerLevelAccessor;
|
||||||
|
import net.minecraft.world.level.block.Rotation;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.ScatteredFeaturePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.RuinedPortalPiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.lang.reflect.ParameterizedType;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
final class NativeStructureReflection {
|
||||||
|
private NativeStructureReflection() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static Field resolveScatteredHeightPositionField() {
|
||||||
|
Field resolved = null;
|
||||||
|
for (Field field : ScatteredFeaturePiece.class.getDeclaredFields()) {
|
||||||
|
int modifiers = field.getModifiers();
|
||||||
|
if (Modifier.isStatic(modifiers) || field.getType() != int.class
|
||||||
|
|| !Modifier.isProtected(modifiers) || Modifier.isFinal(modifiers)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (resolved != null) {
|
||||||
|
throw new IllegalStateException("ScatteredFeaturePiece has multiple mutable protected int fields");
|
||||||
|
}
|
||||||
|
resolved = field;
|
||||||
|
}
|
||||||
|
if (resolved == null) {
|
||||||
|
throw new IllegalStateException("ScatteredFeaturePiece height-position field is missing");
|
||||||
|
}
|
||||||
|
if (!resolved.trySetAccessible()) {
|
||||||
|
throw new IllegalStateException("ScatteredFeaturePiece height-position field is inaccessible");
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
static RuinedPortalPiece.VerticalPlacement ruinedPortalVerticalPlacement(RuinedPortalPiece piece) {
|
||||||
|
try {
|
||||||
|
return (RuinedPortalPiece.VerticalPlacement) RuinedPortalPlacementAccess.FIELD.get(piece);
|
||||||
|
} catch (IllegalAccessException error) {
|
||||||
|
throw new IllegalStateException("Cannot read native ruined portal vertical placement", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Field resolveRuinedPortalPlacementField() {
|
||||||
|
Field resolved = null;
|
||||||
|
for (Field field : RuinedPortalPiece.class.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(field.getModifiers())
|
||||||
|
|| field.getType() != RuinedPortalPiece.VerticalPlacement.class) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (resolved != null) {
|
||||||
|
throw new IllegalStateException("RuinedPortalPiece has multiple vertical-placement fields");
|
||||||
|
}
|
||||||
|
resolved = field;
|
||||||
|
}
|
||||||
|
if (resolved == null || !resolved.trySetAccessible()) {
|
||||||
|
throw new IllegalStateException("RuinedPortalPiece vertical-placement field is inaccessible");
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Field resolveMonumentChildPiecesField() {
|
||||||
|
Field resolved = null;
|
||||||
|
for (Field field : OceanMonumentPieces.MonumentBuilding.class.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(field.getModifiers()) || field.getType() != List.class) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (resolved != null) {
|
||||||
|
throw new IllegalStateException("Ocean Monument has multiple instance List fields");
|
||||||
|
}
|
||||||
|
resolved = field;
|
||||||
|
}
|
||||||
|
if (resolved == null) {
|
||||||
|
throw new IllegalStateException("Ocean Monument child-pieces List field is missing");
|
||||||
|
}
|
||||||
|
if (!Modifier.isPrivate(resolved.getModifiers()) || !Modifier.isFinal(resolved.getModifiers())) {
|
||||||
|
throw new IllegalStateException("Ocean Monument child-pieces field has an unexpected access contract");
|
||||||
|
}
|
||||||
|
if (!resolved.trySetAccessible()) {
|
||||||
|
throw new IllegalStateException("Ocean Monument child-pieces field is inaccessible");
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Field resolveSinglePoolTemplateField() {
|
||||||
|
Field resolved = null;
|
||||||
|
for (Field field : SinglePoolElement.class.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(field.getModifiers()) || field.getType() != Either.class) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (resolved != null) {
|
||||||
|
throw new IllegalStateException("SinglePoolElement has multiple instance Either fields");
|
||||||
|
}
|
||||||
|
resolved = field;
|
||||||
|
}
|
||||||
|
if (resolved == null) {
|
||||||
|
throw new IllegalStateException("SinglePoolElement template Either field is missing");
|
||||||
|
}
|
||||||
|
if (!Modifier.isProtected(resolved.getModifiers()) || !Modifier.isFinal(resolved.getModifiers())) {
|
||||||
|
throw new IllegalStateException("SinglePoolElement template field has an unexpected access contract");
|
||||||
|
}
|
||||||
|
if (!resolved.trySetAccessible()) {
|
||||||
|
throw new IllegalStateException("SinglePoolElement template field is inaccessible");
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
static StructureTemplate resolveTemplate(SinglePoolElement element,
|
||||||
|
Supplier<StructureTemplateManager> templates) {
|
||||||
|
Object value;
|
||||||
|
try {
|
||||||
|
value = SinglePoolTemplateAccess.FIELD.get(element);
|
||||||
|
} catch (IllegalAccessException error) {
|
||||||
|
throw new IllegalStateException("Cannot read native structure pool template", error);
|
||||||
|
}
|
||||||
|
if (!(value instanceof Either<?, ?> reference)) {
|
||||||
|
throw new IllegalStateException("Native structure pool template field is not an Either");
|
||||||
|
}
|
||||||
|
return resolveTemplateReference(reference, templates);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
static List<StructureTemplate.StructureBlockInfo> resolveTemplateBlocks(
|
||||||
|
StructureTemplate template, StructurePlaceSettings settings,
|
||||||
|
BlockPos origin) {
|
||||||
|
Object value;
|
||||||
|
try {
|
||||||
|
value = StructureTemplatePalettesAccess.FIELD.get(template);
|
||||||
|
} catch (IllegalAccessException error) {
|
||||||
|
throw new IllegalStateException("Cannot read native structure template palettes", error);
|
||||||
|
}
|
||||||
|
if (!(value instanceof List<?> paletteValues)) {
|
||||||
|
throw new IllegalStateException("Native structure template palettes field is not a List");
|
||||||
|
}
|
||||||
|
List<StructureTemplate.Palette> palettes =
|
||||||
|
(List<StructureTemplate.Palette>) paletteValues;
|
||||||
|
if (palettes.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
return settings.getRandomPalette(palettes, origin).blocks();
|
||||||
|
}
|
||||||
|
|
||||||
|
static StructurePlaceSettings resolvePlacementSettings(
|
||||||
|
SinglePoolElement element, PoolElementStructurePiece piece,
|
||||||
|
BoundingBox area) {
|
||||||
|
LiquidSettings liquidSettings;
|
||||||
|
try {
|
||||||
|
liquidSettings = (LiquidSettings) PoolPieceLiquidSettingsAccess.FIELD.get(piece);
|
||||||
|
} catch (IllegalAccessException error) {
|
||||||
|
throw new IllegalStateException("Cannot read native structure piece liquid settings", error);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return (StructurePlaceSettings) SinglePoolSettingsAccess.METHOD.invoke(
|
||||||
|
element, piece.getRotation(), area, liquidSettings, false);
|
||||||
|
} catch (IllegalAccessException | InvocationTargetException error) {
|
||||||
|
throw new IllegalStateException("Cannot create native structure placement settings", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
static List<StructureTemplate.StructureBlockInfo> processTemplateBlocks(
|
||||||
|
ServerLevelAccessor world, BlockPos origin, BlockPos referencePos,
|
||||||
|
StructurePlaceSettings settings,
|
||||||
|
List<StructureTemplate.StructureBlockInfo> blocks,
|
||||||
|
StructureTemplate template) {
|
||||||
|
Method forgeMethod = StructureTemplateProcessingAccess.FORGE_METHOD;
|
||||||
|
if (forgeMethod == null) {
|
||||||
|
return StructureTemplate.processBlockInfos(
|
||||||
|
world, origin, referencePos, settings, blocks);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return (List<StructureTemplate.StructureBlockInfo>) forgeMethod.invoke(
|
||||||
|
null, world, origin, referencePos, settings, blocks, template);
|
||||||
|
} catch (IllegalAccessException | InvocationTargetException error) {
|
||||||
|
throw new IllegalStateException("Cannot process native structure template blocks", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static StructureTemplate resolveTemplateReference(Either<?, ?> reference,
|
||||||
|
Supplier<StructureTemplateManager> templates) {
|
||||||
|
return reference.map(
|
||||||
|
location -> resolveNamedTemplate(location, templates),
|
||||||
|
NativeStructureReflection::requireRuntimeTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureTemplate resolveNamedTemplate(Object value,
|
||||||
|
Supplier<StructureTemplateManager> templates) {
|
||||||
|
if (!(value instanceof Identifier identifier)) {
|
||||||
|
throw new IllegalStateException("Native structure pool template identifier is "
|
||||||
|
+ (value == null ? "null" : value.getClass().getName()));
|
||||||
|
}
|
||||||
|
return Objects.requireNonNull(templates == null ? null : templates.get(),
|
||||||
|
"Native structure template manager is unavailable").getOrCreate(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureTemplate requireRuntimeTemplate(Object value) {
|
||||||
|
if (!(value instanceof StructureTemplate template)) {
|
||||||
|
throw new IllegalStateException("Native runtime structure pool template is "
|
||||||
|
+ (value == null ? "null" : value.getClass().getName()));
|
||||||
|
}
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Field resolveStructureTemplatePalettesField() {
|
||||||
|
Field resolved = null;
|
||||||
|
for (Field field : StructureTemplate.class.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(field.getModifiers()) || field.getType() != List.class
|
||||||
|
|| !isPaletteList(field.getGenericType())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (resolved != null) {
|
||||||
|
throw new IllegalStateException("StructureTemplate has multiple palette List fields");
|
||||||
|
}
|
||||||
|
resolved = field;
|
||||||
|
}
|
||||||
|
if (resolved == null) {
|
||||||
|
throw new IllegalStateException("StructureTemplate palette List field is missing");
|
||||||
|
}
|
||||||
|
if (!Modifier.isFinal(resolved.getModifiers())) {
|
||||||
|
throw new IllegalStateException("StructureTemplate palette field has an unexpected access contract");
|
||||||
|
}
|
||||||
|
if (!resolved.trySetAccessible()) {
|
||||||
|
throw new IllegalStateException("StructureTemplate palette field is inaccessible");
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isPaletteList(Type type) {
|
||||||
|
if (!(type instanceof ParameterizedType parameterized)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Type[] arguments = parameterized.getActualTypeArguments();
|
||||||
|
return arguments.length == 1 && arguments[0] == StructureTemplate.Palette.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Method resolveSinglePoolSettingsMethod() {
|
||||||
|
Method resolved = null;
|
||||||
|
Class<?>[] parameterTypes = {
|
||||||
|
Rotation.class, BoundingBox.class, LiquidSettings.class, boolean.class
|
||||||
|
};
|
||||||
|
for (Method method : SinglePoolElement.class.getDeclaredMethods()) {
|
||||||
|
if (Modifier.isStatic(method.getModifiers())
|
||||||
|
|| method.getReturnType() != StructurePlaceSettings.class
|
||||||
|
|| !Arrays.equals(method.getParameterTypes(), parameterTypes)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (resolved != null) {
|
||||||
|
throw new IllegalStateException("SinglePoolElement has multiple placement-settings methods");
|
||||||
|
}
|
||||||
|
resolved = method;
|
||||||
|
}
|
||||||
|
if (resolved == null) {
|
||||||
|
throw new IllegalStateException("SinglePoolElement placement-settings method is missing");
|
||||||
|
}
|
||||||
|
if (!Modifier.isProtected(resolved.getModifiers())) {
|
||||||
|
throw new IllegalStateException("SinglePoolElement placement-settings method has an unexpected access contract");
|
||||||
|
}
|
||||||
|
if (!resolved.trySetAccessible()) {
|
||||||
|
throw new IllegalStateException("SinglePoolElement placement-settings method is inaccessible");
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Field resolvePoolPieceLiquidSettingsField() {
|
||||||
|
Field resolved = null;
|
||||||
|
for (Field field : PoolElementStructurePiece.class.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(field.getModifiers())
|
||||||
|
|| field.getType() != LiquidSettings.class) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (resolved != null) {
|
||||||
|
throw new IllegalStateException("PoolElementStructurePiece has multiple liquid-settings fields");
|
||||||
|
}
|
||||||
|
resolved = field;
|
||||||
|
}
|
||||||
|
if (resolved == null) {
|
||||||
|
throw new IllegalStateException("PoolElementStructurePiece liquid-settings field is missing");
|
||||||
|
}
|
||||||
|
if (!Modifier.isPrivate(resolved.getModifiers())
|
||||||
|
|| !Modifier.isFinal(resolved.getModifiers())) {
|
||||||
|
throw new IllegalStateException("PoolElementStructurePiece liquid-settings field has an unexpected access contract");
|
||||||
|
}
|
||||||
|
if (!resolved.trySetAccessible()) {
|
||||||
|
throw new IllegalStateException("PoolElementStructurePiece liquid-settings field is inaccessible");
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Method resolveForgeTemplateProcessingMethod() {
|
||||||
|
Class<?>[] parameterTypes = {
|
||||||
|
ServerLevelAccessor.class, BlockPos.class, BlockPos.class,
|
||||||
|
StructurePlaceSettings.class, List.class, StructureTemplate.class
|
||||||
|
};
|
||||||
|
Method resolved = null;
|
||||||
|
for (Method method : StructureTemplate.class.getDeclaredMethods()) {
|
||||||
|
if (!Modifier.isPublic(method.getModifiers())
|
||||||
|
|| !Modifier.isStatic(method.getModifiers())
|
||||||
|
|| method.getReturnType() != List.class
|
||||||
|
|| !Arrays.equals(method.getParameterTypes(), parameterTypes)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (resolved != null) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"StructureTemplate has multiple Forge block-processing methods");
|
||||||
|
}
|
||||||
|
resolved = method;
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class MonumentChildPiecesAccess {
|
||||||
|
static final Field FIELD = resolveMonumentChildPiecesField();
|
||||||
|
|
||||||
|
private MonumentChildPiecesAccess() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RuinedPortalPlacementAccess {
|
||||||
|
private static final Field FIELD = resolveRuinedPortalPlacementField();
|
||||||
|
|
||||||
|
private RuinedPortalPlacementAccess() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class ScatteredHeightPositionAccess {
|
||||||
|
static final Field FIELD = resolveScatteredHeightPositionField();
|
||||||
|
|
||||||
|
private ScatteredHeightPositionAccess() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class SinglePoolTemplateAccess {
|
||||||
|
private static final Field FIELD = resolveSinglePoolTemplateField();
|
||||||
|
|
||||||
|
private SinglePoolTemplateAccess() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class StructureTemplatePalettesAccess {
|
||||||
|
private static final Field FIELD = resolveStructureTemplatePalettesField();
|
||||||
|
|
||||||
|
private StructureTemplatePalettesAccess() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class SinglePoolSettingsAccess {
|
||||||
|
private static final Method METHOD = resolveSinglePoolSettingsMethod();
|
||||||
|
|
||||||
|
private SinglePoolSettingsAccess() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class PoolPieceLiquidSettingsAccess {
|
||||||
|
private static final Field FIELD = resolvePoolPieceLiquidSettingsField();
|
||||||
|
|
||||||
|
private PoolPieceLiquidSettingsAccess() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class StructureTemplateProcessingAccess {
|
||||||
|
private static final Method FORGE_METHOD = resolveForgeTemplateProcessingMethod();
|
||||||
|
|
||||||
|
private StructureTemplateProcessingAccess() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+147
@@ -0,0 +1,147 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipStore;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructurePlacementPlanner;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureGenerationStatus;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureSuppression;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.Registry;
|
||||||
|
import net.minecraft.core.RegistryAccess;
|
||||||
|
import net.minecraft.core.SectionPos;
|
||||||
|
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.StructureManager;
|
||||||
|
import net.minecraft.world.level.biome.BiomeSource;
|
||||||
|
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.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public final class NativeStructureStartInjector {
|
||||||
|
private static final Set<String> WARNED_DUPLICATE_STRUCTURES = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
|
private NativeStructureStartInjector() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Map<Structure, NativeStructureStartPlan> inject(InjectionContext context) {
|
||||||
|
Objects.requireNonNull(context, "Native structure injection context must not be null");
|
||||||
|
ChunkAccess chunk = context.chunk();
|
||||||
|
Registry<Structure> registry = context.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||||
|
SectionPos section = SectionPos.bottomOf(chunk);
|
||||||
|
Map<Structure, NativeStructureStartPlan> configuredStarts = new LinkedHashMap<>();
|
||||||
|
for (NativeStructureStartPlan plan : NativeStructurePlacementPlanner.plansAt(
|
||||||
|
context.engine(), chunk.getPos().x(), chunk.getPos().z())) {
|
||||||
|
Identifier identifier = Identifier.tryParse(plan.source().getStructure());
|
||||||
|
if (identifier == null) {
|
||||||
|
throw new IllegalStateException("Configured native structure key is invalid: "
|
||||||
|
+ plan.source().getStructure());
|
||||||
|
}
|
||||||
|
Structure structure = registry.getValue(identifier);
|
||||||
|
if (structure == null) {
|
||||||
|
throw new IllegalStateException("Configured native structure is not registered: " + identifier);
|
||||||
|
}
|
||||||
|
Holder<Structure> holder = registry.wrapAsHolder(structure);
|
||||||
|
if (configuredStarts.containsKey(structure)) {
|
||||||
|
if (WARNED_DUPLICATE_STRUCTURES.add(identifier.toString())) {
|
||||||
|
IrisLogging.warn("Ignoring duplicate native structure placements for '"
|
||||||
|
+ identifier + "'; the first deterministic candidate owns each chunk start");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
StructureStart existing = context.structureManager().getStartForStructure(
|
||||||
|
structure, chunk);
|
||||||
|
boolean replacement = plan.placement().getNativeSuppression()
|
||||||
|
== NativeStructureSuppression.REPLACE_SOURCE;
|
||||||
|
if (!replacement && existing != null && existing.isValid()) {
|
||||||
|
NativeStructureGenerationStatus sourceStatus = NativeStructureGenerationPolicy.resolve(
|
||||||
|
context.engine(), identifier.toString(),
|
||||||
|
NativeStructureVegetationClearer.isUndergroundStep(structure.step())).status();
|
||||||
|
replacement = sourceStatus == NativeStructureGenerationStatus.REPLACED_BY_IRIS;
|
||||||
|
}
|
||||||
|
if (!replacement && existing != null && existing.isValid()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int references = existing != null && existing.isValid() ? existing.getReferences() : 0;
|
||||||
|
NativeStructureFactory.GenerationContext generationContext =
|
||||||
|
new NativeStructureFactory.GenerationContext(
|
||||||
|
context.registryAccess(),
|
||||||
|
context.generator(),
|
||||||
|
context.biomeSource(),
|
||||||
|
context.structureState().randomState(),
|
||||||
|
context.templateManager(),
|
||||||
|
context.structureState().getLevelSeed(),
|
||||||
|
context.levelKey(),
|
||||||
|
chunk,
|
||||||
|
biome -> true,
|
||||||
|
context.generator().getSeaLevel(),
|
||||||
|
(x, z) -> Engine.hostHeight(context.engine(), x, z, true)
|
||||||
|
+ context.engine().getMinHeight()
|
||||||
|
);
|
||||||
|
StructureStart generated = NativeStructureFactory.generate(
|
||||||
|
generationContext, holder, plan, references);
|
||||||
|
if (!isUsableGeneratedStart(generated)) {
|
||||||
|
if (replacement) {
|
||||||
|
context.structureManager().setStartForStructure(
|
||||||
|
structure, StructureStart.INVALID_START, chunk);
|
||||||
|
}
|
||||||
|
NativeStructureOwnershipStore.discard(
|
||||||
|
context.engine(), identifier.toString(),
|
||||||
|
chunk.getPos().x(), chunk.getPos().z());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BoundingBox referenceBounds = NativeStructureReferenceEnvelope.referenceBounds(
|
||||||
|
generated, structure, plan.placement().resolvedTerrain(), identifier.toString());
|
||||||
|
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture(
|
||||||
|
identifier.toString(), generated, plan, referenceBounds);
|
||||||
|
NativeStructureOwnershipStore.record(context.engine(), ownership);
|
||||||
|
try {
|
||||||
|
context.structureManager().setStartForStructure(
|
||||||
|
structure, generated, chunk);
|
||||||
|
} catch (RuntimeException | Error publicationError) {
|
||||||
|
try {
|
||||||
|
NativeStructureOwnershipStore.discard(
|
||||||
|
context.engine(), identifier.toString(),
|
||||||
|
chunk.getPos().x(), chunk.getPos().z());
|
||||||
|
} catch (RuntimeException | Error cleanupError) {
|
||||||
|
publicationError.addSuppressed(cleanupError);
|
||||||
|
}
|
||||||
|
throw publicationError;
|
||||||
|
}
|
||||||
|
configuredStarts.put(structure, plan);
|
||||||
|
}
|
||||||
|
return Map.copyOf(configuredStarts);
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isUsableGeneratedStart(StructureStart start) {
|
||||||
|
return start != null && start.isValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record InjectionContext(
|
||||||
|
Engine engine,
|
||||||
|
RegistryAccess registryAccess,
|
||||||
|
ChunkGeneratorStructureState structureState,
|
||||||
|
StructureManager structureManager,
|
||||||
|
ChunkAccess chunk,
|
||||||
|
StructureTemplateManager templateManager,
|
||||||
|
ResourceKey<Level> levelKey,
|
||||||
|
ChunkGenerator generator,
|
||||||
|
BiomeSource biomeSource
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+1221
File diff suppressed because it is too large
Load Diff
+305
@@ -0,0 +1,305 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
final class NativeStructureSurfaceSupportBuilder {
|
||||||
|
private static final int MAX_BATCH_CELLS = 524_288;
|
||||||
|
private static final int MAX_BRIDGE_SPAN = 2;
|
||||||
|
|
||||||
|
private NativeStructureSurfaceSupportBuilder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static Set<Long> bridgeRigidPieceSupport(
|
||||||
|
WorldGenLevel world, BoundingBox area,
|
||||||
|
List<NativeStructureTerrainIntegrator.TerrainTarget> targets,
|
||||||
|
Supplier<StructureTemplateManager> templates) {
|
||||||
|
if (targets == null || targets.isEmpty()) {
|
||||||
|
return Set.of();
|
||||||
|
}
|
||||||
|
Map<Long, BlockState> planned;
|
||||||
|
try {
|
||||||
|
BatchCellBudget budget = new BatchCellBudget();
|
||||||
|
List<SupportRequest> requests = new ArrayList<>();
|
||||||
|
Set<StructureStart> seenStarts = Collections.newSetFromMap(
|
||||||
|
new IdentityHashMap<>());
|
||||||
|
for (NativeStructureTerrainIntegrator.TerrainTarget target : targets) {
|
||||||
|
if (target == null
|
||||||
|
|| !NativeStructureSurfaceFitter.requiresSurfaceTerrain(target)
|
||||||
|
|| target.terrain().resolvedMode() == IrisStructureTerrainMode.FLATTEN
|
||||||
|
&& !NativeStructureSurfaceFitter.hasExposedFlattenPiece(
|
||||||
|
target.start(), (x, z) -> world.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z) - 1,
|
||||||
|
target.terrain().resolvedFlattenRange())
|
||||||
|
|| !seenStarts.add(target.start())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
collectTargetRequests(
|
||||||
|
world, area, target, templates, budget, requests);
|
||||||
|
}
|
||||||
|
planned = planWrites(world, area, requests, budget);
|
||||||
|
} catch (BatchBudgetExceeded ignored) {
|
||||||
|
return Set.of();
|
||||||
|
}
|
||||||
|
if (planned.isEmpty()) {
|
||||||
|
return Set.of();
|
||||||
|
}
|
||||||
|
List<Map.Entry<Long, BlockState>> ordered = new ArrayList<>(planned.entrySet());
|
||||||
|
ordered.sort(Map.Entry.comparingByKey());
|
||||||
|
Set<Long> written = new HashSet<>();
|
||||||
|
for (Map.Entry<Long, BlockState> entry : ordered) {
|
||||||
|
BlockPos position = BlockPos.of(entry.getKey());
|
||||||
|
BlockState existing = world.getBlockState(position);
|
||||||
|
if (!existing.isSolid()
|
||||||
|
&& !NativeStructureVegetationClearer.isTreeBlock(existing)
|
||||||
|
&& existing.getFluidState().isEmpty()
|
||||||
|
&& world.setBlock(position, entry.getValue(), 2)) {
|
||||||
|
written.add(entry.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Set.copyOf(written);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void collectTargetRequests(
|
||||||
|
WorldGenLevel world, BoundingBox area,
|
||||||
|
NativeStructureTerrainIntegrator.TerrainTarget target,
|
||||||
|
Supplier<StructureTemplateManager> templates,
|
||||||
|
BatchCellBudget budget, List<SupportRequest> requests) {
|
||||||
|
if (!NativeStructureSurfaceFitter.requiresSurfaceTerrain(target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<RigidAnchor> anchors = rigidAnchors(target.start());
|
||||||
|
if (anchors.size() < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BlockPos referencePos = referencePosition(target.start());
|
||||||
|
for (RigidAnchor upper : anchors) {
|
||||||
|
Map<Long, Integer> lowerMeets = candidateLowerMeets(
|
||||||
|
area, upper, anchors, budget);
|
||||||
|
if (lowerMeets.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
NativeStructureTemplateOccupancy.OccupancyResult occupancy =
|
||||||
|
NativeStructureTemplateOccupancy.resolve(
|
||||||
|
world, upper.piece(), referencePos, area, templates,
|
||||||
|
position -> retain(position, area, lowerMeets), budget::consume);
|
||||||
|
if (!occupancy.resolved()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Map<Long, NativeStructureTemplateOccupancy.LowestCell> lowest =
|
||||||
|
NativeStructureTemplateOccupancy.lowestCells(occupancy.cells());
|
||||||
|
for (Map.Entry<Long, NativeStructureTemplateOccupancy.LowestCell> entry
|
||||||
|
: lowest.entrySet()) {
|
||||||
|
NativeStructureTemplateOccupancy.LowestCell cell = entry.getValue();
|
||||||
|
Integer lowerMeet = lowerMeets.get(entry.getKey());
|
||||||
|
if (lowerMeet == null || cell.occupancy().blocker()
|
||||||
|
|| !NativeStructureTemplateOccupancy.isSolidBase(
|
||||||
|
cell.occupancy().state())
|
||||||
|
|| cell.y() - lowerMeet < 2
|
||||||
|
|| cell.y() - lowerMeet > MAX_BRIDGE_SPAN) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
budget.consume(1);
|
||||||
|
requests.add(new SupportRequest(
|
||||||
|
cell.x(), cell.z(), lowerMeet, cell.y()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<RigidAnchor> rigidAnchors(StructureStart start) {
|
||||||
|
List<RigidAnchor> anchors = new ArrayList<>();
|
||||||
|
for (StructurePiece piece : start.getPieces()) {
|
||||||
|
if (!(piece instanceof PoolElementStructurePiece poolPiece)
|
||||||
|
|| poolPiece.getElement().getProjection()
|
||||||
|
!= StructureTemplatePool.Projection.RIGID) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BoundingBox bounds = poolPiece.getBoundingBox();
|
||||||
|
int meetY = bounds.minY() + poolPiece.getGroundLevelDelta() - 1;
|
||||||
|
anchors.add(new RigidAnchor(poolPiece, bounds, meetY));
|
||||||
|
}
|
||||||
|
return List.copyOf(anchors);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<Long, Integer> candidateLowerMeets(
|
||||||
|
BoundingBox area, RigidAnchor upper, List<RigidAnchor> anchors,
|
||||||
|
BatchCellBudget budget) {
|
||||||
|
Map<Long, Integer> lowerMeets = new HashMap<>();
|
||||||
|
for (RigidAnchor lower : anchors) {
|
||||||
|
budget.consume(1);
|
||||||
|
int span = upper.meetY() - lower.meetY();
|
||||||
|
if (lower.piece() == upper.piece() || span < 1
|
||||||
|
|| span > MAX_BRIDGE_SPAN
|
||||||
|
|| lower.meetY() < area.minY() || lower.meetY() > area.maxY()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int minimumX = Math.max(area.minX(),
|
||||||
|
Math.max(upper.bounds().minX(), lower.bounds().minX()));
|
||||||
|
int maximumX = Math.min(area.maxX(),
|
||||||
|
Math.min(upper.bounds().maxX(), lower.bounds().maxX()));
|
||||||
|
int minimumZ = Math.max(area.minZ(),
|
||||||
|
Math.max(upper.bounds().minZ(), lower.bounds().minZ()));
|
||||||
|
int maximumZ = Math.min(area.maxZ(),
|
||||||
|
Math.min(upper.bounds().maxZ(), lower.bounds().maxZ()));
|
||||||
|
if (minimumX > maximumX || minimumZ > maximumZ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int x = minimumX; x <= maximumX; x++) {
|
||||||
|
for (int z = minimumZ; z <= maximumZ; z++) {
|
||||||
|
budget.consume(1);
|
||||||
|
lowerMeets.merge(columnKey(x, z), lower.meetY(), Math::max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lowerMeets;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean retain(
|
||||||
|
BlockPos position, BoundingBox area, Map<Long, Integer> lowerMeets) {
|
||||||
|
if (!area.isInside(position)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Integer lowerMeet = lowerMeets.get(columnKey(position.getX(), position.getZ()));
|
||||||
|
return lowerMeet != null && position.getY() <= lowerMeet + MAX_BRIDGE_SPAN;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<Long, BlockState> planWrites(
|
||||||
|
WorldGenLevel world, BoundingBox area,
|
||||||
|
List<SupportRequest> requests, BatchCellBudget budget) {
|
||||||
|
List<SupportRequest> eligible = new ArrayList<>();
|
||||||
|
Set<MaterialKey> materialKeys = new HashSet<>();
|
||||||
|
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
|
||||||
|
for (SupportRequest request : requests) {
|
||||||
|
BlockState lowerTerrain = world.getBlockState(position.set(
|
||||||
|
request.x(), request.lowerMeetY(), request.z()));
|
||||||
|
if (!isTerrainSupport(lowerTerrain)
|
||||||
|
|| !gapIsClear(world, area, position, request, budget)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
eligible.add(request);
|
||||||
|
materialKeys.add(new MaterialKey(
|
||||||
|
request.x(), request.z(), request.lowerMeetY()));
|
||||||
|
}
|
||||||
|
Map<MaterialKey, BlockState> materials = new HashMap<>();
|
||||||
|
for (MaterialKey key : materialKeys) {
|
||||||
|
materials.put(key, resolveSupportState(
|
||||||
|
world, position, key.x(), key.z(), key.lowerMeetY(), budget));
|
||||||
|
}
|
||||||
|
Map<Long, BlockState> planned = new HashMap<>();
|
||||||
|
Set<Long> conflicts = new HashSet<>();
|
||||||
|
eligible.sort(Comparator.comparingInt(SupportRequest::x)
|
||||||
|
.thenComparingInt(SupportRequest::z)
|
||||||
|
.thenComparingInt(SupportRequest::lowerMeetY)
|
||||||
|
.thenComparingInt(SupportRequest::baseY));
|
||||||
|
for (SupportRequest request : eligible) {
|
||||||
|
MaterialKey materialKey = new MaterialKey(
|
||||||
|
request.x(), request.z(), request.lowerMeetY());
|
||||||
|
BlockState material = materials.get(materialKey);
|
||||||
|
for (int y = request.lowerMeetY() + 1; y < request.baseY(); y++) {
|
||||||
|
budget.consume(1);
|
||||||
|
long positionKey = BlockPos.asLong(request.x(), y, request.z());
|
||||||
|
if (conflicts.contains(positionKey)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BlockState existing = planned.putIfAbsent(positionKey, material);
|
||||||
|
if (existing != null && !existing.equals(material)) {
|
||||||
|
planned.remove(positionKey);
|
||||||
|
conflicts.add(positionKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return planned;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean gapIsClear(
|
||||||
|
WorldGenLevel world, BoundingBox area, BlockPos.MutableBlockPos position,
|
||||||
|
SupportRequest request, BatchCellBudget budget) {
|
||||||
|
for (int y = request.lowerMeetY() + 1; y < request.baseY(); y++) {
|
||||||
|
budget.consume(1);
|
||||||
|
if (!area.isInside(position.set(request.x(), y, request.z()))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
BlockState state = world.getBlockState(position);
|
||||||
|
if (state.isSolid() || NativeStructureVegetationClearer.isTreeBlock(state)
|
||||||
|
|| !state.getFluidState().isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BlockState resolveSupportState(
|
||||||
|
WorldGenLevel world, BlockPos.MutableBlockPos position,
|
||||||
|
int x, int z, int lowerMeetY, BatchCellBudget budget) {
|
||||||
|
budget.consume(1);
|
||||||
|
BlockState below = world.getBlockState(position.set(x, lowerMeetY - 1, z));
|
||||||
|
if (isTerrainSupport(below)) {
|
||||||
|
return below;
|
||||||
|
}
|
||||||
|
return world.getBlockState(position.set(x, lowerMeetY, z));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isTerrainSupport(BlockState state) {
|
||||||
|
return state.isSolid()
|
||||||
|
&& !NativeStructureVegetationClearer.isTreeBlock(state)
|
||||||
|
&& state.getFluidState().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BlockPos referencePosition(StructureStart start) {
|
||||||
|
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
|
||||||
|
BlockPos center = bounds.getCenter();
|
||||||
|
return new BlockPos(center.getX(), bounds.minY(), center.getZ());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long columnKey(int x, int z) {
|
||||||
|
return (long) x << 32 ^ z & 0xffffffffL;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record RigidAnchor(
|
||||||
|
PoolElementStructurePiece piece, BoundingBox bounds, int meetY) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record SupportRequest(int x, int z, int lowerMeetY, int baseY) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record MaterialKey(int x, int z, int lowerMeetY) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class BatchCellBudget {
|
||||||
|
private int consumed;
|
||||||
|
|
||||||
|
private void consume(int amount) {
|
||||||
|
if (amount < 0 || consumed > MAX_BATCH_CELLS - amount) {
|
||||||
|
throw BatchBudgetExceeded.INSTANCE;
|
||||||
|
}
|
||||||
|
consumed += amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class BatchBudgetExceeded extends RuntimeException {
|
||||||
|
private static final BatchBudgetExceeded INSTANCE = new BatchBudgetExceeded();
|
||||||
|
|
||||||
|
private BatchBudgetExceeded() {
|
||||||
|
super(null, null, false, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+165
@@ -0,0 +1,165 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.EmptyPoolElement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
final class NativeStructureTemplateOccupancy {
|
||||||
|
private static final OccupancyCell BLOCKER = new OccupancyCell(null, true);
|
||||||
|
|
||||||
|
private NativeStructureTemplateOccupancy() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static OccupancyResult resolve(
|
||||||
|
WorldGenLevel world, PoolElementStructurePiece piece,
|
||||||
|
BlockPos referencePosition, BoundingBox processingArea,
|
||||||
|
Supplier<StructureTemplateManager> templates,
|
||||||
|
PositionFilter filter, CellBudget budget) {
|
||||||
|
List<SinglePoolElement> leaves = new ArrayList<>();
|
||||||
|
if (!flattenSingles(piece.getElement(), leaves)) {
|
||||||
|
return new OccupancyResult(false, Map.of());
|
||||||
|
}
|
||||||
|
List<LeafPlacement> placements = new ArrayList<>(leaves.size());
|
||||||
|
Map<Long, OccupancyCell> occupancy = new HashMap<>();
|
||||||
|
for (SinglePoolElement leaf : leaves) {
|
||||||
|
StructurePlaceSettings settings = NativeStructureReflection.resolvePlacementSettings(
|
||||||
|
leaf, piece, processingArea);
|
||||||
|
StructureTemplate template = NativeStructureReflection.resolveTemplate(leaf, templates);
|
||||||
|
List<StructureTemplate.StructureBlockInfo> rawBlocks =
|
||||||
|
NativeStructureReflection.resolveTemplateBlocks(
|
||||||
|
template, settings, piece.getPosition());
|
||||||
|
budget.consume(rawBlocks.size());
|
||||||
|
placements.add(new LeafPlacement(settings, rawBlocks, template));
|
||||||
|
for (StructureTemplate.StructureBlockInfo raw : rawBlocks) {
|
||||||
|
BlockPos position = piece.getPosition().offset(
|
||||||
|
StructureTemplate.calculateRelativePosition(settings, raw.pos()));
|
||||||
|
if (filter.retain(position)) {
|
||||||
|
occupancy.put(position.asLong(), BLOCKER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (LeafPlacement placement : placements) {
|
||||||
|
List<StructureTemplate.StructureBlockInfo> processed =
|
||||||
|
NativeStructureReflection.processTemplateBlocks(
|
||||||
|
world, piece.getPosition(), referencePosition,
|
||||||
|
placement.settings(), placement.rawBlocks(), placement.template());
|
||||||
|
budget.consume(processed.size());
|
||||||
|
for (StructureTemplate.StructureBlockInfo block : processed) {
|
||||||
|
BlockPos position = block.pos();
|
||||||
|
if (!filter.retain(position)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BlockState state = block.state()
|
||||||
|
.mirror(placement.settings().getMirror())
|
||||||
|
.rotate(placement.settings().getRotation());
|
||||||
|
occupancy.put(position.asLong(), new OccupancyCell(state, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new OccupancyResult(true, occupancy);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<Long, LowestCell> lowestCells(Map<Long, OccupancyCell> occupancy) {
|
||||||
|
Map<Long, LowestCell> lowest = new HashMap<>();
|
||||||
|
for (Map.Entry<Long, OccupancyCell> entry : occupancy.entrySet()) {
|
||||||
|
OccupancyCell cell = entry.getValue();
|
||||||
|
BlockPos position = BlockPos.of(entry.getKey());
|
||||||
|
long column = columnKey(position.getX(), position.getZ());
|
||||||
|
LowestCell current = lowest.get(column);
|
||||||
|
if (current == null || position.getY() < current.y()) {
|
||||||
|
lowest.put(column, new LowestCell(
|
||||||
|
position.getX(), position.getY(), position.getZ(), cell));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lowest;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<Long, LowestCell> lowestProcessedSolidCells(Map<Long, OccupancyCell> occupancy) {
|
||||||
|
Map<Long, LowestCell> lowest = new HashMap<>();
|
||||||
|
for (Map.Entry<Long, OccupancyCell> entry : occupancy.entrySet()) {
|
||||||
|
OccupancyCell cell = entry.getValue();
|
||||||
|
if (cell.blocker() || !isSolidBase(cell.state())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BlockPos position = BlockPos.of(entry.getKey());
|
||||||
|
long column = columnKey(position.getX(), position.getZ());
|
||||||
|
LowestCell current = lowest.get(column);
|
||||||
|
if (current == null || position.getY() < current.y()) {
|
||||||
|
lowest.put(column, new LowestCell(
|
||||||
|
position.getX(), position.getY(), position.getZ(), cell));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lowest;
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isSolidBase(BlockState state) {
|
||||||
|
return state != null && state.isSolid()
|
||||||
|
&& !state.is(Blocks.STRUCTURE_VOID)
|
||||||
|
&& !state.is(Blocks.JIGSAW)
|
||||||
|
&& state.getFluidState().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
static long columnKey(int x, int z) {
|
||||||
|
return (long) x << 32 ^ z & 0xffffffffL;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean flattenSingles(
|
||||||
|
StructurePoolElement element, List<SinglePoolElement> leaves) {
|
||||||
|
if (element instanceof ListPoolElement listElement) {
|
||||||
|
for (StructurePoolElement child : listElement.getElements()) {
|
||||||
|
if (!flattenSingles(child, leaves)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (element == EmptyPoolElement.INSTANCE) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (element instanceof SinglePoolElement singleElement) {
|
||||||
|
leaves.add(singleElement);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
interface PositionFilter {
|
||||||
|
boolean retain(BlockPos position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
interface CellBudget {
|
||||||
|
void consume(int amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
record OccupancyResult(boolean resolved, Map<Long, OccupancyCell> cells) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record OccupancyCell(BlockState state, boolean blocker) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record LowestCell(int x, int y, int z, OccupancyCell occupancy) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record LeafPlacement(
|
||||||
|
StructurePlaceSettings settings,
|
||||||
|
List<StructureTemplate.StructureBlockInfo> rawBlocks,
|
||||||
|
StructureTemplate template) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+228
@@ -0,0 +1,228 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import com.mojang.datafixers.util.Pair;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
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.block.Rotation;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.EmptyPoolElement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.FeaturePoolElement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.alias.DirectPoolAlias;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.alias.PoolAliasBinding;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.alias.RandomGroupPoolAlias;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.alias.RandomPoolAlias;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
public final class NativeStructureTemplatePoolBounds {
|
||||||
|
private NativeStructureTemplatePoolBounds() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int sourceHorizontalSpan(RegistryAccess registryAccess,
|
||||||
|
StructureTemplateManager templateManager,
|
||||||
|
JigsawStructure source) {
|
||||||
|
Registry<StructureTemplatePool> pools = registryAccess.lookupOrThrow(Registries.TEMPLATE_POOL);
|
||||||
|
return sourceHorizontalSpan(
|
||||||
|
templateManager,
|
||||||
|
source.getStartPool(),
|
||||||
|
source.getPoolAliases(),
|
||||||
|
key -> pools.getValue(key.identifier()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int horizontalSpan(RegistryAccess registryAccess,
|
||||||
|
StructureTemplateManager templateManager,
|
||||||
|
String templatePoolKey) {
|
||||||
|
Identifier identifier = Identifier.tryParse(templatePoolKey);
|
||||||
|
if (identifier == null) {
|
||||||
|
throw new IllegalArgumentException("Invalid registered template pool key: " + templatePoolKey);
|
||||||
|
}
|
||||||
|
Registry<StructureTemplatePool> pools = registryAccess.lookupOrThrow(Registries.TEMPLATE_POOL);
|
||||||
|
StructureTemplatePool pool = pools.getValue(identifier);
|
||||||
|
if (pool == null) {
|
||||||
|
throw new IllegalArgumentException("Registered template pool does not exist: " + templatePoolKey);
|
||||||
|
}
|
||||||
|
return horizontalSpan(pool, templateManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int horizontalSpan(RegistryAccess registryAccess,
|
||||||
|
StructureTemplateManager templateManager,
|
||||||
|
JigsawStructure source,
|
||||||
|
String templatePoolKey) {
|
||||||
|
Identifier identifier = Identifier.tryParse(templatePoolKey);
|
||||||
|
if (identifier == null) {
|
||||||
|
throw new IllegalArgumentException("Invalid registered template pool key: " + templatePoolKey);
|
||||||
|
}
|
||||||
|
Registry<StructureTemplatePool> pools = registryAccess.lookupOrThrow(Registries.TEMPLATE_POOL);
|
||||||
|
ResourceKey<StructureTemplatePool> startPoolKey = ResourceKey.create(
|
||||||
|
Registries.TEMPLATE_POOL, identifier);
|
||||||
|
StructureTemplatePool startPool = pools.getValue(identifier);
|
||||||
|
if (startPool == null) {
|
||||||
|
throw new IllegalArgumentException("Registered template pool does not exist: " + templatePoolKey);
|
||||||
|
}
|
||||||
|
return sourceHorizontalSpan(
|
||||||
|
templateManager,
|
||||||
|
startPool,
|
||||||
|
startPoolKey,
|
||||||
|
source.getPoolAliases(),
|
||||||
|
key -> pools.getValue(key.identifier()));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int sourceHorizontalSpan(StructureTemplateManager templateManager,
|
||||||
|
Holder<StructureTemplatePool> startPool,
|
||||||
|
List<PoolAliasBinding> aliases,
|
||||||
|
Function<ResourceKey<StructureTemplatePool>, StructureTemplatePool> poolLookup) {
|
||||||
|
return sourceHorizontalSpan(templateManager, startPool.value(),
|
||||||
|
startPool.unwrapKey().orElse(null), aliases, poolLookup);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int sourceHorizontalSpan(StructureTemplateManager templateManager,
|
||||||
|
StructureTemplatePool startPool,
|
||||||
|
ResourceKey<StructureTemplatePool> startPoolKey,
|
||||||
|
List<PoolAliasBinding> aliases,
|
||||||
|
Function<ResourceKey<StructureTemplatePool>, StructureTemplatePool> poolLookup) {
|
||||||
|
int maximumSpan = directHorizontalSpan(startPool, templateManager);
|
||||||
|
if (startPoolKey == null) {
|
||||||
|
return maximumSpan;
|
||||||
|
}
|
||||||
|
Set<ResourceKey<StructureTemplatePool>> targets = new HashSet<>();
|
||||||
|
for (PoolAliasBinding binding : aliases) {
|
||||||
|
collectTargets(binding, startPoolKey, targets);
|
||||||
|
}
|
||||||
|
for (ResourceKey<StructureTemplatePool> target : targets) {
|
||||||
|
StructureTemplatePool targetPool = poolLookup.apply(target);
|
||||||
|
if (targetPool == null) {
|
||||||
|
throw new IllegalStateException("Jigsaw start-pool alias target does not exist: "
|
||||||
|
+ target.identifier());
|
||||||
|
}
|
||||||
|
maximumSpan = Math.max(maximumSpan, directHorizontalSpan(targetPool, templateManager));
|
||||||
|
}
|
||||||
|
return maximumSpan;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void collectTargets(PoolAliasBinding binding,
|
||||||
|
ResourceKey<StructureTemplatePool> startPoolKey,
|
||||||
|
Set<ResourceKey<StructureTemplatePool>> targets) {
|
||||||
|
if (binding instanceof DirectPoolAlias direct) {
|
||||||
|
if (direct.alias().equals(startPoolKey)) {
|
||||||
|
targets.add(direct.target());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (binding instanceof RandomPoolAlias random) {
|
||||||
|
if (random.alias().equals(startPoolKey)) {
|
||||||
|
random.targets().unwrap().forEach(entry -> targets.add(entry.value()));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (binding instanceof RandomGroupPoolAlias group) {
|
||||||
|
group.groups().unwrap().forEach(entry -> entry.value().forEach(
|
||||||
|
nested -> collectTargets(nested, startPoolKey, targets)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Unsupported jigsaw pool alias type: "
|
||||||
|
+ binding.getClass().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
static int horizontalSpan(StructureTemplatePool pool,
|
||||||
|
StructureTemplateManager templateManager) {
|
||||||
|
Set<StructureTemplatePool> visited = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||||
|
return horizontalSpan(pool, templateManager, visited);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int horizontalSpan(StructureTemplatePool pool,
|
||||||
|
StructureTemplateManager templateManager,
|
||||||
|
Set<StructureTemplatePool> visited) {
|
||||||
|
if (!visited.add(pool)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int maximumSpan = directHorizontalSpan(pool, templateManager);
|
||||||
|
Holder<StructureTemplatePool> fallback = pool.getFallback();
|
||||||
|
if (fallback == null || !fallback.isBound()) {
|
||||||
|
throw new IllegalStateException("Template pool has an unresolved fallback");
|
||||||
|
}
|
||||||
|
return Math.max(maximumSpan, horizontalSpan(fallback.value(), templateManager, visited));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int directHorizontalSpan(StructureTemplatePool pool,
|
||||||
|
StructureTemplateManager templateManager) {
|
||||||
|
int maximumSpan = 0;
|
||||||
|
for (Pair<StructurePoolElement, Integer> entry : pool.getTemplates()) {
|
||||||
|
StructurePoolElement element = entry.getFirst();
|
||||||
|
validateElement(element, templateManager);
|
||||||
|
maximumSpan = Math.max(maximumSpan, horizontalSpan(element, templateManager));
|
||||||
|
}
|
||||||
|
return maximumSpan;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateElement(StructurePoolElement element,
|
||||||
|
StructureTemplateManager templateManager) {
|
||||||
|
if (element == EmptyPoolElement.INSTANCE || element instanceof FeaturePoolElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (element instanceof SinglePoolElement single) {
|
||||||
|
Identifier template;
|
||||||
|
try {
|
||||||
|
template = single.getTemplateLocation();
|
||||||
|
} catch (RuntimeException inlineTemplate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (templateManager.get(template).isEmpty()) {
|
||||||
|
throw new IllegalStateException("Template pool element does not resolve a structure template: "
|
||||||
|
+ template);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (element instanceof ListPoolElement list) {
|
||||||
|
for (StructurePoolElement child : list.getElements()) {
|
||||||
|
validateElement(child, templateManager);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int horizontalSpan(StructurePoolElement element,
|
||||||
|
StructureTemplateManager templateManager) {
|
||||||
|
if (element == EmptyPoolElement.INSTANCE) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
long maximumSpan = 0L;
|
||||||
|
for (Rotation rotation : Rotation.values()) {
|
||||||
|
BoundingBox box;
|
||||||
|
try {
|
||||||
|
box = element.getBoundingBox(templateManager, BlockPos.ZERO, rotation);
|
||||||
|
} catch (RuntimeException | LinkageError error) {
|
||||||
|
throw new IllegalStateException("Template pool element "
|
||||||
|
+ element.getClass().getName()
|
||||||
|
+ " could not provide bounded geometry for rotation " + rotation, error);
|
||||||
|
}
|
||||||
|
if (box == null) {
|
||||||
|
throw new IllegalStateException("Template pool element "
|
||||||
|
+ element.getClass().getName()
|
||||||
|
+ " returned no bounded geometry for rotation " + rotation);
|
||||||
|
}
|
||||||
|
long xSpan = (long) box.maxX() - box.minX() + 1L;
|
||||||
|
long zSpan = (long) box.maxZ() - box.minZ() + 1L;
|
||||||
|
maximumSpan = Math.max(maximumSpan, Math.max(xSpan, zSpan));
|
||||||
|
}
|
||||||
|
if (maximumSpan < 0L || maximumSpan > Integer.MAX_VALUE) {
|
||||||
|
throw new IllegalStateException("Template pool element has an unbounded horizontal span");
|
||||||
|
}
|
||||||
|
return (int) maximumSpan;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1096
File diff suppressed because it is too large
Load Diff
+224
@@ -0,0 +1,224 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import com.mojang.datafixers.util.Pair;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.core.SectionPos;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.StructureManager;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||||
|
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureCheckResult;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
public final class NativeStructureVanillaLocator {
|
||||||
|
private NativeStructureVanillaLocator() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Candidate predict(ServerLevel level, HolderSet<Structure> holders,
|
||||||
|
BlockPos origin, int radius, boolean findUnexplored) {
|
||||||
|
if (SharedConstants.DEBUG_DISABLE_FEATURES
|
||||||
|
|| !level.getServer().getWorldGenSettings().options().generateStructures()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ChunkGeneratorStructureState state = level.getChunkSource().getGeneratorState();
|
||||||
|
Map<StructurePlacement, Set<Holder<Structure>>> byPlacement = new LinkedHashMap<>();
|
||||||
|
for (Holder<Structure> holder : holders) {
|
||||||
|
for (StructurePlacement placement : state.getPlacementsForStructure(holder)) {
|
||||||
|
byPlacement.computeIfAbsent(placement, ignored -> new LinkedHashSet<>()).add(holder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (byPlacement.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
StructureManager structureManager = level.structureManager();
|
||||||
|
Candidate best = null;
|
||||||
|
double bestDistance = Double.MAX_VALUE;
|
||||||
|
List<Map.Entry<RandomSpreadStructurePlacement, Set<Holder<Structure>>>> randomPlacements =
|
||||||
|
new ArrayList<>(byPlacement.size());
|
||||||
|
for (Map.Entry<StructurePlacement, Set<Holder<Structure>>> entry : byPlacement.entrySet()) {
|
||||||
|
StructurePlacement placement = entry.getKey();
|
||||||
|
if (placement instanceof ConcentricRingsStructurePlacement concentric) {
|
||||||
|
Candidate candidate = predictConcentric(
|
||||||
|
entry.getValue(), level, structureManager, origin, findUnexplored,
|
||||||
|
state, concentric);
|
||||||
|
if (candidate != null) {
|
||||||
|
double distance = origin.distSqr(candidate.result().getFirst());
|
||||||
|
if (distance < bestDistance) {
|
||||||
|
best = candidate;
|
||||||
|
bestDistance = distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (placement instanceof RandomSpreadStructurePlacement randomSpread) {
|
||||||
|
randomPlacements.add(Map.entry(randomSpread, entry.getValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int centerChunkX = SectionPos.blockToSectionCoord(origin.getX());
|
||||||
|
int centerChunkZ = SectionPos.blockToSectionCoord(origin.getZ());
|
||||||
|
int searchRadius = Math.max(0, radius);
|
||||||
|
for (int ring = 0; ring <= searchRadius; ring++) {
|
||||||
|
boolean foundInRing = false;
|
||||||
|
for (Map.Entry<RandomSpreadStructurePlacement, Set<Holder<Structure>>> entry
|
||||||
|
: randomPlacements) {
|
||||||
|
Candidate candidate = predictRandomSpread(
|
||||||
|
entry.getValue(), level, structureManager,
|
||||||
|
centerChunkX, centerChunkZ, ring, findUnexplored,
|
||||||
|
state.getLevelSeed(), entry.getKey());
|
||||||
|
if (candidate == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foundInRing = true;
|
||||||
|
double distance = origin.distSqr(candidate.result().getFirst());
|
||||||
|
if (distance < bestDistance) {
|
||||||
|
best = candidate;
|
||||||
|
bestDistance = distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (foundInRing) {
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Candidate predictConcentric(Set<Holder<Structure>> holders,
|
||||||
|
ServerLevel level,
|
||||||
|
StructureManager structureManager,
|
||||||
|
BlockPos origin,
|
||||||
|
boolean findUnexplored,
|
||||||
|
ChunkGeneratorStructureState state,
|
||||||
|
ConcentricRingsStructurePlacement placement) {
|
||||||
|
List<ChunkPos> positions = state.getRingPositionsFor(placement);
|
||||||
|
if (positions == null) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Tried to locate structures for an unavailable concentric placement");
|
||||||
|
}
|
||||||
|
Candidate best = null;
|
||||||
|
double bestDistance = Double.MAX_VALUE;
|
||||||
|
for (ChunkPos position : positions) {
|
||||||
|
BlockPos locatePosition = new BlockPos(
|
||||||
|
SectionPos.sectionToBlockCoord(position.x(), 8),
|
||||||
|
32,
|
||||||
|
SectionPos.sectionToBlockCoord(position.z(), 8));
|
||||||
|
double distance = locatePosition.distSqr(origin);
|
||||||
|
if (best != null && distance >= bestDistance) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Candidate candidate = predictAt(
|
||||||
|
holders, level, structureManager, findUnexplored, placement, position);
|
||||||
|
if (candidate != null) {
|
||||||
|
best = candidate;
|
||||||
|
bestDistance = distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Candidate predictRandomSpread(Set<Holder<Structure>> holders,
|
||||||
|
ServerLevel level,
|
||||||
|
StructureManager structureManager,
|
||||||
|
int centerChunkX, int centerChunkZ,
|
||||||
|
int ring, boolean findUnexplored,
|
||||||
|
long seed,
|
||||||
|
RandomSpreadStructurePlacement placement) {
|
||||||
|
int spacing = placement.spacing();
|
||||||
|
for (int offsetX = -ring; offsetX <= ring; offsetX++) {
|
||||||
|
boolean edgeX = offsetX == -ring || offsetX == ring;
|
||||||
|
for (int offsetZ = -ring; offsetZ <= ring; offsetZ++) {
|
||||||
|
boolean edgeZ = offsetZ == -ring || offsetZ == ring;
|
||||||
|
if (!edgeX && !edgeZ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int gridChunkX = centerChunkX + spacing * offsetX;
|
||||||
|
int gridChunkZ = centerChunkZ + spacing * offsetZ;
|
||||||
|
ChunkPos candidatePosition = placement.getPotentialStructureChunk(
|
||||||
|
seed, gridChunkX, gridChunkZ);
|
||||||
|
Candidate candidate = predictAt(
|
||||||
|
holders, level, structureManager,
|
||||||
|
findUnexplored, placement, candidatePosition);
|
||||||
|
if (candidate != null) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Candidate predictAt(Set<Holder<Structure>> holders,
|
||||||
|
ServerLevel level,
|
||||||
|
StructureManager structureManager,
|
||||||
|
boolean findUnexplored,
|
||||||
|
StructurePlacement placement,
|
||||||
|
ChunkPos candidatePosition) {
|
||||||
|
for (Holder<Structure> holder : holders) {
|
||||||
|
Structure structure = holder.value();
|
||||||
|
StructureCheckResult result = structureManager.checkStructurePresence(
|
||||||
|
candidatePosition, structure, placement, findUnexplored);
|
||||||
|
if (result == StructureCheckResult.START_NOT_PRESENT) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!findUnexplored && result == StructureCheckResult.START_PRESENT) {
|
||||||
|
return new Candidate(
|
||||||
|
Pair.of(placement.getLocatePos(candidatePosition), holder), null);
|
||||||
|
}
|
||||||
|
ChunkAccess chunk = level.getChunk(
|
||||||
|
candidatePosition.x(), candidatePosition.z(), ChunkStatus.STRUCTURE_STARTS);
|
||||||
|
StructureStart start = structureManager.getStartForStructure(
|
||||||
|
structure, chunk);
|
||||||
|
if (start == null || !start.isValid()
|
||||||
|
|| findUnexplored && !start.canBeReferenced()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return new Candidate(
|
||||||
|
Pair.of(placement.getLocatePos(start.getChunkPos()), holder),
|
||||||
|
findUnexplored ? start : null);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Candidate {
|
||||||
|
private final Pair<BlockPos, Holder<Structure>> result;
|
||||||
|
private final StructureStart referenceStart;
|
||||||
|
private final AtomicBoolean committed;
|
||||||
|
|
||||||
|
private Candidate(Pair<BlockPos, Holder<Structure>> result,
|
||||||
|
StructureStart referenceStart) {
|
||||||
|
this.result = result;
|
||||||
|
this.referenceStart = referenceStart;
|
||||||
|
this.committed = new AtomicBoolean();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pair<BlockPos, Holder<Structure>> result() {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean reference(StructureManager structureManager) {
|
||||||
|
if (referenceStart == null || !committed.compareAndSet(false, true)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!referenceStart.canBeReferenced()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
structureManager.addReference(referenceStart);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+174
@@ -0,0 +1,174 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
|
import net.minecraft.tags.BlockTags;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||||
|
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.BitSet;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class NativeStructureVegetationClearer {
|
||||||
|
private NativeStructureVegetationClearer() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isUndergroundStep(GenerationStep.Decoration step) {
|
||||||
|
return step == GenerationStep.Decoration.UNDERGROUND_STRUCTURES
|
||||||
|
|| step == GenerationStep.Decoration.UNDERGROUND_DECORATION
|
||||||
|
|| step == GenerationStep.Decoration.STRONGHOLDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clearIntersectingVegetation(WorldGenLevel world, ChunkAccess chunk, BoundingBox area,
|
||||||
|
List<StructureStart> starts) {
|
||||||
|
if (starts == null || starts.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
VegetationSnapshot snapshot = captureVegetation(chunk, area);
|
||||||
|
if (snapshot.treeBlockCount() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int columns = area.getXSpan() * area.getZSpan();
|
||||||
|
int[] clearMinY = new int[columns];
|
||||||
|
int[] clearMaxY = new int[columns];
|
||||||
|
Arrays.fill(clearMinY, Integer.MAX_VALUE);
|
||||||
|
Arrays.fill(clearMaxY, Integer.MIN_VALUE);
|
||||||
|
for (StructureStart start : starts) {
|
||||||
|
if (shouldProcessStart(start)) {
|
||||||
|
markVegetationColumns(area, snapshot, start, clearMinY, clearMaxY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clearVegetationColumns(world, area, snapshot, clearMinY, clearMaxY);
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean shouldProcessStart(StructureStart start) {
|
||||||
|
return start != null && start.isValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VegetationSnapshot captureVegetation(ChunkAccess chunk, BoundingBox area) {
|
||||||
|
int width = area.getXSpan();
|
||||||
|
int depth = area.getZSpan();
|
||||||
|
BitSet[] columns = new BitSet[width * depth];
|
||||||
|
int treeBlockCount = 0;
|
||||||
|
LevelChunkSection[] sections = chunk.getSections();
|
||||||
|
int chunkMinX = chunk.getPos().getMinBlockX();
|
||||||
|
int chunkMinZ = chunk.getPos().getMinBlockZ();
|
||||||
|
for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
|
||||||
|
LevelChunkSection section = sections[sectionIndex];
|
||||||
|
int sectionMinY = chunk.getSectionYFromSectionIndex(sectionIndex) << 4;
|
||||||
|
int minY = Math.max(area.minY(), sectionMinY);
|
||||||
|
int maxY = Math.min(area.maxY(), sectionMinY + 15);
|
||||||
|
if (minY > maxY || section.hasOnlyAir()
|
||||||
|
|| !section.maybeHas(NativeStructureVegetationClearer::isTreeBlock)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int y = minY; y <= maxY; y++) {
|
||||||
|
int localY = y - sectionMinY;
|
||||||
|
for (int z = area.minZ(); z <= area.maxZ(); z++) {
|
||||||
|
int localZ = z - chunkMinZ;
|
||||||
|
for (int x = area.minX(); x <= area.maxX(); x++) {
|
||||||
|
int localX = x - chunkMinX;
|
||||||
|
if (!isTreeBlock(section.getBlockState(localX, localY, localZ))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int column = (z - area.minZ()) * width + x - area.minX();
|
||||||
|
BitSet treeBlocks = columns[column];
|
||||||
|
if (treeBlocks == null) {
|
||||||
|
treeBlocks = new BitSet(area.getYSpan());
|
||||||
|
columns[column] = treeBlocks;
|
||||||
|
}
|
||||||
|
treeBlocks.set(y - area.minY());
|
||||||
|
treeBlockCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new VegetationSnapshot(columns, treeBlockCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void markVegetationColumns(BoundingBox area, VegetationSnapshot snapshot,
|
||||||
|
StructureStart start, int[] clearMinY, int[] clearMaxY) {
|
||||||
|
int width = area.getXSpan();
|
||||||
|
int[] pieceTops = new int[clearMinY.length];
|
||||||
|
int[] pieceBottoms = new int[clearMinY.length];
|
||||||
|
Arrays.fill(pieceTops, Integer.MIN_VALUE);
|
||||||
|
Arrays.fill(pieceBottoms, Integer.MAX_VALUE);
|
||||||
|
for (StructurePiece piece : start.getPieces()) {
|
||||||
|
BoundingBox bounds = piece.getBoundingBox();
|
||||||
|
int minX = Math.max(area.minX(), bounds.minX());
|
||||||
|
int maxX = Math.min(area.maxX(), bounds.maxX());
|
||||||
|
int minZ = Math.max(area.minZ(), bounds.minZ());
|
||||||
|
int maxZ = Math.min(area.maxZ(), bounds.maxZ());
|
||||||
|
if (minX > maxX || minZ > maxZ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int z = minZ; z <= maxZ; z++) {
|
||||||
|
for (int x = minX; x <= maxX; x++) {
|
||||||
|
int column = (z - area.minZ()) * width + x - area.minX();
|
||||||
|
pieceTops[column] = Math.max(pieceTops[column], bounds.maxY());
|
||||||
|
pieceBottoms[column] = Math.min(pieceBottoms[column], bounds.minY());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int column = 0; column < pieceTops.length; column++) {
|
||||||
|
if (snapshot.columns()[column] == null || pieceTops[column] == Integer.MIN_VALUE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
clearMinY[column] = Math.min(clearMinY[column], pieceBottoms[column] - 1);
|
||||||
|
clearMaxY[column] = Math.max(clearMaxY[column], pieceTops[column]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void clearVegetationColumns(WorldGenLevel world, BoundingBox area,
|
||||||
|
VegetationSnapshot snapshot, int[] clearMinY, int[] clearMaxY) {
|
||||||
|
int width = area.getXSpan();
|
||||||
|
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
|
||||||
|
BlockState air = Blocks.AIR.defaultBlockState();
|
||||||
|
for (int z = area.minZ(); z <= area.maxZ(); z++) {
|
||||||
|
for (int x = area.minX(); x <= area.maxX(); x++) {
|
||||||
|
int column = (z - area.minZ()) * width + x - area.minX();
|
||||||
|
BitSet treeBlocks = snapshot.columns()[column];
|
||||||
|
if (treeBlocks == null || clearMaxY[column] < clearMinY[column]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int firstBit = Math.max(0, clearMinY[column] - area.minY());
|
||||||
|
int lastBit = Math.min(area.getYSpan() - 1, clearMaxY[column] - area.minY());
|
||||||
|
if (firstBit > lastBit) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int bit = treeBlocks.nextSetBit(firstBit); bit >= 0 && bit <= lastBit;
|
||||||
|
bit = treeBlocks.nextSetBit(bit + 1)) {
|
||||||
|
int y = area.minY() + bit;
|
||||||
|
position.set(x, y, z);
|
||||||
|
BlockState state = world.getBlockState(position);
|
||||||
|
if (isTreeBlock(state)) {
|
||||||
|
world.setBlock(position, air, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isTreeBlock(BlockState state) {
|
||||||
|
if (state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Field reads instead of a registry byValue map hit — this predicate runs per block
|
||||||
|
// probe on the hottest native-structure scans.
|
||||||
|
String path = state.getBlock().builtInRegistryHolder().key().identifier().getPath();
|
||||||
|
return path.endsWith("_log") || path.endsWith("_wood")
|
||||||
|
|| path.endsWith("_stem") || path.endsWith("_hyphae")
|
||||||
|
|| path.endsWith("_leaves");
|
||||||
|
}
|
||||||
|
|
||||||
|
private record VegetationSnapshot(BitSet[] columns, int treeBlockCount) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+384
@@ -0,0 +1,384 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.structure.placement.StructureVerticalBounds;
|
||||||
|
import art.arcane.iris.structure.placement.IrisStructureYBand;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.ScatteredFeaturePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.JigsawJunction;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.JungleTemplePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.JungleTempleStructure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.IntBinaryOperator;
|
||||||
|
|
||||||
|
public final class NativeStructureVerticalPlacer {
|
||||||
|
private static final String DESERT_PYRAMID_ID = "minecraft:desert_pyramid";
|
||||||
|
private static final String JUNGLE_PYRAMID_ID = "minecraft:jungle_pyramid";
|
||||||
|
private static final int MAX_BURIAL_COLUMNS = 2_000_000;
|
||||||
|
private static final int MONUMENT_BASE_BELOW_SEA_LEVEL = 24;
|
||||||
|
private static final String OCEAN_MONUMENT_ID = "minecraft:monument";
|
||||||
|
private static final int UNDERGROUND_SURFACE_CLEARANCE = 1;
|
||||||
|
|
||||||
|
private NativeStructureVerticalPlacer() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int applyVerticalPlacement(StructureStart start, String structureId, int requestedOffset,
|
||||||
|
int seaLevel, int worldMinY, int worldMaxYExclusive,
|
||||||
|
boolean underground, boolean preserveSourceY,
|
||||||
|
IrisStructureYBand yBand,
|
||||||
|
IntBinaryOperator surfaceHeight) {
|
||||||
|
if (isOceanMonument(start, structureId)) {
|
||||||
|
return alignOceanMonumentToSeaLevel(
|
||||||
|
start, requestedOffset, seaLevel, worldMinY, worldMaxYExclusive);
|
||||||
|
}
|
||||||
|
if (isAdjustedScatteredStructure(start, structureId)) {
|
||||||
|
return alignScatteredStructureToSurface(
|
||||||
|
start, structureId, requestedOffset, worldMinY, worldMaxYExclusive, surfaceHeight);
|
||||||
|
}
|
||||||
|
return applyVerticalShift(start, requestedOffset, worldMinY, worldMaxYExclusive,
|
||||||
|
underground, preserveSourceY, yBand, surfaceHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StructureStart relocateToMinY(StructureStart start, Structure source, int targetMinY,
|
||||||
|
LevelHeightAccessor heightAccessor) {
|
||||||
|
Objects.requireNonNull(start, "Native structure start must not be null");
|
||||||
|
Objects.requireNonNull(source, "Native structure source must not be null");
|
||||||
|
Objects.requireNonNull(heightAccessor, "Native structure height accessor must not be null");
|
||||||
|
if (!start.isValid()) {
|
||||||
|
return StructureStart.INVALID_START;
|
||||||
|
}
|
||||||
|
List<StructurePiece> pieces = start.getPieces();
|
||||||
|
int minY = Integer.MAX_VALUE;
|
||||||
|
for (StructurePiece piece : pieces) {
|
||||||
|
minY = Math.min(minY, piece.getBoundingBox().minY());
|
||||||
|
}
|
||||||
|
if (minY == Integer.MAX_VALUE) {
|
||||||
|
return StructureStart.INVALID_START;
|
||||||
|
}
|
||||||
|
int offsetY = Math.subtractExact(targetMinY, minY);
|
||||||
|
if (offsetY != 0) {
|
||||||
|
for (StructurePiece piece : pieces) {
|
||||||
|
moveStructurePiece(piece, offsetY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int worldMinY = heightAccessor.getMinY() + 1;
|
||||||
|
int worldMaxYExclusive = Math.addExact(
|
||||||
|
heightAccessor.getMinY(), heightAccessor.getHeight());
|
||||||
|
for (StructurePiece piece : pieces) {
|
||||||
|
BoundingBox bounds = piece.getBoundingBox();
|
||||||
|
if (bounds.minY() < worldMinY || bounds.maxY() >= worldMaxYExclusive) {
|
||||||
|
throw new IllegalStateException("Native structure cannot fit target minimum Y "
|
||||||
|
+ targetMinY + " inside world bounds [" + worldMinY + ","
|
||||||
|
+ worldMaxYExclusive + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new StructureStart(
|
||||||
|
source,
|
||||||
|
start.getChunkPos(),
|
||||||
|
start.getReferences(),
|
||||||
|
new PiecesContainer(List.copyOf(pieces))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int alignScatteredStructureToSurface(StructureStart start, String structureId,
|
||||||
|
int configuredOffset, int worldMinY,
|
||||||
|
int worldMaxYExclusive,
|
||||||
|
IntBinaryOperator surfaceHeight) {
|
||||||
|
Objects.requireNonNull(surfaceHeight, "Scattered native structure requires a terrain height resolver");
|
||||||
|
ScatteredFeaturePiece piece = requireAdjustedScatteredPiece(start, structureId);
|
||||||
|
BoundingBox bounds = start.getBoundingBox();
|
||||||
|
BoundingBox pieceBounds = piece.getBoundingBox();
|
||||||
|
int surfaceY = representativeScatteredSurfaceY(structureId, pieceBounds, surfaceHeight);
|
||||||
|
int targetMinY = Math.addExact(Math.addExact(surfaceY, 1), configuredOffset);
|
||||||
|
int requestedMove = Math.subtractExact(targetMinY, pieceBounds.minY());
|
||||||
|
int offsetY = StructureVerticalBounds.clampOffset(
|
||||||
|
bounds.minY(), bounds.maxY(), requestedMove, worldMinY, worldMaxYExclusive);
|
||||||
|
if (offsetY != 0) {
|
||||||
|
moveStructureStart(start, bounds, offsetY);
|
||||||
|
}
|
||||||
|
setScatteredHeightPosition(piece, Math.max(0, piece.getBoundingBox().minY()));
|
||||||
|
return offsetY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int applyVerticalShift(StructureStart start, int requestedOffset, int worldMinY,
|
||||||
|
int worldMaxYExclusive, boolean underground,
|
||||||
|
boolean preserveSourceY, IrisStructureYBand yBand,
|
||||||
|
IntBinaryOperator surfaceHeight) {
|
||||||
|
BoundingBox bounds = start.getBoundingBox();
|
||||||
|
int resolvedOffset = resolveShiftOffset(start, bounds, requestedOffset, worldMinY,
|
||||||
|
worldMaxYExclusive, underground, preserveSourceY, yBand, surfaceHeight);
|
||||||
|
int offsetY = StructureVerticalBounds.clampOffset(
|
||||||
|
bounds.minY(), bounds.maxY(), resolvedOffset, worldMinY, worldMaxYExclusive);
|
||||||
|
if (offsetY == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
moveStructureStart(start, bounds, offsetY);
|
||||||
|
return offsetY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int resolveShiftOffset(StructureStart start, BoundingBox bounds, int requestedOffset,
|
||||||
|
int worldMinY, int worldMaxYExclusive, boolean underground,
|
||||||
|
boolean preserveSourceY, IrisStructureYBand yBand,
|
||||||
|
IntBinaryOperator surfaceHeight) {
|
||||||
|
if (preserveSourceY) {
|
||||||
|
return requestedOffset;
|
||||||
|
}
|
||||||
|
if (yBand != null) {
|
||||||
|
return resolveYBandOffset(bounds, yBand, start.getChunkPos());
|
||||||
|
}
|
||||||
|
if (underground) {
|
||||||
|
return resolveBuriedOffset(
|
||||||
|
bounds, requestedOffset, worldMinY, worldMaxYExclusive, surfaceHeight);
|
||||||
|
}
|
||||||
|
return requestedOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int alignOceanMonumentToSeaLevel(StructureStart start, int configuredOffset, int seaLevel,
|
||||||
|
int worldMinY, int worldMaxYExclusive) {
|
||||||
|
OceanMonumentPieces.MonumentBuilding building = requireOceanMonumentBuilding(start);
|
||||||
|
BoundingBox bounds = start.getBoundingBox();
|
||||||
|
int targetMinY = Math.addExact(
|
||||||
|
Math.subtractExact(seaLevel, MONUMENT_BASE_BELOW_SEA_LEVEL), configuredOffset);
|
||||||
|
int requestedOffset = Math.subtractExact(targetMinY, building.getBoundingBox().minY());
|
||||||
|
int offsetY = StructureVerticalBounds.clampOffset(
|
||||||
|
bounds.minY(), bounds.maxY(), requestedOffset, worldMinY, worldMaxYExclusive);
|
||||||
|
if (offsetY != requestedOffset) {
|
||||||
|
throw new IllegalStateException("Ocean monument cannot align to sea level " + seaLevel
|
||||||
|
+ " with configured offset " + configuredOffset + " inside world bounds ["
|
||||||
|
+ worldMinY + "," + worldMaxYExclusive + ")");
|
||||||
|
}
|
||||||
|
if (offsetY == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
moveStructureStart(start, bounds, offsetY);
|
||||||
|
return offsetY;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ensureMonumentSeaLevelAlignment(StructureStart start, String structureId,
|
||||||
|
int configuredOffset, int seaLevel,
|
||||||
|
int worldMinY, int worldMaxYExclusive) {
|
||||||
|
if (!isOceanMonument(start, structureId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
synchronized (start) {
|
||||||
|
alignOceanMonumentToSeaLevel(
|
||||||
|
start, configuredOffset, seaLevel, worldMinY, worldMaxYExclusive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isOceanMonument(StructureStart start, String structureId) {
|
||||||
|
return OCEAN_MONUMENT_ID.equals(structureId)
|
||||||
|
&& start != null
|
||||||
|
&& start.getStructure() instanceof OceanMonumentStructure;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isAdjustedScatteredStructure(StructureStart start, String structureId) {
|
||||||
|
if (start == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Structure source = start.getStructure();
|
||||||
|
return (DESERT_PYRAMID_ID.equals(structureId) && source instanceof DesertPyramidStructure)
|
||||||
|
|| (JUNGLE_PYRAMID_ID.equals(structureId) && source instanceof JungleTempleStructure);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ScatteredFeaturePiece requireAdjustedScatteredPiece(StructureStart start,
|
||||||
|
String structureId) {
|
||||||
|
Objects.requireNonNull(start, "Scattered native structure start must not be null");
|
||||||
|
List<StructurePiece> pieces = start.getPieces();
|
||||||
|
if (pieces.size() != 1) {
|
||||||
|
throw new IllegalStateException(structureId + " must contain exactly one scattered piece, found "
|
||||||
|
+ pieces.size());
|
||||||
|
}
|
||||||
|
StructurePiece piece = pieces.get(0);
|
||||||
|
if (DESERT_PYRAMID_ID.equals(structureId) && piece instanceof DesertPyramidPiece desertPyramid) {
|
||||||
|
return desertPyramid;
|
||||||
|
}
|
||||||
|
if (JUNGLE_PYRAMID_ID.equals(structureId) && piece instanceof JungleTemplePiece jungleTemple) {
|
||||||
|
return jungleTemple;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException(structureId + " contains unexpected piece "
|
||||||
|
+ piece.getClass().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int representativeScatteredSurfaceY(String structureId, BoundingBox bounds,
|
||||||
|
IntBinaryOperator surfaceHeight) {
|
||||||
|
if (DESERT_PYRAMID_ID.equals(structureId)) {
|
||||||
|
return lowestSurfaceY(bounds, surfaceHeight);
|
||||||
|
}
|
||||||
|
if (JUNGLE_PYRAMID_ID.equals(structureId)) {
|
||||||
|
return averageSurfaceY(bounds, surfaceHeight);
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Unsupported scattered native structure " + structureId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int lowestSurfaceY(BoundingBox bounds, IntBinaryOperator surfaceHeight) {
|
||||||
|
int lowestY = Integer.MAX_VALUE;
|
||||||
|
for (int x = bounds.minX(); x <= bounds.maxX(); x++) {
|
||||||
|
for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) {
|
||||||
|
lowestY = Math.min(lowestY, surfaceHeight.applyAsInt(x, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lowestY == Integer.MAX_VALUE) {
|
||||||
|
throw new IllegalStateException("Scattered native structure has an empty terrain footprint");
|
||||||
|
}
|
||||||
|
return lowestY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int averageSurfaceY(BoundingBox bounds, IntBinaryOperator surfaceHeight) {
|
||||||
|
long totalY = 0L;
|
||||||
|
long columns = 0L;
|
||||||
|
for (int x = bounds.minX(); x <= bounds.maxX(); x++) {
|
||||||
|
for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) {
|
||||||
|
totalY += surfaceHeight.applyAsInt(x, z);
|
||||||
|
columns++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (columns == 0L) {
|
||||||
|
throw new IllegalStateException("Scattered native structure has an empty terrain footprint");
|
||||||
|
}
|
||||||
|
return Math.toIntExact(totalY / columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setScatteredHeightPosition(ScatteredFeaturePiece piece, int heightPosition) {
|
||||||
|
try {
|
||||||
|
NativeStructureReflection.ScatteredHeightPositionAccess.FIELD.setInt(piece, heightPosition);
|
||||||
|
} catch (IllegalAccessException error) {
|
||||||
|
throw new IllegalStateException("Cannot lock scattered native structure height", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OceanMonumentPieces.MonumentBuilding requireOceanMonumentBuilding(StructureStart start) {
|
||||||
|
Objects.requireNonNull(start, "Ocean monument start must not be null");
|
||||||
|
List<StructurePiece> pieces = start.getPieces();
|
||||||
|
OceanMonumentPieces.MonumentBuilding building = null;
|
||||||
|
for (StructurePiece piece : pieces) {
|
||||||
|
if (piece instanceof OceanMonumentPieces.MonumentBuilding monumentBuilding) {
|
||||||
|
building = monumentBuilding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pieces.size() != 1 || building == null) {
|
||||||
|
throw new IllegalStateException("minecraft:monument must contain exactly one MonumentBuilding, found "
|
||||||
|
+ pieces.size() + " pieces");
|
||||||
|
}
|
||||||
|
return building;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void moveStructureStart(StructureStart start, BoundingBox cachedBounds, int offsetY) {
|
||||||
|
for (StructurePiece piece : start.getPieces()) {
|
||||||
|
moveStructurePiece(piece, offsetY);
|
||||||
|
}
|
||||||
|
cachedBounds.move(0, offsetY, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void moveStructurePiece(StructurePiece piece, int offsetY) {
|
||||||
|
piece.move(0, offsetY, 0);
|
||||||
|
if (piece instanceof OceanMonumentPieces.MonumentBuilding building) {
|
||||||
|
for (StructurePiece child : monumentChildPieces(building)) {
|
||||||
|
child.move(0, offsetY, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (piece instanceof PoolElementStructurePiece poolPiece) {
|
||||||
|
List<JigsawJunction> junctions = poolPiece.getJunctions();
|
||||||
|
for (int i = 0; i < junctions.size(); i++) {
|
||||||
|
JigsawJunction junction = junctions.get(i);
|
||||||
|
junctions.set(i, new JigsawJunction(
|
||||||
|
junction.getSourceX(),
|
||||||
|
junction.getSourceGroundY() + offsetY,
|
||||||
|
junction.getSourceZ(),
|
||||||
|
junction.getDeltaY(),
|
||||||
|
junction.getDestProjection()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<StructurePiece> monumentChildPieces(OceanMonumentPieces.MonumentBuilding building) {
|
||||||
|
Object value;
|
||||||
|
try {
|
||||||
|
value = NativeStructureReflection.MonumentChildPiecesAccess.FIELD.get(building);
|
||||||
|
} catch (IllegalAccessException error) {
|
||||||
|
throw new IllegalStateException("Cannot read Ocean Monument child pieces", error);
|
||||||
|
}
|
||||||
|
if (!(value instanceof List<?> children)) {
|
||||||
|
throw new IllegalStateException("Ocean Monument child-pieces field is not a list");
|
||||||
|
}
|
||||||
|
List<StructurePiece> pieces = new ArrayList<>(children.size());
|
||||||
|
for (Object child : children) {
|
||||||
|
if (!(child instanceof StructurePiece monumentPiece)
|
||||||
|
|| child.getClass().getEnclosingClass() != OceanMonumentPieces.class) {
|
||||||
|
throw new IllegalStateException("Ocean Monument child-pieces list contains "
|
||||||
|
+ (child == null ? "null" : child.getClass().getName()));
|
||||||
|
}
|
||||||
|
pieces.add(monumentPiece);
|
||||||
|
}
|
||||||
|
return List.copyOf(pieces);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int resolveYBandOffset(BoundingBox bounds, IrisStructureYBand yBand, ChunkPos startChunk) {
|
||||||
|
Objects.requireNonNull(bounds, "Native structure bounds must not be null");
|
||||||
|
Objects.requireNonNull(startChunk, "Native structure Y band requires a start chunk");
|
||||||
|
int target = yBandTargetMidpointY(
|
||||||
|
bounds.minY(), bounds.maxY(), yBand.resolvedMin(), yBand.resolvedMax(), startChunk);
|
||||||
|
return Math.subtractExact(target, midpointY(bounds.minY(), bounds.maxY()));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int yBandTargetMidpointY(int minY, int maxY, int bandMin, int bandMax, ChunkPos startChunk) {
|
||||||
|
int height = Math.subtractExact(maxY, minY);
|
||||||
|
int lowest = Math.addExact(bandMin, height / 2);
|
||||||
|
int highest = Math.subtractExact(bandMax, height - height / 2);
|
||||||
|
if (lowest > highest) {
|
||||||
|
// The band is shorter than the structure, so only its midpoint can be honoured.
|
||||||
|
return Math.floorDiv(Math.addExact(bandMin, bandMax), 2);
|
||||||
|
}
|
||||||
|
int span = highest - lowest + 1;
|
||||||
|
if (span == 1) {
|
||||||
|
return lowest;
|
||||||
|
}
|
||||||
|
long identity = (long) startChunk.x() * 341873128712L ^ (long) startChunk.z() * 132897987541L;
|
||||||
|
return lowest + new RNG(identity).nextInt(span);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int midpointY(int minY, int maxY) {
|
||||||
|
return minY + (maxY - minY) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int resolveBuriedOffset(BoundingBox bounds, int requestedOffset, int worldMinY,
|
||||||
|
int worldMaxYExclusive, IntBinaryOperator surfaceHeight) {
|
||||||
|
Objects.requireNonNull(bounds, "Native structure bounds must not be null");
|
||||||
|
Objects.requireNonNull(surfaceHeight, "Underground native structure requires a terrain height resolver");
|
||||||
|
long width = (long) bounds.maxX() - bounds.minX() + 1L;
|
||||||
|
long depth = (long) bounds.maxZ() - bounds.minZ() + 1L;
|
||||||
|
if (width <= 0L || depth <= 0L || width > MAX_BURIAL_COLUMNS / depth) {
|
||||||
|
throw new IllegalStateException("Underground native structure burial footprint is invalid or exceeds "
|
||||||
|
+ MAX_BURIAL_COLUMNS + " columns");
|
||||||
|
}
|
||||||
|
int maximumOffset = requestedOffset;
|
||||||
|
for (int x = bounds.minX(); x <= bounds.maxX(); x++) {
|
||||||
|
for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) {
|
||||||
|
int allowedTopY = surfaceHeight.applyAsInt(x, z) - UNDERGROUND_SURFACE_CLEARANCE;
|
||||||
|
maximumOffset = Math.min(maximumOffset, allowedTopY - bounds.maxY());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int clampedOffset = StructureVerticalBounds.clampOffset(
|
||||||
|
bounds.minY(), bounds.maxY(), maximumOffset, worldMinY, worldMaxYExclusive);
|
||||||
|
if (clampedOffset > maximumOffset) {
|
||||||
|
IrisLogging.warn("Native structure burial at " + bounds.minX() + "," + bounds.minZ()
|
||||||
|
+ " clamped to world floor: wanted " + maximumOffset + ", used " + clampedOffset);
|
||||||
|
}
|
||||||
|
return clampedOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
+643
@@ -0,0 +1,643 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||||
|
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructurePlacementPlanner;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureVolume;
|
||||||
|
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||||
|
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
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.BiomeSource;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.WeakHashMap;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ForkJoinPool;
|
||||||
|
import java.util.concurrent.ForkJoinTask;
|
||||||
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
import java.util.function.IntBinaryOperator;
|
||||||
|
import java.util.function.IntConsumer;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the world-space piece bounds of every native structure that will generate around a query rect.
|
||||||
|
*
|
||||||
|
* <p>The answer is derived from the seed, the registries and this pack's structure policy only: candidate origins
|
||||||
|
* come from the placement grid and from vanilla structure placements, and each candidate is assembled through the
|
||||||
|
* same {@link NativeStructureFactory} path chunk generation uses. Nothing here reads chunk state, ownership records
|
||||||
|
* or generation progress, so the same query answers identically no matter which chunks already exist.
|
||||||
|
*/
|
||||||
|
public final class NativeStructureVolumeIndex {
|
||||||
|
private static final int ORIGIN_REACH_CHUNKS = NativeStructureOwnershipRecord.MAX_REFERENCE_DISTANCE_CHUNKS;
|
||||||
|
private static final int ORIGIN_WINDOW_DIAMETER = ORIGIN_REACH_CHUNKS * 2 + 1;
|
||||||
|
private static final int ORIGIN_WINDOW_STRIPE_COUNT = Long.SIZE;
|
||||||
|
private static final int MAX_CACHED_ORIGIN_CHUNKS = 16_384;
|
||||||
|
private static final int MAX_CACHED_QUERY_CHUNKS = 4_096;
|
||||||
|
private static final Map<Engine, NativeStructureVolumeIndex> INDEXES =
|
||||||
|
Collections.synchronizedMap(new WeakHashMap<>());
|
||||||
|
private static final Set<String> WARNED_RESOLUTION_FAILURES = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final OriginResolver origins;
|
||||||
|
private final Map<RuntimeChunkKey, KList<NativeStructureVolume>> originCache = lru(MAX_CACHED_ORIGIN_CHUNKS);
|
||||||
|
private final Map<RuntimeChunkKey, KList<NativeStructureVolume>> queryCache = lru(MAX_CACHED_QUERY_CHUNKS);
|
||||||
|
private final ConcurrentHashMap<RuntimeChunkKey, CompletableFuture<KList<NativeStructureVolume>>> originBuilds =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
|
private final ConcurrentHashMap<RuntimeChunkKey, CompletableFuture<KList<NativeStructureVolume>>> queryBuilds =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
|
private final ReentrantLock[] originWindowStripes = createOriginWindowStripes();
|
||||||
|
private final IntConsumer retirementListener = this::evictRuntime;
|
||||||
|
|
||||||
|
private NativeStructureVolumeIndex(Context context) {
|
||||||
|
this.context = Objects.requireNonNull(context, "Native structure volume context must not be null");
|
||||||
|
this.origins = this::buildOriginVolumes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private NativeStructureVolumeIndex(OriginResolver origins) {
|
||||||
|
this.context = null;
|
||||||
|
this.origins = Objects.requireNonNull(origins, "Native structure origin resolver must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void install(Engine engine, Context context) {
|
||||||
|
Engine requiredEngine = Objects.requireNonNull(engine, "Native structure volume index requires an engine");
|
||||||
|
NativeStructureVolumeIndex installed = new NativeStructureVolumeIndex(context);
|
||||||
|
synchronized (INDEXES) {
|
||||||
|
NativeStructureVolumeIndex previous = INDEXES.put(requiredEngine, installed);
|
||||||
|
if (previous != null) {
|
||||||
|
previous.detachRetirementListener(requiredEngine);
|
||||||
|
}
|
||||||
|
installed.attachRetirementListener(requiredEngine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void uninstall(Engine engine) {
|
||||||
|
if (engine != null) {
|
||||||
|
synchronized (INDEXES) {
|
||||||
|
NativeStructureVolumeIndex removed = INDEXES.remove(engine);
|
||||||
|
if (removed != null) {
|
||||||
|
removed.detachRetirementListener(engine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void invalidate(Engine engine) {
|
||||||
|
if (engine == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
synchronized (INDEXES) {
|
||||||
|
NativeStructureVolumeIndex current = INDEXES.get(engine);
|
||||||
|
if (current != null) {
|
||||||
|
NativeStructureVolumeIndex replacement = current.fresh();
|
||||||
|
current.detachRetirementListener(engine);
|
||||||
|
INDEXES.put(engine, replacement);
|
||||||
|
replacement.attachRetirementListener(engine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static KList<NativeStructureVolume> volumes(Engine engine, int minX, int minZ, int maxX, int maxZ) {
|
||||||
|
NativeStructureVolumeIndex index = engine == null ? null : INDEXES.get(engine);
|
||||||
|
return index == null ? NativeStructureVolume.NONE : index.resolve(engine, minX, minZ, maxX, maxZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
static NativeStructureVolumeIndex forTesting(OriginResolver origins) {
|
||||||
|
return new NativeStructureVolumeIndex(origins);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int originReachChunks() {
|
||||||
|
return ORIGIN_REACH_CHUNKS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private NativeStructureVolumeIndex fresh() {
|
||||||
|
return context == null
|
||||||
|
? new NativeStructureVolumeIndex(origins)
|
||||||
|
: new NativeStructureVolumeIndex(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
KList<NativeStructureVolume> resolve(Engine engine, int minX, int minZ, int maxX, int maxZ) {
|
||||||
|
int runtimeId = runtimeId(engine);
|
||||||
|
int fromChunkX = Math.min(minX, maxX) >> 4;
|
||||||
|
int toChunkX = Math.max(minX, maxX) >> 4;
|
||||||
|
int fromChunkZ = Math.min(minZ, maxZ) >> 4;
|
||||||
|
int toChunkZ = Math.max(minZ, maxZ) >> 4;
|
||||||
|
KList<NativeStructureVolume> matches = null;
|
||||||
|
for (int chunkX = fromChunkX; chunkX <= toChunkX; chunkX++) {
|
||||||
|
for (int chunkZ = fromChunkZ; chunkZ <= toChunkZ; chunkZ++) {
|
||||||
|
for (NativeStructureVolume volume : chunkVolumes(engine, runtimeId, chunkX, chunkZ)) {
|
||||||
|
if (!volume.intersectsRect(minX, minZ, maxX, maxZ)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (matches == null) {
|
||||||
|
matches = new KList<>();
|
||||||
|
}
|
||||||
|
if (!matches.contains(volume)) {
|
||||||
|
matches.add(volume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matches == null ? NativeStructureVolume.NONE : matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
private KList<NativeStructureVolume> chunkVolumes(
|
||||||
|
Engine engine,
|
||||||
|
int runtimeId,
|
||||||
|
int chunkX,
|
||||||
|
int chunkZ
|
||||||
|
) {
|
||||||
|
return cached(queryCache, queryBuilds, new RuntimeChunkKey(runtimeId, chunkKey(chunkX, chunkZ)),
|
||||||
|
() -> buildChunkVolumes(engine, chunkX, chunkZ));
|
||||||
|
}
|
||||||
|
|
||||||
|
private KList<NativeStructureVolume> buildChunkVolumes(Engine engine, int chunkX, int chunkZ) {
|
||||||
|
long stripeMask = originWindowStripeMask(chunkX, chunkZ);
|
||||||
|
lockOriginWindow(stripeMask);
|
||||||
|
try {
|
||||||
|
return buildChunkVolumesCoordinated(engine, chunkX, chunkZ);
|
||||||
|
} finally {
|
||||||
|
unlockOriginWindow(stripeMask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private KList<NativeStructureVolume> buildChunkVolumesCoordinated(Engine engine, int chunkX, int chunkZ) {
|
||||||
|
int minX = chunkX << 4;
|
||||||
|
int minZ = chunkZ << 4;
|
||||||
|
int maxX = minX + 15;
|
||||||
|
int maxZ = minZ + 15;
|
||||||
|
Integer pinnedRuntimeId = pinnedRuntimeId(engine);
|
||||||
|
KList<NativeStructureVolume> volumes = null;
|
||||||
|
for (int originX = chunkX - ORIGIN_REACH_CHUNKS; originX <= chunkX + ORIGIN_REACH_CHUNKS; originX++) {
|
||||||
|
for (int originZ = chunkZ - ORIGIN_REACH_CHUNKS; originZ <= chunkZ + ORIGIN_REACH_CHUNKS; originZ++) {
|
||||||
|
for (NativeStructureVolume volume : scopedOriginVolumes(engine, originX, originZ, pinnedRuntimeId)) {
|
||||||
|
if (!volume.intersectsRect(minX, minZ, maxX, maxZ)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (volumes == null) {
|
||||||
|
volumes = new KList<>();
|
||||||
|
}
|
||||||
|
volumes.add(volume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return volumes == null ? NativeStructureVolume.NONE : volumes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Integer pinnedRuntimeId(Engine engine) {
|
||||||
|
if (!(engine instanceof IrisEngine irisEngine) || !irisEngine.hasGenerationRuntimeScope()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null);
|
||||||
|
if (router == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeOwnership ownership = router.currentRuntimeOwnership().orElse(null);
|
||||||
|
int runtimeId = runtimeId(engine);
|
||||||
|
return ownership != null && ownership.binding().runtimeId() == runtimeId ? runtimeId : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private KList<NativeStructureVolume> scopedOriginVolumes(
|
||||||
|
Engine engine, int chunkX, int chunkZ, Integer pinnedRuntimeId
|
||||||
|
) {
|
||||||
|
if (pinnedRuntimeId != null) {
|
||||||
|
synchronized (originCache) {
|
||||||
|
KList<NativeStructureVolume> cached = originCache.get(
|
||||||
|
new RuntimeChunkKey(pinnedRuntimeId, chunkKey(chunkX, chunkZ)));
|
||||||
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return originVolumes(engine, chunkX, chunkZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
static long originWindowStripeMask(int chunkX, int chunkZ) {
|
||||||
|
int minCellX = Math.floorDiv(chunkX - ORIGIN_REACH_CHUNKS, ORIGIN_WINDOW_DIAMETER);
|
||||||
|
int maxCellX = Math.floorDiv(chunkX + ORIGIN_REACH_CHUNKS, ORIGIN_WINDOW_DIAMETER);
|
||||||
|
int minCellZ = Math.floorDiv(chunkZ - ORIGIN_REACH_CHUNKS, ORIGIN_WINDOW_DIAMETER);
|
||||||
|
int maxCellZ = Math.floorDiv(chunkZ + ORIGIN_REACH_CHUNKS, ORIGIN_WINDOW_DIAMETER);
|
||||||
|
long mask = 0L;
|
||||||
|
for (int cellX = minCellX; cellX <= maxCellX; cellX++) {
|
||||||
|
for (int cellZ = minCellZ; cellZ <= maxCellZ; cellZ++) {
|
||||||
|
mask |= 1L << originWindowStripe(cellX, cellZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
KList<NativeStructureVolume> originVolumes(Engine engine, int chunkX, int chunkZ) {
|
||||||
|
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
|
||||||
|
openHistoryCoordinateScope(engine, chunkX, chunkZ)) {
|
||||||
|
int runtimeId = runtimeId(engine);
|
||||||
|
return cached(
|
||||||
|
originCache,
|
||||||
|
originBuilds,
|
||||||
|
new RuntimeChunkKey(runtimeId, chunkKey(chunkX, chunkZ)),
|
||||||
|
() -> origins.volumesAt(engine, chunkX, chunkZ)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private KList<NativeStructureVolume> buildOriginVolumes(Engine engine, int chunkX, int chunkZ) {
|
||||||
|
ChunkGeneratorStructureState state = context.structureState().get();
|
||||||
|
ChunkGenerator generator = context.generator().get();
|
||||||
|
BiomeSource biomeSource = context.biomeSource().get();
|
||||||
|
if (state == null || generator == null || biomeSource == null) {
|
||||||
|
return NativeStructureVolume.NONE;
|
||||||
|
}
|
||||||
|
Registry<Structure> registry = context.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||||
|
Bindings bindings = new Bindings(state, generator, biomeSource);
|
||||||
|
KList<NativeStructureVolume> volumes = appendPlannedVolumes(engine, bindings, registry, chunkX, chunkZ, null);
|
||||||
|
volumes = appendVanillaVolumes(engine, bindings, registry, chunkX, chunkZ, volumes);
|
||||||
|
return volumes == null ? NativeStructureVolume.NONE : volumes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private KList<NativeStructureVolume> appendPlannedVolumes(Engine engine, Bindings bindings,
|
||||||
|
Registry<Structure> registry, int chunkX, int chunkZ,
|
||||||
|
KList<NativeStructureVolume> volumes) {
|
||||||
|
KList<NativeStructureStartPlan> plans;
|
||||||
|
try {
|
||||||
|
plans = NativeStructurePlacementPlanner.plansAt(engine, chunkX, chunkZ);
|
||||||
|
} catch (RuntimeException | Error error) {
|
||||||
|
warnOnce("iris-placements", error);
|
||||||
|
return volumes;
|
||||||
|
}
|
||||||
|
|
||||||
|
KList<NativeStructureVolume> target = volumes;
|
||||||
|
for (NativeStructureStartPlan plan : plans) {
|
||||||
|
IrisNativeStructureDecision decision = NativeStructurePlacementPlanner.decisionFor(plan);
|
||||||
|
if (!decision.generate()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Identifier identifier = Identifier.tryParse(plan.source().getStructure());
|
||||||
|
if (identifier == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Structure structure = registry.getValue(identifier);
|
||||||
|
if (structure == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String structureId = identifier.toString();
|
||||||
|
try {
|
||||||
|
StructureStart generated = NativeStructureFactory.generate(
|
||||||
|
generationContext(engine, bindings), registry.wrapAsHolder(structure), plan, 0);
|
||||||
|
target = appendPieces(target, structureId, generated);
|
||||||
|
} catch (RuntimeException | Error error) {
|
||||||
|
warnOnce(structureId, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
private KList<NativeStructureVolume> appendVanillaVolumes(Engine engine, Bindings bindings,
|
||||||
|
Registry<Structure> registry, int chunkX, int chunkZ,
|
||||||
|
KList<NativeStructureVolume> volumes) {
|
||||||
|
ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
|
||||||
|
IntBinaryOperator surfaceHeight = surfaceHeight(engine);
|
||||||
|
int worldMinY = context.heightAccessor().getMinY();
|
||||||
|
int worldMaxYExclusive = worldMinY + context.heightAccessor().getHeight();
|
||||||
|
KList<NativeStructureVolume> target = volumes;
|
||||||
|
for (Holder<StructureSet> setHolder : bindings.state().possibleStructureSets()) {
|
||||||
|
StructureSet set = setHolder.value();
|
||||||
|
if (!set.placement().isStructureChunk(bindings.state(), chunkX, chunkZ)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (StructureSet.StructureSelectionEntry entry : set.structures()) {
|
||||||
|
Holder<Structure> holder = entry.structure();
|
||||||
|
Structure structure = holder.value();
|
||||||
|
Identifier identifier = registry.getKey(structure);
|
||||||
|
if (identifier == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String structureId = identifier.toString();
|
||||||
|
boolean undergroundStep = NativeStructureVegetationClearer.isUndergroundStep(structure.step());
|
||||||
|
try {
|
||||||
|
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(
|
||||||
|
engine, structureId, undergroundStep);
|
||||||
|
if (!decision.generate()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
StructureStart generated = structure.generate(
|
||||||
|
holder,
|
||||||
|
context.levelKey(),
|
||||||
|
context.registryAccess(),
|
||||||
|
bindings.generator(),
|
||||||
|
bindings.biomeSource(),
|
||||||
|
bindings.state().randomState().createClimateSampler(SamplerContext.EMPTY_UNCACHED),
|
||||||
|
bindings.state().randomState(),
|
||||||
|
context.templateManager(),
|
||||||
|
bindings.state().getLevelSeed(),
|
||||||
|
chunkPos,
|
||||||
|
0,
|
||||||
|
context.heightAccessor(),
|
||||||
|
structure.biomes()::contains
|
||||||
|
);
|
||||||
|
if (generated == null || !generated.isValid()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||||
|
generated,
|
||||||
|
structureId,
|
||||||
|
decision.yShift(),
|
||||||
|
bindings.generator().getSeaLevel(),
|
||||||
|
worldMinY,
|
||||||
|
worldMaxYExclusive,
|
||||||
|
undergroundStep,
|
||||||
|
decision.preserveSourceY(),
|
||||||
|
decision.yBand(),
|
||||||
|
surfaceHeight);
|
||||||
|
target = appendPieces(target, structureId, generated);
|
||||||
|
} catch (RuntimeException | Error error) {
|
||||||
|
warnOnce(structureId, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
private NativeStructureFactory.GenerationContext generationContext(Engine engine, Bindings bindings) {
|
||||||
|
return new NativeStructureFactory.GenerationContext(
|
||||||
|
context.registryAccess(),
|
||||||
|
bindings.generator(),
|
||||||
|
bindings.biomeSource(),
|
||||||
|
bindings.state().randomState(),
|
||||||
|
context.templateManager(),
|
||||||
|
bindings.state().getLevelSeed(),
|
||||||
|
context.levelKey(),
|
||||||
|
context.heightAccessor(),
|
||||||
|
biome -> true,
|
||||||
|
bindings.generator().getSeaLevel(),
|
||||||
|
surfaceHeight(engine)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Bindings(ChunkGeneratorStructureState state, ChunkGenerator generator, BiomeSource biomeSource) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
interface OriginResolver {
|
||||||
|
KList<NativeStructureVolume> volumesAt(Engine engine, int chunkX, int chunkZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IntBinaryOperator surfaceHeight(Engine engine) {
|
||||||
|
return (x, z) -> Engine.hostHeight(engine, x, z, true) + engine.getMinHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
static KList<NativeStructureVolume> appendPieces(KList<NativeStructureVolume> volumes,
|
||||||
|
String structureId, StructureStart start) {
|
||||||
|
if (start == null || !start.isValid()) {
|
||||||
|
return volumes;
|
||||||
|
}
|
||||||
|
KList<NativeStructureVolume> target = volumes;
|
||||||
|
for (StructurePiece piece : start.getPieces()) {
|
||||||
|
BoundingBox bounds = piece.getBoundingBox();
|
||||||
|
if (target == null) {
|
||||||
|
target = new KList<>();
|
||||||
|
}
|
||||||
|
target.add(new NativeStructureVolume(structureId,
|
||||||
|
bounds.minX(), bounds.minY(), bounds.minZ(),
|
||||||
|
bounds.maxX(), bounds.maxY(), bounds.maxZ()));
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
void evictRuntime(int runtimeId) {
|
||||||
|
synchronized (originCache) {
|
||||||
|
originBuilds.keySet().removeIf(key -> key.runtimeId() == runtimeId);
|
||||||
|
originCache.keySet().removeIf(key -> key.runtimeId() == runtimeId);
|
||||||
|
}
|
||||||
|
synchronized (queryCache) {
|
||||||
|
queryBuilds.keySet().removeIf(key -> key.runtimeId() == runtimeId);
|
||||||
|
queryCache.keySet().removeIf(key -> key.runtimeId() == runtimeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private KList<NativeStructureVolume> cached(
|
||||||
|
Map<RuntimeChunkKey, KList<NativeStructureVolume>> cache,
|
||||||
|
ConcurrentHashMap<RuntimeChunkKey, CompletableFuture<KList<NativeStructureVolume>>> builds,
|
||||||
|
RuntimeChunkKey key,
|
||||||
|
Supplier<KList<NativeStructureVolume>> loader
|
||||||
|
) {
|
||||||
|
synchronized (cache) {
|
||||||
|
KList<NativeStructureVolume> hit = cache.get(key);
|
||||||
|
if (hit != null) {
|
||||||
|
return hit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompletableFuture<KList<NativeStructureVolume>> future = new CompletableFuture<>();
|
||||||
|
CompletableFuture<KList<NativeStructureVolume>> existing = builds.putIfAbsent(key, future);
|
||||||
|
if (existing != null) {
|
||||||
|
return NativeBuildFutures.awaitBuild(existing, "Native structure volume build");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the check-then-claim window: a racer whose cache check missed before the
|
||||||
|
// previous builder cached could claim the build slot after it retired and rebuild.
|
||||||
|
synchronized (cache) {
|
||||||
|
KList<NativeStructureVolume> published = cache.get(key);
|
||||||
|
if (published != null) {
|
||||||
|
future.complete(published);
|
||||||
|
builds.remove(key, future);
|
||||||
|
return published;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
KList<NativeStructureVolume> built = loader.get();
|
||||||
|
synchronized (cache) {
|
||||||
|
if (builds.get(key) == future) {
|
||||||
|
cache.put(key, built);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
future.complete(built);
|
||||||
|
return built;
|
||||||
|
} catch (RuntimeException | Error error) {
|
||||||
|
future.completeExceptionally(error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
builds.remove(key, future);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void warnOnce(String structureId, Throwable error) {
|
||||||
|
if (WARNED_RESOLUTION_FAILURES.add(structureId)) {
|
||||||
|
IrisLogging.reportError("Native structure volume resolution failed for '" + structureId
|
||||||
|
+ "'; objects will not be vetoed against it.", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long chunkKey(int chunkX, int chunkZ) {
|
||||||
|
return ((long) chunkX << 32) ^ (chunkZ & 0xffffffffL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int originWindowStripe(int cellX, int cellZ) {
|
||||||
|
long mixed = chunkKey(cellX, cellZ);
|
||||||
|
mixed ^= mixed >>> 33;
|
||||||
|
mixed *= 0xff51afd7ed558ccdL;
|
||||||
|
mixed ^= mixed >>> 33;
|
||||||
|
mixed *= 0xc4ceb9fe1a85ec53L;
|
||||||
|
mixed ^= mixed >>> 33;
|
||||||
|
return (int) mixed & (ORIGIN_WINDOW_STRIPE_COUNT - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ReentrantLock[] createOriginWindowStripes() {
|
||||||
|
ReentrantLock[] stripes = new ReentrantLock[ORIGIN_WINDOW_STRIPE_COUNT];
|
||||||
|
for (int i = 0; i < stripes.length; i++) {
|
||||||
|
stripes[i] = new ReentrantLock();
|
||||||
|
}
|
||||||
|
return stripes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void lockOriginWindow(long stripeMask) {
|
||||||
|
long pending = stripeMask;
|
||||||
|
while (pending != 0L) {
|
||||||
|
int stripe = Long.numberOfTrailingZeros(pending);
|
||||||
|
lockOriginStripe(originWindowStripes[stripe]);
|
||||||
|
pending &= pending - 1L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void lockOriginStripe(ReentrantLock lock) {
|
||||||
|
if (lock.tryLock()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ForkJoinTask.inForkJoinPool()) {
|
||||||
|
lock.lock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
OriginLockBlocker blocker = new OriginLockBlocker(lock);
|
||||||
|
try {
|
||||||
|
ForkJoinPool.managedBlock(blocker);
|
||||||
|
} catch (InterruptedException interruption) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
blocker.block();
|
||||||
|
} catch (RejectedExecutionException exhaustedPool) {
|
||||||
|
blocker.block();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void unlockOriginWindow(long stripeMask) {
|
||||||
|
long pending = stripeMask;
|
||||||
|
while (pending != 0L) {
|
||||||
|
int stripe = Long.SIZE - 1 - Long.numberOfLeadingZeros(pending);
|
||||||
|
originWindowStripes[stripe].unlock();
|
||||||
|
pending &= ~(1L << stripe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class OriginLockBlocker implements ForkJoinPool.ManagedBlocker {
|
||||||
|
private final ReentrantLock lock;
|
||||||
|
private boolean acquired;
|
||||||
|
|
||||||
|
private OriginLockBlocker(ReentrantLock lock) {
|
||||||
|
this.lock = lock;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean block() {
|
||||||
|
if (!acquired) {
|
||||||
|
lock.lock();
|
||||||
|
acquired = true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReleasable() {
|
||||||
|
if (!acquired) {
|
||||||
|
acquired = lock.tryLock();
|
||||||
|
}
|
||||||
|
return acquired;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GenerationHistoryRuntimeRouter.CoordinateScope openHistoryCoordinateScope(
|
||||||
|
Engine engine,
|
||||||
|
int chunkX,
|
||||||
|
int chunkZ
|
||||||
|
) {
|
||||||
|
if (!(engine instanceof IrisEngine irisEngine)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return irisEngine.openGenerationHistoryCoordinateScope(chunkX << 4, chunkZ << 4);
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw new IllegalStateException("Unable to route native structure origin "
|
||||||
|
+ chunkX + "," + chunkZ + " through generation history.", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int runtimeId(Engine engine) {
|
||||||
|
return engine == null ? 0 : engine.getCacheID();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void attachRetirementListener(Engine engine) {
|
||||||
|
if (engine instanceof IrisEngine irisEngine) {
|
||||||
|
irisEngine.addGenerationRuntimeRetirementListener(retirementListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void detachRetirementListener(Engine engine) {
|
||||||
|
if (engine instanceof IrisEngine irisEngine) {
|
||||||
|
irisEngine.removeGenerationRuntimeRetirementListener(retirementListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<RuntimeChunkKey, KList<NativeStructureVolume>> lru(int capacity) {
|
||||||
|
return new LinkedHashMap<>(16, 0.75F, true) {
|
||||||
|
@Override
|
||||||
|
protected boolean removeEldestEntry(
|
||||||
|
Map.Entry<RuntimeChunkKey, KList<NativeStructureVolume>> eldest
|
||||||
|
) {
|
||||||
|
return size() > capacity;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private record RuntimeChunkKey(int runtimeId, long chunkKey) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The generator, biome source and structure state arrive as suppliers so an installed index never holds the
|
||||||
|
* level (and through it the engine) strongly. That keeps the weakly keyed registry collectable on world unload.
|
||||||
|
*/
|
||||||
|
public record Context(
|
||||||
|
RegistryAccess registryAccess,
|
||||||
|
StructureTemplateManager templateManager,
|
||||||
|
ResourceKey<Level> levelKey,
|
||||||
|
LevelHeightAccessor heightAccessor,
|
||||||
|
Supplier<ChunkGenerator> generator,
|
||||||
|
Supplier<BiomeSource> biomeSource,
|
||||||
|
Supplier<ChunkGeneratorStructureState> structureState
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
|
||||||
|
import java.util.OptionalInt;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
public final class NativeTerrainHeightCache {
|
||||||
|
private static final int MAXIMUM_CACHED_QUERIES = 65_536;
|
||||||
|
|
||||||
|
private final Object2IntLinkedOpenHashMap<Query> heights = new Object2IntLinkedOpenHashMap<>();
|
||||||
|
|
||||||
|
public OptionalInt resolvedHeight(Query query, Supplier<OptionalInt> resolver) {
|
||||||
|
synchronized (heights) {
|
||||||
|
if (heights.containsKey(query)) {
|
||||||
|
return OptionalInt.of(heights.getAndMoveToFirst(query));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OptionalInt resolved = resolver.get();
|
||||||
|
if (resolved.isEmpty()) {
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
int height = resolved.getAsInt();
|
||||||
|
synchronized (heights) {
|
||||||
|
heights.putAndMoveToFirst(query, height);
|
||||||
|
while (heights.size() > MAXIMUM_CACHED_QUERIES) {
|
||||||
|
heights.removeLastInt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void evictRuntime(int runtimeId) {
|
||||||
|
synchronized (heights) {
|
||||||
|
heights.keySet().removeIf(query -> query.runtimeId() == runtimeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Query(int runtimeId, int x, int z, Heightmap.Types type, int minimumY, int height) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.world.history.TerrainBoundarySignature;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
|
import com.mojang.brigadier.StringReader;
|
||||||
|
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||||
|
import net.minecraft.commands.arguments.blocks.BlockStateParser;
|
||||||
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
|
import net.minecraft.world.level.NoiseColumn;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class NativeTransitionColumn {
|
||||||
|
private NativeTransitionColumn() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NoiseColumn column(TerrainBoundarySignature signature, LevelHeightAccessor height) {
|
||||||
|
BlockState[] states = new BlockState[height.getHeight()];
|
||||||
|
Map<String, BlockState> palette = new HashMap<>();
|
||||||
|
for (int offset = 0; offset < states.length; offset++) {
|
||||||
|
String state = signature.geometry().voxelAt(height.getMinY() + offset).stateKey();
|
||||||
|
states[offset] = palette.computeIfAbsent(state, NativeTransitionColumn::parse);
|
||||||
|
}
|
||||||
|
return new NoiseColumn(height.getMinY(), states);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int height(TerrainBoundarySignature signature, Heightmap.Types type, LevelHeightAccessor height) {
|
||||||
|
Map<String, BlockState> palette = new HashMap<>();
|
||||||
|
for (int offset = height.getHeight() - 1; offset >= 0; offset--) {
|
||||||
|
String state = signature.geometry().voxelAt(height.getMinY() + offset).stateKey();
|
||||||
|
if (type.isOpaque().test(palette.computeIfAbsent(state, NativeTransitionColumn::parse))) {
|
||||||
|
return height.getMinY() + offset + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return height.getMinY();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BlockState parse(String key) {
|
||||||
|
String nativeKey = key;
|
||||||
|
if (!key.startsWith("minecraft:") && IrisPlatforms.isBound()) {
|
||||||
|
PlatformBlockState logicalState = IrisPlatforms.get().registries().blockOrNull(key);
|
||||||
|
PlatformBlockState baseState = logicalState == null ? null : logicalState.placementBaseState();
|
||||||
|
if (baseState != null) {
|
||||||
|
nativeKey = baseState.key();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StringReader reader = new StringReader(nativeKey);
|
||||||
|
try {
|
||||||
|
BlockState state = BlockStateParser.parseForBlock(BuiltInRegistries.BLOCK, reader, false).blockState();
|
||||||
|
if (reader.canRead()) {
|
||||||
|
throw new IllegalArgumentException("Unexpected terrain block state suffix: " + reader.getRemaining());
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
} catch (CommandSyntaxException failure) {
|
||||||
|
throw new IllegalArgumentException("Saved terrain block state cannot be resolved: " + key, failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import net.minecraft.core.SectionPos;
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
|
import net.minecraft.util.SimpleBitStorage;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.IntBinaryOperator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes WORLD_SURFACE_WG and OCEAN_FLOOR_WG from Iris engine heights.
|
||||||
|
*
|
||||||
|
* Iris writes its terrain straight into LevelChunkSection, so neither vanilla's noise fill nor
|
||||||
|
* ProtoChunk.setBlockState ever updates the two worldgen heightmaps. Without these writes
|
||||||
|
* ChunkAccess.getHeight lazily primes the map from finished blocks - Iris trees, leaves and snow
|
||||||
|
* included - and vanilla pieces that self-snap against WORLD_SURFACE_WG (igloo) or OCEAN_FLOOR_WG
|
||||||
|
* (ocean ruins, buried treasure, mineshaft interiors) anchor to the canopy instead of the terrain.
|
||||||
|
*
|
||||||
|
* Both resolvers take world block coordinates and return the absolute first free Y, matching
|
||||||
|
* Heightmap's internal "first available" semantics: WORLD_SURFACE_WG counts fluid, OCEAN_FLOOR_WG
|
||||||
|
* does not.
|
||||||
|
*/
|
||||||
|
public final class WorldgenTerrainHeightmaps {
|
||||||
|
private static final int COLUMNS = 256;
|
||||||
|
private static final int PLACEMENT_CHUNK_MARGIN = 1;
|
||||||
|
|
||||||
|
private WorldgenTerrainHeightmaps() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void primeTerrain(ChunkAccess chunk, IntBinaryOperator surfaceFirstFreeY,
|
||||||
|
IntBinaryOperator floorFirstFreeY) {
|
||||||
|
Objects.requireNonNull(chunk, "Iris worldgen heightmap priming requires a chunk");
|
||||||
|
Objects.requireNonNull(surfaceFirstFreeY,
|
||||||
|
"Iris worldgen heightmap priming requires a surface height resolver");
|
||||||
|
Objects.requireNonNull(floorFirstFreeY,
|
||||||
|
"Iris worldgen heightmap priming requires an ocean floor height resolver");
|
||||||
|
write(chunk, Heightmap.Types.WORLD_SURFACE_WG, surfaceFirstFreeY);
|
||||||
|
write(chunk, Heightmap.Types.OCEAN_FLOOR_WG, floorFirstFreeY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void primeStructurePlacement(WorldGenLevel world, ChunkPos generationCenter,
|
||||||
|
List<StructureStart> starts,
|
||||||
|
IntBinaryOperator surfaceFirstFreeY,
|
||||||
|
IntBinaryOperator floorFirstFreeY) {
|
||||||
|
Objects.requireNonNull(world,
|
||||||
|
"Iris worldgen heightmap priming requires a generation level");
|
||||||
|
Objects.requireNonNull(generationCenter,
|
||||||
|
"Iris worldgen heightmap priming requires a generation center");
|
||||||
|
if (starts == null || starts.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Set<Long> primed = new HashSet<>();
|
||||||
|
for (StructureStart start : starts) {
|
||||||
|
if (start == null || !start.isValid()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BoundingBox bounds = start.getBoundingBox();
|
||||||
|
int minChunkX = SectionPos.blockToSectionCoord(bounds.minX()) - PLACEMENT_CHUNK_MARGIN;
|
||||||
|
int maxChunkX = SectionPos.blockToSectionCoord(bounds.maxX()) + PLACEMENT_CHUNK_MARGIN;
|
||||||
|
int minChunkZ = SectionPos.blockToSectionCoord(bounds.minZ()) - PLACEMENT_CHUNK_MARGIN;
|
||||||
|
int maxChunkZ = SectionPos.blockToSectionCoord(bounds.maxZ()) + PLACEMENT_CHUNK_MARGIN;
|
||||||
|
for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) {
|
||||||
|
for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) {
|
||||||
|
if (Math.abs(chunkX - generationCenter.x()) > PLACEMENT_CHUNK_MARGIN
|
||||||
|
|| Math.abs(chunkZ - generationCenter.z()) > PLACEMENT_CHUNK_MARGIN) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!primed.add(ChunkPos.pack(chunkX, chunkZ))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!world.hasChunk(chunkX, chunkZ)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
primeTerrain(world.getChunk(chunkX, chunkZ), surfaceFirstFreeY, floorFirstFreeY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void write(ChunkAccess chunk, Heightmap.Types type, IntBinaryOperator firstFreeY) {
|
||||||
|
ChunkPos pos = chunk.getPos();
|
||||||
|
int minY = chunk.getMinY();
|
||||||
|
int height = chunk.getHeight();
|
||||||
|
int baseX = pos.getMinBlockX();
|
||||||
|
int baseZ = pos.getMinBlockZ();
|
||||||
|
SimpleBitStorage storage = new SimpleBitStorage(Mth.ceillog2(height + 1), COLUMNS);
|
||||||
|
for (int z = 0; z < 16; z++) {
|
||||||
|
for (int x = 0; x < 16; x++) {
|
||||||
|
int firstFree = firstFreeY.applyAsInt(baseX + x, baseZ + z) - minY;
|
||||||
|
storage.set(x + z * 16, Mth.clamp(firstFree, 0, height));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Heightmap heightmap = chunk.getOrCreateHeightmapUnprimed(type);
|
||||||
|
long[] packed = storage.getRaw();
|
||||||
|
int expected = heightmap.getRawData().length;
|
||||||
|
if (expected != packed.length) {
|
||||||
|
throw new IllegalStateException("Iris cannot prime " + type + " for chunk "
|
||||||
|
+ pos.x() + "," + pos.z() + ": packed " + packed.length
|
||||||
|
+ " words for a heightmap storing " + expected
|
||||||
|
+ "; Heightmap.setRawData would silently fall back to block scanning");
|
||||||
|
}
|
||||||
|
heightmap.setRawData(chunk, type, packed);
|
||||||
|
}
|
||||||
|
}
|
||||||
+1287
File diff suppressed because it is too large
Load Diff
+513
@@ -0,0 +1,513 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
||||||
|
|
||||||
|
import net.minecraft.world.level.levelgen.structure.placement.AbstractSpreadingStructurePlacement;
|
||||||
|
import art.arcane.iris.pack.datapack.DatapackStructureScopeIndex;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureFrequencyScale;
|
||||||
|
import art.arcane.iris.structure.nativegen.IrisImportedStructureControl;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.HolderOwner;
|
||||||
|
import net.minecraft.core.Vec3i;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
final class DatapackStructureStateFilter {
|
||||||
|
private DatapackStructureStateFilter() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static Selection filter(
|
||||||
|
List<Holder<StructureSet>> structureSets,
|
||||||
|
DatapackStructureScopeIndex scopeIndex,
|
||||||
|
Set<String> declaredSources
|
||||||
|
) {
|
||||||
|
return filter(structureSets, scopeIndex, declaredSources, new IrisImportedStructureControl());
|
||||||
|
}
|
||||||
|
|
||||||
|
static Selection filter(
|
||||||
|
List<Holder<StructureSet>> structureSets,
|
||||||
|
DatapackStructureScopeIndex scopeIndex,
|
||||||
|
Set<String> declaredSources,
|
||||||
|
IrisImportedStructureControl importedStructures
|
||||||
|
) {
|
||||||
|
return filter(
|
||||||
|
structureSets,
|
||||||
|
scopeIndex,
|
||||||
|
declaredSources,
|
||||||
|
importedStructures,
|
||||||
|
keyIndex(structureSets));
|
||||||
|
}
|
||||||
|
|
||||||
|
static Selection filter(
|
||||||
|
List<Holder<StructureSet>> structureSets,
|
||||||
|
DatapackStructureScopeIndex scopeIndex,
|
||||||
|
Set<String> declaredSources,
|
||||||
|
IrisImportedStructureControl importedStructures,
|
||||||
|
StructureSetKeyIndex keyIndex
|
||||||
|
) {
|
||||||
|
if (scopeIndex.isEmpty()) {
|
||||||
|
return new Selection(
|
||||||
|
scaleFrequencyOverrides(structureSets, importedStructures, keyIndex),
|
||||||
|
0,
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
Map<String, Holder<StructureSet>> holdersByKey = new HashMap<>();
|
||||||
|
for (Holder<StructureSet> holder : structureSets) {
|
||||||
|
String key = structureSetKey(holder, keyIndex);
|
||||||
|
if (key != null) {
|
||||||
|
holdersByKey.putIfAbsent(key, holder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<Holder<StructureSet>, Holder<StructureSet>> scopedByIdentity = new IdentityHashMap<>();
|
||||||
|
Set<Holder<StructureSet>> visiting = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||||
|
List<Holder<StructureSet>> filteredSets = new ArrayList<>(structureSets.size());
|
||||||
|
int retainedManagedSets = 0;
|
||||||
|
int excludedManagedSets = 0;
|
||||||
|
for (Holder<StructureSet> holder : structureSets) {
|
||||||
|
String setKey = structureSetKey(holder, keyIndex);
|
||||||
|
boolean managedSet = setKey != null && scopeIndex.isManagedStructureSet(setKey);
|
||||||
|
Holder<StructureSet> scopedHolder = scopeHolder(
|
||||||
|
holder,
|
||||||
|
scopeIndex,
|
||||||
|
declaredSources,
|
||||||
|
holdersByKey,
|
||||||
|
scopedByIdentity,
|
||||||
|
visiting,
|
||||||
|
keyIndex);
|
||||||
|
if (scopedHolder == null) {
|
||||||
|
if (managedSet) {
|
||||||
|
excludedManagedSets++;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (managedSet) {
|
||||||
|
retainedManagedSets++;
|
||||||
|
}
|
||||||
|
filteredSets.add(scopedHolder);
|
||||||
|
}
|
||||||
|
return new Selection(
|
||||||
|
scaleFrequencyOverrides(
|
||||||
|
List.copyOf(filteredSets), importedStructures, keyIndex),
|
||||||
|
retainedManagedSets,
|
||||||
|
excludedManagedSets);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String structureSetKey(Holder<StructureSet> holder) {
|
||||||
|
return structureSetKey(holder, keyIndex(List.of(holder)));
|
||||||
|
}
|
||||||
|
|
||||||
|
static StructureSetKeyIndex keyIndex(
|
||||||
|
Iterable<? extends Holder<StructureSet>> registeredStructureSets
|
||||||
|
) {
|
||||||
|
return StructureSetKeyIndex.create(registeredStructureSets);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String structureSetKey(
|
||||||
|
Holder<StructureSet> holder,
|
||||||
|
StructureSetKeyIndex keyIndex
|
||||||
|
) {
|
||||||
|
return keyIndex.resolve(holder)
|
||||||
|
.map(key -> key.identifier().toString())
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<StructureSet.StructureSelectionEntry> filterEntries(
|
||||||
|
List<StructureSet.StructureSelectionEntry> entries,
|
||||||
|
DatapackStructureScopeIndex scopeIndex,
|
||||||
|
Set<String> declaredSources
|
||||||
|
) {
|
||||||
|
List<StructureSet.StructureSelectionEntry> filtered = new ArrayList<>(entries.size());
|
||||||
|
for (StructureSet.StructureSelectionEntry entry : entries) {
|
||||||
|
String structureKey = entry.structure().unwrapKey()
|
||||||
|
.map(key -> key.identifier().toString())
|
||||||
|
.orElse(null);
|
||||||
|
if (structureKey == null || scopeIndex.allowsStructure(structureKey, declaredSources)) {
|
||||||
|
filtered.add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Holder<StructureSet> scopeHolder(
|
||||||
|
Holder<StructureSet> holder,
|
||||||
|
DatapackStructureScopeIndex scopeIndex,
|
||||||
|
Set<String> declaredSources,
|
||||||
|
Map<String, Holder<StructureSet>> holdersByKey,
|
||||||
|
Map<Holder<StructureSet>, Holder<StructureSet>> scopedByIdentity,
|
||||||
|
Set<Holder<StructureSet>> visiting,
|
||||||
|
StructureSetKeyIndex keyIndex
|
||||||
|
) {
|
||||||
|
if (scopedByIdentity.containsKey(holder)) {
|
||||||
|
return scopedByIdentity.get(holder);
|
||||||
|
}
|
||||||
|
if (!visiting.add(holder)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String setKey = structureSetKey(holder, keyIndex);
|
||||||
|
if (setKey != null && scopeIndex.isManagedStructureSet(setKey)
|
||||||
|
&& !scopeIndex.allowsStructureSet(setKey, declaredSources)) {
|
||||||
|
scopedByIdentity.put(holder, null);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
StructureSet originalSet = holder.value();
|
||||||
|
List<StructureSet.StructureSelectionEntry> entries = filterEntries(
|
||||||
|
originalSet.structures(), scopeIndex, declaredSources);
|
||||||
|
if (entries.isEmpty()) {
|
||||||
|
scopedByIdentity.put(holder, null);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
StructurePlacement placement = scopePlacement(
|
||||||
|
originalSet.placement(),
|
||||||
|
scopeIndex,
|
||||||
|
declaredSources,
|
||||||
|
holdersByKey,
|
||||||
|
scopedByIdentity,
|
||||||
|
visiting,
|
||||||
|
keyIndex);
|
||||||
|
if (entries.size() == originalSet.structures().size()
|
||||||
|
&& placement == originalSet.placement()) {
|
||||||
|
scopedByIdentity.put(holder, holder);
|
||||||
|
return holder;
|
||||||
|
}
|
||||||
|
Holder<StructureSet> scoped = replacementHolder(
|
||||||
|
holder, new StructureSet(entries, placement), keyIndex);
|
||||||
|
scopedByIdentity.put(holder, scoped);
|
||||||
|
return scoped;
|
||||||
|
} finally {
|
||||||
|
visiting.remove(holder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Optional<AbstractSpreadingStructurePlacement.ExclusionZone> exclusionZone(StructurePlacement placement) {
|
||||||
|
if (!(placement instanceof AbstractSpreadingStructurePlacement)) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
Object value = declaredFieldValue(AbstractSpreadingStructurePlacement.class, placement, Optional.class);
|
||||||
|
if (value instanceof Optional<?> optional && (optional.isEmpty()
|
||||||
|
|| optional.get() instanceof AbstractSpreadingStructurePlacement.ExclusionZone)) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Optional<AbstractSpreadingStructurePlacement.ExclusionZone> resolved =
|
||||||
|
(Optional<AbstractSpreadingStructurePlacement.ExclusionZone>) optional;
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Could not read structure placement exclusion zone from "
|
||||||
|
+ placement.getClass().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructurePlacement scopePlacement(
|
||||||
|
StructurePlacement placement,
|
||||||
|
DatapackStructureScopeIndex scopeIndex,
|
||||||
|
Set<String> declaredSources,
|
||||||
|
Map<String, Holder<StructureSet>> holdersByKey,
|
||||||
|
Map<Holder<StructureSet>, Holder<StructureSet>> scopedByIdentity,
|
||||||
|
Set<Holder<StructureSet>> visiting,
|
||||||
|
StructureSetKeyIndex keyIndex
|
||||||
|
) {
|
||||||
|
Optional<AbstractSpreadingStructurePlacement.ExclusionZone> currentZone = exclusionZone(placement);
|
||||||
|
if (currentZone.isEmpty()) {
|
||||||
|
return placement;
|
||||||
|
}
|
||||||
|
|
||||||
|
Holder<StructureSet> target = currentZone.get().otherSet();
|
||||||
|
String targetKey = structureSetKey(target, keyIndex);
|
||||||
|
if (targetKey != null) {
|
||||||
|
target = holdersByKey.getOrDefault(targetKey, target);
|
||||||
|
}
|
||||||
|
Holder<StructureSet> scopedTarget = scopeHolder(
|
||||||
|
target,
|
||||||
|
scopeIndex,
|
||||||
|
declaredSources,
|
||||||
|
holdersByKey,
|
||||||
|
scopedByIdentity,
|
||||||
|
visiting,
|
||||||
|
keyIndex);
|
||||||
|
Optional<AbstractSpreadingStructurePlacement.ExclusionZone> scopedZone = Optional.empty();
|
||||||
|
if (scopedTarget != null) {
|
||||||
|
scopedZone = Optional.of(new AbstractSpreadingStructurePlacement.ExclusionZone(
|
||||||
|
scopedTarget,
|
||||||
|
currentZone.get().chunkCount()));
|
||||||
|
}
|
||||||
|
if (scopedTarget == currentZone.get().otherSet()) {
|
||||||
|
return placement;
|
||||||
|
}
|
||||||
|
return copyPlacement(placement, scopedZone, 1D);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Holder<StructureSet>> scaleFrequencyOverrides(
|
||||||
|
List<Holder<StructureSet>> structureSets,
|
||||||
|
IrisImportedStructureControl importedStructures,
|
||||||
|
StructureSetKeyIndex keyIndex
|
||||||
|
) {
|
||||||
|
if (!importedStructures.hasFrequencyOverrides()) {
|
||||||
|
return structureSets;
|
||||||
|
}
|
||||||
|
Map<String, Holder<StructureSet>> holdersByKey = new HashMap<>();
|
||||||
|
for (Holder<StructureSet> holder : structureSets) {
|
||||||
|
String key = structureSetKey(holder, keyIndex);
|
||||||
|
if (key != null) {
|
||||||
|
holdersByKey.putIfAbsent(key, holder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<Holder<StructureSet>, Holder<StructureSet>> scaledByIdentity = new IdentityHashMap<>();
|
||||||
|
List<Holder<StructureSet>> scaledSets = new ArrayList<>(structureSets.size());
|
||||||
|
boolean changed = false;
|
||||||
|
for (Holder<StructureSet> holder : structureSets) {
|
||||||
|
Holder<StructureSet> scaled = scaleFrequencyHolder(
|
||||||
|
holder,
|
||||||
|
importedStructures,
|
||||||
|
holdersByKey,
|
||||||
|
scaledByIdentity,
|
||||||
|
keyIndex);
|
||||||
|
scaledSets.add(scaled);
|
||||||
|
changed |= scaled != holder;
|
||||||
|
}
|
||||||
|
return changed ? List.copyOf(scaledSets) : structureSets;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Holder<StructureSet> scaleFrequencyHolder(
|
||||||
|
Holder<StructureSet> holder,
|
||||||
|
IrisImportedStructureControl importedStructures,
|
||||||
|
Map<String, Holder<StructureSet>> holdersByKey,
|
||||||
|
Map<Holder<StructureSet>, Holder<StructureSet>> scaledByIdentity,
|
||||||
|
StructureSetKeyIndex keyIndex
|
||||||
|
) {
|
||||||
|
if (scaledByIdentity.containsKey(holder)) {
|
||||||
|
return scaledByIdentity.get(holder);
|
||||||
|
}
|
||||||
|
if (!dependsOnFrequencyOverride(
|
||||||
|
holder, importedStructures, holdersByKey, keyIndex)) {
|
||||||
|
scaledByIdentity.put(holder, holder);
|
||||||
|
return holder;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResourceKey<StructureSet> holderKey = keyIndex.resolve(holder).orElseThrow(() ->
|
||||||
|
new IllegalStateException(
|
||||||
|
"An affected native structure-set exclusion graph has an unkeyed holder"));
|
||||||
|
ReboundStructureSetHolder scaledHolder = new ReboundStructureSetHolder(holderKey);
|
||||||
|
scaledByIdentity.put(holder, scaledHolder);
|
||||||
|
|
||||||
|
StructureSet originalSet = holder.value();
|
||||||
|
StructurePlacement originalPlacement = originalSet.placement();
|
||||||
|
Optional<AbstractSpreadingStructurePlacement.ExclusionZone> originalZone = exclusionZone(originalPlacement);
|
||||||
|
Optional<AbstractSpreadingStructurePlacement.ExclusionZone> scaledZone = originalZone;
|
||||||
|
if (originalZone.isPresent()) {
|
||||||
|
Holder<StructureSet> target = canonicalHolder(
|
||||||
|
originalZone.get().otherSet(), holdersByKey, keyIndex);
|
||||||
|
Holder<StructureSet> scaledTarget = scaleFrequencyHolder(
|
||||||
|
target,
|
||||||
|
importedStructures,
|
||||||
|
holdersByKey,
|
||||||
|
scaledByIdentity,
|
||||||
|
keyIndex);
|
||||||
|
if (scaledTarget != originalZone.get().otherSet()) {
|
||||||
|
scaledZone = Optional.of(new AbstractSpreadingStructurePlacement.ExclusionZone(
|
||||||
|
scaledTarget, originalZone.get().chunkCount()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double multiplier = importedStructures.frequencyMultiplier(
|
||||||
|
structureSetKey(holder, keyIndex));
|
||||||
|
StructurePlacement scaledPlacement = multiplier == 1D && scaledZone.equals(originalZone)
|
||||||
|
? originalPlacement
|
||||||
|
: copyPlacement(originalPlacement, scaledZone, multiplier);
|
||||||
|
scaledHolder.bind(new StructureSet(originalSet.structures(), scaledPlacement));
|
||||||
|
return scaledHolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean dependsOnFrequencyOverride(
|
||||||
|
Holder<StructureSet> holder,
|
||||||
|
IrisImportedStructureControl importedStructures,
|
||||||
|
Map<String, Holder<StructureSet>> holdersByKey,
|
||||||
|
StructureSetKeyIndex keyIndex
|
||||||
|
) {
|
||||||
|
Set<Holder<StructureSet>> visited = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||||
|
Holder<StructureSet> current = holder;
|
||||||
|
while (visited.add(current)) {
|
||||||
|
if (importedStructures.frequencyMultiplier(
|
||||||
|
structureSetKey(current, keyIndex)) != 1D) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Optional<AbstractSpreadingStructurePlacement.ExclusionZone> zone =
|
||||||
|
exclusionZone(current.value().placement());
|
||||||
|
if (zone.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
current = canonicalHolder(zone.get().otherSet(), holdersByKey, keyIndex);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Holder<StructureSet> canonicalHolder(
|
||||||
|
Holder<StructureSet> holder,
|
||||||
|
Map<String, Holder<StructureSet>> holdersByKey,
|
||||||
|
StructureSetKeyIndex keyIndex
|
||||||
|
) {
|
||||||
|
String key = structureSetKey(holder, keyIndex);
|
||||||
|
return key == null ? holder : holdersByKey.getOrDefault(key, holder);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Holder<StructureSet> replacementHolder(
|
||||||
|
Holder<StructureSet> original,
|
||||||
|
StructureSet replacement,
|
||||||
|
StructureSetKeyIndex keyIndex
|
||||||
|
) {
|
||||||
|
Optional<ResourceKey<StructureSet>> key = keyIndex.resolve(original);
|
||||||
|
if (key.isEmpty()) {
|
||||||
|
return Holder.direct(replacement);
|
||||||
|
}
|
||||||
|
ReboundStructureSetHolder holder = new ReboundStructureSetHolder(key.get());
|
||||||
|
holder.bind(replacement);
|
||||||
|
return holder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructurePlacement copyPlacement(
|
||||||
|
StructurePlacement placement,
|
||||||
|
Optional<AbstractSpreadingStructurePlacement.ExclusionZone> exclusionZone,
|
||||||
|
double frequencyMultiplier
|
||||||
|
) {
|
||||||
|
Vec3i locateOffset = (Vec3i) declaredFieldValue(
|
||||||
|
AbstractSpreadingStructurePlacement.class, placement, Vec3i.class);
|
||||||
|
AbstractSpreadingStructurePlacement.FrequencyReductionMethod frequencyReductionMethod =
|
||||||
|
(AbstractSpreadingStructurePlacement.FrequencyReductionMethod) declaredFieldValue(
|
||||||
|
AbstractSpreadingStructurePlacement.class,
|
||||||
|
placement,
|
||||||
|
AbstractSpreadingStructurePlacement.FrequencyReductionMethod.class);
|
||||||
|
float frequency = (float) declaredFieldValue(
|
||||||
|
AbstractSpreadingStructurePlacement.class, placement, float.class);
|
||||||
|
int salt = (int) declaredFieldValue(
|
||||||
|
AbstractSpreadingStructurePlacement.class, placement, int.class);
|
||||||
|
|
||||||
|
if (placement instanceof RandomSpreadStructurePlacement randomSpread) {
|
||||||
|
NativeStructureFrequencyScale scale = NativeStructureFrequencyScale.randomSpread(
|
||||||
|
frequency, randomSpread.spacing(), randomSpread.separation(), frequencyMultiplier);
|
||||||
|
return new RandomSpreadStructurePlacement(
|
||||||
|
locateOffset,
|
||||||
|
frequencyReductionMethod,
|
||||||
|
scale.frequency(),
|
||||||
|
salt,
|
||||||
|
exclusionZone,
|
||||||
|
scale.spacing(),
|
||||||
|
randomSpread.separation(),
|
||||||
|
randomSpread.spreadType());
|
||||||
|
}
|
||||||
|
if (placement.getClass() == ConcentricRingsStructurePlacement.class) {
|
||||||
|
ConcentricRingsStructurePlacement rings =
|
||||||
|
(ConcentricRingsStructurePlacement) placement;
|
||||||
|
float scaledFrequency = NativeStructureFrequencyScale.probability(
|
||||||
|
frequency, frequencyMultiplier);
|
||||||
|
return new ConcentricRingsStructurePlacement(
|
||||||
|
locateOffset,
|
||||||
|
frequencyReductionMethod,
|
||||||
|
scaledFrequency,
|
||||||
|
salt,
|
||||||
|
exclusionZone,
|
||||||
|
rings.distance(),
|
||||||
|
rings.spread(),
|
||||||
|
rings.count(),
|
||||||
|
rings.preferredBiomes());
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Unsupported native structure placement in an affected "
|
||||||
|
+ "frequency-override or datapack-scope graph: "
|
||||||
|
+ placement.getClass().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object declaredFieldValue(
|
||||||
|
Class<?> declaringType,
|
||||||
|
Object target,
|
||||||
|
Class<?> fieldType
|
||||||
|
) {
|
||||||
|
Field match = null;
|
||||||
|
for (Field field : declaringType.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(field.getModifiers()) || field.getType() != fieldType) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (match != null) {
|
||||||
|
throw new IllegalStateException("Ambiguous " + fieldType.getName() + " field on "
|
||||||
|
+ declaringType.getName());
|
||||||
|
}
|
||||||
|
match = field;
|
||||||
|
}
|
||||||
|
if (match == null) {
|
||||||
|
throw new IllegalStateException("Missing " + fieldType.getName() + " field on "
|
||||||
|
+ declaringType.getName());
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
match.setAccessible(true);
|
||||||
|
return match.get(target);
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new IllegalStateException("Could not read " + fieldType.getName() + " field on "
|
||||||
|
+ declaringType.getName(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class StructureSetKeyIndex {
|
||||||
|
private final Map<List<StructureSet.StructureSelectionEntry>, ResourceKey<StructureSet>>
|
||||||
|
keysByEntries;
|
||||||
|
|
||||||
|
private StructureSetKeyIndex(
|
||||||
|
Map<List<StructureSet.StructureSelectionEntry>, ResourceKey<StructureSet>>
|
||||||
|
keysByEntries
|
||||||
|
) {
|
||||||
|
this.keysByEntries = keysByEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureSetKeyIndex create(
|
||||||
|
Iterable<? extends Holder<StructureSet>> registeredStructureSets
|
||||||
|
) {
|
||||||
|
Map<List<StructureSet.StructureSelectionEntry>, ResourceKey<StructureSet>> keys =
|
||||||
|
new IdentityHashMap<>();
|
||||||
|
for (Holder<StructureSet> holder : registeredStructureSets) {
|
||||||
|
Optional<ResourceKey<StructureSet>> key = holder.unwrapKey();
|
||||||
|
if (key.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<StructureSet.StructureSelectionEntry> entries = holder.value().structures();
|
||||||
|
ResourceKey<StructureSet> previous = keys.put(entries, key.get());
|
||||||
|
if (previous != null && !previous.equals(key.get())) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Multiple registered structure sets share the same entry list");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new StructureSetKeyIndex(keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<ResourceKey<StructureSet>> resolve(Holder<StructureSet> holder) {
|
||||||
|
Optional<ResourceKey<StructureSet>> key = holder.unwrapKey();
|
||||||
|
if (key.isPresent()) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
return Optional.ofNullable(keysByEntries.get(holder.value().structures()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record Selection(
|
||||||
|
List<Holder<StructureSet>> structureSets,
|
||||||
|
int retainedManagedSets,
|
||||||
|
int excludedManagedSets
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ReboundStructureSetHolder extends Holder.Reference<StructureSet> {
|
||||||
|
private ReboundStructureSetHolder(ResourceKey<StructureSet> key) {
|
||||||
|
super(Type.STAND_ALONE, new HolderOwner<>() {
|
||||||
|
}, key, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bind(StructureSet structureSet) {
|
||||||
|
bindValue(structureSet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+441
@@ -0,0 +1,441 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
||||||
|
|
||||||
|
import net.minecraft.world.level.levelgen.placement.FeaturePlacer;
|
||||||
|
import art.arcane.iris.generation.runtime.DimensionStackContext;
|
||||||
|
import art.arcane.iris.generation.runtime.DimensionStackLayout;
|
||||||
|
import art.arcane.iris.nativegen.v26_3_R1.NativeGenerationWriteGuard;
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeFeatureGenerationPolicy;
|
||||||
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
|
import art.arcane.iris.generation.biome.IrisBiomeCustom;
|
||||||
|
import art.arcane.iris.generation.decoration.IrisDecorationStep;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
|
import art.arcane.iris.structure.nativegen.IrisImportedFeatureControl;
|
||||||
|
import art.arcane.iris.structure.object.IrisStaticObjectLayer;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import it.unimi.dsi.fastutil.ints.IntArraySet;
|
||||||
|
import it.unimi.dsi.fastutil.ints.IntSet;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.core.Registry;
|
||||||
|
import net.minecraft.core.SectionPos;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.biome.BiomeGenerationSettings;
|
||||||
|
import net.minecraft.world.level.biome.FeatureSorter;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
|
import net.minecraft.world.level.levelgen.RandomSupport;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import net.minecraft.world.level.levelgen.WorldgenRandom;
|
||||||
|
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
|
||||||
|
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.function.IntBinaryOperator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bukkit twin of the modded imported-feature stage. Same control, same semantics, same seeds: with
|
||||||
|
* {@code importedFeatures.enabled} the vanilla placed-feature pass runs over Iris terrain, and with the
|
||||||
|
* control off nothing here allocates or runs.
|
||||||
|
*
|
||||||
|
* <p>Iris does its own native structure pass and calls the delegate with {@code addVanillaDecorations=false},
|
||||||
|
* so the vanilla feature half never runs on its own. This reproduces that half only - structures are never
|
||||||
|
* placed twice.
|
||||||
|
*
|
||||||
|
* <p>Threading: called from {@code applyBiomeDecoration} on the worldgen thread that owns the chunk. The
|
||||||
|
* FEATURES chunk step is not parallel-safe.
|
||||||
|
*/
|
||||||
|
final class ImportedFeatureStage {
|
||||||
|
private static final String CYCLE_MARKER = "Feature order cycle found";
|
||||||
|
|
||||||
|
private final Engine engine;
|
||||||
|
private final ConcurrentHashMap<Integer, RuntimeFeatureState> runtimeStates = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
ImportedFeatureStage(Engine engine) {
|
||||||
|
this.engine = engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generation settings for one biome holder, mapping Iris custom biomes onto their vanilla derivative.
|
||||||
|
* Pass-through while the control is off, which is what keeps {@code BiomeFilter} behaving exactly as it
|
||||||
|
* does today.
|
||||||
|
*/
|
||||||
|
BiomeGenerationSettings generationSettings(Holder<Biome> biome) {
|
||||||
|
RuntimeFeatureState state = currentState();
|
||||||
|
FeatureTable table = state == null ? null : state.featureTable();
|
||||||
|
if (table == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Holder<Biome> mapped = table.derivatives().get(holderKey(biome));
|
||||||
|
return mapped == null ? null : mapped.value().getGenerationSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the table on the first decorated chunk, or leaves the stage inert. A feature-order cycle is
|
||||||
|
* reported once here and degrades to features-off; it never reaches chunk generation as a crash.
|
||||||
|
*/
|
||||||
|
void prepare(WorldGenLevel level) {
|
||||||
|
IrisDimension dimension = engine.getDimension();
|
||||||
|
int runtimeId = engine.getCacheID();
|
||||||
|
if (settled(runtimeStates.get(runtimeId), dimension)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
synchronized (this) {
|
||||||
|
if (settled(runtimeStates.get(runtimeId), dimension)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
runtimeStates.put(runtimeId, build(level, dimension));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void evictRuntime(int runtimeId) {
|
||||||
|
runtimeStates.remove(runtimeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RuntimeFeatureState currentState() {
|
||||||
|
IrisDimension dimension = engine.getDimension();
|
||||||
|
RuntimeFeatureState state = runtimeStates.get(engine.getCacheID());
|
||||||
|
return settled(state, dimension) ? state : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean settled(RuntimeFeatureState state, IrisDimension dimension) {
|
||||||
|
return state != null && state.dimension() == dimension;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RuntimeFeatureState build(WorldGenLevel level, IrisDimension dimension) {
|
||||||
|
IrisImportedFeatureControl control;
|
||||||
|
try {
|
||||||
|
control = NativeFeatureGenerationPolicy.control(engine);
|
||||||
|
} catch (RuntimeException error) {
|
||||||
|
IrisLogging.error("Iris could not read importedFeatures for this dimension; features off: "
|
||||||
|
+ error);
|
||||||
|
return new RuntimeFeatureState(dimension, null);
|
||||||
|
}
|
||||||
|
if (!control.shouldGenerateFeatures()) {
|
||||||
|
return new RuntimeFeatureState(dimension, null);
|
||||||
|
}
|
||||||
|
FeatureTable built;
|
||||||
|
try {
|
||||||
|
built = buildTable(level, control, dimension);
|
||||||
|
} catch (Throwable error) {
|
||||||
|
IrisLogging.error("Iris importedFeatures is off for " + dimensionKey()
|
||||||
|
+ ": feature table construction failed: " + error);
|
||||||
|
return new RuntimeFeatureState(dimension, null);
|
||||||
|
}
|
||||||
|
if (built == null) {
|
||||||
|
return new RuntimeFeatureState(dimension, null);
|
||||||
|
}
|
||||||
|
IrisLogging.info("Iris importedFeatures on for " + dimensionKey() + ": " + built.biomes().size()
|
||||||
|
+ " biomes, " + built.steps().size() + " steps, " + built.derivatives().size()
|
||||||
|
+ " custom-biome derivative maps");
|
||||||
|
return new RuntimeFeatureState(dimension, built);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FeatureTable buildTable(WorldGenLevel level, IrisImportedFeatureControl control,
|
||||||
|
IrisDimension dimension) {
|
||||||
|
Registry<Biome> registry = level.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||||
|
Set<String> visibleKeys = visibleBiomeKeys();
|
||||||
|
// Registry-ordered walk, never a hash-ordered set: FeatureSorter's cycle detection walks this list and
|
||||||
|
// an unordered walk makes detection depend on JVM hash order.
|
||||||
|
List<Holder<Biome>> biomes = new ArrayList<>();
|
||||||
|
Map<String, Holder<Biome>> byKey = new HashMap<>();
|
||||||
|
registry.listElements().forEach((Holder.Reference<Biome> reference) -> {
|
||||||
|
String key = holderKey(reference);
|
||||||
|
if (key != null && visibleKeys.contains(key)) {
|
||||||
|
biomes.add(reference);
|
||||||
|
byKey.put(key, reference);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (biomes.isEmpty()) {
|
||||||
|
IrisLogging.error("Iris importedFeatures is on but " + dimensionKey()
|
||||||
|
+ " exposes no registered biomes; features off");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Map<String, Holder<Biome>> derivatives = customBiomeDerivatives(registry, byKey);
|
||||||
|
List<FeatureSorter.StepFeatureData> steps;
|
||||||
|
try {
|
||||||
|
steps = FeatureSorter.buildFeaturesPerStep(biomes,
|
||||||
|
(Holder<Biome> biome) -> settingsFor(biome, derivatives).features(), true);
|
||||||
|
} catch (IllegalStateException error) {
|
||||||
|
String message = error.getMessage();
|
||||||
|
if (message == null || !message.contains(CYCLE_MARKER)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
IrisLogging.error("Iris importedFeatures is off for " + dimensionKey()
|
||||||
|
+ ": the registered placed features cannot be ordered. " + message
|
||||||
|
+ ". Remove or reorder the conflicting content, or leave importedFeatures.enabled false.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
boolean filtered = control.getDisabled() != null && !control.getDisabled().isEmpty();
|
||||||
|
return new FeatureTable(dimension, control, List.copyOf(biomes), Set.copyOf(biomes),
|
||||||
|
Map.copyOf(byKey), steps, Map.copyOf(derivatives), filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every biome key Iris can write into a chunk section: the structure derivative, the raw derivative, every
|
||||||
|
* scatter entry, and every generated custom biome.
|
||||||
|
*/
|
||||||
|
private Set<String> visibleBiomeKeys() {
|
||||||
|
Set<String> keys = new LinkedHashSet<>();
|
||||||
|
for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) {
|
||||||
|
addKey(keys, irisBiome.getStructureDerivativeKey());
|
||||||
|
addKey(keys, irisBiome.getDerivativeKey());
|
||||||
|
addKey(keys, irisBiome.getVanillaDerivativeKey());
|
||||||
|
for (String scatter : irisBiome.getBiomeScatter()) {
|
||||||
|
addKey(keys, scatter);
|
||||||
|
}
|
||||||
|
for (String scatter : irisBiome.getBiomeSkyScatter()) {
|
||||||
|
addKey(keys, scatter);
|
||||||
|
}
|
||||||
|
if (!irisBiome.isCustom()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||||
|
addKey(keys, engine.getData().customBiomeResourceKey(engine.getDimension(), customBiome));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps every generated Iris custom biome key onto the registry holder of its Iris biome's vanilla
|
||||||
|
* derivative. The custom biome's own datapack JSON carries no features by design.
|
||||||
|
*/
|
||||||
|
private Map<String, Holder<Biome>> customBiomeDerivatives(Registry<Biome> registry,
|
||||||
|
Map<String, Holder<Biome>> byKey) {
|
||||||
|
Map<String, Holder<Biome>> derivatives = new HashMap<>();
|
||||||
|
for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) {
|
||||||
|
if (!irisBiome.isCustom()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String derivativeKey = normalizeKey(irisBiome.getVanillaDerivativeKey());
|
||||||
|
// Resolve from the registry, not only from the visible biome set: a sea or shore biome's structure
|
||||||
|
// derivative is rewritten away from its vanilla derivative, so the derivative whose features we
|
||||||
|
// want is not always a biome Iris can emit.
|
||||||
|
Holder<Biome> derivative = byKey.get(derivativeKey);
|
||||||
|
if (derivative == null) {
|
||||||
|
derivative = resolveHolder(registry, derivativeKey);
|
||||||
|
}
|
||||||
|
if (derivative == null) {
|
||||||
|
IrisLogging.warn("Iris importedFeatures: vanilla derivative " + derivativeKey + " of biome "
|
||||||
|
+ irisBiome.getLoadKey()
|
||||||
|
+ " is not registered; its custom biomes generate no imported features");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||||
|
derivatives.put(
|
||||||
|
engine.getData().customBiomeResourceKey(engine.getDimension(), customBiome),
|
||||||
|
derivative
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return derivatives;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Holder<Biome> resolveHolder(Registry<Biome> registry, String key) {
|
||||||
|
if (registry == null || key == null || key.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Identifier identifier = Identifier.tryParse(key);
|
||||||
|
if (identifier == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return registry.get(identifier).<Holder<Biome>>map((Holder.Reference<Biome> reference) -> reference)
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BiomeGenerationSettings settingsFor(Holder<Biome> biome,
|
||||||
|
Map<String, Holder<Biome>> derivatives) {
|
||||||
|
Holder<Biome> mapped = derivatives.get(holderKey(biome));
|
||||||
|
return mapped == null
|
||||||
|
? biome.value().getGenerationSettings()
|
||||||
|
: mapped.value().getGenerationSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the vanilla placed-feature pass for one chunk on the calling worldgen thread. A no-op while the
|
||||||
|
* control is disabled or the table degraded.
|
||||||
|
*/
|
||||||
|
void run(WorldGenLevel level, ChunkAccess chunk, ChunkGenerator owner) {
|
||||||
|
RuntimeFeatureState state = currentState();
|
||||||
|
FeatureTable table = state == null ? null : state.featureTable();
|
||||||
|
if (table == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ChunkPos centerPos = chunk.getPos();
|
||||||
|
SectionPos sectionPos = SectionPos.of(centerPos, level.getMinSectionY());
|
||||||
|
BlockPos origin = sectionPos.origin();
|
||||||
|
Registry<PlacedFeature> featureRegistry = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE);
|
||||||
|
List<FeatureSorter.StepFeatureData> steps = table.steps();
|
||||||
|
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
|
||||||
|
long decorationSeed = random.setDecorationSeed(level.getSeed(), origin.getX(), origin.getZ());
|
||||||
|
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
|
||||||
|
int staticMinY = engine.getMinHeight();
|
||||||
|
DimensionStackContext stackContext = engine.getDimensionStackContext();
|
||||||
|
Set<Holder<Biome>> chunkBiomes = chunkBiomes(level, sectionPos, table, stackContext);
|
||||||
|
IntBinaryOperator surfaceFirstFreeY = stackContext == null
|
||||||
|
? (x, z) -> level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z)
|
||||||
|
: (x, z) -> Engine.hostHeight(engine, x, z, false) + staticMinY + 1;
|
||||||
|
IntBinaryOperator floorFirstFreeY = stackContext == null
|
||||||
|
? (x, z) -> level.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z)
|
||||||
|
: (x, z) -> Engine.hostHeight(engine, x, z, true) + staticMinY + 1;
|
||||||
|
WorldGenLevel placementLevel = staticObjects.isEmpty() && stackContext == null
|
||||||
|
? level
|
||||||
|
: NativeStructureWorldgenAccess.create(
|
||||||
|
level, centerPos,
|
||||||
|
surfaceFirstFreeY,
|
||||||
|
floorFirstFreeY,
|
||||||
|
stackContext != null,
|
||||||
|
NativeGenerationWriteGuard.protectedPositions(staticObjects, stackContext, staticMinY));
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (int stepIndex = 0; stepIndex < steps.size(); stepIndex++) {
|
||||||
|
if (!table.control().shouldGenerateStep(IrisDecorationStep.byOrdinal(stepIndex))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
placeStep(placementLevel, table, steps.get(stepIndex), featureRegistry, chunkBiomes, owner,
|
||||||
|
random, decorationSeed, origin, stepIndex);
|
||||||
|
}
|
||||||
|
} catch (Throwable error) {
|
||||||
|
throw new IllegalStateException("Iris imported feature placement failed for chunk "
|
||||||
|
+ centerPos.x() + "," + centerPos.z(), error);
|
||||||
|
} finally {
|
||||||
|
level.setCurrentlyGenerating(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void placeStep(WorldGenLevel level, FeatureTable table, FeatureSorter.StepFeatureData stepData,
|
||||||
|
Registry<PlacedFeature> featureRegistry, Set<Holder<Biome>> chunkBiomes,
|
||||||
|
ChunkGenerator owner, WorldgenRandom random, long decorationSeed,
|
||||||
|
BlockPos origin, int stepIndex) {
|
||||||
|
IntSet stepFeatures = new IntArraySet();
|
||||||
|
for (Holder<Biome> biome : chunkBiomes) {
|
||||||
|
List<HolderSet<PlacedFeature>> biomeFeatures = settingsFor(biome, table.derivatives()).features();
|
||||||
|
if (stepIndex >= biomeFeatures.size()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (Holder<PlacedFeature> feature : biomeFeatures.get(stepIndex)) {
|
||||||
|
stepFeatures.add(stepData.indexMapping().applyAsInt(feature.value()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stepFeatures.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Sorted global indices: identical ordering to vanilla, and each feature's seed comes from its own
|
||||||
|
// global index, so denying one feature never shifts another.
|
||||||
|
int[] featureIndices = stepFeatures.toIntArray();
|
||||||
|
Arrays.sort(featureIndices);
|
||||||
|
for (int globalIndex : featureIndices) {
|
||||||
|
PlacedFeature feature = stepData.features().get(globalIndex);
|
||||||
|
if (table.filtered()) {
|
||||||
|
Identifier featureId = featureRegistry.getKey(feature);
|
||||||
|
if (featureId != null && !table.control().shouldGenerate(featureId.toString())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
random.setFeatureSeed(decorationSeed, globalIndex, stepIndex);
|
||||||
|
level.setCurrentlyGenerating(() -> describeFeature(featureRegistry, feature));
|
||||||
|
new FeaturePlacer(level, owner).placeWithBiomeCheck(feature, random, origin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String describeFeature(Registry<PlacedFeature> registry, PlacedFeature feature) {
|
||||||
|
Identifier id = registry.getKey(feature);
|
||||||
|
return id == null ? feature.toString() : id.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<Holder<Biome>> chunkBiomes(
|
||||||
|
WorldGenLevel level,
|
||||||
|
SectionPos sectionPos,
|
||||||
|
FeatureTable table,
|
||||||
|
DimensionStackContext stackContext
|
||||||
|
) {
|
||||||
|
List<Holder<Biome>> collected = new ArrayList<>();
|
||||||
|
ChunkPos.rangeClosed(sectionPos.chunk(), 1).forEach((ChunkPos chunkPos) -> {
|
||||||
|
ChunkAccess neighbour = level.getChunk(chunkPos.x(), chunkPos.z());
|
||||||
|
LevelChunkSection[] sections = neighbour.getSections();
|
||||||
|
for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
|
||||||
|
LevelChunkSection section = sections[sectionIndex];
|
||||||
|
if (stackContext == null) {
|
||||||
|
section.getBiomes().getAll(collected::add);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int sectionMinimumY = neighbour.getSectionYFromSectionIndex(sectionIndex) << 4;
|
||||||
|
for (int quartY = 0; quartY < 4; quartY++) {
|
||||||
|
int internalY = sectionMinimumY + (quartY << 2) - level.getMinY();
|
||||||
|
for (int quartX = 0; quartX < 4; quartX++) {
|
||||||
|
int blockX = chunkPos.getMinBlockX() + (quartX << 2);
|
||||||
|
for (int quartZ = 0; quartZ < 4; quartZ++) {
|
||||||
|
int blockZ = chunkPos.getMinBlockZ() + (quartZ << 2);
|
||||||
|
DimensionStackLayout.Layer layer = stackContext.getLayerAt(
|
||||||
|
blockX, internalY, blockZ);
|
||||||
|
if (layer.terrainContext().isSelfReferencing()) {
|
||||||
|
collected.add(section.getBiomes().get(quartX, quartY, quartZ));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Set<Holder<Biome>> present = new LinkedHashSet<>();
|
||||||
|
for (Holder<Biome> biome : collected) {
|
||||||
|
if (table.biomeSet().contains(biome)) {
|
||||||
|
present.add(biome);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String key = holderKey(biome);
|
||||||
|
Holder<Biome> canonical = key == null ? null : table.byKey().get(key);
|
||||||
|
if (canonical != null) {
|
||||||
|
present.add(canonical);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return present;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addKey(Set<String> keys, String key) {
|
||||||
|
String normalized = normalizeKey(key);
|
||||||
|
if (normalized != null) {
|
||||||
|
keys.add(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String dimensionKey() {
|
||||||
|
IrisDimension dimension = engine.getDimension();
|
||||||
|
return dimension == null ? "<unbound>" : dimension.getLoadKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String holderKey(Holder<Biome> holder) {
|
||||||
|
return holder.unwrapKey()
|
||||||
|
.map(key -> key.identifier().toString().toLowerCase(Locale.ROOT))
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeKey(String key) {
|
||||||
|
Identifier identifier = key == null ? null : Identifier.tryParse(key);
|
||||||
|
return identifier == null ? null : identifier.toString().toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record FeatureTable(IrisDimension dimension, IrisImportedFeatureControl control,
|
||||||
|
List<Holder<Biome>> biomes, Set<Holder<Biome>> biomeSet,
|
||||||
|
Map<String, Holder<Biome>> byKey,
|
||||||
|
List<FeatureSorter.StepFeatureData> steps,
|
||||||
|
Map<String, Holder<Biome>> derivatives, boolean filtered) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record RuntimeFeatureState(IrisDimension dimension, FeatureTable featureTable) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+132
@@ -0,0 +1,132 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.server.level.WorldGenRegion;
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.util.random.WeightedList;
|
||||||
|
import net.minecraft.world.attribute.EnvironmentAttributes;
|
||||||
|
import net.minecraft.world.entity.Entity;
|
||||||
|
import net.minecraft.world.entity.EntityType;
|
||||||
|
import net.minecraft.world.entity.EntitySpawnReason;
|
||||||
|
import net.minecraft.world.entity.Mob;
|
||||||
|
import net.minecraft.world.entity.MobCategory;
|
||||||
|
import net.minecraft.world.entity.SpawnGroupData;
|
||||||
|
import net.minecraft.world.entity.SpawnPlacements;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.LevelReader;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.biome.MobSpawnSettings;
|
||||||
|
import net.minecraft.world.level.gamerules.GameRules;
|
||||||
|
import org.bukkit.event.entity.CreatureSpawnEvent;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
final class InitialMobSpawner {
|
||||||
|
private InitialMobSpawner() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static void spawn(
|
||||||
|
WorldGenRegion level, Holder<Biome> biome, ChunkPos chunkPos, RandomSource random
|
||||||
|
) {
|
||||||
|
if (level.getLevel().getGameRules().get(GameRules.SPAWN_MOBS)) {
|
||||||
|
MobSpawnSettings mobSettings = biome.value().getAttributes().applyModifier(
|
||||||
|
EnvironmentAttributes.NATURAL_MOB_SPAWNS, EnvironmentAttributes.NATURAL_MOB_SPAWNS.defaultValue());
|
||||||
|
WeightedList<MobSpawnSettings.SpawnerData> mobs = mobSettings.getMobsToSpawn(MobCategory.CREATURE);
|
||||||
|
if (!mobs.isEmpty()) {
|
||||||
|
float creatureProbability = biome.value().getAttributes().applyModifier(
|
||||||
|
EnvironmentAttributes.CREATURE_WORLD_GEN_SPAWN_PROBABILITY,
|
||||||
|
EnvironmentAttributes.CREATURE_WORLD_GEN_SPAWN_PROBABILITY.defaultValue());
|
||||||
|
int xo = chunkPos.getMinBlockX();
|
||||||
|
int zo = chunkPos.getMinBlockZ();
|
||||||
|
|
||||||
|
while (random.nextFloat() < creatureProbability) {
|
||||||
|
Optional<MobSpawnSettings.SpawnerData> nextSpawnerData = mobs.getRandom(random);
|
||||||
|
if (!nextSpawnerData.isEmpty()) {
|
||||||
|
MobSpawnSettings.SpawnerData spawnerData = nextSpawnerData.get();
|
||||||
|
int count = spawnerData.count().sample(random);
|
||||||
|
SpawnGroupData groupSpawnData = null;
|
||||||
|
int x = xo + random.nextInt(16);
|
||||||
|
int z = zo + random.nextInt(16);
|
||||||
|
int startX = x;
|
||||||
|
int startZ = z;
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
boolean success = false;
|
||||||
|
|
||||||
|
for (int attempts = 0; !success && attempts < 4; attempts++) {
|
||||||
|
BlockPos pos = getTopNonCollidingPos(level, spawnerData.type(), x, z);
|
||||||
|
if (spawnerData.type().canSummon() && SpawnPlacements.isSpawnPositionOk(spawnerData.type(), level, pos)) {
|
||||||
|
float width = spawnerData.type().getWidth();
|
||||||
|
double fx = Mth.clamp(x, (double)xo + width, xo + 16.0 - width);
|
||||||
|
double fz = Mth.clamp(z, (double)zo + width, zo + 16.0 - width);
|
||||||
|
if (!level.noCollision(spawnerData.type().getSpawnAABB(fx, pos.getY(), fz))
|
||||||
|
|| !SpawnPlacements.checkSpawnRules(
|
||||||
|
spawnerData.type(),
|
||||||
|
level,
|
||||||
|
EntitySpawnReason.CHUNK_GENERATION,
|
||||||
|
BlockPos.containing(fx, pos.getY(), fz),
|
||||||
|
level.getRandom()
|
||||||
|
)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity entity;
|
||||||
|
try {
|
||||||
|
entity = spawnerData.type().create(level.getLevel(), EntitySpawnReason.NATURAL);
|
||||||
|
} catch (Exception e) {
|
||||||
|
IrisLogging.reportError("Failed to create initial mob at chunk " + chunkPos, e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.snapTo(fx, pos.getY(), fz, random.nextFloat() * 360.0F, 0.0F);
|
||||||
|
if (entity instanceof Mob mob
|
||||||
|
&& mob.checkSpawnRules(level, EntitySpawnReason.CHUNK_GENERATION)
|
||||||
|
&& mob.checkSpawnObstruction(level)) {
|
||||||
|
groupSpawnData = mob.finalizeSpawn(
|
||||||
|
level, level.getCurrentDifficultyAt(mob.blockPosition()), EntitySpawnReason.CHUNK_GENERATION, groupSpawnData
|
||||||
|
);
|
||||||
|
level.addFreshEntityWithPassengers(mob, CreatureSpawnEvent.SpawnReason.CHUNK_GEN);
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
x += random.nextInt(5) - random.nextInt(5);
|
||||||
|
|
||||||
|
for (z += random.nextInt(5) - random.nextInt(5);
|
||||||
|
x < xo || x >= xo + 16 || z < zo || z >= zo + 16;
|
||||||
|
z = startZ + random.nextInt(5) - random.nextInt(5)
|
||||||
|
) {
|
||||||
|
x = startX + random.nextInt(5) - random.nextInt(5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BlockPos getTopNonCollidingPos(LevelReader level, EntityType<?> type, int x, int z) {
|
||||||
|
int levelHeight = level.getHeight(SpawnPlacements.getHeightmapType(type), x, z);
|
||||||
|
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(x, levelHeight, z);
|
||||||
|
if (level.dimensionType().hasCeiling()) {
|
||||||
|
do {
|
||||||
|
pos.move(Direction.DOWN);
|
||||||
|
} while (!level.getBlockState(pos).isAir());
|
||||||
|
|
||||||
|
do {
|
||||||
|
pos.move(Direction.DOWN);
|
||||||
|
} while (level.getBlockState(pos).isAir() && pos.getY() > level.getMinY());
|
||||||
|
}
|
||||||
|
|
||||||
|
return SpawnPlacements.getPlacementType(type).adjustSpawnPosition(level, pos.immutable());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+1621
File diff suppressed because it is too large
Load Diff
+1645
File diff suppressed because it is too large
Load Diff
+142
@@ -0,0 +1,142 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
||||||
|
|
||||||
|
import ca.spottedleaf.moonrise.common.util.TickThread;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.SectionPos;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.entity.ai.village.poi.PoiManager;
|
||||||
|
import net.minecraft.world.entity.ai.village.poi.PoiRecord;
|
||||||
|
import net.minecraft.world.entity.ai.village.poi.PoiSection;
|
||||||
|
import net.minecraft.world.entity.ai.village.poi.PoiType;
|
||||||
|
import net.minecraft.world.entity.ai.village.poi.PoiTypes;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.block.Block;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.ImposterProtoChunk;
|
||||||
|
import net.minecraft.world.level.chunk.LevelChunk;
|
||||||
|
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||||
|
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||||
|
import org.bukkit.NamespacedKey;
|
||||||
|
import org.bukkit.persistence.PersistentDataType;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
final class NativeStructurePoiUpdates {
|
||||||
|
private static final NamespacedKey DIRTY_SECTIONS = new NamespacedKey("iris", "native_poi_sections");
|
||||||
|
|
||||||
|
private final int skipPoiFlag;
|
||||||
|
|
||||||
|
NativeStructurePoiUpdates(int skipPoiFlag) {
|
||||||
|
this.skipPoiFlag = skipPoiFlag;
|
||||||
|
}
|
||||||
|
|
||||||
|
static NativeStructurePoiUpdates create() {
|
||||||
|
try {
|
||||||
|
return new NativeStructurePoiUpdates(Block.class.getField("UPDATE_SKIP_POI").getInt(null));
|
||||||
|
} catch (NoSuchFieldException e) {
|
||||||
|
return null;
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new ExceptionInInitializerError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean setBlock(WorldGenLevel world, BlockPos position, BlockState state, int flags, int limit) {
|
||||||
|
if ((flags & skipPoiFlag) != 0) {
|
||||||
|
return world.setBlock(position, state, flags, limit);
|
||||||
|
}
|
||||||
|
ChunkAccess chunk = world.getChunk(position);
|
||||||
|
if (!(chunk instanceof ProtoChunk)) {
|
||||||
|
return world.setBlock(position, state, flags, limit);
|
||||||
|
}
|
||||||
|
if (!(chunk instanceof ImposterProtoChunk)
|
||||||
|
&& !PoiTypes.forState(chunk.getBlockState(position)).equals(PoiTypes.forState(state))) {
|
||||||
|
markSection(chunk, chunk.getSectionIndex(position.getY()));
|
||||||
|
}
|
||||||
|
return world.setBlock(position, state, flags | skipPoiFlag, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void reconcile(LevelChunk chunk) {
|
||||||
|
long[] sections = chunk.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY);
|
||||||
|
if (sections == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
validateSections(chunk, sections);
|
||||||
|
ServerLevel world = (ServerLevel) chunk.getLevel();
|
||||||
|
TickThread.ensureTickThread(world, chunk.getPos(), "Native structure POIs require the chunk owning thread");
|
||||||
|
PoiManager manager = world.getPoiManager();
|
||||||
|
for (int word = 0; word < sections.length; word++) {
|
||||||
|
long remaining = sections[word];
|
||||||
|
while (remaining != 0) {
|
||||||
|
int index = (word << 6) + Long.numberOfTrailingZeros(remaining);
|
||||||
|
reconcileSection(chunk, manager, index);
|
||||||
|
remaining &= remaining - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chunk.persistentDataContainer.remove(DIRTY_SECTIONS);
|
||||||
|
chunk.markUnsaved();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void markSection(ChunkAccess chunk, int index) {
|
||||||
|
long[] sections = chunk.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY);
|
||||||
|
int word = index >>> 6;
|
||||||
|
long bit = 1L << (index & 63);
|
||||||
|
if (sections == null) {
|
||||||
|
sections = new long[word + 1];
|
||||||
|
} else {
|
||||||
|
validateSections(chunk, sections);
|
||||||
|
if (word < sections.length && (sections[word] & bit) != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (word >= sections.length) {
|
||||||
|
sections = Arrays.copyOf(sections, word + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sections[word] |= bit;
|
||||||
|
chunk.persistentDataContainer.set(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY, sections);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateSections(ChunkAccess chunk, long[] sections) {
|
||||||
|
int count = chunk.getSectionsCount();
|
||||||
|
int words = (count + 63) >>> 6;
|
||||||
|
if (sections.length > words
|
||||||
|
|| sections.length == words && (count & 63) != 0
|
||||||
|
&& (sections[words - 1] >>> (count & 63)) != 0) {
|
||||||
|
throw new IllegalArgumentException("Native POI repair sections exceed chunk height at " + chunk.getPos());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void reconcileSection(LevelChunk chunk, PoiManager manager, int index) {
|
||||||
|
SectionPos sectionPosition = SectionPos.of(chunk.getPos(), chunk.getSectionYFromSectionIndex(index));
|
||||||
|
Optional<PoiSection> existing = manager.getOrLoad(sectionPosition.asLong());
|
||||||
|
if (existing.isPresent()) {
|
||||||
|
for (PoiRecord record : existing.get().getRecords(type -> true, PoiManager.Occupancy.ANY).toList()) {
|
||||||
|
if (!PoiTypes.forState(chunk.getBlockState(record.getPos())).equals(Optional.of(record.getPoiType()))) {
|
||||||
|
manager.remove(record.getPos());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LevelChunkSection section = chunk.getSection(index);
|
||||||
|
if (!section.maybeHas(PoiTypes::hasPoi)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
|
||||||
|
for (int y = 0; y < 16; y++) {
|
||||||
|
for (int z = 0; z < 16; z++) {
|
||||||
|
for (int x = 0; x < 16; x++) {
|
||||||
|
Optional<Holder<PoiType>> type = PoiTypes.forState(section.getBlockState(x, y, z));
|
||||||
|
if (type.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
position.set(sectionPosition.minBlockX() + x, sectionPosition.minBlockY() + y,
|
||||||
|
sectionPosition.minBlockZ() + z);
|
||||||
|
if (!manager.getType(position).equals(type)) {
|
||||||
|
manager.add(position.immutable(), type.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+606
@@ -0,0 +1,606 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.nativegen.v26_3_R1.WorldgenTerrainHeightmaps;
|
||||||
|
import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
|
||||||
|
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.QuartPos;
|
||||||
|
import net.minecraft.core.RegistryAccess;
|
||||||
|
import net.minecraft.core.particles.ParticleOptions;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.server.level.WorldGenRegion;
|
||||||
|
import net.minecraft.sounds.SoundEvent;
|
||||||
|
import net.minecraft.sounds.SoundSource;
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.DifficultyInstance;
|
||||||
|
import net.minecraft.world.attribute.EnvironmentAttributeReader;
|
||||||
|
import net.minecraft.world.entity.Entity;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
import net.minecraft.world.flag.FeatureFlagSet;
|
||||||
|
import net.minecraft.world.level.BlockGetter;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.LightLayer;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.biome.BiomeManager;
|
||||||
|
import net.minecraft.world.level.block.Block;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||||
|
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.border.WorldBorder;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkSource;
|
||||||
|
import net.minecraft.world.level.chunk.EmptyLevelChunk;
|
||||||
|
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||||
|
import net.minecraft.world.level.dimension.DimensionType;
|
||||||
|
import net.minecraft.world.level.entity.EntityTypeTest;
|
||||||
|
import net.minecraft.world.level.gameevent.GameEvent;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import net.minecraft.world.level.lighting.LevelLightEngine;
|
||||||
|
import net.minecraft.world.level.material.Fluid;
|
||||||
|
import net.minecraft.world.level.material.FluidState;
|
||||||
|
import net.minecraft.world.level.storage.LevelData;
|
||||||
|
import net.minecraft.world.phys.AABB;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
import net.minecraft.world.ticks.LevelTickAccess;
|
||||||
|
import net.minecraft.world.ticks.ScheduledTick;
|
||||||
|
import org.bukkit.event.entity.CreatureSpawnEvent;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.IntBinaryOperator;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
final class NativeStructureWorldgenAccess implements WorldGenLevel {
|
||||||
|
private static final int WRITE_RADIUS = 1;
|
||||||
|
private static final NativeStructurePoiUpdates POI_UPDATES = NativeStructurePoiUpdates.create();
|
||||||
|
|
||||||
|
private final WorldGenLevel delegate;
|
||||||
|
private final ChunkPos generationCenter;
|
||||||
|
private final IntBinaryOperator surfaceFirstFreeY;
|
||||||
|
private final IntBinaryOperator floorFirstFreeY;
|
||||||
|
private final boolean virtualizeHeights;
|
||||||
|
private final Predicate<BlockPos> protectedPosition;
|
||||||
|
private final Holder<Biome> fallbackBiome;
|
||||||
|
private final BiomeManager biomeManager;
|
||||||
|
private final AABB generationBounds;
|
||||||
|
private final LevelTickAccess<Block> blockTicks;
|
||||||
|
private final LevelTickAccess<Fluid> fluidTicks;
|
||||||
|
private final Long2LongOpenHashMap terrainHeights;
|
||||||
|
private final Long2ObjectOpenHashMap<ChunkAccess> outsideChunks;
|
||||||
|
|
||||||
|
private NativeStructureWorldgenAccess(WorldGenLevel delegate, Boundary boundary) {
|
||||||
|
this.delegate = Objects.requireNonNull(delegate, "Native structure world access requires a delegate");
|
||||||
|
this.generationCenter = Objects.requireNonNull(
|
||||||
|
boundary.generationCenter(), "Native structure world access requires a generation center");
|
||||||
|
this.surfaceFirstFreeY = Objects.requireNonNull(
|
||||||
|
boundary.surfaceFirstFreeY(), "Native structure world access requires a surface resolver");
|
||||||
|
this.floorFirstFreeY = Objects.requireNonNull(
|
||||||
|
boundary.floorFirstFreeY(), "Native structure world access requires an ocean-floor resolver");
|
||||||
|
this.virtualizeHeights = boundary.virtualizeHeights();
|
||||||
|
this.protectedPosition = Objects.requireNonNull(
|
||||||
|
boundary.protectedPosition(), "World access requires a protected-position predicate");
|
||||||
|
BlockPos biomeSample = generationCenter.getMiddleBlockPosition(delegate.getSeaLevel());
|
||||||
|
this.fallbackBiome = delegate.getBiome(biomeSample);
|
||||||
|
this.biomeManager = delegate.getBiomeManager().withDifferentSource(this);
|
||||||
|
this.generationBounds = new AABB(
|
||||||
|
generationCenter.getMinBlockX() - 16,
|
||||||
|
delegate.getMinY(),
|
||||||
|
generationCenter.getMinBlockZ() - 16,
|
||||||
|
generationCenter.getMaxBlockX() + 17,
|
||||||
|
delegate.getMinY() + delegate.getHeight(),
|
||||||
|
generationCenter.getMaxBlockZ() + 17);
|
||||||
|
this.blockTicks = new BoundedTickAccess<>(delegate.getBlockTicks(), this::isWritable);
|
||||||
|
this.fluidTicks = new BoundedTickAccess<>(delegate.getFluidTicks(), this::isWritable);
|
||||||
|
this.terrainHeights = new Long2LongOpenHashMap();
|
||||||
|
this.terrainHeights.defaultReturnValue(Long.MIN_VALUE);
|
||||||
|
this.outsideChunks = new Long2ObjectOpenHashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
static NativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter,
|
||||||
|
IntBinaryOperator surfaceFirstFreeY,
|
||||||
|
IntBinaryOperator floorFirstFreeY,
|
||||||
|
boolean virtualizeHeights,
|
||||||
|
Predicate<BlockPos> protectedPosition) {
|
||||||
|
return new NativeStructureWorldgenAccess(delegate, new Boundary(
|
||||||
|
generationCenter, surfaceFirstFreeY, floorFirstFreeY, virtualizeHeights, protectedPosition));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getSeed() {
|
||||||
|
return delegate.getSeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean ensureCanWrite(BlockPos position) {
|
||||||
|
return isWritable(position) && delegate.ensureCanWrite(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setCurrentlyGenerating(Supplier<String> description) {
|
||||||
|
delegate.setCurrentlyGenerating(description);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ServerLevel getLevel() {
|
||||||
|
return delegate.getLevel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DifficultyInstance getCurrentDifficultyAt(BlockPos position) {
|
||||||
|
if (isReadable(position)) {
|
||||||
|
return delegate.getCurrentDifficultyAt(position);
|
||||||
|
}
|
||||||
|
return delegate.getCurrentDifficultyAt(generationCenter.getMiddleBlockPosition(
|
||||||
|
Mth.clamp(position.getY(), getMinY(), getMaxY() - 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long nextSubTickCount() {
|
||||||
|
return delegate.nextSubTickCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LevelTickAccess<Block> getBlockTicks() {
|
||||||
|
return blockTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LevelTickAccess<Fluid> getFluidTicks() {
|
||||||
|
return fluidTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LevelData getLevelData() {
|
||||||
|
return delegate.getLevelData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MinecraftServer getServer() {
|
||||||
|
return delegate.getServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChunkSource getChunkSource() {
|
||||||
|
return delegate.getChunkSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RandomSource getRandom() {
|
||||||
|
return delegate.getRandom();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateNeighborsAt(BlockPos position, Block block) {
|
||||||
|
if (isWritableNeighbourhood(position)) {
|
||||||
|
delegate.updateNeighborsAt(position, block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void neighborShapeChanged(Direction direction, BlockPos position,
|
||||||
|
BlockPos neighbourPosition, BlockState neighbourState,
|
||||||
|
int updateFlags, int updateLimit) {
|
||||||
|
if (isWritable(position) && isWritable(neighbourPosition)) {
|
||||||
|
delegate.neighborShapeChanged(
|
||||||
|
direction, position, neighbourPosition, neighbourState, updateFlags, updateLimit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void playSound(Entity source, BlockPos position, SoundEvent sound,
|
||||||
|
SoundSource soundSource, float volume, float pitch) {
|
||||||
|
if (isWritable(position)) {
|
||||||
|
delegate.playSound(source, position, sound, soundSource, volume, pitch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addParticle(ParticleOptions particle, double x, double y, double z,
|
||||||
|
double velocityX, double velocityY, double velocityZ) {
|
||||||
|
if (isWritable(BlockPos.containing(x, y, z))) {
|
||||||
|
delegate.addParticle(particle, x, y, z, velocityX, velocityY, velocityZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void levelEvent(Entity source, int eventId, BlockPos position, int data) {
|
||||||
|
if (isWritable(position)) {
|
||||||
|
delegate.levelEvent(source, eventId, position, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void gameEvent(Holder<GameEvent> event, Vec3 position, GameEvent.Context context) {
|
||||||
|
if (isWritable(BlockPos.containing(position))) {
|
||||||
|
delegate.gameEvent(event, position, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChunkAccess getChunk(int chunkX, int chunkZ) {
|
||||||
|
if (isInsideGenerationRegion(chunkX, chunkZ)) {
|
||||||
|
return delegate.getChunk(chunkX, chunkZ);
|
||||||
|
}
|
||||||
|
return outsideChunk(chunkX, chunkZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus status, boolean create) {
|
||||||
|
if (isInsideGenerationRegion(chunkX, chunkZ)) {
|
||||||
|
return delegate.getChunk(chunkX, chunkZ, status, create);
|
||||||
|
}
|
||||||
|
return create ? outsideChunk(chunkX, chunkZ) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChunkAccess getChunkIfLoadedImmediately(int chunkX, int chunkZ) {
|
||||||
|
if (!isInsideGenerationRegion(chunkX, chunkZ)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return delegate.getChunkIfLoadedImmediately(chunkX, chunkZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasChunk(int chunkX, int chunkZ) {
|
||||||
|
return isInsideGenerationRegion(chunkX, chunkZ) && delegate.hasChunk(chunkX, chunkZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getHeight(Heightmap.Types type, int x, int z) {
|
||||||
|
if (!virtualizeHeights && isInsideGenerationRegion(x >> 4, z >> 4)) {
|
||||||
|
return delegate.getHeight(type, x, z);
|
||||||
|
}
|
||||||
|
return height(type, x, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSkyDarken() {
|
||||||
|
return delegate.getSkyDarken();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BiomeManager getBiomeManager() {
|
||||||
|
return biomeManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Holder<Biome> getNoiseBiome(int quartX, int quartY, int quartZ) {
|
||||||
|
return getUncachedNoiseBiome(quartX, quartY, quartZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Holder<Biome> getUncachedNoiseBiome(int quartX, int quartY, int quartZ) {
|
||||||
|
if (isInsideGenerationRegion(QuartPos.toSection(quartX), QuartPos.toSection(quartZ))) {
|
||||||
|
return delegate.getUncachedNoiseBiome(quartX, quartY, quartZ);
|
||||||
|
}
|
||||||
|
return fallbackBiome;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isClientSide() {
|
||||||
|
return delegate.isClientSide();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSeaLevel() {
|
||||||
|
return delegate.getSeaLevel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DimensionType dimensionType() {
|
||||||
|
return delegate.dimensionType();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getMinY() {
|
||||||
|
return delegate.getMinY();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getHeight() {
|
||||||
|
return delegate.getHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RegistryAccess registryAccess() {
|
||||||
|
return delegate.registryAccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FeatureFlagSet enabledFeatures() {
|
||||||
|
return delegate.enabledFeatures();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EnvironmentAttributeReader environmentAttributes() {
|
||||||
|
return delegate.environmentAttributes();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LevelLightEngine getLightEngine() {
|
||||||
|
return delegate.getLightEngine();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getBrightness(LightLayer layer, BlockPos position) {
|
||||||
|
if (isReadable(position)) {
|
||||||
|
return delegate.getBrightness(layer, position);
|
||||||
|
}
|
||||||
|
return layer == LightLayer.SKY && canSeeSky(position) ? 15 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getRawBrightness(BlockPos position, int ambientDarkening) {
|
||||||
|
if (isReadable(position)) {
|
||||||
|
return delegate.getRawBrightness(position, ambientDarkening);
|
||||||
|
}
|
||||||
|
return Math.max(0, getBrightness(LightLayer.SKY, position) - ambientDarkening);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canSeeSky(BlockPos position) {
|
||||||
|
if (isReadable(position)) {
|
||||||
|
return delegate.canSeeSky(position);
|
||||||
|
}
|
||||||
|
return position.getY() >= height(Heightmap.Types.WORLD_SURFACE_WG, position.getX(), position.getZ());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WorldBorder getWorldBorder() {
|
||||||
|
return delegate.getWorldBorder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BlockGetter getChunkForCollisions(int chunkX, int chunkZ) {
|
||||||
|
if (isInsideGenerationRegion(chunkX, chunkZ)) {
|
||||||
|
return delegate.getChunkForCollisions(chunkX, chunkZ);
|
||||||
|
}
|
||||||
|
return outsideChunk(chunkX, chunkZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BlockEntity getBlockEntity(BlockPos position) {
|
||||||
|
return isWritable(position) ? delegate.getBlockEntity(position) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T extends BlockEntity> Optional<T> getBlockEntity(
|
||||||
|
BlockPos position, BlockEntityType<T> type) {
|
||||||
|
return isWritable(position) ? delegate.getBlockEntity(position, type) : Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BlockState getBlockState(BlockPos position) {
|
||||||
|
return isReadable(position) ? delegate.getBlockState(position) : terrainState(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BlockState getBlockStateIfLoaded(BlockPos position) {
|
||||||
|
return isReadable(position) ? delegate.getBlockStateIfLoaded(position) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FluidState getFluidState(BlockPos position) {
|
||||||
|
return isReadable(position) ? delegate.getFluidState(position) : terrainState(position).getFluidState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FluidState getFluidIfLoaded(BlockPos position) {
|
||||||
|
return isReadable(position) ? delegate.getFluidIfLoaded(position) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Entity> getEntities(Entity source, AABB area, Predicate<? super Entity> predicate) {
|
||||||
|
AABB boundedArea = boundedArea(area);
|
||||||
|
return boundedArea == null ? List.of() : delegate.getEntities(source, boundedArea, predicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T extends Entity> List<T> getEntities(
|
||||||
|
EntityTypeTest<Entity, T> type,
|
||||||
|
AABB area, Predicate<? super T> predicate) {
|
||||||
|
AABB boundedArea = boundedArea(area);
|
||||||
|
return boundedArea == null ? List.of() : delegate.getEntities(type, boundedArea, predicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<? extends Player> players() {
|
||||||
|
return delegate.players();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isStateAtPosition(BlockPos position, Predicate<BlockState> predicate) {
|
||||||
|
return predicate.test(getBlockState(position));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isFluidAtPosition(BlockPos position, Predicate<FluidState> predicate) {
|
||||||
|
return predicate.test(getFluidState(position));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BlockPos getHeightmapPos(Heightmap.Types type, BlockPos position) {
|
||||||
|
return position.atY(getHeight(type, position.getX(), position.getZ()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean setBlock(BlockPos position, BlockState state, int updateFlags, int updateLimit) {
|
||||||
|
return isWritable(position)
|
||||||
|
&& (POI_UPDATES != null
|
||||||
|
? POI_UPDATES.setBlock(delegate, position, state, updateFlags, updateLimit)
|
||||||
|
: delegate.setBlock(position, state, updateFlags, updateLimit));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean removeBlock(BlockPos position, boolean move) {
|
||||||
|
if (!isWritable(position)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return POI_UPDATES != null && delegate instanceof WorldGenRegion
|
||||||
|
? setBlock(position, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL, Block.UPDATE_LIMIT)
|
||||||
|
: delegate.removeBlock(position, move);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean destroyBlock(BlockPos position, boolean drop, Entity source, int updateLimit) {
|
||||||
|
if (!isWritable(position)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return POI_UPDATES != null && delegate instanceof WorldGenRegion
|
||||||
|
? !delegate.getBlockState(position).isAir()
|
||||||
|
&& setBlock(position, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL, updateLimit)
|
||||||
|
: delegate.destroyBlock(position, drop, source, updateLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean addFreshEntity(Entity entity) {
|
||||||
|
return isWritable(entity.blockPosition()) && delegate.addFreshEntity(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean addFreshEntity(Entity entity, CreatureSpawnEvent.SpawnReason reason) {
|
||||||
|
return isWritable(entity.blockPosition()) && delegate.addFreshEntity(entity, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isReadable(BlockPos position) {
|
||||||
|
return isInsideGenerationRegion(position.getX() >> 4, position.getZ() >> 4)
|
||||||
|
&& !isOutsideBuildHeight(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isWritable(BlockPos position) {
|
||||||
|
return isReadable(position) && !protectedPosition.test(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isWritableNeighbourhood(BlockPos position) {
|
||||||
|
return isWritable(position)
|
||||||
|
&& isWritable(position.north())
|
||||||
|
&& isWritable(position.south())
|
||||||
|
&& isWritable(position.east())
|
||||||
|
&& isWritable(position.west())
|
||||||
|
&& isWritable(position.above())
|
||||||
|
&& isWritable(position.below());
|
||||||
|
}
|
||||||
|
|
||||||
|
private AABB boundedArea(AABB area) {
|
||||||
|
double minX = Math.max(area.minX, generationBounds.minX);
|
||||||
|
double minY = Math.max(area.minY, generationBounds.minY);
|
||||||
|
double minZ = Math.max(area.minZ, generationBounds.minZ);
|
||||||
|
double maxX = Math.min(area.maxX, generationBounds.maxX);
|
||||||
|
double maxY = Math.min(area.maxY, generationBounds.maxY);
|
||||||
|
double maxZ = Math.min(area.maxZ, generationBounds.maxZ);
|
||||||
|
if (minX >= maxX || minY >= maxY || minZ >= maxZ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (minX == area.minX && minY == area.minY && minZ == area.minZ
|
||||||
|
&& maxX == area.maxX && maxY == area.maxY && maxZ == area.maxZ) {
|
||||||
|
return area;
|
||||||
|
}
|
||||||
|
return new AABB(minX, minY, minZ, maxX, maxY, maxZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isInsideGenerationRegion(int chunkX, int chunkZ) {
|
||||||
|
return Math.abs(chunkX - generationCenter.x()) <= WRITE_RADIUS
|
||||||
|
&& Math.abs(chunkZ - generationCenter.z()) <= WRITE_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isOutsideBuildHeight(BlockPos position) {
|
||||||
|
return position.getY() < getMinY() || position.getY() >= getMaxY();
|
||||||
|
}
|
||||||
|
|
||||||
|
private BlockState terrainState(BlockPos position) {
|
||||||
|
if (isOutsideBuildHeight(position)) {
|
||||||
|
return Blocks.VOID_AIR.defaultBlockState();
|
||||||
|
}
|
||||||
|
long heights = heights(position.getX(), position.getZ());
|
||||||
|
int surface = (int) (heights >> 32);
|
||||||
|
int floor = (int) heights;
|
||||||
|
if (position.getY() < floor) {
|
||||||
|
return Blocks.STONE.defaultBlockState();
|
||||||
|
}
|
||||||
|
if (position.getY() < surface) {
|
||||||
|
return Blocks.WATER.defaultBlockState();
|
||||||
|
}
|
||||||
|
return Blocks.AIR.defaultBlockState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int height(Heightmap.Types type, int x, int z) {
|
||||||
|
long heights = heights(x, z);
|
||||||
|
if (type == Heightmap.Types.OCEAN_FLOOR || type == Heightmap.Types.OCEAN_FLOOR_WG) {
|
||||||
|
return (int) heights;
|
||||||
|
}
|
||||||
|
return (int) (heights >> 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long heights(int x, int z) {
|
||||||
|
long key = ((long) x << 32) ^ (z & 0xffffffffL);
|
||||||
|
long cached = terrainHeights.get(key);
|
||||||
|
if (cached != Long.MIN_VALUE) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
int floor = Mth.clamp(floorFirstFreeY.applyAsInt(x, z), getMinY(), getMaxY());
|
||||||
|
int surface = Mth.clamp(surfaceFirstFreeY.applyAsInt(x, z), floor, getMaxY());
|
||||||
|
long heights = ((long) surface << 32) | (floor & 0xffffffffL);
|
||||||
|
terrainHeights.put(key, heights);
|
||||||
|
return heights;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChunkAccess outsideChunk(int chunkX, int chunkZ) {
|
||||||
|
long key = ChunkPos.pack(chunkX, chunkZ);
|
||||||
|
ChunkAccess cached = outsideChunks.get(key);
|
||||||
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
EmptyLevelChunk chunk = new EmptyLevelChunk(getLevel(), new ChunkPos(chunkX, chunkZ), fallbackBiome);
|
||||||
|
WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY, floorFirstFreeY);
|
||||||
|
outsideChunks.put(key, chunk);
|
||||||
|
return chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Boundary(ChunkPos generationCenter,
|
||||||
|
IntBinaryOperator surfaceFirstFreeY,
|
||||||
|
IntBinaryOperator floorFirstFreeY,
|
||||||
|
boolean virtualizeHeights,
|
||||||
|
Predicate<BlockPos> protectedPosition) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class BoundedTickAccess<T> implements LevelTickAccess<T> {
|
||||||
|
private final LevelTickAccess<T> delegate;
|
||||||
|
private final Predicate<BlockPos> writable;
|
||||||
|
|
||||||
|
private BoundedTickAccess(LevelTickAccess<T> delegate, Predicate<BlockPos> writable) {
|
||||||
|
this.delegate = delegate;
|
||||||
|
this.writable = writable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void schedule(ScheduledTick<T> tick) {
|
||||||
|
if (writable.test(tick.pos())) {
|
||||||
|
delegate.schedule(tick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasScheduledTick(BlockPos position, T type) {
|
||||||
|
return writable.test(position) && delegate.hasScheduledTick(position, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int count() {
|
||||||
|
return delegate.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean willTickThisTick(BlockPos position, T type) {
|
||||||
|
return writable.test(position) && delegate.willTickThisTick(position, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.platform.agent.Agent;
|
||||||
|
import it.unimi.dsi.fastutil.shorts.ShortList;
|
||||||
|
import net.bytebuddy.ByteBuddy;
|
||||||
|
import net.bytebuddy.asm.Advice;
|
||||||
|
import net.bytebuddy.matcher.ElementMatchers;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
|
import net.minecraft.server.level.WorldGenRegion;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import java.util.function.LongPredicate;
|
||||||
|
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||||
|
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
final class NmsGenerationHooks {
|
||||||
|
private NmsGenerationHooks() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static void install() {
|
||||||
|
ByteBuddy buddy = new ByteBuddy();
|
||||||
|
for (Class<?> clazz : List.of(ChunkAccess.class, ProtoChunk.class)) {
|
||||||
|
buddy.redefine(clazz)
|
||||||
|
.visit(Advice.to(ChunkAccessAdvice.class).on(ElementMatchers.isMethod().and(ElementMatchers.takesArguments(ShortList.class, int.class))))
|
||||||
|
.make()
|
||||||
|
.load(clazz.getClassLoader(), Agent.installed());
|
||||||
|
}
|
||||||
|
buddy.redefine(WorldGenRegion.class)
|
||||||
|
.visit(Advice.to(WorldGenerationWriteAdvice.class).on(ElementMatchers.named("ensureCanWrite")
|
||||||
|
.and(ElementMatchers.takesArguments(BlockPos.class))))
|
||||||
|
.make()
|
||||||
|
.load(WorldGenRegion.class.getClassLoader(), Agent.installed());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class WorldGenerationWriteAdvice {
|
||||||
|
@Advice.OnMethodExit
|
||||||
|
static void exit(@Advice.This WorldGenRegion region, @Advice.Argument(0) BlockPos position,
|
||||||
|
@Advice.Return(readOnly = false) boolean allowed) {
|
||||||
|
if (!allowed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ChunkGenerator generator = region.getLevel().getChunkSource().getGenerator();
|
||||||
|
if (generator instanceof LongPredicate predicate
|
||||||
|
&& generator.getClass().getName().equals("art.arcane.iris.platform.bukkit.nms.v26_3_R1.IrisChunkGenerator")
|
||||||
|
&& region.getChunk(position.getX() >> 4, position.getZ() >> 4)
|
||||||
|
.getPersistedStatus().isOrAfter(ChunkStatus.FULL)) {
|
||||||
|
allowed = predicate.test(ChunkPos.pack(position.getX() >> 4, position.getZ() >> 4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ChunkAccessAdvice {
|
||||||
|
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
|
||||||
|
static boolean enter(@Advice.This ChunkAccess access, @Advice.Argument(1) int index) {
|
||||||
|
return index >= access.getPostProcessing().length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+182
@@ -0,0 +1,182 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.spi.PlatformGenerationRegistry;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.JsonOps;
|
||||||
|
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.RegistryOps;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.dimension.DimensionType;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
final class NmsGenerationRegistry implements PlatformGenerationRegistry {
|
||||||
|
private static final String BIOME_REGISTRY = "minecraft:worldgen/biome";
|
||||||
|
private static final String STRUCTURE_REGISTRY = "minecraft:worldgen/structure";
|
||||||
|
private static final String DIMENSION_TYPE_REGISTRY = "minecraft:dimension_type";
|
||||||
|
private static final String BLOCK_REGISTRY = "minecraft:block";
|
||||||
|
private static final String ENTITY_TYPE_REGISTRY = "minecraft:entity_type";
|
||||||
|
|
||||||
|
private final Supplier<RegistryAccess> registryAccess;
|
||||||
|
private final String runtimeIdentity;
|
||||||
|
private final String rendererIdentity;
|
||||||
|
|
||||||
|
NmsGenerationRegistry(
|
||||||
|
Supplier<RegistryAccess> registryAccess,
|
||||||
|
String runtimeIdentity,
|
||||||
|
String rendererIdentity
|
||||||
|
) {
|
||||||
|
this.registryAccess = Objects.requireNonNull(registryAccess, "registryAccess");
|
||||||
|
this.runtimeIdentity = requireText(runtimeIdentity, "runtimeIdentity");
|
||||||
|
this.rendererIdentity = requireText(rendererIdentity, "rendererIdentity");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String runtimeIdentity() {
|
||||||
|
return runtimeIdentity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String generatedDefinitionRendererIdentity() {
|
||||||
|
return rendererIdentity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String customBiomeResourceKey(String identitySha256) {
|
||||||
|
return PlatformGenerationRegistry.contentAddressedCustomBiomeResourceKey(identitySha256);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> legacyCustomBiomeResourceKeys(
|
||||||
|
String packName,
|
||||||
|
String dimensionKey,
|
||||||
|
String customBiomeId
|
||||||
|
) {
|
||||||
|
return List.of(requireText(dimensionKey, "dimensionKey").toLowerCase(Locale.ROOT)
|
||||||
|
+ ":" + requireText(customBiomeId, "customBiomeId").toLowerCase(Locale.ROOT));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String dimensionTypeResourceKey(String packName, String dimensionKey, String dimensionTypeKey) {
|
||||||
|
return "iris:" + requireText(dimensionTypeKey, "dimensionTypeKey").toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Definition definition(String registryKey, String resourceKey) {
|
||||||
|
RegistryAccess access = Objects.requireNonNull(registryAccess.get(), "Minecraft registry access");
|
||||||
|
return switch (registryKey) {
|
||||||
|
case BIOME_REGISTRY -> presence(access, Registries.BIOME, registryKey, resourceKey);
|
||||||
|
case STRUCTURE_REGISTRY -> presence(access, Registries.STRUCTURE, registryKey, resourceKey);
|
||||||
|
case DIMENSION_TYPE_REGISTRY -> presence(
|
||||||
|
access,
|
||||||
|
Registries.DIMENSION_TYPE,
|
||||||
|
registryKey,
|
||||||
|
resourceKey
|
||||||
|
);
|
||||||
|
case BLOCK_REGISTRY -> presence(access, Registries.BLOCK, registryKey, resourceKey);
|
||||||
|
case ENTITY_TYPE_REGISTRY -> presence(access, Registries.ENTITY_TYPE, registryKey, resourceKey);
|
||||||
|
default -> throw new IllegalArgumentException("Unsupported generation registry: " + registryKey);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Definition canonicalDefinition(String registryKey, String resourceKey, String sourceJson) {
|
||||||
|
RegistryAccess access = Objects.requireNonNull(registryAccess.get(), "Minecraft registry access");
|
||||||
|
return switch (registryKey) {
|
||||||
|
case BIOME_REGISTRY -> canonicalize(access, Biome.DIRECT_CODEC, sourceJson);
|
||||||
|
case DIMENSION_TYPE_REGISTRY -> canonicalize(access, DimensionType.DIRECT_CODEC, sourceJson);
|
||||||
|
default -> throw new IllegalArgumentException(
|
||||||
|
"Registry does not accept generated JSON definitions: " + registryKey
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Definition generatedDefinition(String registryKey, String resourceKey) {
|
||||||
|
RegistryAccess access = Objects.requireNonNull(registryAccess.get(), "Minecraft registry access");
|
||||||
|
return switch (registryKey) {
|
||||||
|
case BIOME_REGISTRY -> exactRegistered(
|
||||||
|
access,
|
||||||
|
Registries.BIOME,
|
||||||
|
Biome.DIRECT_CODEC,
|
||||||
|
resourceKey
|
||||||
|
);
|
||||||
|
case DIMENSION_TYPE_REGISTRY -> exactRegistered(
|
||||||
|
access,
|
||||||
|
Registries.DIMENSION_TYPE,
|
||||||
|
DimensionType.DIRECT_CODEC,
|
||||||
|
resourceKey
|
||||||
|
);
|
||||||
|
default -> throw new IllegalArgumentException(
|
||||||
|
"Registry does not contain Iris-generated definitions: " + registryKey
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> Definition presence(
|
||||||
|
RegistryAccess access,
|
||||||
|
ResourceKey<? extends Registry<T>> registryKey,
|
||||||
|
String registryResourceKey,
|
||||||
|
String resourceKey
|
||||||
|
) {
|
||||||
|
Registry<T> registry = access.lookupOrThrow(registryKey);
|
||||||
|
Identifier identifier = requireIdentifier(resourceKey);
|
||||||
|
T value = registry.getValue(identifier);
|
||||||
|
if (value == null) {
|
||||||
|
throw new IllegalStateException("Missing registry definition " + registryKey.identifier()
|
||||||
|
+ " / " + resourceKey + ".");
|
||||||
|
}
|
||||||
|
return Definition.resourceIdentity(registryResourceKey, resourceKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> Definition canonicalize(RegistryAccess access, Codec<T> codec, String sourceJson) {
|
||||||
|
RegistryOps<JsonElement> ops = RegistryOps.create(JsonOps.INSTANCE, access);
|
||||||
|
JsonElement source = JsonParser.parseString(Objects.requireNonNull(sourceJson, "sourceJson"));
|
||||||
|
T decoded = codec.parse(ops, source).getOrThrow(IllegalArgumentException::new);
|
||||||
|
JsonElement encoded = codec.encodeStart(ops, decoded).getOrThrow(IllegalStateException::new);
|
||||||
|
return Definition.exactJson(encoded.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> Definition exactRegistered(
|
||||||
|
RegistryAccess access,
|
||||||
|
ResourceKey<? extends Registry<T>> registryKey,
|
||||||
|
Codec<T> codec,
|
||||||
|
String resourceKey
|
||||||
|
) {
|
||||||
|
Registry<T> registry = access.lookupOrThrow(registryKey);
|
||||||
|
Identifier identifier = requireIdentifier(resourceKey);
|
||||||
|
T value = registry.getValue(identifier);
|
||||||
|
if (value == null) {
|
||||||
|
throw new IllegalStateException("Missing generated registry definition "
|
||||||
|
+ registryKey.identifier() + " / " + resourceKey + ".");
|
||||||
|
}
|
||||||
|
RegistryOps<JsonElement> ops = RegistryOps.create(JsonOps.INSTANCE, access);
|
||||||
|
JsonElement encoded = codec.encodeStart(ops, value).getOrThrow(IllegalStateException::new);
|
||||||
|
return Definition.exactJson(encoded.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Identifier requireIdentifier(String resourceKey) {
|
||||||
|
Identifier identifier = Identifier.tryParse(resourceKey);
|
||||||
|
if (identifier == null) {
|
||||||
|
throw new IllegalArgumentException("Invalid registry resource key: " + resourceKey);
|
||||||
|
}
|
||||||
|
return identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String requireText(String value, String label) {
|
||||||
|
String required = Objects.requireNonNull(value, label).trim();
|
||||||
|
if (required.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException(label + " must not be blank.");
|
||||||
|
}
|
||||||
|
return required;
|
||||||
|
}
|
||||||
|
}
|
||||||
+266
@@ -0,0 +1,266 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.world.history.NativeTerrainReceipt;
|
||||||
|
import art.arcane.iris.world.history.SavedTerrainChunk;
|
||||||
|
import art.arcane.iris.world.task.J;
|
||||||
|
import ca.spottedleaf.concurrentutil.completable.Completable;
|
||||||
|
import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
|
||||||
|
import ca.spottedleaf.concurrentutil.util.Priority;
|
||||||
|
import ca.spottedleaf.moonrise.common.PlatformHooks;
|
||||||
|
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
|
||||||
|
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
|
||||||
|
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
|
||||||
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
import net.minecraft.nbt.NbtIo;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.craftbukkit.CraftWorld;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CompletionException;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
import java.util.concurrent.locks.LockSupport;
|
||||||
|
|
||||||
|
final class NmsSavedTerrainCapture {
|
||||||
|
private static final long WRITE_TIMEOUT_NANOS = TimeUnit.SECONDS.toNanos(30L);
|
||||||
|
private static final int SNAPSHOT_BATCH_SIZE = 64;
|
||||||
|
|
||||||
|
private NmsSavedTerrainCapture() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static CompletableFuture<SavedTerrainChunk> capture(World world, int chunkX, int chunkZ, int minimumY, int height) {
|
||||||
|
ServerLevel level = ((CraftWorld) world).getHandle();
|
||||||
|
CompletableFuture<CapturedChunk> snapshot = new CompletableFuture<>();
|
||||||
|
J.runRegionFuture(world, chunkX, chunkZ, () -> {
|
||||||
|
try {
|
||||||
|
NewChunkHolder holder = level.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ);
|
||||||
|
ChunkAccess chunk = holder == null ? null : holder.getCurrentChunk();
|
||||||
|
if (chunk == null) {
|
||||||
|
snapshot.complete(new CapturedChunk(new ChunkPos(chunkX, chunkZ), CompletableFuture.completedFuture(null)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String status = BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getPersistedStatus()).toString();
|
||||||
|
if (!SavedTerrainChunk.hasTerrain(status)) {
|
||||||
|
throw new IOException("Native chunk " + chunkX + "," + chunkZ + " has no completed terrain at " + status);
|
||||||
|
}
|
||||||
|
snapshot.complete(scheduleSnapshot(level, chunk));
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
snapshot.completeExceptionally(failure);
|
||||||
|
}
|
||||||
|
}).whenComplete((ignored, failure) -> {
|
||||||
|
if (failure != null) {
|
||||||
|
snapshot.completeExceptionally(failure);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return snapshot.thenApplyAsync(captured -> {
|
||||||
|
try {
|
||||||
|
long deadline = System.nanoTime() + WRITE_TIMEOUT_NANOS;
|
||||||
|
CompoundTag data = awaitSnapshot(level, captured, deadline);
|
||||||
|
Checkpoint checkpoint = checkpoint(chunkX, chunkZ, data);
|
||||||
|
awaitWrite(level, checkpoint.chunk(), deadline);
|
||||||
|
if (data == null) {
|
||||||
|
return SavedTerrainChunk.read(world.getWorldFolder().toPath(), chunkX, chunkZ, minimumY, height);
|
||||||
|
}
|
||||||
|
verifyCheckpoint(world.getWorldFolder().toPath(), checkpoint);
|
||||||
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
|
try (DataOutputStream output = new DataOutputStream(bytes)) {
|
||||||
|
NbtIo.write(data, output);
|
||||||
|
}
|
||||||
|
return SavedTerrainChunk.readNbt(bytes.toByteArray(), chunkX, chunkZ, minimumY, height);
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw new CompletionException(failure);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static CompletableFuture<Void> flush(World world) {
|
||||||
|
ServerLevel level = ((CraftWorld) world).getHandle();
|
||||||
|
List<NewChunkHolder> holders = level.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolders();
|
||||||
|
return CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
flushSnapshots(world, level, holders);
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw new CompletionException(failure);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static void flushSnapshots(World world, ServerLevel level, List<NewChunkHolder> holders) throws IOException {
|
||||||
|
long deadline = System.nanoTime() + WRITE_TIMEOUT_NANOS;
|
||||||
|
List<Checkpoint> checkpoints = new ArrayList<>(holders.size());
|
||||||
|
for (int offset = 0; offset < holders.size(); offset += SNAPSHOT_BATCH_SIZE) {
|
||||||
|
int end = Math.min(holders.size(), offset + SNAPSHOT_BATCH_SIZE);
|
||||||
|
List<CompletableFuture<CapturedChunk>> snapshots = requestSnapshots(world, level, holders.subList(offset, end));
|
||||||
|
awaitSnapshots(snapshots, deadline);
|
||||||
|
for (CompletableFuture<CapturedChunk> future : snapshots) {
|
||||||
|
CapturedChunk captured = future.join();
|
||||||
|
CompoundTag data = awaitSnapshot(level, captured, deadline);
|
||||||
|
checkpoints.add(checkpoint(captured.chunk().x(), captured.chunk().z(), data));
|
||||||
|
}
|
||||||
|
for (int index = offset; index < end; index++) {
|
||||||
|
awaitWrite(level, checkpoints.get(index).chunk(), deadline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finishCheckpoint(level, world.getWorldFolder().toPath(), checkpoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<CompletableFuture<CapturedChunk>> requestSnapshots(
|
||||||
|
World world, ServerLevel level, List<NewChunkHolder> holders) {
|
||||||
|
List<CompletableFuture<CapturedChunk>> snapshots = new ArrayList<>(holders.size());
|
||||||
|
for (NewChunkHolder original : holders) {
|
||||||
|
ChunkPos position = new ChunkPos(original.chunkX, original.chunkZ);
|
||||||
|
CompletableFuture<CapturedChunk> snapshot = new CompletableFuture<>();
|
||||||
|
snapshots.add(snapshot);
|
||||||
|
J.runRegionFuture(world, position.x(), position.z(), () -> {
|
||||||
|
try {
|
||||||
|
NewChunkHolder holder = level.moonrise$getChunkTaskScheduler().chunkHolderManager
|
||||||
|
.getChunkHolder(position.x(), position.z());
|
||||||
|
ChunkAccess chunk = holder == null ? null : holder.getCurrentChunk();
|
||||||
|
snapshot.complete(chunk != null && chunk.isUnsaved() ? scheduleSnapshot(level, chunk)
|
||||||
|
: new CapturedChunk(position, CompletableFuture.completedFuture(null)));
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
snapshot.completeExceptionally(failure);
|
||||||
|
}
|
||||||
|
}).whenComplete((ignored, failure) -> {
|
||||||
|
if (failure != null) {
|
||||||
|
snapshot.completeExceptionally(failure);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return snapshots;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void awaitSnapshots(List<CompletableFuture<CapturedChunk>> snapshots, long deadlineNanos)
|
||||||
|
throws IOException {
|
||||||
|
try {
|
||||||
|
CompletableFuture.allOf(snapshots.toArray(CompletableFuture[]::new))
|
||||||
|
.get(Math.max(0L, deadlineNanos - System.nanoTime()), TimeUnit.NANOSECONDS);
|
||||||
|
} catch (InterruptedException failure) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IOException("Interrupted while copying terrain checkpoint chunks.", failure);
|
||||||
|
} catch (ExecutionException | TimeoutException failure) {
|
||||||
|
throw new IOException("Unable to copy terrain checkpoint chunks.", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void finishCheckpoint(ServerLevel level, Path worldFolder, List<Checkpoint> checkpoints) throws IOException {
|
||||||
|
long deadline = System.nanoTime() + WRITE_TIMEOUT_NANOS;
|
||||||
|
for (Checkpoint checkpoint : checkpoints) {
|
||||||
|
awaitWrite(level, checkpoint.chunk(), deadline);
|
||||||
|
}
|
||||||
|
MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA);
|
||||||
|
for (Checkpoint checkpoint : checkpoints) {
|
||||||
|
verifyCheckpoint(worldFolder, checkpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Checkpoint checkpoint(int chunkX, int chunkZ, CompoundTag data) {
|
||||||
|
if (data == null) {
|
||||||
|
return new Checkpoint(new ChunkPos(chunkX, chunkZ), null);
|
||||||
|
}
|
||||||
|
byte[] receipt = data.getCompound("ChunkBukkitValues")
|
||||||
|
.flatMap(values -> values.getByteArray(NativeTerrainReceipt.NBT_KEY)).orElse(null);
|
||||||
|
long structureActivation = data.getCompound("ChunkBukkitValues").map(values -> values.getLongOr(
|
||||||
|
NativeTerrainReceipt.STRUCTURE_ACTIVATION_KEY, 0)).orElse(0L);
|
||||||
|
return new Checkpoint(new ChunkPos(chunkX, chunkZ), new Verification(
|
||||||
|
data.getStringOr("Status", ""), receipt, structureActivation));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyCheckpoint(Path worldFolder, Checkpoint checkpoint) throws IOException {
|
||||||
|
Verification expected = checkpoint.verification();
|
||||||
|
if (expected == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SavedTerrainChunk.verifyCheckpoint(worldFolder, checkpoint.chunk().x(), checkpoint.chunk().z(),
|
||||||
|
expected.status(), expected.receipt(), expected.structureActivation());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void awaitWrite(ServerLevel level, ChunkPos chunk, long deadlineNanos) throws IOException {
|
||||||
|
ChunkTaskScheduler scheduler = level.moonrise$getChunkTaskScheduler();
|
||||||
|
while (true) {
|
||||||
|
checkWriteWait(chunk, deadlineNanos);
|
||||||
|
if (MoonriseRegionFileIO.getPriority(level, chunk.x(), chunk.z(),
|
||||||
|
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA) == Priority.COMPLETING) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
helpSaveTasks(scheduler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static CapturedChunk scheduleSnapshot(ServerLevel level, ChunkAccess chunk) {
|
||||||
|
SerializableChunkData snapshot = SerializableChunkData.copyOf(level, chunk);
|
||||||
|
PlatformHooks.get().chunkSyncSave(level, chunk, snapshot);
|
||||||
|
Completable<CompoundTag> nativeData = new Completable<>();
|
||||||
|
CompletableFuture<CompoundTag> data = new CompletableFuture<>();
|
||||||
|
PrioritisedExecutor.PrioritisedTask task = level.moonrise$getChunkTaskScheduler().saveExecutor
|
||||||
|
.createTask(() -> serializeSnapshot(snapshot, nativeData, data));
|
||||||
|
task.queue();
|
||||||
|
ChunkPos position = chunk.getPos();
|
||||||
|
MoonriseRegionFileIO.scheduleSave(level, position.x(), position.z(), nativeData, task,
|
||||||
|
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, Priority.NORMAL);
|
||||||
|
return new CapturedChunk(position, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
static CompoundTag awaitSnapshot(ServerLevel level, CapturedChunk captured, long deadlineNanos) throws IOException {
|
||||||
|
ChunkTaskScheduler scheduler = level.moonrise$getChunkTaskScheduler();
|
||||||
|
while (!captured.data().isDone()) {
|
||||||
|
checkWriteWait(captured.chunk(), deadlineNanos);
|
||||||
|
helpSaveTasks(scheduler);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return captured.data().join();
|
||||||
|
} catch (CompletionException failure) {
|
||||||
|
throw new IOException("Unable to serialize terrain checkpoint chunk " + captured.chunk(), failure.getCause());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void serializeSnapshot(SerializableChunkData snapshot, Completable<CompoundTag> nativeData,
|
||||||
|
CompletableFuture<CompoundTag> data) {
|
||||||
|
try {
|
||||||
|
CompoundTag serialized = snapshot.write();
|
||||||
|
nativeData.complete(serialized);
|
||||||
|
data.complete(serialized);
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
nativeData.completeExceptionally(failure);
|
||||||
|
data.completeExceptionally(failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void checkWriteWait(ChunkPos chunk, long deadlineNanos) throws IOException {
|
||||||
|
if (Thread.currentThread().isInterrupted()) {
|
||||||
|
throw new IOException("Interrupted while saving terrain checkpoint chunk " + chunk);
|
||||||
|
}
|
||||||
|
if (System.nanoTime() - deadlineNanos >= 0L) {
|
||||||
|
throw new IOException("Timed out saving terrain checkpoint chunk " + chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void helpSaveTasks(ChunkTaskScheduler scheduler) {
|
||||||
|
boolean helped = scheduler.saveExecutor.executeTask();
|
||||||
|
helped |= scheduler.compressionExecutor.executeTask();
|
||||||
|
if (!helped) {
|
||||||
|
LockSupport.parkNanos(1_000_000L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record CapturedChunk(ChunkPos chunk, CompletableFuture<CompoundTag> data) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record Checkpoint(ChunkPos chunk, Verification verification) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record Verification(String status, byte[] receipt, long structureActivation) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+403
@@ -0,0 +1,403 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.platform.bukkit.nms.ServerShutdownBoundary;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.iris.localization.C;
|
||||||
|
import art.arcane.iris.platform.agent.Agent;
|
||||||
|
import net.bytebuddy.agent.builder.AgentBuilder;
|
||||||
|
import net.bytebuddy.agent.builder.ResettableClassFileTransformer;
|
||||||
|
import net.bytebuddy.asm.Advice;
|
||||||
|
import net.bytebuddy.description.type.TypeDescription;
|
||||||
|
import net.bytebuddy.dynamic.DynamicType;
|
||||||
|
import net.bytebuddy.matcher.ElementMatchers;
|
||||||
|
import net.bytebuddy.utility.JavaModule;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.level.dimension.LevelStem;
|
||||||
|
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.craftbukkit.CraftServer;
|
||||||
|
import org.bukkit.generator.ChunkGenerator;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
final class NmsWorldLifecycle {
|
||||||
|
private final AtomicBoolean injected = new AtomicBoolean();
|
||||||
|
private volatile ResettableClassFileTransformer levelStorageAccessTransformer;
|
||||||
|
private volatile ResettableClassFileTransformer serverLevelTransformer;
|
||||||
|
private volatile ResettableClassFileTransformer pluginClassLoaderTransformer;
|
||||||
|
private boolean pluginClassLoaderCloseDeferred;
|
||||||
|
|
||||||
|
public boolean injectBukkit() {
|
||||||
|
synchronized (injected) {
|
||||||
|
if (injected.get()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
IrisLogging.info("Injecting Bukkit");
|
||||||
|
Agent.requireClassLoaderCloseDeferral();
|
||||||
|
Class<?> loaderCloseType = getClass().getClassLoader().getClass()
|
||||||
|
.getMethod("close").getDeclaringClass();
|
||||||
|
PluginClassLoaderInjectionListener loaderListener = new PluginClassLoaderInjectionListener();
|
||||||
|
pluginClassLoaderTransformer = new AgentBuilder.Default()
|
||||||
|
.disableClassFormatChanges()
|
||||||
|
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
||||||
|
.with(AgentBuilder.RedefinitionStrategy.Listener.ErrorEscalating.FAIL_FAST)
|
||||||
|
.with(loaderListener)
|
||||||
|
.type(ElementMatchers.is(loaderCloseType))
|
||||||
|
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
|
||||||
|
builder.visit(Advice.to(PluginClassLoaderCloseAdvice.class)
|
||||||
|
.on(ElementMatchers.named("close")
|
||||||
|
.and(ElementMatchers.takesArguments(0))
|
||||||
|
.and(ElementMatchers.returns(void.class)))))
|
||||||
|
.installOn(Agent.getInstrumentation());
|
||||||
|
loaderListener.requireInstalled();
|
||||||
|
levelStorageAccessTransformer = new AgentBuilder.Default()
|
||||||
|
.disableClassFormatChanges()
|
||||||
|
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
||||||
|
.type(ElementMatchers.is(LevelStorageSource.LevelStorageAccess.class))
|
||||||
|
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
|
||||||
|
builder.visit(Advice.to(LevelStorageAccessAdvice.class).on(ElementMatchers.isConstructor()
|
||||||
|
.and(ElementMatchers.takesArguments(4))
|
||||||
|
.and(ElementMatchers.takesArgument(0, LevelStorageSource.class))
|
||||||
|
.and(ElementMatchers.takesArgument(1, String.class))
|
||||||
|
.and(ElementMatchers.takesArgument(2, Path.class))
|
||||||
|
.and(ElementMatchers.takesArgument(3, ResourceKey.class)))))
|
||||||
|
.installOn(Agent.getInstrumentation());
|
||||||
|
serverLevelTransformer = new AgentBuilder.Default()
|
||||||
|
.disableClassFormatChanges()
|
||||||
|
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
||||||
|
.type(ElementMatchers.is(ServerLevel.class))
|
||||||
|
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
|
||||||
|
builder.visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor()
|
||||||
|
.and(ElementMatchers.takesArgument(0, MinecraftServer.class))
|
||||||
|
.and(ElementMatchers.takesArgument(5, LevelStem.class)))))
|
||||||
|
.installOn(Agent.getInstrumentation());
|
||||||
|
NmsGenerationHooks.install();
|
||||||
|
|
||||||
|
injected.set(true);
|
||||||
|
return true;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
IrisLogging.reportError(C.RED + "Failed to inject Bukkit", e);
|
||||||
|
ResettableClassFileTransformer partialServerLevel = serverLevelTransformer;
|
||||||
|
ResettableClassFileTransformer partialStorageAccess = levelStorageAccessTransformer;
|
||||||
|
ResettableClassFileTransformer partialPluginClassLoader = pluginClassLoaderTransformer;
|
||||||
|
serverLevelTransformer = null;
|
||||||
|
levelStorageAccessTransformer = null;
|
||||||
|
pluginClassLoaderTransformer = null;
|
||||||
|
for (ResettableClassFileTransformer partial : new ResettableClassFileTransformer[]{
|
||||||
|
partialServerLevel,
|
||||||
|
partialStorageAccess,
|
||||||
|
partialPluginClassLoader
|
||||||
|
}) {
|
||||||
|
if (partial == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
partial.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
|
||||||
|
} catch (Throwable cleanupFailure) {
|
||||||
|
IrisLogging.reportError("Failed to remove partial Bukkit lifecycle injection", cleanupFailure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ensureServerLevelInjection() {
|
||||||
|
if (!injected.get()) {
|
||||||
|
throw new IllegalStateException("Iris world lifecycle injection is unavailable. Fix the Java Agent or Code Injection startup failure and restart before creating worlds.");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Agent.getInstrumentation().retransformClasses(
|
||||||
|
LevelStorageSource.LevelStorageAccess.class,
|
||||||
|
ServerLevel.class
|
||||||
|
);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
throw new IllegalStateException("Failed to re-apply Bukkit world lifecycle injection. World creation was stopped before loading the world.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void uninjectBukkit() {
|
||||||
|
synchronized (injected) {
|
||||||
|
ResettableClassFileTransformer activeServerLevel = serverLevelTransformer;
|
||||||
|
ResettableClassFileTransformer activeStorageAccess = levelStorageAccessTransformer;
|
||||||
|
serverLevelTransformer = null;
|
||||||
|
levelStorageAccessTransformer = null;
|
||||||
|
injected.set(false);
|
||||||
|
for (ResettableClassFileTransformer transformer : new ResettableClassFileTransformer[]{
|
||||||
|
activeServerLevel,
|
||||||
|
activeStorageAccess
|
||||||
|
}) {
|
||||||
|
if (transformer == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
transformer.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
IrisLogging.reportError(C.RED + "Failed to remove Bukkit world lifecycle injection", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isServerStopping() {
|
||||||
|
return ((CraftServer) Bukkit.getServer()).getServer().hasStopped();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerShutdownBoundary createServerShutdownBoundary() {
|
||||||
|
MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer();
|
||||||
|
return new ServerShutdownBoundary(
|
||||||
|
() -> server.hasFullyShutdown,
|
||||||
|
server.getRunningThread()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deferPluginClassLoaderClose() {
|
||||||
|
synchronized (injected) {
|
||||||
|
if (pluginClassLoaderTransformer == null || pluginClassLoaderCloseDeferred) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Agent.retainClassLoader(getClass().getClassLoader());
|
||||||
|
pluginClassLoaderCloseDeferred = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void releasePluginClassLoaderClose() {
|
||||||
|
ResettableClassFileTransformer transformer;
|
||||||
|
boolean releaseLoader;
|
||||||
|
synchronized (injected) {
|
||||||
|
transformer = pluginClassLoaderTransformer;
|
||||||
|
pluginClassLoaderTransformer = null;
|
||||||
|
releaseLoader = pluginClassLoaderCloseDeferred;
|
||||||
|
pluginClassLoaderCloseDeferred = false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (transformer != null
|
||||||
|
&& !transformer.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)) {
|
||||||
|
throw new IllegalStateException("Failed to remove Iris plugin class loader lifecycle injection");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (releaseLoader) {
|
||||||
|
Agent.releaseClassLoader(getClass().getClassLoader());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class PluginClassLoaderInjectionListener extends AgentBuilder.Listener.Adapter {
|
||||||
|
private volatile boolean transformed;
|
||||||
|
private volatile Throwable failure;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTransformation(TypeDescription type, ClassLoader loader, JavaModule module,
|
||||||
|
boolean loaded, DynamicType dynamicType) {
|
||||||
|
transformed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(String typeName, ClassLoader loader, JavaModule module,
|
||||||
|
boolean loaded, Throwable throwable) {
|
||||||
|
failure = throwable;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requireInstalled() {
|
||||||
|
if (!transformed || failure != null) {
|
||||||
|
throw new IllegalStateException("Iris plugin class loader lifecycle injection failed", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class PluginClassLoaderCloseAdvice {
|
||||||
|
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
|
||||||
|
static boolean enter(@Advice.This ClassLoader loader) throws ReflectiveOperationException {
|
||||||
|
Class<?> installer = Class.forName(
|
||||||
|
"art.arcane.iris.platform.agent.Installer", true, ClassLoader.getSystemClassLoader());
|
||||||
|
return Boolean.TRUE.equals(installer.getMethod("deferClassLoaderClose", ClassLoader.class)
|
||||||
|
.invoke(null, loader));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class LevelStorageAccessAdvice {
|
||||||
|
@Advice.OnMethodEnter
|
||||||
|
static void enter(
|
||||||
|
@Advice.Argument(1) String levelId,
|
||||||
|
@Advice.Argument(value = 3, readOnly = false) ResourceKey<LevelStem> dimensionType
|
||||||
|
) {
|
||||||
|
if (levelId == null || levelId.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClassLoader pluginClassLoader;
|
||||||
|
Class<?> generatorType;
|
||||||
|
Class<?> stagingType;
|
||||||
|
try {
|
||||||
|
Plugin irisPlugin = Bukkit.getPluginManager().getPlugin("Iris");
|
||||||
|
if (irisPlugin == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pluginClassLoader = irisPlugin.getClass().getClassLoader();
|
||||||
|
generatorType = Class.forName("art.arcane.iris.platform.generation.PlatformChunkGenerator", true, pluginClassLoader);
|
||||||
|
stagingType = Class.forName("art.arcane.iris.world.lifecycle.WorldLifecycleStaging", true, pluginClassLoader);
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object generator;
|
||||||
|
try {
|
||||||
|
generator = stagingType
|
||||||
|
.getDeclaredMethod("peekStemGenerator", String.class)
|
||||||
|
.invoke(null, levelId);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
throw new RuntimeException("Iris failed to inspect the staged world generator",
|
||||||
|
e instanceof InvocationTargetException ex ? ex.getCause() : e);
|
||||||
|
}
|
||||||
|
if (generator == null || !generatorType.isInstance(generator)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Object target = generatorType.getMethod("getTarget").invoke(generator);
|
||||||
|
if (target == null) {
|
||||||
|
throw new IllegalStateException("Iris generator has no engine target.");
|
||||||
|
}
|
||||||
|
Object world = target.getClass().getMethod("getWorld").invoke(target);
|
||||||
|
if (world == null) {
|
||||||
|
throw new IllegalStateException("Iris generator target has no world identity.");
|
||||||
|
}
|
||||||
|
Object rawIdentity = world.getClass().getMethod("identity").invoke(world);
|
||||||
|
String worldIdentity = rawIdentity == null ? "" : rawIdentity.toString().trim();
|
||||||
|
Identifier worldIdentifier = Identifier.parse(worldIdentity);
|
||||||
|
if (!"iris".equals(worldIdentifier.getNamespace())
|
||||||
|
&& !"minecraft".equals(worldIdentifier.getNamespace())) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Iris generator has an unmanaged world identity: " + worldIdentity);
|
||||||
|
}
|
||||||
|
dimensionType = ResourceKey.create(Registries.LEVEL_STEM, worldIdentifier);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
throw new RuntimeException("Iris failed to bind the staged world storage identity",
|
||||||
|
e instanceof InvocationTargetException ex ? ex.getCause() : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ServerLevelAdvice {
|
||||||
|
@Advice.OnMethodEnter
|
||||||
|
static void enter(
|
||||||
|
@Advice.Argument(0) MinecraftServer server,
|
||||||
|
@Advice.Argument(value = 4, readOnly = false) ResourceKey<Level> dimensionKey,
|
||||||
|
@Advice.Argument(value = 5, readOnly = false) LevelStem levelStem,
|
||||||
|
@Advice.AllArguments Object[] constructorArguments
|
||||||
|
) {
|
||||||
|
if (dimensionKey == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// This advice is inlined into every ServerLevel construction on the server. Until a
|
||||||
|
// world is proven Iris-owned, every failure must fail OPEN (keep the vanilla stem):
|
||||||
|
// Iris being unloaded or half-loaded must never break other plugins' world creation.
|
||||||
|
String levelId;
|
||||||
|
ClassLoader pluginClassLoader;
|
||||||
|
Class<?> generatorType;
|
||||||
|
Class<?> stagingType;
|
||||||
|
try {
|
||||||
|
levelId = dimensionKey.identifier().getPath();
|
||||||
|
if (levelId == null || levelId.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Plugin irisPlugin = Bukkit.getPluginManager().getPlugin("Iris");
|
||||||
|
if (irisPlugin == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pluginClassLoader = irisPlugin.getClass().getClassLoader();
|
||||||
|
generatorType = Class.forName("art.arcane.iris.platform.generation.PlatformChunkGenerator", true, pluginClassLoader);
|
||||||
|
stagingType = Class.forName("art.arcane.iris.world.lifecycle.WorldLifecycleStaging", true, pluginClassLoader);
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
// Iris absent or half-loaded: fail OPEN for a world we cannot prove ours.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// From here Iris is present and its classes resolve; a failure resolving the
|
||||||
|
// staged generator must fail LOUD — silently handing a possibly-staged Iris world
|
||||||
|
// the vanilla stem corrupts generation.
|
||||||
|
ChunkGenerator gen = null;
|
||||||
|
try {
|
||||||
|
ChunkGenerator constructorGenerator = null;
|
||||||
|
for (Object argument : constructorArguments) {
|
||||||
|
if (argument instanceof ChunkGenerator candidate) {
|
||||||
|
constructorGenerator = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Object generator = generatorType.isInstance(constructorGenerator) ? constructorGenerator : null;
|
||||||
|
if (generator == null) {
|
||||||
|
generator = stagingType
|
||||||
|
.getDeclaredMethod("consumeStemGenerator", String.class)
|
||||||
|
.invoke(null, levelId);
|
||||||
|
}
|
||||||
|
if (generator instanceof ChunkGenerator owned && owned.getClass().getPackageName().startsWith("art.arcane.iris")) {
|
||||||
|
gen = owned;
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
throw new RuntimeException("Iris failed to resolve the staged world generator",
|
||||||
|
e instanceof InvocationTargetException ex ? ex.getCause() : e);
|
||||||
|
}
|
||||||
|
if (gen == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Past the ownership gate the world is Iris-owned; silently handing back the
|
||||||
|
// vanilla stem would corrupt generation, so failures from here rethrow.
|
||||||
|
try {
|
||||||
|
Object bindings = Class.forName("art.arcane.iris.platform.bukkit.nms.INMS", true, pluginClassLoader)
|
||||||
|
.getDeclaredMethod("get")
|
||||||
|
.invoke(null);
|
||||||
|
if (bindings == null) {
|
||||||
|
throw new IllegalStateException("Iris failed to resolve an INMSBinding instance.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Method stemMethod = null;
|
||||||
|
for (Method candidate : bindings.getClass().getMethods()) {
|
||||||
|
if (candidate.getName().equals("createRuntimeLevelStem") && candidate.getParameterCount() == 2) {
|
||||||
|
stemMethod = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stemMethod == null) {
|
||||||
|
throw new IllegalStateException("Iris binding is missing createRuntimeLevelStem.");
|
||||||
|
}
|
||||||
|
Object target = generatorType.getMethod("getTarget").invoke(gen);
|
||||||
|
if (target == null) {
|
||||||
|
throw new IllegalStateException("Iris generator has no engine target.");
|
||||||
|
}
|
||||||
|
Object world = target.getClass().getMethod("getWorld").invoke(target);
|
||||||
|
if (world == null) {
|
||||||
|
throw new IllegalStateException("Iris generator target has no world identity.");
|
||||||
|
}
|
||||||
|
Object rawIdentity = world.getClass().getMethod("identity").invoke(world);
|
||||||
|
String worldIdentity = rawIdentity == null ? "" : rawIdentity.toString().trim();
|
||||||
|
Identifier worldIdentifier = Identifier.parse(worldIdentity);
|
||||||
|
if (!"iris".equals(worldIdentifier.getNamespace())
|
||||||
|
&& !"minecraft".equals(worldIdentifier.getNamespace())) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Iris generator has an unmanaged world identity: " + worldIdentity);
|
||||||
|
}
|
||||||
|
Object resolvedStem = stemMethod.invoke(bindings, server.registryAccess(), gen);
|
||||||
|
if (!(resolvedStem instanceof LevelStem runtimeStem)) {
|
||||||
|
throw new IllegalStateException("Iris runtime LevelStem binding returned " + (resolvedStem == null ? "null" : resolvedStem.getClass().getName()) + ".");
|
||||||
|
}
|
||||||
|
dimensionKey = ResourceKey.create(Registries.DIMENSION, worldIdentifier);
|
||||||
|
levelStem = runtimeStem;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
throw new RuntimeException("Iris failed to replace the levelStem", e instanceof InvocationTargetException ex ? ex.getCause() : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.nativegen.v26_3_R1.NativeStructureGenerationKeys;
|
||||||
|
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.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.biome.BiomeSource;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
final class VanillaStructureBiomes {
|
||||||
|
private VanillaStructureBiomes() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static Set<String> possibleBiomeKeys(BiomeSource source) {
|
||||||
|
if (source == null) {
|
||||||
|
throw new IllegalStateException("Minecraft chunk generator has no biome source");
|
||||||
|
}
|
||||||
|
Set<String> keys = new LinkedHashSet<>();
|
||||||
|
Set<Holder<Biome>> possibleBiomes = source instanceof CustomBiomeSource customBiomeSource
|
||||||
|
? customBiomeSource.possibleStructureBiomes()
|
||||||
|
: source.possibleBiomes();
|
||||||
|
for (Holder<Biome> holder : possibleBiomes) {
|
||||||
|
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
|
||||||
|
if (key.isPresent()) {
|
||||||
|
keys.add(key.get().identifier().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (keys.isEmpty()) {
|
||||||
|
throw new IllegalStateException("Minecraft biome source exposes no registered possible biomes");
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Set<String> structureBiomeKeys(RegistryAccess access, String structureKey) {
|
||||||
|
if (access == null) {
|
||||||
|
throw new IllegalStateException("Minecraft registry access is unavailable");
|
||||||
|
}
|
||||||
|
if (structureKey == null || structureKey.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("Registered structure key must not be blank");
|
||||||
|
}
|
||||||
|
Set<String> keys = new LinkedHashSet<>();
|
||||||
|
Registry<Structure> registry = access.lookupOrThrow(Registries.STRUCTURE);
|
||||||
|
Structure structure = registry.getValue(Identifier.parse(structureKey));
|
||||||
|
if (structure == null) {
|
||||||
|
throw new IllegalArgumentException("Registered structure does not exist: " + structureKey);
|
||||||
|
}
|
||||||
|
boolean hasFilterEntries = false;
|
||||||
|
for (Holder<Biome> holder : structure.biomes()) {
|
||||||
|
hasFilterEntries = true;
|
||||||
|
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
|
||||||
|
if (key.isPresent()) {
|
||||||
|
keys.add(key.get().identifier().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// An empty biome filter is legal datapack content (opt-in structures whose tags only
|
||||||
|
// reference absent modded biomes); the structure is unreachable, not an error state.
|
||||||
|
if (keys.isEmpty() && hasFilterEntries) {
|
||||||
|
throw new IllegalStateException("Registered structure '" + structureKey
|
||||||
|
+ "' has biome filter entries but none resolve to registered biome keys");
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Set<String> reachableStructureKeys(ServerLevel level, BiomeSource source) {
|
||||||
|
if (level == null) {
|
||||||
|
throw new IllegalStateException("Minecraft server level is unavailable");
|
||||||
|
}
|
||||||
|
Set<String> possible = possibleBiomeKeys(source);
|
||||||
|
return NativeStructureGenerationKeys.reachable(level, possible);
|
||||||
|
}
|
||||||
|
}
|
||||||
+131
@@ -0,0 +1,131 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.platform.bukkit.nms.v26_3_R1.IrisChunkGenerator;
|
||||||
|
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
||||||
|
import art.arcane.iris.world.history.BoundaryColumnGeometry;
|
||||||
|
import art.arcane.iris.world.history.TerrainBoundarySignature;
|
||||||
|
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.OptionalInt;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public final class IrisChunkGeneratorHeightCacheTest {
|
||||||
|
@Rule
|
||||||
|
public final PlatformLeakGuard leakGuard = PlatformLeakGuard.clean();
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrap() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
NativeBlockTags.bindHeightmapFixtures();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void repeatedNativeHeightSurvivesResolvedTerrainEvictionAndRuntimeChange() throws Exception {
|
||||||
|
Fixture fixture = fixture();
|
||||||
|
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
|
||||||
|
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||||
|
when(fixture.complex().resolvedTerrainColumn(-1, -33))
|
||||||
|
.thenThrow(new IllegalStateException("Full terrain was evicted"));
|
||||||
|
|
||||||
|
for (int index = 0; index < 100; index++) {
|
||||||
|
assertEquals(-11, fixture.generator().getBaseHeight(
|
||||||
|
-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||||
|
}
|
||||||
|
verify(fixture.complex(), times(1)).resolvedTerrainColumn(-1, -33);
|
||||||
|
|
||||||
|
when(fixture.engine().getCacheID()).thenReturn(2);
|
||||||
|
doReturn(Optional.of(signature())).when(fixture.complex()).resolvedTerrainColumn(-1, -33);
|
||||||
|
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||||
|
verify(fixture.complex(), times(2)).resolvedTerrainColumn(-1, -33);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void ordinaryHeightRemainsLiveUntilResolvedTransitionTerrainExists() throws Exception {
|
||||||
|
Fixture fixture = fixture();
|
||||||
|
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
|
||||||
|
when(fixture.complex().resolvedTerrainColumn(0, 0))
|
||||||
|
.thenReturn(Optional.empty(), Optional.empty(), Optional.of(signature()));
|
||||||
|
when(fixture.engine().getHeight(0, 0, false)).thenReturn(20, 25);
|
||||||
|
|
||||||
|
assertEquals(5, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||||
|
assertEquals(10, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||||
|
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||||
|
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||||
|
verify(fixture.engine(), times(2)).getHeight(0, 0, false);
|
||||||
|
verify(fixture.complex(), times(3)).resolvedTerrainColumn(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void nativePredicatesAndAccessorBoundsRemainExact() throws Exception {
|
||||||
|
Fixture fixture = fixture();
|
||||||
|
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
|
||||||
|
for (int repeat = 0; repeat < 2; repeat++) {
|
||||||
|
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||||
|
assertEquals(-13, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null));
|
||||||
|
assertEquals(-13, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING, bounds, null));
|
||||||
|
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, bounds, null));
|
||||||
|
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE,
|
||||||
|
LevelHeightAccessor.create(-16, 2), null));
|
||||||
|
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES,
|
||||||
|
LevelHeightAccessor.create(-14, 3), null));
|
||||||
|
}
|
||||||
|
verify(fixture.complex(), times(6)).resolvedTerrainColumn(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Fixture fixture() throws Exception {
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
IrisComplex complex = mock(IrisComplex.class);
|
||||||
|
when(engine.getComplex()).thenReturn(complex);
|
||||||
|
when(engine.getCacheID()).thenReturn(1);
|
||||||
|
when(engine.acquireGenerationLease(anyString())).thenReturn(GenerationSessionLease.noop());
|
||||||
|
when(complex.resolvedTerrainColumn(anyInt(), anyInt())).thenReturn(Optional.of(signature()));
|
||||||
|
IrisChunkGenerator generator = mock(IrisChunkGenerator.class, CALLS_REAL_METHODS);
|
||||||
|
set(generator, "engine", engine);
|
||||||
|
set(generator, "terrainHeights", new NativeTerrainHeightCache());
|
||||||
|
return new Fixture(generator, engine, complex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void set(IrisChunkGenerator generator, String name, Object value) throws Exception {
|
||||||
|
Field field = IrisChunkGenerator.class.getDeclaredField(name);
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(generator, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TerrainBoundarySignature signature() {
|
||||||
|
BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-16, List.of(
|
||||||
|
new BoundaryColumnGeometry.Voxel("minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false),
|
||||||
|
new BoundaryColumnGeometry.Voxel("minecraft:water", BoundaryColumnGeometry.Phase.FLUID, "minecraft:water", false),
|
||||||
|
new BoundaryColumnGeometry.Voxel("minecraft:oak_leaves[persistent=true]", BoundaryColumnGeometry.Phase.SOLID, "", false),
|
||||||
|
new BoundaryColumnGeometry.Voxel("minecraft:short_grass", BoundaryColumnGeometry.Phase.SOLID, "", false),
|
||||||
|
new BoundaryColumnGeometry.Voxel("minecraft:light[level=15]", BoundaryColumnGeometry.Phase.SOLID, "", false)));
|
||||||
|
return new TerrainBoundarySignature(
|
||||||
|
new TerrainBoundarySignature.Column(0, 0, 4, 4, OptionalInt.of(1), OptionalInt.empty()),
|
||||||
|
new TerrainBoundarySignature.Samples(new TerrainBoundarySignature.VerticalLayout(-16, 5, 1),
|
||||||
|
new TerrainBoundarySignature.BiomeEncoding(List.of("minecraft:plains"), new short[]{0})), geometry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Fixture(IrisChunkGenerator generator, Engine engine, IrisComplex complex) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+125
@@ -0,0 +1,125 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.runtime.DimensionStackContext;
|
||||||
|
import art.arcane.iris.generation.runtime.DimensionStackLayout;
|
||||||
|
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||||
|
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||||
|
import art.arcane.iris.world.history.GenerationActivation;
|
||||||
|
import art.arcane.iris.world.history.GenerationHistory;
|
||||||
|
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.structure.object.IrisStaticObjectLayer;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public final class NativeGenerationWriteGuardTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrap() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void staticObjectsProtectRelativeHeightBeforeStackSampling() {
|
||||||
|
IrisStaticObjectLayer objects = mock(IrisStaticObjectLayer.class);
|
||||||
|
DimensionStackContext stack = mock(DimensionStackContext.class);
|
||||||
|
when(objects.contains(-3, 74, -7)).thenReturn(true);
|
||||||
|
|
||||||
|
Predicate<BlockPos> protectedPositions = NativeGenerationWriteGuard.protectedPositions(objects, stack, -64);
|
||||||
|
|
||||||
|
assertTrue(protectedPositions.test(new BlockPos(-3, 10, -7)));
|
||||||
|
verifyNoInteractions(stack);
|
||||||
|
assertFalse(NativeGenerationWriteGuard.protectedPositions(objects, null, -64)
|
||||||
|
.test(new BlockPos(-3, 11, -7)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void emptyStaticLayerSkipsContainsAndCachesEachSignedColumn() {
|
||||||
|
IrisStaticObjectLayer objects = mock(IrisStaticObjectLayer.class);
|
||||||
|
DimensionStackContext stack = mock(DimensionStackContext.class);
|
||||||
|
DimensionStackLayout negative = mock(DimensionStackLayout.class);
|
||||||
|
DimensionStackLayout positive = mock(DimensionStackLayout.class);
|
||||||
|
when(objects.isEmpty()).thenReturn(true);
|
||||||
|
when(stack.sample(-1, -1)).thenReturn(negative);
|
||||||
|
when(stack.sample(-1, 1)).thenReturn(positive);
|
||||||
|
when(negative.isHostFeatureProtectedY(64)).thenReturn(true);
|
||||||
|
when(positive.isHostFeatureProtectedY(65)).thenReturn(true);
|
||||||
|
|
||||||
|
Predicate<BlockPos> protectedPositions = NativeGenerationWriteGuard.protectedPositions(objects, stack, -64);
|
||||||
|
|
||||||
|
assertTrue(protectedPositions.test(new BlockPos(-1, 0, -1)));
|
||||||
|
assertFalse(protectedPositions.test(new BlockPos(-1, 1, -1)));
|
||||||
|
assertTrue(protectedPositions.test(new BlockPos(-1, 1, 1)));
|
||||||
|
verify(stack, times(1)).sample(-1, -1);
|
||||||
|
verify(stack, times(1)).sample(-1, 1);
|
||||||
|
verify(objects, never()).contains(anyInt(), anyInt(), anyInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void preTerrainSavedStampDistinguishesEarlierNativeStarts() {
|
||||||
|
IrisEngine engine = mock(IrisEngine.class);
|
||||||
|
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
|
||||||
|
GenerationHistory history = mock(GenerationHistory.class);
|
||||||
|
GenerationActivation active = mock(GenerationActivation.class);
|
||||||
|
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
|
||||||
|
when(router.history()).thenReturn(history);
|
||||||
|
when(history.activeActivation()).thenReturn(active);
|
||||||
|
when(active.activationId()).thenReturn(4L);
|
||||||
|
assertTrue(NativeGenerationWriteGuard.isHistoricalStructure(engine, 3));
|
||||||
|
assertFalse(NativeGenerationWriteGuard.isHistoricalStructure(engine, 0));
|
||||||
|
assertFalse(NativeGenerationWriteGuard.isHistoricalStructure(engine, 4));
|
||||||
|
assertFalse(NativeGenerationWriteGuard.isHistoricalStructure(engine, 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void partialNeighborsMayFinishButCompletedHistoricalNeighborRejectsPass() {
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
IrisComplex complex = mock(IrisComplex.class);
|
||||||
|
when(engine.getComplex()).thenReturn(complex);
|
||||||
|
WorldGenLevel region = mock(WorldGenLevel.class);
|
||||||
|
ChunkAccess partial = mock(ChunkAccess.class);
|
||||||
|
when(partial.getPersistedStatus()).thenReturn(ChunkStatus.TERRAIN);
|
||||||
|
when(region.getChunk(anyInt(), anyInt())).thenReturn(partial);
|
||||||
|
assertTrue(NativeGenerationWriteGuard.allowsDecoration(engine, region, new ChunkPos(0, 0)));
|
||||||
|
ChunkAccess complete = mock(ChunkAccess.class);
|
||||||
|
when(complete.getPersistedStatus()).thenReturn(ChunkStatus.FULL);
|
||||||
|
when(region.getChunk(1, -1)).thenReturn(complete);
|
||||||
|
assertFalse(NativeGenerationWriteGuard.allowsDecoration(engine, region, new ChunkPos(0, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void historicalNoiseMayFinishFeaturesButNeverReplaysNoiseOrCompletedStage() {
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
IrisComplex complex = mock(IrisComplex.class);
|
||||||
|
when(engine.getComplex()).thenReturn(complex);
|
||||||
|
ChunkAccess chunk = mock(ChunkAccess.class);
|
||||||
|
when(chunk.getPos()).thenReturn(new ChunkPos(-2, 3));
|
||||||
|
when(chunk.getPersistedStatus()).thenReturn(ChunkStatus.TERRAIN);
|
||||||
|
assertTrue(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.FEATURES));
|
||||||
|
assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.TERRAIN));
|
||||||
|
when(chunk.getPersistedStatus()).thenReturn(ChunkStatus.FEATURES);
|
||||||
|
assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.FEATURES));
|
||||||
|
when(chunk.getPersistedStatus()).thenReturn(ChunkStatus.FULL);
|
||||||
|
assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.SPAWN));
|
||||||
|
}
|
||||||
|
}
|
||||||
+462
@@ -0,0 +1,462 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||||
|
import java.util.Set;
|
||||||
|
import art.arcane.iris.structure.jigsaw.IrisJigsawConfiguration;
|
||||||
|
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||||
|
import com.mojang.datafixers.util.Either;
|
||||||
|
import com.mojang.datafixers.util.Pair;
|
||||||
|
import com.mojang.serialization.MapCodec;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.HolderOwner;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.core.Vec3i;
|
||||||
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
import net.minecraft.nbt.IntTag;
|
||||||
|
import net.minecraft.nbt.ListTag;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.server.level.WorldGenRegion;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
|
import net.minecraft.world.level.NoiseColumn;
|
||||||
|
import net.minecraft.world.level.StructureManager;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.biome.BiomeManager;
|
||||||
|
import net.minecraft.world.level.biome.FixedBiomeSource;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.Rotation;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import net.minecraft.world.level.levelgen.RandomState;
|
||||||
|
import net.minecraft.world.level.levelgen.blending.Blender;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.EmptyPoolElement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElementType;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.alias.PoolAliasBinding;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNotSame;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class NativeStructureFactoryTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraftRegistries() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void genericSourceDoesNotRequireJigsawType() {
|
||||||
|
Structure source = new SwampHutStructure(
|
||||||
|
new Structure.StructureSettings(HolderSet.empty()));
|
||||||
|
|
||||||
|
Structure configured = NativeStructureFactory.configure(
|
||||||
|
null, source, null, false, 80);
|
||||||
|
|
||||||
|
assertSame(source, configured);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void jigsawOptionsFailClosedForNonJigsawSource() {
|
||||||
|
Structure source = new SwampHutStructure(
|
||||||
|
new Structure.StructureSettings(HolderSet.empty()));
|
||||||
|
|
||||||
|
assertThrows(IllegalStateException.class, () -> NativeStructureFactory.configure(
|
||||||
|
null, source, new IrisJigsawConfiguration(), false, 80));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sourceMetadataReadsScalarAndAxisSpecificJigsawRanges() {
|
||||||
|
CompoundTag scalar = new CompoundTag();
|
||||||
|
scalar.putInt("max_distance_from_center", 80);
|
||||||
|
CompoundTag axisSpecific = new CompoundTag();
|
||||||
|
CompoundTag distance = new CompoundTag();
|
||||||
|
distance.putInt("horizontal", 96);
|
||||||
|
distance.putInt("vertical", 64);
|
||||||
|
axisSpecific.put("max_distance_from_center", distance);
|
||||||
|
|
||||||
|
assertEquals(80, NativeStructureFactory.sourceDistance(scalar, "horizontal"));
|
||||||
|
assertEquals(80, NativeStructureFactory.sourceDistance(scalar, "vertical"));
|
||||||
|
assertEquals(96, NativeStructureFactory.sourceDistance(axisSpecific, "horizontal"));
|
||||||
|
assertEquals(64, NativeStructureFactory.sourceDistance(axisSpecific, "vertical"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sourceMetadataIncludesAdjustBoundingBoxExpansion() {
|
||||||
|
Structure none = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty()));
|
||||||
|
Structure beardBox = new SwampHutStructure(new Structure.StructureSettings.Builder(HolderSet.empty())
|
||||||
|
.terrainAdapation(TerrainAdjustment.BEARD_BOX)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
assertEquals(0, NativeStructureFactory.horizontalReferenceExpansion(none));
|
||||||
|
assertEquals(12, NativeStructureFactory.horizontalReferenceExpansion(beardBox));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void templatePoolSpanIncludesWeightedListsFallbacksAndRotations() {
|
||||||
|
StructurePoolElement narrow = inlineElement(30, 8, 70);
|
||||||
|
StructurePoolElement wide = inlineElement(96, 8, 12);
|
||||||
|
ListPoolElement list = new ListPoolElement(
|
||||||
|
List.of(narrow, wide, EmptyPoolElement.INSTANCE),
|
||||||
|
StructureTemplatePool.Projection.RIGID);
|
||||||
|
BoundPoolHolder fallbackHolder = new BoundPoolHolder("test:fallback");
|
||||||
|
StructureTemplatePool fallback = new StructureTemplatePool(
|
||||||
|
fallbackHolder,
|
||||||
|
List.of(Pair.of(inlineElement(110, 8, 20), 1)));
|
||||||
|
fallbackHolder.bind(fallback);
|
||||||
|
BoundPoolHolder startHolder = new BoundPoolHolder("test:start");
|
||||||
|
StructureTemplatePool start = new StructureTemplatePool(
|
||||||
|
fallbackHolder,
|
||||||
|
List.of(Pair.of(list, 5), Pair.of(EmptyPoolElement.INSTANCE, 1)));
|
||||||
|
startHolder.bind(start);
|
||||||
|
|
||||||
|
assertEquals(110, NativeStructureTemplatePoolBounds.horizontalSpan(start, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void oversizedInlineStartTemplateReportsItsFullSpan() {
|
||||||
|
BoundPoolHolder holder = new BoundPoolHolder("test:oversized");
|
||||||
|
StructureTemplatePool pool = new StructureTemplatePool(
|
||||||
|
holder,
|
||||||
|
List.of(Pair.of(inlineElement(160, 8, 9), 1)));
|
||||||
|
holder.bind(pool);
|
||||||
|
|
||||||
|
assertEquals(160, NativeStructureTemplatePoolBounds.horizontalSpan(pool, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sourceSpanIncludesOnlyAliasesThatCanReplaceTheStartPool() {
|
||||||
|
BoundPoolHolder emptyHolder = new BoundPoolHolder("test:empty");
|
||||||
|
StructureTemplatePool empty = new StructureTemplatePool(emptyHolder, List.of());
|
||||||
|
emptyHolder.bind(empty);
|
||||||
|
BoundPoolHolder startHolder = new BoundPoolHolder("test:start");
|
||||||
|
StructureTemplatePool start = new StructureTemplatePool(
|
||||||
|
emptyHolder, List.of(Pair.of(inlineElement(20, 8, 12), 1)));
|
||||||
|
startHolder.bind(start);
|
||||||
|
BoundPoolHolder replacementHolder = new BoundPoolHolder("test:replacement");
|
||||||
|
StructureTemplatePool replacement = new StructureTemplatePool(
|
||||||
|
emptyHolder, List.of(Pair.of(inlineElement(30, 8, 12), 1)));
|
||||||
|
replacementHolder.bind(replacement);
|
||||||
|
BoundPoolHolder unrelatedHolder = new BoundPoolHolder("test:unrelated");
|
||||||
|
StructureTemplatePool unrelated = new StructureTemplatePool(
|
||||||
|
emptyHolder, List.of(Pair.of(inlineElement(200, 8, 12), 1)));
|
||||||
|
unrelatedHolder.bind(unrelated);
|
||||||
|
ResourceKey<StructureTemplatePool> startKey = startHolder.unwrapKey().orElseThrow();
|
||||||
|
ResourceKey<StructureTemplatePool> replacementKey = replacementHolder.unwrapKey().orElseThrow();
|
||||||
|
ResourceKey<StructureTemplatePool> unrelatedKey = unrelatedHolder.unwrapKey().orElseThrow();
|
||||||
|
ResourceKey<StructureTemplatePool> childAlias = ResourceKey.create(
|
||||||
|
Registries.TEMPLATE_POOL, Identifier.parse("test:child_alias"));
|
||||||
|
List<PoolAliasBinding> aliases = List.of(
|
||||||
|
PoolAliasBinding.direct(startKey, replacementKey),
|
||||||
|
PoolAliasBinding.direct(childAlias, unrelatedKey));
|
||||||
|
Map<ResourceKey<StructureTemplatePool>, StructureTemplatePool> pools = Map.of(
|
||||||
|
replacementKey, replacement,
|
||||||
|
unrelatedKey, unrelated);
|
||||||
|
|
||||||
|
assertEquals(30, NativeStructureTemplatePoolBounds.sourceHorizontalSpan(
|
||||||
|
null, start, startKey, aliases, pools::get));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sourceStartSpanDoesNotIncludeItsFallbackPool() {
|
||||||
|
BoundPoolHolder fallbackHolder = new BoundPoolHolder("test:large_fallback");
|
||||||
|
StructureTemplatePool fallback = new StructureTemplatePool(
|
||||||
|
fallbackHolder, List.of(Pair.of(inlineElement(180, 8, 10), 1)));
|
||||||
|
fallbackHolder.bind(fallback);
|
||||||
|
BoundPoolHolder startHolder = new BoundPoolHolder("test:small_start");
|
||||||
|
StructureTemplatePool start = new StructureTemplatePool(
|
||||||
|
fallbackHolder, List.of(Pair.of(inlineElement(24, 8, 10), 1)));
|
||||||
|
startHolder.bind(start);
|
||||||
|
|
||||||
|
assertEquals(180, NativeStructureTemplatePoolBounds.horizontalSpan(start, null));
|
||||||
|
assertEquals(24, NativeStructureTemplatePoolBounds.sourceHorizontalSpan(
|
||||||
|
null, start, startHolder.unwrapKey().orElseThrow(), List.of(), ignored -> null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void customPoolElementUsesItsBoundingBoxCapability() {
|
||||||
|
BoundPoolHolder fallbackHolder = new BoundPoolHolder("test:custom_fallback");
|
||||||
|
StructureTemplatePool fallback = new StructureTemplatePool(fallbackHolder, List.of());
|
||||||
|
fallbackHolder.bind(fallback);
|
||||||
|
BoundPoolHolder poolHolder = new BoundPoolHolder("test:custom");
|
||||||
|
StructureTemplatePool pool = new StructureTemplatePool(
|
||||||
|
fallbackHolder, List.of(Pair.of(new BoundingPoolElement(37, 11), 1)));
|
||||||
|
poolHolder.bind(pool);
|
||||||
|
|
||||||
|
assertEquals(37, NativeStructureTemplatePoolBounds.horizontalSpan(pool, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void forcedGeneratorBypassesBiomeEligibilityWithoutFabricatingTerrain() {
|
||||||
|
ChunkGenerator delegate = new TestChunkGenerator();
|
||||||
|
Holder<Biome> biome = Holder.direct((Biome) null);
|
||||||
|
LevelHeightAccessor heightAccessor = LevelHeightAccessor.create(-64, 384);
|
||||||
|
ForcedStructureChunkGenerator generator = new ForcedStructureChunkGenerator(
|
||||||
|
delegate, biome);
|
||||||
|
|
||||||
|
assertEquals(63, generator.getSeaLevel());
|
||||||
|
assertEquals(64, generator.getBaseHeight(
|
||||||
|
0, 0, Heightmap.Types.WORLD_SURFACE_WG, heightAccessor, null));
|
||||||
|
NoiseColumn column = generator.getBaseColumn(
|
||||||
|
0, 0, heightAccessor, null);
|
||||||
|
assertSame(Blocks.STONE, column.getBlock(80).getBlock());
|
||||||
|
assertSame(Blocks.STONE, column.getBlock(81).getBlock());
|
||||||
|
assertNotSame(delegate.getBiomeSource(), generator.getBiomeSource());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void undergroundRelocationMovesGenericPiecesAndReturnsFreshStart() {
|
||||||
|
Structure source = new SwampHutStructure(
|
||||||
|
new Structure.StructureSettings(HolderSet.empty()));
|
||||||
|
SwampHutPiece piece = new SwampHutPiece(RandomSource.create(17L), 0, 0);
|
||||||
|
StructureStart start = new StructureStart(
|
||||||
|
source,
|
||||||
|
new ChunkPos(0, 0),
|
||||||
|
0,
|
||||||
|
new PiecesContainer(List.of(piece))
|
||||||
|
);
|
||||||
|
|
||||||
|
StructureStart relocated = NativeStructureVerticalPlacer.relocateToMinY(
|
||||||
|
start, source, -20, LevelHeightAccessor.create(-64, 384));
|
||||||
|
|
||||||
|
assertNotSame(start, relocated);
|
||||||
|
assertEquals(-20, relocated.getPieces().getFirst().getBoundingBox().minY());
|
||||||
|
assertEquals(-20, relocated.getBoundingBox().minY());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void invalidGeneratedStartIsANormalSkippedCandidate() {
|
||||||
|
Structure source = new SwampHutStructure(
|
||||||
|
new Structure.StructureSettings(HolderSet.empty()));
|
||||||
|
StructureStart valid = new StructureStart(
|
||||||
|
source,
|
||||||
|
new ChunkPos(0, 0),
|
||||||
|
0,
|
||||||
|
new PiecesContainer(List.of(
|
||||||
|
new SwampHutPiece(RandomSource.create(23L), 0, 0)))
|
||||||
|
);
|
||||||
|
|
||||||
|
assertFalse(NativeStructureStartInjector.isUsableGeneratedStart(
|
||||||
|
StructureStart.INVALID_START));
|
||||||
|
assertFalse(NativeStructureStartInjector.isUsableGeneratedStart(null));
|
||||||
|
assertFalse(NativeStructureStartInjector.isUsableGeneratedStart(
|
||||||
|
new StructureStart(source, new ChunkPos(0, 0), 0,
|
||||||
|
new PiecesContainer(List.of()))));
|
||||||
|
assertTrue(NativeStructureStartInjector.isUsableGeneratedStart(valid));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unrepresentableGeneratedContentBecomesAnUnpublishedInvalidStart() {
|
||||||
|
Structure source = new SwampHutStructure(
|
||||||
|
new Structure.StructureSettings(HolderSet.empty()));
|
||||||
|
SwampHutPiece boundaryPiece = new SwampHutPiece(RandomSource.create(29L), 0, 0);
|
||||||
|
boundaryPiece.move(143 - boundaryPiece.getBoundingBox().maxX(), 0, 0);
|
||||||
|
StructureStart boundary = new StructureStart(
|
||||||
|
source,
|
||||||
|
new ChunkPos(0, 0),
|
||||||
|
0,
|
||||||
|
new PiecesContainer(List.of(boundaryPiece))
|
||||||
|
);
|
||||||
|
SwampHutPiece oversizedPiece = new SwampHutPiece(RandomSource.create(31L), 0, 0);
|
||||||
|
oversizedPiece.move(144 - oversizedPiece.getBoundingBox().maxX(), 0, 0);
|
||||||
|
StructureStart oversized = new StructureStart(
|
||||||
|
source,
|
||||||
|
new ChunkPos(0, 0),
|
||||||
|
0,
|
||||||
|
new PiecesContainer(List.of(oversizedPiece))
|
||||||
|
);
|
||||||
|
|
||||||
|
StructureStart boundaryResult = NativeStructureReferenceEnvelope.wrapForPublication(
|
||||||
|
boundary, source, 0, new IrisStructureTerrain(), "test:boundary_content");
|
||||||
|
|
||||||
|
StructureStart result = NativeStructureReferenceEnvelope.wrapForPublication(
|
||||||
|
oversized, source, 0,
|
||||||
|
new IrisStructureTerrain(),
|
||||||
|
"test:oversized_content");
|
||||||
|
|
||||||
|
assertTrue(boundaryResult.isValid());
|
||||||
|
assertSame(StructureStart.INVALID_START, result);
|
||||||
|
assertFalse(NativeStructureStartInjector.isUsableGeneratedStart(result));
|
||||||
|
assertEquals(1, oversized.getPieces().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class TestChunkGenerator extends ChunkGenerator {
|
||||||
|
private TestChunkGenerator() {
|
||||||
|
super(new FixedBiomeSource(Holder.direct((Biome) null)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected MapCodec<? extends ChunkGenerator> codec() {
|
||||||
|
return MapCodec.unit(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void spawnOriginalMobs(WorldGenRegion worldGenRegion) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getGenDepth() {
|
||||||
|
return 384;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<ChunkAccess> buildTerrain(
|
||||||
|
ChunkAccess centerChunk, Blender blender, RandomState randomState,
|
||||||
|
StructureManager structureManager, BiomeManager biomeManager, WorldGenRegion region, Set<Holder<Biome>> possibleBiomes) {
|
||||||
|
return CompletableFuture.completedFuture(centerChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSeaLevel() {
|
||||||
|
return 63;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getMinY() {
|
||||||
|
return -64;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getBaseHeight(int x, int z, Heightmap.Types type,
|
||||||
|
LevelHeightAccessor heightAccessor, RandomState randomState) {
|
||||||
|
return 64;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor heightAccessor,
|
||||||
|
RandomState randomState) {
|
||||||
|
BlockState[] states = new BlockState[heightAccessor.getHeight()];
|
||||||
|
for (int index = 0; index < states.length; index++) {
|
||||||
|
states[index] = Blocks.STONE.defaultBlockState();
|
||||||
|
}
|
||||||
|
return new NoiseColumn(heightAccessor.getMinY(), states);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addDebugScreenInfo(List<String> result, RandomState randomState,
|
||||||
|
BlockPos feetPos, SamplerContext samplerContext) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructurePoolElement inlineElement(int sizeX, int sizeY, int sizeZ) {
|
||||||
|
StructureTemplate template = new StructureTemplate();
|
||||||
|
CompoundTag tag = new CompoundTag();
|
||||||
|
ListTag size = new ListTag();
|
||||||
|
size.add(IntTag.valueOf(sizeX));
|
||||||
|
size.add(IntTag.valueOf(sizeY));
|
||||||
|
size.add(IntTag.valueOf(sizeZ));
|
||||||
|
tag.put("size", size);
|
||||||
|
template.load(BuiltInRegistries.BLOCK, tag);
|
||||||
|
return new InlinePoolElement(template);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class InlinePoolElement extends SinglePoolElement {
|
||||||
|
private final StructureTemplate inlineTemplate;
|
||||||
|
|
||||||
|
private InlinePoolElement(StructureTemplate template) {
|
||||||
|
super(Either.right(template), Holder.direct(new StructureProcessorList(List.of())),
|
||||||
|
StructureTemplatePool.Projection.RIGID, Optional.<LiquidSettings>empty());
|
||||||
|
inlineTemplate = template;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BoundingBox getBoundingBox(StructureTemplateManager templateManager,
|
||||||
|
BlockPos position, Rotation rotation) {
|
||||||
|
return inlineTemplate.getBoundingBox(
|
||||||
|
new StructurePlaceSettings().setRotation(rotation), position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class BoundingPoolElement extends StructurePoolElement {
|
||||||
|
private final int width;
|
||||||
|
private final int depth;
|
||||||
|
|
||||||
|
private BoundingPoolElement(int width, int depth) {
|
||||||
|
super(StructureTemplatePool.Projection.RIGID);
|
||||||
|
this.width = width;
|
||||||
|
this.depth = depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Vec3i getSize(StructureTemplateManager templateManager, Rotation rotation) {
|
||||||
|
return new Vec3i(width, 1, depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<StructureTemplate.JigsawBlockInfo> getShuffledJigsawBlocks(
|
||||||
|
StructureTemplateManager templateManager, BlockPos position,
|
||||||
|
Rotation rotation, RandomSource random) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BoundingBox getBoundingBox(StructureTemplateManager templateManager,
|
||||||
|
BlockPos position, Rotation rotation) {
|
||||||
|
return new BoundingBox(
|
||||||
|
position.getX(), position.getY(), position.getZ(),
|
||||||
|
position.getX() + width - 1, position.getY(),
|
||||||
|
position.getZ() + depth - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean place(StructureTemplateManager templateManager,
|
||||||
|
WorldGenLevel world,
|
||||||
|
StructureManager structureManager, ChunkGenerator chunkGenerator,
|
||||||
|
BlockPos position, BlockPos pivot, Rotation rotation,
|
||||||
|
BoundingBox area, RandomSource random,
|
||||||
|
LiquidSettings liquidSettings, boolean keepJigsaws) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StructurePoolElementType<?> getType() {
|
||||||
|
return StructurePoolElementType.EMPTY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class BoundPoolHolder extends Holder.Reference<StructureTemplatePool> {
|
||||||
|
private BoundPoolHolder(String key) {
|
||||||
|
super(Type.STAND_ALONE, new HolderOwner<>() {
|
||||||
|
}, ResourceKey.create(Registries.TEMPLATE_POOL, Identifier.parse(key)), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bind(StructureTemplatePool pool) {
|
||||||
|
bindValue(pool);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import net.minecraft.world.level.levelgen.structure.placement.AbstractSpreadingStructurePlacement;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.Vec3i;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
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.placement.StructurePlacement;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class NativeStructureGenerationKeysContractTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraft() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void frequencyGateUsesMappedAccessorAndRejectsNonPositivePlacements() {
|
||||||
|
assertFalse(NativeStructureGenerationKeys.isEnabledPlacement(placement(0.0F)));
|
||||||
|
assertFalse(NativeStructureGenerationKeys.isEnabledPlacement(placement(-0.1F)));
|
||||||
|
assertFalse(NativeStructureGenerationKeys.isEnabledPlacement(placement(Float.NaN)));
|
||||||
|
assertTrue(NativeStructureGenerationKeys.isEnabledPlacement(placement(0.25F)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RandomSpreadStructurePlacement placement(float frequency) {
|
||||||
|
return new RandomSpreadStructurePlacement(
|
||||||
|
Vec3i.ZERO,
|
||||||
|
AbstractSpreadingStructurePlacement.FrequencyReductionMethod.DEFAULT,
|
||||||
|
frequency,
|
||||||
|
1,
|
||||||
|
Optional.empty(),
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
RandomSpreadType.LINEAR);
|
||||||
|
}
|
||||||
|
}
|
||||||
+197
@@ -0,0 +1,197 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class NativeStructureLocatePersistenceTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraftRegistries() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void storageProbeScansEachCandidateOnlyOnce() {
|
||||||
|
AtomicInteger reads = new AtomicInteger();
|
||||||
|
CompoundTag chunk = new CompoundTag();
|
||||||
|
chunk.putInt("DataVersion", SharedConstants.getCurrentVersion().dataVersion().version());
|
||||||
|
chunk.putString("Status", "full");
|
||||||
|
CompoundTag start = new CompoundTag();
|
||||||
|
start.putString("id", "minecraft:village_plains");
|
||||||
|
start.putInt("references", 0);
|
||||||
|
CompoundTag starts = new CompoundTag();
|
||||||
|
starts.put("minecraft:village_plains", start);
|
||||||
|
CompoundTag structures = new CompoundTag();
|
||||||
|
structures.put("starts", starts);
|
||||||
|
chunk.put("structures", structures);
|
||||||
|
NativeStructureLocatePersistence.ProbeBudget budget =
|
||||||
|
new NativeStructureLocatePersistence.ProbeBudget(
|
||||||
|
512,
|
||||||
|
(chunkPos, visitor) -> {
|
||||||
|
reads.incrementAndGet();
|
||||||
|
chunk.acceptAsRoot(visitor);
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
});
|
||||||
|
ChunkPos chunkPos = new ChunkPos(7, -11);
|
||||||
|
|
||||||
|
assertTrue(budget.acceptsStored(
|
||||||
|
null, chunkPos, "minecraft:village_plains", true));
|
||||||
|
assertTrue(budget.acceptsStored(
|
||||||
|
null, chunkPos, "minecraft:village_plains", true));
|
||||||
|
assertEquals(1, reads.get());
|
||||||
|
assertEquals(1, budget.used());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void legacyStoredChunkIsDatafixedBeforeItsStartIsClassified() {
|
||||||
|
AtomicBoolean datafixed = new AtomicBoolean();
|
||||||
|
CompoundTag legacy = new CompoundTag();
|
||||||
|
legacy.putInt("DataVersion", 100);
|
||||||
|
legacy.putString("Status", "full");
|
||||||
|
NativeStructureLocatePersistence.ProbeBudget budget =
|
||||||
|
new NativeStructureLocatePersistence.ProbeBudget(
|
||||||
|
512,
|
||||||
|
(chunkPos, visitor) -> {
|
||||||
|
legacy.acceptAsRoot(visitor);
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
},
|
||||||
|
(level, storedChunk) -> {
|
||||||
|
datafixed.set(true);
|
||||||
|
CompoundTag fixed = storedChunk.copy();
|
||||||
|
fixed.putInt("DataVersion",
|
||||||
|
SharedConstants.getCurrentVersion().dataVersion().version());
|
||||||
|
CompoundTag start = new CompoundTag();
|
||||||
|
start.putString("id", "minecraft:village_plains");
|
||||||
|
start.putInt("references", 1);
|
||||||
|
CompoundTag starts = new CompoundTag();
|
||||||
|
starts.put("minecraft:village_plains", start);
|
||||||
|
CompoundTag structures = new CompoundTag();
|
||||||
|
structures.put("starts", starts);
|
||||||
|
fixed.put("structures", structures);
|
||||||
|
return fixed;
|
||||||
|
});
|
||||||
|
|
||||||
|
assertFalse(budget.acceptsStored(
|
||||||
|
null, new ChunkPos(3, 4), "minecraft:village_plains", true));
|
||||||
|
assertTrue(datafixed.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void partialDatafixFailureFallsBackToSelectedChunkVerification() {
|
||||||
|
CompoundTag legacy = new CompoundTag();
|
||||||
|
legacy.putInt("DataVersion", 100);
|
||||||
|
NativeStructureLocatePersistence.ProbeBudget budget =
|
||||||
|
new NativeStructureLocatePersistence.ProbeBudget(
|
||||||
|
512,
|
||||||
|
(chunkPos, visitor) -> {
|
||||||
|
legacy.acceptAsRoot(visitor);
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
},
|
||||||
|
(level, storedChunk) -> {
|
||||||
|
throw new IllegalStateException("broken partial datafix");
|
||||||
|
});
|
||||||
|
|
||||||
|
assertTrue(budget.acceptsStored(
|
||||||
|
null, new ChunkPos(9, -2), "minecraft:village_plains", true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void missingStartsContainerFallsBackToSelectedChunkVerification() {
|
||||||
|
CompoundTag chunk = currentChunk();
|
||||||
|
NativeStructureLocatePersistence.ProbeBudget budget = budget(chunk);
|
||||||
|
|
||||||
|
assertTrue(budget.acceptsStored(
|
||||||
|
null, new ChunkPos(5, 8), "minecraft:village_plains", false));
|
||||||
|
assertTrue(budget.acceptsStored(
|
||||||
|
null, new ChunkPos(5, 8), "minecraft:village_plains", true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void presentEmptyStartsContainerRejectsTheCandidateWithoutLoading() {
|
||||||
|
CompoundTag chunk = currentChunk();
|
||||||
|
CompoundTag structures = new CompoundTag();
|
||||||
|
structures.put("starts", new CompoundTag());
|
||||||
|
chunk.put("structures", structures);
|
||||||
|
NativeStructureLocatePersistence.ProbeBudget budget = budget(chunk);
|
||||||
|
|
||||||
|
assertFalse(budget.acceptsStored(
|
||||||
|
null, new ChunkPos(-3, 2), "minecraft:village_plains", false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void collisionTombstoneIsRejectedForNormalAndUnexploredLocate() {
|
||||||
|
CompoundTag chunk = currentChunk();
|
||||||
|
CompoundTag tombstone = new CompoundTag();
|
||||||
|
tombstone.putString("id", "INVALID");
|
||||||
|
CompoundTag starts = new CompoundTag();
|
||||||
|
starts.put("minecraft:village_plains", tombstone);
|
||||||
|
CompoundTag structures = new CompoundTag();
|
||||||
|
structures.put("starts", starts);
|
||||||
|
chunk.put("structures", structures);
|
||||||
|
ChunkPos chunkPos = new ChunkPos(12, -9);
|
||||||
|
|
||||||
|
assertFalse(budget(chunk).acceptsStored(
|
||||||
|
null, chunkPos, "minecraft:village_plains", false));
|
||||||
|
assertFalse(budget(chunk).acceptsStored(
|
||||||
|
null, chunkPos, "minecraft:village_plains", true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void malformedNonCompoundStartIsRejectedWithoutLoading() {
|
||||||
|
CompoundTag chunk = currentChunk();
|
||||||
|
CompoundTag starts = new CompoundTag();
|
||||||
|
starts.putInt("minecraft:village_plains", 1);
|
||||||
|
CompoundTag structures = new CompoundTag();
|
||||||
|
structures.put("starts", starts);
|
||||||
|
chunk.put("structures", structures);
|
||||||
|
|
||||||
|
assertFalse(budget(chunk).acceptsStored(
|
||||||
|
null, new ChunkPos(1, 1), "minecraft:village_plains", false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void referencedStoredStartIsOnlyRejectedForUnexploredLocate() {
|
||||||
|
CompoundTag chunk = currentChunk();
|
||||||
|
CompoundTag start = new CompoundTag();
|
||||||
|
start.putString("id", "minecraft:village_plains");
|
||||||
|
start.putInt("references", 1);
|
||||||
|
CompoundTag starts = new CompoundTag();
|
||||||
|
starts.put("minecraft:village_plains", start);
|
||||||
|
CompoundTag structures = new CompoundTag();
|
||||||
|
structures.put("starts", starts);
|
||||||
|
chunk.put("structures", structures);
|
||||||
|
ChunkPos chunkPos = new ChunkPos(-4, -7);
|
||||||
|
|
||||||
|
assertTrue(budget(chunk).acceptsStored(
|
||||||
|
null, chunkPos, "minecraft:village_plains", false));
|
||||||
|
assertFalse(budget(chunk).acceptsStored(
|
||||||
|
null, chunkPos, "minecraft:village_plains", true));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CompoundTag currentChunk() {
|
||||||
|
CompoundTag chunk = new CompoundTag();
|
||||||
|
chunk.putInt("DataVersion", SharedConstants.getCurrentVersion().dataVersion().version());
|
||||||
|
return chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NativeStructureLocatePersistence.ProbeBudget budget(CompoundTag chunk) {
|
||||||
|
return new NativeStructureLocatePersistence.ProbeBudget(
|
||||||
|
512,
|
||||||
|
(chunkPos, visitor) -> {
|
||||||
|
chunk.acceptAsRoot(visitor);
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+346
@@ -0,0 +1,346 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
|
||||||
|
import art.arcane.iris.structure.nativegen.IrisNativeStructure;
|
||||||
|
import art.arcane.iris.structure.placement.IrisStructurePlacement;
|
||||||
|
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||||
|
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.core.Vec3i;
|
||||||
|
import net.minecraft.nbt.ListTag;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.StructureManager;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.block.Rotation;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
|
import net.minecraft.world.level.levelgen.LegacyRandomSource;
|
||||||
|
import net.minecraft.world.level.levelgen.RandomSupport;
|
||||||
|
import net.minecraft.world.level.levelgen.WorldgenRandom;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElementType;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class NativeStructureOwnershipFingerprintTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraftRegistries() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void monumentFingerprintSurvivesSourceYRegenerationWithoutSyntheticPieces() {
|
||||||
|
long seed = 8372619L;
|
||||||
|
ChunkPos origin = new ChunkPos(9, -4);
|
||||||
|
OceanMonumentStructure structure = new OceanMonumentStructure(
|
||||||
|
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
|
||||||
|
StructureStart generated = monumentStart(structure, origin, seed);
|
||||||
|
NativeStructureVerticalPlacer.alignOceanMonumentToSeaLevel(
|
||||||
|
generated, 0, 80, -64, 320);
|
||||||
|
int alignedLocatorY = NativeStructureOwnershipFingerprint.locatorY(generated);
|
||||||
|
IrisStructureTerrain terrain = new IrisStructureTerrain()
|
||||||
|
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
|
||||||
|
.setHorizontalPadding(24);
|
||||||
|
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
|
||||||
|
generated,
|
||||||
|
structure,
|
||||||
|
0,
|
||||||
|
terrain);
|
||||||
|
String beforeReload = NativeStructureOwnershipFingerprint.fingerprint(
|
||||||
|
"minecraft:monument", wrapped);
|
||||||
|
|
||||||
|
ListTag serializedPieces = (ListTag) new PiecesContainer(wrapped.getPieces()).save(null);
|
||||||
|
PiecesContainer loadedPieces = PiecesContainer.load(serializedPieces, null);
|
||||||
|
assertEquals(1, loadedPieces.pieces().size());
|
||||||
|
assertTrue(loadedPieces.pieces().getFirst()
|
||||||
|
instanceof OceanMonumentPieces.MonumentBuilding);
|
||||||
|
PiecesContainer regeneratedPieces = OceanMonumentStructure.regeneratePiecesAfterLoad(
|
||||||
|
origin, seed, loadedPieces);
|
||||||
|
StructureStart reloaded = new StructureStart(structure, origin, 0, regeneratedPieces);
|
||||||
|
String afterReload = NativeStructureOwnershipFingerprint.fingerprint(
|
||||||
|
"minecraft:monument", reloaded);
|
||||||
|
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture(
|
||||||
|
"minecraft:monument",
|
||||||
|
wrapped,
|
||||||
|
plan(origin, NativeStructureReferenceEnvelope.contentBounds(wrapped).minY()),
|
||||||
|
NativeStructureReferenceEnvelope.referenceBounds(wrapped, structure, terrain));
|
||||||
|
|
||||||
|
assertEquals(beforeReload, afterReload);
|
||||||
|
assertTrue(NativeStructureOwnershipFingerprint.matches(ownership, reloaded));
|
||||||
|
assertNotEquals(alignedLocatorY, NativeStructureOwnershipFingerprint.locatorY(reloaded));
|
||||||
|
assertEquals(alignedLocatorY, ownership.locatorY());
|
||||||
|
assertEquals(1, reloaded.getPieces().size());
|
||||||
|
assertEquals(1, wrapped.getPieces().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sameKeyAndOriginStillRequiresMatchingContentIdentity() {
|
||||||
|
long seed = 18273L;
|
||||||
|
ChunkPos origin = new ChunkPos(2, 3);
|
||||||
|
OceanMonumentStructure structure = new OceanMonumentStructure(
|
||||||
|
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
|
||||||
|
StructureStart expected = monumentStart(structure, origin, seed);
|
||||||
|
StructureStart moved = monumentStart(structure, origin, seed);
|
||||||
|
for (StructurePiece piece : moved.getPieces()) {
|
||||||
|
piece.move(1, 0, 0);
|
||||||
|
}
|
||||||
|
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture(
|
||||||
|
"minecraft:monument",
|
||||||
|
expected,
|
||||||
|
plan(origin, NativeStructureReferenceEnvelope.contentBounds(expected).minY()),
|
||||||
|
expected.getBoundingBox());
|
||||||
|
|
||||||
|
assertFalse(NativeStructureOwnershipFingerprint.matches(ownership, moved));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void persistedEnvelopeRepairsReloadedMonumentReferenceOutsideLiveBounds() {
|
||||||
|
long seed = 772931L;
|
||||||
|
ChunkPos origin = new ChunkPos(4, -7);
|
||||||
|
OceanMonumentStructure structure = new OceanMonumentStructure(
|
||||||
|
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
|
||||||
|
StructureStart generated = monumentStart(structure, origin, seed);
|
||||||
|
NativeStructureVerticalPlacer.alignOceanMonumentToSeaLevel(
|
||||||
|
generated, 0, 80, -64, 320);
|
||||||
|
IrisStructureTerrain terrain = new IrisStructureTerrain()
|
||||||
|
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
|
||||||
|
.setHorizontalPadding(24);
|
||||||
|
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
|
||||||
|
generated,
|
||||||
|
structure,
|
||||||
|
0,
|
||||||
|
terrain);
|
||||||
|
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture(
|
||||||
|
"minecraft:monument",
|
||||||
|
wrapped,
|
||||||
|
plan(origin, NativeStructureReferenceEnvelope.contentBounds(wrapped).minY()),
|
||||||
|
NativeStructureReferenceEnvelope.referenceBounds(wrapped, structure, terrain));
|
||||||
|
PiecesContainer regeneratedPieces = OceanMonumentStructure.regeneratePiecesAfterLoad(
|
||||||
|
origin, seed, new PiecesContainer(wrapped.getPieces()));
|
||||||
|
StructureStart reloaded = new StructureStart(structure, origin, 0, regeneratedPieces);
|
||||||
|
ChunkPos target = outsideLiveBounds(ownership, reloaded);
|
||||||
|
|
||||||
|
assertNotNull(target);
|
||||||
|
assertTrue(NativeStructureReferenceRepair.requiresReference(
|
||||||
|
target, "minecraft:monument", reloaded, ownership));
|
||||||
|
assertFalse(NativeStructureReferenceRepair.requiresReference(
|
||||||
|
target, "minecraft:monument", reloaded, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void overlappingManualStartsSixteenChunksApartRemainIndependentlyReferenced() {
|
||||||
|
OceanMonumentStructure structure = new OceanMonumentStructure(
|
||||||
|
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
|
||||||
|
ChunkPos westOrigin = new ChunkPos(0, 0);
|
||||||
|
ChunkPos eastOrigin = new ChunkPos(16, 0);
|
||||||
|
StructureStart west = monumentStart(structure, westOrigin, 317L);
|
||||||
|
StructureStart east = monumentStart(structure, eastOrigin, 941L);
|
||||||
|
west.getPieces().getFirst().move(112, 0, 0);
|
||||||
|
east.getPieces().getFirst().move(-96, 0, 0);
|
||||||
|
NativeStructureOwnershipRecord westOwnership = ownership(westOrigin, west, structure);
|
||||||
|
NativeStructureOwnershipRecord eastOwnership = ownership(eastOrigin, east, structure);
|
||||||
|
ChunkPos shared = new ChunkPos(8, 0);
|
||||||
|
|
||||||
|
assertTrue(NativeStructureReferenceEnvelope.contentBounds(west).intersects(
|
||||||
|
NativeStructureReferenceEnvelope.contentBounds(east)));
|
||||||
|
assertTrue(NativeStructureReferenceRepair.requiresReference(
|
||||||
|
shared, "minecraft:monument", west, westOwnership));
|
||||||
|
assertTrue(NativeStructureReferenceRepair.requiresReference(
|
||||||
|
shared, "minecraft:monument", east, eastOwnership));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void denseAdjacentManualStartsDoNotSuppressEachOther() {
|
||||||
|
OceanMonumentStructure structure = new OceanMonumentStructure(
|
||||||
|
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
|
||||||
|
ChunkPos firstOrigin = new ChunkPos(0, 0);
|
||||||
|
ChunkPos secondOrigin = new ChunkPos(1, 0);
|
||||||
|
StructureStart first = monumentStart(structure, firstOrigin, 17L);
|
||||||
|
StructureStart second = monumentStart(structure, secondOrigin, 23L);
|
||||||
|
NativeStructureOwnershipRecord firstOwnership = ownership(firstOrigin, first, structure);
|
||||||
|
NativeStructureOwnershipRecord secondOwnership = ownership(secondOrigin, second, structure);
|
||||||
|
ChunkPos shared = new ChunkPos(0, 0);
|
||||||
|
|
||||||
|
assertTrue(NativeStructureReferenceEnvelope.contentBounds(first).intersects(
|
||||||
|
NativeStructureReferenceEnvelope.contentBounds(second)));
|
||||||
|
assertTrue(NativeStructureReferenceRepair.requiresReference(
|
||||||
|
shared, "minecraft:monument", first, firstOwnership));
|
||||||
|
assertTrue(NativeStructureReferenceRepair.requiresReference(
|
||||||
|
shared, "minecraft:monument", second, secondOwnership));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void customPoolElementIdentityDoesNotDependOnProcessLocalToString() {
|
||||||
|
OceanMonumentStructure structure = new OceanMonumentStructure(
|
||||||
|
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
|
||||||
|
ChunkPos origin = new ChunkPos(3, -2);
|
||||||
|
StructureStart first = poolStart(structure, origin, new IdentityStringPoolElement());
|
||||||
|
StructureStart reloaded = poolStart(structure, origin, new IdentityStringPoolElement());
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
NativeStructureOwnershipFingerprint.fingerprint("test:custom_pool", first),
|
||||||
|
NativeStructureOwnershipFingerprint.fingerprint("test:custom_pool", reloaded));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void namedPoolTemplateRemainsPartOfTheStableFingerprint() {
|
||||||
|
OceanMonumentStructure structure = new OceanMonumentStructure(
|
||||||
|
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
|
||||||
|
ChunkPos origin = new ChunkPos(3, -2);
|
||||||
|
StructurePoolElement firstElement = StructurePoolElement.single("test:first")
|
||||||
|
.apply(StructureTemplatePool.Projection.RIGID);
|
||||||
|
StructurePoolElement secondElement = StructurePoolElement.single("test:second")
|
||||||
|
.apply(StructureTemplatePool.Projection.RIGID);
|
||||||
|
|
||||||
|
assertNotEquals(
|
||||||
|
NativeStructureOwnershipFingerprint.fingerprint(
|
||||||
|
"test:named_pool", poolStart(structure, origin, firstElement)),
|
||||||
|
NativeStructureOwnershipFingerprint.fingerprint(
|
||||||
|
"test:named_pool", poolStart(structure, origin, secondElement)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureStart monumentStart(OceanMonumentStructure structure,
|
||||||
|
ChunkPos origin, long seed) {
|
||||||
|
WorldgenRandom random = new WorldgenRandom(
|
||||||
|
new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
|
||||||
|
random.setLargeFeatureSeed(seed, origin.x(), origin.z());
|
||||||
|
Direction orientation = Direction.Plane.HORIZONTAL.getRandomDirection(random);
|
||||||
|
OceanMonumentPieces.MonumentBuilding building = new OceanMonumentPieces.MonumentBuilding(
|
||||||
|
random,
|
||||||
|
origin.getMinBlockX() - 29,
|
||||||
|
origin.getMinBlockZ() - 29,
|
||||||
|
orientation
|
||||||
|
);
|
||||||
|
return new StructureStart(
|
||||||
|
structure,
|
||||||
|
origin,
|
||||||
|
0,
|
||||||
|
new PiecesContainer(List.of(building))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NativeStructureStartPlan plan(ChunkPos origin, int baseY) {
|
||||||
|
IrisNativeStructure source = new IrisNativeStructure()
|
||||||
|
.setStructure("minecraft:monument")
|
||||||
|
.setWeight(1);
|
||||||
|
IrisStructurePlacement placement = new IrisStructurePlacement()
|
||||||
|
.setPlacementId("monument-test")
|
||||||
|
.setNativeStructures(new KList<IrisNativeStructure>().qadd(source));
|
||||||
|
return new NativeStructureStartPlan(
|
||||||
|
placement,
|
||||||
|
source,
|
||||||
|
origin.x(),
|
||||||
|
origin.z(),
|
||||||
|
baseY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NativeStructureOwnershipRecord ownership(
|
||||||
|
ChunkPos origin, StructureStart start, OceanMonumentStructure structure) {
|
||||||
|
return NativeStructureOwnershipFingerprint.capture(
|
||||||
|
"minecraft:monument",
|
||||||
|
start,
|
||||||
|
plan(origin, NativeStructureReferenceEnvelope.contentBounds(start).minY()),
|
||||||
|
NativeStructureReferenceEnvelope.referenceBounds(
|
||||||
|
start, structure, new IrisStructureTerrain())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureStart poolStart(
|
||||||
|
OceanMonumentStructure structure, ChunkPos origin, StructurePoolElement element) {
|
||||||
|
BlockPos position = new BlockPos(
|
||||||
|
origin.getMiddleBlockX(), 64, origin.getMiddleBlockZ());
|
||||||
|
PoolElementStructurePiece piece = new PoolElementStructurePiece(
|
||||||
|
null, element, position, 0, Rotation.NONE,
|
||||||
|
new BoundingBox(position), LiquidSettings.APPLY_WATERLOGGING);
|
||||||
|
return new StructureStart(
|
||||||
|
structure, origin, 0, new PiecesContainer(List.of(piece)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class IdentityStringPoolElement extends StructurePoolElement {
|
||||||
|
private IdentityStringPoolElement() {
|
||||||
|
super(StructureTemplatePool.Projection.RIGID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Vec3i getSize(StructureTemplateManager templateManager, Rotation rotation) {
|
||||||
|
return new Vec3i(1, 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<StructureTemplate.JigsawBlockInfo> getShuffledJigsawBlocks(
|
||||||
|
StructureTemplateManager templateManager, BlockPos position,
|
||||||
|
Rotation rotation, RandomSource random) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BoundingBox getBoundingBox(StructureTemplateManager templateManager,
|
||||||
|
BlockPos position, Rotation rotation) {
|
||||||
|
return new BoundingBox(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean place(StructureTemplateManager templateManager,
|
||||||
|
WorldGenLevel world, StructureManager structureManager,
|
||||||
|
ChunkGenerator chunkGenerator, BlockPos position, BlockPos pivot,
|
||||||
|
Rotation rotation, BoundingBox area, RandomSource random,
|
||||||
|
LiquidSettings liquidSettings, boolean keepJigsaws) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StructurePoolElementType<?> getType() {
|
||||||
|
return StructurePoolElementType.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "IdentityStringPoolElement@"
|
||||||
|
+ Integer.toHexString(System.identityHashCode(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ChunkPos outsideLiveBounds(NativeStructureOwnershipRecord ownership,
|
||||||
|
StructureStart start) {
|
||||||
|
for (int chunkX = ownership.referenceMinChunkX();
|
||||||
|
chunkX <= ownership.referenceMaxChunkX(); chunkX++) {
|
||||||
|
for (int chunkZ = ownership.referenceMinChunkZ();
|
||||||
|
chunkZ <= ownership.referenceMaxChunkZ(); chunkZ++) {
|
||||||
|
ChunkPos candidate = new ChunkPos(chunkX, chunkZ);
|
||||||
|
if (!start.getBoundingBox().intersects(
|
||||||
|
candidate.getMinBlockX(), candidate.getMinBlockZ(),
|
||||||
|
candidate.getMaxBlockX(), candidate.getMaxBlockZ())) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+314
@@ -0,0 +1,314 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
|
||||||
|
import art.arcane.iris.structure.placement.StructurePlacementGrid;
|
||||||
|
import art.arcane.iris.structure.nativegen.IrisNativeStructure;
|
||||||
|
import art.arcane.iris.structure.placement.IrisStructurePlacement;
|
||||||
|
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||||
|
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.levelgen.LegacyRandomSource;
|
||||||
|
import net.minecraft.world.level.levelgen.RandomSupport;
|
||||||
|
import net.minecraft.world.level.levelgen.WorldgenRandom;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class NativeStructureOwnershipRecoveryTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraftRegistries() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void missingSidecarRecoversExactPolicyEnvelopeAndLocatorAfterReload() {
|
||||||
|
long seed = 5519231L;
|
||||||
|
ChunkPos origin = new ChunkPos(7, -5);
|
||||||
|
OceanMonumentStructure structure = structure();
|
||||||
|
StructureStart expected = monumentStart(structure, origin, seed);
|
||||||
|
NativeStructureVerticalPlacer.alignOceanMonumentToSeaLevel(
|
||||||
|
expected, 0, 80, -64, 320);
|
||||||
|
int expectedLocatorY = NativeStructureOwnershipFingerprint.locatorY(expected);
|
||||||
|
NativeStructureStartPlan plan = plan(origin, "recovery-exact", 24);
|
||||||
|
StructureStart reloaded = new StructureStart(
|
||||||
|
structure,
|
||||||
|
origin,
|
||||||
|
expected.getReferences(),
|
||||||
|
OceanMonumentStructure.regeneratePiecesAfterLoad(
|
||||||
|
origin, seed, new PiecesContainer(expected.getPieces()))
|
||||||
|
);
|
||||||
|
|
||||||
|
NativeStructureOwnershipRecord recovered =
|
||||||
|
NativeStructureOwnershipRecovery.proveCandidate(
|
||||||
|
"minecraft:monument", structure, reloaded,
|
||||||
|
plan, expected, false);
|
||||||
|
|
||||||
|
assertNotNull(recovered);
|
||||||
|
assertEquals(StructurePlacementGrid.placementIdentity(plan.placement()),
|
||||||
|
recovered.placementIdentity());
|
||||||
|
assertEquals(expectedLocatorY, recovered.locatorY());
|
||||||
|
assertNotEquals(NativeStructureOwnershipFingerprint.locatorY(reloaded),
|
||||||
|
recovered.locatorY());
|
||||||
|
assertEquals(IrisStructureTerrainMode.FORCE_CARVE,
|
||||||
|
recovered.restoredDecision().terrain().resolvedMode());
|
||||||
|
assertEquals(24, recovered.restoredDecision().terrain().getHorizontalPadding());
|
||||||
|
assertTrue(recovered.referenceMinChunkX()
|
||||||
|
< NativeStructureReferenceEnvelope.contentBounds(reloaded).minX() >> 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void naturalSameKeyStartRemainsUnownedWhenOriginIsAmbiguous() {
|
||||||
|
long seed = 22817L;
|
||||||
|
ChunkPos origin = new ChunkPos(-3, 6);
|
||||||
|
OceanMonumentStructure structure = structure();
|
||||||
|
StructureStart natural = monumentStart(structure, origin, seed);
|
||||||
|
NativeStructureStartPlan plan = plan(origin, "natural-ambiguous", 0);
|
||||||
|
|
||||||
|
NativeStructureOwnershipRecord recovered =
|
||||||
|
NativeStructureOwnershipRecovery.proveCandidate(
|
||||||
|
"minecraft:monument", structure, natural,
|
||||||
|
plan, natural, true);
|
||||||
|
|
||||||
|
assertNull(recovered);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void changedPlacementIdentityOrGeometryCannotRecoverOwnership() {
|
||||||
|
long seed = 991723L;
|
||||||
|
ChunkPos origin = new ChunkPos(2, 4);
|
||||||
|
OceanMonumentStructure structure = structure();
|
||||||
|
StructureStart expected = monumentStart(structure, origin, seed);
|
||||||
|
StructureStart changedGeometry = monumentStart(structure, origin, seed);
|
||||||
|
for (StructurePiece piece : changedGeometry.getPieces()) {
|
||||||
|
piece.move(1, 0, 0);
|
||||||
|
}
|
||||||
|
NativeStructureStartPlan originalIdentity = plan(
|
||||||
|
origin, "original-identity", 0);
|
||||||
|
NativeStructureStartPlan changedIdentity = plan(
|
||||||
|
new ChunkPos(origin.x() + 1, origin.z()), "changed-identity", 0);
|
||||||
|
|
||||||
|
assertNotEquals(
|
||||||
|
StructurePlacementGrid.placementIdentity(originalIdentity.placement()),
|
||||||
|
StructurePlacementGrid.placementIdentity(changedIdentity.placement()));
|
||||||
|
assertNull(NativeStructureOwnershipRecovery.proveCandidate(
|
||||||
|
"minecraft:monument", structure, expected,
|
||||||
|
changedIdentity, expected, false));
|
||||||
|
assertNull(NativeStructureOwnershipRecovery.proveCandidate(
|
||||||
|
"minecraft:monument", structure, changedGeometry,
|
||||||
|
plan(origin, "geometry-check", 0), expected, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void recoveredEnvelopeStillRequiresExactFingerprint() {
|
||||||
|
long seed = 81337L;
|
||||||
|
ChunkPos origin = new ChunkPos(-8, -9);
|
||||||
|
OceanMonumentStructure structure = structure();
|
||||||
|
StructureStart expected = monumentStart(structure, origin, seed);
|
||||||
|
NativeStructureOwnershipRecord recovered =
|
||||||
|
NativeStructureOwnershipRecovery.proveCandidate(
|
||||||
|
"minecraft:monument", structure, expected,
|
||||||
|
plan(origin, "fingerprint-check", 16), expected, false);
|
||||||
|
assertNotNull(recovered);
|
||||||
|
|
||||||
|
StructureStart moved = monumentStart(structure, origin, seed);
|
||||||
|
for (StructurePiece piece : moved.getPieces()) {
|
||||||
|
piece.move(0, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFalse(NativeStructureOwnershipFingerprint.matches(recovered, moved));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void staleVacuumEnvelopeRefreshesWithoutReplacingOwnershipIdentity() {
|
||||||
|
String structureKey = "minecraft:monument";
|
||||||
|
long seed = 648231L;
|
||||||
|
ChunkPos origin = new ChunkPos(5, -6);
|
||||||
|
OceanMonumentStructure structure = structure();
|
||||||
|
StructureStart start = monumentStart(structure, origin, seed);
|
||||||
|
NativeStructureStartPlan plan = plan(
|
||||||
|
origin, "vacuum-envelope-refresh", IrisStructureTerrainMode.VACUUM, 0);
|
||||||
|
BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(start);
|
||||||
|
NativeStructureOwnershipRecord stale = NativeStructureOwnershipFingerprint.capture(
|
||||||
|
structureKey, start, plan, content);
|
||||||
|
|
||||||
|
NativeStructureOwnershipRecord refreshed =
|
||||||
|
NativeStructureOwnershipRecovery.refreshReferenceEnvelope(
|
||||||
|
structureKey, structure, start, stale);
|
||||||
|
|
||||||
|
assertNotNull(refreshed);
|
||||||
|
assertNotEquals(stale, refreshed);
|
||||||
|
assertEquals(stale.schema(), refreshed.schema());
|
||||||
|
assertEquals(stale.ownershipKey(), refreshed.ownershipKey());
|
||||||
|
assertEquals(stale.placementIdentity(), refreshed.placementIdentity());
|
||||||
|
assertEquals(stale.baseY(), refreshed.baseY());
|
||||||
|
assertEquals(stale.locatorY(), refreshed.locatorY());
|
||||||
|
assertEquals(stale.contentFingerprint(), refreshed.contentFingerprint());
|
||||||
|
assertEquals(stale.decision(), refreshed.decision());
|
||||||
|
assertEquals(IrisStructureTerrainMode.VACUUM,
|
||||||
|
refreshed.restoredDecision().terrain().resolvedMode());
|
||||||
|
BoundingBox expected = NativeStructureReferenceEnvelope.referenceBounds(
|
||||||
|
start, structure, plan.placement().resolvedTerrain(), structureKey);
|
||||||
|
assertEquals(expected.minX() >> 4, refreshed.referenceMinChunkX());
|
||||||
|
assertEquals(expected.maxX() >> 4, refreshed.referenceMaxChunkX());
|
||||||
|
assertEquals(expected.minZ() >> 4, refreshed.referenceMinChunkZ());
|
||||||
|
assertEquals(expected.maxZ() >> 4, refreshed.referenceMaxChunkZ());
|
||||||
|
assertTrue(hasExpandedCoverage(stale, refreshed));
|
||||||
|
assertSame(refreshed, NativeStructureOwnershipRecovery.refreshReferenceEnvelope(
|
||||||
|
structureKey, structure, start, refreshed));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void staleEnvelopeCannotRefreshAgainstDifferentContent() {
|
||||||
|
String structureKey = "minecraft:monument";
|
||||||
|
long seed = 412987L;
|
||||||
|
ChunkPos origin = new ChunkPos(-2, 7);
|
||||||
|
OceanMonumentStructure structure = structure();
|
||||||
|
StructureStart expected = monumentStart(structure, origin, seed);
|
||||||
|
NativeStructureStartPlan plan = plan(
|
||||||
|
origin, "vacuum-content-check", IrisStructureTerrainMode.VACUUM, 0);
|
||||||
|
NativeStructureOwnershipRecord stale = NativeStructureOwnershipFingerprint.capture(
|
||||||
|
structureKey, expected, plan,
|
||||||
|
NativeStructureReferenceEnvelope.contentBounds(expected));
|
||||||
|
StructureStart moved = monumentStart(structure, origin, seed);
|
||||||
|
for (StructurePiece piece : moved.getPieces()) {
|
||||||
|
piece.move(1, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNull(NativeStructureOwnershipRecovery.refreshReferenceEnvelope(
|
||||||
|
structureKey, structure, moved, stale));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void currentNonVacuumEnvelopeRemainsThePersistedAuthority() {
|
||||||
|
String structureKey = "minecraft:monument";
|
||||||
|
long seed = 927451L;
|
||||||
|
ChunkPos origin = new ChunkPos(3, 8);
|
||||||
|
OceanMonumentStructure structure = structure();
|
||||||
|
StructureStart start = monumentStart(structure, origin, seed);
|
||||||
|
NativeStructureStartPlan plan = plan(origin, "current-force-carve", 24);
|
||||||
|
BoundingBox envelope = NativeStructureReferenceEnvelope.referenceBounds(
|
||||||
|
start, structure, plan.placement().resolvedTerrain(), structureKey);
|
||||||
|
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture(
|
||||||
|
structureKey, start, plan, envelope);
|
||||||
|
|
||||||
|
assertSame(ownership, NativeStructureOwnershipRecovery.refreshReferenceEnvelope(
|
||||||
|
structureKey, structure, start, ownership));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void clippedVacuumEnvelopeRemainsStableAtTheReferenceLimit() {
|
||||||
|
String structureKey = "minecraft:monument";
|
||||||
|
long seed = 381729L;
|
||||||
|
ChunkPos origin = new ChunkPos(0, 0);
|
||||||
|
OceanMonumentStructure structure = structure();
|
||||||
|
StructureStart start = monumentStart(structure, origin, seed);
|
||||||
|
BoundingBox initial = NativeStructureReferenceEnvelope.contentBounds(start);
|
||||||
|
int maximumReferenceBlockX = ((origin.x()
|
||||||
|
+ NativeStructureOwnershipRecord.MAX_REFERENCE_DISTANCE_CHUNKS) << 4) + 15;
|
||||||
|
int shiftX = maximumReferenceBlockX - initial.maxX();
|
||||||
|
for (StructurePiece piece : start.getPieces()) {
|
||||||
|
piece.move(shiftX, 0, 0);
|
||||||
|
}
|
||||||
|
NativeStructureStartPlan plan = plan(
|
||||||
|
origin, "clipped-vacuum", IrisStructureTerrainMode.VACUUM, 0);
|
||||||
|
BoundingBox envelope = NativeStructureReferenceEnvelope.referenceBounds(
|
||||||
|
start, structure, plan.placement().resolvedTerrain(), structureKey);
|
||||||
|
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture(
|
||||||
|
structureKey, start, plan, envelope);
|
||||||
|
|
||||||
|
assertEquals(origin.x() + NativeStructureOwnershipRecord.MAX_REFERENCE_DISTANCE_CHUNKS,
|
||||||
|
ownership.referenceMaxChunkX());
|
||||||
|
assertSame(ownership, NativeStructureOwnershipRecovery.refreshReferenceEnvelope(
|
||||||
|
structureKey, structure, start, ownership));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OceanMonumentStructure structure() {
|
||||||
|
return new OceanMonumentStructure(
|
||||||
|
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureStart monumentStart(OceanMonumentStructure structure,
|
||||||
|
ChunkPos origin, long seed) {
|
||||||
|
WorldgenRandom random = new WorldgenRandom(
|
||||||
|
new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
|
||||||
|
random.setLargeFeatureSeed(seed, origin.x(), origin.z());
|
||||||
|
Direction orientation = Direction.Plane.HORIZONTAL.getRandomDirection(random);
|
||||||
|
OceanMonumentPieces.MonumentBuilding building =
|
||||||
|
new OceanMonumentPieces.MonumentBuilding(
|
||||||
|
random,
|
||||||
|
origin.getMinBlockX() - 29,
|
||||||
|
origin.getMinBlockZ() - 29,
|
||||||
|
orientation
|
||||||
|
);
|
||||||
|
return new StructureStart(
|
||||||
|
structure, origin, 0, new PiecesContainer(List.of(building)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NativeStructureStartPlan plan(ChunkPos origin,
|
||||||
|
String placementId,
|
||||||
|
int horizontalPadding) {
|
||||||
|
return plan(origin, placementId,
|
||||||
|
IrisStructureTerrainMode.FORCE_CARVE, horizontalPadding);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NativeStructureStartPlan plan(ChunkPos origin,
|
||||||
|
String placementId,
|
||||||
|
IrisStructureTerrainMode terrainMode,
|
||||||
|
int horizontalPadding) {
|
||||||
|
IrisNativeStructure source = new IrisNativeStructure()
|
||||||
|
.setStructure("minecraft:monument")
|
||||||
|
.setWeight(1);
|
||||||
|
IrisStructureTerrain terrain = new IrisStructureTerrain()
|
||||||
|
.setMode(terrainMode)
|
||||||
|
.setHorizontalPadding(horizontalPadding);
|
||||||
|
IrisStructurePlacement placement = new IrisStructurePlacement()
|
||||||
|
.setPlacementId(placementId)
|
||||||
|
.setTerrain(terrain)
|
||||||
|
.setNativeStructures(new KList<IrisNativeStructure>().qadd(source));
|
||||||
|
return new NativeStructureStartPlan(
|
||||||
|
placement,
|
||||||
|
source,
|
||||||
|
origin.x(),
|
||||||
|
origin.z(),
|
||||||
|
NativeStructureReferenceEnvelope.contentBounds(
|
||||||
|
monumentStart(structure(), origin, 1L)).minY()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasExpandedCoverage(
|
||||||
|
NativeStructureOwnershipRecord stale,
|
||||||
|
NativeStructureOwnershipRecord refreshed) {
|
||||||
|
for (int chunkX = refreshed.referenceMinChunkX();
|
||||||
|
chunkX <= refreshed.referenceMaxChunkX(); chunkX++) {
|
||||||
|
for (int chunkZ = refreshed.referenceMinChunkZ();
|
||||||
|
chunkZ <= refreshed.referenceMaxChunkZ(); chunkZ++) {
|
||||||
|
if (!stale.covers(chunkX, chunkZ)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
+532
@@ -0,0 +1,532 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
|
||||||
|
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||||
|
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
|
||||||
|
import art.arcane.iris.structure.placement.IrisStructureYBand;
|
||||||
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.Rotation;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.dimension.BuiltinDimensionTypes;
|
||||||
|
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.BlockIgnoreProcessor;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationHandler;
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
|
import java.util.BitSet;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class NativeStructurePostProcessorEncaseTest {
|
||||||
|
private static final long TEST_SEED = 1234L;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraft() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void encaseFillsAirAndLiquidWithoutOverwritingExistingTerrain() {
|
||||||
|
StructureStart start = start(TerrainAdjustment.BURY, 64);
|
||||||
|
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
|
||||||
|
BoundingBox area = new BoundingBox(
|
||||||
|
bounds.minX() - 1, bounds.minY() - 1, bounds.minZ() - 1,
|
||||||
|
bounds.maxX() + 1, bounds.maxY() + 1, bounds.maxZ() + 1);
|
||||||
|
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||||
|
put(blocks, bounds.minX(), bounds.minY(), bounds.minZ(), Blocks.DIRT.defaultBlockState());
|
||||||
|
put(blocks, bounds.maxX(), bounds.minY(), bounds.maxZ(), Blocks.WATER.defaultBlockState());
|
||||||
|
|
||||||
|
NativeStructureTerrainIntegrator.integrateTerrain(
|
||||||
|
world(blocks), area, "minecraft:stronghold", start,
|
||||||
|
new IrisStructureTerrain()
|
||||||
|
.setMode(IrisStructureTerrainMode.ENCASE)
|
||||||
|
.setHorizontalPadding(1)
|
||||||
|
.setCeilingPadding(1)
|
||||||
|
.setFloorPadding(1),
|
||||||
|
null);
|
||||||
|
|
||||||
|
assertEquals(Blocks.DIRT.defaultBlockState(),
|
||||||
|
state(blocks, bounds.minX(), bounds.minY(), bounds.minZ()));
|
||||||
|
assertEquals(Blocks.STONE.defaultBlockState(),
|
||||||
|
state(blocks, bounds.maxX(), bounds.minY(), bounds.maxZ()));
|
||||||
|
assertEquals(Blocks.STONE.defaultBlockState(),
|
||||||
|
state(blocks, bounds.minX() - 1, bounds.minY() - 1, bounds.minZ() - 1));
|
||||||
|
assertEquals(Blocks.STONE.defaultBlockState(),
|
||||||
|
state(blocks, bounds.maxX() + 1, bounds.maxY() + 1, bounds.maxZ() + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultEncasePaletteSplitsStoneAboveZeroAndDeepslateBelow() {
|
||||||
|
StructureStart start = start(TerrainAdjustment.BURY, -1);
|
||||||
|
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
|
||||||
|
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||||
|
|
||||||
|
NativeStructureTerrainIntegrator.integrateTerrain(
|
||||||
|
world(blocks), bounds, "minecraft:stronghold", start,
|
||||||
|
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.ENCASE),
|
||||||
|
null);
|
||||||
|
|
||||||
|
assertEquals(-1, bounds.minY());
|
||||||
|
assertEquals(Blocks.DEEPSLATE.defaultBlockState(),
|
||||||
|
state(blocks, bounds.minX(), -1, bounds.minZ()));
|
||||||
|
assertEquals(Blocks.STONE.defaultBlockState(),
|
||||||
|
state(blocks, bounds.minX(), 0, bounds.minZ()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultEncaseMaterialFollowsVanillaDimensionTerrain() {
|
||||||
|
assertEquals(Blocks.NETHERRACK.defaultBlockState(),
|
||||||
|
NativeStructureTerrainIntegrator.defaultEncaseBlock(Level.NETHER, 64));
|
||||||
|
assertEquals(Blocks.END_STONE.defaultBlockState(),
|
||||||
|
NativeStructureTerrainIntegrator.defaultEncaseBlock(Level.END, 64));
|
||||||
|
assertEquals(Blocks.STONE.defaultBlockState(),
|
||||||
|
NativeStructureTerrainIntegrator.defaultEncaseBlock(Level.OVERWORLD, 64));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sourceEncaseMaterialFollowsNearbyIrisTerrain() {
|
||||||
|
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||||
|
BlockPos fill = new BlockPos(4, 64, 8);
|
||||||
|
put(blocks, fill.getX() + 3, fill.getY(), fill.getZ(), Blocks.TUFF.defaultBlockState());
|
||||||
|
|
||||||
|
assertEquals(Blocks.TUFF.defaultBlockState(),
|
||||||
|
NativeStructureTerrainIntegrator.sourceEncaseBlock(world(blocks), fill));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sourceMaterialSnapshotIgnoresPreviouslyDecoratedAdjacentChunks() {
|
||||||
|
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||||
|
BlockPos fill = new BlockPos(0, 64, 8);
|
||||||
|
put(blocks, -1, 64, 8, Blocks.OAK_PLANKS.defaultBlockState());
|
||||||
|
put(blocks, 3, 64, 8, Blocks.TUFF.defaultBlockState());
|
||||||
|
WorldGenLevel world = world(blocks);
|
||||||
|
NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceTerrain =
|
||||||
|
sourceSnapshot(world, new BoundingBox(0, 64, 0, 15, 64, 15));
|
||||||
|
|
||||||
|
assertEquals(Blocks.TUFF.defaultBlockState(),
|
||||||
|
NativeStructureTerrainIntegrator.sourceEncaseBlock(
|
||||||
|
world, fill, sourceTerrain));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sourceMaterialSnapshotIsStableAcrossEarlierFillsInTheSameChunk() {
|
||||||
|
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||||
|
BlockPos fill = new BlockPos(0, 64, 8);
|
||||||
|
put(blocks, 3, 64, 8, Blocks.TUFF.defaultBlockState());
|
||||||
|
WorldGenLevel world = world(blocks);
|
||||||
|
NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceTerrain =
|
||||||
|
sourceSnapshot(world, new BoundingBox(0, 64, 0, 15, 64, 15));
|
||||||
|
put(blocks, 1, 64, 8, Blocks.OAK_PLANKS.defaultBlockState());
|
||||||
|
|
||||||
|
assertEquals(Blocks.TUFF.defaultBlockState(),
|
||||||
|
NativeStructureTerrainIntegrator.sourceEncaseBlock(
|
||||||
|
world, fill, sourceTerrain));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sourceMaterialRejectsStructureBlocksAndOresBeforeDimensionFallback() {
|
||||||
|
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||||
|
BlockPos fill = new BlockPos(4, -20, 8);
|
||||||
|
put(blocks, 3, -20, 8, Blocks.OAK_PLANKS.defaultBlockState());
|
||||||
|
put(blocks, 5, -20, 8, Blocks.STONE_BRICKS.defaultBlockState());
|
||||||
|
put(blocks, 4, -20, 7, Blocks.IRON_ORE.defaultBlockState());
|
||||||
|
|
||||||
|
assertEquals(Blocks.DEEPSLATE.defaultBlockState(),
|
||||||
|
NativeStructureTerrainIntegrator.sourceEncaseBlock(world(blocks), fill));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sourceMaterialSnapshotOnlyAllocatesActualFillLayers() {
|
||||||
|
StructureStart start = start(TerrainAdjustment.BURY, 64);
|
||||||
|
BoundingBox area = new BoundingBox(0, -2032, 0, 15, 2030, 15);
|
||||||
|
NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceTerrain =
|
||||||
|
NativeStructureTerrainIntegrator.captureSourceTerrain(
|
||||||
|
world(new HashMap<>()), area,
|
||||||
|
List.of(new NativeStructureTerrainIntegrator.TerrainTarget(
|
||||||
|
"test:bounded-snapshot", start,
|
||||||
|
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE))));
|
||||||
|
|
||||||
|
assertNotNull(sourceTerrain);
|
||||||
|
assertEquals(23 * 16 * 16, sourceTerrain.sampledCells());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void customDimensionKeyUsesRegisteredVanillaDimensionType() {
|
||||||
|
ResourceKey<Level> customDimension = ResourceKey.create(
|
||||||
|
Registries.DIMENSION, Identifier.parse("iris:custom_nether"));
|
||||||
|
|
||||||
|
assertEquals(Blocks.NETHERRACK.defaultBlockState(),
|
||||||
|
NativeStructureTerrainIntegrator.defaultEncaseBlock(
|
||||||
|
customDimension, BuiltinDimensionTypes.NETHER, null, 64));
|
||||||
|
assertEquals(Blocks.END_STONE.defaultBlockState(),
|
||||||
|
NativeStructureTerrainIntegrator.defaultEncaseBlock(
|
||||||
|
customDimension, BuiltinDimensionTypes.END, null, 64));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void configuredEncasePaletteReplacesTheDefaultMaterial() {
|
||||||
|
StructureStart start = start(TerrainAdjustment.BURY, 64);
|
||||||
|
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
|
||||||
|
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||||
|
IrisMaterialPalette palette = new IrisMaterialPalette().qclear().qadd("minecraft:tuff");
|
||||||
|
|
||||||
|
NativeStructureTerrainIntegrator.integrateTerrain(
|
||||||
|
world(blocks), bounds, "minecraft:stronghold", start,
|
||||||
|
new IrisStructureTerrain()
|
||||||
|
.setMode(IrisStructureTerrainMode.ENCASE)
|
||||||
|
.setEncasePalette(palette),
|
||||||
|
NativeStructurePostProcessorEncaseTest::tuffBlock);
|
||||||
|
|
||||||
|
assertEquals(Blocks.TUFF.defaultBlockState(),
|
||||||
|
state(blocks, bounds.minX(), bounds.minY(), bounds.minZ()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void omittedTerrainConfigurationRetainsSourceSemantics() {
|
||||||
|
for (TerrainAdjustment adjustment : TerrainAdjustment.values()) {
|
||||||
|
IrisStructureTerrain resolved = NativeStructureTerrainIntegrator.resolveNativeTerrain(
|
||||||
|
start(adjustment, 64), null);
|
||||||
|
|
||||||
|
assertEquals(IrisStructureTerrainMode.SOURCE, resolved.resolvedMode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sourceBuryUsesTheVanillaSixByTwelveInfluenceEnvelope() {
|
||||||
|
StructureStart start = start(TerrainAdjustment.BURY, 64);
|
||||||
|
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
|
||||||
|
int groundY = bounds.minY();
|
||||||
|
BoundingBox area = new BoundingBox(
|
||||||
|
bounds.minX() - 7, groundY - 13, bounds.minZ(),
|
||||||
|
bounds.maxX(), groundY + 13, bounds.maxZ());
|
||||||
|
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||||
|
|
||||||
|
NativeStructureTerrainIntegrator.integrateTerrain(
|
||||||
|
world(blocks), area, "test:bury", start,
|
||||||
|
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE), null);
|
||||||
|
|
||||||
|
assertEquals(Blocks.STONE.defaultBlockState(),
|
||||||
|
state(blocks, bounds.minX() - 5, groundY, bounds.minZ()));
|
||||||
|
assertEquals(Blocks.STONE.defaultBlockState(),
|
||||||
|
state(blocks, bounds.minX(), groundY + 11, bounds.minZ()));
|
||||||
|
assertEquals(Blocks.AIR.defaultBlockState(),
|
||||||
|
state(blocks, bounds.minX() - 6, groundY, bounds.minZ()));
|
||||||
|
assertEquals(Blocks.AIR.defaultBlockState(),
|
||||||
|
state(blocks, bounds.minX(), groundY + 12, bounds.minZ()));
|
||||||
|
assertEquals(Blocks.AIR.defaultBlockState(),
|
||||||
|
state(blocks, bounds.minX() - 7, groundY, bounds.minZ()));
|
||||||
|
assertEquals(Blocks.AIR.defaultBlockState(),
|
||||||
|
state(blocks, bounds.minX(), groundY + 13, bounds.minZ()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sourceEncapsulateWrapsTheFullPieceBounds() {
|
||||||
|
StructureStart start = start(TerrainAdjustment.ENCAPSULATE, 64);
|
||||||
|
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
|
||||||
|
BoundingBox area = new BoundingBox(
|
||||||
|
bounds.minX() - 13, bounds.minY(), bounds.minZ(),
|
||||||
|
bounds.maxX(), bounds.maxY() + 13, bounds.maxZ());
|
||||||
|
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||||
|
|
||||||
|
NativeStructureTerrainIntegrator.integrateTerrain(
|
||||||
|
world(blocks), area, "test:encapsulate", start,
|
||||||
|
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE), null);
|
||||||
|
|
||||||
|
assertEquals(Blocks.STONE.defaultBlockState(),
|
||||||
|
state(blocks, bounds.minX() - 11, bounds.minY(), bounds.minZ()));
|
||||||
|
assertEquals(Blocks.STONE.defaultBlockState(),
|
||||||
|
state(blocks, bounds.minX(), bounds.maxY() + 11, bounds.minZ()));
|
||||||
|
assertEquals(Blocks.AIR.defaultBlockState(),
|
||||||
|
state(blocks, bounds.minX() - 12, bounds.minY(), bounds.minZ()));
|
||||||
|
assertEquals(Blocks.AIR.defaultBlockState(),
|
||||||
|
state(blocks, bounds.minX(), bounds.maxY() + 12, bounds.minZ()));
|
||||||
|
assertEquals(Blocks.AIR.defaultBlockState(),
|
||||||
|
state(blocks, bounds.minX() - 13, bounds.minY(), bounds.minZ()));
|
||||||
|
assertEquals(Blocks.AIR.defaultBlockState(),
|
||||||
|
state(blocks, bounds.minX(), bounds.maxY() + 13, bounds.minZ()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sourceTerrainDensityOnlyUsesRigidPoolPieces() {
|
||||||
|
PoolElementStructurePiece terrainMatching = poolPiece(StructureTemplatePool.Projection.TERRAIN_MATCHING);
|
||||||
|
PoolElementStructurePiece rigid = poolPiece(StructureTemplatePool.Projection.RIGID);
|
||||||
|
|
||||||
|
assertFalse(NativeStructureTerrainIntegrator.isSourceRigidPiece(terrainMatching));
|
||||||
|
assertTrue(NativeStructureTerrainIntegrator.isSourceRigidPiece(rigid));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sourceJunctionTerrainUsesTheStrictLowerTwelveBlockEnvelope() {
|
||||||
|
assertTrue(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(0, -1, 0));
|
||||||
|
assertTrue(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(0, -11, 0));
|
||||||
|
assertFalse(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(0, 0, 0));
|
||||||
|
assertFalse(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(0, -12, 0));
|
||||||
|
assertFalse(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(12, -1, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void explicitTerrainConfigurationWinsOverAutoEncase() {
|
||||||
|
IrisStructureTerrain configured = new IrisStructureTerrain()
|
||||||
|
.setMode(IrisStructureTerrainMode.PRESERVE);
|
||||||
|
|
||||||
|
assertSame(configured, NativeStructureTerrainIntegrator.resolveNativeTerrain(
|
||||||
|
start(TerrainAdjustment.BURY, 64), configured));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void legacyTemplateAirIsClearedAfterEveryTerrainFillPath() {
|
||||||
|
IrisStructureTerrain source = new IrisStructureTerrain()
|
||||||
|
.setMode(IrisStructureTerrainMode.SOURCE);
|
||||||
|
IrisStructureTerrain preserve = new IrisStructureTerrain()
|
||||||
|
.setMode(IrisStructureTerrainMode.PRESERVE);
|
||||||
|
IrisStructureTerrain encase = new IrisStructureTerrain()
|
||||||
|
.setMode(IrisStructureTerrainMode.ENCASE);
|
||||||
|
|
||||||
|
assertTrue(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
|
||||||
|
start(TerrainAdjustment.BURY, 64), source));
|
||||||
|
assertTrue(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
|
||||||
|
start(TerrainAdjustment.BEARD_BOX, 64), source));
|
||||||
|
assertTrue(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
|
||||||
|
start(TerrainAdjustment.NONE, 64), encase));
|
||||||
|
assertFalse(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
|
||||||
|
start(TerrainAdjustment.NONE, 64), source));
|
||||||
|
assertFalse(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
|
||||||
|
start(TerrainAdjustment.BURY, 64), preserve));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void encaseReservesTheNeighborChunkEnvelope() {
|
||||||
|
StructureStart generated = start(TerrainAdjustment.BURY, 64);
|
||||||
|
BoundingBox sourceBounds = generated.getBoundingBox();
|
||||||
|
BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(generated);
|
||||||
|
IrisStructureTerrain terrain = new IrisStructureTerrain()
|
||||||
|
.setMode(IrisStructureTerrainMode.ENCASE)
|
||||||
|
.setHorizontalPadding(4);
|
||||||
|
|
||||||
|
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
|
||||||
|
generated,
|
||||||
|
generated.getStructure(),
|
||||||
|
0,
|
||||||
|
terrain);
|
||||||
|
BoundingBox referenceBounds = NativeStructureReferenceEnvelope.referenceBounds(
|
||||||
|
wrapped, wrapped.getStructure(), terrain);
|
||||||
|
|
||||||
|
assertEquals(sourceBounds, wrapped.getBoundingBox());
|
||||||
|
assertEquals(content.minX() - 4, referenceBounds.minX());
|
||||||
|
assertEquals(content.maxX() + 4, referenceBounds.maxX());
|
||||||
|
assertEquals(content.minZ() - 4, referenceBounds.minZ());
|
||||||
|
assertEquals(content.maxZ() + 4, referenceBounds.maxZ());
|
||||||
|
assertEquals(generated.getPieces().size(), wrapped.getPieces().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void referenceEnvelopeRejectsChunkCoordinateOverflow() {
|
||||||
|
StructureStart generated = start(TerrainAdjustment.NONE, 64);
|
||||||
|
StructureStart extreme = new StructureStart(
|
||||||
|
generated.getStructure(), new ChunkPos(Integer.MAX_VALUE, 0), 0,
|
||||||
|
new PiecesContainer(generated.getPieces()));
|
||||||
|
|
||||||
|
assertThrows(IllegalStateException.class, () ->
|
||||||
|
NativeStructureReferenceEnvelope.referenceBounds(
|
||||||
|
extreme, extreme.getStructure(), new IrisStructureTerrain()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void explicitTerrainEnvelopeDoesNotInheritSourceAdjustment() {
|
||||||
|
StructureStart generated = start(TerrainAdjustment.BURY, 64);
|
||||||
|
BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(generated);
|
||||||
|
IrisStructureTerrain terrain = new IrisStructureTerrain()
|
||||||
|
.setMode(IrisStructureTerrainMode.ENCASE)
|
||||||
|
.setHorizontalPadding(112);
|
||||||
|
|
||||||
|
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
|
||||||
|
generated, generated.getStructure(), 0, terrain);
|
||||||
|
BoundingBox referenceBounds = NativeStructureReferenceEnvelope.referenceBounds(
|
||||||
|
wrapped, wrapped.getStructure(), terrain);
|
||||||
|
|
||||||
|
assertEquals(content.minX() - 112, referenceBounds.minX());
|
||||||
|
assertEquals(content.maxX() + 112, referenceBounds.maxX());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void yBandRelocatesTheStartMidpointDeterministically() {
|
||||||
|
IrisStructureYBand band = new IrisStructureYBand().setMin(-120).setMax(-20);
|
||||||
|
StructureStart first = start(TerrainAdjustment.BURY, 64);
|
||||||
|
StructureStart second = start(TerrainAdjustment.BURY, 64);
|
||||||
|
|
||||||
|
NativeStructureVerticalPlacer.applyVerticalShift(
|
||||||
|
first, -64, -256, 320, true, false, band, (x, z) -> 40);
|
||||||
|
NativeStructureVerticalPlacer.applyVerticalShift(
|
||||||
|
second, -64, -256, 320, true, false, band, (x, z) -> 40);
|
||||||
|
|
||||||
|
BoundingBox bounds = first.getBoundingBox();
|
||||||
|
assertEquals(bounds.minY(), second.getBoundingBox().minY());
|
||||||
|
assertTrue(bounds.minY() >= -120);
|
||||||
|
assertTrue(bounds.maxY() <= -20);
|
||||||
|
|
||||||
|
int repeated = NativeStructureVerticalPlacer.applyVerticalShift(
|
||||||
|
first, -64, -256, 320, true, false, band, (x, z) -> 40);
|
||||||
|
assertEquals(0, repeated);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void yBandClampsWhenTheBandCannotContainTheStructure() {
|
||||||
|
IrisStructureYBand band = new IrisStructureYBand().setMin(-50).setMax(-45);
|
||||||
|
StructureStart start = start(TerrainAdjustment.BURY, 64);
|
||||||
|
|
||||||
|
NativeStructureVerticalPlacer.applyVerticalShift(
|
||||||
|
start, 0, -256, 320, true, false, band, (x, z) -> 40);
|
||||||
|
|
||||||
|
BoundingBox bounds = start.getBoundingBox();
|
||||||
|
int midpoint = bounds.minY() + (bounds.maxY() - bounds.minY()) / 2;
|
||||||
|
assertTrue(midpoint >= -50);
|
||||||
|
assertTrue(midpoint <= -45);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void yBandReplacesBothTheBlindShiftAndBurial() {
|
||||||
|
IrisStructureYBand band = new IrisStructureYBand().setMin(-64).setMax(-64);
|
||||||
|
StructureStart start = start(TerrainAdjustment.BURY, 64);
|
||||||
|
BoundingBox bounds = start.getBoundingBox();
|
||||||
|
int height = bounds.maxY() - bounds.minY();
|
||||||
|
|
||||||
|
NativeStructureVerticalPlacer.applyVerticalShift(
|
||||||
|
start, -200, -256, 320, true, false, band, (x, z) -> 40);
|
||||||
|
|
||||||
|
assertEquals(-64 - height / 2, start.getBoundingBox().minY());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void preserveSourceYWinsOverTheConfiguredBand() {
|
||||||
|
IrisStructureYBand band = new IrisStructureYBand().setMin(-120).setMax(-20);
|
||||||
|
StructureStart start = start(TerrainAdjustment.BURY, 64);
|
||||||
|
int minY = start.getBoundingBox().minY();
|
||||||
|
|
||||||
|
int offset = NativeStructureVerticalPlacer.applyVerticalShift(
|
||||||
|
start, 0, -256, 320, true, true, band, (x, z) -> 40);
|
||||||
|
|
||||||
|
assertEquals(0, offset);
|
||||||
|
assertEquals(minY, start.getBoundingBox().minY());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BlockState tuffBlock(IrisMaterialPalette palette, RNG rng, int x, int y, int z) {
|
||||||
|
assertNotNull(palette);
|
||||||
|
assertNotNull(rng);
|
||||||
|
return Blocks.TUFF.defaultBlockState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureStart start(TerrainAdjustment adjustment, int minY) {
|
||||||
|
Structure structure = new DesertPyramidStructure(new Structure.StructureSettings(
|
||||||
|
HolderSet.empty(), Map.of(), GenerationStep.Decoration.STRONGHOLDS, adjustment));
|
||||||
|
StructurePiece piece = new DesertPyramidPiece(RandomSource.create(7L), 0, 0);
|
||||||
|
piece.move(0, minY - piece.getBoundingBox().minY(), 0);
|
||||||
|
return new StructureStart(
|
||||||
|
structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PoolElementStructurePiece poolPiece(StructureTemplatePool.Projection projection) {
|
||||||
|
BlockPos position = new BlockPos(0, 64, 0);
|
||||||
|
StructurePoolElement element = StructurePoolElement.single(
|
||||||
|
"minecraft:empty",
|
||||||
|
Holder.direct(new StructureProcessorList(List.of(
|
||||||
|
BlockIgnoreProcessor.STRUCTURE_AND_AIR))),
|
||||||
|
LiquidSettings.APPLY_WATERLOGGING
|
||||||
|
).apply(projection);
|
||||||
|
return new PoolElementStructurePiece(
|
||||||
|
null,
|
||||||
|
element,
|
||||||
|
position,
|
||||||
|
0,
|
||||||
|
Rotation.NONE,
|
||||||
|
new BoundingBox(position),
|
||||||
|
LiquidSettings.APPLY_WATERLOGGING);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorldGenLevel world(Map<BlockPos, BlockState> blocks) {
|
||||||
|
InvocationHandler handler = (proxy, method, arguments) -> {
|
||||||
|
String methodName = method.getName();
|
||||||
|
if (methodName.equals("getBlockState")) {
|
||||||
|
BlockPos position = (BlockPos) arguments[0];
|
||||||
|
return state(blocks, position.getX(), position.getY(), position.getZ());
|
||||||
|
}
|
||||||
|
if (methodName.equals("setBlock")) {
|
||||||
|
BlockPos position = (BlockPos) arguments[0];
|
||||||
|
BlockState blockState = (BlockState) arguments[1];
|
||||||
|
put(blocks, position.getX(), position.getY(), position.getZ(), blockState);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (methodName.equals("getSeed")) {
|
||||||
|
return TEST_SEED;
|
||||||
|
}
|
||||||
|
if (methodName.equals("getLevel")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (methodName.equals("hashCode")) {
|
||||||
|
return System.identityHashCode(proxy);
|
||||||
|
}
|
||||||
|
if (methodName.equals("equals")) {
|
||||||
|
return proxy == arguments[0];
|
||||||
|
}
|
||||||
|
if (methodName.equals("toString")) {
|
||||||
|
return "encase-test-world";
|
||||||
|
}
|
||||||
|
throw new UnsupportedOperationException(method.toString());
|
||||||
|
};
|
||||||
|
return (WorldGenLevel) Proxy.newProxyInstance(
|
||||||
|
WorldGenLevel.class.getClassLoader(), new Class<?>[]{WorldGenLevel.class}, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceSnapshot(
|
||||||
|
WorldGenLevel world, BoundingBox area) {
|
||||||
|
BitSet requiredLayers = new BitSet(area.getYSpan());
|
||||||
|
requiredLayers.set(0, area.getYSpan());
|
||||||
|
return NativeStructureTerrainIntegrator.SourceTerrainSnapshot.capture(
|
||||||
|
world, area, requiredLayers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void put(Map<BlockPos, BlockState> blocks,
|
||||||
|
int x, int y, int z, BlockState state) {
|
||||||
|
blocks.put(new BlockPos(x, y, z), state);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BlockState state(Map<BlockPos, BlockState> blocks, int x, int y, int z) {
|
||||||
|
return blocks.getOrDefault(new BlockPos(x, y, z), Blocks.AIR.defaultBlockState());
|
||||||
|
}
|
||||||
|
}
|
||||||
+202
@@ -0,0 +1,202 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||||
|
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class NativeStructurePostProcessorMonumentTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraftRegistries() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void vanillaSeaLevelKeepsTheVanillaMonumentHeight() {
|
||||||
|
StructureStart start = monumentStart(1337L);
|
||||||
|
|
||||||
|
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||||
|
start, "minecraft:monument", 0, 63, -64, 320, false, false, null, (x, z) -> 0);
|
||||||
|
|
||||||
|
assertEquals(0, offset);
|
||||||
|
assertEquals(39, start.getBoundingBox().minY());
|
||||||
|
assertEquals(61, start.getBoundingBox().maxY());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shiftedSeaLevelMovesTheShellAndEveryRoomTogether() {
|
||||||
|
StructureStart start = monumentStart(1337L);
|
||||||
|
OceanMonumentPieces.MonumentBuilding building = monumentBuilding(start);
|
||||||
|
List<StructurePiece> children = NativeStructureVerticalPlacer.monumentChildPieces(building);
|
||||||
|
int[] childMinY = minimumYs(children);
|
||||||
|
BoundingBox cachedBounds = start.getBoundingBox();
|
||||||
|
|
||||||
|
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||||
|
start, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
|
||||||
|
|
||||||
|
assertEquals(-13, offset);
|
||||||
|
assertSame(cachedBounds, start.getBoundingBox());
|
||||||
|
assertEquals(26, building.getBoundingBox().minY());
|
||||||
|
assertEquals(48, building.getBoundingBox().maxY());
|
||||||
|
assertEquals(26, start.getBoundingBox().minY());
|
||||||
|
assertEquals(48, start.getBoundingBox().maxY());
|
||||||
|
assertTrue(children.size() > 20);
|
||||||
|
for (int i = 0; i < children.size(); i++) {
|
||||||
|
assertEquals(childMinY[i] - 13, children.get(i).getBoundingBox().minY());
|
||||||
|
}
|
||||||
|
|
||||||
|
int repeatedOffset = NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||||
|
start, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
|
||||||
|
assertEquals(0, repeatedOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void configuredOffsetIsRelativeToTheActualSeaLevel() {
|
||||||
|
StructureStart start = monumentStart(1337L);
|
||||||
|
|
||||||
|
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||||
|
start, "minecraft:monument", 3, 50, -256, 512, false, false, null, (x, z) -> 0);
|
||||||
|
|
||||||
|
assertEquals(-10, offset);
|
||||||
|
assertEquals(29, start.getBoundingBox().minY());
|
||||||
|
assertEquals(51, start.getBoundingBox().maxY());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void referenceEnvelopeDoesNotAddMonumentPieces() {
|
||||||
|
StructureStart start = monumentStart(1337L);
|
||||||
|
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
|
||||||
|
start,
|
||||||
|
start.getStructure(),
|
||||||
|
0,
|
||||||
|
new IrisStructureTerrain()
|
||||||
|
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
|
||||||
|
.setHorizontalPadding(4));
|
||||||
|
|
||||||
|
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||||
|
wrapped, "minecraft:monument", 0, 63, -64, 320,
|
||||||
|
false, false, null, (x, z) -> 0);
|
||||||
|
|
||||||
|
assertEquals(0, offset);
|
||||||
|
assertEquals(1, wrapped.getPieces().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void vanillaReloadRegenerationIsRealignedBeforePlacement() {
|
||||||
|
long seed = 1337L;
|
||||||
|
ChunkPos chunkPos = new ChunkPos(0, 0);
|
||||||
|
StructureStart initial = monumentStart(seed);
|
||||||
|
NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||||
|
initial, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
|
||||||
|
|
||||||
|
PiecesContainer regenerated = OceanMonumentStructure.regeneratePiecesAfterLoad(
|
||||||
|
chunkPos, seed, new PiecesContainer(initial.getPieces()));
|
||||||
|
StructureStart reloaded = new StructureStart(
|
||||||
|
monumentStructure(), chunkPos, 0, regenerated);
|
||||||
|
|
||||||
|
assertEquals(39, reloaded.getBoundingBox().minY());
|
||||||
|
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||||
|
reloaded, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
|
||||||
|
assertEquals(-13, offset);
|
||||||
|
assertEquals(26, reloaded.getBoundingBox().minY());
|
||||||
|
assertEquals(48, reloaded.getBoundingBox().maxY());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void neighboringChunkPlacementRealignsReloadedMonumentOnce() throws Exception {
|
||||||
|
long seed = 1337L;
|
||||||
|
ChunkPos chunkPos = new ChunkPos(0, 0);
|
||||||
|
StructureStart initial = monumentStart(seed);
|
||||||
|
NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||||
|
initial, "minecraft:monument", 0, 50, -256, 512,
|
||||||
|
false, false, null, (x, z) -> 0);
|
||||||
|
PiecesContainer regenerated = OceanMonumentStructure.regeneratePiecesAfterLoad(
|
||||||
|
chunkPos, seed, new PiecesContainer(initial.getPieces()));
|
||||||
|
StructureStart reloaded = new StructureStart(
|
||||||
|
monumentStructure(), chunkPos, 0, regenerated);
|
||||||
|
ExecutorService executor = Executors.newFixedThreadPool(8);
|
||||||
|
CountDownLatch release = new CountDownLatch(1);
|
||||||
|
List<Future<?>> futures = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
for (int task = 0; task < 16; task++) {
|
||||||
|
futures.add(executor.submit(() -> {
|
||||||
|
release.await();
|
||||||
|
NativeStructureVerticalPlacer.ensureMonumentSeaLevelAlignment(
|
||||||
|
reloaded, "minecraft:monument", 0, 50, -256, 512);
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
release.countDown();
|
||||||
|
for (Future<?> future : futures) {
|
||||||
|
future.get();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
executor.shutdownNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(26, reloaded.getBoundingBox().minY());
|
||||||
|
assertEquals(48, reloaded.getBoundingBox().maxY());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void impossibleSeaLevelAlignmentFailsInsteadOfClippingTheMonument() {
|
||||||
|
StructureStart start = monumentStart(1337L);
|
||||||
|
try {
|
||||||
|
NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||||
|
start, "minecraft:monument", 0, -50, -64, 320, false, false, null, (x, z) -> 0);
|
||||||
|
} catch (IllegalStateException error) {
|
||||||
|
assertTrue(error.getMessage().contains("cannot align"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new AssertionError("Expected an out-of-bounds monument alignment to fail");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureStart monumentStart(long seed) {
|
||||||
|
ChunkPos chunkPos = new ChunkPos(0, 0);
|
||||||
|
OceanMonumentPieces.MonumentBuilding building = new OceanMonumentPieces.MonumentBuilding(
|
||||||
|
RandomSource.create(seed), -29, -29, Direction.NORTH);
|
||||||
|
return new StructureStart(
|
||||||
|
monumentStructure(), chunkPos, 0, new PiecesContainer(List.of(building)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OceanMonumentStructure monumentStructure() {
|
||||||
|
return new OceanMonumentStructure(new Structure.StructureSettings(HolderSet.empty()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OceanMonumentPieces.MonumentBuilding monumentBuilding(StructureStart start) {
|
||||||
|
return (OceanMonumentPieces.MonumentBuilding) start.getPieces().get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int[] minimumYs(List<StructurePiece> pieces) {
|
||||||
|
int[] ys = new int[pieces.size()];
|
||||||
|
for (int i = 0; i < pieces.size(); i++) {
|
||||||
|
ys[i] = pieces.get(i).getBoundingBox().minY();
|
||||||
|
}
|
||||||
|
return ys;
|
||||||
|
}
|
||||||
|
}
|
||||||
+198
@@ -0,0 +1,198 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.LevelAccessor;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.ScatteredFeaturePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.JungleTemplePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.JungleTempleStructure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class NativeStructurePostProcessorScatteredFeatureTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraftRegistries() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void desertPyramidUsesTheLowestFootprintSurfaceAndLocksVanillaReanchor() {
|
||||||
|
TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(11L), 0, 0);
|
||||||
|
StructureStart start = desertStart(piece);
|
||||||
|
BoundingBox cachedBounds = start.getBoundingBox();
|
||||||
|
BoundingBox footprint = piece.getBoundingBox();
|
||||||
|
|
||||||
|
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||||
|
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null,
|
||||||
|
(x, z) -> x == footprint.maxX() && z == footprint.maxZ() ? 64 : 92);
|
||||||
|
|
||||||
|
assertEquals(1, offset);
|
||||||
|
assertSame(cachedBounds, start.getBoundingBox());
|
||||||
|
assertEquals(65, piece.getBoundingBox().minY());
|
||||||
|
assertEquals(65, start.getBoundingBox().minY());
|
||||||
|
assertTrue(piece.attemptVanillaAlignment(-2));
|
||||||
|
assertEquals(65, piece.getBoundingBox().minY());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void jungleTempleUsesAverageFootprintSurfaceAndHonorsConfiguredShift() {
|
||||||
|
TestJungleTemplePiece piece = new TestJungleTemplePiece(RandomSource.create(19L), 32, -16);
|
||||||
|
StructureStart start = jungleStart(piece);
|
||||||
|
BoundingBox footprint = piece.getBoundingBox();
|
||||||
|
|
||||||
|
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||||
|
start, "minecraft:jungle_pyramid", 4, 63, -64, 320, false, false, null,
|
||||||
|
(x, z) -> x == footprint.minX() && z == footprint.minZ() ? 88 : 70);
|
||||||
|
|
||||||
|
assertEquals(11, offset);
|
||||||
|
assertEquals(75, piece.getBoundingBox().minY());
|
||||||
|
assertTrue(piece.attemptVanillaAlignment());
|
||||||
|
assertEquals(75, piece.getBoundingBox().minY());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void repeatedAlignmentIsIdempotent() {
|
||||||
|
TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(23L), 0, 0);
|
||||||
|
StructureStart start = desertStart(piece);
|
||||||
|
|
||||||
|
int initialOffset = NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||||
|
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 80);
|
||||||
|
int repeatedOffset = NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||||
|
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 80);
|
||||||
|
|
||||||
|
assertEquals(17, initialOffset);
|
||||||
|
assertEquals(0, repeatedOffset);
|
||||||
|
assertEquals(81, piece.getBoundingBox().minY());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void scatteredAlignmentStaysInsideWorldBounds() {
|
||||||
|
TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(29L), 0, 0);
|
||||||
|
StructureStart start = desertStart(piece);
|
||||||
|
|
||||||
|
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||||
|
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 318);
|
||||||
|
|
||||||
|
assertEquals(241, offset);
|
||||||
|
assertEquals(305, piece.getBoundingBox().minY());
|
||||||
|
assertEquals(319, start.getBoundingBox().maxY());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void scatteredStructuresDoNotOptIntoFullFootprintSurfaceFitting() {
|
||||||
|
TestDesertPyramidPiece desert = new TestDesertPyramidPiece(RandomSource.create(31L), 0, 0);
|
||||||
|
TestJungleTemplePiece jungle = new TestJungleTemplePiece(RandomSource.create(37L), 0, 0);
|
||||||
|
SwampHutPiece swamp = new SwampHutPiece(RandomSource.create(41L), 0, 0);
|
||||||
|
StructureStart desertStart = desertStart(desert);
|
||||||
|
StructureStart jungleStart = jungleStart(jungle);
|
||||||
|
StructureStart swampStart = swampStart(swamp);
|
||||||
|
|
||||||
|
assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(desertStart));
|
||||||
|
assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(jungleStart));
|
||||||
|
assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(swampStart));
|
||||||
|
|
||||||
|
AtomicInteger terrainQueries = new AtomicInteger();
|
||||||
|
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||||
|
swampStart, "minecraft:swamp_hut", 0, 63, -64, 320, false, false, null,
|
||||||
|
(x, z) -> terrainQueries.incrementAndGet());
|
||||||
|
assertEquals(0, offset);
|
||||||
|
assertEquals(0, terrainQueries.get());
|
||||||
|
assertEquals(64, swamp.getBoundingBox().minY());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void scatteredHeightFieldMatchesTheRuntimeContract() {
|
||||||
|
Field field = NativeStructureReflection.resolveScatteredHeightPositionField();
|
||||||
|
|
||||||
|
assertEquals(ScatteredFeaturePiece.class, field.getDeclaringClass());
|
||||||
|
assertEquals(int.class, field.getType());
|
||||||
|
assertTrue(Modifier.isProtected(field.getModifiers()));
|
||||||
|
assertFalse(Modifier.isFinal(field.getModifiers()));
|
||||||
|
assertFalse(Modifier.isStatic(field.getModifiers()));
|
||||||
|
assertTrue(field.trySetAccessible());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void datapackOverridesOfSpecialVanillaIdsUseGenericPlacement() {
|
||||||
|
StructureStart pyramidOverride = swampStart(
|
||||||
|
new SwampHutPiece(RandomSource.create(43L), 0, 0));
|
||||||
|
int pyramidMinY = pyramidOverride.getBoundingBox().minY();
|
||||||
|
int pyramidOffset = NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||||
|
pyramidOverride, "minecraft:desert_pyramid", 5,
|
||||||
|
63, -64, 320, false, false, null, (x, z) -> 200);
|
||||||
|
|
||||||
|
assertEquals(5, pyramidOffset);
|
||||||
|
assertEquals(pyramidMinY + 5, pyramidOverride.getBoundingBox().minY());
|
||||||
|
|
||||||
|
StructureStart monumentOverride = swampStart(
|
||||||
|
new SwampHutPiece(RandomSource.create(47L), 0, 0));
|
||||||
|
int monumentMinY = monumentOverride.getBoundingBox().minY();
|
||||||
|
NativeStructureVerticalPlacer.ensureMonumentSeaLevelAlignment(
|
||||||
|
monumentOverride, "minecraft:monument", 0, 40, -64, 320);
|
||||||
|
|
||||||
|
assertEquals(monumentMinY, monumentOverride.getBoundingBox().minY());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureStart desertStart(StructurePiece piece) {
|
||||||
|
Structure structure = new DesertPyramidStructure(new Structure.StructureSettings(HolderSet.empty()));
|
||||||
|
return start(structure, piece);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureStart jungleStart(StructurePiece piece) {
|
||||||
|
Structure structure = new JungleTempleStructure(new Structure.StructureSettings(HolderSet.empty()));
|
||||||
|
return start(structure, piece);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureStart swampStart(StructurePiece piece) {
|
||||||
|
Structure structure = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty()));
|
||||||
|
return start(structure, piece);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureStart start(Structure structure, StructurePiece piece) {
|
||||||
|
return new StructureStart(
|
||||||
|
structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class TestDesertPyramidPiece extends DesertPyramidPiece {
|
||||||
|
private TestDesertPyramidPiece(RandomSource random, int west, int north) {
|
||||||
|
super(random, west, north);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean attemptVanillaAlignment(int offset) {
|
||||||
|
return updateHeightPositionToLowestGroundHeight((LevelAccessor) null, offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class TestJungleTemplePiece extends JungleTemplePiece {
|
||||||
|
private TestJungleTemplePiece(RandomSource random, int west, int north) {
|
||||||
|
super(random, west, north);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean attemptVanillaAlignment() {
|
||||||
|
return updateAverageGroundHeight((LevelAccessor) null, null, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2900
File diff suppressed because it is too large
Load Diff
+354
@@ -0,0 +1,354 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.core.IdMapper;
|
||||||
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.block.Block;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||||
|
import net.minecraft.world.level.chunk.PalettedContainer;
|
||||||
|
import net.minecraft.world.level.chunk.PalettedContainerFactory;
|
||||||
|
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||||
|
import net.minecraft.world.level.chunk.Strategy;
|
||||||
|
import net.minecraft.world.level.chunk.UpgradeData;
|
||||||
|
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationHandler;
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class NativeStructurePostProcessorVegetationTest {
|
||||||
|
private static final long TEST_SEED = 1234L;
|
||||||
|
private static final int MIN_Y = -64;
|
||||||
|
private static final int MAX_Y = 319;
|
||||||
|
private static final int GROUND_Y = 64;
|
||||||
|
private static final BoundingBox AREA = new BoundingBox(0, MIN_Y, 0, 15, MAX_Y, 15);
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraftRegistries() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void canopyAboveTheStructureRoofSurvivesVegetationClearing() {
|
||||||
|
ProtoChunk chunk = chunk();
|
||||||
|
StructureStart start = desertStart(0, 0, GROUND_Y + 15);
|
||||||
|
BoundingBox bounds = start.getBoundingBox();
|
||||||
|
for (int y = bounds.maxY() - 1; y <= GROUND_Y + 40; y++) {
|
||||||
|
fillLayer(chunk, y, leaves());
|
||||||
|
}
|
||||||
|
write(chunk, 4, bounds.minY() - 1, 4, log());
|
||||||
|
|
||||||
|
clearVegetation(chunk, start);
|
||||||
|
|
||||||
|
for (int z = 0; z < 16; z++) {
|
||||||
|
for (int x = 0; x < 16; x++) {
|
||||||
|
assertEquals(leaves(), chunk.getBlockState(new BlockPos(x, GROUND_Y + 40, z)));
|
||||||
|
assertEquals(leaves(), chunk.getBlockState(new BlockPos(x, bounds.maxY() + 1, z)));
|
||||||
|
assertEquals(air(), chunk.getBlockState(new BlockPos(x, bounds.maxY(), z)));
|
||||||
|
assertEquals(air(), chunk.getBlockState(new BlockPos(x, bounds.maxY() - 1, z)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertEquals(air(), chunk.getBlockState(new BlockPos(4, bounds.minY() - 1, 4)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void buriedStructuresPreserveTheForestAboveThem() {
|
||||||
|
ProtoChunk chunk = chunk();
|
||||||
|
StructureStart start = desertStart(0, 0, -20);
|
||||||
|
write(chunk, 8, GROUND_Y + 1, 8, log());
|
||||||
|
write(chunk, 8, GROUND_Y + 2, 8, log());
|
||||||
|
write(chunk, 8, GROUND_Y + 3, 8, log());
|
||||||
|
write(chunk, 8, GROUND_Y + 4, 8, leaves());
|
||||||
|
|
||||||
|
clearVegetation(chunk, start);
|
||||||
|
|
||||||
|
assertEquals(log(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 1, 8)));
|
||||||
|
assertEquals(log(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 2, 8)));
|
||||||
|
assertEquals(log(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 3, 8)));
|
||||||
|
assertEquals(leaves(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 4, 8)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void columnsOutsideThePieceFootprintKeepEveryTreeBlock() {
|
||||||
|
ProtoChunk chunk = chunk();
|
||||||
|
StructureStart start = desertStart(8, 8, GROUND_Y + 15);
|
||||||
|
BoundingBox bounds = start.getBoundingBox();
|
||||||
|
int inside = bounds.minY() + 4;
|
||||||
|
write(chunk, 0, inside, 0, log());
|
||||||
|
write(chunk, 0, GROUND_Y + 40, 0, leaves());
|
||||||
|
write(chunk, 12, inside, 12, log());
|
||||||
|
write(chunk, 12, GROUND_Y + 40, 12, leaves());
|
||||||
|
|
||||||
|
clearVegetation(chunk, start);
|
||||||
|
|
||||||
|
assertEquals(log(), chunk.getBlockState(new BlockPos(0, inside, 0)));
|
||||||
|
assertEquals(leaves(), chunk.getBlockState(new BlockPos(0, GROUND_Y + 40, 0)));
|
||||||
|
assertEquals(air(), chunk.getBlockState(new BlockPos(12, inside, 12)));
|
||||||
|
assertEquals(leaves(), chunk.getBlockState(new BlockPos(12, GROUND_Y + 40, 12)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void clearingStopsAtThePieceEnvelopeBoundaries() {
|
||||||
|
ProtoChunk chunk = chunk();
|
||||||
|
StructureStart start = desertStart(0, 0, GROUND_Y + 15);
|
||||||
|
BoundingBox bounds = start.getBoundingBox();
|
||||||
|
write(chunk, 4, bounds.maxY() + 1, 4, log());
|
||||||
|
write(chunk, 4, bounds.maxY(), 4, log());
|
||||||
|
write(chunk, 4, bounds.minY() - 1, 4, log());
|
||||||
|
write(chunk, 4, bounds.minY() - 2, 4, log());
|
||||||
|
|
||||||
|
clearVegetation(chunk, start);
|
||||||
|
|
||||||
|
assertEquals(log(), chunk.getBlockState(new BlockPos(4, bounds.maxY() + 1, 4)));
|
||||||
|
assertEquals(air(), chunk.getBlockState(new BlockPos(4, bounds.maxY(), 4)));
|
||||||
|
assertEquals(air(), chunk.getBlockState(new BlockPos(4, bounds.minY() - 1, 4)));
|
||||||
|
assertEquals(log(), chunk.getBlockState(new BlockPos(4, bounds.minY() - 2, 4)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void overlappingPieceEnvelopesMergeIntoOneClearedRange() {
|
||||||
|
ProtoChunk chunk = chunk();
|
||||||
|
StructureStart low = desertStart(0, 0, GROUND_Y);
|
||||||
|
StructureStart high = desertStart(0, 0, GROUND_Y + 40);
|
||||||
|
int between = GROUND_Y + 20;
|
||||||
|
write(chunk, 4, between, 4, log());
|
||||||
|
write(chunk, 4, high.getBoundingBox().maxY() + 1, 4, log());
|
||||||
|
write(chunk, 4, low.getBoundingBox().minY() - 2, 4, log());
|
||||||
|
|
||||||
|
NativeStructureVegetationClearer.clearIntersectingVegetation(
|
||||||
|
world(chunk), chunk, AREA, List.of(low, high));
|
||||||
|
|
||||||
|
assertEquals(air(), chunk.getBlockState(new BlockPos(4, between, 4)));
|
||||||
|
assertEquals(log(), chunk.getBlockState(
|
||||||
|
new BlockPos(4, high.getBoundingBox().maxY() + 1, 4)));
|
||||||
|
assertEquals(log(), chunk.getBlockState(
|
||||||
|
new BlockPos(4, low.getBoundingBox().minY() - 2, 4)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validStartsReachIntersectionProcessing() {
|
||||||
|
assertTrue(NativeStructureVegetationClearer.shouldProcessStart(
|
||||||
|
desertStart(0, 0, GROUND_Y + 15)));
|
||||||
|
assertFalse(NativeStructureVegetationClearer.shouldProcessStart(
|
||||||
|
StructureStart.INVALID_START));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void allUndergroundGenerationStepsShareOneClassification() {
|
||||||
|
assertTrue(NativeStructureVegetationClearer.isUndergroundStep(
|
||||||
|
GenerationStep.Decoration.UNDERGROUND_STRUCTURES));
|
||||||
|
assertTrue(NativeStructureVegetationClearer.isUndergroundStep(
|
||||||
|
GenerationStep.Decoration.UNDERGROUND_DECORATION));
|
||||||
|
assertTrue(NativeStructureVegetationClearer.isUndergroundStep(
|
||||||
|
GenerationStep.Decoration.STRONGHOLDS));
|
||||||
|
assertFalse(NativeStructureVegetationClearer.isUndergroundStep(
|
||||||
|
GenerationStep.Decoration.SURFACE_STRUCTURES));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void undergroundStructuresUseTheLowestTerrainColumn() {
|
||||||
|
BoundingBox bounds = new BoundingBox(0, 60, 0, 1, 80, 0);
|
||||||
|
int offset = NativeStructureVerticalPlacer.resolveBuriedOffset(
|
||||||
|
bounds, 0, -64, 320, (x, z) -> x == 1 ? 76 : 100);
|
||||||
|
assertEquals(-5, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void undergroundBurialClampsToTheWorldFloorInsteadOfFailing() {
|
||||||
|
BoundingBox bounds = new BoundingBox(0, 60, 0, 1, 80, 0);
|
||||||
|
assertEquals(-2, NativeStructureVerticalPlacer.resolveBuriedOffset(
|
||||||
|
bounds, 0, 58, 320, (x, z) -> x == 1 ? 76 : 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void loweringSurfaceRemovesCactusStacksAndTheirFlowers() {
|
||||||
|
ProtoChunk chunk = chunk();
|
||||||
|
for (int y = 80; y <= 102; y++) {
|
||||||
|
write(chunk, 4, y, 4, Blocks.SAND.defaultBlockState());
|
||||||
|
}
|
||||||
|
for (int y = 103; y <= 105; y++) {
|
||||||
|
write(chunk, 4, y, 4, Blocks.CACTUS.defaultBlockState());
|
||||||
|
}
|
||||||
|
write(chunk, 4, 106, 4, Blocks.CACTUS_FLOWER.defaultBlockState());
|
||||||
|
|
||||||
|
NativeStructureSurfaceFitter.applySurfaceColumn(
|
||||||
|
world(chunk), new BlockPos.MutableBlockPos(), 4, 4, 102, 93, MIN_Y, MAX_Y);
|
||||||
|
|
||||||
|
assertEquals(Blocks.SAND.defaultBlockState(), chunk.getBlockState(new BlockPos(4, 93, 4)));
|
||||||
|
for (int y = 94; y <= 106; y++) {
|
||||||
|
assertTrue("Unsupported decoration at " + y, chunk.getBlockState(new BlockPos(4, y, 4)).isAir());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void loweringSurfacePreservesCactusOnUnchangedColumns() {
|
||||||
|
ProtoChunk chunk = chunk();
|
||||||
|
for (int x : List.of(4, 5)) {
|
||||||
|
for (int y = 60; y <= 64; y++) {
|
||||||
|
write(chunk, x, y, 4, Blocks.SAND.defaultBlockState());
|
||||||
|
}
|
||||||
|
write(chunk, x, 65, 4, Blocks.CACTUS.defaultBlockState());
|
||||||
|
write(chunk, x, 66, 4, Blocks.CACTUS_FLOWER.defaultBlockState());
|
||||||
|
}
|
||||||
|
WorldGenLevel world = world(chunk);
|
||||||
|
|
||||||
|
NativeStructureSurfaceFitter.applySurfaceColumn(
|
||||||
|
world, new BlockPos.MutableBlockPos(), 4, 4, 64, 60, MIN_Y, MAX_Y);
|
||||||
|
NativeStructureSurfaceFitter.applySurfaceColumn(
|
||||||
|
world, new BlockPos.MutableBlockPos(), 5, 4, 64, 64, MIN_Y, MAX_Y);
|
||||||
|
|
||||||
|
assertTrue(chunk.getBlockState(new BlockPos(4, 65, 4)).isAir());
|
||||||
|
assertTrue(chunk.getBlockState(new BlockPos(4, 66, 4)).isAir());
|
||||||
|
assertEquals(Blocks.SAND.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 64, 4)));
|
||||||
|
assertEquals(Blocks.CACTUS.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 65, 4)));
|
||||||
|
assertEquals(Blocks.CACTUS_FLOWER.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 66, 4)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void loweringSurfaceKeepsTreeAndSolidObjectBoundaries() {
|
||||||
|
for (BlockState boundary : List.of(Blocks.OAK_LOG.defaultBlockState(), Blocks.OAK_PLANKS.defaultBlockState())) {
|
||||||
|
ProtoChunk chunk = chunk();
|
||||||
|
write(chunk, 4, 63, 4, Blocks.SAND.defaultBlockState());
|
||||||
|
write(chunk, 4, 64, 4, Blocks.SAND.defaultBlockState());
|
||||||
|
write(chunk, 4, 65, 4, boundary);
|
||||||
|
write(chunk, 4, 66, 4, Blocks.CACTUS.defaultBlockState());
|
||||||
|
write(chunk, 4, 67, 4, Blocks.CACTUS_FLOWER.defaultBlockState());
|
||||||
|
|
||||||
|
NativeStructureSurfaceFitter.applySurfaceColumn(
|
||||||
|
world(chunk), new BlockPos.MutableBlockPos(), 4, 4, 64, 60, MIN_Y, MAX_Y);
|
||||||
|
|
||||||
|
assertEquals(boundary, chunk.getBlockState(new BlockPos(4, 65, 4)));
|
||||||
|
assertEquals(Blocks.CACTUS.defaultBlockState(), chunk.getBlockState(new BlockPos(4, 66, 4)));
|
||||||
|
assertEquals(Blocks.CACTUS_FLOWER.defaultBlockState(), chunk.getBlockState(new BlockPos(4, 67, 4)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void clearVegetation(ProtoChunk chunk, StructureStart start) {
|
||||||
|
NativeStructureVegetationClearer.clearIntersectingVegetation(
|
||||||
|
world(chunk), chunk, AREA, List.of(start));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureStart desertStart(int originX, int originZ, int topY) {
|
||||||
|
Structure structure = new DesertPyramidStructure(
|
||||||
|
new Structure.StructureSettings(
|
||||||
|
HolderSet.empty(), Map.of(),
|
||||||
|
GenerationStep.Decoration.SURFACE_STRUCTURES, TerrainAdjustment.NONE));
|
||||||
|
DesertPyramidPiece piece = new DesertPyramidPiece(RandomSource.create(7L), originX, originZ);
|
||||||
|
piece.move(0, topY - piece.getBoundingBox().maxY(), 0);
|
||||||
|
return new StructureStart(
|
||||||
|
structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProtoChunk chunk() {
|
||||||
|
return new ProtoChunk(
|
||||||
|
new ChunkPos(0, 0), UpgradeData.EMPTY,
|
||||||
|
LevelHeightAccessor.create(MIN_Y, MAX_Y - MIN_Y + 1), containerFactory(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void fillLayer(ProtoChunk chunk, int y, BlockState state) {
|
||||||
|
for (int z = 0; z < 16; z++) {
|
||||||
|
for (int x = 0; x < 16; x++) {
|
||||||
|
write(chunk, x, y, z, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void write(ProtoChunk chunk, int x, int y, int z, BlockState state) {
|
||||||
|
LevelChunkSection section = chunk.getSection(chunk.getSectionIndex(y));
|
||||||
|
section.setBlockState(x, y & 15, z, state, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BlockState log() {
|
||||||
|
return Blocks.OAK_LOG.defaultBlockState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BlockState leaves() {
|
||||||
|
return Blocks.OAK_LEAVES.defaultBlockState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BlockState air() {
|
||||||
|
return Blocks.AIR.defaultBlockState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorldGenLevel world(ChunkAccess chunk) {
|
||||||
|
InvocationHandler handler = (proxy, method, arguments) -> {
|
||||||
|
String methodName = method.getName();
|
||||||
|
if (methodName.equals("getBlockState")) {
|
||||||
|
return chunk.getBlockState((BlockPos) arguments[0]);
|
||||||
|
}
|
||||||
|
if (methodName.equals("setBlock")) {
|
||||||
|
return chunk.setBlockState(
|
||||||
|
((BlockPos) arguments[0]).immutable(),
|
||||||
|
(BlockState) arguments[1], (int) arguments[2]) != null;
|
||||||
|
}
|
||||||
|
if (methodName.equals("getSeed")) {
|
||||||
|
return TEST_SEED;
|
||||||
|
}
|
||||||
|
if (methodName.equals("getLevel")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (methodName.equals("holderLookup")) {
|
||||||
|
return BuiltInRegistries.BLOCK;
|
||||||
|
}
|
||||||
|
if (methodName.equals("hashCode")) {
|
||||||
|
return System.identityHashCode(proxy);
|
||||||
|
}
|
||||||
|
if (methodName.equals("equals")) {
|
||||||
|
return proxy == arguments[0];
|
||||||
|
}
|
||||||
|
if (methodName.equals("toString")) {
|
||||||
|
return "vegetation-test-world";
|
||||||
|
}
|
||||||
|
throw new UnsupportedOperationException(method.toString());
|
||||||
|
};
|
||||||
|
return (WorldGenLevel) Proxy.newProxyInstance(
|
||||||
|
WorldGenLevel.class.getClassLoader(), new Class<?>[]{WorldGenLevel.class}, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PalettedContainerFactory containerFactory() {
|
||||||
|
Strategy<BlockState> blockStates = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY);
|
||||||
|
BlockState air = Blocks.AIR.defaultBlockState();
|
||||||
|
IdMapper<Holder<Biome>> biomeIds = new IdMapper<>();
|
||||||
|
Holder<Biome> defaultBiome = Holder.direct((Biome) null);
|
||||||
|
biomeIds.add(defaultBiome);
|
||||||
|
Strategy<Holder<Biome>> biomes = Strategy.createForBiomes(biomeIds);
|
||||||
|
Codec<Holder<Biome>> biomeCodec = Codec.STRING.xmap(
|
||||||
|
name -> defaultBiome, holder -> "iris-test-biome");
|
||||||
|
return new PalettedContainerFactory(
|
||||||
|
blockStates,
|
||||||
|
air,
|
||||||
|
PalettedContainer.codecRW(BlockState.CODEC, blockStates, air),
|
||||||
|
biomes,
|
||||||
|
defaultBiome,
|
||||||
|
PalettedContainer.codecRO(biomeCodec, biomes, defaultBiome),
|
||||||
|
PalettedContainer.codecRW(biomeCodec, biomes, defaultBiome));
|
||||||
|
}
|
||||||
|
}
|
||||||
+236
@@ -0,0 +1,236 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import com.mojang.datafixers.util.Either;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.tags.TagKey;
|
||||||
|
import net.minecraft.util.valueproviders.ConstantInt;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.Rotation;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.BlackstoneReplaceProcessor;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.BlockAgeProcessor;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.BlockIgnoreProcessor;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.CappedProcessor;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.GravityProcessor;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.JigsawReplacementProcessor;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.LavaSubmergedBlockProcessor;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.NopProcessor;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.ProtectedBlockProcessor;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.RuleProcessor;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||||
|
import org.junit.Assume;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.InvocationHandler;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class NativeStructureTemplateOccupancyTest {
|
||||||
|
private static final BoundingBox PROCESSING_AREA = new BoundingBox(0, 56, 0, 15, 80, 0);
|
||||||
|
private static final int TEMPLATE_WIDTH = 32;
|
||||||
|
private static final int TERRAIN_HEIGHT = 64;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraft() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void straddlingGravityPieceHeightReadsHonorTheVanillaClipContract() throws Exception {
|
||||||
|
ColumnRecorder recorder = new ColumnRecorder();
|
||||||
|
|
||||||
|
resolve(straddlingPiece(
|
||||||
|
List.of(new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, 0))), recorder);
|
||||||
|
|
||||||
|
// 26.2 vanilla clips processor evaluation to the placement bounding box; 26.1.2 vanilla
|
||||||
|
// runs processors across the whole piece. Iris mirrors the pinned version's semantics.
|
||||||
|
assertEquals(expectedClippedColumns(), recorder.heightColumns);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cappedStraddlingPieceRunsProcessorsAcrossTheWholePiece() throws Exception {
|
||||||
|
ColumnRecorder recorder = new ColumnRecorder();
|
||||||
|
|
||||||
|
resolve(straddlingPiece(List.of(
|
||||||
|
new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, 0),
|
||||||
|
new CappedProcessor(NopProcessor.INSTANCE, ConstantInt.of(0)))), recorder);
|
||||||
|
|
||||||
|
assertEquals(columns(0, TEMPLATE_WIDTH - 1), recorder.heightColumns);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cappedProcessorIsTheOnlyProcessorThatDisablesTheProcessingAreaClip() throws Exception {
|
||||||
|
Method contract = clipContractMethod();
|
||||||
|
Assume.assumeTrue("evaluatesEntirePieceState only exists on 26.2+", contract != null);
|
||||||
|
|
||||||
|
for (StructureProcessor processor : List.of(
|
||||||
|
BlockIgnoreProcessor.STRUCTURE_BLOCK,
|
||||||
|
JigsawReplacementProcessor.INSTANCE,
|
||||||
|
LavaSubmergedBlockProcessor.INSTANCE,
|
||||||
|
NopProcessor.INSTANCE,
|
||||||
|
BlackstoneReplaceProcessor.INSTANCE,
|
||||||
|
new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, 0),
|
||||||
|
new RuleProcessor(List.of()),
|
||||||
|
new BlockAgeProcessor(0.5F),
|
||||||
|
protectedBlockProcessor())) {
|
||||||
|
assertFalse(processor.getClass().getName(),
|
||||||
|
(Boolean) contract.invoke(processor));
|
||||||
|
}
|
||||||
|
assertTrue((Boolean) contract.invoke(
|
||||||
|
new CappedProcessor(NopProcessor.INSTANCE, ConstantInt.of(4))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void placementSettingsCarryTheProcessingAreaAsTheirClipBox() throws Exception {
|
||||||
|
PoolElementStructurePiece piece = straddlingPiece(List.of());
|
||||||
|
|
||||||
|
StructurePlaceSettings settings = NativeStructureReflection.resolvePlacementSettings(
|
||||||
|
(SinglePoolElement) piece.getElement(), piece, PROCESSING_AREA);
|
||||||
|
|
||||||
|
assertEquals(PROCESSING_AREA, settings.getBoundingBox());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void resolve(PoolElementStructurePiece piece, ColumnRecorder recorder) {
|
||||||
|
NativeStructureTemplateOccupancy.resolve(
|
||||||
|
world(recorder), piece, piece.getPosition(), PROCESSING_AREA,
|
||||||
|
NativeStructureTemplateOccupancyTest::forbiddenTemplateManager,
|
||||||
|
PROCESSING_AREA::isInside, amount -> {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PoolElementStructurePiece straddlingPiece(
|
||||||
|
List<StructureProcessor> processors) throws Exception {
|
||||||
|
BoundingBox bounds = new BoundingBox(0, 70, 0, TEMPLATE_WIDTH - 1, 76, 0);
|
||||||
|
return new PoolElementStructurePiece(
|
||||||
|
null, new InlineSinglePoolElement(template(), processors),
|
||||||
|
new BlockPos(bounds.minX(), bounds.minY(), bounds.minZ()),
|
||||||
|
1, Rotation.NONE, bounds, LiquidSettings.APPLY_WATERLOGGING);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<Integer> columns(int minimumX, int maximumX) {
|
||||||
|
Set<Integer> columns = new TreeSet<>();
|
||||||
|
for (int x = minimumX; x <= maximumX; x++) {
|
||||||
|
columns.add(x);
|
||||||
|
}
|
||||||
|
return columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Method clipContractMethod() {
|
||||||
|
try {
|
||||||
|
return StructureProcessor.class.getMethod("evaluatesEntirePieceState");
|
||||||
|
} catch (NoSuchMethodException absent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<Integer> expectedClippedColumns() {
|
||||||
|
return clipContractMethod() != null
|
||||||
|
? columns(PROCESSING_AREA.minX(), PROCESSING_AREA.maxX())
|
||||||
|
: columns(0, TEMPLATE_WIDTH - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureProcessor protectedBlockProcessor() throws Exception {
|
||||||
|
// Constructed reflectively: the constructor takes HolderSet on 26.2 and TagKey on 26.1.2.
|
||||||
|
Constructor<?> constructor = ProtectedBlockProcessor.class.getConstructors()[0];
|
||||||
|
Class<?> parameter = constructor.getParameterTypes()[0];
|
||||||
|
Object argument = parameter == HolderSet.class
|
||||||
|
? HolderSet.empty()
|
||||||
|
: TagKey.create(Registries.BLOCK, Identifier.fromNamespaceAndPath("minecraft", "air"));
|
||||||
|
return (StructureProcessor) constructor.newInstance(argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureTemplateManager forbiddenTemplateManager() {
|
||||||
|
throw new AssertionError("Inline templates must not resolve a template manager");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureTemplate template() throws Exception {
|
||||||
|
List<StructureTemplate.StructureBlockInfo> blocks = new ArrayList<>();
|
||||||
|
for (int x = 0; x < TEMPLATE_WIDTH; x++) {
|
||||||
|
blocks.add(new StructureTemplate.StructureBlockInfo(
|
||||||
|
new BlockPos(x, 0, 0), Blocks.COBBLESTONE.defaultBlockState(), null));
|
||||||
|
}
|
||||||
|
Constructor<StructureTemplate.Palette> constructor =
|
||||||
|
StructureTemplate.Palette.class.getDeclaredConstructor(List.class);
|
||||||
|
assertTrue(constructor.trySetAccessible());
|
||||||
|
StructureTemplate template = new StructureTemplate();
|
||||||
|
template.palettes.add(constructor.newInstance(blocks));
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorldGenLevel world(ColumnRecorder recorder) {
|
||||||
|
InvocationHandler handler = (proxy, method, arguments) -> {
|
||||||
|
String methodName = method.getName();
|
||||||
|
if (methodName.equals("getHeight") && arguments.length == 3) {
|
||||||
|
recorder.heightColumns.add((Integer) arguments[1]);
|
||||||
|
return TERRAIN_HEIGHT;
|
||||||
|
}
|
||||||
|
if (methodName.equals("getBlockState")) {
|
||||||
|
recorder.stateColumns.add(((BlockPos) arguments[0]).getX());
|
||||||
|
return Blocks.STONE.defaultBlockState();
|
||||||
|
}
|
||||||
|
if (methodName.equals("getLevel")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (methodName.equals("holderLookup")) {
|
||||||
|
return BuiltInRegistries.BLOCK;
|
||||||
|
}
|
||||||
|
if (methodName.equals("hashCode")) {
|
||||||
|
return System.identityHashCode(proxy);
|
||||||
|
}
|
||||||
|
if (methodName.equals("equals")) {
|
||||||
|
return proxy == arguments[0];
|
||||||
|
}
|
||||||
|
if (methodName.equals("toString")) {
|
||||||
|
return "occupancy-test-world";
|
||||||
|
}
|
||||||
|
throw new UnsupportedOperationException(method.toString());
|
||||||
|
};
|
||||||
|
return (WorldGenLevel) Proxy.newProxyInstance(
|
||||||
|
WorldGenLevel.class.getClassLoader(),
|
||||||
|
new Class<?>[]{WorldGenLevel.class}, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ColumnRecorder {
|
||||||
|
private final Set<Integer> heightColumns = new TreeSet<>();
|
||||||
|
private final Set<Integer> stateColumns = new TreeSet<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class InlineSinglePoolElement extends SinglePoolElement {
|
||||||
|
private InlineSinglePoolElement(
|
||||||
|
StructureTemplate template, List<StructureProcessor> processors) {
|
||||||
|
super(Either.right(template),
|
||||||
|
Holder.direct(new StructureProcessorList(processors)),
|
||||||
|
StructureTemplatePool.Projection.RIGID,
|
||||||
|
Optional.<LiquidSettings>empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+561
@@ -0,0 +1,561 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.structure.nativegen.NativeStructureVolume;
|
||||||
|
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.ForkJoinPool;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.concurrent.locks.LockSupport;
|
||||||
|
import java.util.function.IntConsumer;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class NativeStructureVolumeIndexTest {
|
||||||
|
private static final String STRUCTURE_KEY = "minecraft:swamp_hut";
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraftRegistries() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void assembledPiecesBecomeWorldSpacePieceVolumes() {
|
||||||
|
StructureStart start = swampHut(0, 0, 4, 6);
|
||||||
|
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
|
||||||
|
|
||||||
|
KList<NativeStructureVolume> volumes = NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, start);
|
||||||
|
|
||||||
|
assertEquals(1, volumes.size());
|
||||||
|
NativeStructureVolume volume = volumes.getFirst();
|
||||||
|
assertEquals(STRUCTURE_KEY, volume.structure());
|
||||||
|
assertEquals(bounds.minX(), volume.minX());
|
||||||
|
assertEquals(bounds.minY(), volume.minY());
|
||||||
|
assertEquals(bounds.minZ(), volume.minZ());
|
||||||
|
assertEquals(bounds.maxX(), volume.maxX());
|
||||||
|
assertEquals(bounds.maxY(), volume.maxY());
|
||||||
|
assertEquals(bounds.maxZ(), volume.maxZ());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void invalidStartsContributeNoVolumes() {
|
||||||
|
assertNull(NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, StructureStart.INVALID_START));
|
||||||
|
assertNull(NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void coldAndCachedQueriesResolveIdenticalVolumes() {
|
||||||
|
CountingResolver resolver = new CountingResolver(0, 0);
|
||||||
|
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
|
||||||
|
|
||||||
|
KList<NativeStructureVolume> cold = index.resolve(null, 0, 0, 15, 15);
|
||||||
|
int coldResolutions = resolver.resolutions();
|
||||||
|
KList<NativeStructureVolume> cached = index.resolve(null, 0, 0, 15, 15);
|
||||||
|
|
||||||
|
assertTrue(coldResolutions > 0);
|
||||||
|
assertFalse(cold.isEmpty());
|
||||||
|
assertEquals(cold, cached);
|
||||||
|
assertEquals(coldResolutions, resolver.resolutions());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void runtimeBucketsStayIsolatedAndRetirementEvictsOnlyTheirEntries() {
|
||||||
|
AtomicInteger runtimeId = new AtomicInteger(1);
|
||||||
|
Engine engine = runtimeEngine(runtimeId);
|
||||||
|
CountingResolver resolver = new CountingResolver(0, 0);
|
||||||
|
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
|
||||||
|
|
||||||
|
index.resolve(engine, 0, 0, 15, 15);
|
||||||
|
assertEquals(289, resolver.resolutions());
|
||||||
|
index.resolve(engine, 0, 0, 15, 15);
|
||||||
|
assertEquals(289, resolver.resolutions());
|
||||||
|
|
||||||
|
runtimeId.set(2);
|
||||||
|
index.resolve(engine, 0, 0, 15, 15);
|
||||||
|
assertEquals(578, resolver.resolutions());
|
||||||
|
index.resolve(engine, 0, 0, 15, 15);
|
||||||
|
assertEquals(578, resolver.resolutions());
|
||||||
|
|
||||||
|
index.evictRuntime(1);
|
||||||
|
runtimeId.set(1);
|
||||||
|
index.resolve(engine, 0, 0, 15, 15);
|
||||||
|
assertEquals(867, resolver.resolutions());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void pinnedRuntimeReusesCachedOriginsWithoutOpeningCoordinateScopes() throws Exception {
|
||||||
|
ScopedEngine scoped = scopedEngine(true, 1);
|
||||||
|
CountingResolver resolver = new CountingResolver(0, 0);
|
||||||
|
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
|
||||||
|
|
||||||
|
KList<NativeStructureVolume> first = index.resolve(scoped.engine(), 0, 0, 15, 15);
|
||||||
|
assertEquals(289, scoped.scopes().get());
|
||||||
|
index.resolve(scoped.engine(), 16, 0, 31, 15);
|
||||||
|
assertEquals(306, resolver.resolutions());
|
||||||
|
assertEquals(306, scoped.scopes().get());
|
||||||
|
assertEquals(first, index.resolve(scoped.engine(), 0, 0, 15, 15));
|
||||||
|
assertEquals(306, scoped.scopes().get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void absentOrMismatchedRuntimeOwnershipKeepsCoordinateRouting() throws Exception {
|
||||||
|
ScopedEngine detached = scopedEngine(true, 1);
|
||||||
|
when(detached.engine().getGenerationHistoryRuntimeRouter()).thenReturn(Optional.empty());
|
||||||
|
for (ScopedEngine scoped : List.of(scopedEngine(false, 1), scopedEngine(true, 0),
|
||||||
|
scopedEngine(true, 2), detached)) {
|
||||||
|
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
|
||||||
|
index.resolve(scoped.engine(), 0, 0, 15, 15);
|
||||||
|
index.resolve(scoped.engine(), 16, 0, 31, 15);
|
||||||
|
assertEquals(578, scoped.scopes().get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void retiredPinnedOriginsRebuildThroughCoordinateScopes() throws Exception {
|
||||||
|
ScopedEngine scoped = scopedEngine(true, 1);
|
||||||
|
CountingResolver resolver = new CountingResolver(0, 0);
|
||||||
|
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
|
||||||
|
|
||||||
|
KList<NativeStructureVolume> first = index.resolve(scoped.engine(), 0, 0, 15, 15);
|
||||||
|
index.evictRuntime(1);
|
||||||
|
assertEquals(first, index.resolve(scoped.engine(), 0, 0, 15, 15));
|
||||||
|
assertEquals(578, resolver.resolutions());
|
||||||
|
assertEquals(578, scoped.scopes().get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void invalidatedIndexReceivesRuntimeRetirementAndDetachesOnUninstall() throws Exception {
|
||||||
|
IrisEngine engine = mock(IrisEngine.class);
|
||||||
|
Set<IntConsumer> listeners = new CopyOnWriteArraySet<>();
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
listeners.add(invocation.getArgument(0));
|
||||||
|
return null;
|
||||||
|
}).when(engine).addGenerationRuntimeRetirementListener(any());
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
listeners.remove(invocation.getArgument(0));
|
||||||
|
return null;
|
||||||
|
}).when(engine).removeGenerationRuntimeRetirementListener(any());
|
||||||
|
NativeStructureVolumeIndex.Context context = new NativeStructureVolumeIndex.Context(
|
||||||
|
null, null, null, null, () -> null, () -> null, () -> null);
|
||||||
|
NativeStructureVolumeIndex.install(engine, context);
|
||||||
|
try {
|
||||||
|
assertEquals(1, listeners.size());
|
||||||
|
IntConsumer original = listeners.iterator().next();
|
||||||
|
NativeStructureVolumeIndex.invalidate(engine);
|
||||||
|
assertEquals(1, listeners.size());
|
||||||
|
assertFalse(listeners.contains(original));
|
||||||
|
|
||||||
|
NativeStructureVolumeIndex.volumes(engine, 0, 0, 15, 15);
|
||||||
|
NativeStructureVolumeIndex current = installedIndexes().get(engine);
|
||||||
|
assertEquals(289, cache(current, "originCache").size());
|
||||||
|
assertEquals(1, cache(current, "queryCache").size());
|
||||||
|
for (IntConsumer listener : listeners) {
|
||||||
|
listener.accept(0);
|
||||||
|
}
|
||||||
|
assertTrue(cache(current, "originCache").isEmpty());
|
||||||
|
assertTrue(cache(current, "queryCache").isEmpty());
|
||||||
|
} finally {
|
||||||
|
NativeStructureVolumeIndex.uninstall(engine);
|
||||||
|
}
|
||||||
|
assertTrue(listeners.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void retiringOriginBuildCannotRepublishWhileQueryEvictionWaits() throws Exception {
|
||||||
|
BlockingResolver resolver = new BlockingResolver();
|
||||||
|
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
|
||||||
|
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||||
|
Map<?, ?> origins = cache(index, "originCache");
|
||||||
|
Map<?, ?> queries = cache(index, "queryCache");
|
||||||
|
try {
|
||||||
|
Future<KList<NativeStructureVolume>> builder = executor.submit(() -> index.originVolumes(null, 0, 0));
|
||||||
|
assertTrue(resolver.awaitFirstEntry());
|
||||||
|
index.originVolumes(null, 1, 0);
|
||||||
|
assertEquals(1, origins.size());
|
||||||
|
Future<?> retirement;
|
||||||
|
synchronized (queries) {
|
||||||
|
retirement = executor.submit(() -> index.evictRuntime(0));
|
||||||
|
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
|
||||||
|
boolean cleared = false;
|
||||||
|
while (System.nanoTime() < deadline) {
|
||||||
|
synchronized (origins) {
|
||||||
|
cleared = origins.isEmpty();
|
||||||
|
}
|
||||||
|
if (cleared) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(10L));
|
||||||
|
}
|
||||||
|
assertTrue("Origin eviction did not complete before the query cache lock", cleared);
|
||||||
|
resolver.release();
|
||||||
|
assertTrue(builder.get(5, TimeUnit.SECONDS).isEmpty());
|
||||||
|
synchronized (origins) {
|
||||||
|
assertTrue("The retired in-flight origin was cached again", origins.isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
retirement.get(5, TimeUnit.SECONDS);
|
||||||
|
index.originVolumes(null, 0, 0);
|
||||||
|
assertEquals(3, resolver.resolutions());
|
||||||
|
} finally {
|
||||||
|
resolver.release();
|
||||||
|
executor.shutdownNow();
|
||||||
|
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void independentIndexesResolveIdenticalVolumesFromTheSameSeed() {
|
||||||
|
NativeStructureVolumeIndex first = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
|
||||||
|
NativeStructureVolumeIndex second = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
|
||||||
|
|
||||||
|
assertEquals(first.resolve(null, 0, 0, 15, 15), second.resolve(null, 0, 0, 15, 15));
|
||||||
|
assertEquals(first.resolve(null, -32, -32, 47, 47), second.resolve(null, -32, -32, 47, 47));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void volumesOutsideTheQueryRectAreExcluded() {
|
||||||
|
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
|
||||||
|
BoundingBox bounds = swampHut(0, 0, 4, 6).getPieces().getFirst().getBoundingBox();
|
||||||
|
|
||||||
|
KList<NativeStructureVolume> hit = index.resolve(null, bounds.minX(), bounds.minZ(), bounds.maxX(), bounds.maxZ());
|
||||||
|
KList<NativeStructureVolume> miss = index.resolve(null,
|
||||||
|
bounds.maxX() + 1, bounds.minZ(), bounds.maxX() + 8, bounds.maxZ());
|
||||||
|
|
||||||
|
assertEquals(1, hit.size());
|
||||||
|
assertTrue(miss.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void startsWithinTheOriginReachContributeAndBeyondItDoNot() {
|
||||||
|
int reach = NativeStructureVolumeIndex.originReachChunks();
|
||||||
|
NativeStructureVolumeIndex reachable = NativeStructureVolumeIndex.forTesting(new CountingResolver(reach, 0));
|
||||||
|
NativeStructureVolumeIndex unreachable = NativeStructureVolumeIndex.forTesting(new CountingResolver(reach + 1, 0));
|
||||||
|
|
||||||
|
assertFalse(reachable.resolve(null, 0, 0, 15, 15).isEmpty());
|
||||||
|
assertTrue(unreachable.resolve(null, 0, 0, 15, 15).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void overlappingColdQueriesCoordinateBeforeResolvingOrigins() throws Exception {
|
||||||
|
BlockingResolver resolver = new BlockingResolver();
|
||||||
|
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
|
||||||
|
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||||
|
try {
|
||||||
|
Future<KList<NativeStructureVolume>> first = executor.submit(() -> index.resolve(null, 0, 0, 15, 15));
|
||||||
|
assertTrue(resolver.awaitFirstEntry());
|
||||||
|
Future<KList<NativeStructureVolume>> adjacent = executor.submit(() -> index.resolve(null, 16, 0, 31, 15));
|
||||||
|
|
||||||
|
assertFalse(resolver.awaitSecondEntry(250));
|
||||||
|
resolver.release();
|
||||||
|
assertTrue(first.get(5, TimeUnit.SECONDS).isEmpty());
|
||||||
|
assertTrue(adjacent.get(5, TimeUnit.SECONDS).isEmpty());
|
||||||
|
assertEquals(306, resolver.resolutions());
|
||||||
|
} finally {
|
||||||
|
resolver.release();
|
||||||
|
executor.shutdownNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void contendedNativeWindowsAllowQueuedTerrainWorkToRun() throws Exception {
|
||||||
|
assertQueuedTerrainCanComplete(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void interruptedNativeWindowWaitStillAcquiresAndReleasesOnce() throws Exception {
|
||||||
|
assertQueuedTerrainCanComplete(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertQueuedTerrainCanComplete(boolean interruptWaiter) throws Exception {
|
||||||
|
CountDownLatch ownerEntered = new CountDownLatch(1);
|
||||||
|
CompletableFuture<Void> terrain = new CompletableFuture<>();
|
||||||
|
AtomicBoolean firstOrigin = new AtomicBoolean(true);
|
||||||
|
AtomicBoolean interruptedAfterQuery = new AtomicBoolean();
|
||||||
|
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting((engine, chunkX, chunkZ) -> {
|
||||||
|
if (firstOrigin.compareAndSet(true, false)) {
|
||||||
|
ownerEntered.countDown();
|
||||||
|
terrain.join();
|
||||||
|
}
|
||||||
|
return new KList<>(new NativeStructureVolume(chunkX + ":" + chunkZ,
|
||||||
|
-256, 0, -256, 256, 100, 256));
|
||||||
|
});
|
||||||
|
ExecutorService owner = Executors.newSingleThreadExecutor();
|
||||||
|
ForkJoinPool workers = new ForkJoinPool(2);
|
||||||
|
AtomicReference<Thread> firstWaiter = new AtomicReference<>();
|
||||||
|
AtomicReference<Thread> secondWaiter = new AtomicReference<>();
|
||||||
|
try {
|
||||||
|
Future<KList<NativeStructureVolume>> original = owner.submit(
|
||||||
|
() -> index.resolve(null, 0, 0, 15, 15));
|
||||||
|
assertTrue(ownerEntered.await(5, TimeUnit.SECONDS));
|
||||||
|
Future<KList<NativeStructureVolume>> adjacent = workers.submit(() -> {
|
||||||
|
firstWaiter.set(Thread.currentThread());
|
||||||
|
KList<NativeStructureVolume> result = index.resolve(null, 16, 0, 31, 15);
|
||||||
|
interruptedAfterQuery.set(Thread.currentThread().isInterrupted());
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
Future<KList<NativeStructureVolume>> next = workers.submit(() -> {
|
||||||
|
secondWaiter.set(Thread.currentThread());
|
||||||
|
return index.resolve(null, 32, 0, 47, 15);
|
||||||
|
});
|
||||||
|
awaitOriginWindowWaiter(firstWaiter);
|
||||||
|
awaitOriginWindowWaiter(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());
|
||||||
|
KList<NativeStructureVolume> adjacentVolumes = adjacent.get(3, TimeUnit.SECONDS);
|
||||||
|
assertEquals(289, adjacentVolumes.size());
|
||||||
|
assertEquals(adjacentVolumes, index.resolve(null, 16, 0, 31, 15));
|
||||||
|
assertEquals(289, next.get(3, TimeUnit.SECONDS).size());
|
||||||
|
assertEquals(interruptWaiter, interruptedAfterQuery.get());
|
||||||
|
} finally {
|
||||||
|
terrain.complete(null);
|
||||||
|
owner.shutdownNow();
|
||||||
|
workers.shutdownNow();
|
||||||
|
assertTrue(owner.awaitTermination(5, TimeUnit.SECONDS));
|
||||||
|
assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void awaitOriginWindowWaiter(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")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
|
||||||
|
}
|
||||||
|
throw new AssertionError("Native query did not wait for the occupied origin window");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void distantColdQueriesCanResolveOriginsConcurrently() throws Exception {
|
||||||
|
BlockingResolver resolver = new BlockingResolver();
|
||||||
|
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
|
||||||
|
int distantChunkX = findDisjointWindow(0, 0);
|
||||||
|
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||||
|
try {
|
||||||
|
Future<KList<NativeStructureVolume>> first = executor.submit(() -> index.resolve(null, 0, 0, 15, 15));
|
||||||
|
assertTrue(resolver.awaitFirstEntry());
|
||||||
|
Future<KList<NativeStructureVolume>> distant = executor.submit(
|
||||||
|
() -> index.resolve(null, distantChunkX << 4, 0, (distantChunkX << 4) + 15, 15));
|
||||||
|
|
||||||
|
assertTrue(resolver.awaitSecondEntry(5_000));
|
||||||
|
resolver.release();
|
||||||
|
assertTrue(first.get(5, TimeUnit.SECONDS).isEmpty());
|
||||||
|
assertTrue(distant.get(5, TimeUnit.SECONDS).isEmpty());
|
||||||
|
} finally {
|
||||||
|
resolver.release();
|
||||||
|
executor.shutdownNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void coordinatedResolutionPreservesCanonicalOriginOrder() {
|
||||||
|
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(new OrderedResolver());
|
||||||
|
|
||||||
|
KList<NativeStructureVolume> volumes = index.resolve(null, 0, 0, 15, 15);
|
||||||
|
|
||||||
|
assertEquals(289, volumes.size());
|
||||||
|
assertEquals("-8:-8", volumes.getFirst().structure());
|
||||||
|
assertEquals("0:0", volumes.get(144).structure());
|
||||||
|
assertEquals("8:8", volumes.getLast().structure());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ScopedEngine scopedEngine(boolean scoped, int ownerRuntimeId) throws Exception {
|
||||||
|
IrisEngine engine = mock(IrisEngine.class);
|
||||||
|
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
|
||||||
|
when(engine.hasGenerationRuntimeScope()).thenReturn(scoped);
|
||||||
|
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
|
||||||
|
when(engine.getCacheID()).thenReturn(1);
|
||||||
|
if (ownerRuntimeId != 0) {
|
||||||
|
IrisEngine.GenerationRuntimeBinding binding = mock(IrisEngine.GenerationRuntimeBinding.class);
|
||||||
|
when(binding.runtimeId()).thenReturn(ownerRuntimeId);
|
||||||
|
when(router.currentRuntimeOwnership()).thenReturn(Optional.of(
|
||||||
|
new GenerationHistoryRuntimeRouter.RuntimeOwnership(1L, binding)));
|
||||||
|
}
|
||||||
|
AtomicInteger scopes = new AtomicInteger();
|
||||||
|
when(engine.openGenerationHistoryCoordinateScope(anyInt(), anyInt())).thenAnswer(invocation -> {
|
||||||
|
scopes.incrementAndGet();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
return new ScopedEngine(engine, scopes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static Map<Engine, NativeStructureVolumeIndex> installedIndexes() throws Exception {
|
||||||
|
Field indexes = NativeStructureVolumeIndex.class.getDeclaredField("INDEXES");
|
||||||
|
indexes.setAccessible(true);
|
||||||
|
return (Map<Engine, NativeStructureVolumeIndex>) indexes.get(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<?, ?> cache(NativeStructureVolumeIndex index, String name) throws Exception {
|
||||||
|
Field field = NativeStructureVolumeIndex.class.getDeclaredField(name);
|
||||||
|
field.setAccessible(true);
|
||||||
|
return (Map<?, ?>) field.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureStart swampHut(int chunkX, int chunkZ, int x, int z) {
|
||||||
|
Structure source = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty()));
|
||||||
|
SwampHutPiece piece = new SwampHutPiece(RandomSource.create(17L), x, z);
|
||||||
|
return new StructureStart(source, new ChunkPos(chunkX, chunkZ), 0, new PiecesContainer(List.of(piece)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Engine runtimeEngine(AtomicInteger runtimeId) {
|
||||||
|
return (Engine) Proxy.newProxyInstance(
|
||||||
|
Engine.class.getClassLoader(),
|
||||||
|
new Class<?>[]{Engine.class},
|
||||||
|
(proxy, method, arguments) -> {
|
||||||
|
if (method.getName().equals("getCacheID")) {
|
||||||
|
return runtimeId.get();
|
||||||
|
}
|
||||||
|
if (method.getName().equals("hashCode")) {
|
||||||
|
return System.identityHashCode(proxy);
|
||||||
|
}
|
||||||
|
if (method.getName().equals("equals")) {
|
||||||
|
return proxy == arguments[0];
|
||||||
|
}
|
||||||
|
if (method.getName().equals("toString")) {
|
||||||
|
return "runtime-engine-" + runtimeId.get();
|
||||||
|
}
|
||||||
|
throw new UnsupportedOperationException(method.getName());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
private final int originChunkZ;
|
||||||
|
|
||||||
|
private CountingResolver(int originChunkX, int originChunkZ) {
|
||||||
|
this.originChunkX = originChunkX;
|
||||||
|
this.originChunkZ = originChunkZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int resolutions() {
|
||||||
|
return resolutions.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KList<NativeStructureVolume> volumesAt(Engine engine, int chunkX, int chunkZ) {
|
||||||
|
resolutions.incrementAndGet();
|
||||||
|
if (chunkX != originChunkX || chunkZ != originChunkZ) {
|
||||||
|
return NativeStructureVolume.NONE;
|
||||||
|
}
|
||||||
|
return NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, swampHut(chunkX, chunkZ, 4, 6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class BlockingResolver implements NativeStructureVolumeIndex.OriginResolver {
|
||||||
|
private final AtomicInteger resolutions = new AtomicInteger();
|
||||||
|
private final CountDownLatch firstEntry = new CountDownLatch(1);
|
||||||
|
private final CountDownLatch secondEntry = new CountDownLatch(1);
|
||||||
|
private final CountDownLatch release = new CountDownLatch(1);
|
||||||
|
|
||||||
|
private int resolutions() {
|
||||||
|
return resolutions.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean awaitFirstEntry() throws InterruptedException {
|
||||||
|
return firstEntry.await(5, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean awaitSecondEntry(long timeoutMillis) throws InterruptedException {
|
||||||
|
return secondEntry.await(timeoutMillis, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void release() {
|
||||||
|
release.countDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KList<NativeStructureVolume> volumesAt(Engine engine, int chunkX, int chunkZ) {
|
||||||
|
int resolution = resolutions.incrementAndGet();
|
||||||
|
if (resolution == 1) {
|
||||||
|
firstEntry.countDown();
|
||||||
|
try {
|
||||||
|
if (!release.await(5, TimeUnit.SECONDS)) {
|
||||||
|
throw new IllegalStateException("Timed out waiting to release native structure resolution");
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IllegalStateException("Interrupted while coordinating native structure resolution", e);
|
||||||
|
}
|
||||||
|
} else if (resolution == 2) {
|
||||||
|
secondEntry.countDown();
|
||||||
|
}
|
||||||
|
return NativeStructureVolume.NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class OrderedResolver implements NativeStructureVolumeIndex.OriginResolver {
|
||||||
|
@Override
|
||||||
|
public KList<NativeStructureVolume> volumesAt(Engine engine, int chunkX, int chunkZ) {
|
||||||
|
return new KList<>(new NativeStructureVolume(
|
||||||
|
chunkX + ":" + chunkZ,
|
||||||
|
0, 0, 0,
|
||||||
|
15, 255, 15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private record ScopedEngine(IrisEngine engine, AtomicInteger scopes) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+129
@@ -0,0 +1,129 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.OptionalInt;
|
||||||
|
import java.util.function.IntSupplier;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class NativeTerrainHeightCacheTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrap() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void coordinatesRuntimePredicatesAndBoundsHaveSeparateEntries() {
|
||||||
|
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
|
||||||
|
AtomicInteger calls = new AtomicInteger();
|
||||||
|
NativeTerrainHeightCache.Query[] queries = {
|
||||||
|
query(1, -1, 1),
|
||||||
|
query(1, 1, -1),
|
||||||
|
query(2, -1, 1),
|
||||||
|
new NativeTerrainHeightCache.Query(1, -1, 1, Heightmap.Types.OCEAN_FLOOR, -256, 768),
|
||||||
|
new NativeTerrainHeightCache.Query(1, -1, 1, Heightmap.Types.WORLD_SURFACE, -64, 768),
|
||||||
|
new NativeTerrainHeightCache.Query(1, -1, 1, Heightmap.Types.WORLD_SURFACE, -256, 256)
|
||||||
|
};
|
||||||
|
for (int index = 0; index < queries.length; index++) {
|
||||||
|
assertEquals(index + 1, cached(cache, queries[index], calls::incrementAndGet));
|
||||||
|
}
|
||||||
|
for (int index = 0; index < queries.length; index++) {
|
||||||
|
assertEquals(index + 1, cached(cache, queries[index], calls::incrementAndGet));
|
||||||
|
}
|
||||||
|
assertEquals(queries.length, calls.get());
|
||||||
|
cache.evictRuntime(1);
|
||||||
|
assertEquals(3, cached(cache, queries[2], calls::incrementAndGet));
|
||||||
|
assertEquals(7, cached(cache, queries[0], calls::incrementAndGet));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void allIntegerResultsAreCacheable() {
|
||||||
|
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
|
||||||
|
NativeTerrainHeightCache.Query query = query(1, 0, 0);
|
||||||
|
assertEquals(Integer.MIN_VALUE, cached(cache, query, () -> Integer.MIN_VALUE));
|
||||||
|
assertEquals(Integer.MIN_VALUE, cached(cache, query, () -> 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failedResolutionCanBeRetried() {
|
||||||
|
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
|
||||||
|
NativeTerrainHeightCache.Query query = query(1, 0, 0);
|
||||||
|
assertThrows(IllegalStateException.class, () -> cached(cache, query, () -> {
|
||||||
|
throw new IllegalStateException("terrain unavailable");
|
||||||
|
}));
|
||||||
|
assertEquals(42, cached(cache, query, () -> 42));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void coldResolutionDoesNotHoldCacheMonitor() throws Exception {
|
||||||
|
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
|
||||||
|
CountDownLatch entered = new CountDownLatch(1);
|
||||||
|
CountDownLatch release = new CountDownLatch(1);
|
||||||
|
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||||
|
try {
|
||||||
|
Future<Integer> slow = executor.submit(() -> cached(cache, query(1, 0, 0), () -> {
|
||||||
|
entered.countDown();
|
||||||
|
try {
|
||||||
|
assertTrue(release.await(5, TimeUnit.SECONDS));
|
||||||
|
} catch (InterruptedException interruption) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IllegalStateException(interruption);
|
||||||
|
}
|
||||||
|
return 9;
|
||||||
|
}));
|
||||||
|
assertTrue(entered.await(5, TimeUnit.SECONDS));
|
||||||
|
Future<Integer> independent = executor.submit(() -> cached(cache, query(1, 1, 0), () -> 17));
|
||||||
|
assertEquals(Integer.valueOf(17), independent.get(1, TimeUnit.SECONDS));
|
||||||
|
release.countDown();
|
||||||
|
assertEquals(Integer.valueOf(9), slow.get(5, TimeUnit.SECONDS));
|
||||||
|
} finally {
|
||||||
|
release.countDown();
|
||||||
|
executor.shutdownNow();
|
||||||
|
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void boundedCacheKeepsRecentlyReadCoordinates() {
|
||||||
|
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
|
||||||
|
for (int x = 0; x < 65_536; x++) {
|
||||||
|
cached(cache, query(1, x, 0), () -> 8);
|
||||||
|
}
|
||||||
|
assertEquals(8, cached(cache, query(1, 0, 0), () -> 99));
|
||||||
|
cached(cache, query(1, 65_536, 0), () -> 9);
|
||||||
|
assertEquals(8, cached(cache, query(1, 0, 0), () -> 99));
|
||||||
|
assertEquals(99, cached(cache, query(1, 1, 0), () -> 99));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unavailableResolvedHeightIsNotCached() {
|
||||||
|
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
|
||||||
|
NativeTerrainHeightCache.Query query = query(1, 0, 0);
|
||||||
|
assertTrue(cache.resolvedHeight(query, OptionalInt::empty).isEmpty());
|
||||||
|
assertEquals(42, cache.resolvedHeight(query, () -> OptionalInt.of(42)).orElseThrow());
|
||||||
|
assertEquals(42, cache.resolvedHeight(query, OptionalInt::empty).orElseThrow());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int cached(NativeTerrainHeightCache cache, NativeTerrainHeightCache.Query query,
|
||||||
|
IntSupplier resolver) {
|
||||||
|
return cache.resolvedHeight(query, () -> OptionalInt.of(resolver.getAsInt())).orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NativeTerrainHeightCache.Query query(int runtimeId, int x, int z) {
|
||||||
|
return new NativeTerrainHeightCache.Query(runtimeId, x, z, Heightmap.Types.WORLD_SURFACE, -256, 768);
|
||||||
|
}
|
||||||
|
}
|
||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.world.history.BoundaryColumnGeometry;
|
||||||
|
import art.arcane.iris.world.history.TerrainBoundarySignature;
|
||||||
|
import art.arcane.iris.spi.IrisPlatform;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
|
import art.arcane.iris.spi.PlatformRegistries;
|
||||||
|
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
|
import net.minecraft.world.level.NoiseColumn;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.RotatedPillarBlock;
|
||||||
|
import net.minecraft.world.level.block.LiquidBlock;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.OptionalInt;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public final class NativeTransitionColumnTest {
|
||||||
|
@Rule
|
||||||
|
public final PlatformLeakGuard leakGuard = PlatformLeakGuard.clean();
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrap() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
NativeBlockTags.bindHeightmapFixtures();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void customKeysUseCarrierForNativeColumnsAndHeightmaps() {
|
||||||
|
String key = "itemsadder:rocks/ruby_ore";
|
||||||
|
PlatformBlockState custom = mock(PlatformBlockState.class);
|
||||||
|
PlatformBlockState carrier = mock(PlatformBlockState.class);
|
||||||
|
when(custom.placementBaseState()).thenReturn(carrier);
|
||||||
|
when(carrier.key()).thenReturn("minecraft:oak_log[axis=z]");
|
||||||
|
IrisPlatform platform = mock(IrisPlatform.class);
|
||||||
|
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||||
|
when(platform.registries()).thenReturn(registries);
|
||||||
|
when(registries.blockOrNull(key)).thenReturn(custom);
|
||||||
|
LevelHeightAccessor height = LevelHeightAccessor.create(-16, 32);
|
||||||
|
TerrainBoundarySignature signature = signature(key);
|
||||||
|
|
||||||
|
try (MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class)) {
|
||||||
|
platforms.when(IrisPlatforms::isBound).thenReturn(true);
|
||||||
|
platforms.when(IrisPlatforms::get).thenReturn(platform);
|
||||||
|
NoiseColumn column = NativeTransitionColumn.column(signature, height);
|
||||||
|
|
||||||
|
assertEquals(Direction.Axis.Z, column.getBlock(-15).getValue(RotatedPillarBlock.AXIS));
|
||||||
|
assertEquals(-14, NativeTransitionColumn.height(signature, Heightmap.Types.OCEAN_FLOOR_WG, height));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void preservesNativePropertiesCavesAndFluids() {
|
||||||
|
NoiseColumn column = NativeTransitionColumn.column(signature("minecraft:oak_log[axis=x]"),
|
||||||
|
LevelHeightAccessor.create(-16, 32));
|
||||||
|
assertEquals(Direction.Axis.X, column.getBlock(-15).getValue(RotatedPillarBlock.AXIS));
|
||||||
|
assertTrue(column.getBlock(-14).is(Blocks.CAVE_AIR));
|
||||||
|
assertEquals(Integer.valueOf(3), column.getBlock(-13).getValue(LiquidBlock.LEVEL));
|
||||||
|
assertTrue(column.getBlock(0).isAir());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void usesNativeHeightmapPredicatesForWaterAndOpenSpace() {
|
||||||
|
TerrainBoundarySignature signature = signature("minecraft:oak_log[axis=x]");
|
||||||
|
LevelHeightAccessor height = LevelHeightAccessor.create(-16, 32);
|
||||||
|
assertEquals(-12, NativeTransitionColumn.height(signature, Heightmap.Types.WORLD_SURFACE_WG, height));
|
||||||
|
assertEquals(-14, NativeTransitionColumn.height(signature, Heightmap.Types.OCEAN_FLOOR_WG, height));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsUnknownSavedStatesInsteadOfReplacingTerrain() {
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> NativeTransitionColumn.column(
|
||||||
|
signature("missing:removed_block"), LevelHeightAccessor.create(-16, 32)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TerrainBoundarySignature signature(String solidState) {
|
||||||
|
BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-16, List.of(
|
||||||
|
new BoundaryColumnGeometry.Voxel("minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false),
|
||||||
|
new BoundaryColumnGeometry.Voxel(solidState, BoundaryColumnGeometry.Phase.SOLID, "", false),
|
||||||
|
new BoundaryColumnGeometry.Voxel("minecraft:cave_air", BoundaryColumnGeometry.Phase.AIR, "", false),
|
||||||
|
new BoundaryColumnGeometry.Voxel("minecraft:water[level=3]", BoundaryColumnGeometry.Phase.FLUID,
|
||||||
|
"minecraft:water[level=3]", false)));
|
||||||
|
return new TerrainBoundarySignature(
|
||||||
|
new TerrainBoundarySignature.Column(0, 0, 3, 1, OptionalInt.of(3), OptionalInt.empty()),
|
||||||
|
new TerrainBoundarySignature.Samples(new TerrainBoundarySignature.VerticalLayout(-16, 4, 1),
|
||||||
|
new TerrainBoundarySignature.BiomeEncoding(List.of("minecraft:plains"), new short[]{0})),
|
||||||
|
geometry);
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.tags.BlockTags;
|
||||||
|
import net.minecraft.tags.TagLoader;
|
||||||
|
import net.minecraft.world.level.block.Block;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class NativeBlockTags {
|
||||||
|
private NativeBlockTags() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void bindHeightmapFixtures() {
|
||||||
|
List<Holder<Block>> solid = List.of(Blocks.STONE.builtInRegistryHolder(),
|
||||||
|
Blocks.OAK_LOG.builtInRegistryHolder(), Blocks.SPRUCE_LOG.builtInRegistryHolder());
|
||||||
|
List<Holder<Block>> canopy = List.of(Blocks.STONE.builtInRegistryHolder(),
|
||||||
|
Blocks.OAK_LOG.builtInRegistryHolder(), Blocks.SPRUCE_LOG.builtInRegistryHolder(),
|
||||||
|
Blocks.OAK_LEAVES.builtInRegistryHolder(), Blocks.SPRUCE_LEAVES.builtInRegistryHolder());
|
||||||
|
BuiltInRegistries.BLOCK.prepareTagReload(new TagLoader.LoadResult<>(Registries.BLOCK, Map.of(
|
||||||
|
BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP, canopy,
|
||||||
|
BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP_NO_LEAVES, solid))).apply();
|
||||||
|
}
|
||||||
|
}
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.biome.IrisBiomeCustom;
|
||||||
|
import art.arcane.iris.generation.biome.IrisBiomeCustomSpawn;
|
||||||
|
import art.arcane.iris.generation.biome.IrisBiomeCustomSpawnType;
|
||||||
|
import art.arcane.iris.platform.bukkit.nms.datapack.v263.DataFixerV263;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import com.mojang.serialization.JsonOps;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.HolderLookup;
|
||||||
|
import net.minecraft.data.registries.VanillaRegistries;
|
||||||
|
import net.minecraft.resources.RegistryOps;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.world.attribute.EnvironmentAttributes;
|
||||||
|
import net.minecraft.world.entity.MobCategory;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.biome.MobSpawnSettings;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
public class CustomBiomeCodecTest {
|
||||||
|
private static HolderLookup.Provider registries;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrap() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
registries = VanillaRegistries.createWorldLookup();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void nativeCodecAcceptsCustomColorsAndSpawnCounts() {
|
||||||
|
IrisBiomeCustom custom = new IrisBiomeCustom();
|
||||||
|
custom.setId("codec_test");
|
||||||
|
custom.setWaterColor("#102030");
|
||||||
|
custom.setFogColor("#405060");
|
||||||
|
custom.setSpawnRarity(2);
|
||||||
|
IrisBiomeCustomSpawn spawn = new IrisBiomeCustomSpawn();
|
||||||
|
spawn.setType("minecraft:zombie");
|
||||||
|
spawn.setGroup(IrisBiomeCustomSpawnType.MONSTER);
|
||||||
|
spawn.setMinCount(2);
|
||||||
|
spawn.setMaxCount(4);
|
||||||
|
spawn.setWeight(7);
|
||||||
|
custom.setSpawns(new KList<>(spawn));
|
||||||
|
Biome biome = Biome.DIRECT_CODEC.parse(RegistryOps.create(JsonOps.INSTANCE, registries),
|
||||||
|
JsonParser.parseString(custom.generateJson(new DataFixerV263()))).getOrThrow();
|
||||||
|
MobSpawnSettings settings = biome.getAttributes().applyModifier(EnvironmentAttributes.NATURAL_MOB_SPAWNS,
|
||||||
|
EnvironmentAttributes.NATURAL_MOB_SPAWNS.defaultValue());
|
||||||
|
MobSpawnSettings.SpawnerData decoded = settings.getMobsToSpawn(MobCategory.MONSTER).unwrap().getFirst().value();
|
||||||
|
assertEquals(2, decoded.count().minInclusive());
|
||||||
|
assertEquals(4, decoded.count().maxInclusive());
|
||||||
|
assertEquals(0x102030, biome.getWaterColor() & 0xFFFFFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class CustomBiomeSourceStructureContractTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraftRegistries() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void stackedCustomBiomesUseTheirOwningDimensionNamespace() {
|
||||||
|
IrisDimension host = new IrisDimension();
|
||||||
|
host.setLoadKey("Host");
|
||||||
|
IrisDimension upper = new IrisDimension();
|
||||||
|
upper.setLoadKey("Layers/Upper");
|
||||||
|
|
||||||
|
assertEquals("layers:upper/aurora", CustomBiomeSource.customBiomeKey(upper, "Aurora"));
|
||||||
|
assertNotEquals(
|
||||||
|
CustomBiomeSource.customBiomeKey(host, "Aurora"),
|
||||||
|
CustomBiomeSource.customBiomeKey(upper, "Aurora")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void temporaryNaturalBiomeAnswersAreNotMemoized() {
|
||||||
|
Engine temporary = engineAnsweringNaturalFallback(true, 12, -8);
|
||||||
|
Engine stable = engineAnsweringNaturalFallback(false, 12, -8);
|
||||||
|
assertFalse(CustomBiomeSource.isBiomeCacheable(temporary, 12, -8));
|
||||||
|
assertTrue(CustomBiomeSource.isBiomeCacheable(stable, 12, -8));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Engine engineAnsweringNaturalFallback(
|
||||||
|
boolean naturalFallback,
|
||||||
|
int expectedX,
|
||||||
|
int expectedZ
|
||||||
|
) {
|
||||||
|
return (Engine) Proxy.newProxyInstance(
|
||||||
|
Engine.class.getClassLoader(),
|
||||||
|
new Class<?>[]{Engine.class},
|
||||||
|
(proxy, method, arguments) -> {
|
||||||
|
if (!method.getName().equals("answersFromNaturalTerrain")) {
|
||||||
|
throw new UnsupportedOperationException(method.getName());
|
||||||
|
}
|
||||||
|
assertEquals(Integer.valueOf(expectedX), arguments[0]);
|
||||||
|
assertEquals(Integer.valueOf(expectedZ), arguments[1]);
|
||||||
|
return naturalFallback;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.concurrent.CancellationException;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CompletionException;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.Semaphore;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.locks.LockSupport;
|
||||||
|
|
||||||
|
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 class IrisChunkGeneratorFailureContractTest {
|
||||||
|
@Test
|
||||||
|
public void releasedNoiseAdmissionLetsSynchronousDependentWaitForQueuedExclusive() throws Exception {
|
||||||
|
Semaphore admission = new Semaphore(1, true);
|
||||||
|
admission.acquire();
|
||||||
|
CountDownLatch exclusiveEntered = new CountDownLatch(1);
|
||||||
|
CountDownLatch releaseExclusive = new CountDownLatch(1);
|
||||||
|
AtomicBoolean dependentFinished = new AtomicBoolean(false);
|
||||||
|
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||||
|
boolean stageReleased = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Future<?> exclusive = executor.submit(() -> {
|
||||||
|
boolean acquired = false;
|
||||||
|
try {
|
||||||
|
admission.acquire();
|
||||||
|
acquired = true;
|
||||||
|
exclusiveEntered.countDown();
|
||||||
|
assertTrue(releaseExclusive.await(2, TimeUnit.SECONDS));
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
} finally {
|
||||||
|
if (acquired) {
|
||||||
|
admission.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
awaitQueueLength(admission, 1);
|
||||||
|
CompletableFuture<Void> outward = new CompletableFuture<>();
|
||||||
|
outward.thenRun(() -> {
|
||||||
|
try {
|
||||||
|
assertTrue(exclusiveEntered.await(2, TimeUnit.SECONDS));
|
||||||
|
dependentFinished.set(true);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
admission.release();
|
||||||
|
stageReleased = true;
|
||||||
|
assertTrue(outward.complete(null));
|
||||||
|
assertTrue(dependentFinished.get());
|
||||||
|
releaseExclusive.countDown();
|
||||||
|
exclusive.get(2, TimeUnit.SECONDS);
|
||||||
|
assertEquals(1, admission.availablePermits());
|
||||||
|
} finally {
|
||||||
|
if (!stageReleased) {
|
||||||
|
admission.release();
|
||||||
|
}
|
||||||
|
releaseExclusive.countDown();
|
||||||
|
executor.shutdownNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void transformedDelegateCancellationIsDetectedThroughItsCompletionFailure() {
|
||||||
|
CompletableFuture<Void> delegate = new CompletableFuture<>();
|
||||||
|
CompletableFuture<Void> transformed = delegate.thenApply(value -> value);
|
||||||
|
|
||||||
|
assertTrue(delegate.cancel(false));
|
||||||
|
assertFalse(transformed.isCancelled());
|
||||||
|
CompletionException failure = assertThrows(CompletionException.class, transformed::join);
|
||||||
|
assertTrue(failure.getCause() instanceof CancellationException);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void awaitQueueLength(Semaphore semaphore, int expected) {
|
||||||
|
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2);
|
||||||
|
while (semaphore.getQueueLength() < expected && System.nanoTime() < deadline) {
|
||||||
|
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
|
||||||
|
}
|
||||||
|
assertTrue("Expected at least " + expected + " queued semaphore threads",
|
||||||
|
semaphore.getQueueLength() >= expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.nativegen.v26_3_R1.NativeStructureLocateResults;
|
||||||
|
import com.mojang.datafixers.util.Pair;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
|
||||||
|
public class IrisChunkGeneratorMonumentLocateContractTest {
|
||||||
|
@Test
|
||||||
|
public void mixedLocateSelectsNearestProviderAndPrefersNativeOnTie() {
|
||||||
|
BlockPos origin = BlockPos.ZERO;
|
||||||
|
Pair<BlockPos, Holder<Structure>> irisNear = Pair.of(new BlockPos(4, 70, 0), null);
|
||||||
|
Pair<BlockPos, Holder<Structure>> nativeFar = Pair.of(new BlockPos(8, 70, 0), null);
|
||||||
|
Pair<BlockPos, Holder<Structure>> nativeNear = Pair.of(new BlockPos(2, 70, 0), null);
|
||||||
|
Pair<BlockPos, Holder<Structure>> nativeTie = Pair.of(new BlockPos(0, 70, 4), null);
|
||||||
|
|
||||||
|
assertSame(irisNear, NativeStructureLocateResults.nearest(origin, irisNear, nativeFar));
|
||||||
|
assertSame(nativeNear, NativeStructureLocateResults.nearest(origin, irisNear, nativeNear));
|
||||||
|
assertSame(nativeTie, NativeStructureLocateResults.nearest(origin, irisNear, nativeTie));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mixedUnexploredLocateReferencesOnlyTheSelectedProvider() {
|
||||||
|
BlockPos origin = BlockPos.ZERO;
|
||||||
|
Pair<BlockPos, Holder<Structure>> irisNear = Pair.of(new BlockPos(4, 70, 0), null);
|
||||||
|
Pair<BlockPos, Holder<Structure>> nativeFar = Pair.of(new BlockPos(8, 70, 0), null);
|
||||||
|
AtomicInteger irisReferences = new AtomicInteger();
|
||||||
|
AtomicInteger nativeReferences = new AtomicInteger();
|
||||||
|
|
||||||
|
Pair<BlockPos, Holder<Structure>> irisSelected =
|
||||||
|
NativeStructureLocateResults.selectAndReference(
|
||||||
|
origin,
|
||||||
|
irisNear, () -> irisReferences.incrementAndGet(),
|
||||||
|
nativeFar, () -> nativeReferences.incrementAndGet());
|
||||||
|
|
||||||
|
assertSame(irisNear, irisSelected);
|
||||||
|
assertEquals(1, irisReferences.get());
|
||||||
|
assertEquals(0, nativeReferences.get());
|
||||||
|
|
||||||
|
Pair<BlockPos, Holder<Structure>> nativeNear = Pair.of(new BlockPos(2, 70, 0), null);
|
||||||
|
Pair<BlockPos, Holder<Structure>> nativeSelected =
|
||||||
|
NativeStructureLocateResults.selectAndReference(
|
||||||
|
origin,
|
||||||
|
irisNear, () -> irisReferences.incrementAndGet(),
|
||||||
|
nativeNear, () -> nativeReferences.incrementAndGet());
|
||||||
|
|
||||||
|
assertSame(nativeNear, nativeSelected);
|
||||||
|
assertEquals(1, irisReferences.get());
|
||||||
|
assertEquals(1, nativeReferences.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
+148
@@ -0,0 +1,148 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
||||||
|
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||||
|
import art.arcane.iris.generation.runtime.GenerationSessionManager;
|
||||||
|
import art.arcane.iris.generation.runtime.GenerationTransitionGate;
|
||||||
|
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||||
|
import art.arcane.iris.world.history.NativeBiomeSpawnSelection;
|
||||||
|
import art.arcane.iris.world.history.SavedBiomeRuntime;
|
||||||
|
import art.arcane.iris.generation.context.IrisContext;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.util.random.WeightedList;
|
||||||
|
import net.minecraft.world.entity.MobCategory;
|
||||||
|
import net.minecraft.world.level.StructureManager;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.biome.MobSpawnSettings;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class IrisChunkGeneratorSpawnAdmissionTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraftRegistries() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void tickSpawnQueryLeavesOwnerThreadAvailableForBoundaryCheckpoint() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
ExecutorService owner = Executors.newSingleThreadExecutor();
|
||||||
|
ExecutorService reloader = Executors.newSingleThreadExecutor();
|
||||||
|
AtomicInteger snapshots = new AtomicInteger();
|
||||||
|
long previousSession = fixture.sessions.currentSessionId();
|
||||||
|
try {
|
||||||
|
assertSame(fixture.expected, owner.submit(fixture::query).get(2L, TimeUnit.SECONDS));
|
||||||
|
assertEquals(previousSession, fixture.readSession.get());
|
||||||
|
CompletableFuture<Void> cutover = CompletableFuture.runAsync(() -> {
|
||||||
|
try (GenerationTransitionGate.Transition ignored = fixture.sessions.transitionGate().beginTransition(2_000L)) {
|
||||||
|
fixture.sessions.sealAndAwait("Studio generation cutover", 2_000L);
|
||||||
|
Future<?> query = owner.submit(() -> {
|
||||||
|
assertTrue(fixture.query().isEmpty());
|
||||||
|
assertNull(IrisContext.get());
|
||||||
|
});
|
||||||
|
owner.submit(snapshots::incrementAndGet).get(2L, TimeUnit.SECONDS);
|
||||||
|
query.get(2L, TimeUnit.SECONDS);
|
||||||
|
assertEquals(1, fixture.savedReads.get());
|
||||||
|
assertEquals(1, fixture.routes.get());
|
||||||
|
fixture.sessions.activateNextSession();
|
||||||
|
} catch (Exception failure) {
|
||||||
|
throw new IllegalStateException(failure);
|
||||||
|
}
|
||||||
|
}, reloader);
|
||||||
|
|
||||||
|
cutover.get(3L, TimeUnit.SECONDS);
|
||||||
|
assertEquals(1, snapshots.get());
|
||||||
|
assertEquals(0, fixture.sessions.activeLeases());
|
||||||
|
assertSame(fixture.expected, owner.submit(fixture::query).get(2L, TimeUnit.SECONDS));
|
||||||
|
assertEquals(fixture.sessions.currentSessionId(), fixture.readSession.get());
|
||||||
|
assertTrue(fixture.readSession.get() > previousSession);
|
||||||
|
assertEquals(2, fixture.savedReads.get());
|
||||||
|
assertEquals(2, fixture.routes.get());
|
||||||
|
assertEquals(0, fixture.sessions.activeLeases());
|
||||||
|
} finally {
|
||||||
|
owner.shutdownNow();
|
||||||
|
reloader.shutdownNow();
|
||||||
|
assertTrue(owner.awaitTermination(2L, TimeUnit.SECONDS));
|
||||||
|
assertTrue(reloader.awaitTermination(2L, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setField(IrisChunkGenerator generator, String name, Object value) throws Exception {
|
||||||
|
Field field = IrisChunkGenerator.class.getDeclaredField(name);
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(generator, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Fixture {
|
||||||
|
private final GenerationSessionManager sessions = new GenerationSessionManager(true);
|
||||||
|
private final IrisEngine engine = mock(IrisEngine.class);
|
||||||
|
private final IrisChunkGenerator generator = mock(IrisChunkGenerator.class, CALLS_REAL_METHODS);
|
||||||
|
private final Holder<Biome> biome = Holder.direct(mock(Biome.class));
|
||||||
|
private final StructureManager structures = mock(StructureManager.class);
|
||||||
|
private final WeightedList<MobSpawnSettings.SpawnerData> expected = mock(WeightedList.class);
|
||||||
|
private final ServerLevel level = mock(ServerLevel.class);
|
||||||
|
private final AtomicInteger savedReads = new AtomicInteger();
|
||||||
|
private final AtomicInteger routes = new AtomicInteger();
|
||||||
|
private final AtomicLong readSession = new AtomicLong();
|
||||||
|
|
||||||
|
private Fixture() throws Exception {
|
||||||
|
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
|
||||||
|
SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class);
|
||||||
|
ChunkGenerator delegate = mock(ChunkGenerator.class);
|
||||||
|
CustomBiomeSource source = mock(CustomBiomeSource.class);
|
||||||
|
when(level.getBiome(new BlockPos(0, 64, 0))).thenReturn(biome);
|
||||||
|
setField(generator, "runtimeLevel", level);
|
||||||
|
when(engine.getGenerationSessions()).thenReturn(sessions);
|
||||||
|
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
|
||||||
|
when(router.biomes()).thenReturn(saved);
|
||||||
|
when(saved.nativeSpawnSelection(0, 64, 0, "")).thenAnswer(invocation -> {
|
||||||
|
assertAdmitted();
|
||||||
|
savedReads.incrementAndGet();
|
||||||
|
return new NativeBiomeSpawnSelection(NativeBiomeSpawnSelection.Mode.RETAINED, "minecraft:plains");
|
||||||
|
});
|
||||||
|
when(engine.openGenerationHistoryCoordinateScope(0, 0)).thenAnswer(invocation -> {
|
||||||
|
assertAdmitted();
|
||||||
|
routes.incrementAndGet();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
when(delegate.getMobsAt(level, structures, MobCategory.MONSTER, new BlockPos(0, 64, 0)))
|
||||||
|
.thenReturn(expected);
|
||||||
|
setField(generator, "engine", engine);
|
||||||
|
setField(generator, "delegate", delegate);
|
||||||
|
setField(generator, "customBiomeSource", source);
|
||||||
|
}
|
||||||
|
|
||||||
|
private WeightedList<MobSpawnSettings.SpawnerData> query() {
|
||||||
|
return generator.getMobsAt(level, structures, MobCategory.MONSTER, new BlockPos(0, 64, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertAdmitted() {
|
||||||
|
assertEquals(1, sessions.activeLeases());
|
||||||
|
assertSame(engine, IrisContext.get().getEngine());
|
||||||
|
readSession.set(IrisContext.get().getGenerationSessionId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+437
@@ -0,0 +1,437 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
||||||
|
|
||||||
|
import net.minecraft.world.level.levelgen.structure.placement.AbstractSpreadingStructurePlacement;
|
||||||
|
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
||||||
|
import art.arcane.iris.pack.datapack.DatapackStructureScopeIndex;
|
||||||
|
import art.arcane.iris.structure.nativegen.IrisImportedStructureControl;
|
||||||
|
import art.arcane.iris.structure.placement.IrisStructureSetFrequencyOverride;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import com.mojang.datafixers.util.Either;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.HolderOwner;
|
||||||
|
import net.minecraft.core.Vec3i;
|
||||||
|
import net.minecraft.core.component.DataComponentMap;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.tags.TagKey;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||||
|
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.placement.StructurePlacement;
|
||||||
|
import com.mojang.serialization.MapCodec;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNotSame;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
|
||||||
|
public class NMSBindingDatapackStructureScopeTest {
|
||||||
|
private static final String SOURCE = "https://example.test/managed.zip";
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraft() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void managedSetContainingVanillaStructureIsAbsentOutsideDeclaringDimension() {
|
||||||
|
Holder<Structure> vanillaStructure = structureHolder("minecraft:pillager_outpost");
|
||||||
|
Holder<StructureSet> managedSet = structureSetHolder(
|
||||||
|
"managed:illager_barracks", vanillaStructure);
|
||||||
|
DatapackStructureScopeIndex index = index(
|
||||||
|
List.of(),
|
||||||
|
List.of("managed:illager_barracks"));
|
||||||
|
|
||||||
|
DatapackStructureStateFilter.Selection vanilla = DatapackStructureStateFilter.filter(
|
||||||
|
List.of(managedSet), index, Set.of());
|
||||||
|
DatapackStructureStateFilter.Selection declaring = DatapackStructureStateFilter.filter(
|
||||||
|
List.of(managedSet), index, index.declaredSources(List.of(SOURCE)));
|
||||||
|
|
||||||
|
assertEquals(0, vanilla.structureSets().size());
|
||||||
|
assertEquals(1, vanilla.excludedManagedSets());
|
||||||
|
assertEquals(1, declaring.structureSets().size());
|
||||||
|
assertSame(managedSet, declaring.structureSets().getFirst());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unmanagedSetRetainsOnlyDefinitionsAllowedInTheWorld() {
|
||||||
|
Holder<Structure> vanillaStructure = structureHolder("minecraft:village_plains");
|
||||||
|
Holder<Structure> managedStructure = structureHolder("managed:tavern");
|
||||||
|
Holder<StructureSet> vanillaSet = structureSetHolder(
|
||||||
|
"minecraft:villages", vanillaStructure, managedStructure);
|
||||||
|
DatapackStructureScopeIndex index = index(
|
||||||
|
List.of("managed:tavern"),
|
||||||
|
List.of());
|
||||||
|
|
||||||
|
DatapackStructureStateFilter.Selection vanilla = DatapackStructureStateFilter.filter(
|
||||||
|
List.of(vanillaSet), index, Set.of());
|
||||||
|
DatapackStructureStateFilter.Selection declaring = DatapackStructureStateFilter.filter(
|
||||||
|
List.of(vanillaSet), index, index.declaredSources(List.of(SOURCE)));
|
||||||
|
|
||||||
|
assertEquals(1, vanilla.structureSets().size());
|
||||||
|
assertEquals(1, vanilla.structureSets().getFirst().value().structures().size());
|
||||||
|
assertSame(vanillaStructure,
|
||||||
|
vanilla.structureSets().getFirst().value().structures().getFirst().structure());
|
||||||
|
assertSame(vanillaSet.value().placement(),
|
||||||
|
vanilla.structureSets().getFirst().value().placement());
|
||||||
|
assertSame(vanillaSet, declaring.structureSets().getFirst());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setWithNoAllowedDefinitionsIsRemoved() {
|
||||||
|
Holder<StructureSet> unmanagedSet = structureSetHolder(
|
||||||
|
"minecraft:custom", structureHolder("managed:only"));
|
||||||
|
DatapackStructureScopeIndex index = index(List.of("managed:only"), List.of());
|
||||||
|
|
||||||
|
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
|
||||||
|
List.of(unmanagedSet), index, Set.of());
|
||||||
|
|
||||||
|
assertEquals(0, selection.structureSets().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void spigotDirectHolderRecoversItsRegistryKeyFromSharedEntries() {
|
||||||
|
ResourceKey<StructureSet> key = ResourceKey.create(
|
||||||
|
Registries.STRUCTURE_SET,
|
||||||
|
Identifier.parse("managed:illager_barracks"));
|
||||||
|
List<StructureSet.StructureSelectionEntry> entries = List.of(
|
||||||
|
new StructureSet.StructureSelectionEntry(
|
||||||
|
structureHolder("minecraft:pillager_outpost"), 1));
|
||||||
|
Holder<StructureSet> registered = new KeyedHolder<>(key, new StructureSet(
|
||||||
|
entries,
|
||||||
|
new RandomSpreadStructurePlacement(
|
||||||
|
34, 8, RandomSpreadType.LINEAR, 10387312)));
|
||||||
|
Holder<StructureSet> spigotDirect = Holder.direct(new StructureSet(
|
||||||
|
entries,
|
||||||
|
new RandomSpreadStructurePlacement(
|
||||||
|
34, 8, RandomSpreadType.LINEAR, 14357620)));
|
||||||
|
DatapackStructureScopeIndex scopeIndex = index(
|
||||||
|
List.of(), List.of("managed:illager_barracks"));
|
||||||
|
DatapackStructureStateFilter.StructureSetKeyIndex keyIndex =
|
||||||
|
DatapackStructureStateFilter.keyIndex(List.of(registered));
|
||||||
|
|
||||||
|
DatapackStructureStateFilter.Selection excluded =
|
||||||
|
DatapackStructureStateFilter.filter(
|
||||||
|
List.of(spigotDirect),
|
||||||
|
scopeIndex,
|
||||||
|
Set.of(),
|
||||||
|
new IrisImportedStructureControl(),
|
||||||
|
keyIndex);
|
||||||
|
DatapackStructureStateFilter.Selection retained =
|
||||||
|
DatapackStructureStateFilter.filter(
|
||||||
|
List.of(spigotDirect),
|
||||||
|
scopeIndex,
|
||||||
|
scopeIndex.declaredSources(List.of(SOURCE)),
|
||||||
|
new IrisImportedStructureControl(),
|
||||||
|
keyIndex);
|
||||||
|
|
||||||
|
assertEquals(0, excluded.structureSets().size());
|
||||||
|
assertEquals(1, excluded.excludedManagedSets());
|
||||||
|
assertSame(spigotDirect, retained.structureSets().getFirst());
|
||||||
|
assertEquals(1, retained.retainedManagedSets());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void structureScopeDoesNotLinkPaperOnlyPlacementClasses() throws IOException {
|
||||||
|
InputStream classResource = NMSBindingDatapackStructureScopeTest.class
|
||||||
|
.getResourceAsStream("DatapackStructureStateFilter.class");
|
||||||
|
assertNotNull(classResource);
|
||||||
|
try (InputStream input = classResource) {
|
||||||
|
String classFile = new String(input.readAllBytes(), StandardCharsets.ISO_8859_1);
|
||||||
|
assertFalse(classFile.contains("KeyedRandomSpreadStructurePlacement"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void exactFrequencyOverridesScaleOnlyTheNamedNativeStructureSet() {
|
||||||
|
Holder<StructureSet> complexes = structureSetHolder(
|
||||||
|
"minecraft:nether_complexes",
|
||||||
|
new RandomSpreadStructurePlacement(27, 4, RandomSpreadType.LINEAR, 30084232),
|
||||||
|
structureHolder("minecraft:fortress"));
|
||||||
|
Holder<StructureSet> fossils = structureSetHolder(
|
||||||
|
"minecraft:nether_fossils",
|
||||||
|
new RandomSpreadStructurePlacement(2, 1, RandomSpreadType.LINEAR, 14357921),
|
||||||
|
structureHolder("minecraft:nether_fossil"));
|
||||||
|
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
|
||||||
|
overrides.add(new IrisStructureSetFrequencyOverride()
|
||||||
|
.setStructureSet("minecraft:nether_complexes")
|
||||||
|
.setMultiplier(1.1D));
|
||||||
|
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||||
|
.setFrequencyOverrides(overrides);
|
||||||
|
|
||||||
|
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
|
||||||
|
List.of(complexes, fossils), index(List.of(), List.of()), Set.of(), control);
|
||||||
|
|
||||||
|
RandomSpreadStructurePlacement scaledComplexes =
|
||||||
|
(RandomSpreadStructurePlacement) selection.structureSets().get(0).value().placement();
|
||||||
|
RandomSpreadStructurePlacement unchangedFossils =
|
||||||
|
(RandomSpreadStructurePlacement) selection.structureSets().get(1).value().placement();
|
||||||
|
assertEquals(26, scaledComplexes.spacing());
|
||||||
|
assertEquals(4, scaledComplexes.separation());
|
||||||
|
assertEquals(2, unchangedFossils.spacing());
|
||||||
|
assertSame(fossils, selection.structureSets().get(1));
|
||||||
|
assertEquals(27, ((RandomSpreadStructurePlacement) complexes.value().placement()).spacing());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void frequencyOnlyPassLeavesUnrelatedCustomPlacementUntouched() {
|
||||||
|
Holder<StructureSet> exclusionTarget = structureSetHolder(
|
||||||
|
"example:target", structureHolder("example:target"));
|
||||||
|
Holder<StructureSet> custom = structureSetHolder(
|
||||||
|
"example:custom",
|
||||||
|
new UnsupportedPlacement(Optional.of(
|
||||||
|
new AbstractSpreadingStructurePlacement.ExclusionZone(exclusionTarget, 1))),
|
||||||
|
structureHolder("example:custom"));
|
||||||
|
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
|
||||||
|
overrides.add(new IrisStructureSetFrequencyOverride()
|
||||||
|
.setStructureSet("minecraft:nether_complexes")
|
||||||
|
.setMultiplier(1.1D));
|
||||||
|
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||||
|
.setFrequencyOverrides(overrides);
|
||||||
|
|
||||||
|
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
|
||||||
|
List.of(custom), index(List.of(), List.of()), Set.of(), control);
|
||||||
|
|
||||||
|
assertSame(custom, selection.structureSets().getFirst());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void frequencyOnlyPassRebindsDependentExclusionTarget() {
|
||||||
|
Holder<StructureSet> target = structureSetHolder(
|
||||||
|
"example:target",
|
||||||
|
new RandomSpreadStructurePlacement(27, 4, RandomSpreadType.LINEAR, 30084232),
|
||||||
|
structureHolder("example:target"));
|
||||||
|
RandomSpreadStructurePlacement dependentPlacement = new RandomSpreadStructurePlacement(
|
||||||
|
Vec3i.ZERO,
|
||||||
|
AbstractSpreadingStructurePlacement.FrequencyReductionMethod.DEFAULT,
|
||||||
|
1.0F,
|
||||||
|
4567,
|
||||||
|
Optional.of(new AbstractSpreadingStructurePlacement.ExclusionZone(target, 1)),
|
||||||
|
32,
|
||||||
|
8,
|
||||||
|
RandomSpreadType.LINEAR);
|
||||||
|
Holder<StructureSet> dependent = structureSetHolder(
|
||||||
|
"example:dependent",
|
||||||
|
dependentPlacement,
|
||||||
|
structureHolder("example:dependent"));
|
||||||
|
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
|
||||||
|
overrides.add(new IrisStructureSetFrequencyOverride()
|
||||||
|
.setStructureSet("example:target")
|
||||||
|
.setMultiplier(1.1D));
|
||||||
|
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||||
|
.setFrequencyOverrides(overrides);
|
||||||
|
|
||||||
|
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
|
||||||
|
List.of(dependent, target), index(List.of(), List.of()), Set.of(), control);
|
||||||
|
|
||||||
|
Holder<StructureSet> scaledDependent = selection.structureSets().get(0);
|
||||||
|
Holder<StructureSet> scaledTarget = selection.structureSets().get(1);
|
||||||
|
assertNotSame(dependent, scaledDependent);
|
||||||
|
assertNotSame(target, scaledTarget);
|
||||||
|
assertSame(scaledTarget, DatapackStructureStateFilter.exclusionZone(
|
||||||
|
scaledDependent.value().placement()).orElseThrow().otherSet());
|
||||||
|
assertEquals(26, ((RandomSpreadStructurePlacement)
|
||||||
|
scaledTarget.value().placement()).spacing());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void frequencyOnlyPassRebindsAffectedExclusionCycle() {
|
||||||
|
MutableStructureSetHolder first = new MutableStructureSetHolder("example:first");
|
||||||
|
MutableStructureSetHolder second = new MutableStructureSetHolder("example:second");
|
||||||
|
first.bind(structureSet(
|
||||||
|
new RandomSpreadStructurePlacement(
|
||||||
|
Vec3i.ZERO,
|
||||||
|
AbstractSpreadingStructurePlacement.FrequencyReductionMethod.DEFAULT,
|
||||||
|
1.0F,
|
||||||
|
101,
|
||||||
|
Optional.of(new AbstractSpreadingStructurePlacement.ExclusionZone(second, 1)),
|
||||||
|
32,
|
||||||
|
8,
|
||||||
|
RandomSpreadType.LINEAR),
|
||||||
|
"example:first"));
|
||||||
|
second.bind(structureSet(
|
||||||
|
new RandomSpreadStructurePlacement(
|
||||||
|
Vec3i.ZERO,
|
||||||
|
AbstractSpreadingStructurePlacement.FrequencyReductionMethod.DEFAULT,
|
||||||
|
1.0F,
|
||||||
|
102,
|
||||||
|
Optional.of(new AbstractSpreadingStructurePlacement.ExclusionZone(first, 1)),
|
||||||
|
27,
|
||||||
|
4,
|
||||||
|
RandomSpreadType.LINEAR),
|
||||||
|
"example:second"));
|
||||||
|
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
|
||||||
|
overrides.add(new IrisStructureSetFrequencyOverride()
|
||||||
|
.setStructureSet("example:second")
|
||||||
|
.setMultiplier(1.1D));
|
||||||
|
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||||
|
.setFrequencyOverrides(overrides);
|
||||||
|
|
||||||
|
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
|
||||||
|
List.of(first, second), index(List.of(), List.of()), Set.of(), control);
|
||||||
|
|
||||||
|
Holder<StructureSet> scaledFirst = selection.structureSets().get(0);
|
||||||
|
Holder<StructureSet> scaledSecond = selection.structureSets().get(1);
|
||||||
|
assertSame(scaledSecond, DatapackStructureStateFilter.exclusionZone(
|
||||||
|
scaledFirst.value().placement()).orElseThrow().otherSet());
|
||||||
|
assertSame(scaledFirst, DatapackStructureStateFilter.exclusionZone(
|
||||||
|
scaledSecond.value().placement()).orElseThrow().otherSet());
|
||||||
|
assertEquals(26, ((RandomSpreadStructurePlacement)
|
||||||
|
scaledSecond.value().placement()).spacing());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void randomSpreadSubtypeUsesTheCanonicalPlacementContract() {
|
||||||
|
Holder<StructureSet> custom = structureSetHolder(
|
||||||
|
"example:custom",
|
||||||
|
new CustomRandomSpreadPlacement(),
|
||||||
|
structureHolder("example:custom"));
|
||||||
|
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
|
||||||
|
overrides.add(new IrisStructureSetFrequencyOverride()
|
||||||
|
.setStructureSet("example:custom")
|
||||||
|
.setMultiplier(1.1D));
|
||||||
|
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||||
|
.setFrequencyOverrides(overrides);
|
||||||
|
|
||||||
|
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
|
||||||
|
List.of(custom), index(List.of(), List.of()), Set.of(), control);
|
||||||
|
StructurePlacement placement = selection.structureSets().getFirst().value().placement();
|
||||||
|
|
||||||
|
assertEquals(RandomSpreadStructurePlacement.class, placement.getClass());
|
||||||
|
assertEquals(26, ((RandomSpreadStructurePlacement) placement).spacing());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void excludedManagedSetCannotSuppressAnAllowedSetThroughExclusionZone() {
|
||||||
|
Holder<StructureSet> managedSet = structureSetHolder(
|
||||||
|
"managed:blocked",
|
||||||
|
structureHolder("managed:blocked"));
|
||||||
|
RandomSpreadStructurePlacement originalPlacement = new RandomSpreadStructurePlacement(
|
||||||
|
Vec3i.ZERO,
|
||||||
|
AbstractSpreadingStructurePlacement.FrequencyReductionMethod.DEFAULT,
|
||||||
|
1.0F,
|
||||||
|
4567,
|
||||||
|
Optional.of(new AbstractSpreadingStructurePlacement.ExclusionZone(managedSet, 1)),
|
||||||
|
32,
|
||||||
|
8,
|
||||||
|
RandomSpreadType.LINEAR);
|
||||||
|
Holder<StructureSet> vanillaSet = structureSetHolder(
|
||||||
|
"minecraft:allowed",
|
||||||
|
originalPlacement,
|
||||||
|
structureHolder("minecraft:village_plains"));
|
||||||
|
DatapackStructureScopeIndex index = index(
|
||||||
|
List.of("managed:blocked"),
|
||||||
|
List.of("managed:blocked"));
|
||||||
|
|
||||||
|
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
|
||||||
|
List.of(vanillaSet, managedSet), index, Set.of());
|
||||||
|
|
||||||
|
assertEquals(1, selection.structureSets().size());
|
||||||
|
StructurePlacement scopedPlacement = selection.structureSets().getFirst().value().placement();
|
||||||
|
assertEquals(0, DatapackStructureStateFilter.exclusionZone(scopedPlacement).stream().count());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DatapackStructureScopeIndex index(
|
||||||
|
List<String> structureKeys,
|
||||||
|
List<String> structureSetKeys
|
||||||
|
) {
|
||||||
|
return DatapackStructureScopeIndex.create(List.of(
|
||||||
|
new DatapackIngestService.StructureScopeResources(
|
||||||
|
SOURCE,
|
||||||
|
structureKeys,
|
||||||
|
structureSetKeys)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Holder<Structure> structureHolder(String key) {
|
||||||
|
return new KeyedHolder<>(ResourceKey.create(Registries.STRUCTURE, Identifier.parse(key)), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Holder<StructureSet> structureSetHolder(
|
||||||
|
String key,
|
||||||
|
Holder<Structure>... structures
|
||||||
|
) {
|
||||||
|
return structureSetHolder(
|
||||||
|
key,
|
||||||
|
new RandomSpreadStructurePlacement(32, 8, RandomSpreadType.LINEAR, 12345),
|
||||||
|
structures);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Holder<StructureSet> structureSetHolder(
|
||||||
|
String key,
|
||||||
|
StructurePlacement placement,
|
||||||
|
Holder<Structure>... structures
|
||||||
|
) {
|
||||||
|
List<StructureSet.StructureSelectionEntry> entries = Stream.of(structures)
|
||||||
|
.map(structure -> new StructureSet.StructureSelectionEntry(structure, 1))
|
||||||
|
.toList();
|
||||||
|
StructureSet value = new StructureSet(entries, placement);
|
||||||
|
return new KeyedHolder<>(
|
||||||
|
ResourceKey.create(Registries.STRUCTURE_SET, Identifier.parse(key)),
|
||||||
|
value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureSet structureSet(StructurePlacement placement, String structureKey) {
|
||||||
|
return new StructureSet(
|
||||||
|
List.of(new StructureSet.StructureSelectionEntry(
|
||||||
|
structureHolder(structureKey), 1)),
|
||||||
|
placement);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class KeyedHolder<T> extends Holder.Reference<T> {
|
||||||
|
private KeyedHolder(ResourceKey<T> key, T value) {
|
||||||
|
super(Type.STAND_ALONE, new HolderOwner<>() {}, key, null);
|
||||||
|
bindValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class UnsupportedPlacement extends AbstractSpreadingStructurePlacement {
|
||||||
|
private UnsupportedPlacement(Optional<ExclusionZone> exclusionZone) {
|
||||||
|
super(Vec3i.ZERO, FrequencyReductionMethod.DEFAULT, 1F, 1, exclusionZone);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isPlacementChunk(ChunkGeneratorStructureState state, int x, int z) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MapCodec<? extends AbstractSpreadingStructurePlacement> codec() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class MutableStructureSetHolder extends Holder.Reference<StructureSet> {
|
||||||
|
private MutableStructureSetHolder(String key) {
|
||||||
|
super(Type.STAND_ALONE, new HolderOwner<>() {
|
||||||
|
}, ResourceKey.create(Registries.STRUCTURE_SET, Identifier.parse(key)), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bind(StructureSet structureSet) {
|
||||||
|
bindValue(structureSet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class CustomRandomSpreadPlacement extends RandomSpreadStructurePlacement {
|
||||||
|
private CustomRandomSpreadPlacement() {
|
||||||
|
super(27, 4, RandomSpreadType.LINEAR, 30084232);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+250
@@ -0,0 +1,250 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
||||||
|
|
||||||
|
import ca.spottedleaf.moonrise.common.util.TickThread;
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.IdMapper;
|
||||||
|
import net.minecraft.core.SectionPos;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.entity.ai.village.poi.PoiManager;
|
||||||
|
import net.minecraft.world.entity.ai.village.poi.PoiRecord;
|
||||||
|
import net.minecraft.world.entity.ai.village.poi.PoiSection;
|
||||||
|
import net.minecraft.world.entity.ai.village.poi.PoiType;
|
||||||
|
import net.minecraft.world.entity.ai.village.poi.PoiTypes;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.block.Block;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ImposterProtoChunk;
|
||||||
|
import net.minecraft.world.level.chunk.LevelChunk;
|
||||||
|
import net.minecraft.world.level.chunk.PalettedContainer;
|
||||||
|
import net.minecraft.world.level.chunk.PalettedContainerFactory;
|
||||||
|
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||||
|
import net.minecraft.world.level.chunk.Strategy;
|
||||||
|
import net.minecraft.world.level.chunk.UpgradeData;
|
||||||
|
import org.bukkit.NamespacedKey;
|
||||||
|
import org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry;
|
||||||
|
import org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer;
|
||||||
|
import org.bukkit.persistence.PersistentDataType;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.FutureTask;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class NativeStructurePoiUpdatesTest {
|
||||||
|
private static final NamespacedKey DIRTY_SECTIONS = new NamespacedKey("iris", "native_poi_sections");
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraftRegistries() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void generationRecordsIntentBeforeSuppressingNativePoiCallbacks() {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
BlockPos position = new BlockPos(-31, 5, 49);
|
||||||
|
doAnswer(call -> {
|
||||||
|
assertNotNull(fixture.proto.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY));
|
||||||
|
assertEquals(Block.UPDATE_CLIENTS | 4096, (int) call.getArgument(2));
|
||||||
|
fixture.proto.setBlockState(position, call.getArgument(1), 0);
|
||||||
|
return true;
|
||||||
|
}).when(fixture.world).setBlock(any(), any(), anyInt(), anyInt());
|
||||||
|
|
||||||
|
assertTrue(fixture.updates.setBlock(fixture.world, position,
|
||||||
|
Blocks.COMPOSTER.defaultBlockState(), Block.UPDATE_CLIENTS, 17));
|
||||||
|
assertArrayEquals(new long[]{16L}, fixture.marker());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void denseWritesStoreOneSectionBitAndSurvivePersistentDataRoundTrip() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
for (int z = 0; z < 16; z++) {
|
||||||
|
for (int x = 0; x < 16; x++) {
|
||||||
|
fixture.write(new BlockPos(-32 + x, 5, 48 + z), Blocks.COMPOSTER.defaultBlockState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertArrayEquals(new long[]{16L}, fixture.marker());
|
||||||
|
DirtyCraftPersistentDataContainer restored = new DirtyCraftPersistentDataContainer(new CraftPersistentDataTypeRegistry());
|
||||||
|
restored.putAll(fixture.proto.persistentDataContainer.toTagCompound());
|
||||||
|
fixture.loaded.persistentDataContainer = restored;
|
||||||
|
|
||||||
|
onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded));
|
||||||
|
|
||||||
|
assertEquals(256L, fixture.records().getRecords(type -> true, PoiManager.Occupancy.ANY).count());
|
||||||
|
assertFalse(restored.has(DIRTY_SECTIONS));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validSectionsReconcileFinalStatesAndKeepMatchingClaimedTickets() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
BlockPos retained = new BlockPos(-31, 5, 49);
|
||||||
|
BlockPos replaced = new BlockPos(-30, 5, 49);
|
||||||
|
BlockPos removed = new BlockPos(-29, 5, 49);
|
||||||
|
Holder<PoiType> farmer = PoiTypes.forState(Blocks.COMPOSTER.defaultBlockState()).orElseThrow();
|
||||||
|
PoiSection existing = new PoiSection.Packed(true, List.of(
|
||||||
|
new PoiRecord.Packed(retained, farmer, 0),
|
||||||
|
new PoiRecord.Packed(replaced, farmer, 1),
|
||||||
|
new PoiRecord.Packed(removed, farmer, 1))).unpack(() -> {});
|
||||||
|
fixture.pois.put(SectionPos.of(retained).asLong(), existing);
|
||||||
|
PoiRecord retainedRecord = existing.getRecords(type -> true, PoiManager.Occupancy.ANY)
|
||||||
|
.filter(record -> record.getPos().equals(retained)).findFirst().orElseThrow();
|
||||||
|
for (BlockPos position : List.of(retained, replaced, removed)) {
|
||||||
|
fixture.proto.setBlockState(position, Blocks.COMPOSTER.defaultBlockState(), 0);
|
||||||
|
}
|
||||||
|
fixture.write(replaced, Blocks.BARREL.defaultBlockState());
|
||||||
|
fixture.write(removed, Blocks.AIR.defaultBlockState());
|
||||||
|
|
||||||
|
onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded));
|
||||||
|
|
||||||
|
assertEquals(PoiTypes.forState(Blocks.BARREL.defaultBlockState()), existing.getType(replaced));
|
||||||
|
assertTrue(existing.getType(removed).isEmpty());
|
||||||
|
assertSame(retainedRecord, existing.getRecords(type -> true, PoiManager.Occupancy.ANY)
|
||||||
|
.filter(record -> record.getPos().equals(retained)).findFirst().orElseThrow());
|
||||||
|
assertEquals(0, existing.getFreeTickets(retained));
|
||||||
|
onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded));
|
||||||
|
assertEquals(0, existing.getFreeTickets(retained));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failedReconciliationRetainsIntentForRetry() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
fixture.write(new BlockPos(-31, 5, 49), Blocks.COMPOSTER.defaultBlockState());
|
||||||
|
IllegalStateException failure = new IllegalStateException("POI storage failure");
|
||||||
|
doThrow(failure).when(fixture.manager).add(any(), any());
|
||||||
|
|
||||||
|
onOwner(() -> assertSame(failure, assertThrows(IllegalStateException.class,
|
||||||
|
() -> NativeStructurePoiUpdates.reconcile(fixture.loaded))));
|
||||||
|
|
||||||
|
assertArrayEquals(new long[]{16L}, fixture.marker());
|
||||||
|
fixture.allowPoiAdds();
|
||||||
|
onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded));
|
||||||
|
assertFalse(fixture.proto.persistentDataContainer.has(DIRTY_SECTIONS));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void explicitPoiSuppressionAndReadOnlyFullChunksDoNotCreateIntent() {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
BlockPos position = new BlockPos(-31, 5, 49);
|
||||||
|
fixture.updates.setBlock(fixture.world, position, Blocks.COMPOSTER.defaultBlockState(),
|
||||||
|
4096, 17);
|
||||||
|
assertFalse(fixture.proto.persistentDataContainer.has(DIRTY_SECTIONS));
|
||||||
|
ImposterProtoChunk full = mock(ImposterProtoChunk.class);
|
||||||
|
when(fixture.world.getChunk(any(BlockPos.class))).thenReturn(full);
|
||||||
|
fixture.updates.setBlock(fixture.world, position, Blocks.BARREL.defaultBlockState(), 2, 17);
|
||||||
|
assertFalse(fixture.proto.persistentDataContainer.has(DIRTY_SECTIONS));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void invalidSectionBitsRetainTheStoredRecord() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
fixture.proto.persistentDataContainer.set(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY, new long[]{1L << 24});
|
||||||
|
onOwner(() -> assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> NativeStructurePoiUpdates.reconcile(fixture.loaded)));
|
||||||
|
assertArrayEquals(new long[]{1L << 24}, fixture.marker());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void onOwner(Runnable action) throws Exception {
|
||||||
|
FutureTask<Void> task = new FutureTask<>(action, null);
|
||||||
|
new TickThread(task, "Native POI test").start();
|
||||||
|
task.get(5L, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PalettedContainerFactory containerFactory() {
|
||||||
|
Strategy<BlockState> blocks = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY);
|
||||||
|
BlockState air = Blocks.AIR.defaultBlockState();
|
||||||
|
IdMapper<Holder<Biome>> biomeIds = new IdMapper<>();
|
||||||
|
Holder<Biome> biome = Holder.direct((Biome) null);
|
||||||
|
biomeIds.add(biome);
|
||||||
|
Strategy<Holder<Biome>> biomes = Strategy.createForBiomes(biomeIds);
|
||||||
|
Codec<Holder<Biome>> biomeCodec = Codec.STRING.xmap(name -> biome, holder -> "iris-test-biome");
|
||||||
|
return new PalettedContainerFactory(blocks, air, PalettedContainer.codecRW(BlockState.CODEC, blocks, air),
|
||||||
|
biomes, biome, PalettedContainer.codecRO(biomeCodec, biomes, biome),
|
||||||
|
PalettedContainer.codecRW(biomeCodec, biomes, biome));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Fixture {
|
||||||
|
private final NativeStructurePoiUpdates updates = new NativeStructurePoiUpdates(4096);
|
||||||
|
private final ProtoChunk proto = new ProtoChunk(new ChunkPos(-2, 3), UpgradeData.EMPTY,
|
||||||
|
LevelHeightAccessor.create(-64, 384), containerFactory(), null);
|
||||||
|
private final LevelChunk loaded = mock(LevelChunk.class);
|
||||||
|
private final WorldGenLevel world = mock(WorldGenLevel.class);
|
||||||
|
private final PoiManager manager = mock(PoiManager.class);
|
||||||
|
private final Map<Long, PoiSection> pois = new HashMap<>();
|
||||||
|
|
||||||
|
private Fixture() {
|
||||||
|
ServerLevel level = mock(ServerLevel.class);
|
||||||
|
when(level.getPoiManager()).thenReturn(manager);
|
||||||
|
loaded.persistentDataContainer = proto.persistentDataContainer;
|
||||||
|
when(loaded.getLevel()).thenReturn(level);
|
||||||
|
when(loaded.getPos()).thenReturn(proto.getPos());
|
||||||
|
when(loaded.getSectionsCount()).thenReturn(proto.getSectionsCount());
|
||||||
|
when(loaded.getSectionYFromSectionIndex(anyInt())).thenAnswer(call -> proto.getSectionYFromSectionIndex(call.getArgument(0)));
|
||||||
|
when(loaded.getSection(anyInt())).thenAnswer(call -> proto.getSection(call.getArgument(0)));
|
||||||
|
when(loaded.getBlockState(any())).thenAnswer(call -> proto.getBlockState(call.getArgument(0)));
|
||||||
|
when(world.getChunk(any(BlockPos.class))).thenReturn(proto);
|
||||||
|
when(world.setBlock(any(), any(), anyInt(), anyInt())).thenAnswer(call -> {
|
||||||
|
proto.setBlockState(call.getArgument(0), call.getArgument(1), call.getArgument(2));
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
when(manager.getOrLoad(anyLong())).thenAnswer(call -> Optional.ofNullable(pois.get((long) call.getArgument(0))));
|
||||||
|
when(manager.getType(any())).thenAnswer(call -> {
|
||||||
|
BlockPos position = call.getArgument(0);
|
||||||
|
PoiSection section = pois.get(SectionPos.of(position).asLong());
|
||||||
|
return section == null ? Optional.empty() : section.getType(position);
|
||||||
|
});
|
||||||
|
doAnswer(call -> {
|
||||||
|
BlockPos position = call.getArgument(0);
|
||||||
|
pois.get(SectionPos.of(position).asLong()).remove(position);
|
||||||
|
return null;
|
||||||
|
}).when(manager).remove(any());
|
||||||
|
allowPoiAdds();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void allowPoiAdds() {
|
||||||
|
doAnswer(call -> {
|
||||||
|
BlockPos position = call.getArgument(0);
|
||||||
|
return pois.computeIfAbsent(SectionPos.of(position).asLong(), ignored -> new PoiSection(() -> {}))
|
||||||
|
.add(position, call.getArgument(1));
|
||||||
|
}).when(manager).add(any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void write(BlockPos position, BlockState state) {
|
||||||
|
updates.setBlock(world, position, state, Block.UPDATE_CLIENTS, Block.UPDATE_LIMIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long[] marker() {
|
||||||
|
return proto.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PoiSection records() {
|
||||||
|
return pois.get(SectionPos.of(new BlockPos(-31, 5, 49)).asLong());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+464
@@ -0,0 +1,464 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||||
|
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
|
||||||
|
import art.arcane.iris.nativegen.v26_3_R1.NativeStructurePostProcessor;
|
||||||
|
import art.arcane.iris.nativegen.v26_3_R1.NativeStructureTerrainIntegrator;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.world.entity.Entity;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.biome.BiomeManager;
|
||||||
|
import net.minecraft.world.level.block.Block;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.Rotation;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||||
|
import net.minecraft.world.level.entity.EntityTypeTest;
|
||||||
|
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.GravityProcessor;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
|
||||||
|
import net.minecraft.world.level.material.Fluid;
|
||||||
|
import net.minecraft.world.level.material.FluidState;
|
||||||
|
import net.minecraft.world.phys.AABB;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
import net.minecraft.world.ticks.LevelTickAccess;
|
||||||
|
import net.minecraft.world.ticks.ScheduledTick;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationHandler;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class NativeStructureWorldgenAccessTest {
|
||||||
|
private static final int GENERATION_CENTER_X = 60;
|
||||||
|
private static final int GENERATION_CENTER_Z = 15;
|
||||||
|
private static final int SURFACE_FIRST_FREE_Y = 80;
|
||||||
|
private static final int FLOOR_FIRST_FREE_Y = 70;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraftRegistries() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void ordinaryWorldFlattenPreparationHandlesReferencedVillagePiecesBeyondEightChunks() {
|
||||||
|
int farX = (GENERATION_CENTER_X - 9) << 4;
|
||||||
|
int farZ = (GENERATION_CENTER_Z + 4) << 4;
|
||||||
|
BoundingBox bounds = new BoundingBox(farX, 64, farZ, farX + 4, 90, farZ + 4);
|
||||||
|
PoolElementStructurePiece piece = new PoolElementStructurePiece(
|
||||||
|
null, StructurePoolElement.single("minecraft:empty").apply(StructureTemplatePool.Projection.RIGID),
|
||||||
|
new BlockPos(farX, 64, farZ), 1, Rotation.NONE, bounds, LiquidSettings.APPLY_WATERLOGGING);
|
||||||
|
Structure structure = new DesertPyramidStructure(new Structure.StructureSettings(
|
||||||
|
HolderSet.empty(), Map.of(), GenerationStep.Decoration.SURFACE_STRUCTURES, TerrainAdjustment.BEARD_BOX));
|
||||||
|
StructureStart start = new StructureStart(structure, generationCenter(), 0, new PiecesContainer(List.of(piece)));
|
||||||
|
List<NativeStructureTerrainIntegrator.TerrainTarget> targets = List.of(
|
||||||
|
new NativeStructureTerrainIntegrator.TerrainTarget("minecraft:village_plains", start,
|
||||||
|
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.FLATTEN)));
|
||||||
|
BoundingBox area = new BoundingBox(generationCenter().getMinBlockX(), -64, generationCenter().getMinBlockZ(),
|
||||||
|
generationCenter().getMaxBlockX(), 319, generationCenter().getMaxBlockZ());
|
||||||
|
RecordingDelegate unbounded = new RecordingDelegate();
|
||||||
|
unbounded.rejectDistantTerrainReads = true;
|
||||||
|
|
||||||
|
assertThrows(IllegalStateException.class, () -> NativeStructurePostProcessor.prepareTerrain(
|
||||||
|
unbounded.world(), area, targets, (palette, rng, x, y, z) -> Blocks.STONE.defaultBlockState()));
|
||||||
|
|
||||||
|
RecordingDelegate recording = new RecordingDelegate();
|
||||||
|
recording.rejectDistantTerrainReads = true;
|
||||||
|
NativeStructureWorldgenAccess access = access(recording);
|
||||||
|
NativeStructurePostProcessor.prepareTerrain(
|
||||||
|
access, area, targets, (palette, rng, x, y, z) -> Blocks.STONE.defaultBlockState());
|
||||||
|
|
||||||
|
assertEquals(0, recording.heightReads);
|
||||||
|
assertEquals(0, recording.chunkReads);
|
||||||
|
assertEquals(0, recording.terrainReads);
|
||||||
|
assertEquals(0, recording.mutations);
|
||||||
|
assertEquals(91, access.getHeight(Heightmap.Types.WORLD_SURFACE_WG,
|
||||||
|
generationCenter().getMinBlockX(), generationCenter().getMinBlockZ()));
|
||||||
|
assertEquals(1, recording.heightReads);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void projectedTemplatePrefiltersUseRemoteTerrainAndRetainLocalHeightmaps() {
|
||||||
|
RecordingDelegate recording = new RecordingDelegate();
|
||||||
|
recording.rejectDistantTerrainReads = true;
|
||||||
|
NativeStructureWorldgenAccess access = access(recording);
|
||||||
|
GravityProcessor gravity = new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, -1);
|
||||||
|
BlockPos remote = new BlockPos((GENERATION_CENTER_X - 9) << 4, 64, (GENERATION_CENTER_Z + 4) << 4);
|
||||||
|
StructureTemplate.StructureBlockInfo raw = new StructureTemplate.StructureBlockInfo(
|
||||||
|
new BlockPos(0, 4, 0), Blocks.DIRT_PATH.defaultBlockState(), null);
|
||||||
|
StructurePlaceSettings settings = new StructurePlaceSettings().addProcessor(gravity);
|
||||||
|
|
||||||
|
StructureTemplate.StructureBlockInfo projected = StructureTemplate.processBlockInfos(
|
||||||
|
access, remote, BlockPos.ZERO, settings, List.of(raw)).getFirst();
|
||||||
|
|
||||||
|
assertEquals(remote.atY(SURFACE_FIRST_FREE_Y + 3), projected.pos());
|
||||||
|
assertEquals(0, recording.heightReads);
|
||||||
|
BlockPos local = generationCenter().getMiddleBlockPosition(64);
|
||||||
|
StructureTemplate.StructureBlockInfo localBlock = new StructureTemplate.StructureBlockInfo(
|
||||||
|
BlockPos.ZERO, Blocks.DIRT_PATH.defaultBlockState(), null);
|
||||||
|
StructureTemplate.StructureBlockInfo localProjection = StructureTemplate.processBlockInfos(
|
||||||
|
access, local, BlockPos.ZERO, settings, List.of(localBlock)).getFirst();
|
||||||
|
assertEquals(local.atY(90), localProjection.pos());
|
||||||
|
assertEquals(1, recording.heightReads);
|
||||||
|
assertEquals(0, recording.chunkReads);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void distanceTwoTerrainReadsNeverReachTheDelegate() {
|
||||||
|
RecordingDelegate recording = new RecordingDelegate();
|
||||||
|
NativeStructureWorldgenAccess access = access(recording);
|
||||||
|
ChunkPos generationCenter = generationCenter();
|
||||||
|
int x = generationCenter.getMiddleBlockX();
|
||||||
|
int z = (generationCenter.z() + 2) << 4;
|
||||||
|
|
||||||
|
assertSame(Blocks.STONE, access.getBlockState(new BlockPos(x, FLOOR_FIRST_FREE_Y - 1, z)).getBlock());
|
||||||
|
assertSame(Blocks.WATER, access.getBlockState(new BlockPos(x, FLOOR_FIRST_FREE_Y, z)).getBlock());
|
||||||
|
assertSame(Blocks.AIR, access.getBlockState(new BlockPos(x, SURFACE_FIRST_FREE_Y, z)).getBlock());
|
||||||
|
assertSame(Blocks.WATER.defaultBlockState().getFluidState(),
|
||||||
|
access.getFluidState(new BlockPos(x, FLOOR_FIRST_FREE_Y, z)));
|
||||||
|
assertEquals(SURFACE_FIRST_FREE_Y, access.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z));
|
||||||
|
assertEquals(FLOOR_FIRST_FREE_Y, access.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z));
|
||||||
|
assertNull(access.getBlockStateIfLoaded(new BlockPos(x, FLOOR_FIRST_FREE_Y, z)));
|
||||||
|
assertNull(access.getChunk(generationCenter.x(), generationCenter.z() + 2, ChunkStatus.FEATURES, false));
|
||||||
|
assertNull(access.getChunkIfLoadedImmediately(
|
||||||
|
generationCenter.x(), generationCenter.z() + 2));
|
||||||
|
List<BlockState> streamed = access.getBlockStates(new AABB(
|
||||||
|
x, FLOOR_FIRST_FREE_Y, z, x, FLOOR_FIRST_FREE_Y, z)).toList();
|
||||||
|
|
||||||
|
assertEquals(1, streamed.size());
|
||||||
|
assertSame(Blocks.WATER, streamed.getFirst().getBlock());
|
||||||
|
assertEquals(0, recording.terrainReads);
|
||||||
|
assertEquals(0, recording.heightReads);
|
||||||
|
assertEquals(0, recording.chunkReads);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void distanceTwoMutationsAndSideEffectsNeverReachTheDelegate() {
|
||||||
|
RecordingDelegate recording = new RecordingDelegate();
|
||||||
|
NativeStructureWorldgenAccess access = access(recording);
|
||||||
|
ChunkPos generationCenter = generationCenter();
|
||||||
|
BlockPos position = new BlockPos(
|
||||||
|
generationCenter.getMiddleBlockX(),
|
||||||
|
FLOOR_FIRST_FREE_Y,
|
||||||
|
(generationCenter.z() + 2) << 4);
|
||||||
|
|
||||||
|
assertFalse(access.ensureCanWrite(position));
|
||||||
|
assertFalse(access.setBlock(position, Blocks.DIRT.defaultBlockState(), 2));
|
||||||
|
assertFalse(access.removeBlock(position, false));
|
||||||
|
assertFalse(access.destroyBlock(position, false));
|
||||||
|
access.updateNeighborsAt(position, Blocks.DIRT);
|
||||||
|
access.neighborShapeChanged(
|
||||||
|
Direction.NORTH, position, position.north(),
|
||||||
|
Blocks.DIRT.defaultBlockState(), 2, 512);
|
||||||
|
access.levelEvent(null, 2001, position, 0);
|
||||||
|
access.gameEvent(null, Vec3.atCenterOf(position), null);
|
||||||
|
access.addParticle(null, position.getX(), position.getY(), position.getZ(), 0, 0, 0);
|
||||||
|
access.getBlockTicks().schedule(new ScheduledTick<>(
|
||||||
|
Blocks.DIRT, position, 1L, 0L));
|
||||||
|
|
||||||
|
assertEquals(0, recording.mutations);
|
||||||
|
assertEquals(0, recording.events);
|
||||||
|
assertEquals(0, recording.blockTicks.count());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void radiusOneReadsWritesAndTicksRemainUnchanged() {
|
||||||
|
RecordingDelegate recording = new RecordingDelegate();
|
||||||
|
NativeStructureWorldgenAccess access = access(recording);
|
||||||
|
ChunkPos generationCenter = generationCenter();
|
||||||
|
BlockPos position = new BlockPos(
|
||||||
|
(generationCenter.x() + 1) << 4,
|
||||||
|
FLOOR_FIRST_FREE_Y,
|
||||||
|
(generationCenter.z() - 1) << 4);
|
||||||
|
|
||||||
|
assertTrue(access.isInsideGenerationRegion(
|
||||||
|
generationCenter.x() + 1, generationCenter.z() - 1));
|
||||||
|
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
|
||||||
|
assertEquals(91, access.getHeight(
|
||||||
|
Heightmap.Types.WORLD_SURFACE_WG, position.getX(), position.getZ()));
|
||||||
|
assertTrue(access.ensureCanWrite(position));
|
||||||
|
assertTrue(access.setBlock(position, Blocks.STONE.defaultBlockState(), 2));
|
||||||
|
assertTrue(access.removeBlock(position, false));
|
||||||
|
assertTrue(access.destroyBlock(position, false));
|
||||||
|
access.getBlockTicks().schedule(new ScheduledTick<>(
|
||||||
|
Blocks.DIRT, position, 1L, 0L));
|
||||||
|
|
||||||
|
assertEquals(1, recording.terrainReads);
|
||||||
|
assertEquals(1, recording.heightReads);
|
||||||
|
assertEquals(4, recording.mutations);
|
||||||
|
assertEquals(1, recording.blockTicks.count());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void virtualizedHeightsHideInRegionTerrainFromTheDelegate() {
|
||||||
|
RecordingDelegate recording = new RecordingDelegate();
|
||||||
|
ChunkPos generationCenter = generationCenter();
|
||||||
|
NativeStructureWorldgenAccess access = NativeStructureWorldgenAccess.create(
|
||||||
|
recording.world(), generationCenter,
|
||||||
|
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||||
|
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||||
|
true,
|
||||||
|
position -> false);
|
||||||
|
int x = generationCenter.getMiddleBlockX();
|
||||||
|
int z = generationCenter.getMiddleBlockZ();
|
||||||
|
|
||||||
|
assertEquals(SURFACE_FIRST_FREE_Y, access.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z));
|
||||||
|
assertEquals(FLOOR_FIRST_FREE_Y, access.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z));
|
||||||
|
assertEquals(0, recording.heightReads);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void protectedCellsRemainReadableButRejectWritesTilesAndTicks() {
|
||||||
|
RecordingDelegate recording = new RecordingDelegate();
|
||||||
|
BlockPos position = generationCenter().getMiddleBlockPosition(70);
|
||||||
|
NativeStructureWorldgenAccess access = NativeStructureWorldgenAccess.create(
|
||||||
|
recording.world(), generationCenter(),
|
||||||
|
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||||
|
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||||
|
false,
|
||||||
|
position::equals);
|
||||||
|
|
||||||
|
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
|
||||||
|
assertFalse(access.ensureCanWrite(position));
|
||||||
|
assertFalse(access.setBlock(position, Blocks.STONE.defaultBlockState(), 2));
|
||||||
|
assertFalse(access.removeBlock(position, false));
|
||||||
|
assertFalse(access.destroyBlock(position, false));
|
||||||
|
assertNull(access.getBlockEntity(position));
|
||||||
|
assertTrue(access.getBlockEntity(position, null).isEmpty());
|
||||||
|
access.getBlockTicks().schedule(new ScheduledTick<>(Blocks.DIRT, position, 1L, 0L));
|
||||||
|
access.updateNeighborsAt(position.east(), Blocks.DIRT);
|
||||||
|
access.neighborShapeChanged(
|
||||||
|
Direction.EAST, position, position.east(), Blocks.DIRT.defaultBlockState(), 2, 512);
|
||||||
|
|
||||||
|
assertEquals(1, recording.terrainReads);
|
||||||
|
assertEquals(0, recording.mutations);
|
||||||
|
assertEquals(0, recording.blockTicks.count());
|
||||||
|
assertTrue(access.setBlock(position.above(), Blocks.STONE.defaultBlockState(), 2));
|
||||||
|
assertEquals(1, recording.mutations);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void statuslessChunkReadsPreserveWorldgenDelegateSemantics() {
|
||||||
|
RecordingDelegate recording = new RecordingDelegate();
|
||||||
|
NativeStructureWorldgenAccess access = access(recording);
|
||||||
|
ChunkPos generationCenter = generationCenter();
|
||||||
|
|
||||||
|
assertNull(access.getChunk(generationCenter.getWorldPosition()));
|
||||||
|
|
||||||
|
assertEquals(1, recording.statuslessChunkReads);
|
||||||
|
assertEquals(0, recording.statusChunkReads);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void entityQueriesRejectDisjointAreasAndClampOverlaps() {
|
||||||
|
RecordingDelegate recording = new RecordingDelegate();
|
||||||
|
NativeStructureWorldgenAccess access = access(recording);
|
||||||
|
ChunkPos generationCenter = generationCenter();
|
||||||
|
int safeMaxX = generationCenter.getMaxBlockX() + 17;
|
||||||
|
int safeMinZ = generationCenter.getMinBlockZ() - 16;
|
||||||
|
int safeMaxZ = generationCenter.getMaxBlockZ() + 17;
|
||||||
|
AABB disjoint = new AABB(
|
||||||
|
safeMaxX, -64, safeMinZ,
|
||||||
|
safeMaxX + 16, 320, safeMaxZ);
|
||||||
|
AABB overlap = new AABB(
|
||||||
|
safeMaxX - 8, -128, safeMinZ - 8,
|
||||||
|
safeMaxX + 16, 400, safeMaxZ + 8);
|
||||||
|
EntityTypeTest<Entity, Entity> type = EntityTypeTest.forClass(Entity.class);
|
||||||
|
|
||||||
|
assertTrue(access.getEntities((Entity) null, disjoint, entity -> true).isEmpty());
|
||||||
|
assertTrue(access.getEntities(type, disjoint, entity -> true).isEmpty());
|
||||||
|
access.getEntities((Entity) null, overlap, entity -> true);
|
||||||
|
access.getEntities(type, overlap, entity -> true);
|
||||||
|
|
||||||
|
assertEquals(2, recording.entityAreas.size());
|
||||||
|
for (AABB delegatedArea : recording.entityAreas) {
|
||||||
|
assertEquals(safeMaxX - 8, delegatedArea.minX, 0D);
|
||||||
|
assertEquals(-64, delegatedArea.minY, 0D);
|
||||||
|
assertEquals(safeMinZ, delegatedArea.minZ, 0D);
|
||||||
|
assertEquals(safeMaxX, delegatedArea.maxX, 0D);
|
||||||
|
assertEquals(320, delegatedArea.maxY, 0D);
|
||||||
|
assertEquals(safeMaxZ, delegatedArea.maxZ, 0D);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NativeStructureWorldgenAccess access(RecordingDelegate recording) {
|
||||||
|
return NativeStructureWorldgenAccess.create(
|
||||||
|
recording.world(), generationCenter(),
|
||||||
|
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||||
|
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||||
|
false,
|
||||||
|
position -> false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ChunkPos generationCenter() {
|
||||||
|
return new ChunkPos(GENERATION_CENTER_X, GENERATION_CENTER_Z);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RecordingDelegate implements InvocationHandler {
|
||||||
|
private final Holder<Biome> biome;
|
||||||
|
private final BiomeManager biomeManager;
|
||||||
|
private final RecordingTicks<Block> blockTicks;
|
||||||
|
private final RecordingTicks<Fluid> fluidTicks;
|
||||||
|
private final List<AABB> entityAreas;
|
||||||
|
private int terrainReads;
|
||||||
|
private int heightReads;
|
||||||
|
private int chunkReads;
|
||||||
|
private int statuslessChunkReads;
|
||||||
|
private int statusChunkReads;
|
||||||
|
private int mutations;
|
||||||
|
private int events;
|
||||||
|
private boolean rejectDistantTerrainReads;
|
||||||
|
|
||||||
|
private RecordingDelegate() {
|
||||||
|
this.biome = Holder.direct((Biome) null);
|
||||||
|
this.biomeManager = new BiomeManager((x, y, z) -> biome, 13L);
|
||||||
|
this.blockTicks = new RecordingTicks<>();
|
||||||
|
this.fluidTicks = new RecordingTicks<>();
|
||||||
|
this.entityAreas = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private WorldGenLevel world() {
|
||||||
|
return (WorldGenLevel) Proxy.newProxyInstance(
|
||||||
|
WorldGenLevel.class.getClassLoader(),
|
||||||
|
new Class<?>[]{WorldGenLevel.class}, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object invoke(Object proxy, Method method, Object[] arguments) {
|
||||||
|
String name = method.getName();
|
||||||
|
if (name.equals("getSeaLevel")) {
|
||||||
|
return 63;
|
||||||
|
}
|
||||||
|
if (name.equals("getBiome")) {
|
||||||
|
return biome;
|
||||||
|
}
|
||||||
|
if (name.equals("getBiomeManager")) {
|
||||||
|
return biomeManager;
|
||||||
|
}
|
||||||
|
if (name.equals("getBlockTicks")) {
|
||||||
|
return blockTicks;
|
||||||
|
}
|
||||||
|
if (name.equals("getFluidTicks")) {
|
||||||
|
return fluidTicks;
|
||||||
|
}
|
||||||
|
if (name.equals("getMinY")) {
|
||||||
|
return -64;
|
||||||
|
}
|
||||||
|
if (name.equals("getHeight") && arguments == null) {
|
||||||
|
return 384;
|
||||||
|
}
|
||||||
|
if (name.equals("getHeight")) {
|
||||||
|
if (rejectDistantTerrainReads
|
||||||
|
&& (Math.abs(((int) arguments[1] >> 4) - GENERATION_CENTER_X) > 1
|
||||||
|
|| Math.abs(((int) arguments[2] >> 4) - GENERATION_CENTER_Z) > 1)) {
|
||||||
|
throw new IllegalStateException("Requested chunk unavailable during world generation");
|
||||||
|
}
|
||||||
|
heightReads++;
|
||||||
|
return 91;
|
||||||
|
}
|
||||||
|
if (name.equals("getBlockState")) {
|
||||||
|
terrainReads++;
|
||||||
|
return Blocks.DIRT.defaultBlockState();
|
||||||
|
}
|
||||||
|
if (name.equals("getFluidState")) {
|
||||||
|
terrainReads++;
|
||||||
|
return Blocks.WATER.defaultBlockState().getFluidState();
|
||||||
|
}
|
||||||
|
if (name.equals("getChunk")) {
|
||||||
|
chunkReads++;
|
||||||
|
if (arguments.length == 2) {
|
||||||
|
statuslessChunkReads++;
|
||||||
|
} else {
|
||||||
|
statusChunkReads++;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (name.equals("getChunkIfLoadedImmediately")) {
|
||||||
|
chunkReads++;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (name.equals("getEntities")) {
|
||||||
|
entityAreas.add((AABB) arguments[1]);
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
if (name.equals("ensureCanWrite") || name.equals("setBlock")
|
||||||
|
|| name.equals("removeBlock") || name.equals("destroyBlock")) {
|
||||||
|
mutations++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (name.equals("updateNeighborsAt") || name.equals("neighborShapeChanged")) {
|
||||||
|
mutations++;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (name.equals("levelEvent") || name.equals("gameEvent") || name.equals("addParticle")) {
|
||||||
|
events++;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (name.equals("hashCode")) {
|
||||||
|
return System.identityHashCode(proxy);
|
||||||
|
}
|
||||||
|
if (name.equals("equals")) {
|
||||||
|
return proxy == arguments[0];
|
||||||
|
}
|
||||||
|
if (name.equals("toString")) {
|
||||||
|
return "native-structure-worldgen-test-delegate";
|
||||||
|
}
|
||||||
|
throw new UnsupportedOperationException(method.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RecordingTicks<T> implements LevelTickAccess<T> {
|
||||||
|
private int count;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void schedule(ScheduledTick<T> tick) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasScheduledTick(BlockPos position, T type) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int count() {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean willTickThisTick(BlockPos position, T type) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+100
@@ -0,0 +1,100 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
||||||
|
|
||||||
|
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import org.bukkit.craftbukkit.CraftWorld;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class NmsChunkIOFlushTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraftRegistries() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void nativeDrainCanWaitOffOwnerUntilChunkPoiAndEntityIoFinish() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
AtomicBoolean chunkPending = new AtomicBoolean(true);
|
||||||
|
AtomicBoolean poiPending = new AtomicBoolean(true);
|
||||||
|
AtomicBoolean entityPending = new AtomicBoolean(true);
|
||||||
|
CountDownLatch chunkDrain = new CountDownLatch(1);
|
||||||
|
CountDownLatch poiDrain = new CountDownLatch(1);
|
||||||
|
CountDownLatch entityDrain = new CountDownLatch(1);
|
||||||
|
when(fixture.chunks.hasTasks()).thenAnswer(invocation -> {
|
||||||
|
chunkDrain.countDown();
|
||||||
|
return chunkPending.get();
|
||||||
|
});
|
||||||
|
when(fixture.pois.hasTasks()).thenAnswer(invocation -> {
|
||||||
|
poiDrain.countDown();
|
||||||
|
return poiPending.get();
|
||||||
|
});
|
||||||
|
when(fixture.entities.hasTasks()).thenAnswer(invocation -> {
|
||||||
|
entityDrain.countDown();
|
||||||
|
return entityPending.get();
|
||||||
|
});
|
||||||
|
ExecutorService io = Executors.newSingleThreadExecutor();
|
||||||
|
try {
|
||||||
|
Future<?> drain = io.submit(() -> fixture.binding.flushChunkIO(fixture.world));
|
||||||
|
assertTrue(chunkDrain.await(5L, TimeUnit.SECONDS));
|
||||||
|
assertFalse(drain.isDone());
|
||||||
|
chunkPending.set(false);
|
||||||
|
assertTrue(poiDrain.await(5L, TimeUnit.SECONDS));
|
||||||
|
assertFalse(drain.isDone());
|
||||||
|
poiPending.set(false);
|
||||||
|
assertTrue(entityDrain.await(5L, TimeUnit.SECONDS));
|
||||||
|
assertFalse(drain.isDone());
|
||||||
|
entityPending.set(false);
|
||||||
|
drain.get(5L, TimeUnit.SECONDS);
|
||||||
|
} finally {
|
||||||
|
chunkPending.set(false);
|
||||||
|
poiPending.set(false);
|
||||||
|
entityPending.set(false);
|
||||||
|
io.shutdownNow();
|
||||||
|
assertTrue(io.awaitTermination(5L, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void nativeDrainFailurePropagatesToItsCompletionReceipt() {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
IllegalStateException failure = new IllegalStateException("native IO failed");
|
||||||
|
when(fixture.chunks.hasTasks()).thenThrow(failure);
|
||||||
|
assertSame(failure, assertThrows(IllegalStateException.class,
|
||||||
|
() -> fixture.binding.flushChunkIO(fixture.world)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Fixture {
|
||||||
|
private final NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
|
||||||
|
private final CraftWorld world = mock(CraftWorld.class);
|
||||||
|
private final ServerLevel level = mock(ServerLevel.class);
|
||||||
|
private final RegionDataController chunks = mock(RegionDataController.class);
|
||||||
|
private final RegionDataController pois = mock(RegionDataController.class);
|
||||||
|
private final RegionDataController entities = mock(RegionDataController.class);
|
||||||
|
|
||||||
|
private Fixture() {
|
||||||
|
when(world.getHandle()).thenReturn(level);
|
||||||
|
when(level.moonrise$getChunkDataController()).thenReturn(chunks);
|
||||||
|
when(level.moonrise$getPoiChunkDataController()).thenReturn(pois);
|
||||||
|
when(level.moonrise$getEntityChunkDataController()).thenReturn(entities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+95
@@ -0,0 +1,95 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.volmlib.util.scheduling.FoliaScheduler;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
import net.minecraft.server.level.ServerChunkCache;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Server;
|
||||||
|
import org.bukkit.craftbukkit.CraftWorld;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class NmsChunkTaskPollingTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraftRegistries() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void serverOwnerPumpsOnlyTheTargetWorldQueue() {
|
||||||
|
Server bukkitServer = mock(Server.class);
|
||||||
|
CraftWorld world = mock(CraftWorld.class);
|
||||||
|
ServerLevel level = mock(ServerLevel.class);
|
||||||
|
MinecraftServer server = mock(MinecraftServer.class);
|
||||||
|
ServerChunkCache chunks = mock(ServerChunkCache.class);
|
||||||
|
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
|
||||||
|
CompletableFuture<Void> completed = new CompletableFuture<>();
|
||||||
|
when(world.getHandle()).thenReturn(level);
|
||||||
|
when(level.getServer()).thenReturn(server);
|
||||||
|
when(server.getRunningThread()).thenReturn(Thread.currentThread());
|
||||||
|
when(level.getChunkSource()).thenReturn(chunks);
|
||||||
|
when(chunks.pollTask()).thenAnswer(invocation -> completed.complete(null)).thenReturn(false);
|
||||||
|
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
|
||||||
|
MockedStatic<FoliaScheduler> folia = mockStatic(FoliaScheduler.class)) {
|
||||||
|
bukkit.when(Bukkit::getServer).thenReturn(bukkitServer);
|
||||||
|
folia.when(() -> FoliaScheduler.isRegionizedRuntime(bukkitServer)).thenReturn(false);
|
||||||
|
assertTrue(binding.pollChunkTask(world));
|
||||||
|
assertTrue(completed.isDone());
|
||||||
|
assertFalse(binding.pollChunkTask(world));
|
||||||
|
}
|
||||||
|
verify(chunks, times(2)).pollTask();
|
||||||
|
verify(server, never()).executeAllRecentInternalTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void nonOwnerIsRejectedBeforeAccessingTheChunkQueue() {
|
||||||
|
Server bukkitServer = mock(Server.class);
|
||||||
|
CraftWorld world = mock(CraftWorld.class);
|
||||||
|
ServerLevel level = mock(ServerLevel.class);
|
||||||
|
MinecraftServer server = mock(MinecraftServer.class);
|
||||||
|
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
|
||||||
|
when(world.getHandle()).thenReturn(level);
|
||||||
|
when(level.getServer()).thenReturn(server);
|
||||||
|
when(server.getRunningThread()).thenReturn(new Thread("Other server owner"));
|
||||||
|
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
|
||||||
|
MockedStatic<FoliaScheduler> folia = mockStatic(FoliaScheduler.class)) {
|
||||||
|
bukkit.when(Bukkit::getServer).thenReturn(bukkitServer);
|
||||||
|
folia.when(() -> FoliaScheduler.isRegionizedRuntime(bukkitServer)).thenReturn(false);
|
||||||
|
assertThrows(IllegalStateException.class, () -> binding.pollChunkTask(world));
|
||||||
|
}
|
||||||
|
verify(level, never()).getChunkSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void foliaNeverUsesTheWorldWideTaskPump() {
|
||||||
|
Server bukkitServer = mock(Server.class);
|
||||||
|
CraftWorld world = mock(CraftWorld.class);
|
||||||
|
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
|
||||||
|
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
|
||||||
|
MockedStatic<FoliaScheduler> folia = mockStatic(FoliaScheduler.class)) {
|
||||||
|
bukkit.when(Bukkit::getServer).thenReturn(bukkitServer);
|
||||||
|
folia.when(() -> FoliaScheduler.isRegionizedRuntime(bukkitServer)).thenReturn(true);
|
||||||
|
assertFalse(binding.pollChunkTask(world));
|
||||||
|
}
|
||||||
|
verifyNoInteractions(world);
|
||||||
|
}
|
||||||
|
}
|
||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
||||||
|
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import org.bukkit.Difficulty;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.craftbukkit.CraftWorld;
|
||||||
|
import org.bukkit.entity.Entity;
|
||||||
|
import org.bukkit.entity.EntityType;
|
||||||
|
import org.bukkit.event.entity.CreatureSpawnEvent;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.ArgumentMatchers.isNull;
|
||||||
|
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class NmsEntitySpawnDifficultyTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraftRegistries() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void peacefulRejectsEveryNativeForbiddenTypeBeforeBukkitCreatesIt() {
|
||||||
|
CraftWorld world = mock(CraftWorld.class);
|
||||||
|
when(world.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
|
||||||
|
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
|
||||||
|
Location location = new Location(world, 0.5, 80, 0.5);
|
||||||
|
int rejected = 0;
|
||||||
|
|
||||||
|
for (EntityType type : EntityType.values()) {
|
||||||
|
if (type.getEntityClass() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (BuiltInRegistries.ENTITY_TYPE.getValue(Identifier.parse(type.getKey().toString())).isAllowedInPeaceful()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
assertNull(type.name(), binding.spawnEntity(location, type, CreatureSpawnEvent.SpawnReason.NATURAL));
|
||||||
|
rejected++;
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(rejected > 20);
|
||||||
|
verify(world, never()).spawn(any(Location.class), any(Class.class), isNull(), any(CreatureSpawnEvent.SpawnReason.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void peacefulPreservesPassiveAndNativeMonsterCategoryExceptions() {
|
||||||
|
CraftWorld world = mock(CraftWorld.class);
|
||||||
|
when(world.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
|
||||||
|
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
|
||||||
|
Location location = new Location(world, 0.5, 80, 0.5);
|
||||||
|
Entity created = mock(Entity.class);
|
||||||
|
doReturn(created).when(world).spawn(eq(location), any(Class.class), isNull(), eq(CreatureSpawnEvent.SpawnReason.NATURAL));
|
||||||
|
|
||||||
|
for (EntityType type : List.of(EntityType.COW, EntityType.VILLAGER, EntityType.WOLF, EntityType.PIGLIN, EntityType.SHULKER)) {
|
||||||
|
assertSame(type.name(), created, binding.spawnEntity(location, type, CreatureSpawnEvent.SpawnReason.NATURAL));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void nonPeacefulDifficultiesPreserveHostileSpawns() {
|
||||||
|
CraftWorld world = mock(CraftWorld.class);
|
||||||
|
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
|
||||||
|
Location location = new Location(world, 0.5, 80, 0.5);
|
||||||
|
Entity created = mock(Entity.class);
|
||||||
|
doReturn(created).when(world).spawn(eq(location), any(Class.class), isNull(), eq(CreatureSpawnEvent.SpawnReason.NATURAL));
|
||||||
|
|
||||||
|
for (Difficulty difficulty : List.of(Difficulty.EASY, Difficulty.NORMAL, Difficulty.HARD)) {
|
||||||
|
when(world.getDifficulty()).thenReturn(difficulty);
|
||||||
|
assertSame(created, binding.spawnEntity(location, EntityType.ZOMBIE, CreatureSpawnEvent.SpawnReason.NATURAL));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unrelatedBukkitSpawnFailuresStillPropagate() {
|
||||||
|
CraftWorld world = mock(CraftWorld.class);
|
||||||
|
when(world.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
|
||||||
|
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
|
||||||
|
Location location = new Location(world, 0.5, 80, 0.5);
|
||||||
|
IllegalStateException failure = new IllegalStateException("Spawn rejected by platform");
|
||||||
|
doThrow(failure).when(world).spawn(eq(location), any(Class.class), isNull(), eq(CreatureSpawnEvent.SpawnReason.NATURAL));
|
||||||
|
|
||||||
|
assertSame(failure, assertThrows(IllegalStateException.class,
|
||||||
|
() -> binding.spawnEntity(location, EntityType.COW, CreatureSpawnEvent.SpawnReason.NATURAL)));
|
||||||
|
}
|
||||||
|
}
|
||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.world.history.SavedTerrainChunk;
|
||||||
|
import ca.spottedleaf.concurrentutil.util.Priority;
|
||||||
|
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
|
||||||
|
public class NmsSavedTerrainCheckpointTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraftRegistries() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void checkpointWaitsForDirtyAndUnloadedChunkWritesBeforeFlushAndVerification() throws Exception {
|
||||||
|
ServerLevel level = mock(ServerLevel.class);
|
||||||
|
Path root = Path.of("checkpoint-world");
|
||||||
|
byte[] receipt = new byte[]{1, 2, 3};
|
||||||
|
NmsSavedTerrainCapture.Checkpoint saved = new NmsSavedTerrainCapture.Checkpoint(new ChunkPos(4, -5),
|
||||||
|
new NmsSavedTerrainCapture.Verification("minecraft:full", receipt, 7L));
|
||||||
|
NmsSavedTerrainCapture.Checkpoint unloaded = NmsSavedTerrainCapture.checkpoint(-8, 9, null);
|
||||||
|
Set<ChunkPos> completed = new HashSet<>();
|
||||||
|
AtomicBoolean flushed = new AtomicBoolean();
|
||||||
|
try (MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class);
|
||||||
|
MockedStatic<SavedTerrainChunk> terrain = mockStatic(SavedTerrainChunk.class)) {
|
||||||
|
io.when(() -> MoonriseRegionFileIO.getPriority(level, 4, -5, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
completed.add(saved.chunk());
|
||||||
|
return Priority.COMPLETING;
|
||||||
|
});
|
||||||
|
io.when(() -> MoonriseRegionFileIO.getPriority(level, -8, 9, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
completed.add(unloaded.chunk());
|
||||||
|
return Priority.COMPLETING;
|
||||||
|
});
|
||||||
|
io.when(() -> MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
assertEquals(Set.of(saved.chunk(), unloaded.chunk()), completed);
|
||||||
|
flushed.set(true);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
terrain.when(() -> SavedTerrainChunk.verifyCheckpoint(root, 4, -5, "minecraft:full", receipt, 7L))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
assertTrue(flushed.get());
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
NmsSavedTerrainCapture.finishCheckpoint(level, root, List.of(saved, unloaded));
|
||||||
|
|
||||||
|
terrain.verify(() -> SavedTerrainChunk.verifyCheckpoint(root, 4, -5, "minecraft:full", receipt, 7L));
|
||||||
|
io.verify(() -> MoonriseRegionFileIO.flush(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), never());
|
||||||
|
io.verify(() -> MoonriseRegionFileIO.getPriority(level, 100, 100, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), never());
|
||||||
|
terrain.verifyNoMoreInteractions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void storageFlushFailureCannotCompleteTheCheckpoint() throws Exception {
|
||||||
|
ServerLevel level = mock(ServerLevel.class);
|
||||||
|
IOException failure = new IOException("storage flush failed");
|
||||||
|
try (MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class);
|
||||||
|
MockedStatic<SavedTerrainChunk> terrain = mockStatic(SavedTerrainChunk.class)) {
|
||||||
|
io.when(() -> MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
|
||||||
|
.thenThrow(failure);
|
||||||
|
|
||||||
|
assertSame(failure, assertThrows(IOException.class,
|
||||||
|
() -> NmsSavedTerrainCapture.finishCheckpoint(level, Path.of("checkpoint-world"), List.of())));
|
||||||
|
terrain.verifyNoInteractions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void persistedReceiptMismatchCannotCompleteTheCheckpoint() throws Exception {
|
||||||
|
ServerLevel level = mock(ServerLevel.class);
|
||||||
|
Path root = Path.of("checkpoint-world");
|
||||||
|
byte[] receipt = new byte[]{4, 5};
|
||||||
|
IOException failure = new IOException("saved terrain receipt mismatch");
|
||||||
|
NmsSavedTerrainCapture.Checkpoint saved = new NmsSavedTerrainCapture.Checkpoint(new ChunkPos(1, 2),
|
||||||
|
new NmsSavedTerrainCapture.Verification("minecraft:noise", receipt, 3L));
|
||||||
|
try (MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class);
|
||||||
|
MockedStatic<SavedTerrainChunk> terrain = mockStatic(SavedTerrainChunk.class)) {
|
||||||
|
io.when(() -> MoonriseRegionFileIO.getPriority(level, 1, 2, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
|
||||||
|
.thenReturn(Priority.COMPLETING);
|
||||||
|
terrain.when(() -> SavedTerrainChunk.verifyCheckpoint(root, 1, 2, "minecraft:noise", receipt, 3L))
|
||||||
|
.thenThrow(failure);
|
||||||
|
|
||||||
|
assertSame(failure, assertThrows(IOException.class,
|
||||||
|
() -> NmsSavedTerrainCapture.finishCheckpoint(level, root, List.of(saved))));
|
||||||
|
io.verify(() -> MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+223
@@ -0,0 +1,223 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.world.task.J;
|
||||||
|
import ca.spottedleaf.concurrentutil.completable.Completable;
|
||||||
|
import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
|
||||||
|
import ca.spottedleaf.concurrentutil.executor.thread.BalancedPrioritisedThreadPool;
|
||||||
|
import ca.spottedleaf.concurrentutil.util.Priority;
|
||||||
|
import ca.spottedleaf.moonrise.common.PlatformHooks;
|
||||||
|
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
|
||||||
|
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
|
||||||
|
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
|
||||||
|
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNotSame;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class NmsSavedTerrainSnapshotTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraftRegistries() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void ownerReservesSaveOrderBeforeOffOwnerSerializationAndLaterAutosave() throws Exception {
|
||||||
|
try (SaveFixture fixture = new SaveFixture();
|
||||||
|
ExecutorService checkpoint = Executors.newSingleThreadExecutor();
|
||||||
|
MockedStatic<SerializableChunkData> snapshots = mockStatic(SerializableChunkData.class);
|
||||||
|
MockedStatic<PlatformHooks> hooks = mockStatic(PlatformHooks.class);
|
||||||
|
MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class)) {
|
||||||
|
Thread owner = Thread.currentThread();
|
||||||
|
ChunkAccess chunk = mock(ChunkAccess.class);
|
||||||
|
SerializableChunkData snapshot = mock(SerializableChunkData.class);
|
||||||
|
PlatformHooks platform = mock(PlatformHooks.class);
|
||||||
|
CompoundTag capturedData = new CompoundTag();
|
||||||
|
CompoundTag autosave = new CompoundTag();
|
||||||
|
List<String> submissions = new ArrayList<>();
|
||||||
|
when(chunk.getPos()).thenReturn(new ChunkPos(4, -9));
|
||||||
|
snapshots.when(() -> SerializableChunkData.copyOf(fixture.level, chunk)).thenAnswer(invocation -> {
|
||||||
|
assertSame(owner, Thread.currentThread());
|
||||||
|
return snapshot;
|
||||||
|
});
|
||||||
|
hooks.when(PlatformHooks::get).thenReturn(platform);
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
assertSame(owner, Thread.currentThread());
|
||||||
|
return null;
|
||||||
|
}).when(platform).chunkSyncSave(fixture.level, chunk, snapshot);
|
||||||
|
when(snapshot.write()).thenAnswer(invocation -> {
|
||||||
|
assertNotSame(owner, Thread.currentThread());
|
||||||
|
assertEquals(List.of("checkpoint", "autosave"), submissions);
|
||||||
|
return capturedData;
|
||||||
|
});
|
||||||
|
io.when(() -> MoonriseRegionFileIO.scheduleSave(eq(fixture.level), eq(4), eq(-9),
|
||||||
|
any(Completable.class), any(PrioritisedExecutor.PrioritisedTask.class),
|
||||||
|
eq(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), eq(Priority.NORMAL)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
assertSame(owner, Thread.currentThread());
|
||||||
|
submissions.add("checkpoint");
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
io.when(() -> MoonriseRegionFileIO.scheduleSave(fixture.level, 4, -9, autosave,
|
||||||
|
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA)).thenAnswer(invocation -> {
|
||||||
|
submissions.add("autosave");
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
NmsSavedTerrainCapture.CapturedChunk captured = NmsSavedTerrainCapture.scheduleSnapshot(fixture.level, chunk);
|
||||||
|
|
||||||
|
assertEquals(List.of("checkpoint"), submissions);
|
||||||
|
assertFalse(captured.data().isDone());
|
||||||
|
verify(snapshot, never()).write();
|
||||||
|
verify(platform).chunkSyncSave(fixture.level, chunk, snapshot);
|
||||||
|
MoonriseRegionFileIO.scheduleSave(fixture.level, 4, -9, autosave,
|
||||||
|
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA);
|
||||||
|
Future<CompoundTag> result = checkpoint.submit(() -> NmsSavedTerrainCapture.awaitSnapshot(
|
||||||
|
fixture.level, captured, System.nanoTime() + TimeUnit.SECONDS.toNanos(3)));
|
||||||
|
|
||||||
|
assertSame(capturedData, result.get(5, TimeUnit.SECONDS));
|
||||||
|
verify(snapshot).write();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failedOffOwnerSerializationCannotSucceedAsACheckpoint() throws Exception {
|
||||||
|
try (SaveFixture fixture = new SaveFixture();
|
||||||
|
ExecutorService checkpoint = Executors.newSingleThreadExecutor();
|
||||||
|
MockedStatic<SerializableChunkData> snapshots = mockStatic(SerializableChunkData.class);
|
||||||
|
MockedStatic<PlatformHooks> hooks = mockStatic(PlatformHooks.class);
|
||||||
|
MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class)) {
|
||||||
|
ChunkAccess chunk = mock(ChunkAccess.class);
|
||||||
|
SerializableChunkData snapshot = mock(SerializableChunkData.class);
|
||||||
|
IllegalStateException failure = new IllegalStateException("serialization failed");
|
||||||
|
when(chunk.getPos()).thenReturn(new ChunkPos(4, -9));
|
||||||
|
snapshots.when(() -> SerializableChunkData.copyOf(fixture.level, chunk)).thenReturn(snapshot);
|
||||||
|
hooks.when(PlatformHooks::get).thenReturn(mock(PlatformHooks.class));
|
||||||
|
when(snapshot.write()).thenThrow(failure);
|
||||||
|
NmsSavedTerrainCapture.CapturedChunk captured = NmsSavedTerrainCapture.scheduleSnapshot(fixture.level, chunk);
|
||||||
|
|
||||||
|
Future<IOException> result = checkpoint.submit(() -> assertThrows(IOException.class,
|
||||||
|
() -> NmsSavedTerrainCapture.awaitSnapshot(fixture.level, captured,
|
||||||
|
System.nanoTime() + TimeUnit.SECONDS.toNanos(3))));
|
||||||
|
|
||||||
|
assertSame(failure, result.get(5, TimeUnit.SECONDS).getCause());
|
||||||
|
assertTrue(captured.data().isCompletedExceptionally());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void checkpointBoundsOwnerRequestsUntilPriorNativeWritesFinish() throws Exception {
|
||||||
|
try (SaveFixture fixture = new SaveFixture();
|
||||||
|
ExecutorService owner = Executors.newSingleThreadExecutor();
|
||||||
|
ExecutorService checkpoint = Executors.newSingleThreadExecutor()) {
|
||||||
|
World world = mock(World.class);
|
||||||
|
when(world.getWorldFolder()).thenReturn(new File("checkpoint-world"));
|
||||||
|
List<NewChunkHolder> holders = new ArrayList<>(129);
|
||||||
|
for (int index = 0; index < 129; index++) {
|
||||||
|
NewChunkHolder holder = mock(NewChunkHolder.class);
|
||||||
|
setField(holder, "chunkX", index);
|
||||||
|
holders.add(holder);
|
||||||
|
}
|
||||||
|
AtomicInteger requested = new AtomicInteger();
|
||||||
|
AtomicBoolean writesBlocked = new AtomicBoolean(true);
|
||||||
|
CountDownLatch firstBatchWaiting = new CountDownLatch(1);
|
||||||
|
Future<?> result = checkpoint.submit(() -> {
|
||||||
|
try (MockedStatic<J> scheduling = mockStatic(J.class);
|
||||||
|
MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class)) {
|
||||||
|
scheduling.when(() -> J.runRegionFuture(eq(world), anyInt(), anyInt(), any(Runnable.class)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
requested.incrementAndGet();
|
||||||
|
Runnable action = invocation.getArgument(3);
|
||||||
|
return CompletableFuture.runAsync(action, owner);
|
||||||
|
});
|
||||||
|
io.when(() -> MoonriseRegionFileIO.getPriority(eq(fixture.level), anyInt(), anyInt(),
|
||||||
|
eq(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))).thenAnswer(invocation -> {
|
||||||
|
firstBatchWaiting.countDown();
|
||||||
|
return writesBlocked.get() ? Priority.NORMAL : Priority.COMPLETING;
|
||||||
|
});
|
||||||
|
NmsSavedTerrainCapture.flushSnapshots(world, fixture.level, holders);
|
||||||
|
io.verify(() -> MoonriseRegionFileIO.getPriority(fixture.level, 128, 0,
|
||||||
|
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), times(2));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
assertTrue(firstBatchWaiting.await(5, TimeUnit.SECONDS));
|
||||||
|
assertEquals(64, requested.get());
|
||||||
|
assertTrue(owner.submit(() -> true).get(5, TimeUnit.SECONDS));
|
||||||
|
assertFalse(result.isDone());
|
||||||
|
writesBlocked.set(false);
|
||||||
|
result.get(5, TimeUnit.SECONDS);
|
||||||
|
assertEquals(129, requested.get());
|
||||||
|
} finally {
|
||||||
|
writesBlocked.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setField(Object instance, String name, Object value) throws Exception {
|
||||||
|
Field field = instance.getClass().getField(name);
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(instance, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class SaveFixture implements AutoCloseable {
|
||||||
|
private final BalancedPrioritisedThreadPool pool = new BalancedPrioritisedThreadPool(
|
||||||
|
TimeUnit.MILLISECONDS.toNanos(1), Executors.defaultThreadFactory());
|
||||||
|
private final ServerLevel level = mock(ServerLevel.class);
|
||||||
|
|
||||||
|
private SaveFixture() throws Exception {
|
||||||
|
BalancedPrioritisedThreadPool.OrderedStreamGroup group = pool.createOrderedStreamGroup();
|
||||||
|
ChunkTaskScheduler scheduler = mock(ChunkTaskScheduler.class);
|
||||||
|
setField(scheduler, "saveExecutor", group.createExecutor());
|
||||||
|
setField(scheduler, "compressionExecutor", group.createExecutor());
|
||||||
|
setField(scheduler, "chunkHolderManager", mock(ChunkHolderManager.class));
|
||||||
|
when(level.moonrise$getChunkTaskScheduler()).thenReturn(scheduler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws Exception {
|
||||||
|
pool.halt(true);
|
||||||
|
assertTrue(pool.joinInterruptable(5_000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+135
@@ -0,0 +1,135 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
||||||
|
|
||||||
|
import ca.spottedleaf.concurrentutil.executor.thread.BalancedPrioritisedThreadPool;
|
||||||
|
import ca.spottedleaf.concurrentutil.util.Priority;
|
||||||
|
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
|
||||||
|
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class NmsSavedTerrainWriteBarrierTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraftRegistries() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void checkpointHelpsSaveAndCompressionWhileNativeGenerationOccupiesTheWorker() throws Exception {
|
||||||
|
BalancedPrioritisedThreadPool pool = new BalancedPrioritisedThreadPool(
|
||||||
|
TimeUnit.MILLISECONDS.toNanos(1), Executors.defaultThreadFactory());
|
||||||
|
BalancedPrioritisedThreadPool.OrderedStreamGroup group = pool.createOrderedStreamGroup();
|
||||||
|
BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue generation = group.createExecutor();
|
||||||
|
BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue saves = group.createExecutor();
|
||||||
|
BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue compression = group.createExecutor();
|
||||||
|
ServerLevel level = level(saves, compression);
|
||||||
|
ChunkPos chunk = new ChunkPos(4, -9);
|
||||||
|
CountDownLatch generationEntered = new CountDownLatch(1);
|
||||||
|
CountDownLatch releaseGeneration = new CountDownLatch(1);
|
||||||
|
AtomicBoolean pendingWrite = new AtomicBoolean(true);
|
||||||
|
AtomicInteger serialized = new AtomicInteger();
|
||||||
|
AtomicInteger compressed = new AtomicInteger();
|
||||||
|
AtomicReference<Thread> saveThread = new AtomicReference<>();
|
||||||
|
AtomicReference<Thread> compressionThread = new AtomicReference<>();
|
||||||
|
try (MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class)) {
|
||||||
|
io.when(() -> MoonriseRegionFileIO.getPriority(level, chunk.x(), chunk.z(),
|
||||||
|
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
|
||||||
|
.thenAnswer(invocation -> pendingWrite.get() ? Priority.NORMAL : Priority.COMPLETING);
|
||||||
|
generation.queueTask(() -> {
|
||||||
|
generationEntered.countDown();
|
||||||
|
try {
|
||||||
|
assertTrue(releaseGeneration.await(5, TimeUnit.SECONDS));
|
||||||
|
} catch (InterruptedException interruption) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new AssertionError(interruption);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
pool.adjustThreadCount(1);
|
||||||
|
assertTrue(generationEntered.await(5, TimeUnit.SECONDS));
|
||||||
|
saves.queueTask(() -> {
|
||||||
|
saveThread.set(Thread.currentThread());
|
||||||
|
serialized.incrementAndGet();
|
||||||
|
compression.queueTask(() -> {
|
||||||
|
compressionThread.set(Thread.currentThread());
|
||||||
|
compressed.incrementAndGet();
|
||||||
|
pendingWrite.set(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
NmsSavedTerrainCapture.awaitWrite(level, chunk, System.nanoTime() + TimeUnit.SECONDS.toNanos(3));
|
||||||
|
|
||||||
|
assertEquals(1L, releaseGeneration.getCount());
|
||||||
|
assertEquals(1, serialized.get());
|
||||||
|
assertEquals(1, compressed.get());
|
||||||
|
assertFalse(pendingWrite.get());
|
||||||
|
assertSame(Thread.currentThread(), saveThread.get());
|
||||||
|
assertSame(Thread.currentThread(), compressionThread.get());
|
||||||
|
} finally {
|
||||||
|
releaseGeneration.countDown();
|
||||||
|
pool.halt(true);
|
||||||
|
assertTrue(pool.joinInterruptable(5_000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void waitingForNativeWriteHonorsTheSharedDeadline() throws Exception {
|
||||||
|
ServerLevel level = mock(ServerLevel.class);
|
||||||
|
ChunkPos chunk = new ChunkPos(2, 3);
|
||||||
|
IOException failure = assertThrows(IOException.class,
|
||||||
|
() -> NmsSavedTerrainCapture.awaitWrite(level, chunk, System.nanoTime() - 1L));
|
||||||
|
assertTrue(failure.getMessage().contains("Timed out"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void interruptedNativeWriteWaitPreservesInterruption() throws Exception {
|
||||||
|
ServerLevel level = mock(ServerLevel.class);
|
||||||
|
ChunkPos chunk = new ChunkPos(2, 3);
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
try {
|
||||||
|
IOException failure = assertThrows(IOException.class,
|
||||||
|
() -> NmsSavedTerrainCapture.awaitWrite(level, chunk,
|
||||||
|
System.nanoTime() + TimeUnit.SECONDS.toNanos(3)));
|
||||||
|
assertTrue(failure.getMessage().contains("Interrupted"));
|
||||||
|
assertTrue(Thread.currentThread().isInterrupted());
|
||||||
|
} finally {
|
||||||
|
Thread.interrupted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ServerLevel level(BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue saves,
|
||||||
|
BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue compression) throws Exception {
|
||||||
|
ServerLevel level = mock(ServerLevel.class);
|
||||||
|
ChunkTaskScheduler scheduler = mock(ChunkTaskScheduler.class);
|
||||||
|
Field savesField = ChunkTaskScheduler.class.getField("saveExecutor");
|
||||||
|
Field compressionField = ChunkTaskScheduler.class.getField("compressionExecutor");
|
||||||
|
savesField.setAccessible(true);
|
||||||
|
compressionField.setAccessible(true);
|
||||||
|
savesField.set(scheduler, saves);
|
||||||
|
compressionField.set(scheduler, compression);
|
||||||
|
when(level.moonrise$getChunkTaskScheduler()).thenReturn(scheduler);
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
}
|
||||||
+345
@@ -0,0 +1,345 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.nativegen.v26_3_R1.NativeBlockTags;
|
||||||
|
import art.arcane.iris.nativegen.v26_3_R1.WorldgenTerrainHeightmaps;
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.core.IdMapper;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.Block;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||||
|
import net.minecraft.world.level.chunk.PalettedContainer;
|
||||||
|
import net.minecraft.world.level.chunk.PalettedContainerFactory;
|
||||||
|
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||||
|
import net.minecraft.world.level.chunk.Strategy;
|
||||||
|
import net.minecraft.world.level.chunk.UpgradeData;
|
||||||
|
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.IglooPieces;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationHandler;
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.IntBinaryOperator;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class WorldgenTerrainHeightmapsTest {
|
||||||
|
private static final int MIN_Y = -64;
|
||||||
|
private static final int HEIGHT = 384;
|
||||||
|
private static final int LAND_TERRAIN_TOP = 70;
|
||||||
|
private static final int LAND_CANOPY_TOP = 80;
|
||||||
|
private static final int OCEAN_FLOOR_TOP = 60;
|
||||||
|
private static final int OCEAN_FLUID_TOP = 64;
|
||||||
|
private static final int LAND_X = 3;
|
||||||
|
private static final int LAND_Z = 5;
|
||||||
|
private static final int OCEAN_X = 8;
|
||||||
|
private static final int OCEAN_Z = 8;
|
||||||
|
private static final int TERRAIN_SLAB_DEPTH = 4;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraftRegistries() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
NativeBlockTags.bindHeightmapFixtures();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authoringHeightmapsReadPlacedObjectsAndFluidsWithoutTerrainResolvers() {
|
||||||
|
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
|
||||||
|
|
||||||
|
IrisChunkGenerator.primeAuthoringHeightmaps(chunk);
|
||||||
|
|
||||||
|
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
|
||||||
|
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, LAND_X, LAND_Z));
|
||||||
|
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(Heightmap.Types.MOTION_BLOCKING, LAND_X, LAND_Z));
|
||||||
|
assertEquals(LAND_TERRAIN_TOP + 6,
|
||||||
|
chunk.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, LAND_X, LAND_Z));
|
||||||
|
assertEquals(OCEAN_FLUID_TOP, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, OCEAN_X, OCEAN_Z));
|
||||||
|
assertEquals(OCEAN_FLOOR_TOP, chunk.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, OCEAN_X, OCEAN_Z));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void primeTerrainWritesEngineSurfaceAndOceanFloorHeights() {
|
||||||
|
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
|
||||||
|
|
||||||
|
assertFalse(chunk.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
|
||||||
|
assertFalse(chunk.hasPrimedHeightmap(Heightmap.Types.OCEAN_FLOOR_WG));
|
||||||
|
|
||||||
|
WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY(), floorFirstFreeY());
|
||||||
|
|
||||||
|
assertTrue(chunk.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
|
||||||
|
assertTrue(chunk.hasPrimedHeightmap(Heightmap.Types.OCEAN_FLOOR_WG));
|
||||||
|
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
|
||||||
|
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
|
||||||
|
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
|
||||||
|
Heightmap.Types.OCEAN_FLOOR_WG, LAND_X, LAND_Z));
|
||||||
|
assertEquals(OCEAN_FLUID_TOP, chunk.getHeight(
|
||||||
|
Heightmap.Types.WORLD_SURFACE_WG, OCEAN_X, OCEAN_Z));
|
||||||
|
assertEquals(OCEAN_FLOOR_TOP, chunk.getHeight(
|
||||||
|
Heightmap.Types.OCEAN_FLOOR_WG, OCEAN_X, OCEAN_Z));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void finalHeightmapPrimingDoesNotClobberTheWorldgenHeightmaps() {
|
||||||
|
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
|
||||||
|
|
||||||
|
WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY(), floorFirstFreeY());
|
||||||
|
Heightmap.primeHeightmaps(chunk, ChunkStatus.FINAL_HEIGHTMAPS);
|
||||||
|
|
||||||
|
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
|
||||||
|
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
|
||||||
|
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
|
||||||
|
Heightmap.Types.OCEAN_FLOOR_WG, LAND_X, LAND_Z));
|
||||||
|
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(
|
||||||
|
Heightmap.Types.WORLD_SURFACE, LAND_X, LAND_Z));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unprimedWorldgenHeightmapResolvesTheIrisCanopyInstead() {
|
||||||
|
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
|
||||||
|
|
||||||
|
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(
|
||||||
|
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void primeTerrainOverwritesAnAlreadyLazyPrimedCanopyHeight() {
|
||||||
|
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
|
||||||
|
|
||||||
|
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(
|
||||||
|
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
|
||||||
|
WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY(), floorFirstFreeY());
|
||||||
|
|
||||||
|
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
|
||||||
|
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void structurePlacementPrimesTheWestNeighbourAnIglooEntranceReads() {
|
||||||
|
Map<Long, ChunkAccess> chunks = new HashMap<>();
|
||||||
|
chunks.put(ChunkPos.pack(0, 0), terrainChunk(new ChunkPos(0, 0)));
|
||||||
|
chunks.put(ChunkPos.pack(-1, 0), terrainChunk(new ChunkPos(-1, 0)));
|
||||||
|
WorldGenLevel world = world(chunks);
|
||||||
|
StructureStart start = startInOrigin();
|
||||||
|
|
||||||
|
int canopySnap = IglooPieces.GENERATION_HEIGHT + iglooSnapOffset(world);
|
||||||
|
WorldgenTerrainHeightmaps.primeStructurePlacement(
|
||||||
|
world, new ChunkPos(0, 0), List.of(start), surfaceFirstFreeY(), floorFirstFreeY());
|
||||||
|
int terrainSnap = IglooPieces.GENERATION_HEIGHT + iglooSnapOffset(world);
|
||||||
|
|
||||||
|
assertEquals(LAND_TERRAIN_TOP + 1, world.getHeight(
|
||||||
|
Heightmap.Types.WORLD_SURFACE_WG, -2, 5));
|
||||||
|
assertEquals(LAND_TERRAIN_TOP, terrainSnap);
|
||||||
|
assertEquals(LAND_CANOPY_TOP, canopySnap);
|
||||||
|
assertEquals(LAND_CANOPY_TOP - LAND_TERRAIN_TOP, canopySnap - terrainSnap);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void structurePlacementPrimesEveryChunkTheStartFootprintTouches() {
|
||||||
|
Map<Long, ChunkAccess> chunks = new HashMap<>();
|
||||||
|
for (int chunkX = -1; chunkX <= 1; chunkX++) {
|
||||||
|
for (int chunkZ = -1; chunkZ <= 1; chunkZ++) {
|
||||||
|
chunks.put(ChunkPos.pack(chunkX, chunkZ), terrainChunk(new ChunkPos(chunkX, chunkZ)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WorldGenLevel world = world(chunks);
|
||||||
|
|
||||||
|
WorldgenTerrainHeightmaps.primeStructurePlacement(
|
||||||
|
world, new ChunkPos(0, 0), List.of(startInOrigin()),
|
||||||
|
surfaceFirstFreeY(), floorFirstFreeY());
|
||||||
|
|
||||||
|
for (ChunkAccess chunk : chunks.values()) {
|
||||||
|
assertTrue(chunk.getPos().toString(),
|
||||||
|
chunk.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
|
||||||
|
assertTrue(chunk.getPos().toString(),
|
||||||
|
chunk.hasPrimedHeightmap(Heightmap.Types.OCEAN_FLOOR_WG));
|
||||||
|
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
|
||||||
|
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void structurePlacementDoesNotPrimeALoadedDistanceTwoChunk() {
|
||||||
|
Map<Long, ChunkAccess> chunks = new HashMap<>();
|
||||||
|
ProtoChunk origin = terrainChunk(new ChunkPos(0, 0));
|
||||||
|
ProtoChunk neighbour = terrainChunk(new ChunkPos(1, 0));
|
||||||
|
ProtoChunk distanceTwo = terrainChunk(new ChunkPos(2, 0));
|
||||||
|
chunks.put(ChunkPos.pack(0, 0), origin);
|
||||||
|
chunks.put(ChunkPos.pack(1, 0), neighbour);
|
||||||
|
chunks.put(ChunkPos.pack(2, 0), distanceTwo);
|
||||||
|
WorldGenLevel world = world(chunks);
|
||||||
|
StructureStart shifted = startInOrigin();
|
||||||
|
shifted.getPieces().forEach(piece -> piece.move(16, 0, 0));
|
||||||
|
|
||||||
|
WorldgenTerrainHeightmaps.primeStructurePlacement(
|
||||||
|
world, new ChunkPos(0, 0), List.of(shifted),
|
||||||
|
surfaceFirstFreeY(), floorFirstFreeY());
|
||||||
|
|
||||||
|
assertTrue(origin.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
|
||||||
|
assertTrue(neighbour.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
|
||||||
|
assertFalse(distanceTwo.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void structurePlacementSkipsChunksOutsideTheGenerationRegion() {
|
||||||
|
Map<Long, ChunkAccess> chunks = new HashMap<>();
|
||||||
|
ProtoChunk origin = terrainChunk(new ChunkPos(0, 0));
|
||||||
|
chunks.put(ChunkPos.pack(0, 0), origin);
|
||||||
|
WorldGenLevel world = world(chunks);
|
||||||
|
|
||||||
|
WorldgenTerrainHeightmaps.primeStructurePlacement(
|
||||||
|
world, new ChunkPos(0, 0), List.of(startInOrigin()),
|
||||||
|
surfaceFirstFreeY(), floorFirstFreeY());
|
||||||
|
|
||||||
|
assertTrue(origin.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
|
||||||
|
assertEquals(LAND_TERRAIN_TOP, origin.getHeight(
|
||||||
|
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int iglooSnapOffset(WorldGenLevel world) {
|
||||||
|
return world.getHeight(Heightmap.Types.WORLD_SURFACE_WG, -2, 5)
|
||||||
|
- IglooPieces.GENERATION_HEIGHT - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureStart startInOrigin() {
|
||||||
|
Structure structure = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty()));
|
||||||
|
SwampHutPiece piece = new SwampHutPiece(RandomSource.create(13L), 0, 0);
|
||||||
|
return new StructureStart(
|
||||||
|
structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IntBinaryOperator surfaceFirstFreeY() {
|
||||||
|
return (x, z) -> isOcean(x, z) ? OCEAN_FLUID_TOP + 1 : LAND_TERRAIN_TOP + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IntBinaryOperator floorFirstFreeY() {
|
||||||
|
return (x, z) -> isOcean(x, z) ? OCEAN_FLOOR_TOP + 1 : LAND_TERRAIN_TOP + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isOcean(int x, int z) {
|
||||||
|
return Math.floorMod(x, 16) == OCEAN_X && Math.floorMod(z, 16) == OCEAN_Z;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProtoChunk terrainChunk(ChunkPos pos) {
|
||||||
|
ProtoChunk chunk = new ProtoChunk(
|
||||||
|
pos, UpgradeData.EMPTY, LevelHeightAccessor.create(MIN_Y, HEIGHT),
|
||||||
|
containerFactory(), null);
|
||||||
|
for (int x = 0; x < 16; x++) {
|
||||||
|
for (int z = 0; z < 16; z++) {
|
||||||
|
boolean ocean = isOcean(pos.getMinBlockX() + x, pos.getMinBlockZ() + z);
|
||||||
|
int stoneTop = ocean ? OCEAN_FLOOR_TOP : LAND_TERRAIN_TOP;
|
||||||
|
for (int y = stoneTop - TERRAIN_SLAB_DEPTH; y <= stoneTop; y++) {
|
||||||
|
write(chunk, x, y, z, Blocks.STONE.defaultBlockState());
|
||||||
|
}
|
||||||
|
if (ocean) {
|
||||||
|
for (int y = OCEAN_FLOOR_TOP + 1; y <= OCEAN_FLUID_TOP; y++) {
|
||||||
|
write(chunk, x, y, z, Blocks.WATER.defaultBlockState());
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int y = LAND_TERRAIN_TOP + 1; y <= LAND_TERRAIN_TOP + 6; y++) {
|
||||||
|
write(chunk, x, y, z, Blocks.SPRUCE_LOG.defaultBlockState());
|
||||||
|
}
|
||||||
|
for (int y = LAND_TERRAIN_TOP + 7; y <= LAND_CANOPY_TOP; y++) {
|
||||||
|
write(chunk, x, y, z, Blocks.SPRUCE_LEAVES.defaultBlockState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void write(ProtoChunk chunk, int x, int y, int z, BlockState state) {
|
||||||
|
LevelChunkSection section = chunk.getSection(chunk.getSectionIndex(y));
|
||||||
|
section.setBlockState(x, y & 15, z, state, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PalettedContainerFactory containerFactory() {
|
||||||
|
Strategy<BlockState> blockStates = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY);
|
||||||
|
BlockState air = Blocks.AIR.defaultBlockState();
|
||||||
|
IdMapper<Holder<Biome>> biomeIds = new IdMapper<>();
|
||||||
|
Holder<Biome> defaultBiome = Holder.direct((Biome) null);
|
||||||
|
biomeIds.add(defaultBiome);
|
||||||
|
Strategy<Holder<Biome>> biomes = Strategy.createForBiomes(biomeIds);
|
||||||
|
Codec<Holder<Biome>> biomeCodec = Codec.STRING.xmap(
|
||||||
|
name -> defaultBiome, holder -> "iris-test-biome");
|
||||||
|
return new PalettedContainerFactory(
|
||||||
|
blockStates,
|
||||||
|
air,
|
||||||
|
PalettedContainer.codecRW(BlockState.CODEC, blockStates, air),
|
||||||
|
biomes,
|
||||||
|
defaultBiome,
|
||||||
|
PalettedContainer.codecRO(biomeCodec, biomes, defaultBiome),
|
||||||
|
PalettedContainer.codecRW(biomeCodec, biomes, defaultBiome));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorldGenLevel world(Map<Long, ChunkAccess> chunks) {
|
||||||
|
InvocationHandler handler = (proxy, method, arguments) -> {
|
||||||
|
String methodName = method.getName();
|
||||||
|
if (methodName.equals("hasChunk")) {
|
||||||
|
return chunks.containsKey(ChunkPos.pack((int) arguments[0], (int) arguments[1]));
|
||||||
|
}
|
||||||
|
if (methodName.equals("getChunk") && arguments.length == 2) {
|
||||||
|
ChunkAccess chunk = chunks.get(ChunkPos.pack((int) arguments[0], (int) arguments[1]));
|
||||||
|
if (chunk == null) {
|
||||||
|
throw new AssertionError("Heightmap priming requested an unavailable chunk "
|
||||||
|
+ arguments[0] + "," + arguments[1]);
|
||||||
|
}
|
||||||
|
return chunk;
|
||||||
|
}
|
||||||
|
if (methodName.equals("getHeight") && arguments.length == 3) {
|
||||||
|
Heightmap.Types type = (Heightmap.Types) arguments[0];
|
||||||
|
int x = (int) arguments[1];
|
||||||
|
int z = (int) arguments[2];
|
||||||
|
ChunkAccess chunk = chunks.get(ChunkPos.pack(x >> 4, z >> 4));
|
||||||
|
if (chunk == null) {
|
||||||
|
throw new AssertionError("Heightmap read requested an unavailable chunk at "
|
||||||
|
+ x + "," + z);
|
||||||
|
}
|
||||||
|
return chunk.getHeight(type, x & 15, z & 15) + 1;
|
||||||
|
}
|
||||||
|
if (methodName.equals("getBlockState")) {
|
||||||
|
BlockPos position = (BlockPos) arguments[0];
|
||||||
|
ChunkAccess chunk = chunks.get(ChunkPos.pack(position.getX() >> 4, position.getZ() >> 4));
|
||||||
|
if (chunk == null) {
|
||||||
|
throw new AssertionError("Block read requested an unavailable chunk at " + position);
|
||||||
|
}
|
||||||
|
return chunk.getBlockState(position);
|
||||||
|
}
|
||||||
|
if (methodName.equals("hashCode")) {
|
||||||
|
return System.identityHashCode(proxy);
|
||||||
|
}
|
||||||
|
if (methodName.equals("equals")) {
|
||||||
|
return proxy == arguments[0];
|
||||||
|
}
|
||||||
|
if (methodName.equals("toString")) {
|
||||||
|
return "worldgen-heightmap-test-world";
|
||||||
|
}
|
||||||
|
throw new UnsupportedOperationException(method.toString());
|
||||||
|
};
|
||||||
|
return (WorldGenLevel) Proxy.newProxyInstance(
|
||||||
|
WorldGenLevel.class.getClassLoader(), new Class<?>[]{WorldGenLevel.class}, handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
+294
@@ -0,0 +1,294 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||||
|
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package art.arcane.iris.structure;
|
||||||
|
|
||||||
|
import art.arcane.iris.structure.authoring.StructureCapability;
|
||||||
|
import art.arcane.iris.structure.authoring.StructureLoss;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class VillageImporterListPoolElementRuntimeContractTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraft() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void pillagerOutpostTowerCompositeProducesOnePhysicalTowerChoice() throws Exception {
|
||||||
|
StructurePoolElement tower = StructurePoolElement.legacy("minecraft:pillager_outpost/watchtower")
|
||||||
|
.apply(StructureTemplatePool.Projection.RIGID);
|
||||||
|
StructurePoolElement overgrown = StructurePoolElement.legacy("minecraft:pillager_outpost/watchtower_overgrown")
|
||||||
|
.apply(StructureTemplatePool.Projection.RIGID);
|
||||||
|
ListPoolElement towers = new ListPoolElement(
|
||||||
|
List.of(tower, overgrown),
|
||||||
|
StructureTemplatePool.Projection.RIGID
|
||||||
|
);
|
||||||
|
|
||||||
|
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(towers);
|
||||||
|
Map<String, Object> entry = VillageImporter.piecePoolEntry(
|
||||||
|
"qa/pillager_outpost/piece/minecraft/pillager_outpost/watchtower",
|
||||||
|
1
|
||||||
|
);
|
||||||
|
StructureLoss loss = VillageImporter.listElementFallbackLoss(
|
||||||
|
resolution,
|
||||||
|
"minecraft:pillager_outpost/towers"
|
||||||
|
);
|
||||||
|
Map<String, Object> basePlatePiece = VillageImporter.pieceJson(
|
||||||
|
"qa/pillager_outpost/piece/minecraft/pillager_outpost/base_plate",
|
||||||
|
List.of(Map.of("name", "minecraft:bottom")),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
Map<String, Object> watchtowerPiece = VillageImporter.pieceJson(
|
||||||
|
"qa/pillager_outpost/piece/minecraft/pillager_outpost/watchtower",
|
||||||
|
List.of(),
|
||||||
|
1155
|
||||||
|
);
|
||||||
|
VillageImporter.PoolMemberNormalization connectorScaffold = VillageImporter.normalizePoolMember(
|
||||||
|
"minecraft:pillager_outpost/features",
|
||||||
|
"qa/pillager_outpost/pool/minecraft/pillager_outpost/features",
|
||||||
|
false,
|
||||||
|
2,
|
||||||
|
"minecraft:empty",
|
||||||
|
"minecraft:pillager_outpost/feature_plate",
|
||||||
|
"qa/pillager_outpost/piece/minecraft/pillager_outpost/feature_plate",
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
List.of(Map.of("name", "minecraft:bottom")));
|
||||||
|
|
||||||
|
assertSame(tower, resolution.physicalElement());
|
||||||
|
assertEquals("minecraft:pillager_outpost/watchtower", resolution.templateLocation());
|
||||||
|
assertEquals(1, resolution.omittedElements());
|
||||||
|
assertEquals("qa/pillager_outpost/piece/minecraft/pillager_outpost/watchtower", entry.get("piece"));
|
||||||
|
assertEquals(1, entry.get("weight"));
|
||||||
|
assertEquals(StructureCapability.LIST_ELEMENTS, loss.capability());
|
||||||
|
assertEquals("list_pool_overlays_not_imported", loss.code());
|
||||||
|
assertEquals(false, basePlatePiece.get("collidable"));
|
||||||
|
assertFalse(watchtowerPiece.containsKey("collidable"));
|
||||||
|
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, connectorScaffold.disposition());
|
||||||
|
assertTrue(connectorScaffold.losses().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void actualWaystoneLikeAllAirMemberNormalizesToEmpty() throws Exception {
|
||||||
|
StructurePoolElement waystone = StructurePoolElement
|
||||||
|
.legacy("dungeons_and_taverns:waystones/waystone_scaffold")
|
||||||
|
.apply(StructureTemplatePool.Projection.RIGID);
|
||||||
|
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(waystone);
|
||||||
|
VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember(
|
||||||
|
"dungeons_and_taverns:waystones",
|
||||||
|
"qa/pool/dungeons_and_taverns/waystones",
|
||||||
|
false,
|
||||||
|
1,
|
||||||
|
"dungeons_and_taverns:waystones",
|
||||||
|
resolution.templateLocation(),
|
||||||
|
"qa/piece/dungeons_and_taverns/waystones/waystone_scaffold",
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
List.of());
|
||||||
|
|
||||||
|
assertSame(waystone, resolution.physicalElement());
|
||||||
|
assertEquals("dungeons_and_taverns:waystones/waystone_scaffold", resolution.templateLocation());
|
||||||
|
assertEquals(VillageImporter.PoolMemberDisposition.EMPTY, normalization.disposition());
|
||||||
|
assertEquals(true, normalization.poolEntry().get("empty"));
|
||||||
|
assertEquals(2, normalization.poolEntry().get("weight"));
|
||||||
|
assertEquals("connectorless_all_air_member_normalized_empty",
|
||||||
|
normalization.losses().getFirst().code());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void actualNonAirOrphanMemberIsOmitted() throws Exception {
|
||||||
|
StructurePoolElement orphan = StructurePoolElement
|
||||||
|
.legacy("test:orphan_house")
|
||||||
|
.apply(StructureTemplatePool.Projection.RIGID);
|
||||||
|
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(orphan);
|
||||||
|
VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember(
|
||||||
|
"test:orphan_pool",
|
||||||
|
"qa/pool/test/orphan_pool",
|
||||||
|
false,
|
||||||
|
1,
|
||||||
|
null,
|
||||||
|
resolution.templateLocation(),
|
||||||
|
"qa/piece/test/orphan_house",
|
||||||
|
4,
|
||||||
|
37,
|
||||||
|
List.of());
|
||||||
|
|
||||||
|
assertSame(orphan, resolution.physicalElement());
|
||||||
|
assertEquals("test:orphan_house", resolution.templateLocation());
|
||||||
|
assertEquals(VillageImporter.PoolMemberDisposition.OMITTED, normalization.disposition());
|
||||||
|
assertTrue(normalization.poolEntry().isEmpty());
|
||||||
|
assertEquals("connectorless_non_air_member_omitted",
|
||||||
|
normalization.losses().getFirst().code());
|
||||||
|
assertTrue(normalization.losses().getFirst().detail().contains("37 non-air block(s)"));
|
||||||
|
assertTrue(normalization.losses().getFirst().detail().contains("no source fallback"));
|
||||||
|
assertTrue(normalization.losses().getFirst().detail().contains("selection weights"));
|
||||||
|
assertTrue(normalization.losses().getFirst().detail().contains("RNG consumption"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void actualConnectorlessStartMemberRemainsPhysical() throws Exception {
|
||||||
|
StructurePoolElement start = StructurePoolElement
|
||||||
|
.legacy("test:air_start")
|
||||||
|
.apply(StructureTemplatePool.Projection.RIGID);
|
||||||
|
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(start);
|
||||||
|
VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember(
|
||||||
|
"test:start",
|
||||||
|
"qa/pool/test/start",
|
||||||
|
true,
|
||||||
|
2,
|
||||||
|
"test:fallback",
|
||||||
|
resolution.templateLocation(),
|
||||||
|
"qa/piece/test/air_start",
|
||||||
|
6,
|
||||||
|
0,
|
||||||
|
List.of());
|
||||||
|
|
||||||
|
assertSame(start, resolution.physicalElement());
|
||||||
|
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, normalization.disposition());
|
||||||
|
assertEquals("qa/piece/test/air_start", normalization.poolEntry().get("piece"));
|
||||||
|
assertTrue(normalization.losses().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void actualMixedPoolAllAirMemberIsOmittedWithoutBecomingEmpty() throws Exception {
|
||||||
|
StructurePoolElement inert = StructurePoolElement
|
||||||
|
.legacy("test:mixed_air_inert")
|
||||||
|
.apply(StructureTemplatePool.Projection.RIGID);
|
||||||
|
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(inert);
|
||||||
|
VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember(
|
||||||
|
"test:mixed",
|
||||||
|
"qa/pool/test/mixed",
|
||||||
|
false,
|
||||||
|
4,
|
||||||
|
"test:mixed",
|
||||||
|
resolution.templateLocation(),
|
||||||
|
"qa/piece/test/mixed_air_inert",
|
||||||
|
9,
|
||||||
|
0,
|
||||||
|
List.of());
|
||||||
|
|
||||||
|
assertSame(inert, resolution.physicalElement());
|
||||||
|
assertEquals(VillageImporter.PoolMemberDisposition.OMITTED, normalization.disposition());
|
||||||
|
assertTrue(normalization.poolEntry().isEmpty());
|
||||||
|
assertEquals("connectorless_all_air_mixed_member_omitted",
|
||||||
|
normalization.losses().getFirst().code());
|
||||||
|
assertTrue(normalization.losses().getFirst().detail().contains("selection weights"));
|
||||||
|
assertTrue(normalization.losses().getFirst().detail().contains("RNG consumption"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void actualMixedDistinctFallbackRetainsAllConnectorlessMembers() throws Exception {
|
||||||
|
StructurePoolElement allAirElement = StructurePoolElement
|
||||||
|
.legacy("test:mixed_all_air_primary")
|
||||||
|
.apply(StructureTemplatePool.Projection.RIGID);
|
||||||
|
StructurePoolElement nonAirElement = StructurePoolElement
|
||||||
|
.legacy("test:mixed_non_air_primary")
|
||||||
|
.apply(StructureTemplatePool.Projection.RIGID);
|
||||||
|
VillageImporter.PoolElementResolution allAirResolution = VillageImporter.resolvePoolElement(allAirElement);
|
||||||
|
VillageImporter.PoolElementResolution nonAirResolution = VillageImporter.resolvePoolElement(nonAirElement);
|
||||||
|
VillageImporter.PoolMemberNormalization allAir = VillageImporter.normalizePoolMember(
|
||||||
|
"test:mixed",
|
||||||
|
"qa/pool/test/mixed",
|
||||||
|
false,
|
||||||
|
4,
|
||||||
|
"test:fallback",
|
||||||
|
allAirResolution.templateLocation(),
|
||||||
|
"qa/piece/test/mixed_all_air_primary",
|
||||||
|
9,
|
||||||
|
0,
|
||||||
|
List.of());
|
||||||
|
VillageImporter.PoolMemberNormalization nonAir = VillageImporter.normalizePoolMember(
|
||||||
|
"test:mixed",
|
||||||
|
"qa/pool/test/mixed",
|
||||||
|
false,
|
||||||
|
4,
|
||||||
|
"test:fallback",
|
||||||
|
nonAirResolution.templateLocation(),
|
||||||
|
"qa/piece/test/mixed_non_air_primary",
|
||||||
|
12,
|
||||||
|
41,
|
||||||
|
List.of());
|
||||||
|
|
||||||
|
assertSame(allAirElement, allAirResolution.physicalElement());
|
||||||
|
assertSame(nonAirElement, nonAirResolution.physicalElement());
|
||||||
|
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, allAir.disposition());
|
||||||
|
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, nonAir.disposition());
|
||||||
|
assertEquals("qa/piece/test/mixed_all_air_primary", allAir.poolEntry().get("piece"));
|
||||||
|
assertEquals("qa/piece/test/mixed_non_air_primary", nonAir.poolEntry().get("piece"));
|
||||||
|
assertTrue(allAir.losses().isEmpty());
|
||||||
|
assertTrue(nonAir.losses().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void actualSingletonDistinctFallbackRetainsAllAirAndNonAirMembers() throws Exception {
|
||||||
|
StructurePoolElement allAirElement = StructurePoolElement
|
||||||
|
.legacy("test:all_air_primary")
|
||||||
|
.apply(StructureTemplatePool.Projection.RIGID);
|
||||||
|
StructurePoolElement nonAirElement = StructurePoolElement
|
||||||
|
.legacy("test:non_air_primary")
|
||||||
|
.apply(StructureTemplatePool.Projection.RIGID);
|
||||||
|
VillageImporter.PoolElementResolution allAirResolution = VillageImporter.resolvePoolElement(allAirElement);
|
||||||
|
VillageImporter.PoolElementResolution nonAirResolution = VillageImporter.resolvePoolElement(nonAirElement);
|
||||||
|
VillageImporter.PoolMemberNormalization allAir = VillageImporter.normalizePoolMember(
|
||||||
|
"test:primary",
|
||||||
|
"qa/pool/test/primary",
|
||||||
|
false,
|
||||||
|
1,
|
||||||
|
"test:fallback",
|
||||||
|
allAirResolution.templateLocation(),
|
||||||
|
"qa/piece/test/all_air_primary",
|
||||||
|
5,
|
||||||
|
0,
|
||||||
|
List.of());
|
||||||
|
VillageImporter.PoolMemberNormalization nonAir = VillageImporter.normalizePoolMember(
|
||||||
|
"test:primary",
|
||||||
|
"qa/pool/test/primary",
|
||||||
|
false,
|
||||||
|
1,
|
||||||
|
"test:fallback",
|
||||||
|
nonAirResolution.templateLocation(),
|
||||||
|
"qa/piece/test/non_air_primary",
|
||||||
|
7,
|
||||||
|
29,
|
||||||
|
List.of());
|
||||||
|
|
||||||
|
assertSame(allAirElement, allAirResolution.physicalElement());
|
||||||
|
assertSame(nonAirElement, nonAirResolution.physicalElement());
|
||||||
|
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, allAir.disposition());
|
||||||
|
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, nonAir.disposition());
|
||||||
|
assertEquals("qa/piece/test/all_air_primary", allAir.poolEntry().get("piece"));
|
||||||
|
assertEquals("qa/piece/test/non_air_primary", nonAir.poolEntry().get("piece"));
|
||||||
|
assertTrue(allAir.losses().isEmpty());
|
||||||
|
assertTrue(nonAir.losses().isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,9 @@ import art.arcane.iris.world.lifecycle.MissingWorldStorageLog;
|
|||||||
import art.arcane.iris.world.lifecycle.WorldReplacementBootstrap;
|
import art.arcane.iris.world.lifecycle.WorldReplacementBootstrap;
|
||||||
import art.arcane.iris.world.lifecycle.WorldReplacementBootstrapMarker;
|
import art.arcane.iris.world.lifecycle.WorldReplacementBootstrapMarker;
|
||||||
import art.arcane.iris.pack.DefaultPackBootstrapProvisioner;
|
import art.arcane.iris.pack.DefaultPackBootstrapProvisioner;
|
||||||
|
import art.arcane.iris.pack.DefaultPackBootstrapProvisioner.BootstrapRequest;
|
||||||
|
import art.arcane.iris.platform.bukkit.nms.datapack.DataVersion;
|
||||||
|
import io.papermc.paper.ServerBuildInfo;
|
||||||
import art.arcane.iris.pack.DefaultPackBootstrapProvisioner.ProvisionResult;
|
import art.arcane.iris.pack.DefaultPackBootstrapProvisioner.ProvisionResult;
|
||||||
import art.arcane.iris.platform.bootstrap.SlimJar;
|
import art.arcane.iris.platform.bootstrap.SlimJar;
|
||||||
import io.papermc.paper.plugin.bootstrap.BootstrapContext;
|
import io.papermc.paper.plugin.bootstrap.BootstrapContext;
|
||||||
@@ -184,11 +187,12 @@ public final class IrisBootstrap implements PluginBootstrap {
|
|||||||
|
|
||||||
private static ProvisionResult provision(BootstrapContext context, BukkitStartupPaths startupPaths) {
|
private static ProvisionResult provision(BootstrapContext context, BukkitStartupPaths startupPaths) {
|
||||||
try {
|
try {
|
||||||
return DefaultPackBootstrapProvisioner.provision(
|
return DefaultPackBootstrapProvisioner.provision(new BootstrapRequest(
|
||||||
context.getDataDirectory(),
|
context.getDataDirectory(),
|
||||||
message -> context.getLogger().info(message),
|
message -> context.getLogger().info(message),
|
||||||
startupPaths
|
startupPaths,
|
||||||
);
|
DataVersion.forMinecraftVersion(ServerBuildInfo.buildInfo().minecraftVersionId())
|
||||||
|
));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new IllegalStateException("Unable to provision the Iris startup datapack", e);
|
throw new IllegalStateException("Unable to provision the Iris startup datapack", e);
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-5
@@ -343,11 +343,11 @@ public final class ModdedForcedDatapack {
|
|||||||
throw new IllegalStateException("SHA-256 is unavailable", missing);
|
throw new IllegalStateException("SHA-256 is unavailable", missing);
|
||||||
}
|
}
|
||||||
digest.update((HASH_SALT + '|' + ModdedEngineBootstrap.loader().platformName()
|
digest.update((HASH_SALT + '|' + ModdedEngineBootstrap.loader().platformName()
|
||||||
+ '|' + DataVersion.getLatest().getPackFormat() + '\n').getBytes(StandardCharsets.UTF_8));
|
+ '|' + DataVersion.V26_2.getPackFormat() + '\n').getBytes(StandardCharsets.UTF_8));
|
||||||
String inputFingerprint = IrisDatapackCompiler.computeInputFingerprint(
|
String inputFingerprint = IrisDatapackCompiler.computeInputFingerprint(
|
||||||
compilationPackRoots(),
|
compilationPackRoots(),
|
||||||
List.of(),
|
List.of(),
|
||||||
DataVersion.getLatest().get(),
|
DataVersion.V26_2.get(),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
digest.update(inputFingerprint.getBytes(StandardCharsets.UTF_8));
|
digest.update(inputFingerprint.getBytes(StandardCharsets.UTF_8));
|
||||||
@@ -356,7 +356,7 @@ public final class ModdedForcedDatapack {
|
|||||||
|
|
||||||
private static void writeStagedPack(Path stagingDirectory) throws IOException {
|
private static void writeStagedPack(Path stagingDirectory) throws IOException {
|
||||||
Map<String, KSet<String>> seenBiomes = new LinkedHashMap<>();
|
Map<String, KSet<String>> seenBiomes = new LinkedHashMap<>();
|
||||||
IDataFixer fixer = DataVersion.getLatest().get();
|
IDataFixer fixer = DataVersion.V26_2.get();
|
||||||
|
|
||||||
int packCount = 0;
|
int packCount = 0;
|
||||||
KList<String> presetIds = new KList<>();
|
KList<String> presetIds = new KList<>();
|
||||||
@@ -706,8 +706,8 @@ public final class ModdedForcedDatapack {
|
|||||||
+ "}\n", StandardCharsets.UTF_8);
|
+ "}\n", StandardCharsets.UTF_8);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void writePackMeta(Path packDirectory) throws IOException {
|
static void writePackMeta(Path packDirectory) throws IOException {
|
||||||
int packFormat = DataVersion.getLatest().getPackFormat();
|
int packFormat = DataVersion.V26_2.getPackFormat();
|
||||||
String json = "{\n"
|
String json = "{\n"
|
||||||
+ " \"pack\": {\n"
|
+ " \"pack\": {\n"
|
||||||
+ " \"description\": \"Iris world generation biomes and dimension types for installed packs.\",\n"
|
+ " \"description\": \"Iris world generation biomes and dimension types for installed packs.\",\n"
|
||||||
|
|||||||
+1
-1
@@ -55,7 +55,7 @@ final class WorldCheckDimensionContract {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static DimensionContract expectedDimensionContract(IrisDimension dimension) {
|
static DimensionContract expectedDimensionContract(IrisDimension dimension) {
|
||||||
JSONObject json = new JSONObject(dimension.getDimensionType().toJson(DataVersion.getLatest().get()));
|
JSONObject json = new JSONObject(dimension.getDimensionType().toJson(DataVersion.V26_2.get()));
|
||||||
return new DimensionContract(
|
return new DimensionContract(
|
||||||
json.getInt("min_y"),
|
json.getInt("min_y"),
|
||||||
json.getInt("height"),
|
json.getInt("height"),
|
||||||
|
|||||||
+2
-2
@@ -166,7 +166,7 @@ public final class ModdedDatapackCommands {
|
|||||||
IrisDimension dimension = engine.getDimension();
|
IrisDimension dimension = engine.getDimension();
|
||||||
String json;
|
String json;
|
||||||
try {
|
try {
|
||||||
json = dimension.getDimensionType().toJson(DataVersion.getLatest().get());
|
json = dimension.getDimensionType().toJson(DataVersion.V26_2.get());
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
ModdedIrisLog.error("Iris dimension type generation failed for {}", dimension.getLoadKey(), e);
|
ModdedIrisLog.error("Iris dimension type generation failed for {}", dimension.getLoadKey(), e);
|
||||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_DATAPACK_COMMANDS_DIMENSION_TYPE_GENERATION_FAILED, MessageArgument.untrusted("value", level.dimension().identifier()), MessageArgument.untrusted("value2", String.valueOf(e.getMessage()))));
|
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_DATAPACK_COMMANDS_DIMENSION_TYPE_GENERATION_FAILED, MessageArgument.untrusted("value", level.dimension().identifier()), MessageArgument.untrusted("value2", String.valueOf(e.getMessage()))));
|
||||||
@@ -188,7 +188,7 @@ public final class ModdedDatapackCommands {
|
|||||||
}
|
}
|
||||||
|
|
||||||
File mcmeta = new File(worldDatapacksFolder(server), WORLD_PACK_NAME + "/pack.mcmeta");
|
File mcmeta = new File(worldDatapacksFolder(server), WORLD_PACK_NAME + "/pack.mcmeta");
|
||||||
int packFormat = DataVersion.getLatest().getPackFormat();
|
int packFormat = DataVersion.V26_2.getPackFormat();
|
||||||
String meta = "{\n"
|
String meta = "{\n"
|
||||||
+ " \"pack\": {\n"
|
+ " \"pack\": {\n"
|
||||||
+ " \"description\": \"Iris dimension types derived from the installed Iris packs.\",\n"
|
+ " \"description\": \"Iris dimension types derived from the installed Iris packs.\",\n"
|
||||||
|
|||||||
+1
-1
@@ -77,7 +77,7 @@ public class ModdedDimensionTypeParityTest {
|
|||||||
IrisDimension end = dimension("the_end", IrisEnvironment.THE_END, 0, 256, 256, new IrisDimensionTypeOptions());
|
IrisDimension end = dimension("the_end", IrisEnvironment.THE_END, 0, 256, 256, new IrisDimensionTypeOptions());
|
||||||
IrisDimension custom = dimension("custom_contract", IrisEnvironment.CUSTOM, -128, 384, 384, customOptions);
|
IrisDimension custom = dimension("custom_contract", IrisEnvironment.CUSTOM, -128, 384, 384, customOptions);
|
||||||
List<IrisDimension> dimensions = List.of(overworld, nether, end, custom);
|
List<IrisDimension> dimensions = List.of(overworld, nether, end, custom);
|
||||||
IDataFixer fixer = DataVersion.getLatest().get();
|
IDataFixer fixer = DataVersion.V26_2.get();
|
||||||
Path packDirectory = Files.createTempDirectory("iris-dimension-contracts");
|
Path packDirectory = Files.createTempDirectory("iris-dimension-contracts");
|
||||||
KList<File> roots = new KList<>();
|
KList<File> roots = new KList<>();
|
||||||
roots.add(packDirectory.toFile());
|
roots.add(packDirectory.toFile());
|
||||||
|
|||||||
+16
@@ -19,6 +19,7 @@
|
|||||||
package art.arcane.iris.modded;
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
import art.arcane.volmlib.util.collection.KSet;
|
import art.arcane.volmlib.util.collection.KSet;
|
||||||
|
import art.arcane.volmlib.util.json.JSONObject;
|
||||||
import art.arcane.iris.platform.bukkit.nms.datapack.v1217.DataFixerV1217;
|
import art.arcane.iris.platform.bukkit.nms.datapack.v1217.DataFixerV1217;
|
||||||
import art.arcane.iris.world.history.GenerationEpochContractFactory;
|
import art.arcane.iris.world.history.GenerationEpochContractFactory;
|
||||||
import art.arcane.iris.world.history.GenerationRegistryContract;
|
import art.arcane.iris.world.history.GenerationRegistryContract;
|
||||||
@@ -49,6 +50,21 @@ import static org.junit.Assert.assertThrows;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class ModdedForcedDatapackTest {
|
public class ModdedForcedDatapackTest {
|
||||||
|
@Test
|
||||||
|
public void metadataRetains262FormatWhenBukkitAlsoSupports263() throws IOException {
|
||||||
|
Path packDirectory = Files.createTempDirectory("iris-modded-pack-format");
|
||||||
|
try {
|
||||||
|
ModdedForcedDatapack.writePackMeta(packDirectory);
|
||||||
|
JSONObject pack = new JSONObject(Files.readString(packDirectory.resolve("pack.mcmeta")))
|
||||||
|
.getJSONObject("pack");
|
||||||
|
assertEquals(107, pack.getInt("pack_format"));
|
||||||
|
assertEquals(107, pack.getInt("min_format"));
|
||||||
|
assertEquals(107, pack.getInt("max_format"));
|
||||||
|
} finally {
|
||||||
|
deleteTree(packDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void retainedDimensionSourceRendersBeforeNativeRegistryExists() throws Exception {
|
public void retainedDimensionSourceRendersBeforeNativeRegistryExists() throws Exception {
|
||||||
DataFixerV1217 fixer = new DataFixerV1217();
|
DataFixerV1217 fixer = new DataFixerV1217();
|
||||||
|
|||||||
+33
-4
@@ -74,8 +74,8 @@ project(':core') {
|
|||||||
version = rootProject.version
|
version = rootProject.version
|
||||||
}
|
}
|
||||||
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse('26.2')
|
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse('26.2')
|
||||||
// The Bukkit jar runs Paper 26.1.2 through 26.2 (compile-low pins); the loader jars stay 26.2-only.
|
// The Bukkit jar retains 26.1.2 and 26.2 alongside 26.3; loader jars stay 26.2-only.
|
||||||
String bukkitMinecraftRange = providers.gradleProperty('bukkitMinecraftRange').getOrElse('26.1.2-26.2')
|
String bukkitMinecraftRange = providers.gradleProperty('bukkitMinecraftRange').getOrElse('26.1.2-26.3')
|
||||||
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse('0.19.3')
|
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse('0.19.3')
|
||||||
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse('26.2-65.1.1')
|
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse('26.2-65.1.1')
|
||||||
String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse('26.2.0.59')
|
String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse('26.2.0.59')
|
||||||
@@ -141,6 +141,7 @@ String neoForgeConsumerPath = "${consumerLocation}/neoforge-mod-consumers/dropin
|
|||||||
|
|
||||||
def nmsBindings = [
|
def nmsBindings = [
|
||||||
v26_2_R1: '26.1.2.build.74-stable',
|
v26_2_R1: '26.1.2.build.74-stable',
|
||||||
|
v26_3_R1: '26.3.build.25-alpha',
|
||||||
]
|
]
|
||||||
Class nmsTypeClass = Class.forName('NMSBinding$Type')
|
Class nmsTypeClass = Class.forName('NMSBinding$Type')
|
||||||
nmsBindings.each { key, value ->
|
nmsBindings.each { key, value ->
|
||||||
@@ -154,7 +155,9 @@ nmsBindings.each { key, value ->
|
|||||||
extensions.extraProperties.set('nms', nmsConfig)
|
extensions.extraProperties.set('nms', nmsConfig)
|
||||||
plugins.apply(NMSBinding)
|
plugins.apply(NMSBinding)
|
||||||
|
|
||||||
sourceSets.main.java.srcDir(rootProject.file('adapters/minecraft-common/src/main/java'))
|
if (key == 'v26_2_R1') {
|
||||||
|
sourceSets.main.java.srcDir(rootProject.file('adapters/minecraft-common/src/main/java'))
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(project(':core'))
|
compileOnly(project(':core'))
|
||||||
@@ -619,6 +622,7 @@ List<String> requiredBukkitArtifactEntries = [
|
|||||||
'art/arcane/iris/IrisBootstrap.class',
|
'art/arcane/iris/IrisBootstrap.class',
|
||||||
'art/arcane/iris/platform/bukkit/nms/INMS.class',
|
'art/arcane/iris/platform/bukkit/nms/INMS.class',
|
||||||
'art/arcane/iris/platform/bukkit/nms/v26_2_R1/NMSBinding.class',
|
'art/arcane/iris/platform/bukkit/nms/v26_2_R1/NMSBinding.class',
|
||||||
|
'art/arcane/iris/platform/bukkit/nms/v26_3_R1/NMSBinding.class',
|
||||||
'art/arcane/iris/platform/bukkit/nms/v1X/NMSBinding1X.class',
|
'art/arcane/iris/platform/bukkit/nms/v1X/NMSBinding1X.class',
|
||||||
'art/arcane/iris/platform/generation/PlatformChunkGenerator.class',
|
'art/arcane/iris/platform/generation/PlatformChunkGenerator.class',
|
||||||
'art/arcane/iris/world/lifecycle/WorldLifecycleStaging.class',
|
'art/arcane/iris/world/lifecycle/WorldLifecycleStaging.class',
|
||||||
@@ -856,9 +860,34 @@ pluginPackaging {
|
|||||||
prunePrefixes = ['art/arcane/volmlib/']
|
prunePrefixes = ['art/arcane/volmlib/']
|
||||||
keepPrefixes = ['art/arcane/volmlib/util/matter/slices/']
|
keepPrefixes = ['art/arcane/volmlib/util/matter/slices/']
|
||||||
shrinkRules = [file('packaging/shrink.pro')]
|
shrinkRules = [file('packaging/shrink.pro')]
|
||||||
|
shrinkDontwarn = [
|
||||||
|
'java.lang.invoke.MethodHandle',
|
||||||
|
'java.lang.invoke.VarHandle',
|
||||||
|
'net.minecraft.server.MinecraftServer',
|
||||||
|
'net.minecraft.server.level.ServerLevel',
|
||||||
|
'net.minecraft.world.attribute.EnvironmentAttributes',
|
||||||
|
'net.minecraft.world.level.NaturalSpawner',
|
||||||
|
'net.minecraft.world.level.StructureManager',
|
||||||
|
'net.minecraft.world.level.biome.BiomeManager',
|
||||||
|
'net.minecraft.world.level.biome.BiomeResolver',
|
||||||
|
'net.minecraft.world.level.biome.BiomeSource',
|
||||||
|
'net.minecraft.world.level.biome.MobSpawnSettings',
|
||||||
|
'net.minecraft.world.level.biome.MobSpawnSettings$SpawnerData',
|
||||||
|
'net.minecraft.world.level.chunk.ChunkAccess',
|
||||||
|
'net.minecraft.world.level.chunk.ChunkGenerator',
|
||||||
|
'net.minecraft.world.level.chunk.status.ChunkStatus',
|
||||||
|
'net.minecraft.world.level.levelgen.RandomState',
|
||||||
|
'net.minecraft.world.level.levelgen.feature.Feature',
|
||||||
|
'net.minecraft.world.level.levelgen.structure.Structure',
|
||||||
|
'net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement',
|
||||||
|
'net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement',
|
||||||
|
'org.bukkit.craftbukkit.inventory.CraftItemStack',
|
||||||
|
]
|
||||||
shrinkLibraries.from(provider { project(':core').configurations.compileClasspath })
|
shrinkLibraries.from(provider { project(':core').configurations.compileClasspath })
|
||||||
shrinkLibraries.from(provider { project(':adapters:bukkit:plugin').configurations.compileClasspath })
|
shrinkLibraries.from(provider { project(':adapters:bukkit:plugin').configurations.compileClasspath })
|
||||||
shrinkLibraries.from(provider { project(':adapters:bukkit:nms:v26_2_R1').configurations.compileClasspath })
|
nmsBindings.keySet().each { binding ->
|
||||||
|
shrinkLibraries.from(provider { project(":adapters:bukkit:nms:${binding}").configurations.compileClasspath })
|
||||||
|
}
|
||||||
shrinkRelocations = runtimeDownloadRelocations
|
shrinkRelocations = runtimeDownloadRelocations
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ public final class GenerationRevisionScope {
|
|||||||
"spi/src/main/java",
|
"spi/src/main/java",
|
||||||
"adapters/bukkit/plugin/src/main/java",
|
"adapters/bukkit/plugin/src/main/java",
|
||||||
"adapters/bukkit/nms/v26_2_R1/src/main/java",
|
"adapters/bukkit/nms/v26_2_R1/src/main/java",
|
||||||
|
"adapters/bukkit/nms/v26_3_R1/src/main/java",
|
||||||
"adapters/minecraft-common/src/main/java",
|
"adapters/minecraft-common/src/main/java",
|
||||||
"adapters/modded-common/src/main/java",
|
"adapters/modded-common/src/main/java",
|
||||||
"adapters/client-common/src/main/java",
|
"adapters/client-common/src/main/java",
|
||||||
@@ -31,6 +32,7 @@ public final class GenerationRevisionScope {
|
|||||||
"core/src/main/java/art/arcane/iris/world/safeguard/ValueWithDiagnostics.java",
|
"core/src/main/java/art/arcane/iris/world/safeguard/ValueWithDiagnostics.java",
|
||||||
"core/src/main/java/art/arcane/iris/diagnostics/splash/IrisSplashRenderer.java",
|
"core/src/main/java/art/arcane/iris/diagnostics/splash/IrisSplashRenderer.java",
|
||||||
"adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_2_R1/NmsWorldLifecycle.java",
|
"adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_2_R1/NmsWorldLifecycle.java",
|
||||||
|
"adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsWorldLifecycle.java",
|
||||||
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandDeveloper.java",
|
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandDeveloper.java",
|
||||||
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandPregen.java",
|
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandPregen.java",
|
||||||
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandIris.java",
|
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandIris.java",
|
||||||
|
|||||||
@@ -62,15 +62,10 @@ public final class DefaultPackBootstrapProvisioner {
|
|||||||
return DEFAULT_PACKS;
|
return DEFAULT_PACKS;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ProvisionResult provision(Path dataDirectory, Consumer<String> feedback) throws IOException {
|
public static ProvisionResult provision(BootstrapRequest request) throws IOException {
|
||||||
return provision(dataDirectory, feedback, BukkitStartupPaths.resolveCurrent());
|
Path dataDirectory = request.dataDirectory();
|
||||||
}
|
Consumer<String> feedback = request.feedback();
|
||||||
|
BukkitStartupPaths startupPaths = request.startupPaths();
|
||||||
public static ProvisionResult provision(
|
|
||||||
Path dataDirectory,
|
|
||||||
Consumer<String> feedback,
|
|
||||||
BukkitStartupPaths startupPaths
|
|
||||||
) throws IOException {
|
|
||||||
Objects.requireNonNull(dataDirectory, "dataDirectory");
|
Objects.requireNonNull(dataDirectory, "dataDirectory");
|
||||||
Objects.requireNonNull(feedback, "feedback");
|
Objects.requireNonNull(feedback, "feedback");
|
||||||
BukkitStartupPaths requiredStartupPaths = Objects.requireNonNull(startupPaths, "startupPaths");
|
BukkitStartupPaths requiredStartupPaths = Objects.requireNonNull(startupPaths, "startupPaths");
|
||||||
@@ -94,7 +89,8 @@ public final class DefaultPackBootstrapProvisioner {
|
|||||||
3,
|
3,
|
||||||
Duration.ofMillis(250),
|
Duration.ofMillis(250),
|
||||||
MAX_ARCHIVE_BYTES,
|
MAX_ARCHIVE_BYTES,
|
||||||
levelRoot
|
levelRoot,
|
||||||
|
request.dataVersion()
|
||||||
);
|
);
|
||||||
return provision(dataDirectory, feedback, options);
|
return provision(dataDirectory, feedback, options);
|
||||||
}
|
}
|
||||||
@@ -135,7 +131,7 @@ public final class DefaultPackBootstrapProvisioner {
|
|||||||
if (!Integer.toString(MARKER_SCHEMA).equals(marker.getProperty("schema"))) {
|
if (!Integer.toString(MARKER_SCHEMA).equals(marker.getProperty("schema"))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
IDataFixer fixer = DataVersion.getLatest().get();
|
IDataFixer fixer = DataVersion.getRuntime().get();
|
||||||
if (fixer == null
|
if (fixer == null
|
||||||
|| !IrisDatapackCompiler.compilerIdentity(fixer)
|
|| !IrisDatapackCompiler.compilerIdentity(fixer)
|
||||||
.equals(marker.getProperty("compilerIdentity"))) {
|
.equals(marker.getProperty("compilerIdentity"))) {
|
||||||
@@ -248,9 +244,9 @@ public final class DefaultPackBootstrapProvisioner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<File> packRoots = IrisDatapackCompiler.collectPackRoots(normalizedData, options.levelRoot());
|
List<File> packRoots = IrisDatapackCompiler.collectPackRoots(normalizedData, options.levelRoot());
|
||||||
IDataFixer fixer = DataVersion.getLatest().get();
|
IDataFixer fixer = options.dataVersion().get();
|
||||||
if (fixer == null) {
|
if (fixer == null) {
|
||||||
throw new IOException("Latest Iris datapack fixer is unavailable during bootstrap");
|
throw new IOException("Selected Iris datapack fixer is unavailable during bootstrap");
|
||||||
}
|
}
|
||||||
String compilerIdentity = IrisDatapackCompiler.compilerIdentity(fixer);
|
String compilerIdentity = IrisDatapackCompiler.compilerIdentity(fixer);
|
||||||
String aggregateFingerprint = packRootsFingerprint(packRoots);
|
String aggregateFingerprint = packRootsFingerprint(packRoots);
|
||||||
@@ -915,6 +911,16 @@ public final class DefaultPackBootstrapProvisioner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record BootstrapRequest(Path dataDirectory, Consumer<String> feedback,
|
||||||
|
BukkitStartupPaths startupPaths, DataVersion dataVersion) {
|
||||||
|
public BootstrapRequest {
|
||||||
|
Objects.requireNonNull(dataDirectory);
|
||||||
|
Objects.requireNonNull(feedback);
|
||||||
|
Objects.requireNonNull(startupPaths);
|
||||||
|
Objects.requireNonNull(dataVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
record ProvisionOptions(
|
record ProvisionOptions(
|
||||||
List<PackSpec> packs,
|
List<PackSpec> packs,
|
||||||
List<IrisGeneratorBinding> bindings,
|
List<IrisGeneratorBinding> bindings,
|
||||||
@@ -925,7 +931,8 @@ public final class DefaultPackBootstrapProvisioner {
|
|||||||
int attempts,
|
int attempts,
|
||||||
Duration retryDelay,
|
Duration retryDelay,
|
||||||
long maxArchiveBytes,
|
long maxArchiveBytes,
|
||||||
Path levelRoot
|
Path levelRoot,
|
||||||
|
DataVersion dataVersion
|
||||||
) {
|
) {
|
||||||
ProvisionOptions {
|
ProvisionOptions {
|
||||||
packs = List.copyOf(Objects.requireNonNull(packs, "packs"));
|
packs = List.copyOf(Objects.requireNonNull(packs, "packs"));
|
||||||
@@ -936,6 +943,10 @@ public final class DefaultPackBootstrapProvisioner {
|
|||||||
Objects.requireNonNull(requestTimeout, "requestTimeout");
|
Objects.requireNonNull(requestTimeout, "requestTimeout");
|
||||||
Objects.requireNonNull(retryDelay, "retryDelay");
|
Objects.requireNonNull(retryDelay, "retryDelay");
|
||||||
Objects.requireNonNull(levelRoot, "levelRoot");
|
Objects.requireNonNull(levelRoot, "levelRoot");
|
||||||
|
Objects.requireNonNull(dataVersion, "dataVersion");
|
||||||
|
if (dataVersion == DataVersion.UNSUPPORTED) {
|
||||||
|
throw new IllegalArgumentException("Unsupported bootstrap datapack version");
|
||||||
|
}
|
||||||
if (attempts < 1 || maxArchiveBytes < 1L) {
|
if (attempts < 1 || maxArchiveBytes < 1L) {
|
||||||
throw new IllegalArgumentException("Invalid bootstrap provisioning options");
|
throw new IllegalArgumentException("Invalid bootstrap provisioning options");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ public interface INMSBinding {
|
|||||||
if (!supportsIrisWorldGeneration()) {
|
if (!supportsIrisWorldGeneration()) {
|
||||||
throw new IllegalStateException("Iris world '" + c.name() + "' cannot be created with limited NMS binding "
|
throw new IllegalStateException("Iris world '" + c.name() + "' cannot be created with limited NMS binding "
|
||||||
+ getClass().getSimpleName()
|
+ getClass().getSimpleName()
|
||||||
+ "; set general.disableNMS=false and use a supported Minecraft server runtime (26.1.2 or 26.2)");
|
+ "; set general.disableNMS=false and use a supported Minecraft server runtime (26.1.2, 26.2 or 26.3)");
|
||||||
}
|
}
|
||||||
if (missingDimensionTypes(generator.getTarget().getDimension().getDimensionTypeKey())) {
|
if (missingDimensionTypes(generator.getTarget().getDimension().getDimensionTypeKey())) {
|
||||||
throw new IllegalStateException("Missing dimension types to create world");
|
throw new IllegalStateException("Missing dimension types to create world");
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package art.arcane.iris.platform.bukkit.nms;
|
package art.arcane.iris.platform.bukkit.nms;
|
||||||
|
|
||||||
final class NmsBindingSelector {
|
final class NmsBindingSelector {
|
||||||
private static final String SUPPORTED_VERSIONS = "26.1.2 or 26.2";
|
private static final String SUPPORTED_VERSIONS = "26.1.2, 26.2 or 26.3";
|
||||||
private static final String SUPPORTED_TAG = "v26_2_R1";
|
private static final String SUPPORTED_TAG = "v26_2_R1";
|
||||||
|
|
||||||
private NmsBindingSelector() {
|
private NmsBindingSelector() {
|
||||||
@@ -11,6 +11,9 @@ final class NmsBindingSelector {
|
|||||||
if (version == null) {
|
if (version == null) {
|
||||||
throw new IllegalStateException("Iris requires an exact Minecraft version before selecting NMS");
|
throw new IllegalStateException("Iris requires an exact Minecraft version before selecting NMS");
|
||||||
}
|
}
|
||||||
|
if (version.isSameRelease(26, 3, 0)) {
|
||||||
|
return "v26_3_R1";
|
||||||
|
}
|
||||||
if (!version.isSameRelease(26, 2, 0) && !version.isSameRelease(26, 1, 2)) {
|
if (!version.isSameRelease(26, 2, 0) && !version.isSameRelease(26, 1, 2)) {
|
||||||
throw new IllegalStateException("Iris requires Minecraft " + SUPPORTED_VERSIONS
|
throw new IllegalStateException("Iris requires Minecraft " + SUPPORTED_VERSIONS
|
||||||
+ ". Detected server version: " + version.value());
|
+ ". Detected server version: " + version.value());
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package art.arcane.iris.platform.bukkit.nms.datapack;
|
package art.arcane.iris.platform.bukkit.nms.datapack;
|
||||||
|
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
import art.arcane.iris.platform.bukkit.nms.INMS;
|
import art.arcane.iris.platform.bukkit.nms.INMS;
|
||||||
import art.arcane.iris.platform.bukkit.nms.datapack.v1217.DataFixerV1217;
|
import art.arcane.iris.platform.bukkit.nms.datapack.v1217.DataFixerV1217;
|
||||||
import art.arcane.volmlib.util.collection.KMap;
|
import art.arcane.volmlib.util.collection.KMap;
|
||||||
@@ -8,13 +9,15 @@ import lombok.AccessLevel;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
import art.arcane.iris.platform.bukkit.nms.datapack.v263.DataFixerV263;
|
||||||
|
|
||||||
//https://minecraft.wiki/w/Pack_format
|
//https://minecraft.wiki/w/Pack_format
|
||||||
@Getter
|
@Getter
|
||||||
public enum DataVersion {
|
public enum DataVersion {
|
||||||
UNSUPPORTED("0.0.0", 0, () -> null),
|
UNSUPPORTED("0.0.0", 0, () -> null),
|
||||||
V26_1_2("26.1.2", 101, DataFixerV1217::new),
|
V26_1_2("26.1.2", 101, DataFixerV1217::new),
|
||||||
V26_2("26.2", 107, DataFixerV1217::new);
|
V26_2("26.2", 107, DataFixerV1217::new),
|
||||||
|
V26_3("26.3", 121, DataFixerV263::new);
|
||||||
private static final KMap<DataVersion, IDataFixer> cache = new KMap<>();
|
private static final KMap<DataVersion, IDataFixer> cache = new KMap<>();
|
||||||
@Getter(AccessLevel.NONE)
|
@Getter(AccessLevel.NONE)
|
||||||
private final Supplier<IDataFixer> constructor;
|
private final Supplier<IDataFixer> constructor;
|
||||||
@@ -49,6 +52,29 @@ public enum DataVersion {
|
|||||||
return fixer;
|
return fixer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static DataVersion forMinecraftVersion(String version) {
|
||||||
|
if (version == null) {
|
||||||
|
return UNSUPPORTED;
|
||||||
|
}
|
||||||
|
return switch (version) {
|
||||||
|
case "26.1.2" -> V26_1_2;
|
||||||
|
case "26.2", "26.2.0" -> V26_2;
|
||||||
|
case "26.3", "26.3.0" -> V26_3;
|
||||||
|
default -> UNSUPPORTED;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DataVersion getRuntime() {
|
||||||
|
if (!IrisPlatforms.isBound() || IrisPlatforms.get().minecraftVersion() == null) {
|
||||||
|
return getLatest();
|
||||||
|
}
|
||||||
|
DataVersion version = forMinecraftVersion(IrisPlatforms.get().minecraftVersion());
|
||||||
|
if (version == UNSUPPORTED) {
|
||||||
|
throw new IllegalStateException("Unsupported Minecraft datapack version: " + IrisPlatforms.get().minecraftVersion());
|
||||||
|
}
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
public static DataVersion getLatest() {
|
public static DataVersion getLatest() {
|
||||||
return values()[values().length - 1];
|
return values()[values().length - 1];
|
||||||
}
|
}
|
||||||
|
|||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit.nms.datapack.v263;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.biome.IrisBiomeCustom;
|
||||||
|
import art.arcane.iris.platform.bukkit.nms.datapack.v1217.DataFixerV1217;
|
||||||
|
import art.arcane.volmlib.util.json.JSONArray;
|
||||||
|
import art.arcane.volmlib.util.json.JSONObject;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public final class DataFixerV263 extends DataFixerV1217 {
|
||||||
|
@Override
|
||||||
|
public JSONObject fixCustomBiome(IrisBiomeCustom biome, JSONObject json) {
|
||||||
|
JSONObject fixed = super.fixCustomBiome(biome, json);
|
||||||
|
JSONObject attributes = fixed.getJSONObject("attributes");
|
||||||
|
JSONObject effects = fixed.getJSONObject("effects");
|
||||||
|
for (String key : new String[]{"water_color", "foliage_color", "dry_foliage_color", "grass_color"}) {
|
||||||
|
color(effects, key);
|
||||||
|
}
|
||||||
|
for (String key : new String[]{"minecraft:visual/sky_color", "minecraft:visual/fog_color", "minecraft:visual/water_fog_color"}) {
|
||||||
|
color(attributes, key);
|
||||||
|
}
|
||||||
|
JSONObject spawners = fixed.getJSONObject("spawners");
|
||||||
|
for (String category : spawners.keySet()) {
|
||||||
|
JSONArray entries = spawners.getJSONArray(category);
|
||||||
|
for (int index = 0; index < entries.length(); index++) {
|
||||||
|
JSONObject spawn = entries.getJSONObject(index);
|
||||||
|
int minimum = spawn.getInt("minCount");
|
||||||
|
int maximum = spawn.getInt("maxCount");
|
||||||
|
spawn.remove("minCount");
|
||||||
|
spawn.remove("maxCount");
|
||||||
|
spawn.put("count", minimum == maximum ? minimum : new JSONObject()
|
||||||
|
.put("type", "minecraft:uniform")
|
||||||
|
.put("min_inclusive", minimum)
|
||||||
|
.put("max_inclusive", maximum));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attributes.put("minecraft:gameplay/natural_mob_spawns", new JSONObject()
|
||||||
|
.put("spawns_by_category", spawners)
|
||||||
|
.put("spawn_costs", fixed.getJSONObject("spawn_costs")));
|
||||||
|
Object probability = fixed.remove("creature_spawn_probability");
|
||||||
|
if (probability != null) {
|
||||||
|
attributes.put("minecraft:gameplay/creature_world_gen_spawn_probability", probability);
|
||||||
|
}
|
||||||
|
fixed.remove("spawners");
|
||||||
|
fixed.remove("spawn_costs");
|
||||||
|
return fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void color(JSONObject object, String key) {
|
||||||
|
if (object.opt(key) instanceof Number value) {
|
||||||
|
object.put(key, String.format(Locale.ROOT, "#%06x", value.intValue() & 0xFFFFFF));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -317,6 +317,6 @@ public class NMSBinding1X implements INMSBinding {
|
|||||||
private IllegalStateException unsupportedStructureHook(String operation) {
|
private IllegalStateException unsupportedStructureHook(String operation) {
|
||||||
return new IllegalStateException("Iris cannot " + operation + " with limited NMS binding "
|
return new IllegalStateException("Iris cannot " + operation + " with limited NMS binding "
|
||||||
+ getClass().getSimpleName()
|
+ getClass().getSimpleName()
|
||||||
+ "; set general.disableNMS=false and use a supported Minecraft server runtime (26.1.2 or 26.2)");
|
+ "; set general.disableNMS=false and use a supported Minecraft server runtime (26.1.2, 26.2 or 26.3)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package art.arcane.iris.structure.export;
|
package art.arcane.iris.structure.export;
|
||||||
|
|
||||||
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
|
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
|
||||||
|
import art.arcane.volmlib.util.nbt.mca.MCABlockStateCodecSupport;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
@@ -73,15 +74,15 @@ record VanillaBlockState(String name, Map<String, String> properties) {
|
|||||||
return builder.append(']').toString();
|
return builder.append(']').toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
CompoundTag toNbt() {
|
CompoundTag toNbt(MCABlockStateCodecSupport.Format format) {
|
||||||
CompoundTag state = new CompoundTag();
|
CompoundTag state = new CompoundTag();
|
||||||
state.putString("Name", name);
|
state.putString(format.nameKey(), name);
|
||||||
if (!properties.isEmpty()) {
|
if (!properties.isEmpty()) {
|
||||||
CompoundTag propertyTag = new CompoundTag();
|
CompoundTag propertyTag = new CompoundTag();
|
||||||
for (Map.Entry<String, String> property : properties.entrySet()) {
|
for (Map.Entry<String, String> property : properties.entrySet()) {
|
||||||
propertyTag.putString(property.getKey(), property.getValue());
|
propertyTag.putString(property.getKey(), property.getValue());
|
||||||
}
|
}
|
||||||
state.put("Properties", propertyTag);
|
state.put(format.propertiesKey(), propertyTag);
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-6
@@ -211,7 +211,7 @@ final class VanillaJigsawExportCompiler {
|
|||||||
addError(diagnostics,
|
addError(diagnostics,
|
||||||
VanillaJigsawExportDiagnostic.Code.INVALID_MAX_DEPTH,
|
VanillaJigsawExportDiagnostic.Code.INVALID_MAX_DEPTH,
|
||||||
structureKey(structure),
|
structureKey(structure),
|
||||||
"Minecraft 26.2 jigsaw size must be within 0..20; Iris export requires 1..20.");
|
"Minecraft jigsaw size must be within 0..20; Iris export requires 1..20.");
|
||||||
}
|
}
|
||||||
long horizontalDistance = (long) structure.getMaxSizeChunks() * 16L;
|
long horizontalDistance = (long) structure.getMaxSizeChunks() * 16L;
|
||||||
if (horizontalDistance < 1L || horizontalDistance > MAX_HORIZONTAL_DISTANCE) {
|
if (horizontalDistance < 1L || horizontalDistance > MAX_HORIZONTAL_DISTANCE) {
|
||||||
@@ -219,7 +219,7 @@ final class VanillaJigsawExportCompiler {
|
|||||||
VanillaJigsawExportDiagnostic.Code.INVALID_MAX_DISTANCE,
|
VanillaJigsawExportDiagnostic.Code.INVALID_MAX_DISTANCE,
|
||||||
structureKey(structure),
|
structureKey(structure),
|
||||||
"Iris maxSizeChunks maps to " + horizontalDistance
|
"Iris maxSizeChunks maps to " + horizontalDistance
|
||||||
+ " blocks, outside Minecraft 26.2's 1..128 horizontal limit.");
|
+ " blocks, outside Minecraft's 1..128 horizontal limit.");
|
||||||
}
|
}
|
||||||
int terrainPadding = settings.terrainAdaptation() == VanillaJigsawExportSettings.TerrainAdaptation.NONE
|
int terrainPadding = settings.terrainAdaptation() == VanillaJigsawExportSettings.TerrainAdaptation.NONE
|
||||||
? 0 : 12;
|
? 0 : 12;
|
||||||
@@ -315,7 +315,7 @@ final class VanillaJigsawExportCompiler {
|
|||||||
addError(diagnostics,
|
addError(diagnostics,
|
||||||
VanillaJigsawExportDiagnostic.Code.INVALID_POOL_WEIGHT,
|
VanillaJigsawExportDiagnostic.Code.INVALID_POOL_WEIGHT,
|
||||||
poolKey + "/pieces[" + index + "]",
|
poolKey + "/pieces[" + index + "]",
|
||||||
"Minecraft 26.2 template-pool weights must be within 1..150.");
|
"Minecraft template-pool weights must be within 1..150.");
|
||||||
}
|
}
|
||||||
if (entry.getChance() != 1D) {
|
if (entry.getChance() != 1D) {
|
||||||
addError(diagnostics,
|
addError(diagnostics,
|
||||||
@@ -577,10 +577,12 @@ final class VanillaJigsawExportCompiler {
|
|||||||
JsonObject pack = new JsonObject();
|
JsonObject pack = new JsonObject();
|
||||||
pack.addProperty("description", request.description());
|
pack.addProperty("description", request.description());
|
||||||
JsonArray minimum = new JsonArray();
|
JsonArray minimum = new JsonArray();
|
||||||
minimum.add(107);
|
boolean current = VanillaStructureTemplateEncoder.targets263();
|
||||||
minimum.add(1);
|
int packFormat = current ? 121 : 107;
|
||||||
|
minimum.add(packFormat);
|
||||||
|
minimum.add(current ? 0 : 1);
|
||||||
pack.add("min_format", minimum);
|
pack.add("min_format", minimum);
|
||||||
pack.addProperty("max_format", 107);
|
pack.addProperty("max_format", packFormat);
|
||||||
JsonObject root = new JsonObject();
|
JsonObject root = new JsonObject();
|
||||||
root.add("pack", pack);
|
root.add("pack", pack);
|
||||||
return root;
|
return root;
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ public final class VanillaJigsawExportRequest {
|
|||||||
private final Path output;
|
private final Path output;
|
||||||
private String namespace = "iris";
|
private String namespace = "iris";
|
||||||
private String resourcePath;
|
private String resourcePath;
|
||||||
private String description = "Iris vanilla jigsaw export for Minecraft 26.2";
|
private String description = "Iris vanilla jigsaw export";
|
||||||
private VanillaJigsawExportFormat format = VanillaJigsawExportFormat.DIRECTORY;
|
private VanillaJigsawExportFormat format = VanillaJigsawExportFormat.DIRECTORY;
|
||||||
private VanillaJigsawExportSettings settings = VanillaJigsawExportSettings.defaults();
|
private VanillaJigsawExportSettings settings = VanillaJigsawExportSettings.defaults();
|
||||||
private boolean replaceExisting;
|
private boolean replaceExisting;
|
||||||
|
|||||||
+14
-4
@@ -1,6 +1,8 @@
|
|||||||
package art.arcane.iris.structure.export;
|
package art.arcane.iris.structure.export;
|
||||||
|
|
||||||
import art.arcane.iris.pack.value.IrisDirection;
|
import art.arcane.iris.pack.value.IrisDirection;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
|
import art.arcane.volmlib.util.nbt.mca.MCABlockStateCodecSupport;
|
||||||
import art.arcane.iris.structure.jigsaw.IrisJigsawConnector;
|
import art.arcane.iris.structure.jigsaw.IrisJigsawConnector;
|
||||||
import art.arcane.iris.structure.jigsaw.IrisJigsawPiece;
|
import art.arcane.iris.structure.jigsaw.IrisJigsawPiece;
|
||||||
import art.arcane.iris.structure.object.IrisObject;
|
import art.arcane.iris.structure.object.IrisObject;
|
||||||
@@ -51,18 +53,26 @@ final class VanillaStructureTemplateEncoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean lowercase = targets263();
|
||||||
|
MCABlockStateCodecSupport.Format format = lowercase
|
||||||
|
? MCABlockStateCodecSupport.Format.LOWERCASE : MCABlockStateCodecSupport.Format.CAPITALIZED;
|
||||||
CompoundTag root = new CompoundTag();
|
CompoundTag root = new CompoundTag();
|
||||||
root.put("size", intList(object.getW(), object.getH(), object.getD()));
|
root.put("size", intList(object.getW(), object.getH(), object.getD()));
|
||||||
root.put("palette", palette(paletteStates));
|
root.put("palette", palette(paletteStates, format));
|
||||||
root.put("blocks", blocks(orderedBlocks, paletteIndexes));
|
root.put("blocks", blocks(orderedBlocks, paletteIndexes));
|
||||||
root.put("entities", new ListTag<>(CompoundTag.class));
|
root.put("entities", new ListTag<>(CompoundTag.class));
|
||||||
root.putInt("DataVersion", DATA_VERSION_26_2);
|
root.putInt("DataVersion", lowercase ? 5023 : DATA_VERSION_26_2);
|
||||||
|
|
||||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||||
NBTUtil.write(new NamedTag("", root), output, true);
|
NBTUtil.write(new NamedTag("", root), output, true);
|
||||||
return output.toByteArray();
|
return output.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static boolean targets263() {
|
||||||
|
String minecraftVersion = IrisPlatforms.get().minecraftVersion();
|
||||||
|
return "26.3".equals(minecraftVersion) || "26.3.0".equals(minecraftVersion);
|
||||||
|
}
|
||||||
|
|
||||||
private Map<BlockPosition, BlockEntry> objectBlocks(IrisObject object) {
|
private Map<BlockPosition, BlockEntry> objectBlocks(IrisObject object) {
|
||||||
Map<BlockPosition, BlockEntry> blocks = new LinkedHashMap<>();
|
Map<BlockPosition, BlockEntry> blocks = new LinkedHashMap<>();
|
||||||
Vector3i center = object.getCenter();
|
Vector3i center = object.getCenter();
|
||||||
@@ -135,10 +145,10 @@ final class VanillaStructureTemplateEncoder {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private ListTag<CompoundTag> palette(List<VanillaBlockState> states) {
|
private ListTag<CompoundTag> palette(List<VanillaBlockState> states, MCABlockStateCodecSupport.Format format) {
|
||||||
ListTag<CompoundTag> palette = new ListTag<>(CompoundTag.class);
|
ListTag<CompoundTag> palette = new ListTag<>(CompoundTag.class);
|
||||||
for (VanillaBlockState state : states) {
|
for (VanillaBlockState state : states) {
|
||||||
palette.add(state.toNbt());
|
palette.add(state.toNbt(format));
|
||||||
}
|
}
|
||||||
return palette;
|
return palette;
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-6
@@ -110,7 +110,7 @@ public final class GenerationRegistryContractFactory {
|
|||||||
data,
|
data,
|
||||||
dimension,
|
dimension,
|
||||||
packFingerprint,
|
packFingerprint,
|
||||||
DataVersion.getLatest().get(),
|
DataVersion.getRuntime().get(),
|
||||||
registries,
|
registries,
|
||||||
registries.generationRegistry(),
|
registries.generationRegistry(),
|
||||||
aliasPolicy
|
aliasPolicy
|
||||||
@@ -224,7 +224,7 @@ public final class GenerationRegistryContractFactory {
|
|||||||
data,
|
data,
|
||||||
dimension,
|
dimension,
|
||||||
customBiome,
|
customBiome,
|
||||||
DataVersion.getLatest().get(),
|
DataVersion.getRuntime().get(),
|
||||||
registry
|
registry
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -508,7 +508,7 @@ public final class GenerationRegistryContractFactory {
|
|||||||
) throws IOException {
|
) throws IOException {
|
||||||
return captureRequiredDefinitions(
|
return captureRequiredDefinitions(
|
||||||
requiredContracts,
|
requiredContracts,
|
||||||
DataVersion.getLatest().get(),
|
DataVersion.getRuntime().get(),
|
||||||
IrisPlatforms.get().registries().generationRegistry()
|
IrisPlatforms.get().registries().generationRegistry()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -519,7 +519,7 @@ public final class GenerationRegistryContractFactory {
|
|||||||
) throws IOException {
|
) throws IOException {
|
||||||
return captureRequiredDefinitions(
|
return captureRequiredDefinitions(
|
||||||
requiredContracts,
|
requiredContracts,
|
||||||
DataVersion.getLatest().get(),
|
DataVersion.getRuntime().get(),
|
||||||
generationRegistry
|
generationRegistry
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -603,7 +603,7 @@ public final class GenerationRegistryContractFactory {
|
|||||||
return requireGeneratedSource(
|
return requireGeneratedSource(
|
||||||
contract,
|
contract,
|
||||||
key,
|
key,
|
||||||
DataVersion.getLatest().get(),
|
DataVersion.getRuntime().get(),
|
||||||
IrisPlatforms.get().registries().generationRegistry()
|
IrisPlatforms.get().registries().generationRegistry()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -652,7 +652,7 @@ public final class GenerationRegistryContractFactory {
|
|||||||
return requireGeneratedSource(
|
return requireGeneratedSource(
|
||||||
contract,
|
contract,
|
||||||
key,
|
key,
|
||||||
DataVersion.getLatest().get(),
|
DataVersion.getRuntime().get(),
|
||||||
generationRegistry
|
generationRegistry
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ public final class SavedTerrainChunk {
|
|||||||
case "biomes" -> 3;
|
case "biomes" -> 3;
|
||||||
case "noise" -> 4;
|
case "noise" -> 4;
|
||||||
case "surface" -> 5;
|
case "surface" -> 5;
|
||||||
case "carvers" -> 6;
|
case "carvers", "terrain" -> 6;
|
||||||
case "features" -> 7;
|
case "features" -> 7;
|
||||||
case "initialize_light" -> 8;
|
case "initialize_light" -> 8;
|
||||||
case "light" -> 9;
|
case "light" -> 9;
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package art.arcane.iris.world.history;
|
package art.arcane.iris.world.history;
|
||||||
|
|
||||||
import art.arcane.volmlib.util.nbt.io.NBTUtil;
|
import art.arcane.volmlib.util.nbt.io.NBTUtil;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
|
import art.arcane.volmlib.util.nbt.mca.MCABlockStateCodecSupport;
|
||||||
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
|
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
|
||||||
import art.arcane.volmlib.util.nbt.tag.ByteArrayTag;
|
import art.arcane.volmlib.util.nbt.tag.ByteArrayTag;
|
||||||
import art.arcane.volmlib.util.nbt.tag.ListTag;
|
import art.arcane.volmlib.util.nbt.tag.ListTag;
|
||||||
@@ -81,7 +84,8 @@ final class SavedTerrainChunkReader {
|
|||||||
if (sectionY * 16 < minimumY || sectionY * 16 >= (long) minimumY + height) {
|
if (sectionY * 16 < minimumY || sectionY * 16 >= (long) minimumY + height) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (sections.put(sectionY, decodeSection(section)) != null) {
|
if (sections.put(sectionY, decodeSection(section, root.getInt("DataVersion") >= 5023
|
||||||
|
? MCABlockStateCodecSupport.Format.LOWERCASE : MCABlockStateCodecSupport.Format.CAPITALIZED)) != null) {
|
||||||
throw new IOException("Saved chunk contains duplicate section " + sectionY);
|
throw new IOException("Saved chunk contains duplicate section " + sectionY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,16 +162,29 @@ final class SavedTerrainChunkReader {
|
|||||||
return section;
|
return section;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Section decodeSection(CompoundTag section) throws IOException {
|
private static Section decodeSection(CompoundTag section, MCABlockStateCodecSupport.Format format) throws IOException {
|
||||||
CompoundTag blocks = compound(section, "block_states");
|
CompoundTag blocks = compound(section, "block_states");
|
||||||
CompoundTag biomes = compound(section, "biomes");
|
CompoundTag biomes = compound(section, "biomes");
|
||||||
ListTag<?> blockPalette = list(blocks, "palette");
|
ListTag<?> blockPalette = list(blocks, "palette");
|
||||||
List<BoundaryColumnGeometry.Voxel> decodedBlocks = new ArrayList<>(blockPalette.size());
|
List<BoundaryColumnGeometry.Voxel> decodedBlocks = new ArrayList<>(blockPalette.size());
|
||||||
for (Tag<?> entry : blockPalette) {
|
for (Tag<?> entry : blockPalette) {
|
||||||
if (!(entry instanceof CompoundTag state)) {
|
Tag<?> state = entry;
|
||||||
throw new IOException("Saved block palette entry is not a compound");
|
if (format == MCABlockStateCodecSupport.Format.LOWERCASE
|
||||||
|
&& state instanceof CompoundTag wrapped && wrapped.size() == 1 && wrapped.containsKey("")) {
|
||||||
|
state = wrapped.get("");
|
||||||
|
}
|
||||||
|
if (format == MCABlockStateCodecSupport.Format.LOWERCASE && state instanceof StringTag name) {
|
||||||
|
PlatformBlockState resolved = IrisPlatforms.get().registries().blockOrNull(name.getValue());
|
||||||
|
if (resolved == null) {
|
||||||
|
throw new IOException("Saved block state cannot be resolved: " + name.getValue());
|
||||||
|
}
|
||||||
|
decodedBlocks.add(decodeBlock(MCABlockStateCodecSupport.encodeBlockState(
|
||||||
|
resolved.key(), name.getValue(), format), format));
|
||||||
|
} else if (state instanceof CompoundTag compound) {
|
||||||
|
decodedBlocks.add(decodeBlock(compound, format));
|
||||||
|
} else {
|
||||||
|
throw new IOException("Saved block palette entry is not a block state");
|
||||||
}
|
}
|
||||||
decodedBlocks.add(decodeBlock(state));
|
|
||||||
}
|
}
|
||||||
ListTag<?> biomePalette = list(biomes, "palette");
|
ListTag<?> biomePalette = list(biomes, "palette");
|
||||||
List<String> decodedBiomes = new ArrayList<>(biomePalette.size());
|
List<String> decodedBiomes = new ArrayList<>(biomePalette.size());
|
||||||
@@ -181,10 +198,10 @@ final class SavedTerrainChunkReader {
|
|||||||
new Palette<>(decodedBiomes, packedData(biomes), 1, 64));
|
new Palette<>(decodedBiomes, packedData(biomes), 1, 64));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static BoundaryColumnGeometry.Voxel decodeBlock(CompoundTag state) throws IOException {
|
private static BoundaryColumnGeometry.Voxel decodeBlock(CompoundTag state, MCABlockStateCodecSupport.Format format) throws IOException {
|
||||||
String name = string(state, "Name");
|
String name = string(state, format.nameKey());
|
||||||
TreeMap<String, String> properties = new TreeMap<>();
|
TreeMap<String, String> properties = new TreeMap<>();
|
||||||
if (state.get("Properties") instanceof CompoundTag values) {
|
if (state.get(format.propertiesKey()) instanceof CompoundTag values) {
|
||||||
for (Map.Entry<String, Tag<?>> property : values) {
|
for (Map.Entry<String, Tag<?>> property : values) {
|
||||||
if (!(property.getValue() instanceof StringTag value)) {
|
if (!(property.getValue() instanceof StringTag value)) {
|
||||||
throw new IOException("Saved block property is not a string");
|
throw new IOException("Saved block property is not a string");
|
||||||
@@ -192,6 +209,13 @@ final class SavedTerrainChunkReader {
|
|||||||
properties.put(property.getKey(), value.getValue());
|
properties.put(property.getKey(), value.getValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return decodeBlock(name, properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BoundaryColumnGeometry.Voxel decodeBlock(String name, Map<String, String> properties) throws IOException {
|
||||||
|
if (name.isBlank()) {
|
||||||
|
throw new IOException("Saved block state has no resource key");
|
||||||
|
}
|
||||||
String stateKey = stateKey(name, properties);
|
String stateKey = stateKey(name, properties);
|
||||||
if (name.equals("minecraft:air") || name.equals("minecraft:cave_air") || name.equals("minecraft:void_air")) {
|
if (name.equals("minecraft:air") || name.equals("minecraft:cave_air") || name.equals("minecraft:void_air")) {
|
||||||
return name.equals("minecraft:air") ? AIR
|
return name.equals("minecraft:air") ? AIR
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
package art.arcane.iris.world.storage.region;
|
package art.arcane.iris.world.storage.region;
|
||||||
|
|
||||||
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
|
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
|
||||||
|
import art.arcane.volmlib.util.nbt.mca.MCABlockStateCodecSupport;
|
||||||
|
import art.arcane.iris.platform.bukkit.nms.datapack.DataVersion;
|
||||||
|
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.platform.bukkit.nms.INMS;
|
import art.arcane.iris.platform.bukkit.nms.INMS;
|
||||||
@@ -30,6 +32,7 @@ import art.arcane.volmlib.util.nbt.mca.NBTWorldSupport;
|
|||||||
import art.arcane.iris.platform.reflect.KeyedType;
|
import art.arcane.iris.platform.reflect.KeyedType;
|
||||||
import art.arcane.volmlib.util.math.M;
|
import art.arcane.volmlib.util.math.M;
|
||||||
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
|
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
|
||||||
|
import art.arcane.volmlib.util.nbt.tag.Tag;
|
||||||
import art.arcane.iris.generation.concurrent.HyperLock;
|
import art.arcane.iris.generation.concurrent.HyperLock;
|
||||||
import org.bukkit.NamespacedKey;
|
import org.bukkit.NamespacedKey;
|
||||||
import org.bukkit.Registry;
|
import org.bukkit.Registry;
|
||||||
@@ -43,7 +46,7 @@ import java.util.Locale;
|
|||||||
public class NBTWorld {
|
public class NBTWorld {
|
||||||
private static final class Holder {
|
private static final class Holder {
|
||||||
private static final BlockData AIR = BukkitBlockResolution.get("AIR");
|
private static final BlockData AIR = BukkitBlockResolution.get("AIR");
|
||||||
private static final NBTWorldSupport.BlockStateCodec<BlockData> BLOCK_STATE_CODEC = NBTWorldSupport.blockStateCodec(
|
private static final NBTWorldSupport.BlockStateCodec<BlockData> BLOCK_STATE_CODEC = NBTWorldSupport.blockStateCodec(new NBTWorldSupport.BlockStateCodecOptions<>(
|
||||||
blockStateString -> BukkitBlockResolution.getOrNull(blockStateString, true),
|
blockStateString -> BukkitBlockResolution.getOrNull(blockStateString, true),
|
||||||
BukkitBlockResolution::getAir,
|
BukkitBlockResolution::getAir,
|
||||||
blockData -> blockData.getAsString(true),
|
blockData -> blockData.getAsString(true),
|
||||||
@@ -53,8 +56,11 @@ public class NBTWorld {
|
|||||||
return "minecraft:" + blockData.getMaterial().name().toLowerCase(Locale.ROOT);
|
return "minecraft:" + blockData.getMaterial().name().toLowerCase(Locale.ROOT);
|
||||||
}
|
}
|
||||||
return key.getNamespace() + ":" + key.getKey();
|
return key.getNamespace() + ":" + key.getKey();
|
||||||
}
|
},
|
||||||
);
|
INMS.get().getDataVersion() == DataVersion.V26_3
|
||||||
|
? MCABlockStateCodecSupport.Format.LOWERCASE
|
||||||
|
: MCABlockStateCodecSupport.Format.CAPITALIZED
|
||||||
|
));
|
||||||
private static final Map<Biome, Integer> BIOME_IDS = computeBiomeIDs();
|
private static final Map<Biome, Integer> BIOME_IDS = computeBiomeIDs();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +133,7 @@ public class NBTWorld {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static BlockData getBlockData(CompoundTag tag) {
|
public static BlockData getBlockData(Tag<?> tag) {
|
||||||
return Holder.BLOCK_STATE_CODEC.decode(tag);
|
return Holder.BLOCK_STATE_CODEC.decode(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package art.arcane.iris.pack;
|
package art.arcane.iris.pack;
|
||||||
|
|
||||||
|
import art.arcane.iris.platform.bukkit.nms.datapack.DataVersion;
|
||||||
|
|
||||||
import art.arcane.iris.world.lifecycle.BukkitStartupPaths;
|
import art.arcane.iris.world.lifecycle.BukkitStartupPaths;
|
||||||
import art.arcane.iris.world.history.GenerationRegistryContractFactory;
|
import art.arcane.iris.world.history.GenerationRegistryContractFactory;
|
||||||
import art.arcane.iris.generation.biome.IrisBiomeCustom;
|
import art.arcane.iris.generation.biome.IrisBiomeCustom;
|
||||||
@@ -49,6 +51,28 @@ public class DefaultPackBootstrapProvisionerTest {
|
|||||||
assertTrue(DefaultPackBootstrapProvisioner.defaultPacks().isEmpty());
|
assertTrue(DefaultPackBootstrapProvisioner.defaultPacks().isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void bootstrapUsesTheSelectedRuntimeBiomeSchema() throws Exception {
|
||||||
|
Path root = Files.createTempDirectory("iris-bootstrap-runtime");
|
||||||
|
try {
|
||||||
|
Path dataDirectory = root.resolve("plugins/Iris");
|
||||||
|
writePack(dataDirectory.resolve("packs/overworld"), "overworld", "runtime");
|
||||||
|
BukkitStartupPaths startupPaths = BukkitStartupPaths.resolve(root, new String[0]);
|
||||||
|
for (DataVersion version : new DataVersion[]{DataVersion.V26_1_2, DataVersion.V26_2, DataVersion.V26_3}) {
|
||||||
|
DefaultPackBootstrapProvisioner.ProvisionResult result = DefaultPackBootstrapProvisioner.provision(
|
||||||
|
new DefaultPackBootstrapProvisioner.BootstrapRequest(dataDirectory, ignored -> {}, startupPaths, version));
|
||||||
|
Path biomeRoot = result.datapackRoot().resolve("data/iris/worldgen/biome/biomes");
|
||||||
|
try (Stream<Path> paths = Files.list(biomeRoot)) {
|
||||||
|
String json = Files.readString(paths.findFirst().orElseThrow());
|
||||||
|
assertEquals(version != DataVersion.V26_3, json.contains("\"spawners\""));
|
||||||
|
assertEquals(version == DataVersion.V26_3, json.contains("minecraft:gameplay/natural_mob_spawns"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
delete(root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void emptyInstallPublishesValidDatapackWithoutNetworkRequests() throws Exception {
|
public void emptyInstallPublishesValidDatapackWithoutNetworkRequests() throws Exception {
|
||||||
AtomicInteger requests = new AtomicInteger();
|
AtomicInteger requests = new AtomicInteger();
|
||||||
@@ -66,7 +90,8 @@ public class DefaultPackBootstrapProvisionerTest {
|
|||||||
1,
|
1,
|
||||||
Duration.ZERO,
|
Duration.ZERO,
|
||||||
8L * 1024L * 1024L,
|
8L * 1024L * 1024L,
|
||||||
root
|
root,
|
||||||
|
DataVersion.getLatest()
|
||||||
);
|
);
|
||||||
|
|
||||||
DefaultPackBootstrapProvisioner.ProvisionResult result = DefaultPackBootstrapProvisioner.provision(
|
DefaultPackBootstrapProvisioner.ProvisionResult result = DefaultPackBootstrapProvisioner.provision(
|
||||||
@@ -648,8 +673,8 @@ public class DefaultPackBootstrapProvisionerTest {
|
|||||||
BukkitStartupPaths startupPaths = BukkitStartupPaths.resolve(serverRoot, new String[0]);
|
BukkitStartupPaths startupPaths = BukkitStartupPaths.resolve(serverRoot, new String[0]);
|
||||||
|
|
||||||
DefaultPackBootstrapProvisioner.ProvisionResult installed =
|
DefaultPackBootstrapProvisioner.ProvisionResult installed =
|
||||||
DefaultPackBootstrapProvisioner.provision(dataDirectory, ignored -> {
|
DefaultPackBootstrapProvisioner.provision(new DefaultPackBootstrapProvisioner.BootstrapRequest(dataDirectory, ignored -> {
|
||||||
}, startupPaths);
|
}, startupPaths, DataVersion.getLatest()));
|
||||||
|
|
||||||
Path levelStem = installed.datapackRoot().resolve("data/iris/dimension/moon.json");
|
Path levelStem = installed.datapackRoot().resolve("data/iris/dimension/moon.json");
|
||||||
assertEquals(DefaultPackBootstrapProvisioner.ProvisionStatus.INSTALLED, installed.status());
|
assertEquals(DefaultPackBootstrapProvisioner.ProvisionStatus.INSTALLED, installed.status());
|
||||||
@@ -661,8 +686,8 @@ public class DefaultPackBootstrapProvisionerTest {
|
|||||||
StandardCharsets.UTF_8
|
StandardCharsets.UTF_8
|
||||||
);
|
);
|
||||||
DefaultPackBootstrapProvisioner.ProvisionResult updated =
|
DefaultPackBootstrapProvisioner.ProvisionResult updated =
|
||||||
DefaultPackBootstrapProvisioner.provision(dataDirectory, ignored -> {
|
DefaultPackBootstrapProvisioner.provision(new DefaultPackBootstrapProvisioner.BootstrapRequest(dataDirectory, ignored -> {
|
||||||
}, startupPaths);
|
}, startupPaths, DataVersion.getLatest()));
|
||||||
|
|
||||||
assertEquals(DefaultPackBootstrapProvisioner.ProvisionStatus.UPDATED, updated.status());
|
assertEquals(DefaultPackBootstrapProvisioner.ProvisionStatus.UPDATED, updated.status());
|
||||||
assertTrue(Files.readString(levelStem).contains("\"type\": \"iris:underworld\""));
|
assertTrue(Files.readString(levelStem).contains("\"type\": \"iris:underworld\""));
|
||||||
@@ -709,7 +734,8 @@ public class DefaultPackBootstrapProvisionerTest {
|
|||||||
1,
|
1,
|
||||||
Duration.ZERO,
|
Duration.ZERO,
|
||||||
8L * 1024L * 1024L,
|
8L * 1024L * 1024L,
|
||||||
serverRoot
|
serverRoot,
|
||||||
|
DataVersion.getLatest()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -170,14 +170,16 @@ public class IrisDatapackCompilerTest {
|
|||||||
int maxFormat = pack.getInt("max_format");
|
int maxFormat = pack.getInt("max_format");
|
||||||
|
|
||||||
assertEquals(101, minFormat);
|
assertEquals(101, minFormat);
|
||||||
assertEquals(107, maxFormat);
|
assertEquals(121, maxFormat);
|
||||||
assertEquals(maxFormat, pack.getInt("pack_format"));
|
assertEquals(maxFormat, pack.getInt("pack_format"));
|
||||||
|
|
||||||
// One artifact serves both runtimes: each supported DataVersion's format must be in range.
|
// One artifact serves supported runtimes: each supported DataVersion's format must be in range.
|
||||||
assertTrue(minFormat <= DataVersion.V26_1_2.getPackFormat()
|
assertTrue(minFormat <= DataVersion.V26_1_2.getPackFormat()
|
||||||
&& DataVersion.V26_1_2.getPackFormat() <= maxFormat);
|
&& DataVersion.V26_1_2.getPackFormat() <= maxFormat);
|
||||||
assertTrue(minFormat <= DataVersion.V26_2.getPackFormat()
|
assertTrue(minFormat <= DataVersion.V26_2.getPackFormat()
|
||||||
&& DataVersion.V26_2.getPackFormat() <= maxFormat);
|
&& DataVersion.V26_2.getPackFormat() <= maxFormat);
|
||||||
|
assertTrue(minFormat <= DataVersion.V26_3.getPackFormat()
|
||||||
|
&& DataVersion.V26_3.getPackFormat() <= maxFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -496,14 +498,14 @@ public class IrisDatapackCompilerTest {
|
|||||||
Path dataDirectory = server.resolve("plugins/Iris");
|
Path dataDirectory = server.resolve("plugins/Iris");
|
||||||
BukkitStartupPaths startupPaths = BukkitStartupPaths.resolve(server, new String[0]);
|
BukkitStartupPaths startupPaths = BukkitStartupPaths.resolve(server, new String[0]);
|
||||||
DefaultPackBootstrapProvisioner.ProvisionResult installed =
|
DefaultPackBootstrapProvisioner.ProvisionResult installed =
|
||||||
DefaultPackBootstrapProvisioner.provision(dataDirectory, ignored -> {
|
DefaultPackBootstrapProvisioner.provision(new DefaultPackBootstrapProvisioner.BootstrapRequest(dataDirectory, ignored -> {
|
||||||
}, startupPaths);
|
}, startupPaths, DataVersion.getLatest()));
|
||||||
assertEquals(DefaultPackBootstrapProvisioner.ProvisionStatus.INSTALLED, installed.status());
|
assertEquals(DefaultPackBootstrapProvisioner.ProvisionStatus.INSTALLED, installed.status());
|
||||||
assertTrue(Files.isRegularFile(installed.datapackRoot().resolve("data/iris/dimension_type/upper.json")));
|
assertTrue(Files.isRegularFile(installed.datapackRoot().resolve("data/iris/dimension_type/upper.json")));
|
||||||
assertEquals(2, regularFileCount(installed.datapackRoot().resolve("data/iris/worldgen/biome/biomes")));
|
assertEquals(2, regularFileCount(installed.datapackRoot().resolve("data/iris/worldgen/biome/biomes")));
|
||||||
DefaultPackBootstrapProvisioner.ProvisionResult unchanged =
|
DefaultPackBootstrapProvisioner.ProvisionResult unchanged =
|
||||||
DefaultPackBootstrapProvisioner.provision(dataDirectory, ignored -> {
|
DefaultPackBootstrapProvisioner.provision(new DefaultPackBootstrapProvisioner.BootstrapRequest(dataDirectory, ignored -> {
|
||||||
}, startupPaths);
|
}, startupPaths, DataVersion.getLatest()));
|
||||||
assertEquals(DefaultPackBootstrapProvisioner.ProvisionStatus.UNCHANGED, unchanged.status());
|
assertEquals(DefaultPackBootstrapProvisioner.ProvisionStatus.UNCHANGED, unchanged.status());
|
||||||
assertFalse(IrisPlatforms.isBound());
|
assertFalse(IrisPlatforms.isBound());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ import static org.junit.Assert.assertThrows;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class NmsBindingSelectorTest {
|
public class NmsBindingSelectorTest {
|
||||||
|
@Test
|
||||||
|
public void selects263Revision() {
|
||||||
|
assertEquals("v26_3_R1", NmsBindingSelector.select(MinecraftVersion.fromBukkitVersion("26.3-R0.1-SNAPSHOT")));
|
||||||
|
assertEquals("v26_3_R1", NmsBindingSelector.select(MinecraftVersion.fromBukkitVersion("26.3.0-R0.1-SNAPSHOT")));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void selectsOnlyTheExactSupportedRevision() {
|
public void selectsOnlyTheExactSupportedRevision() {
|
||||||
MinecraftVersion version = MinecraftVersion.fromBukkitVersion("26.2-R0.1-SNAPSHOT");
|
MinecraftVersion version = MinecraftVersion.fromBukkitVersion("26.2-R0.1-SNAPSHOT");
|
||||||
@@ -23,12 +29,12 @@ public class NmsBindingSelectorTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void rejectsUnsupportedVersionsWithoutProbingAnotherRevision() {
|
public void rejectsUnsupportedVersionsWithoutProbingAnotherRevision() {
|
||||||
MinecraftVersion version = MinecraftVersion.fromBukkitVersion("26.3-R0.1-SNAPSHOT");
|
MinecraftVersion version = MinecraftVersion.fromBukkitVersion("26.4-R0.1-SNAPSHOT");
|
||||||
|
|
||||||
IllegalStateException failure = assertThrows(IllegalStateException.class,
|
IllegalStateException failure = assertThrows(IllegalStateException.class,
|
||||||
() -> NmsBindingSelector.select(version));
|
() -> NmsBindingSelector.select(version));
|
||||||
|
|
||||||
assertTrue(failure.getMessage().contains("26.3"));
|
assertTrue(failure.getMessage().contains("26.4"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user