From 68b842d5dc8ca6de82834de4a6fc7162a3a531bd Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Fri, 7 Aug 2026 14:04:17 -0600 Subject: [PATCH] First Test MV --- README.md | 6 +- .../iris/core/nms/v26_2_R1/NMSBinding.java | 38 ++++---- ...ucturePostProcessorSurfaceTerrainTest.java | 50 ++++------- .../NativeStructureTemplateOccupancyTest.java | 89 ++++++++++--------- adapters/bukkit/plugin/build.gradle | 2 +- build.gradle | 2 +- .../iris/core/IrisDatapackCompiler.java | 3 +- .../arcane/iris/core/ServerConfigurator.java | 1 - .../art/arcane/iris/core/nms/INMSBinding.java | 2 +- .../iris/core/nms/NmsBindingSelector.java | 6 +- .../iris/core/nms/datapack/DataVersion.java | 12 +++ .../iris/core/nms/v1X/NMSBinding1X.java | 2 +- .../pack/DefaultPackBootstrapProvisioner.java | 3 +- .../iris/engine/object/IrisDimension.java | 16 ++-- .../iris/platform/bukkit/BukkitPlatform.java | 4 - .../common/director/DirectorExecutor.java | 5 ++ .../specialhandlers/StructureHandler.java | 4 +- .../iris/core/IrisDatapackCompilerTest.java | 32 ++++++- .../iris/core/nms/MinecraftVersionTest.java | 24 +++++ .../iris/core/nms/NmsBindingSelectorTest.java | 17 ++++ .../core/nms/datapack/DataVersionTest.java | 32 +++++++ docs/mc-version-bump.md | 11 ++- gradle.properties | 2 + gradle/libs.versions.toml | 2 +- 24 files changed, 237 insertions(+), 128 deletions(-) create mode 100644 core/src/test/java/art/arcane/iris/core/nms/datapack/DataVersionTest.java diff --git a/README.md b/README.md index a8a88f8cf..519a6ed9b 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,9 @@ Canonical English is defined in the typed Java catalogs under `core/src/main/jav | Platform | Artifact | Minecraft | Notes | |---|---|---|---| -| Paper / Purpur / Leaf / Canvas | plugin jar | 26.2 | Full feature set | -| Folia | plugin jar | 26.2 | Region-safe scheduling throughout | -| Spigot / CraftBukkit | plugin jar | 26.2 | Full feature set | +| Paper / Purpur / Leaf / Canvas | plugin jar | 26.1.2 - 26.2 | Full feature set | +| Folia | plugin jar | 26.1.2 - 26.2 | Region-safe scheduling throughout | +| Spigot / CraftBukkit | plugin jar | 26.1.2 - 26.2 | Full feature set | | Fabric | mod jar | 26.2 | Server worldgen + client HUD; requires Fabric Loader 0.19.3+ | | Forge | mod jar | 26.2 | Server worldgen + client HUD; requires Forge 65.0.4+ | | NeoForge | mod jar | 26.2 | Server worldgen + client HUD; requires NeoForge 26.2.0.12-beta+ | diff --git a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NMSBinding.java b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NMSBinding.java index 384debbee..ed16cdbd3 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NMSBinding.java +++ b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NMSBinding.java @@ -6,6 +6,7 @@ import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; import com.mojang.brigadier.exceptions.CommandSyntaxException; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.core.nms.INMSBinding; +import art.arcane.iris.core.nms.MinecraftVersion; import art.arcane.iris.core.nms.container.BiomeColor; import art.arcane.iris.core.nms.container.Pair; import art.arcane.iris.core.nms.container.BlockProperty; @@ -161,6 +162,7 @@ import java.util.concurrent.atomic.AtomicInteger; public class NMSBinding implements INMSBinding { private final KMap baseBiomeCache = new KMap<>(); + private volatile DataVersion dataVersion; private final BlockData AIR = Material.AIR.createBlockData(); private final AtomicCache> biomeMapCache = new AtomicCache<>(); private final AtomicBoolean injected = new AtomicBoolean(); @@ -1238,23 +1240,19 @@ public class NMSBinding implements INMSBinding { } try { - String descriptionId = "entity.minecraft." + entity.name().toLowerCase(Locale.ROOT); - Field[] fields = EntityType.class.getDeclaredFields(); - for (Field field : fields) { - if (!Modifier.isStatic(field.getModifiers()) || !field.getType().equals(EntityType.class)) { - continue; - } - - EntityType entityType = (EntityType) field.get(null); - if (entityType == null) { - continue; - } - - if (descriptionId.equals(entityType.getDescriptionId())) { - return new Vector3d(entityType.getWidth(), entityType.getHeight(), entityType.getWidth()); - } + // Registry lookup instead of an EntityType static-field scan: 26.2 moved the constants + // to a separate EntityTypes holder class that 26.1.2 does not have, while the registry + // resolves identically on both. ENTITY_TYPE is a DefaultedRegistry, so guard containsKey + // to avoid silently resolving unknown keys to the default entry. + Identifier key = Identifier.fromNamespaceAndPath(entity.getKey().getNamespace(), entity.getKey().getKey()); + if (!BuiltInRegistries.ENTITY_TYPE.containsKey(key)) { + return null; } - return null; + EntityType entityType = BuiltInRegistries.ENTITY_TYPE.getValue(key); + if (entityType == null) { + return null; + } + return new Vector3d(entityType.getWidth(), entityType.getHeight(), entityType.getWidth()); } catch (Throwable e) { IrisLogging.error("Unable to get entity dimensions for " + entity + "!"); IrisLogging.reportError(e); @@ -1362,7 +1360,13 @@ public class NMSBinding implements INMSBinding { @Override public DataVersion getDataVersion() { - return DataVersion.V26_2; + DataVersion cached = dataVersion; + if (cached == null) { + MinecraftVersion detected = MinecraftVersion.detect(Bukkit.getServer()); + cached = detected != null && detected.isSameRelease(26, 1, 2) ? DataVersion.V26_1_2 : DataVersion.V26_2; + dataVersion = cached; + } + return cached; } @Override diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorSurfaceTerrainTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorSurfaceTerrainTest.java index 1e6fb632f..2826eab70 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorSurfaceTerrainTest.java +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorSurfaceTerrainTest.java @@ -6,7 +6,6 @@ import art.arcane.iris.engine.object.IrisStructureTerrain; import art.arcane.iris.engine.object.IrisStructureTerrainMode; import com.mojang.datafixers.util.Either; import com.mojang.serialization.Codec; -import com.mojang.serialization.MapCodec; import net.minecraft.SharedConstants; import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; @@ -17,7 +16,6 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.server.Bootstrap; import net.minecraft.util.RandomSource; import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.LevelReader; import net.minecraft.world.level.LevelHeightAccessor; import net.minecraft.world.level.WorldGenLevel; import net.minecraft.world.level.biome.Biome; @@ -46,8 +44,11 @@ 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.templatesystem.AlwaysTrueTest; +import net.minecraft.world.level.levelgen.structure.templatesystem.BlockStateMatchTest; import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings; -import net.minecraft.world.level.levelgen.structure.templatesystem.BlockIgnoreProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.ProcessorRule; +import net.minecraft.world.level.levelgen.structure.templatesystem.RuleProcessor; import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece; import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure; import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings; @@ -267,7 +268,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState()), block(15, 0, 0, Blocks.STONE.defaultBlockState()))); InlineSinglePoolElement element = new InlineSinglePoolElement( - sparseTemplate, List.of(new ReplaceBlockProcessor( + sparseTemplate, List.of(replaceBlockProcessor( Blocks.STONE.defaultBlockState(), Blocks.AIR.defaultBlockState()))); PoolElementStructurePiece piece = rigidTemplatePiece( element, new BoundingBox(0, 64, 0, 15, 70, 3), 1, Rotation.NONE); @@ -369,7 +370,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { block(2, 0, 0, Blocks.AIR.defaultBlockState()), block(2, 2, 0, Blocks.COBBLESTONE.defaultBlockState()))); InlineSinglePoolElement element = new InlineSinglePoolElement( - template, List.of(new ReplaceBlockProcessor( + template, List.of(replaceBlockProcessor( Blocks.STONE.defaultBlockState(), Blocks.AIR.defaultBlockState()))); PoolElementStructurePiece piece = rigidTemplatePiece( element, new BoundingBox(0, 65, 0, 2, 72, 0), 1, Rotation.NONE); @@ -551,7 +552,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { block(0, 0, 0, Blocks.STONE.defaultBlockState()), block(0, 2, 0, Blocks.COBBLESTONE.defaultBlockState()))); PoolElementStructurePiece piece = rigidTemplatePiece( - new InlineSinglePoolElement(template, List.of(new ReplaceBlockProcessor( + new InlineSinglePoolElement(template, List.of(replaceBlockProcessor( Blocks.STONE.defaultBlockState(), Blocks.AIR.defaultBlockState()))), new BoundingBox(0, 66, 0, 7, 72, 7), 1, Rotation.NONE); StructureStart start = rigidSurfaceStart(List.of(piece), TerrainAdjustment.NONE); @@ -1081,10 +1082,10 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { block(1, 0, 0, Blocks.STONE.defaultBlockState()))); InlineSinglePoolElement upperElement = new InlineSinglePoolElement( upperTemplate, List.of( - new ReplaceBlockProcessor( + replaceBlockProcessor( Blocks.COBBLESTONE.defaultBlockState(), Blocks.AIR.defaultBlockState()), - new ReplaceBlockProcessor( + replaceBlockProcessor( Blocks.STONE.defaultBlockState(), Blocks.WATER.defaultBlockState()))); PoolElementStructurePiece lower = rigidTemplatePiece( @@ -2131,32 +2132,13 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { } } - private static final class ReplaceBlockProcessor implements StructureProcessor { - private final BlockState source; - private final BlockState replacement; - - private ReplaceBlockProcessor(BlockState source, BlockState replacement) { - this.source = source; - this.replacement = replacement; - } - - @Override - public StructureTemplate.StructureBlockInfo processBlock( - LevelReader level, BlockPos targetPosition, BlockPos referencePos, - BlockPos templateRelativePos, - StructureTemplate.StructureBlockInfo processedBlockInfo, - StructurePlaceSettings settings) { - if (!processedBlockInfo.state().equals(source)) { - return processedBlockInfo; - } - return new StructureTemplate.StructureBlockInfo( - processedBlockInfo.pos(), replacement, processedBlockInfo.nbt()); - } - - @Override - public MapCodec codec() { - return BlockIgnoreProcessor.STRUCTURE_BLOCK.codec(); - } + private static StructureProcessor replaceBlockProcessor(BlockState source, BlockState replacement) { + // Vanilla RuleProcessor instead of a custom StructureProcessor subtype: StructureProcessor + // is an abstract class on 26.1.2 and an interface on 26.2, so a direct subtype cannot + // compile against both dev bundles. BlockStateMatchTest matches the exact source state, + // matching the previous custom processor's identity-equality semantics. + return new RuleProcessor(List.of(new ProcessorRule( + new BlockStateMatchTest(source), AlwaysTrueTest.INSTANCE, replacement))); } private static StructureStart desertStart() { diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureTemplateOccupancyTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureTemplateOccupancyTest.java index 769c79904..0e8d00137 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureTemplateOccupancyTest.java +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureTemplateOccupancyTest.java @@ -1,15 +1,16 @@ package art.arcane.iris.nativegen; import com.mojang.datafixers.util.Either; -import com.mojang.serialization.MapCodec; 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.LevelReader; import net.minecraft.world.level.WorldGenLevel; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.Rotation; @@ -34,11 +35,13 @@ import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProc 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; @@ -62,38 +65,33 @@ public class NativeStructureTemplateOccupancyTest { } @Test - public void straddlingGravityPieceReadsHeightsOnlyInsideTheProcessingArea() throws Exception { + public void straddlingGravityPieceHeightReadsHonorTheVanillaClipContract() throws Exception { ColumnRecorder recorder = new ColumnRecorder(); resolve(straddlingPiece( List.of(new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, 0))), recorder); - assertEquals(columns(0, 15), recorder.heightColumns); - } - - @Test - public void straddlingPieceRunsProcessorsOnlyInsideTheProcessingArea() throws Exception { - CountingProcessor counter = new CountingProcessor(); - - resolve(straddlingPiece(List.of(counter)), new ColumnRecorder()); - - assertEquals(columns(0, 15), counter.columns); - assertTrue(counter.columns.size() < TEMPLATE_WIDTH); + // 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 { - CountingProcessor counter = new CountingProcessor(); + ColumnRecorder recorder = new ColumnRecorder(); - resolve(straddlingPiece(List.of(counter, - new CappedProcessor(NopProcessor.INSTANCE, ConstantInt.of(0)))), - 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), counter.columns); + assertEquals(columns(0, TEMPLATE_WIDTH - 1), recorder.heightColumns); } @Test - public void cappedProcessorIsTheOnlyProcessorThatDisablesTheProcessingAreaClip() { + 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, @@ -103,12 +101,12 @@ public class NativeStructureTemplateOccupancyTest { new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, 0), new RuleProcessor(List.of()), new BlockAgeProcessor(0.5F), - new ProtectedBlockProcessor(HolderSet.empty()))) { + protectedBlockProcessor())) { assertFalse(processor.getClass().getName(), - processor.evaluatesEntirePieceState()); + (Boolean) contract.invoke(processor)); } - assertTrue(new CappedProcessor(NopProcessor.INSTANCE, ConstantInt.of(4)) - .evaluatesEntirePieceState()); + assertTrue((Boolean) contract.invoke( + new CappedProcessor(NopProcessor.INSTANCE, ConstantInt.of(4)))); } @Test @@ -146,6 +144,30 @@ public class NativeStructureTemplateOccupancyTest { return columns; } + private static Method clipContractMethod() { + try { + return StructureProcessor.class.getMethod("evaluatesEntirePieceState"); + } catch (NoSuchMethodException absent) { + return null; + } + } + + private static Set 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"); } @@ -202,25 +224,6 @@ public class NativeStructureTemplateOccupancyTest { private final Set stateColumns = new TreeSet<>(); } - private static final class CountingProcessor implements StructureProcessor { - private final Set columns = new TreeSet<>(); - - @Override - public StructureTemplate.StructureBlockInfo processBlock( - LevelReader level, BlockPos targetPosition, BlockPos referencePos, - BlockPos templateRelativePos, - StructureTemplate.StructureBlockInfo processedBlockInfo, - StructurePlaceSettings settings) { - columns.add(processedBlockInfo.pos().getX()); - return processedBlockInfo; - } - - @Override - public MapCodec codec() { - return BlockIgnoreProcessor.STRUCTURE_BLOCK.codec(); - } - } - private static final class InlineSinglePoolElement extends SinglePoolElement { private InlineSinglePoolElement( StructureTemplate template, List processors) { diff --git a/adapters/bukkit/plugin/build.gradle b/adapters/bukkit/plugin/build.gradle index 98e3cc3a2..1832545b5 100644 --- a/adapters/bukkit/plugin/build.gradle +++ b/adapters/bukkit/plugin/build.gradle @@ -1,4 +1,4 @@ -String apiVersion = providers.gradleProperty('minecraftVersion').get() +String apiVersion = providers.gradleProperty('apiVersion').get() def mainClass = 'art.arcane.iris.Iris' def bootstrapperClass = 'art.arcane.iris.IrisBootstrap' diff --git a/build.gradle b/build.gradle index e363fa2ad..b5ba32622 100644 --- a/build.gradle +++ b/build.gradle @@ -87,7 +87,7 @@ String forgeConsumerPath = "${consumerLocation}/forge-mod-consumers/dropins/mods String neoForgeConsumerPath = "${consumerLocation}/neoforge-mod-consumers/dropins/mods" def nmsBindings = [ - v26_2_R1: '26.2.build.60-beta', + v26_2_R1: '26.1.2.build.74-stable', ] Class nmsTypeClass = Class.forName('NMSBinding$Type') nmsBindings.each { key, value -> diff --git a/core/src/main/java/art/arcane/iris/core/IrisDatapackCompiler.java b/core/src/main/java/art/arcane/iris/core/IrisDatapackCompiler.java index e9531c0d6..03ac1b90f 100644 --- a/core/src/main/java/art/arcane/iris/core/IrisDatapackCompiler.java +++ b/core/src/main/java/art/arcane/iris/core/IrisDatapackCompiler.java @@ -46,7 +46,6 @@ public final class IrisDatapackCompiler { List packRoots, KList datapackRoots, IDataFixer fixer, - int packFormat, boolean adjustVanillaHeight ) throws IOException { Objects.requireNonNull(packRoots, "packRoots"); @@ -98,7 +97,7 @@ public final class IrisDatapackCompiler { } } - IrisDimension.writeShared(datapackRoots, height, packFormat, adjustVanillaHeight); + IrisDimension.writeShared(datapackRoots, height, adjustVanillaHeight); validateOutputs(datapackRoots, dimensionCount); return new CompilationResult(packCount, dimensionCount, countBiomes(biomes)); } diff --git a/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java b/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java index 798a8102c..aefdcfb6c 100644 --- a/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java +++ b/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java @@ -200,7 +200,6 @@ public class ServerConfigurator { packRoots, stagedRoots, fixer, - BukkitPlatform.dataPackFormat(), IrisSettings.get().getGeneral().adjustVanillaHeight ); for (int i = 0; i < liveRoots.size(); i++) { diff --git a/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java b/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java index 49ba51533..f03ace43f 100644 --- a/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java +++ b/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java @@ -257,7 +257,7 @@ public interface INMSBinding { if (!supportsIrisWorldGeneration()) { throw new IllegalStateException("Iris world '" + c.name() + "' cannot be created with limited NMS binding " + getClass().getSimpleName() - + "; set general.disableNMS=false and use the supported Minecraft 26.2 server runtime"); + + "; set general.disableNMS=false and use a supported Minecraft server runtime (26.1.2 or 26.2)"); } if (missingDimensionTypes(generator.getTarget().getDimension().getDimensionTypeKey())) { throw new IllegalStateException("Missing dimension types to create world"); diff --git a/core/src/main/java/art/arcane/iris/core/nms/NmsBindingSelector.java b/core/src/main/java/art/arcane/iris/core/nms/NmsBindingSelector.java index 6f5f48717..797936ce1 100644 --- a/core/src/main/java/art/arcane/iris/core/nms/NmsBindingSelector.java +++ b/core/src/main/java/art/arcane/iris/core/nms/NmsBindingSelector.java @@ -1,7 +1,7 @@ package art.arcane.iris.core.nms; final class NmsBindingSelector { - private static final String SUPPORTED_VERSION = "26.2"; + private static final String SUPPORTED_VERSIONS = "26.1.2 or 26.2"; private static final String SUPPORTED_TAG = "v26_2_R1"; private NmsBindingSelector() { @@ -11,8 +11,8 @@ final class NmsBindingSelector { if (version == null) { throw new IllegalStateException("Iris requires an exact Minecraft version before selecting NMS"); } - if (!version.isSameRelease(26, 2, 0)) { - throw new IllegalStateException("Iris requires Minecraft " + SUPPORTED_VERSION + if (!version.isSameRelease(26, 2, 0) && !version.isSameRelease(26, 1, 2)) { + throw new IllegalStateException("Iris requires Minecraft " + SUPPORTED_VERSIONS + ". Detected server version: " + version.value()); } return SUPPORTED_TAG; diff --git a/core/src/main/java/art/arcane/iris/core/nms/datapack/DataVersion.java b/core/src/main/java/art/arcane/iris/core/nms/datapack/DataVersion.java index 621268102..cd8a964c7 100644 --- a/core/src/main/java/art/arcane/iris/core/nms/datapack/DataVersion.java +++ b/core/src/main/java/art/arcane/iris/core/nms/datapack/DataVersion.java @@ -13,6 +13,7 @@ import java.util.function.Supplier; @Getter public enum DataVersion { UNSUPPORTED("0.0.0", 0, () -> null), + V26_1_2("26.1.2", 101, DataFixerV1217::new), V26_2("26.2", 107, DataFixerV1217::new); private static final KMap cache = new KMap<>(); @Getter(AccessLevel.NONE) @@ -51,4 +52,15 @@ public enum DataVersion { public static DataVersion getLatest() { return values()[values().length - 1]; } + + public static int minSupportedPackFormat() { + int minimum = Integer.MAX_VALUE; + for (DataVersion version : values()) { + if (version == UNSUPPORTED) { + continue; + } + minimum = Math.min(minimum, version.packFormat); + } + return minimum; + } } diff --git a/core/src/main/java/art/arcane/iris/core/nms/v1X/NMSBinding1X.java b/core/src/main/java/art/arcane/iris/core/nms/v1X/NMSBinding1X.java index 5a1c73db7..af7dc0fad 100644 --- a/core/src/main/java/art/arcane/iris/core/nms/v1X/NMSBinding1X.java +++ b/core/src/main/java/art/arcane/iris/core/nms/v1X/NMSBinding1X.java @@ -293,6 +293,6 @@ public class NMSBinding1X implements INMSBinding { private IllegalStateException unsupportedStructureHook(String operation) { return new IllegalStateException("Iris cannot " + operation + " with limited NMS binding " + getClass().getSimpleName() - + "; set general.disableNMS=false and use the supported Minecraft 26.2 server runtime"); + + "; set general.disableNMS=false and use a supported Minecraft server runtime (26.1.2 or 26.2)"); } } diff --git a/core/src/main/java/art/arcane/iris/core/pack/DefaultPackBootstrapProvisioner.java b/core/src/main/java/art/arcane/iris/core/pack/DefaultPackBootstrapProvisioner.java index 70396e2c0..e2840f7cd 100644 --- a/core/src/main/java/art/arcane/iris/core/pack/DefaultPackBootstrapProvisioner.java +++ b/core/src/main/java/art/arcane/iris/core/pack/DefaultPackBootstrapProvisioner.java @@ -41,7 +41,6 @@ import java.util.zip.ZipInputStream; public final class DefaultPackBootstrapProvisioner { private static final URI DEFAULT_SOURCE = URI.create("https://github.com/IrisDimensions/overworld/releases/download/beta/overworld.zip"); private static final String WORLD_DATAPACK_DIRECTORY = "iris"; - private static final int PACK_FORMAT = 107; private static final int MARKER_SCHEMA = 2; private static final int MAX_ARCHIVE_ENTRIES = 100_000; private static final long MAX_ARCHIVE_BYTES = 512L * 1024L * 1024L; @@ -189,7 +188,7 @@ public final class DefaultPackBootstrapProvisioner { if (fixer == null) { throw new IOException("Latest Iris datapack fixer is unavailable during bootstrap"); } - IrisDatapackCompiler.compile(packRoots, outputFolders, fixer, PACK_FORMAT, false); + IrisDatapackCompiler.compile(packRoots, outputFolders, fixer, false); if (!isDatapackRoot(compileContainer)) { throw new IOException("Canonical Iris datapack compiler produced incomplete output at " + compileContainer); } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java index 4717c3e3b..8ff04f53d 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java @@ -23,6 +23,7 @@ import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.core.nms.datapack.DataVersion; import art.arcane.iris.core.nms.datapack.IDataFixer; import art.arcane.iris.core.nms.datapack.IDataFixer.Dimension; import art.arcane.iris.engine.data.cache.AtomicCache; @@ -763,7 +764,6 @@ public class IrisDimension extends IrisRegistrant { public static void writeShared( KList datapackRoots, DimensionHeight height, - int packFormat, boolean adjustVanillaHeight ) throws IOException { IrisLogging.debug(" Installing Data Pack Vanilla Dimension Types"); @@ -774,16 +774,22 @@ public class IrisDimension extends IrisRegistrant { write(datapackRoot, "the_end", jsonStrings[2], adjustVanillaHeight); } + // Ranged formats spanning every supported runtime (26.1.2 = 101, 26.2 = 107). The bootstrap + // provisioner stages this datapack before the server (and INMS) exists, so the runtime + // version cannot be resolved there; a range keeps the emitted bytes identical on both + // install paths and both Paper versions parse ranged min_format/max_format. + int minFormat = DataVersion.minSupportedPackFormat(); + int maxFormat = DataVersion.getLatest().getPackFormat(); String raw = """ { "pack": { "description": "Iris Data Pack. This pack contains all installed Iris Packs' resources.", - "pack_format": {}, - "min_format": {}, - "max_format": {} + "pack_format": %d, + "min_format": %d, + "max_format": %d } } - """.replace("{}", Integer.toString(packFormat)); + """.formatted(maxFormat, minFormat, maxFormat); for (File datapackRoot : datapackRoots) { File mcm = new File(datapackRoot, "pack.mcmeta"); diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitPlatform.java b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitPlatform.java index b2e5875d3..5f801dc27 100644 --- a/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitPlatform.java +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitPlatform.java @@ -242,10 +242,6 @@ public final class BukkitPlatform implements IrisPlatform { return INMS.get().applyCustomNbt(itemStack, customNbt); } - public static int dataPackFormat() { - return INMS.get().getDataVersion().getPackFormat(); - } - public static java.util.concurrent.CompletableFuture teleportAsync(Entity entity, Location destination) { return io.papermc.lib.PaperLib.teleportAsync(entity, destination); } diff --git a/core/src/main/java/art/arcane/iris/util/common/director/DirectorExecutor.java b/core/src/main/java/art/arcane/iris/util/common/director/DirectorExecutor.java index 6c0c23169..754e3c263 100644 --- a/core/src/main/java/art/arcane/iris/util/common/director/DirectorExecutor.java +++ b/core/src/main/java/art/arcane/iris/util/common/director/DirectorExecutor.java @@ -36,6 +36,11 @@ public interface DirectorExecutor extends DirectorExecutorBase { return sender == null ? null : sender.player(); } + default boolean playerWorldGeneratesStructures() { + Player activePlayer = player(); + return activePlayer != null && activePlayer.getWorld().canGenerateStructures(); + } + default IrisData data() { var access = access(); if (access != null) { diff --git a/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/StructureHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/StructureHandler.java index 80de4fad1..3484f99c3 100644 --- a/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/StructureHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/StructureHandler.java @@ -29,7 +29,6 @@ import art.arcane.iris.spi.PlatformStructureHooks; import art.arcane.iris.util.common.director.DirectorParameterHandler; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.director.exceptions.DirectorParsingException; -import org.bukkit.entity.Player; import java.util.LinkedHashMap; import java.util.List; @@ -100,8 +99,7 @@ public class StructureHandler implements DirectorParameterHandler { } protected boolean nativeStructureGenerationEnabled() { - Player activePlayer = player(); - return activePlayer != null && activePlayer.getWorld().canGenerateStructures(); + return playerWorldGeneratesStructures(); } private static Map distinctKeys(List keys) { diff --git a/core/src/test/java/art/arcane/iris/core/IrisDatapackCompilerTest.java b/core/src/test/java/art/arcane/iris/core/IrisDatapackCompilerTest.java index 20d3423ff..6cf5c92e5 100644 --- a/core/src/test/java/art/arcane/iris/core/IrisDatapackCompilerTest.java +++ b/core/src/test/java/art/arcane/iris/core/IrisDatapackCompilerTest.java @@ -1,7 +1,9 @@ package art.arcane.iris.core; +import art.arcane.iris.core.nms.datapack.DataVersion; import art.arcane.iris.core.nms.datapack.v1217.DataFixerV1217; import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.json.JSONObject; import org.junit.Rule; import org.junit.Test; import org.junit.Assume; @@ -41,7 +43,6 @@ public class IrisDatapackCompilerTest { packRoots, new KList().qadd(datapackRoot.toFile()), new DataFixerV1217(), - 107, false ); @@ -72,7 +73,6 @@ public class IrisDatapackCompilerTest { List.of(packRoot.toFile()), new KList().qadd(datapackRoot.toFile()), new DataFixerV1217(), - 107, false ); @@ -99,6 +99,33 @@ public class IrisDatapackCompilerTest { IrisDatapackCompiler.collectPackRoots(dataDirectory, serverRoot)); } + @Test + public void packMcmetaSpansEverySupportedRuntimeFormat() throws Exception { + Path datapackRoot = temporaryFolder.newFolder("mcmeta-datapack").toPath(); + + IrisDatapackCompiler.compile( + List.of(), + new KList().qadd(datapackRoot.toFile()), + new DataFixerV1217(), + false + ); + + JSONObject pack = new JSONObject(Files.readString(datapackRoot.resolve("pack.mcmeta"), StandardCharsets.UTF_8)) + .getJSONObject("pack"); + int minFormat = pack.getInt("min_format"); + int maxFormat = pack.getInt("max_format"); + + assertEquals(101, minFormat); + assertEquals(107, maxFormat); + assertEquals(maxFormat, pack.getInt("pack_format")); + + // One artifact serves both runtimes: each supported DataVersion's format must be in range. + assertTrue(minFormat <= DataVersion.V26_1_2.getPackFormat() + && DataVersion.V26_1_2.getPackFormat() <= maxFormat); + assertTrue(minFormat <= DataVersion.V26_2.getPackFormat() + && DataVersion.V26_2.getPackFormat() <= maxFormat); + } + @Test public void compilingNoPacksPublishesCleanEmptyDatapack() throws Exception { Path datapackRoot = temporaryFolder.newFolder("empty-datapack").toPath(); @@ -110,7 +137,6 @@ public class IrisDatapackCompilerTest { List.of(), new KList().qadd(datapackRoot.toFile()), new DataFixerV1217(), - 107, false ); diff --git a/core/src/test/java/art/arcane/iris/core/nms/MinecraftVersionTest.java b/core/src/test/java/art/arcane/iris/core/nms/MinecraftVersionTest.java index 0e4184f15..52061a3ef 100644 --- a/core/src/test/java/art/arcane/iris/core/nms/MinecraftVersionTest.java +++ b/core/src/test/java/art/arcane/iris/core/nms/MinecraftVersionTest.java @@ -57,6 +57,30 @@ public class MinecraftVersionTest { assertEquals(0, version.patch()); } + @Test + public void parsesThreePartNewSchemeVersion() { + MinecraftVersion version = MinecraftVersion.fromBukkitVersion("26.1.2-R0.1-SNAPSHOT"); + assertEquals("26.1.2", version.value()); + assertEquals(26, version.major()); + assertEquals(1, version.minor()); + assertEquals(2, version.patch()); + assertTrue(version.isSameRelease(26, 1, 2)); + assertFalse(version.isSameRelease(26, 2, 0)); + } + + @Test + public void detectsThreePartVersionFromDecoratedVersion() { + Server server = mock(Server.class); + doReturn("git-Paper-74 (MC: 26.1.2)").when(server).getVersion(); + doReturn("26.1.2-R0.1-SNAPSHOT").when(server).getBukkitVersion(); + + MinecraftVersion version = MinecraftVersion.detect(server); + assertEquals("26.1.2", version.value()); + assertEquals(26, version.major()); + assertEquals(1, version.minor()); + assertEquals(2, version.patch()); + } + @Test public void comparesMajorBeforeMinor() { MinecraftVersion version = MinecraftVersion.fromBukkitVersion("26.2-R0.1-SNAPSHOT"); diff --git a/core/src/test/java/art/arcane/iris/core/nms/NmsBindingSelectorTest.java b/core/src/test/java/art/arcane/iris/core/nms/NmsBindingSelectorTest.java index ebea5ed2e..adb059e75 100644 --- a/core/src/test/java/art/arcane/iris/core/nms/NmsBindingSelectorTest.java +++ b/core/src/test/java/art/arcane/iris/core/nms/NmsBindingSelectorTest.java @@ -14,6 +14,13 @@ public class NmsBindingSelectorTest { assertEquals("v26_2_R1", NmsBindingSelector.select(version)); } + @Test + public void selectsSharedRevisionFor2612() { + MinecraftVersion version = MinecraftVersion.fromBukkitVersion("26.1.2-R0.1-SNAPSHOT"); + + assertEquals("v26_2_R1", NmsBindingSelector.select(version)); + } + @Test public void rejectsUnsupportedVersionsWithoutProbingAnotherRevision() { MinecraftVersion version = MinecraftVersion.fromBukkitVersion("26.3-R0.1-SNAPSHOT"); @@ -24,6 +31,16 @@ public class NmsBindingSelectorTest { assertTrue(failure.getMessage().contains("26.3")); } + @Test + public void rejectsBasePatchOfSupportedMinorLine() { + MinecraftVersion version = MinecraftVersion.fromBukkitVersion("26.1-R0.1-SNAPSHOT"); + + IllegalStateException failure = assertThrows(IllegalStateException.class, + () -> NmsBindingSelector.select(version)); + + assertTrue(failure.getMessage().contains("26.1")); + } + @Test public void rejectsMissingVersionDetection() { assertThrows(IllegalStateException.class, () -> NmsBindingSelector.select(null)); diff --git a/core/src/test/java/art/arcane/iris/core/nms/datapack/DataVersionTest.java b/core/src/test/java/art/arcane/iris/core/nms/datapack/DataVersionTest.java new file mode 100644 index 000000000..a13c8d373 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/nms/datapack/DataVersionTest.java @@ -0,0 +1,32 @@ +package art.arcane.iris.core.nms.datapack; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; + +public class DataVersionTest { + @Test + public void latestRemainsNewestSupportedRelease() { + assertSame(DataVersion.V26_2, DataVersion.getLatest()); + } + + @Test + public void packFormatsMatchServerVersionJson() { + assertEquals(101, DataVersion.V26_1_2.getPackFormat()); + assertEquals(107, DataVersion.V26_2.getPackFormat()); + } + + @Test + public void minSupportedPackFormatIsTheOldestSupportedRuntime() { + assertEquals(101, DataVersion.minSupportedPackFormat()); + } + + @Test + public void bothSupportedReleasesShareAFixer() { + assertNotNull(DataVersion.V26_1_2.get()); + assertNotNull(DataVersion.V26_2.get()); + assertSame(DataVersion.V26_1_2.get().getClass(), DataVersion.V26_2.get().getClass()); + } +} diff --git a/docs/mc-version-bump.md b/docs/mc-version-bump.md index 794511038..54998e363 100644 --- a/docs/mc-version-bump.md +++ b/docs/mc-version-bump.md @@ -8,9 +8,13 @@ to a new Minecraft version, in order. `gradle.properties`: -- `minecraftVersion` — target MC version (e.g. `26.2`). Drives the Bukkit plugin `api-version`, +- `minecraftVersion` — target MC version (e.g. `26.2`). Drives `BuildConstants.MINECRAFT_VERSION`, the `com.mojang:minecraft` coordinate, all mod-metadata minecraft ranges, and every dist/jar artifact name. +- `apiVersion` — Bukkit plugin `api-version` (e.g. `26.1`). Deliberately decoupled from + `minecraftVersion`: it is the lowest Minecraft release line the single plugin artifact loads on + (currently `26.1` so one jar serves 26.1.2 and 26.2). Bump it only when dropping support for the + older line. - `fabricLoaderVersion` — Fabric Loader version. - `forgeVersion` — Forge version (`-`). - `neoForgeVersion` — NeoForge version. @@ -19,7 +23,8 @@ to a new Minecraft version, in order. ## Ordered steps 1. Edit `gradle.properties`: update `minecraftVersion`, `fabricLoaderVersion`, `forgeVersion`, - `neoForgeVersion`, and the `irisVersion` suffix. + `neoForgeVersion`, and the `irisVersion` suffix. Revisit `apiVersion` only if the bump drops + support for the oldest Minecraft line the plugin artifact still loads on. 2. Edit `gradle/libs.versions.toml`: - `spigot` — the Spigot/Paper API pin used to compile against (`-R0.1-SNAPSHOT`). @@ -148,7 +153,7 @@ to a new Minecraft version, in order. ## Derived automatically (do not hand-edit on a version bump) -- Bukkit plugin `api-version` — `adapters/bukkit/plugin/build.gradle` reads `minecraftVersion`. +- Bukkit plugin `api-version` — `adapters/bukkit/plugin/build.gradle` reads `apiVersion`. - `BuildConstants.MINECRAFT_VERSION` — stamped by the `generateTemplates` task in `core/build.gradle` from `minecraftVersion`; consumed by `Tasks.supportedVersions`. - Mod-metadata `minecraft` version ranges — templated from `minecraftVersion` at `processResources`. diff --git a/gradle.properties b/gradle.properties index 07a54451c..c0854b544 100644 --- a/gradle.properties +++ b/gradle.properties @@ -25,6 +25,8 @@ nmsTools.repo-url=https://repo.codemc.org/repository/nms/ nmsTools.specialSourceVersion=1.11.4 irisVersion=4.0.0-26.2 minecraftVersion=26.2 +# Bukkit plugin api-version: lowest supported Minecraft release line so one artifact loads on 26.1.2 and 26.2 +apiVersion=26.1 fabricLoaderVersion=0.19.3 forgeVersion=26.2-65.0.4 neoForgeVersion=26.2.0.12-beta diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a2ee01bf7..e51ecba29 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ sentryPlugin = "6.14.0" # https://github.com/getsentry/sentry-android-gradle-plu # Core Libraries lombok = "1.18.46" spigot = "26.2-R0.1-SNAPSHOT" # https://hub.spigotmc.org/nexus/repository/snapshots/org/spigotmc/spigot-api/maven-metadata.xml -paper-api = "26.2.build.60-beta" +paper-api = "26.1.2.build.74-stable" log4j = "2.26.1" # https://central.sonatype.com/artifact/org.apache.logging.log4j/log4j-api adventure-api = "4.26.1" # https://github.com/KyoriPowered/adventure adventure-platform = "4.4.1" # https://github.com/KyoriPowered/adventure-platform