From aedef3c38aa0184625cbb51e843ce38e410b0232 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Wed, 14 Oct 2026 21:10:25 -0400 Subject: [PATCH] Limited borrowed types to modded - Pack validation rejects borrowedDimensionType on platforms that generate their own Iris dimension type, with an error naming the supported loaders - Dimension type keys and registry contracts honor borrowedDimensionType only when the platform generation registry supports borrowed types, so Bukkit always derives its generated iris type - The Bukkit runtime contract always expects the generated Iris type; hotload still refuses a borrowedDimensionType change --- .../iris/modded/ModdedGenerationRegistry.java | 5 +++ .../arcane/iris/modded/ModdedPlatform.java | 5 +++ .../generation/terrain/IrisDimension.java | 6 ++- .../terrain/IrisDimensionRuntimeContract.java | 24 ++++++++-- .../iris/pack/PackDimensionValidator.java | 24 +++++++--- .../GenerationRegistryContractFactory.java | 2 +- .../IrisDimensionRuntimeContractTest.java | 20 ++++----- ...ackValidatorBorrowedDimensionTypeTest.java | 16 ++++++- ...GenerationRegistryContractFactoryTest.java | 45 ++++++++++++++++++- .../art/arcane/iris/spi/IrisPlatform.java | 4 ++ .../iris/spi/PlatformGenerationRegistry.java | 4 ++ 11 files changed, 133 insertions(+), 22 deletions(-) diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedGenerationRegistry.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedGenerationRegistry.java index 976e6bdfa..7267487f4 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedGenerationRegistry.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedGenerationRegistry.java @@ -46,6 +46,11 @@ final class ModdedGenerationRegistry implements PlatformGenerationRegistry { ); } + @Override + public boolean supportsBorrowedDimensionTypes() { + return true; + } + @Override public Definition definition(String registryKey, String resourceKey) { return nativeRegistry.definition(registryKey, resourceKey); diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPlatform.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPlatform.java index be4de0d5e..b0359432e 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPlatform.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPlatform.java @@ -80,6 +80,11 @@ public final class ModdedPlatform implements IrisPlatform { return loader.platformName(); } + @Override + public boolean supportsBorrowedDimensionTypes() { + return true; + } + @Override public String minecraftVersion() { return loader.minecraftVersion(); diff --git a/core/src/main/java/art/arcane/iris/generation/terrain/IrisDimension.java b/core/src/main/java/art/arcane/iris/generation/terrain/IrisDimension.java index 040fc1caf..c45f08688 100644 --- a/core/src/main/java/art/arcane/iris/generation/terrain/IrisDimension.java +++ b/core/src/main/java/art/arcane/iris/generation/terrain/IrisDimension.java @@ -1112,8 +1112,12 @@ public class IrisDimension extends IrisRegistrant { return borrowedDimensionType != null && !borrowedDimensionType.isBlank(); } + public boolean borrowsDimensionType(PlatformGenerationRegistry registry) { + return hasBorrowedDimensionType() && registry.supportsBorrowedDimensionTypes(); + } + public String dimensionTypeResourceKey(PlatformGenerationRegistry registry, String packName) { - if (hasBorrowedDimensionType()) { + if (borrowsDimensionType(registry)) { return borrowedDimensionType.trim(); } return registry.dimensionTypeResourceKey(packName, getLoadKey(), getDimensionTypeKey()); diff --git a/core/src/main/java/art/arcane/iris/generation/terrain/IrisDimensionRuntimeContract.java b/core/src/main/java/art/arcane/iris/generation/terrain/IrisDimensionRuntimeContract.java index a3d4818c9..5fc224f56 100644 --- a/core/src/main/java/art/arcane/iris/generation/terrain/IrisDimensionRuntimeContract.java +++ b/core/src/main/java/art/arcane/iris/generation/terrain/IrisDimensionRuntimeContract.java @@ -46,9 +46,7 @@ public record IrisDimensionRuntimeContract( Objects.requireNonNull(dimension, "dimension"); Objects.requireNonNull(namespace, "namespace"); return new IrisDimensionRuntimeContract( - dimension.hasBorrowedDimensionType() - ? dimension.getBorrowedDimensionType().trim() - : namespace + ":" + dimension.getDimensionTypeKey(), + namespace + ":" + dimension.getDimensionTypeKey(), dimension.getMinHeight(), dimension.getMaxHeight() - dimension.getMinHeight(), dimension.getLogicalHeight()); @@ -63,6 +61,7 @@ public record IrisDimensionRuntimeContract( IrisDimensionRuntimeContract actual = expected(active, namespace); IrisDimensionRuntimeContract proposed = expected(replacement, namespace); proposed.requireExact(runtimeName, actual); + requireBorrowedTypeUnchanged(runtimeName, active, replacement); requireGeneratedTypeCompatible(runtimeName, active, replacement); } @@ -120,6 +119,25 @@ public record IrisDimensionRuntimeContract( + ". Generation was refused before any chunk writes. Restart after installing the exact Iris dimension type; terrain clipping is not allowed."); } + private static void requireBorrowedTypeUnchanged( + String runtimeName, + IrisDimension active, + IrisDimension replacement + ) { + String activeBorrowed = borrowedType(active); + String replacementBorrowed = borrowedType(replacement); + if (activeBorrowed.equals(replacementBorrowed)) { + return; + } + throw new IrisDimensionContractException(runtimeName + " cannot hotload a changed borrowedDimensionType. The active pack borrows '" + + activeBorrowed + "' and the replacement borrows '" + replacementBorrowed + + "'. Close and reopen Studio after changing borrowedDimensionType."); + } + + private static String borrowedType(IrisDimension dimension) { + return dimension.hasBorrowedDimensionType() ? dimension.getBorrowedDimensionType().trim() : ""; + } + private static void requireGeneratedTypeCompatible( String runtimeName, IrisDimension active, diff --git a/core/src/main/java/art/arcane/iris/pack/PackDimensionValidator.java b/core/src/main/java/art/arcane/iris/pack/PackDimensionValidator.java index fd0faca69..27de49967 100644 --- a/core/src/main/java/art/arcane/iris/pack/PackDimensionValidator.java +++ b/core/src/main/java/art/arcane/iris/pack/PackDimensionValidator.java @@ -19,6 +19,7 @@ package art.arcane.iris.pack; import art.arcane.iris.generation.terrain.IrisDimensionType; +import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.structure.object.IrisObjectScale; import art.arcane.iris.world.IrisWorldBoundary; import art.arcane.volmlib.util.json.JSONArray; @@ -60,7 +61,7 @@ final class PackDimensionValidator { validateImportedStructurePolicy(dimensionKey, dimJson, blockingErrors, warnings); validateDimensionHeights(packFolder, dimensionKey, dimJson, blockingErrors); - validateBorrowedDimensionType(dimensionKey, dimJson, blockingErrors); + validateBorrowedDimensionType(dimensionKey, dimJson, borrowedDimensionTypesSupported(), blockingErrors); validateSamplingStep(dimensionKey, dimJson, "terrainSamplingStep", 1, blockingErrors); validateSamplingStep(dimensionKey, dimJson, "caveDensitySamplingStep", 1, blockingErrors); validateSamplingStep(dimensionKey, dimJson, "biomeBoundsSamplingStep", 4, blockingErrors); @@ -128,16 +129,29 @@ final class PackDimensionValidator { } } - static void validateBorrowedDimensionType(String dimensionKey, JSONObject dimension, List errors) { + static void validateBorrowedDimensionType(String dimensionKey, JSONObject dimension, boolean borrowingSupported, + List errors) { if (!dimension.has(BORROWED_DIMENSION_TYPE) || dimension.isNull(BORROWED_DIMENSION_TYPE)) { return; } Object value = dimension.opt(BORROWED_DIMENSION_TYPE); - if (value instanceof String key && (key.isEmpty() || RESOURCE_KEY.matcher(key).matches())) { + if (value instanceof String key && key.isEmpty()) { return; } - errors.add("Dimension '" + dimensionKey + "' " + BORROWED_DIMENSION_TYPE - + " must be a namespaced dimension type key such as aether:the_aether, but was '" + value + "'."); + if (!(value instanceof String key) || !RESOURCE_KEY.matcher(key).matches()) { + errors.add("Dimension '" + dimensionKey + "' " + BORROWED_DIMENSION_TYPE + + " must be a namespaced dimension type key such as aether:the_aether, but was '" + value + "'."); + return; + } + if (!borrowingSupported) { + errors.add("Dimension '" + dimensionKey + "' sets " + BORROWED_DIMENSION_TYPE + " to '" + key + + "', which only Fabric, Forge, and NeoForge servers support. This server generates its own Iris dimension type, so remove " + + BORROWED_DIMENSION_TYPE + " to use this pack here."); + } + } + + private static boolean borrowedDimensionTypesSupported() { + return !IrisPlatforms.isBound() || IrisPlatforms.get().supportsBorrowedDimensionTypes(); } static void validateObjectScaleFactor(String dimensionKey, JSONObject dimension, List errors) { diff --git a/core/src/main/java/art/arcane/iris/world/history/GenerationRegistryContractFactory.java b/core/src/main/java/art/arcane/iris/world/history/GenerationRegistryContractFactory.java index fff744dbf..d19ae13d2 100644 --- a/core/src/main/java/art/arcane/iris/world/history/GenerationRegistryContractFactory.java +++ b/core/src/main/java/art/arcane/iris/world/history/GenerationRegistryContractFactory.java @@ -175,7 +175,7 @@ public final class GenerationRegistryContractFactory { List reachableBiomes = new ArrayList<>(requiredDimension.getReachableBiomes(() -> requiredData)); for (IrisDimension registryDimension : registryDimensions(requiredData, requiredDimension)) { String dimensionKey = requireText(registryDimension.getLoadKey(), "Dimension load key"); - if (!registryDimension.hasBorrowedDimensionType()) { + if (!registryDimension.borrowsDimensionType(requiredGenerationRegistry)) { addGeneratedDimensionType( definitions, generatedSources, diff --git a/core/src/test/java/art/arcane/iris/generation/terrain/IrisDimensionRuntimeContractTest.java b/core/src/test/java/art/arcane/iris/generation/terrain/IrisDimensionRuntimeContractTest.java index ef5fc5772..7439a122f 100644 --- a/core/src/test/java/art/arcane/iris/generation/terrain/IrisDimensionRuntimeContractTest.java +++ b/core/src/test/java/art/arcane/iris/generation/terrain/IrisDimensionRuntimeContractTest.java @@ -139,16 +139,15 @@ public class IrisDimensionRuntimeContractTest { } @Test - public void expectedContractUsesTheBorrowedTypeKey() { + public void expectedContractIsTheGeneratedIrisTypeEvenWhenThePackBorrowsOne() { IrisDimension dimension = borrowedDimension("aether:the_aether", 0, 256); - assertEquals("aether:the_aether", IrisDimensionRuntimeContract.expected(dimension, "iris").typeKey()); + assertEquals("iris:skylands", IrisDimensionRuntimeContract.expected(dimension, "iris").typeKey()); } @Test public void borrowedTypeAcceptsAPackRangeInsideTheBorrowedRange() { - IrisDimensionRuntimeContract expected = IrisDimensionRuntimeContract.expected( - borrowedDimension("aether:the_aether", 0, 192), "iris"); + IrisDimensionRuntimeContract expected = borrowedContract("aether:the_aether", 0, 192); expected.requireBorrowedFit("Modded level 'aether:the_aether'", new IrisDimensionRuntimeContract("aether:the_aether", 0, 256, 256)); @@ -158,8 +157,7 @@ public class IrisDimensionRuntimeContractTest { @Test public void borrowedTypeRejectsAPackTopAboveTheBorrowedTop() { - IrisDimensionRuntimeContract expected = IrisDimensionRuntimeContract.expected( - borrowedDimension("aether:the_aether", 0, 320), "iris"); + IrisDimensionRuntimeContract expected = borrowedContract("aether:the_aether", 0, 320); assertBorrowedFitRejected(expected, new IrisDimensionRuntimeContract("aether:the_aether", 0, 256, 256), "0..320", "0..256"); @@ -167,8 +165,7 @@ public class IrisDimensionRuntimeContractTest { @Test public void borrowedTypeRejectsAPackMinimumThatDiffersFromTheBorrowedMinimum() { - IrisDimensionRuntimeContract expected = IrisDimensionRuntimeContract.expected( - borrowedDimension("aether:the_aether", 16, 256), "iris"); + IrisDimensionRuntimeContract expected = borrowedContract("aether:the_aether", 16, 256); assertBorrowedFitRejected(expected, new IrisDimensionRuntimeContract("aether:the_aether", 0, 256, 256), "16..256", "0..256"); @@ -176,8 +173,7 @@ public class IrisDimensionRuntimeContractTest { @Test public void borrowedTypeRejectsALevelUsingAnotherType() { - IrisDimensionRuntimeContract expected = IrisDimensionRuntimeContract.expected( - borrowedDimension("aether:the_aether", 0, 256), "iris"); + IrisDimensionRuntimeContract expected = borrowedContract("aether:the_aether", 0, 256); assertBorrowedFitRejected(expected, new IrisDimensionRuntimeContract("minecraft:overworld", 0, 256, 256), "aether:the_aether", "minecraft:overworld"); @@ -192,6 +188,10 @@ public class IrisDimensionRuntimeContractTest { assertHotloadRejected(active, replacement, "aether:the_aether"); } + private static IrisDimensionRuntimeContract borrowedContract(String typeKey, int minHeight, int maxHeight) { + return new IrisDimensionRuntimeContract(typeKey, minHeight, maxHeight - minHeight, maxHeight - minHeight); + } + private IrisDimension borrowedDimension(String typeKey, int minHeight, int maxHeight) { IrisDimension dimension = new IrisDimension(); dimension.setLoadKey("skylands"); diff --git a/core/src/test/java/art/arcane/iris/pack/PackValidatorBorrowedDimensionTypeTest.java b/core/src/test/java/art/arcane/iris/pack/PackValidatorBorrowedDimensionTypeTest.java index 4da05d4db..e8cc475a3 100644 --- a/core/src/test/java/art/arcane/iris/pack/PackValidatorBorrowedDimensionTypeTest.java +++ b/core/src/test/java/art/arcane/iris/pack/PackValidatorBorrowedDimensionTypeTest.java @@ -46,9 +46,23 @@ public class PackValidatorBorrowedDimensionTypeTest { validate("{\"borrowedDimensionType\":42}")); } + @Test + public void rejectsBorrowedTypesOnPlatformsThatGenerateTheirOwnType() { + assertTrue(validate("{\"borrowedDimensionType\":\"\"}", false).isEmpty()); + List errors = validate("{\"borrowedDimensionType\":\"aether:the_aether\"}", false); + + assertEquals(1, errors.size()); + assertTrue(errors.getFirst(), errors.getFirst().contains("borrowedDimensionType")); + assertTrue(errors.getFirst(), errors.getFirst().contains("Fabric, Forge, and NeoForge")); + } + private static List validate(String json) { + return validate(json, true); + } + + private static List validate(String json, boolean borrowingSupported) { List errors = new ArrayList<>(); - PackDimensionValidator.validateBorrowedDimensionType("main", new JSONObject(json), errors); + PackDimensionValidator.validateBorrowedDimensionType("main", new JSONObject(json), borrowingSupported, errors); return errors; } } diff --git a/core/src/test/java/art/arcane/iris/world/history/GenerationRegistryContractFactoryTest.java b/core/src/test/java/art/arcane/iris/world/history/GenerationRegistryContractFactoryTest.java index 60700f117..8d24658c1 100644 --- a/core/src/test/java/art/arcane/iris/world/history/GenerationRegistryContractFactoryTest.java +++ b/core/src/test/java/art/arcane/iris/world/history/GenerationRegistryContractFactoryTest.java @@ -167,7 +167,7 @@ public class GenerationRegistryContractFactoryTest { PlatformRegistries registries = mock(PlatformRegistries.class); when(registries.blockTypeKeys()).thenReturn(new KList<>()); when(registries.entityKeys()).thenReturn(new KList<>()); - FakeGenerationRegistry generationRegistry = new FakeGenerationRegistry("test-runtime"); + FakeGenerationRegistry generationRegistry = new BorrowingGenerationRegistry("test-runtime"); GenerationRegistryContract contract = GenerationRegistryContractFactory.create( data, @@ -187,6 +187,38 @@ public class GenerationRegistryContractFactoryTest { assertEquals("aether:the_aether", dimension.dimensionTypeResourceKey(generationRegistry, "borrowed-pack")); } + @Test + public void platformWithoutBorrowedTypesKeepsTheGeneratedDimensionType() throws Exception { + File pack = temporary.newFolder("borrowed-pack-generated"); + IrisData data = mock(IrisData.class); + when(data.getDataFolder()).thenReturn(pack); + when(data.getContentGate()).thenReturn(mock(ContentGate.class)); + IrisDimension dimension = spy(new IrisDimension().setBorrowedDimensionType("aether:the_aether")); + dimension.setLoadKey("skylands"); + stubDimensions(data, dimension); + doReturn(new KList<>()).when(dimension).getReachableBiomes(any()); + doReturn(new KList<>()).when(dimension).getAllRegions(any()); + PlatformRegistries registries = mock(PlatformRegistries.class); + when(registries.blockTypeKeys()).thenReturn(new KList<>()); + when(registries.entityKeys()).thenReturn(new KList<>()); + FakeGenerationRegistry generationRegistry = new FakeGenerationRegistry("test-runtime"); + + GenerationRegistryContract contract = GenerationRegistryContractFactory.create( + data, + dimension, + GenerationPackFingerprint.compute(pack.toPath(), GenerationPackFingerprint.CURRENT_VERSION), + DataVersion.getLatest().get(), + registries, + generationRegistry + ); + + assertFalse(contract.definitions().containsKey(key( + GenerationRegistryContractFactory.DIMENSION_TYPE_REGISTRY, "aether:the_aether"))); + assertTrue(contract.definitions().containsKey(key( + GenerationRegistryContractFactory.DIMENSION_TYPE_REGISTRY, "test:skylands_type"))); + assertEquals("test:skylands_type", dimension.dimensionTypeResourceKey(generationRegistry, "borrowed-pack-generated")); + } + @Test public void duplicateLogicalBiomeIdsKeepExactDefinitionsAndChooseStableLegacyAlias() throws Exception { File pack = temporary.newFolder("duplicate-biomes"); @@ -956,6 +988,17 @@ public class GenerationRegistryContractFactoryTest { } } + private static final class BorrowingGenerationRegistry extends FakeGenerationRegistry { + private BorrowingGenerationRegistry(String runtimeIdentity) { + super(runtimeIdentity); + } + + @Override + public boolean supportsBorrowedDimensionTypes() { + return true; + } + } + private static class FakeGenerationRegistry implements PlatformGenerationRegistry { private final String runtimeIdentity; private final Map definitions = diff --git a/spi/src/main/java/art/arcane/iris/spi/IrisPlatform.java b/spi/src/main/java/art/arcane/iris/spi/IrisPlatform.java index 1fa643441..f0abe7cf9 100644 --- a/spi/src/main/java/art/arcane/iris/spi/IrisPlatform.java +++ b/spi/src/main/java/art/arcane/iris/spi/IrisPlatform.java @@ -76,6 +76,10 @@ public interface IrisPlatform { return false; } + default boolean supportsBorrowedDimensionTypes() { + return false; + } + /** * Root folder Iris owns for packs, settings and generated data. Created if missing. Never null. */ diff --git a/spi/src/main/java/art/arcane/iris/spi/PlatformGenerationRegistry.java b/spi/src/main/java/art/arcane/iris/spi/PlatformGenerationRegistry.java index 456c10859..a8d64c8c7 100644 --- a/spi/src/main/java/art/arcane/iris/spi/PlatformGenerationRegistry.java +++ b/spi/src/main/java/art/arcane/iris/spi/PlatformGenerationRegistry.java @@ -25,6 +25,10 @@ public interface PlatformGenerationRegistry extends NativeGenerationRegistry { String dimensionTypeResourceKey(String packName, String dimensionKey, String dimensionTypeKey); + default boolean supportsBorrowedDimensionTypes() { + return false; + } + static String contentAddressedCustomBiomeResourceKey(String identitySha256) { String requiredFingerprint = Objects.requireNonNull(identitySha256, "identitySha256"); if (!SHA_256_PATTERN.matcher(requiredFingerprint).matches()) {