This commit is contained in:
Brian Neumann-Fopiano
2026-07-18 18:12:04 -04:00
parent 0193f75daa
commit ad41b6795c
48 changed files with 1193 additions and 141 deletions
+4
View File
@@ -29,3 +29,7 @@ jobs:
run: ./gradlew :core:check :adapters:bukkit:plugin:test :spi:build :probe:test :probe:run :probe:deserializationProbe -PuseLocalVolmLib=false --console=plain --stacktrace run: ./gradlew :core:check :adapters:bukkit:plugin:test :spi:build :probe:test :probe:run :probe:deserializationProbe -PuseLocalVolmLib=false --console=plain --stacktrace
- name: Run modded shared tests - name: Run modded shared tests
run: ./gradlew -p adapters/fabric test -PuseLocalVolmLib=false --console=plain --stacktrace run: ./gradlew -p adapters/fabric test -PuseLocalVolmLib=false --console=plain --stacktrace
- name: Test modded artifact verifier
run: ./gradlew -p buildSrc test --console=plain --stacktrace
- name: Verify modded artifacts
run: ./gradlew verifyModdedArtifacts --no-parallel -PuseLocalVolmLib=false --console=plain --stacktrace
@@ -0,0 +1,176 @@
package art.arcane.iris.nativegen;
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 desertPyramidUsesTheHighestFootprintSurfaceAndLocksVanillaReanchor() {
TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(11L), 0, 0);
StructureStart start = desertStart(piece);
BoundingBox cachedBounds = start.getBoundingBox();
BoundingBox footprint = piece.getBoundingBox();
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false,
(x, z) -> x == footprint.maxX() && z == footprint.maxZ() ? 92 : 64);
assertEquals(29, offset);
assertSame(cachedBounds, start.getBoundingBox());
assertEquals(93, piece.getBoundingBox().minY());
assertEquals(93, start.getBoundingBox().minY());
assertTrue(piece.attemptVanillaAlignment(-2));
assertEquals(93, piece.getBoundingBox().minY());
}
@Test
public void jungleTempleHonorsConfiguredShiftAndLocksVanillaReanchor() {
TestJungleTemplePiece piece = new TestJungleTemplePiece(RandomSource.create(19L), 32, -16);
StructureStart start = jungleStart(piece);
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
start, "minecraft:jungle_pyramid", 4, 63, -64, 320, false,
(x, z) -> x == 32 && z == -16 ? 88 : 70);
assertEquals(29, offset);
assertEquals(93, piece.getBoundingBox().minY());
assertTrue(piece.attemptVanillaAlignment());
assertEquals(93, piece.getBoundingBox().minY());
}
@Test
public void repeatedAlignmentIsIdempotent() {
TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(23L), 0, 0);
StructureStart start = desertStart(piece);
int initialOffset = NativeStructurePostProcessor.applyVerticalPlacement(
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, (x, z) -> 80);
int repeatedOffset = NativeStructurePostProcessor.applyVerticalPlacement(
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, (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 = NativeStructurePostProcessor.applyVerticalPlacement(
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, (x, z) -> 318);
assertEquals(241, offset);
assertEquals(305, piece.getBoundingBox().minY());
assertEquals(319, start.getBoundingBox().maxY());
}
@Test
public void onlyPyramidsAndTemplesOptIntoScatteredSurfaceFitting() {
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);
assertTrue(NativeStructurePostProcessor.requiresSurfaceTerrain(desertStart));
assertTrue(NativeStructurePostProcessor.requiresSurfaceTerrain(jungleStart));
assertFalse(NativeStructurePostProcessor.requiresSurfaceTerrain(swampStart));
AtomicInteger terrainQueries = new AtomicInteger();
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
swampStart, "minecraft:swamp_hut", 0, 63, -64, 320, false,
(x, z) -> terrainQueries.incrementAndGet());
assertEquals(0, offset);
assertEquals(0, terrainQueries.get());
assertEquals(64, swamp.getBoundingBox().minY());
}
@Test
public void scatteredHeightFieldMatchesTheRuntimeContract() {
Field field = NativeStructurePostProcessor.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());
}
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);
}
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ def mainClass = 'art.arcane.iris.Iris'
def bootstrapperClass = 'art.arcane.iris.IrisBootstrap' def bootstrapperClass = 'art.arcane.iris.IrisBootstrap'
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate') String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
.orElse('com.github.VolmitSoftware:VolmLib:4c38988b344792a79f925f57f8a675bc85fc1bed') .orElse('com.github.VolmitSoftware:VolmLib:d9026a7c8ebc391c8109f401ce79a0ce65df3969')
.get() .get()
dependencies { dependencies {
+4 -5
View File
@@ -31,7 +31,7 @@ file('../../gradle.properties').withInputStream { InputStream stream -> rootProp
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.2')) String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.2'))
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2')) String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2'))
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(rootProperties.getProperty('fabricLoaderVersion', '0.19.3')) String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(rootProperties.getProperty('fabricLoaderVersion', '0.19.3'))
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:4c38988b344792a79f925f57f8a675bc85fc1bed')) String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:d9026a7c8ebc391c8109f401ce79a0ce65df3969'))
Closure<String> irisArtifactName = { String platform, String targetVersion -> Closure<String> irisArtifactName = { String platform, String targetVersion ->
return "Iris v${project.version} [${platform}] ${targetVersion}.jar" return "Iris v${project.version} [${platform}] ${targetVersion}.jar"
} }
@@ -204,6 +204,9 @@ processResources {
tasks.named('shadowJar', ShadowJar).configure { tasks.named('shadowJar', ShadowJar).configure {
doFirst { doFirst {
delete(fileTree(layout.buildDirectory.dir('libs')) {
include('Iris v* [Fabric] *.jar')
})
delete(layout.buildDirectory.file("libs/Iris-${project.version}+mc${minecraftVersion}-fabric.jar")) delete(layout.buildDirectory.file("libs/Iris-${project.version}+mc${minecraftVersion}-fabric.jar"))
} }
archiveFileName.set(irisArtifactName('Fabric', "${minecraftVersion}+${fabricLoaderVersion}")) archiveFileName.set(irisArtifactName('Fabric', "${minecraftVersion}+${fabricLoaderVersion}"))
@@ -217,10 +220,6 @@ tasks.named('shadowJar', ShadowJar).configure {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE duplicatesStrategy = DuplicatesStrategy.EXCLUDE
relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm') relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm')
relocate('io.sentry', 'art.arcane.iris.shadow.sentry') relocate('io.sentry', 'art.arcane.iris.shadow.sentry')
relocate('com.sun.jna', 'art.arcane.iris.shadow.jna')
relocate('oshi', 'art.arcane.iris.shadow.oshi')
relocate('net.jpountz', 'art.arcane.iris.shadow.jpountz')
exclude('oshi.properties')
from(project.configurations.named('jij')) { from(project.configurations.named('jij')) {
into('META-INF/jars') into('META-INF/jars')
rename { String fileName -> fileName.replaceAll(/-[0-9][^-]*\.jar$/, '.jar') } rename { String fileName -> fileName.replaceAll(/-[0-9][^-]*\.jar$/, '.jar') }
Binary file not shown.
+11 -11
View File
@@ -1,5 +1,5 @@
[05:44:33] [Test worker/INFO]: Iris registered custom content provider 'iris_deferred_test' [17:42:11] [Test worker/INFO]: Iris registered custom content provider 'iris_deferred_test'
[05:44:33] [Test worker/ERROR]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService [17:42:11] [Test worker/ERROR]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: cleanup failed java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
@@ -44,7 +44,7 @@ java.lang.RuntimeException: cleanup failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[05:44:33] [Test worker/ERROR]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService [17:42:11] [Test worker/ERROR]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: enable failed java.lang.RuntimeException: enable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31) at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
@@ -92,10 +92,10 @@ java.lang.RuntimeException: enable failed
Suppressed: java.lang.RuntimeException: cleanup failed Suppressed: java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32)
... 42 more ... 42 more
[05:44:33] [Test worker/ERROR]: [worldcheck] server stop request failed [17:42:11] [Test worker/ERROR]: [worldcheck] server stop request failed
java.lang.IllegalStateException: stop request failed java.lang.IllegalStateException: stop request failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:235) at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:235)
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:176) at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:179)
at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:232) at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:232)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565) at java.base/java.lang.reflect.Method.invoke(Method.java:565)
@@ -139,10 +139,10 @@ java.lang.IllegalStateException: stop request failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[05:44:33] [Test worker/ERROR]: [worldcheck] waiting for server shutdown failed [17:42:11] [Test worker/ERROR]: [worldcheck] waiting for server shutdown failed
java.lang.IllegalStateException: shutdown wait failed java.lang.IllegalStateException: shutdown wait failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:261) at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:261)
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:191) at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:194)
at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:259) at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:259)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565) at java.base/java.lang.reflect.Method.invoke(Method.java:565)
@@ -186,10 +186,10 @@ java.lang.IllegalStateException: shutdown wait failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[05:44:33] [Test worker/ERROR]: [worldcheck] check failed [17:42:11] [Test worker/ERROR]: [worldcheck] check failed
java.lang.IllegalStateException: check failed java.lang.IllegalStateException: check failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:221) at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:221)
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:171) at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:174)
at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:219) at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:219)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565) at java.base/java.lang.reflect.Method.invoke(Method.java:565)
@@ -233,8 +233,8 @@ java.lang.IllegalStateException: check failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[05:44:33] [Test worker/INFO]: Iris registered custom content provider 'iris_discovery_success' [17:42:11] [Test worker/INFO]: Iris registered custom content provider 'iris_discovery_success'
[05:44:33] [Test worker/ERROR]: Iris custom content provider discovery failed [17:42:11] [Test worker/ERROR]: Iris custom content provider discovery failed
java.lang.RuntimeException: provider init failed java.lang.RuntimeException: provider init failed
at art.arcane.iris.modded.api.ModdedCustomContentRegistryTest.failedDiscoveryPublishesNothingAndPreservesTheCause(ModdedCustomContentRegistryTest.java:39) at art.arcane.iris.modded.api.ModdedCustomContentRegistryTest.failedDiscoveryPublishesNothingAndPreservesTheCause(ModdedCustomContentRegistryTest.java:39)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
@@ -53,6 +53,10 @@ public final class FabricModdedLoader implements ModdedLoader {
return instance instanceof MinecraftServer server ? server : null; return instance instanceof MinecraftServer server ? server : null;
} }
@Override
public void invalidateLevelCache(MinecraftServer server) {
}
@Override @Override
public boolean clientEnvironment() { public boolean clientEnvironment() {
return FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT; return FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT;
+4 -5
View File
@@ -31,7 +31,7 @@ file('../../gradle.properties').withInputStream { InputStream stream -> rootProp
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.2')) String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.2'))
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2')) String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2'))
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(rootProperties.getProperty('forgeVersion', '26.2-65.0.4')) String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(rootProperties.getProperty('forgeVersion', '26.2-65.0.4'))
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:4c38988b344792a79f925f57f8a675bc85fc1bed')) String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:d9026a7c8ebc391c8109f401ce79a0ce65df3969'))
Closure<String> loaderDisplayVersion = { String loaderVersion -> Closure<String> loaderDisplayVersion = { String loaderVersion ->
String coordinatePrefix = "${minecraftVersion}-" String coordinatePrefix = "${minecraftVersion}-"
if (loaderVersion.startsWith(coordinatePrefix)) { if (loaderVersion.startsWith(coordinatePrefix)) {
@@ -217,6 +217,9 @@ processResources {
tasks.named('shadowJar', ShadowJar).configure { tasks.named('shadowJar', ShadowJar).configure {
doFirst { doFirst {
delete(fileTree(layout.buildDirectory.dir('libs')) {
include('Iris v* [Forge] *.jar')
})
delete(layout.buildDirectory.file("libs/Iris-${project.version}+mc${minecraftVersion}-forge.jar")) delete(layout.buildDirectory.file("libs/Iris-${project.version}+mc${minecraftVersion}-forge.jar"))
} }
manifest { manifest {
@@ -235,10 +238,6 @@ tasks.named('shadowJar', ShadowJar).configure {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE duplicatesStrategy = DuplicatesStrategy.EXCLUDE
relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm') relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm')
relocate('io.sentry', 'art.arcane.iris.shadow.sentry') relocate('io.sentry', 'art.arcane.iris.shadow.sentry')
relocate('com.sun.jna', 'art.arcane.iris.shadow.jna')
relocate('oshi', 'art.arcane.iris.shadow.oshi')
relocate('net.jpountz', 'art.arcane.iris.shadow.jpountz')
exclude('oshi.properties')
exclude('org/jspecify/**') exclude('org/jspecify/**')
exclude('com/google/errorprone/**') exclude('com/google/errorprone/**')
exclude('com/google/j2objc/**') exclude('com/google/j2objc/**')
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+14 -14
View File
@@ -1,8 +1,8 @@
[16Jul2026 05:44:24.794] [Test worker/DEBUG] [io.netty.util.internal.logging.InternalLoggerFactory/]: Using SLF4J as the default logging framework [18Jul2026 17:42:16.031] [Test worker/DEBUG] [io.netty.util.internal.logging.InternalLoggerFactory/]: Using SLF4J as the default logging framework
[16Jul2026 05:44:24.795] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.level: simple [18Jul2026 17:42:16.033] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.level: simple
[16Jul2026 05:44:24.795] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.targetRecords: 4 [18Jul2026 17:42:16.033] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.targetRecords: 4
[16Jul2026 05:44:26.716] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test' [18Jul2026 17:42:18.043] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test'
[16Jul2026 05:44:26.771] [Test worker/ERROR] [Iris/]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService [18Jul2026 17:42:18.104] [Test worker/ERROR] [Iris/]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: cleanup failed java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?] at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
@@ -47,7 +47,7 @@ java.lang.RuntimeException: cleanup failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?] at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[16Jul2026 05:44:26.779] [Test worker/ERROR] [Iris/]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService [18Jul2026 17:42:18.110] [Test worker/ERROR] [Iris/]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: enable failed java.lang.RuntimeException: enable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31) ~[test/:?] at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
@@ -95,10 +95,10 @@ java.lang.RuntimeException: enable failed
Suppressed: java.lang.RuntimeException: cleanup failed Suppressed: java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?] at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?]
... 42 more ... 42 more
[16Jul2026 05:44:26.814] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed [18Jul2026 17:42:18.144] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed
java.lang.IllegalStateException: stop request failed java.lang.IllegalStateException: stop request failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:235) ~[test/:?] at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:235) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:176) ~[main/:?] at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:179) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:232) ~[test/:?] at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:232) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?] at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
@@ -142,10 +142,10 @@ java.lang.IllegalStateException: stop request failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?] at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[16Jul2026 05:44:26.821] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed [18Jul2026 17:42:18.147] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed
java.lang.IllegalStateException: shutdown wait failed java.lang.IllegalStateException: shutdown wait failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:261) ~[test/:?] at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:261) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:191) ~[main/:?] at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:194) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:259) ~[test/:?] at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:259) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?] at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
@@ -189,10 +189,10 @@ java.lang.IllegalStateException: shutdown wait failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?] at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[16Jul2026 05:44:26.826] [Test worker/ERROR] [Iris/]: [worldcheck] check failed [18Jul2026 17:42:18.154] [Test worker/ERROR] [Iris/]: [worldcheck] check failed
java.lang.IllegalStateException: check failed java.lang.IllegalStateException: check failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:221) ~[test/:?] at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:221) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:171) ~[main/:?] at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:174) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:219) ~[test/:?] at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:219) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?] at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
@@ -236,8 +236,8 @@ java.lang.IllegalStateException: check failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?] at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[16Jul2026 05:44:26.832] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_discovery_success' [18Jul2026 17:42:18.160] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_discovery_success'
[16Jul2026 05:44:26.833] [Test worker/ERROR] [Iris/]: Iris custom content provider discovery failed [18Jul2026 17:42:18.161] [Test worker/ERROR] [Iris/]: Iris custom content provider discovery failed
java.lang.RuntimeException: provider init failed java.lang.RuntimeException: provider init failed
at art.arcane.iris.modded.api.ModdedCustomContentRegistryTest.failedDiscoveryPublishesNothingAndPreservesTheCause(ModdedCustomContentRegistryTest.java:39) ~[test/:?] at art.arcane.iris.modded.api.ModdedCustomContentRegistryTest.failedDiscoveryPublishesNothingAndPreservesTheCause(ModdedCustomContentRegistryTest.java:39) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
+11 -11
View File
@@ -1,5 +1,5 @@
[16Jul2026 05:44:26.716] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test' [18Jul2026 17:42:18.043] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test'
[16Jul2026 05:44:26.771] [Test worker/ERROR] [Iris/]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService [18Jul2026 17:42:18.104] [Test worker/ERROR] [Iris/]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: cleanup failed java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?] at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
@@ -44,7 +44,7 @@ java.lang.RuntimeException: cleanup failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?] at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[16Jul2026 05:44:26.779] [Test worker/ERROR] [Iris/]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService [18Jul2026 17:42:18.110] [Test worker/ERROR] [Iris/]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: enable failed java.lang.RuntimeException: enable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31) ~[test/:?] at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
@@ -92,10 +92,10 @@ java.lang.RuntimeException: enable failed
Suppressed: java.lang.RuntimeException: cleanup failed Suppressed: java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?] at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?]
... 42 more ... 42 more
[16Jul2026 05:44:26.814] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed [18Jul2026 17:42:18.144] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed
java.lang.IllegalStateException: stop request failed java.lang.IllegalStateException: stop request failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:235) ~[test/:?] at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:235) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:176) ~[main/:?] at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:179) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:232) ~[test/:?] at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:232) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?] at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
@@ -139,10 +139,10 @@ java.lang.IllegalStateException: stop request failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?] at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[16Jul2026 05:44:26.821] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed [18Jul2026 17:42:18.147] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed
java.lang.IllegalStateException: shutdown wait failed java.lang.IllegalStateException: shutdown wait failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:261) ~[test/:?] at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:261) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:191) ~[main/:?] at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:194) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:259) ~[test/:?] at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:259) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?] at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
@@ -186,10 +186,10 @@ java.lang.IllegalStateException: shutdown wait failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?] at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[16Jul2026 05:44:26.826] [Test worker/ERROR] [Iris/]: [worldcheck] check failed [18Jul2026 17:42:18.154] [Test worker/ERROR] [Iris/]: [worldcheck] check failed
java.lang.IllegalStateException: check failed java.lang.IllegalStateException: check failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:221) ~[test/:?] at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:221) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:171) ~[main/:?] at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:174) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:219) ~[test/:?] at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:219) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?] at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
@@ -233,8 +233,8 @@ java.lang.IllegalStateException: check failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?] at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[16Jul2026 05:44:26.832] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_discovery_success' [18Jul2026 17:42:18.160] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_discovery_success'
[16Jul2026 05:44:26.833] [Test worker/ERROR] [Iris/]: Iris custom content provider discovery failed [18Jul2026 17:42:18.161] [Test worker/ERROR] [Iris/]: Iris custom content provider discovery failed
java.lang.RuntimeException: provider init failed java.lang.RuntimeException: provider init failed
at art.arcane.iris.modded.api.ModdedCustomContentRegistryTest.failedDiscoveryPublishesNothingAndPreservesTheCause(ModdedCustomContentRegistryTest.java:39) ~[test/:?] at art.arcane.iris.modded.api.ModdedCustomContentRegistryTest.failedDiscoveryPublishesNothingAndPreservesTheCause(ModdedCustomContentRegistryTest.java:39) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
@@ -53,6 +53,11 @@ public final class ForgeModdedLoader implements ModdedLoader {
return ServerLifecycleHooks.getCurrentServer(); return ServerLifecycleHooks.getCurrentServer();
} }
@Override
public void invalidateLevelCache(MinecraftServer server) {
server.markWorldsDirty();
}
@Override @Override
public boolean clientEnvironment() { public boolean clientEnvironment() {
return FMLEnvironment.dist.isClient(); return FMLEnvironment.dist.isClient();
@@ -22,6 +22,7 @@ import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.WorldgenRandom; import net.minecraft.world.level.levelgen.WorldgenRandom;
import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.ScatteredFeaturePiece;
import net.minecraft.world.level.levelgen.structure.StructurePiece; import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart; import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment; import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
@@ -31,6 +32,8 @@ 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.SinglePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement; 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.StructureTemplatePool;
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece;
import net.minecraft.world.level.levelgen.structure.structures.JungleTemplePiece;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces; import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings; 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.StructureTemplate;
@@ -46,7 +49,9 @@ import java.util.Objects;
import java.util.function.IntBinaryOperator; import java.util.function.IntBinaryOperator;
public final class NativeStructurePostProcessor { public final class NativeStructurePostProcessor {
private static final String DESERT_PYRAMID_ID = "minecraft:desert_pyramid";
private static final int FOUNDATION_VERTICAL_TOLERANCE = 1; private static final int FOUNDATION_VERTICAL_TOLERANCE = 1;
private static final String JUNGLE_PYRAMID_ID = "minecraft:jungle_pyramid";
private static final int MAX_BURIAL_COLUMNS = 2_000_000; private static final int MAX_BURIAL_COLUMNS = 2_000_000;
private static final int MONUMENT_BASE_BELOW_SEA_LEVEL = 24; private static final int MONUMENT_BASE_BELOW_SEA_LEVEL = 24;
private static final String OCEAN_MONUMENT_ID = "minecraft:monument"; private static final String OCEAN_MONUMENT_ID = "minecraft:monument";
@@ -80,10 +85,34 @@ public final class NativeStructurePostProcessor {
return alignOceanMonumentToSeaLevel( return alignOceanMonumentToSeaLevel(
start, requestedOffset, seaLevel, worldMinY, worldMaxYExclusive); start, requestedOffset, seaLevel, worldMinY, worldMaxYExclusive);
} }
if (isRaisedScatteredStructure(structureId)) {
return alignScatteredStructureToSurface(
start, structureId, requestedOffset, worldMinY, worldMaxYExclusive, surfaceHeight);
}
return applyVerticalShift( return applyVerticalShift(
start, requestedOffset, worldMinY, worldMaxYExclusive, underground, surfaceHeight); start, requestedOffset, worldMinY, worldMaxYExclusive, underground, surfaceHeight);
} }
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 = requireRaisedScatteredPiece(start, structureId);
BoundingBox bounds = start.getBoundingBox();
BoundingBox pieceBounds = piece.getBoundingBox();
int surfaceY = highestSurfaceY(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, public static int applyVerticalShift(StructureStart start, int requestedOffset, int worldMinY,
int worldMaxYExclusive, boolean underground, int worldMaxYExclusive, boolean underground,
IntBinaryOperator surfaceHeight) { IntBinaryOperator surfaceHeight) {
@@ -137,6 +166,72 @@ public final class NativeStructurePostProcessor {
return OCEAN_MONUMENT_ID.equals(structureId); return OCEAN_MONUMENT_ID.equals(structureId);
} }
private static boolean isRaisedScatteredStructure(String structureId) {
return DESERT_PYRAMID_ID.equals(structureId) || JUNGLE_PYRAMID_ID.equals(structureId);
}
private static ScatteredFeaturePiece requireRaisedScatteredPiece(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 highestSurfaceY(BoundingBox bounds, IntBinaryOperator surfaceHeight) {
int highestY = Integer.MIN_VALUE;
for (int x = bounds.minX(); x <= bounds.maxX(); x++) {
for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) {
highestY = Math.max(highestY, surfaceHeight.applyAsInt(x, z));
}
}
if (highestY == Integer.MIN_VALUE) {
throw new IllegalStateException("Scattered native structure has an empty terrain footprint");
}
return highestY;
}
private static void setScatteredHeightPosition(ScatteredFeaturePiece piece, int heightPosition) {
try {
ScatteredHeightPositionAccess.FIELD.setInt(piece, heightPosition);
} catch (IllegalAccessException error) {
throw new IllegalStateException("Cannot lock scattered native structure height", error);
}
}
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;
}
private static OceanMonumentPieces.MonumentBuilding requireOceanMonumentBuilding(StructureStart start) { private static OceanMonumentPieces.MonumentBuilding requireOceanMonumentBuilding(StructureStart start) {
Objects.requireNonNull(start, "Ocean monument start must not be null"); Objects.requireNonNull(start, "Ocean monument start must not be null");
List<StructurePiece> pieces = start.getPieces(); List<StructurePiece> pieces = start.getPieces();
@@ -383,11 +478,21 @@ public final class NativeStructurePostProcessor {
return List.copyOf(anchors); return List.copyOf(anchors);
} }
private static boolean requiresSurfaceTerrain(StructureStart start) { static boolean requiresSurfaceTerrain(StructureStart start) {
return start != null return start != null
&& start.isValid() && start.isValid()
&& shouldPrepareSurfaceTerrain( && (shouldPrepareSurfaceTerrain(
start.getStructure().terrainAdaptation(), start.getStructure().step()); start.getStructure().terrainAdaptation(), start.getStructure().step())
|| containsRaisedScatteredPiece(start));
}
private static boolean containsRaisedScatteredPiece(StructureStart start) {
for (StructurePiece piece : start.getPieces()) {
if (piece instanceof DesertPyramidPiece || piece instanceof JungleTemplePiece) {
return true;
}
}
return false;
} }
private static void fitSurfaceTerrain(WorldGenLevel world, BoundingBox area, private static void fitSurfaceTerrain(WorldGenLevel world, BoundingBox area,
@@ -927,6 +1032,13 @@ public final class NativeStructurePostProcessor {
} }
} }
private static final class ScatteredHeightPositionAccess {
private static final Field FIELD = resolveScatteredHeightPositionField();
private ScatteredHeightPositionAccess() {
}
}
private static final class SinglePoolTemplateAccess { private static final class SinglePoolTemplateAccess {
private static final Field FIELD = resolveSinglePoolTemplateField(); private static final Field FIELD = resolveSinglePoolTemplateField();
@@ -104,7 +104,6 @@ public final class ModdedEngineBootstrap {
ModdedStartup.prepareForStartup(); ModdedStartup.prepareForStartup();
IrisModdedChunkGenerator.startGenPool(); IrisModdedChunkGenerator.startGenPool();
bindWorldGenerators(server); bindWorldGenerators(server);
ModdedStartup.runOnce(server);
services().enableAll(); services().enableAll();
ModdedProtocolHandler.start(server); ModdedProtocolHandler.start(server);
ModdedSentry.start(loader()); ModdedSentry.start(loader());
@@ -125,6 +124,7 @@ public final class ModdedEngineBootstrap {
public static void serverStarted(MinecraftServer server) { public static void serverStarted(MinecraftServer server) {
bindWorldGenerators(server); bindWorldGenerators(server);
ModdedStartup.runOnce(server);
reconcileSpawn(server); reconcileSpawn(server);
ModdedWorldCheck.serverStarted(server); ModdedWorldCheck.serverStarted(server);
} }
@@ -284,7 +284,8 @@ public final class ModdedEngineBootstrap {
IrisPlatforms.bind(created); IrisPlatforms.bind(created);
rollback.add(() -> restorePlatform(previousPlatform)); rollback.add(() -> restorePlatform(previousPlatform));
ModdedServerAccess previousServerAccess = ModdedDimensionManager.bindAccess(new ModdedServerLevels()); ModdedServerAccess previousServerAccess = ModdedDimensionManager.bindAccess(
new ModdedServerLevels(boundLoader::invalidateLevelCache));
rollback.add(() -> ModdedDimensionManager.restoreAccess(previousServerAccess)); rollback.add(() -> ModdedDimensionManager.restoreAccess(previousServerAccess));
IrisObjectRotation.StateRotator previousRotator = IrisObjectRotation.bindPlatformRotator(new ModdedStateRotator()); IrisObjectRotation.StateRotator previousRotator = IrisObjectRotation.bindPlatformRotator(new ModdedStateRotator());
@@ -32,6 +32,8 @@ public interface ModdedLoader {
MinecraftServer currentServer(); MinecraftServer currentServer();
void invalidateLevelCache(MinecraftServer server);
boolean clientEnvironment(); boolean clientEnvironment();
Path configDir(); Path configDir();
@@ -24,9 +24,17 @@ import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import net.minecraft.world.level.storage.LevelStorageSource; import net.minecraft.world.level.storage.LevelStorageSource;
import java.util.Objects;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.function.Consumer;
public final class ModdedServerLevels implements ModdedServerAccess { public final class ModdedServerLevels implements ModdedServerAccess {
private final Consumer<MinecraftServer> levelCacheInvalidator;
public ModdedServerLevels(Consumer<MinecraftServer> levelCacheInvalidator) {
this.levelCacheInvalidator = Objects.requireNonNull(levelCacheInvalidator);
}
@Override @Override
public Executor levelExecutor(MinecraftServer server) { public Executor levelExecutor(MinecraftServer server) {
return server.executor; return server.executor;
@@ -39,17 +47,29 @@ public final class ModdedServerLevels implements ModdedServerAccess {
@Override @Override
public ServerLevel putLevel(MinecraftServer server, ResourceKey<Level> key, ServerLevel level) { public ServerLevel putLevel(MinecraftServer server, ResourceKey<Level> key, ServerLevel level) {
return server.levels.put(key, level); ServerLevel previous = server.levels.put(key, level);
if (previous != level) {
levelCacheInvalidator.accept(server);
}
return previous;
} }
@Override @Override
public ServerLevel putLevelIfAbsent(MinecraftServer server, ResourceKey<Level> key, ServerLevel level) { public ServerLevel putLevelIfAbsent(MinecraftServer server, ResourceKey<Level> key, ServerLevel level) {
return server.levels.putIfAbsent(key, level); ServerLevel previous = server.levels.putIfAbsent(key, level);
if (previous == null) {
levelCacheInvalidator.accept(server);
}
return previous;
} }
@Override @Override
public ServerLevel removeLevel(MinecraftServer server, ResourceKey<Level> key) { public ServerLevel removeLevel(MinecraftServer server, ResourceKey<Level> key) {
return server.levels.remove(key); ServerLevel removed = server.levels.remove(key);
if (removed != null) {
levelCacheInvalidator.accept(server);
}
return removed;
} }
@Override @Override
@@ -66,7 +66,7 @@ public final class ModdedStartup {
} }
public static void runOnce(MinecraftServer server) { public static void runOnce(MinecraftServer server) {
if (server == null) { if (server == null || server.getPlayerList() == null) {
return; return;
} }
prepareForStartup(); prepareForStartup();
@@ -72,6 +72,44 @@ public class ModdedLifecycleFailureContractTest {
assertTrue(failure.contains(", e);")); assertTrue(failure.contains(", e);"));
} }
@Test
public void persistentReinjectionWaitsForPlayerServicesAndServerStarted() throws IOException {
String bootstrapSource = source("ModdedEngineBootstrap.java");
String start = method(bootstrapSource, "public static void start(");
String serverAboutToStart = method(bootstrapSource, "public static void serverAboutToStart(");
String serverStarted = method(bootstrapSource, "public static void serverStarted(");
assertFalse(start.contains("ModdedStartup.runOnce(server);"));
assertFalse(serverAboutToStart.contains("ModdedStartup.runOnce(server);"));
assertEquals(1, occurrences(serverStarted, "ModdedStartup.runOnce(server);"));
assertBefore(serverStarted, "bindWorldGenerators(server);", "ModdedStartup.runOnce(server);");
assertBefore(serverStarted, "ModdedStartup.runOnce(server);", "reconcileSpawn(server);");
String startupSource = source("ModdedStartup.java");
String runOnce = method(startupSource, "public static void runOnce(");
assertBefore(runOnce, "server.getPlayerList() == null", "STARTED.compareAndSet(false, true)");
}
@Test
public void dynamicLevelMutationsInvalidateLoaderTickCaches() throws IOException {
String levelsSource = source("ModdedServerLevels.java");
String put = method(levelsSource, "public ServerLevel putLevel(");
String putIfAbsent = method(levelsSource, "public ServerLevel putLevelIfAbsent(");
String remove = method(levelsSource, "public ServerLevel removeLevel(");
assertBefore(put, "server.levels.put(key, level);", "levelCacheInvalidator.accept(server);");
assertTrue(put.contains("if (previous != level)"));
assertBefore(putIfAbsent, "server.levels.putIfAbsent(key, level);", "levelCacheInvalidator.accept(server);");
assertTrue(putIfAbsent.contains("if (previous == null)"));
assertBefore(remove, "server.levels.remove(key);", "levelCacheInvalidator.accept(server);");
assertTrue(remove.contains("if (removed != null)"));
String loaderSource = source("ModdedLoader.java");
assertTrue(loaderSource.contains("void invalidateLevelCache(MinecraftServer server);"));
String bootstrapSource = source("ModdedEngineBootstrap.java");
assertTrue(bootstrapSource.contains("new ModdedServerLevels(boundLoader::invalidateLevelCache)"));
}
@Test @Test
public void packDimensionLoadingNeverSuppressesTheFailureAsNull() throws IOException { public void packDimensionLoadingNeverSuppressesTheFailureAsNull() throws IOException {
String source = source("ModdedDimensionManager.java"); String source = source("ModdedDimensionManager.java");
+4 -5
View File
@@ -31,7 +31,7 @@ file('../../gradle.properties').withInputStream { InputStream stream -> rootProp
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.2')) String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.2'))
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2')) String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2'))
String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse(rootProperties.getProperty('neoForgeVersion', '26.2.0.12-beta')) String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse(rootProperties.getProperty('neoForgeVersion', '26.2.0.12-beta'))
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:4c38988b344792a79f925f57f8a675bc85fc1bed')) String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:d9026a7c8ebc391c8109f401ce79a0ce65df3969'))
Closure<String> irisArtifactName = { String platform, String targetVersion -> Closure<String> irisArtifactName = { String platform, String targetVersion ->
return "Iris v${project.version} [${platform}] ${targetVersion}.jar" return "Iris v${project.version} [${platform}] ${targetVersion}.jar"
} }
@@ -201,6 +201,9 @@ processResources {
tasks.named('shadowJar', ShadowJar).configure { tasks.named('shadowJar', ShadowJar).configure {
doFirst { doFirst {
delete(fileTree(layout.buildDirectory.dir('libs')) {
include('Iris v* [NeoForge] *.jar')
})
delete(layout.buildDirectory.file("libs/Iris-${project.version}+mc${minecraftVersion}-neoforge.jar")) delete(layout.buildDirectory.file("libs/Iris-${project.version}+mc${minecraftVersion}-neoforge.jar"))
} }
archiveFileName.set(irisArtifactName('NeoForge', "${minecraftVersion}+${neoForgeVersion}")) archiveFileName.set(irisArtifactName('NeoForge', "${minecraftVersion}+${neoForgeVersion}"))
@@ -215,10 +218,6 @@ tasks.named('shadowJar', ShadowJar).configure {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE duplicatesStrategy = DuplicatesStrategy.EXCLUDE
relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm') relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm')
relocate('io.sentry', 'art.arcane.iris.shadow.sentry') relocate('io.sentry', 'art.arcane.iris.shadow.sentry')
relocate('com.sun.jna', 'art.arcane.iris.shadow.jna')
relocate('oshi', 'art.arcane.iris.shadow.oshi')
relocate('net.jpountz', 'art.arcane.iris.shadow.jpountz')
exclude('oshi.properties')
exclude('org/jspecify/**') exclude('org/jspecify/**')
exclude('com/google/errorprone/**') exclude('com/google/errorprone/**')
exclude('com/google/j2objc/**') exclude('com/google/j2objc/**')
@@ -53,6 +53,11 @@ public final class NeoForgeModdedLoader implements ModdedLoader {
return ServerLifecycleHooks.getCurrentServer(); return ServerLifecycleHooks.getCurrentServer();
} }
@Override
public void invalidateLevelCache(MinecraftServer server) {
server.markWorldsDirty();
}
@Override @Override
public boolean clientEnvironment() { public boolean clientEnvironment() {
return FMLEnvironment.getDist().isClient(); return FMLEnvironment.getDist().isClient();
+71 -2
View File
@@ -52,7 +52,7 @@ String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').get
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse('26.2-65.0.4') String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse('26.2-65.0.4')
String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse('26.2.0.12-beta') String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse('26.2.0.12-beta')
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate') String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
.orElse('com.github.VolmitSoftware:VolmLib:4c38988b344792a79f925f57f8a675bc85fc1bed') .orElse('com.github.VolmitSoftware:VolmLib:d9026a7c8ebc391c8109f401ce79a0ce65df3969')
.get() .get()
String useLocalVolmLib = providers.gradleProperty('useLocalVolmLib').getOrElse('true') String useLocalVolmLib = providers.gradleProperty('useLocalVolmLib').getOrElse('true')
String localVolmLibDirectory = providers.gradleProperty('localVolmLibDirectory').getOrNull() String localVolmLibDirectory = providers.gradleProperty('localVolmLibDirectory').getOrNull()
@@ -276,6 +276,75 @@ tasks.named('neoforgeJar').configure {
mustRunAfter(tasks.named('forgeJar')) mustRunAfter(tasks.named('forgeJar'))
} }
List<String> commonModdedArtifactEntries = [
'art/arcane/iris/engine/IrisEngine.class',
'art/arcane/iris/spi/IrisPlatform.class',
'art/arcane/volmlib/util/mantle/io/Lz4IOWorkerCodecSupport.class',
'art/arcane/iris/util/common/misc/getHardware.class'
]
tasks.register('verifyFabricArtifact') {
group = 'verification'
dependsOn('fabricJar')
File artifact = layout.projectDirectory.file("adapters/fabric/build/libs/${fabricArtifactName}").asFile
inputs.file(artifact)
doLast {
List<String> requiredEntries = commonModdedArtifactEntries + [
'fabric.mod.json',
'art/arcane/iris/fabric/IrisFabricBootstrap.class'
]
ModdedArtifactVerifier.verify(artifact, requiredEntries)
logger.lifecycle("Verified ${artifact.name} uses Minecraft-provided LZ4, OSHI, and JNA")
}
}
tasks.register('verifyForgeArtifact') {
group = 'verification'
dependsOn('forgeJar')
File artifact = layout.projectDirectory.file("adapters/forge/build/libs/${forgeArtifactName}").asFile
inputs.file(artifact)
doLast {
List<String> requiredEntries = commonModdedArtifactEntries + [
'META-INF/mods.toml',
'art/arcane/iris/forge/IrisForgeBootstrap.class'
]
ModdedArtifactVerifier.verify(artifact, requiredEntries)
logger.lifecycle("Verified ${artifact.name} uses Minecraft-provided LZ4, OSHI, and JNA")
}
}
tasks.register('verifyNeoforgeArtifact') {
group = 'verification'
dependsOn('neoforgeJar')
File artifact = layout.projectDirectory.file("adapters/neoforge/build/libs/${neoForgeArtifactName}").asFile
inputs.file(artifact)
doLast {
List<String> requiredEntries = commonModdedArtifactEntries + [
'META-INF/neoforge.mods.toml',
'art/arcane/iris/neoforge/IrisNeoForgeBootstrap.class'
]
ModdedArtifactVerifier.verify(artifact, requiredEntries)
logger.lifecycle("Verified ${artifact.name} uses Minecraft-provided LZ4, OSHI, and JNA")
}
}
tasks.register('verifyModdedArtifacts') {
group = 'verification'
dependsOn('verifyFabricArtifact', 'verifyForgeArtifact', 'verifyNeoforgeArtifact')
}
tasks.named('buildFabric').configure {
dependsOn('verifyFabricArtifact')
}
tasks.named('buildForge').configure {
dependsOn('verifyForgeArtifact')
}
tasks.named('buildNeoforge').configure {
dependsOn('verifyNeoforgeArtifact')
}
tasks.register('buildAllToOut') { tasks.register('buildAllToOut') {
group = 'iris' group = 'iris'
dependsOn('buildBukkit', 'buildFabric', 'buildForge', 'buildNeoforge', ':spi:jar') dependsOn('buildBukkit', 'buildFabric', 'buildForge', 'buildNeoforge', ':spi:jar')
@@ -295,7 +364,7 @@ tasks.register('buildAllToOut') {
tasks.register('buildAll') { tasks.register('buildAll') {
group = 'iris' group = 'iris'
outputs.upToDateWhen { false } outputs.upToDateWhen { false }
dependsOn('iris', 'fabricJar', 'forgeJar', 'neoforgeJar') dependsOn('iris', 'verifyModdedArtifacts')
doLast { doLast {
delete( delete(
file("${bukkitConsumerPath}/Iris-Fabric.jar"), file("${bukkitConsumerPath}/Iris-Fabric.jar"),
+2
View File
@@ -19,7 +19,9 @@ repositories {
} }
dependencies { dependencies {
implementation(gradleApi())
implementation('org.ow2.asm:asm:9.10.1') implementation('org.ow2.asm:asm:9.10.1')
implementation('com.github.VolmitSoftware:NMSTools:c88961416f') implementation('com.github.VolmitSoftware:NMSTools:c88961416f')
implementation('io.papermc.paperweight:paperweight-userdev:2.0.0-beta.21') implementation('io.papermc.paperweight:paperweight-userdev:2.0.0-beta.21')
testImplementation('junit:junit:4.13.2')
} }
@@ -0,0 +1,181 @@
import org.gradle.api.GradleException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
public final class ModdedArtifactVerifier {
private static final String CODEC_CLASS = "art/arcane/volmlib/util/mantle/io/Lz4IOWorkerCodecSupport.class";
private static final List<String> INTERNAL_LZ4_REFERENCES = List.of(
"net/jpountz/lz4/LZ4BlockInputStream",
"net/jpountz/lz4/LZ4BlockOutputStream"
);
private static final String INTERNAL_OSHI_REFERENCE = "oshi/SystemInfo";
private static final List<String> BUNDLED_RUNTIME_PREFIXES = List.of(
"net/jpountz/",
"oshi/",
"com/sun/jna/",
"art/arcane/iris/shadow/jpountz/",
"art/arcane/iris/shadow/oshi/",
"art/arcane/iris/shadow/jna/"
);
private static final List<String> PRIVATE_REFERENCE_PREFIXES = List.of(
"art/arcane/iris/shadow/jpountz",
"art/arcane/iris/shadow/oshi",
"art/arcane/iris/shadow/jna"
);
private ModdedArtifactVerifier() {
}
public static void verify(File artifact, List<String> requiredEntries) {
if (!artifact.isFile()) {
throw new GradleException("Missing modded Iris artifact: " + artifact.getAbsolutePath());
}
try (JarFile jar = new JarFile(artifact)) {
for (String requiredEntry : requiredEntries) {
if (jar.getJarEntry(requiredEntry) == null) {
throw new GradleException(artifact.getName() + " is missing " + requiredEntry);
}
}
Set<String> bundledRuntimeEntries = new LinkedHashSet<>();
Set<String> privateReferenceClasses = new LinkedHashSet<>();
Set<String> codecReferences = new LinkedHashSet<>();
boolean oshiReference = false;
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (entry.isDirectory()) {
continue;
}
String name = entry.getName();
if (startsWithAny(name, BUNDLED_RUNTIME_PREFIXES)) {
bundledRuntimeEntries.add(name);
}
if (isNestedJar(name)) {
inspectNestedJar(jar, entry, bundledRuntimeEntries, privateReferenceClasses);
}
if (!name.endsWith(".class")) {
continue;
}
String bytecode = readClass(jar, entry);
if (containsAny(bytecode, PRIVATE_REFERENCE_PREFIXES)) {
privateReferenceClasses.add(name);
}
if (bytecode.contains(INTERNAL_OSHI_REFERENCE)) {
oshiReference = true;
}
if (name.equals(CODEC_CLASS)) {
for (String reference : INTERNAL_LZ4_REFERENCES) {
if (bytecode.contains(reference)) {
codecReferences.add(reference);
}
}
}
}
if (!bundledRuntimeEntries.isEmpty()) {
throw new GradleException(artifact.getName() + " duplicates Minecraft runtime libraries: "
+ preview(bundledRuntimeEntries));
}
if (!privateReferenceClasses.isEmpty()) {
throw new GradleException(artifact.getName() + " contains unresolved Iris-private runtime references in: "
+ preview(privateReferenceClasses));
}
if (!codecReferences.containsAll(INTERNAL_LZ4_REFERENCES)) {
throw new GradleException(artifact.getName() + " does not link region storage to Minecraft's LZ4 runtime");
}
if (!oshiReference) {
throw new GradleException(artifact.getName() + " does not link hardware diagnostics to Minecraft's OSHI runtime");
}
} catch (IOException e) {
throw new GradleException("Unable to verify modded Iris artifact " + artifact.getAbsolutePath(), e);
}
}
private static String readClass(JarFile jar, JarEntry entry) throws IOException {
try (InputStream input = jar.getInputStream(entry)) {
return new String(input.readAllBytes(), StandardCharsets.ISO_8859_1);
}
}
private static boolean startsWithAny(String value, List<String> prefixes) {
for (String prefix : prefixes) {
if (value.startsWith(prefix)) {
return true;
}
}
return false;
}
private static boolean containsAny(String value, List<String> fragments) {
for (String fragment : fragments) {
if (value.contains(fragment)) {
return true;
}
}
return false;
}
private static void inspectNestedJar(JarFile jar, JarEntry nestedEntry,
Set<String> bundledRuntimeEntries,
Set<String> privateReferenceClasses) throws IOException {
String nestedName = nestedEntry.getName();
if (isNamedRuntimeLibrary(nestedName)) {
bundledRuntimeEntries.add(nestedName);
}
try (InputStream input = jar.getInputStream(nestedEntry);
JarInputStream nestedJar = new JarInputStream(input)) {
JarEntry entry;
while ((entry = nestedJar.getNextJarEntry()) != null) {
if (entry.isDirectory()) {
continue;
}
String name = entry.getName();
String path = nestedName + "!/" + name;
if (startsWithAny(name, BUNDLED_RUNTIME_PREFIXES)) {
bundledRuntimeEntries.add(path);
}
if (name.endsWith(".class")) {
String bytecode = new String(nestedJar.readAllBytes(), StandardCharsets.ISO_8859_1);
if (containsAny(bytecode, PRIVATE_REFERENCE_PREFIXES)) {
privateReferenceClasses.add(path);
}
}
}
}
}
private static boolean isNestedJar(String name) {
return name.endsWith(".jar")
&& (name.startsWith("META-INF/jars/") || name.startsWith("META-INF/jarjar/"));
}
private static boolean isNamedRuntimeLibrary(String name) {
if (!isNestedJar(name)) {
return false;
}
String fileName = name.substring(name.lastIndexOf('/') + 1).toLowerCase(Locale.ROOT);
return fileName.startsWith("lz4") || fileName.startsWith("oshi") || fileName.startsWith("jna");
}
private static String preview(Set<String> values) {
return String.join(", ", values.stream().limit(8).toList());
}
}
@@ -0,0 +1,130 @@
import org.gradle.api.GradleException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class ModdedArtifactVerifierTest {
private static final String METADATA = "fabric.mod.json";
private static final String CODEC_CLASS = "art/arcane/volmlib/util/mantle/io/Lz4IOWorkerCodecSupport.class";
private static final String HARDWARE_CLASS = "art/arcane/iris/util/common/misc/getHardware.class";
private static final List<String> REQUIRED_ENTRIES = List.of(METADATA, CODEC_CLASS, HARDWARE_CLASS);
private static final byte[] INTERNAL_CODEC = (
"net/jpountz/lz4/LZ4BlockInputStream net/jpountz/lz4/LZ4BlockOutputStream"
).getBytes(StandardCharsets.ISO_8859_1);
@Rule
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void acceptsMinecraftRuntimeReferences() throws Exception {
Map<String, byte[]> entries = validEntries();
File artifact = createArtifact(entries);
ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES);
}
@Test
public void rejectsRelocatedRuntimeReference() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.put("art/arcane/iris/Test.class",
"art/arcane/iris/shadow/jpountz/lz4/LZ4BlockInputStream"
.getBytes(StandardCharsets.ISO_8859_1));
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES));
assertTrue(failure.getMessage().contains("Iris-private runtime references"));
}
@Test
public void rejectsBundledMinecraftRuntimeLibrary() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.put("net/jpountz/lz4/LZ4BlockInputStream.class", new byte[]{0});
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES));
assertTrue(failure.getMessage().contains("duplicates Minecraft runtime libraries"));
}
@Test
public void rejectsNestedMinecraftRuntimeLibrary() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.put("META-INF/jars/lz4-java.jar", new byte[]{0});
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES));
assertTrue(failure.getMessage().contains("duplicates Minecraft runtime libraries"));
}
@Test
public void rejectsRenamedJarJarRuntimeLibrary() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.put("META-INF/jarjar/runtime-support.jar", createNestedArtifact(Map.of(
"com/sun/jna/Native.class", new byte[]{0}
)));
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES));
assertTrue(failure.getMessage().contains("duplicates Minecraft runtime libraries"));
}
@Test
public void rejectsArtifactWithoutPlatformMetadata() throws Exception {
Map<String, byte[]> entries = new LinkedHashMap<>();
entries.put(CODEC_CLASS, INTERNAL_CODEC);
entries.put(HARDWARE_CLASS, "oshi/SystemInfo".getBytes(StandardCharsets.ISO_8859_1));
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES));
assertTrue(failure.getMessage().contains("is missing " + METADATA));
}
private Map<String, byte[]> validEntries() {
Map<String, byte[]> entries = new LinkedHashMap<>();
entries.put(METADATA, new byte[]{0});
entries.put(CODEC_CLASS, INTERNAL_CODEC);
entries.put(HARDWARE_CLASS, "oshi/SystemInfo".getBytes(StandardCharsets.ISO_8859_1));
return entries;
}
private File createArtifact(Map<String, byte[]> entries) throws Exception {
File artifact = temporaryFolder.newFile("artifact-" + System.nanoTime() + ".jar");
try (JarOutputStream output = new JarOutputStream(new FileOutputStream(artifact))) {
for (Map.Entry<String, byte[]> entry : entries.entrySet()) {
output.putNextEntry(new JarEntry(entry.getKey()));
output.write(entry.getValue());
output.closeEntry();
}
}
return artifact;
}
private byte[] createNestedArtifact(Map<String, byte[]> entries) throws Exception {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (JarOutputStream output = new JarOutputStream(bytes)) {
for (Map.Entry<String, byte[]> entry : entries.entrySet()) {
output.putNextEntry(new JarEntry(entry.getKey()));
output.write(entry.getValue());
output.closeEntry();
}
}
return bytes.toByteArray();
}
}
+1 -1
View File
@@ -37,7 +37,7 @@ plugins {
def lib = 'art.arcane.iris.util' def lib = 'art.arcane.iris.util'
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate') String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
.orElse('com.github.VolmitSoftware:VolmLib:4c38988b344792a79f925f57f8a675bc85fc1bed') .orElse('com.github.VolmitSoftware:VolmLib:d9026a7c8ebc391c8109f401ce79a0ce65df3969')
.get() .get()
String sentryAuthToken = findProperty('sentry.auth.token') as String ?: System.getenv('SENTRY_AUTH_TOKEN') String sentryAuthToken = findProperty('sentry.auth.token') as String ?: System.getenv('SENTRY_AUTH_TOKEN')
boolean hasSentryAuthToken = sentryAuthToken != null && !sentryAuthToken.isBlank() boolean hasSentryAuthToken = sentryAuthToken != null && !sentryAuthToken.isBlank()
@@ -163,7 +163,6 @@ final class DecoratorCore {
} }
data.set(x, height, z, fixFacesForHunk( data.set(x, height, z, fixFacesForHunk(
decorator.getForceBlock().getBlockData(irisData), data, x, z, realX, height, realZ, mantle)); decorator.getForceBlock().getBlockData(irisData), data, x, z, realX, height, realZ, mantle));
return;
} }
if (!decorator.isForcePlace()) { if (!decorator.isForcePlace()) {
@@ -135,36 +135,42 @@ public class IrisStructureComponent extends IrisMantleComponent {
ObjectPlaceMode mode = structure.getPlaceMode(); ObjectPlaceMode mode = structure.getPlaceMode();
int failedPieces = 0; int failedPieces = 0;
Long2IntOpenHashMap foundationColumns = placement.getStilt() == null IrisStructureStiltSettings stilt = placement.getStilt();
Long2IntOpenHashMap foundationColumns = stilt == null
? null : new Long2IntOpenHashMap(); ? null : new Long2IntOpenHashMap();
boolean supportNonOccluding = stilt != null && stilt.isSupportNonOccluding();
if (placement.isUnderground()) { if (placement.isUnderground()) {
ObjectPlaceMode undergroundMode = (mode == ObjectPlaceMode.ORGANIC_STILT || mode == ObjectPlaceMode.CEILING_HANG) ObjectPlaceMode undergroundMode = (mode == ObjectPlaceMode.ORGANIC_STILT || mode == ObjectPlaceMode.CEILING_HANG)
? mode : ObjectPlaceMode.STRUCTURE_PIECE; ? mode : ObjectPlaceMode.STRUCTURE_PIECE;
for (PlacedStructurePiece p : pieces) { for (PlacedStructurePiece p : pieces) {
if (placeObject(writer, structure, p, undergroundMode, p.getY(), rng, foundationColumns) == -1) { if (placeObject(writer, structure, p, undergroundMode, p.getY(), rng,
foundationColumns, supportNonOccluding) == -1) {
failedPieces++; failedPieces++;
} }
} }
} else if (mode == ObjectPlaceMode.STRUCTURE_PIECE || mode == ObjectPlaceMode.FLOATING) { } else if (mode == ObjectPlaceMode.STRUCTURE_PIECE || mode == ObjectPlaceMode.FLOATING) {
for (PlacedStructurePiece p : pieces) { for (PlacedStructurePiece p : pieces) {
if (placeObject(writer, structure, p, ObjectPlaceMode.STRUCTURE_PIECE, p.getY(), rng, foundationColumns) == -1) { if (placeObject(writer, structure, p, ObjectPlaceMode.STRUCTURE_PIECE, p.getY(), rng,
foundationColumns, supportNonOccluding) == -1) {
failedPieces++; failedPieces++;
} }
} }
} else if (pieces.size() == 1) { } else if (pieces.size() == 1) {
if (placeObject(writer, structure, pieces.getFirst(), mode, -1, rng, foundationColumns) == -1) { if (placeObject(writer, structure, pieces.getFirst(), mode, -1, rng,
foundationColumns, supportNonOccluding) == -1) {
failedPieces++; failedPieces++;
} }
} else { } else {
for (PlacedStructurePiece p : pieces) { for (PlacedStructurePiece p : pieces) {
if (placeObject(writer, structure, p, ObjectPlaceMode.STRUCTURE_PIECE, p.getY(), rng, foundationColumns) == -1) { if (placeObject(writer, structure, p, ObjectPlaceMode.STRUCTURE_PIECE, p.getY(), rng,
foundationColumns, supportNonOccluding) == -1) {
failedPieces++; failedPieces++;
} }
} }
} }
requireAppliedPieces(resolved, cx, cz, failedPieces); requireAppliedPieces(resolved, cx, cz, failedPieces);
if (failedPieces == 0 && placement.getStilt() != null) { if (failedPieces == 0 && stilt != null) {
placeFoundation(writer, foundationColumns, placement.getStilt(), rng, !placement.isUnderground()); placeFoundation(writer, foundationColumns, stilt, rng, !placement.isUnderground());
} }
} }
@@ -533,7 +539,8 @@ public class IrisStructureComponent extends IrisMantleComponent {
} }
private int placeObject(MantleWriter writer, IrisStructure structure, PlacedStructurePiece p, private int placeObject(MantleWriter writer, IrisStructure structure, PlacedStructurePiece p,
ObjectPlaceMode mode, int y, RNG rng, Long2IntOpenHashMap foundationColumns) { ObjectPlaceMode mode, int y, RNG rng, Long2IntOpenHashMap foundationColumns,
boolean supportNonOccluding) {
IrisObject object = p.getObject(); IrisObject object = p.getObject();
String objectKey = object.getLoadKey(); String objectKey = object.getLoadKey();
IrisObjectPlacement config = structure.createLootPlacement(objectKey); IrisObjectPlacement config = structure.createLootPlacement(objectKey);
@@ -549,7 +556,8 @@ public class IrisStructureComponent extends IrisMantleComponent {
String marker = structurePlacementMarker(structure, p, objectKey); String marker = structurePlacementMarker(structure, p, objectKey);
return object.place(p.getX(), placeY, p.getZ(), writer, config, rng, (position, state) -> { return object.place(p.getX(), placeY, p.getZ(), writer, config, rng, (position, state) -> {
StructureFoundationPlanner.recordBaseCell( StructureFoundationPlanner.recordBaseCell(
foundationColumns, position.getX(), position.getY(), position.getZ(), state); foundationColumns, position.getX(), position.getY(), position.getZ(), state,
supportNonOccluding);
if (marker != null && shouldWriteStructureMarker(state)) { if (marker != null && shouldWriteStructureMarker(state)) {
writer.setData(position.getX(), position.getY(), position.getZ(), marker); writer.setData(position.getX(), position.getY(), position.getZ(), marker);
} }
@@ -13,8 +13,9 @@ final class StructureFoundationPlanner {
} }
static void recordBaseCell(Long2IntOpenHashMap columns, int x, int y, int z, static void recordBaseCell(Long2IntOpenHashMap columns, int x, int y, int z,
PlatformBlockState state) { PlatformBlockState state, boolean supportNonOccluding) {
if (columns == null || state == null || !state.isOccluding()) { if (columns == null || state == null
|| !(supportNonOccluding ? state.isSolid() : state.isOccluding())) {
return; return;
} }
long columnKey = pack(x, z); long columnKey = pack(x, z);
@@ -24,6 +24,7 @@ import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDepositGenerator; import art.arcane.iris.engine.object.IrisDepositGenerator;
import art.arcane.iris.engine.object.IrisDepositVariant; import art.arcane.iris.engine.object.IrisDepositVariant;
import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
import art.arcane.iris.engine.object.IrisObject; import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisProceduralBlocks; import art.arcane.iris.engine.object.IrisProceduralBlocks;
import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.engine.object.IrisRegion;
@@ -87,6 +88,7 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
} }
public void generate(IrisDepositGenerator k, MantleChunk chunk, Hunk<PlatformBlockState> data, RNG rng, int cx, int cz, boolean safe, HeightMap he, ChunkContext context) { public void generate(IrisDepositGenerator k, MantleChunk chunk, Hunk<PlatformBlockState> data, RNG rng, int cx, int cz, boolean safe, HeightMap he, ChunkContext context) {
IrisDimensionCarvingResolver.State carvingState = new IrisDimensionCarvingResolver.State();
if (k.getSpawnChance() < rng.d()) if (k.getSpawnChance() < rng.d())
return; return;
@@ -123,6 +125,29 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
if (y > k.getMaxHeight() || y < k.getMinHeight() || y > height - 2) if (y > k.getMaxHeight() || y < k.getMinHeight() || y > height - 2)
continue; continue;
if (k.isOre(getData())) {
IrisBiome depositBiome = getEngine().getCaveBiome(
(cx << 4) + x, y, (cz << 4) + z, carvingState);
if (depositBiome != null) {
double frequencyMultiplier = depositBiome.getOreDepositFrequencyMultiplier();
if (frequencyMultiplier < 1D
&& !passesOreFrequency(frequencyMultiplier, rng.d())) {
continue;
}
double sizeMultiplier = depositBiome.getOreDepositSizeMultiplier();
if (sizeMultiplier != 1D) {
IrisObject scaledClump = k.getClump(getEngine(), rng, getData(), sizeMultiplier);
int scaledDimension = scaledClump.getW();
x = clampDepositCenter(x, scaledDimension, 16);
y = clampDepositCenter(y, scaledDimension, getEngine().getHeight());
z = clampDepositCenter(z, scaledDimension, 16);
clump = scaledClump;
}
}
}
IrisDimension dimension = getDimension(); IrisDimension dimension = getDimension();
for (art.arcane.iris.util.common.math.IrisBlockVector j : clump.getBlocks().keys()) { for (art.arcane.iris.util.common.math.IrisBlockVector j : clump.getBlocks().keys()) {
@@ -148,7 +173,8 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
if (chunk.get(nx, ny, nz, MatterCavern.class) == null) { if (chunk.get(nx, ny, nz, MatterCavern.class) == null) {
PlatformBlockState ore = clump.getBlocks().get(j); PlatformBlockState ore = clump.getBlocks().get(j);
PlatformBlockState remapped = resolveDepositVariant(cx, cz, nx, ny, nz, ore, dimension, context); PlatformBlockState remapped = resolveDepositVariant(
cx, cz, nx, ny, nz, ore, dimension, context, carvingState);
PlatformBlockState finalBlock = remapped != null PlatformBlockState finalBlock = remapped != null
? remapped ? remapped
: B.toDeepSlateOre(current, ore); : B.toDeepSlateOre(current, ore);
@@ -177,12 +203,22 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
return state != null && !state.isAir() && !state.isFluid(); return state != null && !state.isAir() && !state.isFluid();
} }
private PlatformBlockState resolveDepositVariant(int cx, int cz, int nx, int localY, int nz, PlatformBlockState ore, IrisDimension dimension, ChunkContext context) { static boolean passesOreFrequency(double multiplier, double sample) {
return multiplier >= 1D || sample < Math.max(0D, multiplier);
}
static int clampDepositCenter(int center, int dimension, int limit) {
int minimum = dimension / 2;
int maximum = (int) (limit - dimension / 2D);
return Math.max(minimum, Math.min(center, maximum));
}
private PlatformBlockState resolveDepositVariant(int cx, int cz, int nx, int localY, int nz, PlatformBlockState ore, IrisDimension dimension, ChunkContext context, IrisDimensionCarvingResolver.State carvingState) {
int worldX = (cx << 4) + nx; int worldX = (cx << 4) + nx;
int worldZ = (cz << 4) + nz; int worldZ = (cz << 4) + nz;
int worldY = absoluteWorldY(getEngine().getMinHeight(), localY); int worldY = absoluteWorldY(getEngine().getMinHeight(), localY);
IrisBiome biome = getEngine().getBiome(worldX, localY, worldZ); IrisBiome biome = getEngine().getCaveBiome(worldX, localY, worldZ, carvingState);
if (biome != null) { if (biome != null) {
PlatformBlockState match = matchDepositVariant(biome.getDepositVariants(), ore, worldY); PlatformBlockState match = matchDepositVariant(biome.getDepositVariants(), ore, worldY);
if (match != null) { if (match != null) {
@@ -205,6 +205,14 @@ public class IrisBiome extends IrisRegistrant implements IRare {
@ArrayType(min = 1, type = IrisDepositGenerator.class) @ArrayType(min = 1, type = IrisDepositGenerator.class)
@Desc("Define biome deposit generators that add onto the existing regional and global deposit generators") @Desc("Define biome deposit generators that add onto the existing regional and global deposit generators")
private KList<IrisDepositGenerator> deposits = new KList<>(); private KList<IrisDepositGenerator> deposits = new KList<>();
@MinNumber(0)
@MaxNumber(1)
@Desc("Multiplier applied to the frequency of every ore deposit whose center is inside this biome. A value of 0.4 keeps 40% of ore veins while leaving non-ore deposits unchanged.")
private double oreDepositFrequencyMultiplier = 1D;
@MinNumber(0.01)
@MaxNumber(16)
@Desc("Multiplier applied to the block count of every ore deposit whose center is inside this biome. Non-ore deposits are unchanged.")
private double oreDepositSizeMultiplier = 1D;
@ArrayType(min = 1, type = IrisDepositVariant.class) @ArrayType(min = 1, type = IrisDepositVariant.class)
@Desc("Deposit ore remap rules scoped to this biome. Each entry declares a vertical band and a source->replacement block id map. Applied before regional and dimension rules; first matching biome rule wins.") @Desc("Deposit ore remap rules scoped to this biome. Each entry declares a vertical band and a source->replacement block id map. Applied before regional and dimension rules; first matching biome rule wins.")
private KList<IrisDepositVariant> depositVariants = new KList<>(); private KList<IrisDepositVariant> depositVariants = new KList<>();
@@ -27,16 +27,18 @@ import art.arcane.iris.engine.object.annotations.MaxNumber;
import art.arcane.iris.engine.object.annotations.MinNumber; import art.arcane.iris.engine.object.annotations.MinNumber;
import art.arcane.iris.engine.object.annotations.Required; import art.arcane.iris.engine.object.annotations.Required;
import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.engine.object.annotations.Snippet;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.math.IrisBlockVector; import art.arcane.iris.util.common.math.IrisBlockVector;
import art.arcane.iris.util.common.math.Vector3i; import art.arcane.iris.util.common.math.Vector3i;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KSet; import art.arcane.volmlib.util.collection.KSet;
import art.arcane.volmlib.util.math.BlockPosition; import art.arcane.volmlib.util.math.BlockPosition;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import art.arcane.iris.spi.PlatformBlockState;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
@Snippet("deposit") @Snippet("deposit")
@@ -46,8 +48,10 @@ import lombok.experimental.Accessors;
@Desc("Creates ore & other block deposits underground") @Desc("Creates ore & other block deposits underground")
@Data @Data
public class IrisDepositGenerator { public class IrisDepositGenerator {
private final transient AtomicCache<KList<IrisObject>> objects = new AtomicCache<>(); private final transient ConcurrentMap<ClumpCacheKey, KList<IrisObject>> objects = new ConcurrentHashMap<>();
private final transient AtomicCache<KList<PlatformBlockState>> blockData = new AtomicCache<>(); private final transient AtomicCache<KList<PlatformBlockState>> blockData = new AtomicCache<>();
private final transient AtomicCache<Boolean> ore = new AtomicCache<>();
private final transient ConcurrentMap<ClumpCacheKey, KList<IrisObject>> scaledObjects = new ConcurrentHashMap<>();
@Required @Required
@MinNumber(0) @MinNumber(0)
@MaxNumber(8192) // TODO: WARNING HEIGHT @MaxNumber(8192) // TODO: WARNING HEIGHT
@@ -98,13 +102,14 @@ public class IrisDepositGenerator {
private boolean replaceBedrock = false; private boolean replaceBedrock = false;
public IrisObject getClump(Engine engine, RNG rng, IrisData rdata) { public IrisObject getClump(Engine engine, RNG rng, IrisData rdata) {
KList<IrisObject> objects = this.objects.aquire(() -> ClumpCacheKey cacheKey = new ClumpCacheKey(engine.getSeedManager().getDeposit(), minSize, maxSize);
{ KList<IrisObject> objects = this.objects.computeIfAbsent(cacheKey, key -> {
RNG rngv = new RNG(engine.getSeedManager().getDeposit() + hashCode()); RNG rngv = new RNG(key.depositSeed() + hashCode());
KList<IrisObject> objectsf = new KList<>(); KList<IrisObject> objectsf = new KList<>();
for (int i = 0; i < varience; i++) { for (int i = 0; i < varience; i++) {
objectsf.add(generateClumpObject(rngv.nextParallelRNG(2349 * i + 3598), rdata)); objectsf.add(generateClumpObject(
rngv.nextParallelRNG(2349 * i + 3598), rdata, key.minSize(), key.maxSize()));
} }
return objectsf; return objectsf;
@@ -112,12 +117,40 @@ public class IrisDepositGenerator {
return objects.get(rng.i(0, objects.size())); return objects.get(rng.i(0, objects.size()));
} }
public IrisObject getClump(Engine engine, RNG rng, IrisData rdata, double sizeMultiplier) {
if (sizeMultiplier == 1D) {
return getClump(engine, rng, rdata);
}
int scaledMinSize = scaledDepositSize(minSize, sizeMultiplier);
int scaledMaxSize = scaledDepositSize(maxSize, sizeMultiplier);
ClumpCacheKey cacheKey = new ClumpCacheKey(
engine.getSeedManager().getDeposit(), scaledMinSize, scaledMaxSize);
KList<IrisObject> objects = scaledObjects.computeIfAbsent(cacheKey, key -> {
long sizeSeed = ((long) key.minSize() << 32) ^ (key.maxSize() & 0xffffffffL);
RNG rngv = new RNG(key.depositSeed() + hashCode() + sizeSeed);
KList<IrisObject> generated = new KList<>();
for (int i = 0; i < varience; i++) {
generated.add(generateClumpObject(
rngv.nextParallelRNG(2349 * i + 3598), rdata, key.minSize(), key.maxSize()));
}
return generated;
});
return objects.get(rng.i(0, objects.size()));
}
public int getMaxDimension() { public int getMaxDimension() {
return Math.min(11, (int) Math.ceil(Math.cbrt(maxSize))); return Math.min(11, (int) Math.ceil(Math.cbrt(maxSize)));
} }
private IrisObject generateClumpObject(RNG rngv, IrisData rdata) { static int scaledDepositSize(int size, double multiplier) {
int s = rngv.i(minSize, maxSize + 1); return Math.max(0, Math.min(8192, (int) Math.round(size * multiplier)));
}
private IrisObject generateClumpObject(RNG rngv, IrisData rdata, int clumpMinSize, int clumpMaxSize) {
int s = rngv.i(clumpMinSize, clumpMaxSize + 1);
if (s == 1) { if (s == 1) {
IrisObject o = new IrisObject(1, 1, 1); IrisObject o = new IrisObject(1, 1, 1);
Vector3i center = o.getCenter(); Vector3i center = o.getCenter();
@@ -184,4 +217,19 @@ public class IrisDepositGenerator {
return blockData; return blockData;
}); });
} }
public boolean isOre(IrisData rdata) {
return ore.aquire(() -> {
for (PlatformBlockState block : getBlockData(rdata)) {
if (block.isOre()) {
return true;
}
}
return false;
});
}
record ClumpCacheKey(long depositSeed, int minSize, int maxSize) {
}
} }
@@ -69,6 +69,7 @@ import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
@@ -100,7 +101,7 @@ public class IrisObject extends IrisRegistrant {
protected transient volatile boolean smartBored = false; protected transient volatile boolean smartBored = false;
@Setter @Setter
protected transient AtomicCache<AxisAlignedBB> aabb = new AtomicCache<>(); protected transient AtomicCache<AxisAlignedBB> aabb = new AtomicCache<>();
private transient final AtomicCache<Boolean> treeBlockPresence = new AtomicCache<>(); private transient final AtomicCache<KList<IrisBlockVector>> surfaceSupportOffsets = new AtomicCache<>();
@Getter @Getter
private VectorMap<PlatformBlockState> blocks; private VectorMap<PlatformBlockState> blocks;
@Getter @Getter
@@ -326,7 +327,7 @@ public class IrisObject extends IrisRegistrant {
} }
public void readLegacy(InputStream in) throws IOException { public void readLegacy(InputStream in) throws IOException {
treeBlockPresence.reset(); surfaceSupportOffsets.reset();
DataInputStream din = new DataInputStream(in); DataInputStream din = new DataInputStream(in);
this.w = din.readInt(); this.w = din.readInt();
this.h = din.readInt(); this.h = din.readInt();
@@ -358,7 +359,7 @@ public class IrisObject extends IrisRegistrant {
} }
public void read(InputStream in) throws Throwable { public void read(InputStream in) throws Throwable {
treeBlockPresence.reset(); surfaceSupportOffsets.reset();
DataInputStream din = new DataInputStream(in); DataInputStream din = new DataInputStream(in);
this.w = din.readInt(); this.w = din.readInt();
this.h = din.readInt(); this.h = din.readInt();
@@ -602,6 +603,7 @@ public class IrisObject extends IrisRegistrant {
shrinkOffset = offset; shrinkOffset = offset;
blocks = b; blocks = b;
states = s; states = s;
surfaceSupportOffsets.reset();
} }
public void clean() { public void clean() {
@@ -613,6 +615,7 @@ public class IrisObject extends IrisRegistrant {
blocks = d; blocks = d;
states = dx; states = dx;
surfaceSupportOffsets.reset();
} }
public IrisBlockVector getSigned(int x, int y, int z) { public IrisBlockVector getSigned(int x, int y, int z) {
@@ -624,7 +627,7 @@ public class IrisObject extends IrisRegistrant {
} }
public void setUnsigned(int x, int y, int z, PlatformBlockState block) { public void setUnsigned(int x, int y, int z, PlatformBlockState block) {
treeBlockPresence.reset(); surfaceSupportOffsets.reset();
IrisBlockVector v = getSigned(x, y, z); IrisBlockVector v = getSigned(x, y, z);
if (block == null) { if (block == null) {
@@ -646,7 +649,7 @@ public class IrisObject extends IrisRegistrant {
} }
public void setUnsigned(int x, int y, int z, Block block, boolean legacy) { public void setUnsigned(int x, int y, int z, Block block, boolean legacy) {
treeBlockPresence.reset(); surfaceSupportOffsets.reset();
IrisBlockVector v = getSigned(x, y, z); IrisBlockVector v = getSigned(x, y, z);
if (block == null) { if (block == null) {
@@ -932,9 +935,8 @@ public class IrisObject extends IrisRegistrant {
&& !config.isUnderwater() && !config.isUnderwater()
&& !config.isOnwater() && !config.isOnwater()
&& config.getCarvingSupport().supportsSurface() && config.getCarvingSupport().supportsSurface()
&& hasTreeBlocks()
&& IrisSurfaceOpening.isOpen(oplacer, getLoader(), x, z, config.getTranslate(), && IrisSurfaceOpening.isOpen(oplacer, getLoader(), x, z, config.getTranslate(),
config.getRotation(), spinx, spiny, spinz)) { config.getRotation(), spinx, spiny, spinz, getSurfaceSupportOffsets())) {
return -1; return -1;
} }
@@ -1511,16 +1513,32 @@ public class IrisObject extends IrisRegistrant {
}; };
} }
boolean hasTreeBlocks() { KList<IrisBlockVector> getSurfaceSupportOffsets() {
Boolean present = treeBlockPresence.aquire(() -> { return surfaceSupportOffsets.aquire(() -> {
readLock.lock(); readLock.lock();
try { try {
return IrisSurfaceOpening.containsTreeBlocks(blocks.values()); int lowestY = Integer.MAX_VALUE;
KList<IrisBlockVector> offsets = new KList<>();
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : blocks) {
PlatformBlockState state = entry.getValue();
if (state == null || !state.isSolid() || state.isFoliage()) {
continue;
}
IrisBlockVector position = entry.getKey();
int blockY = position.getBlockY();
if (blockY < lowestY) {
lowestY = blockY;
offsets.clear();
}
if (blockY == lowestY) {
offsets.add(position.clone());
}
}
return offsets;
} finally { } finally {
readLock.unlock(); readLock.unlock();
} }
}); });
return Boolean.TRUE.equals(present);
} }
private boolean isCarvedCaveAnchor(IObjectPlacer placer, int x, int y, int z) { private boolean isCarvedCaveAnchor(IObjectPlacer placer, int x, int y, int z) {
@@ -1584,6 +1602,7 @@ public class IrisObject extends IrisRegistrant {
blocks = d; blocks = d;
states = dx; states = dx;
surfaceSupportOffsets.reset();
shrinkwrap(); shrinkwrap();
writeLock.unlock(); writeLock.unlock();
} }
@@ -1713,6 +1732,7 @@ public class IrisObject extends IrisRegistrant {
} }
blocks = b; blocks = b;
surfaceSupportOffsets.reset();
writeLock.unlock(); writeLock.unlock();
} }
@@ -1744,6 +1764,7 @@ public class IrisObject extends IrisRegistrant {
} }
blocks = b; blocks = b;
surfaceSupportOffsets.reset();
writeLock.unlock(); writeLock.unlock();
} }
@@ -1779,6 +1800,7 @@ public class IrisObject extends IrisRegistrant {
} }
blocks = b; blocks = b;
surfaceSupportOffsets.reset();
writeLock.unlock(); writeLock.unlock();
} }
@@ -21,4 +21,7 @@ public class IrisStructureStiltSettings {
@Desc("Block palette used for foundation columns.") @Desc("Block palette used for foundation columns.")
private IrisMaterialPalette palette = new IrisMaterialPalette().qclear().qadd("minecraft:cobblestone"); private IrisMaterialPalette palette = new IrisMaterialPalette().qclear().qadd("minecraft:cobblestone");
@Desc("For Iris-authored structures, whether solid partial blocks such as slabs, stairs, and walls may seed foundation columns instead of requiring a fully occluding base block.")
private boolean supportNonOccluding = false;
} }
@@ -1,9 +1,10 @@
package art.arcane.iris.engine.object; package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.math.IrisBlockVector; import art.arcane.iris.util.common.math.IrisBlockVector;
import java.util.List;
final class IrisSurfaceOpening { final class IrisSurfaceOpening {
private static final int SUPPORT_RADIUS = 1; private static final int SUPPORT_RADIUS = 1;
@@ -11,15 +12,37 @@ final class IrisSurfaceOpening {
} }
static boolean isOpen(IObjectPlacer placer, IrisData data, int x, int z, IrisObjectTranslate translate, static boolean isOpen(IObjectPlacer placer, IrisData data, int x, int z, IrisObjectTranslate translate,
IrisObjectRotation rotation, int spinX, int spinY, int spinZ) { IrisObjectRotation rotation, int spinX, int spinY, int spinZ,
IrisBlockVector offset = new IrisBlockVector(0, 0, 0); List<IrisBlockVector> supportOffsets) {
if (translate != null) { if (supportOffsets.isEmpty()) {
offset = rotation == null IrisBlockVector origin = transform(new IrisBlockVector(0, 0, 0), translate, rotation, spinX, spinY, spinZ);
? translate.translate(offset) return isOpenAround(placer, data, x + origin.getBlockX(), z + origin.getBlockZ());
: translate.translate(offset, rotation, spinX, spinY, spinZ);
} }
int centerX = x + offset.getBlockX();
int centerZ = z + offset.getBlockZ(); for (IrisBlockVector supportOffset : supportOffsets) {
IrisBlockVector transformed = transform(supportOffset, translate, rotation, spinX, spinY, spinZ);
if (isOpenAround(placer, data, x + transformed.getBlockX(), z + transformed.getBlockZ())) {
return true;
}
}
return false;
}
private static IrisBlockVector transform(IrisBlockVector offset, IrisObjectTranslate translate,
IrisObjectRotation rotation, int spinX, int spinY, int spinZ) {
IrisBlockVector transformed = offset.clone();
if (rotation != null) {
transformed = rotation.rotate(transformed, spinX, spinY, spinZ);
}
if (translate == null) {
return transformed;
}
return rotation == null
? translate.translate(transformed)
: translate.translate(transformed, rotation, spinX, spinY, spinZ);
}
private static boolean isOpenAround(IObjectPlacer placer, IrisData data, int centerX, int centerZ) {
for (int dx = -SUPPORT_RADIUS; dx <= SUPPORT_RADIUS; dx++) { for (int dx = -SUPPORT_RADIUS; dx <= SUPPORT_RADIUS; dx++) {
for (int dz = -SUPPORT_RADIUS; dz <= SUPPORT_RADIUS; dz++) { for (int dz = -SUPPORT_RADIUS; dz <= SUPPORT_RADIUS; dz++) {
int sampleX = centerX + dx; int sampleX = centerX + dx;
@@ -32,13 +55,4 @@ final class IrisSurfaceOpening {
} }
return false; return false;
} }
static boolean containsTreeBlocks(Iterable<PlatformBlockState> states) {
for (PlatformBlockState state : states) {
if (state != null && state.isTreeBlock()) {
return true;
}
}
return false;
}
} }
@@ -158,6 +158,7 @@ public class SchemaBuilderParityTest {
assertTrue(properties.has("distribution")); assertTrue(properties.has("distribution"));
assertEquals("object", stilt.getString("type")); assertEquals("object", stilt.getString("type"));
assertTrue(stiltProperties.has("palette")); assertTrue(stiltProperties.has("palette"));
assertTrue(stiltProperties.has("supportNonOccluding"));
assertEquals(1, maxDepth.getInt("minimum")); assertEquals(1, maxDepth.getInt("minimum"));
assertEquals(4064, maxDepth.getInt("maximum")); assertEquals(4064, maxDepth.getInt("maximum"));
assertFalse(properties.has("rotation")); assertFalse(properties.has("rotation"));
@@ -2,6 +2,7 @@ package art.arcane.iris.engine.decorator;
import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBlockData;
import art.arcane.iris.engine.object.IrisDecorationPart; import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator; import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.spi.PlatformBlockState;
@@ -378,6 +379,31 @@ public class DecoratorCoreTest {
assertSame(upper, output.get(0, 2, 0)); assertSame(upper, output.get(0, 2, 0));
} }
@Test
public void forcedSurfaceBlockStillPlacesDecorantAboveIt() {
IrisDecorator decorator = mock(IrisDecorator.class);
IrisBlockData forcedBlock = mock(IrisBlockData.class);
IrisData data = mock(IrisData.class);
PlatformBlockState support = sturdyState();
PlatformBlockState farmland = sturdyState();
PlatformBlockState air = airState();
PlatformBlockState wheat = mock(PlatformBlockState.class);
when(wheat.key()).thenReturn("minecraft:wheat[age=7]");
when(decorator.getForceBlock()).thenReturn(forcedBlock);
when(forcedBlock.getBlockData(data)).thenReturn(farmland);
when(decorator.pickBlockData(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(wheat);
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(1, 3, 1);
output.set(0, 0, 0, support);
output.set(0, 1, 0, air);
DecoratorCore.placeSurfaceSingle(
decorator, 0, 0, 0, 0, 0, output, new RNG(1L), data, false, false, null);
assertSame(farmland, output.get(0, 0, 0));
assertSame(wheat, output.get(0, 1, 0));
}
private PlatformBlockState airState() { private PlatformBlockState airState() {
PlatformBlockState air = mock(PlatformBlockState.class); PlatformBlockState air = mock(PlatformBlockState.class);
when(air.isAir()).thenReturn(true); when(air.isAir()).thenReturn(true);
@@ -15,26 +15,52 @@ import static org.mockito.Mockito.when;
public class StructureFoundationPlannerTest { public class StructureFoundationPlannerTest {
@Test @Test
public void recordsLowestPlacedOccludingCellAcrossTheAssembledFootprint() { public void recordsLowestPlacedOccludingCellByDefault() {
PlatformBlockState solid = mock(PlatformBlockState.class); PlatformBlockState solid = mock(PlatformBlockState.class);
when(solid.isOccluding()).thenReturn(true); when(solid.isOccluding()).thenReturn(true);
when(solid.isSolid()).thenReturn(true);
PlatformBlockState nonOccludingSolid = mock(PlatformBlockState.class);
when(nonOccludingSolid.isSolid()).thenReturn(true);
when(nonOccludingSolid.isOccluding()).thenReturn(false);
PlatformBlockState decoration = mock(PlatformBlockState.class); PlatformBlockState decoration = mock(PlatformBlockState.class);
when(decoration.isOccluding()).thenReturn(false); when(decoration.isOccluding()).thenReturn(false);
Long2IntOpenHashMap columns = new Long2IntOpenHashMap(); Long2IntOpenHashMap columns = new Long2IntOpenHashMap();
StructureFoundationPlanner.recordBaseCell(columns, 100, 48, 200, decoration); StructureFoundationPlanner.recordBaseCell(columns, 100, 48, 200, decoration, false);
StructureFoundationPlanner.recordBaseCell(columns, 100, 49, 200, solid); StructureFoundationPlanner.recordBaseCell(columns, 100, 49, 200, solid, false);
StructureFoundationPlanner.recordBaseCell(columns, 100, 50, 200, solid); StructureFoundationPlanner.recordBaseCell(columns, 100, 50, 200, solid, false);
StructureFoundationPlanner.recordBaseCell(columns, 100, 45, 200, solid); StructureFoundationPlanner.recordBaseCell(columns, 100, 45, 200, solid, false);
StructureFoundationPlanner.recordBaseCell(columns, -17, 60, -33, solid); StructureFoundationPlanner.recordBaseCell(columns, -17, 60, -33, solid, false);
StructureFoundationPlanner.recordBaseCell(columns, 12, 41, 44, solid, false);
StructureFoundationPlanner.recordBaseCell(columns, 12, 36, 44, nonOccludingSolid, false);
assertEquals(2, columns.size()); assertEquals(3, columns.size());
assertEquals(45, columns.get(StructureFoundationPlanner.pack(100, 200))); assertEquals(45, columns.get(StructureFoundationPlanner.pack(100, 200)));
assertEquals(60, columns.get(StructureFoundationPlanner.pack(-17, -33))); assertEquals(60, columns.get(StructureFoundationPlanner.pack(-17, -33)));
assertEquals(41, columns.get(StructureFoundationPlanner.pack(12, 44)));
assertEquals(-17, StructureFoundationPlanner.unpackX(StructureFoundationPlanner.pack(-17, -33))); assertEquals(-17, StructureFoundationPlanner.unpackX(StructureFoundationPlanner.pack(-17, -33)));
assertEquals(-33, StructureFoundationPlanner.unpackZ(StructureFoundationPlanner.pack(-17, -33))); assertEquals(-33, StructureFoundationPlanner.unpackZ(StructureFoundationPlanner.pack(-17, -33)));
} }
@Test
public void recordsSolidNonOccludingBaseWhenEnabled() {
PlatformBlockState partialStructureBlock = mock(PlatformBlockState.class);
when(partialStructureBlock.isSolid()).thenReturn(true);
when(partialStructureBlock.isOccluding()).thenReturn(false);
PlatformBlockState fullStructureBlock = mock(PlatformBlockState.class);
when(fullStructureBlock.isSolid()).thenReturn(true);
when(fullStructureBlock.isOccluding()).thenReturn(true);
Long2IntOpenHashMap columns = new Long2IntOpenHashMap();
StructureFoundationPlanner.recordBaseCell(
columns, 12, 41, 44, fullStructureBlock, true);
StructureFoundationPlanner.recordBaseCell(
columns, 12, 36, 44, partialStructureBlock, true);
assertEquals(1, columns.size());
assertEquals(36, columns.get(StructureFoundationPlanner.pack(12, 44)));
}
@Test @Test
public void scansThroughAirAndFluidUntilSolidGround() { public void scansThroughAirAndFluidUntilSolidGround() {
int groundY = StructureFoundationPlanner.findGroundY(10, 10, 0, y -> y == 3); int groundY = StructureFoundationPlanner.findGroundY(10, 10, 0, y -> y == 3);
@@ -53,4 +53,21 @@ public class IrisDepositModifierContainmentTest {
assertFalse(IrisDepositModifier.canReplaceDepositTarget(fluid)); assertFalse(IrisDepositModifier.canReplaceDepositTarget(fluid));
assertFalse(IrisDepositModifier.canReplaceDepositTarget(null)); assertFalse(IrisDepositModifier.canReplaceDepositTarget(null));
} }
@Test
public void oreFrequencyMultiplierKeepsConfiguredShareOfVeins() {
assertTrue(IrisDepositModifier.passesOreFrequency(0.4D, 0.399D));
assertFalse(IrisDepositModifier.passesOreFrequency(0.4D, 0.4D));
assertTrue(IrisDepositModifier.passesOreFrequency(1D, 0.999D));
assertFalse(IrisDepositModifier.passesOreFrequency(0D, 0D));
}
@Test
public void largerVeinsRemainCenteredAndInsideTheChunk() {
assertEquals(6, IrisDepositModifier.clampDepositCenter(6, 5, 16));
assertEquals(2, IrisDepositModifier.clampDepositCenter(1, 5, 16));
assertEquals(13, IrisDepositModifier.clampDepositCenter(14, 5, 16));
assertEquals(2, IrisDepositModifier.clampDepositCenter(1, 4, 16));
assertEquals(14, IrisDepositModifier.clampDepositCenter(15, 4, 16));
}
} }
@@ -0,0 +1,64 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.spi.PlatformBlockState;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisDepositTuningTest {
@Test
public void depositSizesScaleAndRemainWithinSchemaLimit() {
assertEquals(8, IrisDepositGenerator.scaledDepositSize(4, 2D));
assertEquals(16, IrisDepositGenerator.scaledDepositSize(8, 2D));
assertEquals(0, IrisDepositGenerator.scaledDepositSize(0, 2D));
assertEquals(8192, IrisDepositGenerator.scaledDepositSize(8192, 2D));
}
@Test
public void biomeOreTuningDefaultsPreserveExistingGeneration() {
IrisBiome biome = new IrisBiome();
assertEquals(1D, biome.getOreDepositFrequencyMultiplier(), 0D);
assertEquals(1D, biome.getOreDepositSizeMultiplier(), 0D);
}
@Test
public void clumpCachesAreScopedByWorldSeedAndSize() {
IrisDepositGenerator.ClumpCacheKey firstWorld =
new IrisDepositGenerator.ClumpCacheKey(41L, 4, 8);
IrisDepositGenerator.ClumpCacheKey secondWorld =
new IrisDepositGenerator.ClumpCacheKey(42L, 4, 8);
IrisDepositGenerator.ClumpCacheKey largerVein =
new IrisDepositGenerator.ClumpCacheKey(41L, 8, 16);
assertNotEquals(firstWorld, secondWorld);
assertNotEquals(firstWorld, largerVein);
assertEquals(firstWorld, new IrisDepositGenerator.ClumpCacheKey(41L, 4, 8));
}
@Test
public void onlyOreDepositPalettesReceiveBiomeTuning() {
IrisData data = mock(IrisData.class);
IrisDepositGenerator oreGenerator = generatorWithState(data, true);
IrisDepositGenerator stoneGenerator = generatorWithState(data, false);
assertTrue(oreGenerator.isOre(data));
assertFalse(stoneGenerator.isOre(data));
}
private IrisDepositGenerator generatorWithState(IrisData data, boolean ore) {
IrisBlockData block = mock(IrisBlockData.class);
PlatformBlockState state = mock(PlatformBlockState.class);
when(block.getBlockData(data)).thenReturn(state);
when(state.isOre()).thenReturn(ore);
IrisDepositGenerator generator = new IrisDepositGenerator();
generator.getPalette().add(block);
return generator;
}
}
@@ -7,6 +7,7 @@ import org.junit.Test;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
public class IrisStructureStiltSettingsTest { public class IrisStructureStiltSettingsTest {
@@ -15,6 +16,7 @@ public class IrisStructureStiltSettingsTest {
IrisStructureStiltSettings settings = new IrisStructureStiltSettings(); IrisStructureStiltSettings settings = new IrisStructureStiltSettings();
assertEquals(64, settings.getMaxDepth()); assertEquals(64, settings.getMaxDepth());
assertFalse(settings.isSupportNonOccluding());
assertNotNull(settings.getPalette()); assertNotNull(settings.getPalette());
assertEquals(1, settings.getPalette().getPalette().size()); assertEquals(1, settings.getPalette().getPalette().size());
assertEquals("minecraft:cobblestone", settings.getPalette().getPalette().get(0).getBlock()); assertEquals("minecraft:cobblestone", settings.getPalette().getPalette().get(0).getBlock());
@@ -3,6 +3,7 @@ package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.math.IrisBlockVector;
import org.junit.Test; import org.junit.Test;
import java.util.HashMap; import java.util.HashMap;
@@ -23,7 +24,8 @@ public class IrisSurfaceOpeningTest {
SurfacePlacer placer = new SurfacePlacer(80); SurfacePlacer placer = new SurfacePlacer(80);
placer.carve(1, 80, 1); placer.carve(1, 80, 1);
assertTrue(IrisSurfaceOpening.isOpen(placer, null, 0, 0, null, null, 0, 0, 0)); assertTrue(IrisSurfaceOpening.isOpen(placer, null, 0, 0, null, null, 0, 0, 0,
List.of(new IrisBlockVector(0, 0, 0))));
assertEquals(9, placer.heightQueries()); assertEquals(9, placer.heightQueries());
} }
@@ -34,7 +36,8 @@ public class IrisSurfaceOpeningTest {
placer.carve(0, 78, 0); placer.carve(0, 78, 0);
placer.carve(0, 77, 0); placer.carve(0, 77, 0);
assertFalse(IrisSurfaceOpening.isOpen(placer, null, 0, 0, null, null, 0, 0, 0)); assertFalse(IrisSurfaceOpening.isOpen(placer, null, 0, 0, null, null, 0, 0, 0,
List.of(new IrisBlockVector(0, 0, 0))));
} }
@Test @Test
@@ -47,7 +50,8 @@ public class IrisSurfaceOpeningTest {
} }
placer.carve(1, 82, 1); placer.carve(1, 82, 1);
assertTrue(IrisSurfaceOpening.isOpen(placer, null, 0, 0, null, null, 0, 0, 0)); assertTrue(IrisSurfaceOpening.isOpen(placer, null, 0, 0, null, null, 0, 0, 0,
List.of(new IrisBlockVector(0, 0, 0))));
} }
@Test @Test
@@ -55,36 +59,63 @@ public class IrisSurfaceOpeningTest {
SurfacePlacer placer = new SurfacePlacer(80); SurfacePlacer placer = new SurfacePlacer(80);
placer.carve(2, 80, 0); placer.carve(2, 80, 0);
assertFalse(IrisSurfaceOpening.isOpen(placer, null, 0, 0, null, null, 0, 0, 0)); assertFalse(IrisSurfaceOpening.isOpen(placer, null, 0, 0, null, null, 0, 0, 0,
List.of(new IrisBlockVector(0, 0, 0))));
} }
@Test @Test
public void placementTranslationMovesSupportStencil() { public void offsetSupportFootprintMovesSupportStencil() {
SurfacePlacer placer = new SurfacePlacer(80); SurfacePlacer placer = new SurfacePlacer(80);
IrisObjectTranslate translate = new IrisObjectTranslate().setX(5).setZ(-3);
placer.carve(5, 80, -3); placer.carve(5, 80, -3);
assertTrue(IrisSurfaceOpening.isOpen(placer, null, 0, 0, translate, null, 0, 0, 0)); assertTrue(IrisSurfaceOpening.isOpen(placer, null, 0, 0, null, null, 0, 0, 0,
List.of(new IrisBlockVector(5, 0, -3))));
} }
@Test @Test
public void placementTranslationUsesTheSameRotationAsObjectBlocks() { public void placementTranslationAndRotationMatchObjectBlocks() {
SurfacePlacer placer = new SurfacePlacer(80); SurfacePlacer placer = new SurfacePlacer(80);
IrisObjectTranslate translate = new IrisObjectTranslate().setX(5).setZ(-3); IrisObjectTranslate translate = new IrisObjectTranslate().setX(5).setZ(-3);
IrisObjectRotation rotation = IrisObjectRotation.of(0, 90, 0); IrisObjectRotation rotation = IrisObjectRotation.of(0, 90, 0);
placer.carve(-3, 80, -5); placer.carve(-3, 80, -7);
assertTrue(IrisSurfaceOpening.isOpen(placer, null, 0, 0, translate, rotation, 0, 0, 0)); assertTrue(IrisSurfaceOpening.isOpen(placer, null, 0, 0, translate, rotation, 0, 0, 0,
List.of(new IrisBlockVector(2, 0, 0))));
} }
@Test @Test
public void treeBlockClassificationDoesNotMatchUnrelatedObjects() { public void emptyFootprintFallsBackToPlacementOrigin() {
PlatformBlockState stone = mock(PlatformBlockState.class); SurfacePlacer placer = new SurfacePlacer(80);
PlatformBlockState log = mock(PlatformBlockState.class); IrisObjectTranslate translate = new IrisObjectTranslate().setX(4).setZ(-2);
when(log.isTreeBlock()).thenReturn(true); placer.carve(4, 80, -2);
assertFalse(IrisSurfaceOpening.containsTreeBlocks(List.of(stone))); assertTrue(IrisSurfaceOpening.isOpen(placer, null, 0, 0, translate, null, 0, 0, 0, List.of()));
assertTrue(IrisSurfaceOpening.containsTreeBlocks(List.of(stone, log))); }
@Test
public void objectCachesItsLowestNonFoliageSupportBlocks() {
PlatformBlockState solid = mock(PlatformBlockState.class);
PlatformBlockState foliage = mock(PlatformBlockState.class);
when(solid.isSolid()).thenReturn(true);
when(foliage.isSolid()).thenReturn(true);
when(foliage.isFoliage()).thenReturn(true);
IrisObject object = new IrisObject(9, 9, 9);
object.setUnsigned(7, 1, 4, solid);
object.setUnsigned(4, 0, 4, foliage);
object.setUnsigned(4, 5, 4, solid);
List<IrisBlockVector> initial = object.getSurfaceSupportOffsets();
assertEquals(1, initial.size());
assertEquals(3, initial.getFirst().getBlockX());
assertEquals(-3, initial.getFirst().getBlockY());
object.setUnsigned(2, 0, 4, solid);
List<IrisBlockVector> updated = object.getSurfaceSupportOffsets();
assertEquals(1, updated.size());
assertEquals(-2, updated.getFirst().getBlockX());
assertEquals(-4, updated.getFirst().getBlockY());
} }
private static final class SurfacePlacer implements IObjectPlacer { private static final class SurfacePlacer implements IObjectPlacer {
+1 -1
View File
@@ -28,4 +28,4 @@ minecraftVersion=26.2
fabricLoaderVersion=0.19.3 fabricLoaderVersion=0.19.3
forgeVersion=26.2-65.0.4 forgeVersion=26.2-65.0.4
neoForgeVersion=26.2.0.12-beta neoForgeVersion=26.2.0.12-beta
volmLibCoordinate=com.github.VolmitSoftware:VolmLib:4c38988b344792a79f925f57f8a675bc85fc1bed volmLibCoordinate=com.github.VolmitSoftware:VolmLib:d9026a7c8ebc391c8109f401ce79a0ce65df3969
+1 -1
View File
@@ -10,7 +10,7 @@ application {
} }
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate') String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
.orElse('com.github.VolmitSoftware:VolmLib:4c38988b344792a79f925f57f8a675bc85fc1bed') .orElse('com.github.VolmitSoftware:VolmLib:d9026a7c8ebc391c8109f401ce79a0ce65df3969')
.get() .get()
dependencies { dependencies {