Added 1.21.1 mod targets

- Build Fabric, Forge and NeoForge per target listed in gradle/mod-targets.json (1.21.1, 26.3)
- Add 1.21.1 adapter source overlays under adapters/*/src/versions/1.21.1
- Add verifyModdedMatrix and per-target Java runtime verification of loader jars
- Re-inject persistent dimensions from the levels-ready hook before the watchdog starts
- Wait for queued chunks and the final save before pregen completes
- Add the 1.21.1 data version and datapack supported_formats range
- Cascade regions whose sea or shore pool loses every biome
- Use Gson calls available on the 1.21.1 runtime
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-16 10:25:19 -04:00
1 parent 0ca33068f3
commit 62062c4a95
58 files changed
+3279 -133

No files matched your search

+59 -15
View File
@@ -22,6 +22,7 @@ import org.gradle.api.file.FileCopyDetails
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.file.FileTree
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.api.attributes.Bundling
import org.gradle.jvm.toolchain.JavaLanguageVersion
import javax.inject.Inject
@@ -38,10 +39,14 @@ buildscript {
plugins {
id 'java'
id 'net.fabricmc.fabric-loom' version '1.17.14'
id 'net.fabricmc.fabric-loom' version '1.17.21' apply false
id 'net.fabricmc.fabric-loom-remap' version '1.17.21' apply false
alias(libs.plugins.shadow)
}
apply from: file('../../gradle/mod-target.gradle')
apply plugin: irisModTarget.fabricRemap ? 'net.fabricmc.fabric-loom-remap' : 'net.fabricmc.fabric-loom'
apply plugin: 'art.arcane.volmit-packaging'
interface IrisFileOperations {
@@ -58,7 +63,7 @@ Properties rootProperties = new Properties()
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.3'))
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(rootProperties.getProperty('fabricLoaderVersion', '0.19.5'))
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(irisModTarget.fabricLoader.toString())
Closure<String> irisArtifactName = { String platform, String targetVersion ->
return "Iris v${project.version} [${platform}] ${targetVersion}.jar"
}
@@ -141,7 +146,11 @@ configurations.create('jij') {
dependencies {
minecraft("com.mojang:minecraft:${minecraftVersion}")
implementation("net.fabricmc:fabric-loader:${fabricLoaderVersion}")
if (irisModTarget.fabricRemap) {
mappings(loom.officialMojangMappings())
}
String modConfiguration = irisModTarget.fabricRemap ? 'modImplementation' : 'implementation'
add(modConfiguration, "net.fabricmc:fabric-loader:${fabricLoaderVersion}")
testImplementation('junit:junit:4.13.2')
testImplementation('org.mockito:mockito-core:5.23.0')
testImplementation(testFixtures('art.arcane:core'))
@@ -168,11 +177,17 @@ dependencies {
libs.fabricApi.keyMapping,
libs.fabricApi.permission
]
if (irisModTarget.fabricModules != null) {
fabricApi = ((Map<String, String>) irisModTarget.fabricModules).collect { String module, String moduleVersion ->
"net.fabricmc.fabric-api:${module}:${moduleVersion}".toString()
}
fabricApi.add("me.lucko:fabric-permissions-api:${irisModTarget.fabricPermissions}".toString())
}
fabricApi.each { Object notation ->
add('jij', notation)
add('implementation', notation)
add(modConfiguration, notation)
}
compileOnly('org.slf4j:slf4j-api:2.0.17')
compileOnly("org.slf4j:slf4j-api:${irisModTarget.slf4j}")
compileOnly(libs.spigot) {
transitive = false
}
@@ -201,21 +216,28 @@ dependencies {
libs.jaxen
]
shared.each { Object notation ->
add('bundle', notation)
add('devBundle', notation)
['bundle', 'devBundle'].each { String configuration ->
add(configuration, notation) {
if (group == 'art.arcane' && name == 'core') {
attributes {
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL))
}
}
}
}
}
}
tasks.named('compileJava', JavaCompile).configure {
options.encoding = 'UTF-8'
options.release.set(25)
options.release.set((int) irisModTarget.java)
}
tasks.named('test').configure {
}
loom {
accessWidenerPath = art.arcane.volmit.packaging.NativeAccessRules.prepare(nativeSourceImplementation, "fabric", layout.buildDirectory.dir("native-access").get().asFile)
accessWidenerPath = art.arcane.volmit.packaging.NativeAccessRules.prepare(irisModTarget.nativeAccess.toString(), "fabric", layout.buildDirectory.dir("native-access").get().asFile)
runs {
client {
runDir(providers.gradleProperty('irisClientRunDir').getOrElse('run'))
@@ -270,6 +292,13 @@ Map<String, String> nestedFabricApiJars = [
'fabric-permission-api-v1' : 'fabric-permission-api-v1.jar'
]
if (irisModTarget.fabricModules != null) {
nestedFabricApiJars = ((Map<String, String>) irisModTarget.fabricModules).collectEntries { String module, String ignored ->
[(module): "${module}.jar".toString()]
}
nestedFabricApiJars.put('fabric-permissions-api', 'fabric-permissions-api.jar')
}
FileTree staleLoaderJars = fileTree(layout.buildDirectory.dir('libs')) {
include('Iris v* [Fabric] *.jar')
include('iris-fabric-*.jar')
@@ -288,6 +317,10 @@ tasks.named('shadowJar', ShadowJar).configure {
archiveFileName.set(irisArtifactName('Fabric', "${minecraftVersion}+${fabricLoaderVersion}"))
configurations = [project.configurations.named('bundle').get()]
mergeServiceFiles()
if ((int) irisModTarget.java == 21) {
exclude('art/arcane/volmlib/util/noise/NativeBillowLoader*.class')
exclude('art/arcane/volmlib/util/noise/BillowBatch*.class')
}
exclude('META-INF/maven/**')
exclude('META-INF/proguard/**')
exclude('META-INF/*.SF')
@@ -346,28 +379,39 @@ tasks.named('shadowJar', ShadowJar).configure {
}
}
// The thin `jar`/`remapJar` outputs carry valid fabric.mod.json metadata but bundle zero
// dependencies. Installing one is an instant NoClassDefFoundError, and they sit in build/libs right
// next to the real artifact. shadowJar is the only shippable Fabric jar.
// The thin jar omits bundled dependencies. Obfuscated targets remap the complete shadow jar.
tasks.named('jar').configure {
enabled = false
}
tasks.matching { it.name == 'remapJar' }.configureEach {
enabled = false
enabled = (boolean) irisModTarget.fabricRemap
if (irisModTarget.fabricRemap) {
dependsOn(tasks.named('shadowJar'))
inputFile.set(tasks.named('shadowJar', ShadowJar).flatMap { it.archiveFile })
archiveFileName.set(irisArtifactName('Fabric', "${minecraftVersion}+${fabricLoaderVersion}"))
addNestedDependencies.set(false)
}
}
if (irisModTarget.fabricRemap) {
tasks.named('shadowJar', ShadowJar).configure {
destinationDirectory.set(layout.buildDirectory.dir('devlibs'))
}
}
tasks.named('assemble').configure {
dependsOn(tasks.named('shadowJar'))
dependsOn(tasks.named(irisModTarget.fabricRemap ? 'remapJar' : 'shadowJar'))
}
pluginPackaging {
artifacts {
create('distribution') {
taskName = 'shadowJar'
taskName = irisModTarget.fabricRemap ? 'remapJar' : 'shadowJar'
policyName = 'Iris Fabric'
prunePrefixes = ['art/arcane/volmlib/']
keepPrefixes = ['art/arcane/volmlib/util/matter/slices/']
}
}
}
apply from: file('../../gradle/modded-version-sources.gradle')
@@ -0,0 +1,78 @@
{
"schemaVersion": 1,
"id": "irisworldgen",
"version": "${version}",
"name": "Iris",
"description": "Iris World Generation Engine (Fabric adapter - native chunk generator, explicit Iris world datapack workflow, engine lifecycle)",
"authors": [
"Arcane Arts (Volmit Software)"
],
"contributors": [
"cyberpwn",
"NextdoorPsycho",
"Vatuu"
],
"contact": {
"homepage": "https://docs.volmit.com/iris/",
"issues": "https://github.com/VolmitSoftware/Iris/issues",
"sources": "https://github.com/VolmitSoftware/Iris"
},
"license": "GPL-3.0",
"icon": "assets/irisworldgen/icon.png",
"environment": "*",
"accessWidener": "volmlib.accesswidener",
"mixins": [
"volmlib.fabric.mixins.json",
"volmlib.entity.mixins.json",
{
"config": "volmlib.client.mixins.json",
"environment": "client"
}
],
"entrypoints": {
"main": [
"art.arcane.iris.fabric.IrisFabricBootstrap"
],
"client": [
"art.arcane.iris.fabric.IrisFabricClient"
]
},
"jars": [
{
"file": "META-INF/jars/fabric-api-base.jar"
},
{
"file": "META-INF/jars/fabric-registry-sync-v0.jar"
},
{
"file": "META-INF/jars/fabric-resource-loader-v0.jar"
},
{
"file": "META-INF/jars/fabric-lifecycle-events-v1.jar"
},
{
"file": "META-INF/jars/fabric-command-api-v2.jar"
},
{
"file": "META-INF/jars/fabric-events-interaction-v0.jar"
},
{
"file": "META-INF/jars/fabric-networking-api-v1.jar"
},
{
"file": "META-INF/jars/fabric-rendering-v1.jar"
},
{
"file": "META-INF/jars/fabric-key-binding-api-v1.jar"
},
{
"file": "META-INF/jars/fabric-permissions-api.jar"
}
],
"depends": {
"fabricloader": ">=0.19.5",
"minecraft": "${minecraftVersion}",
"java": ">=21",
"fabric-permissions-api-v0": "*"
}
}
@@ -0,0 +1,102 @@
package art.arcane.volmlib.nativelib.minecraft26_2.fabric;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeRegistryDefinitions;
import art.arcane.volmlib.nativelib.modded.NativeLoaderOptions;
import com.google.gson.JsonParser;
import com.mojang.serialization.JsonOps;
import com.mojang.serialization.Lifecycle;
import net.minecraft.SharedConstants;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.MappedRegistry;
import net.minecraft.core.RegistrationInfo;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeGenerationSettings;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.carver.ConfiguredWorldCarver;
import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BooleanSupplier;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
public class NativeFabricBiomeDefinitionTest {
private static final String BIOME_REGISTRY = "minecraft:worldgen/biome";
private static final String BIOME_KEY = "iris:biomes/fabric_modified";
private static final String SOURCE_JSON = "{\"has_precipitation\":true,\"temperature\":0.8,\"downfall\":0.4,"
+ "\"effects\":{\"fog_color\":12638463,\"water_color\":4159204,\"water_fog_color\":329011,\"sky_color\":7907327},"
+ "\"carvers\":{},\"features\":[],\"spawners\":{},\"spawn_costs\":{}}";
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void generatedBiomeDefinitionExcludesFabricBiomeModifications() throws Exception {
MappedRegistry<PlacedFeature> features = new MappedRegistry<>(Registries.PLACED_FEATURE, Lifecycle.stable());
Holder.Reference<PlacedFeature> ore = features.register(
ResourceKey.create(Registries.PLACED_FEATURE, ResourceLocation.parse("iris:modded_ore")),
new PlacedFeature(Holder.direct(mock(ConfiguredFeature.class)), List.of()),
RegistrationInfo.BUILT_IN);
features.freeze();
MappedRegistry<ConfiguredWorldCarver<?>> carvers = new MappedRegistry<>(Registries.CONFIGURED_CARVER, Lifecycle.stable());
carvers.freeze();
MappedRegistry<Biome> biomes = new MappedRegistry<>(Registries.BIOME, Lifecycle.stable());
RegistryAccess access = new RegistryAccess.ImmutableRegistryAccess(List.of(biomes, features, carvers));
Biome source = Biome.DIRECT_CODEC.parse(RegistryOps.create(JsonOps.INSTANCE, access),
JsonParser.parseString(SOURCE_JSON)).getOrThrow();
NativeFabricLoader loader = new NativeFabricLoader(new NativeLoaderOptions("irisworldgen", "treefeller",
BooleanSupplier::getAsBoolean));
loader.recordOriginalBiome(source);
Holder.Reference<Biome> biome = biomes.register(
ResourceKey.create(Registries.BIOME, ResourceLocation.parse(BIOME_KEY)), source, RegistrationInfo.BUILT_IN);
biomes.freeze();
addFeatureInPlace(biome.value().getGenerationSettings(), ore);
warmInPlace(biome.value());
NativeRegistryDefinitions definitions = new NativeRegistryDefinitions(() -> access, loader::unmodifiedBiome);
assertEquals(0.1F, biome.value().getBaseTemperature(), 0.0F);
assertTrue(biome.value().getGenerationSettings().features()
.get(GenerationStep.Decoration.UNDERGROUND_ORES.ordinal()).contains(ore));
assertEquals(JsonParser.parseString(definitions.canonicalDefinition(BIOME_REGISTRY, BIOME_KEY, SOURCE_JSON).value()),
JsonParser.parseString(definitions.generatedDefinition(BIOME_REGISTRY, BIOME_KEY).value()));
}
private static void addFeatureInPlace(BiomeGenerationSettings settings, Holder<PlacedFeature> feature)
throws ReflectiveOperationException {
List<HolderSet<PlacedFeature>> steps = new ArrayList<>(settings.features());
while (steps.size() <= GenerationStep.Decoration.UNDERGROUND_ORES.ordinal()) {
steps.add(HolderSet.direct());
}
steps.set(GenerationStep.Decoration.UNDERGROUND_ORES.ordinal(), HolderSet.direct(feature));
Field features = BiomeGenerationSettings.class.getDeclaredField("features");
features.setAccessible(true);
features.set(settings, List.copyOf(steps));
}
private static void warmInPlace(Biome biome) throws ReflectiveOperationException {
Constructor<Biome.ClimateSettings> climate = Biome.ClimateSettings.class.getDeclaredConstructor(
boolean.class, float.class, Biome.TemperatureModifier.class, float.class);
climate.setAccessible(true);
Field climateSettings = Biome.class.getDeclaredField("climateSettings");
climateSettings.setAccessible(true);
climateSettings.set(biome, climate.newInstance(true, 0.1F, Biome.TemperatureModifier.NONE, 0.4F));
}
}
+26 -7
View File
@@ -22,6 +22,7 @@ import org.gradle.api.file.FileCopyDetails
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.file.FileTree
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.api.attributes.Bundling
import org.gradle.jvm.toolchain.JavaLanguageVersion
import javax.inject.Inject
@@ -42,6 +43,8 @@ plugins {
alias(libs.plugins.shadow)
}
apply from: file('../../gradle/mod-target.gradle')
apply plugin: 'art.arcane.volmit-packaging'
interface IrisFileOperations {
@@ -58,7 +61,7 @@ Properties rootProperties = new Properties()
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.3'))
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(rootProperties.getProperty('forgeVersion', '26.3-66.0.8'))
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(irisModTarget.forge.toString())
Closure<String> loaderDisplayVersion = { String loaderVersion ->
String coordinatePrefix = "${minecraftVersion}-"
if (loaderVersion.startsWith(coordinatePrefix)) {
@@ -165,7 +168,7 @@ dependencies {
testImplementation(libs.spigot) {
transitive = false
}
compileOnly('org.slf4j:slf4j-api:2.0.17')
compileOnly("org.slf4j:slf4j-api:${irisModTarget.slf4j}")
compileOnly(libs.spigot) {
transitive = false
}
@@ -200,14 +203,23 @@ dependencies {
libs.jaxen
]
shared.each { Object notation ->
add('bundle', notation)
add('devBundle', notation)
add('runBundle', notation)
['bundle', 'devBundle', 'runBundle'].each { String configuration ->
add(configuration, notation) {
if (group == 'art.arcane' && name == 'core') {
attributes {
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL))
}
}
}
}
}
}
minecraft {
accessTransformer.from(art.arcane.volmit.packaging.NativeAccessRules.prepare(nativeSourceImplementation, "forge", layout.buildDirectory.dir("native-access").get().asFile))
if (irisModTarget.fabricRemap) {
mappings channel: 'official', version: minecraftVersion
}
accessTransformer.from(art.arcane.volmit.packaging.NativeAccessRules.prepare(irisModTarget.nativeAccess.toString(), "forge", layout.buildDirectory.dir("native-access").get().asFile))
runs {
register('client') {
workingDir = layout.projectDirectory.dir('run')
@@ -241,7 +253,8 @@ minecraft {
tasks.named('compileJava', JavaCompile).configure {
options.encoding = 'UTF-8'
options.release.set(25)
options.compilerArgs.addAll(['-Xmaxerrs', '2000'])
options.release.set((int) irisModTarget.java)
}
tasks.named('test').configure {
@@ -280,6 +293,10 @@ tasks.named('shadowJar', ShadowJar).configure {
configurations = [project.configurations.named('bundle').get()]
from(sourceSets.main.output)
mergeServiceFiles()
if ((int) irisModTarget.java == 21) {
exclude('art/arcane/volmlib/util/noise/NativeBillowLoader*.class')
exclude('art/arcane/volmlib/util/noise/BillowBatch*.class')
}
exclude('META-INF/maven/**')
exclude('META-INF/proguard/**')
exclude('META-INF/*.SF')
@@ -345,3 +362,5 @@ pluginPackaging {
}
}
}
apply from: file('../../gradle/modded-version-sources.gradle')
@@ -0,0 +1,29 @@
package art.arcane.iris.forge;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedLifecycleCallbacks;
import art.arcane.iris.modded.service.ModdedTreeFellerService;
import art.arcane.volmlib.nativelib.modded.NativeLoaderOptions;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeChunkGeneratorDefinition;
import art.arcane.volmlib.nativelib.minecraft26_2.forge.NativeForgeLoader;
import art.arcane.volmlib.nativelib.minecraft26_2.forge.NativeForgeBootstrap;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.fml.loading.FMLLoader;
@Mod("irisworldgen")
public final class IrisForgeBootstrap {
public IrisForgeBootstrap(FMLJavaModLoadingContext context) {
NativeForgeLoader loader = new NativeForgeLoader(new NativeLoaderOptions(
"irisworldgen", "treefeller", ModdedTreeFellerService::runBreakProbe));
NativeChunkGeneratorDefinition generator = IrisModdedChunkGenerator.DEFINITION;
ModdedEngineBootstrap.bootCommon(loader, "Forge " + FMLLoader.versionInfo().forgeVersion(),
() -> NativeForgeBootstrap.registerGenerators(context, generator, "block_drops"));
ForgeProtocolNetworking.register();
if (loader.clientEnvironment()) {
IrisForgeClient.init(context);
}
NativeForgeBootstrap.install(context, loader, ModdedLifecycleCallbacks.create());
}
}
@@ -0,0 +1,25 @@
package art.arcane.iris.forge;
import art.arcane.iris.client.IrisClient;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import art.arcane.volmlib.nativelib.minecraft26_2.forge.NativeForgeClientHooks;
import art.arcane.iris.client.IrisClientHud;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolChannel;
public final class IrisForgeClient {
private IrisForgeClient() {
}
public static void init(FMLJavaModLoadingContext context) {
IrisClient.bindSender(IrisForgeClient::sendToServer);
NativeForgeClientHooks.install(context, IrisClientHud.binding(), IrisClient::onWorldJoin, IrisClient::onDisconnect);
}
private static void sendToServer(byte[] frame) {
NativeProtocolChannel channel = ForgeProtocolNetworking.channel();
if (channel == null) {
return;
}
channel.sendToServer(frame);
}
}
@@ -0,0 +1,28 @@
modLoader = "javafml"
loaderVersion = "[52,53)"
license = "GPL-3.0"
issueTrackerURL = "https://github.com/VolmitSoftware/Iris/issues"
[[mods]]
modId = "irisworldgen"
version = "${version}"
displayName = "Iris"
description = "Iris World Generation Engine (Forge adapter - native chunk generator, explicit Iris world datapack workflow, engine lifecycle)"
authors = "Arcane Arts (Volmit Software)"
credits = "cyberpwn, NextdoorPsycho, Vatuu"
displayURL = "https://docs.volmit.com/iris/"
logoFile = "assets/irisworldgen/icon.png"
[[dependencies.irisworldgen]]
modId = "forge"
mandatory = true
versionRange = "[52,53)"
ordering = "NONE"
side = "BOTH"
[[dependencies.irisworldgen]]
modId = "minecraft"
mandatory = true
versionRange = "[${minecraftVersion}]"
ordering = "NONE"
side = "BOTH"
@@ -0,0 +1,100 @@
package art.arcane.volmlib.nativelib.minecraft26_2.forge;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeRegistryDefinitions;
import art.arcane.volmlib.nativelib.modded.NativeLoaderOptions;
import com.google.gson.JsonParser;
import com.mojang.serialization.JsonOps;
import com.mojang.serialization.Lifecycle;
import com.mojang.serialization.MapCodec;
import net.minecraft.SharedConstants;
import net.minecraft.core.Holder;
import net.minecraft.core.MappedRegistry;
import net.minecraft.core.RegistrationInfo;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.MobCategory;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.MobSpawnSettings;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.carver.ConfiguredWorldCarver;
import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
import net.minecraftforge.common.world.BiomeModifier;
import net.minecraftforge.common.world.ModifiableBiomeInfo;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import java.util.function.BooleanSupplier;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
public class NativeForgeBiomeDefinitionTest {
private static final String BIOME_REGISTRY = "minecraft:worldgen/biome";
private static final String BIOME_KEY = "iris:biomes/forge_modified";
private static final String SOURCE_JSON = "{\"has_precipitation\":true,\"temperature\":0.8,\"downfall\":0.4,"
+ "\"effects\":{\"fog_color\":12638463,\"water_color\":4159204,\"water_fog_color\":329011,\"sky_color\":7907327},"
+ "\"carvers\":{},\"features\":[],\"spawners\":{},\"spawn_costs\":{}}";
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void generatedBiomeDefinitionExcludesForgeBiomeModifierOutput() {
MappedRegistry<PlacedFeature> features = new MappedRegistry<>(Registries.PLACED_FEATURE, Lifecycle.stable());
Holder.Reference<PlacedFeature> ore = features.register(
ResourceKey.create(Registries.PLACED_FEATURE, ResourceLocation.parse("iris:modded_ore")),
new PlacedFeature(Holder.direct(mock(ConfiguredFeature.class)), List.of()),
RegistrationInfo.BUILT_IN);
features.freeze();
MappedRegistry<ConfiguredWorldCarver<?>> carvers = new MappedRegistry<>(Registries.CONFIGURED_CARVER, Lifecycle.stable());
carvers.freeze();
MappedRegistry<Biome> biomes = new MappedRegistry<>(Registries.BIOME, Lifecycle.stable());
RegistryAccess access = new RegistryAccess.ImmutableRegistryAccess(List.of(biomes, features, carvers));
Biome source = Biome.DIRECT_CODEC.parse(RegistryOps.create(JsonOps.INSTANCE, access),
JsonParser.parseString(SOURCE_JSON)).getOrThrow();
Holder.Reference<Biome> biome = biomes.register(
ResourceKey.create(Registries.BIOME, ResourceLocation.parse(BIOME_KEY)), source, RegistrationInfo.BUILT_IN);
biomes.freeze();
biome.value().modifiableBiomeInfo().applyBiomeModifiers(biome, List.of(new ModdedContent(ore)));
NativeRegistryDefinitions definitions = new NativeRegistryDefinitions(() -> access,
new NativeForgeLoader(new NativeLoaderOptions("irisworldgen", "treefeller",
BooleanSupplier::getAsBoolean))::unmodifiedBiome);
assertEquals(0.1F, biome.value().getBaseTemperature(), 0.0F);
assertEquals(0x102030, biome.value().getWaterColor() & 0xFFFFFF);
assertTrue(biome.value().getGenerationSettings().hasFeature(ore.value()));
assertTrue(biome.value().getMobSettings().getMobs(MobCategory.MONSTER).unwrap().stream()
.anyMatch(spawn -> spawn.type == EntityType.ZOMBIE));
assertEquals(JsonParser.parseString(definitions.canonicalDefinition(BIOME_REGISTRY, BIOME_KEY, SOURCE_JSON).value()),
JsonParser.parseString(definitions.generatedDefinition(BIOME_REGISTRY, BIOME_KEY).value()));
}
private record ModdedContent(Holder<PlacedFeature> ore) implements BiomeModifier {
@Override
public void modify(Holder<Biome> biome, Phase phase, ModifiableBiomeInfo.BiomeInfo.Builder builder) {
if (phase != Phase.ADD) {
return;
}
builder.getClimateSettings().setTemperature(0.1F);
builder.getSpecialEffects().waterColor(0x102030);
builder.getMobSpawnSettings().addSpawn(MobCategory.MONSTER, new MobSpawnSettings.SpawnerData(EntityType.ZOMBIE, 5, 1, 2));
builder.getGenerationSettings().addFeature(GenerationStep.Decoration.UNDERGROUND_ORES, ore);
}
@Override
public MapCodec<? extends BiomeModifier> codec() {
throw new UnsupportedOperationException();
}
}
}
@@ -0,0 +1,54 @@
package art.arcane.volmlib.nativelib.minecraft26_2.forge;
import art.arcane.volmlib.nativelib.modded.NativeLoaderOptions;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.entity.MobSpawnType;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.level.ServerLevelAccessor;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.entity.living.MobSpawnEvent;
import net.minecraftforge.eventbus.api.Event;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class NativeForgeSpawnPositionTest {
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void chunkGenerationSpawnsFireThePositionCheckEventAndHonorItsDenial() {
NativeForgeLoader loader = new NativeForgeLoader(
new NativeLoaderOptions("irisworldgen", "treefeller", BooleanSupplier::getAsBoolean));
ServerLevelAccessor level = mock(ServerLevelAccessor.class);
Mob mob = mock(Mob.class);
when(mob.checkSpawnRules(level, MobSpawnType.CHUNK_GENERATION)).thenReturn(true);
when(mob.checkSpawnObstruction(level)).thenReturn(true);
AtomicReference<MobSpawnType> observed = new AtomicReference<>();
Consumer<MobSpawnEvent.PositionCheck> listener = event -> {
observed.set(event.getSpawnType());
event.setResult(Event.Result.DENY);
};
MinecraftForge.EVENT_BUS.addListener(listener);
try {
assertFalse(loader.checkSpawnPosition(mob, level, MobSpawnType.CHUNK_GENERATION));
} finally {
MinecraftForge.EVENT_BUS.unregister(listener);
}
assertEquals(MobSpawnType.CHUNK_GENERATION, observed.get());
assertTrue(loader.checkSpawnPosition(mob, level, MobSpawnType.CHUNK_GENERATION));
}
}
@@ -166,6 +166,12 @@ public final class ModdedEngineBootstrap {
ModdedStartup.prepareForStartup();
}
public static void levelsReady(NativeModdedServer server) {
server.refreshWorlds();
bindWorldGenerators(server);
ModdedStartup.reinjectPersistentDimensions(server);
}
public static void serverStarted(NativeModdedServer server) {
// Prime the off-thread level snapshot before anything can read it; the per-tick refresh in
// ModdedScheduler.tick has not run yet at this point.
@@ -14,6 +14,7 @@ public final class ModdedLifecycleCallbacks {
return new NativeModdedCallbacks(
ModdedEngineBootstrap::serverAboutToStart,
ModdedEngineBootstrap::start,
ModdedEngineBootstrap::levelsReady,
ModdedEngineBootstrap::serverStarted,
ModdedEngineBootstrap::stop,
ModdedEngineBootstrap::stopped,
@@ -45,6 +45,9 @@ public final class ModdedMixinAudit {
private static final AtomicBoolean AUDITED = new AtomicBoolean(false);
static final List<ExpectedMixin> EXPECTED = List.of(
new ExpectedMixin(ENTITY_CONFIG, "ServerLevelsReadyMixin",
NativeMixinTarget.SERVER, "volmlib$levelsReady",
false, NativeMixinFlags::levelsReadyRan),
new ExpectedMixin(ENTITY_CONFIG, "RegistryClientNamesMixin",
NativeMixinTarget.REGISTRY_PACKET, "volmlib$clientRegistryNames",
false, NativeMixinFlags::registryClientNamesRan),
@@ -91,7 +91,6 @@ public final class ModdedStartup {
}
ModdedForcedDatapack.verifyInjected();
ModdedMixinAudit.runOnce();
reinjectPersistentDimensions(server);
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
if (scheduler == null) {
@@ -114,15 +114,9 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
@Override
public void close() {
try {
if (!sync) {
try {
semaphore.tryAcquire(maxInFlight, 5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
ModdedIrisLog.info("Iris modded pregen done: dim={} completed={} peakInFlight={} finalLimit={}",
runtime.worldIdentity(), completed.get(), inFlightPeak.get(), adaptiveLimit.get());
awaitPendingChunks();
ModdedIrisLog.info("Iris modded pregen closing: dim={} completed={} pending={} peakInFlight={} finalLimit={}",
runtime.worldIdentity(), completed.get(), inFlight.get(), inFlightPeak.get(), adaptiveLimit.get());
try {
engine.syncGenerationHistory();
} catch (RuntimeException e) {
@@ -180,6 +174,39 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|| queuedFinalSave.get() != null;
}
private void awaitPendingChunks() {
if (sync || runtime.isServerThread()) {
return;
}
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(timeoutSeconds)
+ TimeUnit.MILLISECONDS.toNanos(FINAL_SAVE_TIMEOUT_MILLIS);
boolean interrupted = false;
try {
while (inFlight.get() > 0) {
if (finalSaveOnServerThread.get() || !runtime.isRunning()) {
return;
}
if (System.nanoTime() >= deadline) {
throw new IllegalStateException("Iris pregen chunk callbacks did not finish for "
+ runtime.worldIdentity() + ": " + inFlight.get() + " pending");
}
try {
synchronized (permitMonitor) {
if (inFlight.get() > 0) {
permitMonitor.wait(FINAL_SAVE_POLL_MILLIS);
}
}
} catch (InterruptedException ignored) {
interrupted = true;
}
}
} finally {
if (interrupted) {
Thread.currentThread().interrupt();
}
}
}
private void saveLevel(boolean wait) {
if (wait) {
saveFinalLevel();
@@ -237,30 +264,36 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
private void awaitFinalSave(FinalSaveRequest request) {
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(FINAL_SAVE_TIMEOUT_MILLIS);
while (true) {
if (deferFinalSaveIfRequested()) {
return;
boolean interrupted = false;
try {
while (true) {
if (deferFinalSaveIfRequested()) {
return;
}
long remainingNanos = deadline - System.nanoTime();
if (remainingNanos <= 0L) {
ModdedIrisLog.warn("Iris pregen level save did not complete in time for {}", runtime.worldIdentity());
return;
}
long waitMillis = Math.max(1L, Math.min(FINAL_SAVE_POLL_MILLIS,
TimeUnit.NANOSECONDS.toMillis(remainingNanos)));
try {
request.completion().get(waitMillis, TimeUnit.MILLISECONDS);
return;
} catch (InterruptedException ignored) {
interrupted = true;
} catch (TimeoutException e) {
continue;
} catch (ExecutionException e) {
Throwable cause = e.getCause() == null ? e : e.getCause();
ModdedIrisLog.error("Iris pregen level save failed for {}", runtime.worldIdentity(), cause);
throw new IllegalStateException("Iris pregen level save failed for "
+ runtime.worldIdentity(), cause);
}
}
long remainingNanos = deadline - System.nanoTime();
if (remainingNanos <= 0L) {
ModdedIrisLog.warn("Iris pregen level save did not complete in time for {}", runtime.worldIdentity());
return;
}
long waitMillis = Math.max(1L, Math.min(FINAL_SAVE_POLL_MILLIS,
TimeUnit.NANOSECONDS.toMillis(remainingNanos)));
try {
request.completion().get(waitMillis, TimeUnit.MILLISECONDS);
return;
} catch (InterruptedException e) {
} finally {
if (interrupted) {
Thread.currentThread().interrupt();
return;
} catch (TimeoutException e) {
continue;
} catch (ExecutionException e) {
Throwable cause = e.getCause() == null ? e : e.getCause();
ModdedIrisLog.error("Iris pregen level save failed for {}", runtime.worldIdentity(), cause);
throw new IllegalStateException("Iris pregen level save failed for "
+ runtime.worldIdentity(), cause);
}
}
}
@@ -0,0 +1,61 @@
package art.arcane.iris.modded;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
import org.junit.After;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
public class ModdedStartupLifecycleTest {
@After
public void resetStartup() {
ModdedStartup.reset();
}
@Test
public void restoresAfterWorldsLoadWithoutRepeatingDuringServerTicks() {
NativeModdedServer server = mock(NativeModdedServer.class);
AtomicBoolean worldsRefreshed = new AtomicBoolean();
when(server.refreshWorlds()).thenAnswer(invocation -> {
worldsRefreshed.set(true);
return Thread.currentThread();
});
when(server.hasPlayerList()).thenReturn(true);
ModdedDimensionRegistryStore.PersistentDimension dimension =
new ModdedDimensionRegistryStore.PersistentDimension("iris:moon", "overworld", "overworld", 1337L);
try (MockedStatic<ModdedStartup> startup = mockStatic(ModdedStartup.class, CALLS_REAL_METHODS);
MockedStatic<ModdedDimensionRegistryStore> store = mockStatic(ModdedDimensionRegistryStore.class);
MockedStatic<ModdedDimensionManager> manager = mockStatic(ModdedDimensionManager.class);
MockedStatic<ModdedForcedDatapack> datapack = mockStatic(ModdedForcedDatapack.class);
MockedStatic<ModdedMixinAudit> audit = mockStatic(ModdedMixinAudit.class);
MockedStatic<ModdedIrisLog> log = mockStatic(ModdedIrisLog.class)) {
startup.when(ModdedStartup::prepareForStartup).thenAnswer(invocation -> null);
store.when(() -> ModdedDimensionRegistryStore.loadForStartup(server)).thenReturn(List.of(dimension));
manager.when(() -> ModdedDimensionManager.restorePersistent(server, "iris:moon", "overworld", "overworld", 1337L))
.thenAnswer(invocation -> {
assertTrue(worldsRefreshed.get());
return null;
});
ModdedLifecycleCallbacks.create().levelsReady().accept(server);
manager.verify(() -> ModdedDimensionManager.restorePersistent(server, "iris:moon", "overworld", "overworld", 1337L));
ModdedStartup.runOnce(server);
ModdedStartup.runOnce(server);
store.verify(() -> ModdedDimensionRegistryStore.loadForStartup(server), times(1));
manager.verify(() -> ModdedDimensionManager.restorePersistent(server, "iris:moon", "overworld", "overworld", 1337L), times(1));
datapack.verify(ModdedForcedDatapack::verifyInjected, times(1));
audit.verify(ModdedMixinAudit::runOnce, times(1));
}
}
}
@@ -3,14 +3,25 @@ package art.arcane.iris.modded.command;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.modded.ModdedGenPool;
import art.arcane.iris.world.pregen.MantleHeapPressure;
import art.arcane.iris.world.pregen.PregenListener;
import art.arcane.volmlib.nativelib.terrain.NativePregenRuntime;
import art.arcane.volmlib.nativelib.terrain.NativePregenRuntime.ChunkLoad;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.MockedStatic;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -20,6 +31,7 @@ import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
@@ -31,6 +43,7 @@ import static org.mockito.Mockito.when;
public class ModdedPregenMethodLifecycleTest {
private MockedStatic<IrisSettings> settings;
private NativePregenRuntime runtime;
private Engine engine;
private ModdedPregenMethod method;
@Before
@@ -40,7 +53,7 @@ public class ModdedPregenMethodLifecycleTest {
settings.when(IrisSettings::get).thenReturn(configured);
runtime = mock(NativePregenRuntime.class);
when(runtime.worldIdentity()).thenReturn("example:terrain");
Engine engine = mock(Engine.class);
engine = mock(Engine.class, RETURNS_DEEP_STUBS);
when(engine.getHeight()).thenReturn(384);
method = new ModdedPregenMethod(new ModdedPregenMethod.Configuration(runtime, engine, true));
}
@@ -82,6 +95,131 @@ public class ModdedPregenMethodLifecycleTest {
verify(runtime, never()).save();
}
@Test
public void finalSaveWaitsForEveryCallbackBeyondFiveSeconds() throws Exception {
CompletableFuture<ChunkLoad> load = new CompletableFuture<>();
CompletableFuture<ChunkLoad> failedLoad = new CompletableFuture<>();
PregenListener listener = mock(PregenListener.class);
ExecutorService executor = Executors.newSingleThreadExecutor();
when(runtime.isRunning()).thenReturn(true);
when(runtime.loadChunk(2, 3)).thenReturn(load);
when(runtime.loadChunk(4, 5)).thenReturn(failedLoad);
doAnswer(call -> {
call.getArgument(0, Runnable.class).run();
return null;
}).when(runtime).execute(any());
try (MockedStatic<MantleHeapPressure> pressure = mockStatic(MantleHeapPressure.class)) {
pressure.when(MantleHeapPressure::memoryBudgetBytes).thenReturn(512L << 20);
method = new ModdedPregenMethod(new ModdedPregenMethod.Configuration(runtime, engine, false));
method.generateChunk(2, 3, listener);
method.generateChunk(4, 5, listener);
CountDownLatch started = new CountDownLatch(1);
Future<?> closing = executor.submit(() -> {
started.countDown();
method.close();
});
assertTrue(started.await(5, TimeUnit.SECONDS));
assertThrows(TimeoutException.class, () -> closing.get(6, TimeUnit.SECONDS));
verify(runtime, never()).save();
verify(engine, never()).syncGenerationHistory();
load.complete(new ChunkLoad(true, null));
assertThrows(TimeoutException.class, () -> closing.get(100, TimeUnit.MILLISECONDS));
verify(runtime, never()).save();
failedLoad.completeExceptionally(new IllegalStateException("chunk failed"));
closing.get(5, TimeUnit.SECONDS);
verify(listener).onChunkGenerated(2, 3);
verify(listener).onChunkFailed(4, 5);
verify(engine).syncGenerationHistory();
verify(runtime).save();
} finally {
load.complete(new ChunkLoad(true, null));
failedLoad.complete(new ChunkLoad(false, "chunk failed"));
executor.shutdownNow();
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
}
}
@Test
public void interruptedCloseDrainsPendingChunksAndWaitsForFinalSave() throws Exception {
CompletableFuture<ChunkLoad> load = new CompletableFuture<>();
ExecutorService executor = Executors.newSingleThreadExecutor();
AtomicReference<Thread> closingThread = new AtomicReference<>();
AtomicReference<Runnable> queuedSave = new AtomicReference<>();
CountDownLatch started = new CountDownLatch(1);
CountDownLatch saveQueued = new CountDownLatch(1);
when(runtime.isRunning()).thenReturn(true);
when(runtime.loadChunk(2, 3)).thenReturn(load);
doAnswer(call -> {
queuedSave.set(call.getArgument(0, Runnable.class));
saveQueued.countDown();
return null;
}).when(runtime).execute(any());
try (MockedStatic<MantleHeapPressure> pressure = mockStatic(MantleHeapPressure.class)) {
pressure.when(MantleHeapPressure::memoryBudgetBytes).thenReturn(256L << 20);
method = new ModdedPregenMethod(new ModdedPregenMethod.Configuration(runtime, engine, false));
method.generateChunk(2, 3, mock(PregenListener.class));
Future<Boolean> closing = executor.submit(() -> {
closingThread.set(Thread.currentThread());
started.countDown();
method.close();
return Thread.currentThread().isInterrupted();
});
assertTrue(started.await(5, TimeUnit.SECONDS));
closingThread.get().interrupt();
assertThrows(TimeoutException.class, () -> closing.get(100, TimeUnit.MILLISECONDS));
verify(runtime, never()).save();
verify(engine, never()).syncGenerationHistory();
load.complete(new ChunkLoad(true, null));
assertTrue(saveQueued.await(5, TimeUnit.SECONDS));
assertThrows(TimeoutException.class, () -> closing.get(100, TimeUnit.MILLISECONDS));
verify(runtime, never()).save();
closingThread.get().interrupt();
assertThrows(TimeoutException.class, () -> closing.get(100, TimeUnit.MILLISECONDS));
queuedSave.get().run();
assertTrue(closing.get(5, TimeUnit.SECONDS));
verify(engine).syncGenerationHistory();
verify(runtime).save();
} finally {
load.complete(new ChunkLoad(true, null));
saveQueued.await(5, TimeUnit.SECONDS);
Runnable pendingSave = queuedSave.get();
if (pendingSave != null) {
pendingSave.run();
}
executor.shutdownNow();
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
}
}
@Test
public void serverShutdownClaimsFinalSaveWithoutWaitingForPendingChunks() throws Exception {
CompletableFuture<ChunkLoad> load = new CompletableFuture<>();
PregenListener listener = mock(PregenListener.class);
ExecutorService executor = Executors.newSingleThreadExecutor();
Thread serverThread = Thread.currentThread();
when(runtime.isServerThread()).thenAnswer(call -> Thread.currentThread() == serverThread);
when(runtime.isRunning()).thenReturn(true);
when(runtime.loadChunk(2, 3)).thenReturn(load);
try (MockedStatic<MantleHeapPressure> pressure = mockStatic(MantleHeapPressure.class)) {
pressure.when(MantleHeapPressure::memoryBudgetBytes).thenReturn(256L << 20);
method = new ModdedPregenMethod(new ModdedPregenMethod.Configuration(runtime, engine, false));
method.generateChunk(2, 3, listener);
Future<?> closing = executor.submit(method::close);
assertThrows(TimeoutException.class, () -> closing.get(100, TimeUnit.MILLISECONDS));
assertTrue(method.deferFinalSaveToServerThread());
closing.get(5, TimeUnit.SECONDS);
verify(runtime, never()).save();
assertTrue(method.hasPendingFinalSave());
method.completeDeferredFinalSave();
verify(runtime).save();
verify(runtime, never()).execute(any());
} finally {
load.complete(new ChunkLoad(true, null));
executor.shutdownNow();
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
}
}
@Test
public void failedFinalSaveStillRestoresTheEmptyServerPauseSetting() {
AtomicInteger pause = new AtomicInteger(60);
@@ -8,6 +8,7 @@ import art.arcane.volmlib.nativelib.terrain.NativeWorld;
import com.mojang.serialization.JsonOps;
import net.minecraft.server.level.ServerChunkCache;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.chunk.ChunkGenerator;
import org.junit.Test;
@@ -66,4 +67,26 @@ public class NativeGeneratorOwnershipTest {
assertNull(NativeWorldGenerators.find(world, IrisModdedChunkGenerator.class));
assertFalse(nativeGenerator.represents(world));
}
@Test
public void protocolLookupTracksInstalledGeneratorAndPolicyOwner() {
IrisModdedChunkGenerator owner = new IrisModdedChunkGenerator(
new NativeGeneratorContext(mock(BiomeSource.class), "example:surface"));
NativeModdedChunkGenerator<?, ?, ?> nativeGenerator = (NativeModdedChunkGenerator<?, ?, ?>) owner.nativeGenerator();
ServerLevel level = mock(ServerLevel.class);
ServerChunkCache chunks = mock(ServerChunkCache.class);
when(level.dimension()).thenReturn(Level.OVERWORLD);
when(level.getChunkSource()).thenReturn(chunks);
when(chunks.getGenerator()).thenReturn(nativeGenerator);
NativeProtocolWorld world = new NativeProtocolWorld(level);
assertSame(owner, world.generator(IrisModdedChunkGenerator.class));
assertSame(nativeGenerator, world.generator(ChunkGenerator.class));
assertNull(world.generator(String.class));
ChunkGenerator replacement = mock(ChunkGenerator.class);
when(chunks.getGenerator()).thenReturn(replacement);
assertNull(world.generator(IrisModdedChunkGenerator.class));
assertSame(replacement, world.generator(ChunkGenerator.class));
}
}
@@ -0,0 +1,141 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import art.arcane.volmlib.nativelib.modded.NativeMixinFlags;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeJigsawBounds;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeMixinTarget;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BooleanSupplier;
/**
* Boot audit for the Iris mixin configs. Mixin registration is per-loader (fabric.mod.json entry, NeoForge
* mods.toml [[mixins]] block, Forge shadowJar MixinConfigs manifest attribute) and a config that never gets
* registered fails silently: no crash, the hooks simply never exist, and entity persistence, custom mob loot
* and the Iris world-type labels quietly stop working.
*
* <p>Application is checked structurally: Mixin transfers an {@code @Inject} handler into the target class,
* so the handler's presence on the target proves the mixin applied. Mixin 0.8.7 renames the transferred
* method to {@code handler$<ids>$<originalName>}, so the declared name is matched as an exact name or as a
* {@code $}-prefixed suffix. Client targets are resolved by name so this class stays free of client
* references and is safe on a dedicated server.
*/
public final class ModdedMixinAudit {
private static final String ENTITY_CONFIG = "volmlib.entity.mixins.json";
private static final String CLIENT_CONFIG = "volmlib.client.mixins.json";
private static final AtomicBoolean AUDITED = new AtomicBoolean(false);
static final List<ExpectedMixin> EXPECTED = List.of(
new ExpectedMixin(ENTITY_CONFIG, "ServerLevelsReadyMixin",
NativeMixinTarget.SERVER, "volmlib$levelsReady",
false, NativeMixinFlags::levelsReadyRan),
new ExpectedMixin(ENTITY_CONFIG, "JigsawPlacementBoundsMixin",
NativeMixinTarget.JIGSAW_PLACEMENT, "volmlib$verticalBounds",
false, NativeJigsawBounds::placementBoundsApplied),
new ExpectedMixin(ENTITY_CONFIG, "RegistryClientNamesMixin",
NativeMixinTarget.REGISTRY_PACKET, "volmlib$clientRegistryNames",
false, NativeMixinFlags::registryClientNamesRan),
new ExpectedMixin(ENTITY_CONFIG, "EntityPersistenceMixin",
NativeMixinTarget.ENTITY, "iris$applyGeneratedPersistence",
false, NativeMixinFlags::entityPersistenceRan),
new ExpectedMixin(ENTITY_CONFIG, "LivingEntityLootMixin",
NativeMixinTarget.LIVING_ENTITY, "iris$replaceBaseLoot",
false, NativeMixinFlags::livingEntityLootRan),
new ExpectedMixin(ENTITY_CONFIG, "MobAwarenessMixin",
NativeMixinTarget.MOB, "iris$tickUnawareMob",
false, NativeMixinFlags::mobAwarenessRan),
new ExpectedMixin(ENTITY_CONFIG, "StructureTemplatePaletteConcurrencyMixin",
NativeMixinTarget.STRUCTURE_PALETTE,
"iris$installConcurrentBlockCache",
false, NativeMixinFlags::structureTemplatePaletteRan),
new ExpectedMixin(ENTITY_CONFIG, "PackRepositorySelectionMixin",
NativeMixinTarget.PACK_REPOSITORY,
"volmlib$keepNativeDatapacksLast",
false, NativeMixinFlags::packSelectionRan),
new ExpectedMixin(CLIENT_CONFIG, "NativeWorldOpenFlowsMixin",
NativeMixinTarget.WORLD_OPEN_FLOWS,
"volmlib$openWorldCheckWorldStemCompatibility",
true, NativeMixinFlags::worldOpenFlowsRan),
new ExpectedMixin(CLIENT_CONFIG, "NativeWorldTypeEntryMixin",
NativeMixinTarget.WORLD_TYPE_ENTRY,
"volmlib$describePreset",
true, NativeMixinFlags::worldTypeEntryRan));
private ModdedMixinAudit() {
}
public static void runOnce() {
if (!AUDITED.compareAndSet(false, true)) {
return;
}
audit(ModdedEngineBootstrap.loader().platformName(),
ModdedEngineBootstrap.loader().clientEnvironment());
}
static void reset() {
AUDITED.set(false);
}
static void audit(String platform, boolean clientEnvironment) {
List<String> missing = new ArrayList<>();
List<String> applied = new ArrayList<>();
for (ExpectedMixin expected : EXPECTED) {
if (expected.clientOnly() && !clientEnvironment) {
continue;
}
if (isApplied(expected)) {
applied.add(expected.mixinName() + (expected.ran().getAsBoolean() ? "" : " (not yet exercised)"));
} else {
missing.add(expected.config() + '/' + expected.mixinName() + " -> " + expected.targetClass());
}
}
if (missing.isEmpty()) {
ModdedIrisLog.info("Iris mixin audit ok on {} ({} dist): {}", platform,
clientEnvironment ? "client" : "server", String.join(", ", applied));
return;
}
ModdedIrisLog.error("===============================================================");
ModdedIrisLog.error("Iris mixin audit FAILED on {} ({} dist): {} of {} expected mixin(s) were not applied.",
platform, clientEnvironment ? "client" : "server", missing.size(),
missing.size() + applied.size());
for (String entry : missing) {
ModdedIrisLog.error(" missing: {}", entry);
}
ModdedIrisLog.error("The mixin config was not registered for this loader (fabric.mod.json mixins, neoforge.mods.toml [[mixins]], forge MixinConfigs manifest attribute).");
ModdedIrisLog.error("Entity persistence, custom mob loot, parallel structure safety, Iris datapack priority, readable biome identifiers, or Iris world-type labels are disabled until this is fixed.");
ModdedIrisLog.error("===============================================================");
}
private static boolean isApplied(ExpectedMixin expected) {
try {
return expected.targetClass().hasInjectedHandler(expected.handlerMethod());
} catch (LinkageError unavailable) {
ModdedIrisLog.warn("Iris mixin audit could not inspect {}", expected.targetClass(), unavailable);
return false;
}
}
record ExpectedMixin(String config, String mixinName, NativeMixinTarget targetClass, String handlerMethod,
boolean clientOnly, BooleanSupplier ran) {
}
}
@@ -0,0 +1,7 @@
{
"pack": {
"description": "Iris client resources and mod data",
"pack_format": 34,
"supported_formats": [34, 48]
}
}
@@ -0,0 +1,145 @@
package art.arcane.iris.modded;
import net.minecraft.resources.ResourceKey;
import net.minecraft.core.registries.Registries;
import net.minecraft.core.RegistrationInfo;
import net.minecraft.core.Registry;
import net.minecraft.core.MappedRegistry;
import com.mojang.serialization.Lifecycle;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedChunkGenerator;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeContainerLoot;
import art.arcane.volmlib.util.math.RNG;
import com.mojang.serialization.Codec;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.IdMapper;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.component.DataComponentMap;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.ProblemReporter;
import net.minecraft.world.Container;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeGenerationSettings;
import net.minecraft.world.level.biome.BiomeSpecialEffects;
import net.minecraft.world.level.biome.MobSpawnSettings;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.BannerBlockEntity;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.ChestBlockEntity;
import net.minecraft.world.level.block.entity.SignBlockEntity;
import net.minecraft.world.level.block.entity.SpawnerBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.PalettedContainer;
import net.minecraft.world.level.chunk.PalettedContainerRO;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.UpgradeData;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class IrisModdedBlockEntityParityTest {
private static RegistryAccess registries;
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
registries = RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY);
}
@Test
public void generatedContainerGetsBlockEntityAndPersistsFilledLoot() {
ProtoChunk chunk = newChunk();
BlockPos position = new BlockPos(3, 70, 5);
chunk.setBlockState(position, Blocks.CHEST.defaultBlockState(), false);
NativeModdedChunkGenerator.createDefaultBlockEntity(chunk, position, Blocks.CHEST.defaultBlockState());
BlockEntity blockEntity = chunk.getBlockEntity(position);
assertNotNull(blockEntity);
assertTrue(blockEntity instanceof Container);
Container container = (Container) blockEntity;
NativeContainerLoot.fillContainer(container, List.of(new ItemStack(Items.DIAMOND)), new RNG(17L), message -> {});
assertEquals(1, countItem(container, Items.DIAMOND));
CompoundTag saved = blockEntity.saveWithoutMetadata(registries);
ChestBlockEntity restored = new ChestBlockEntity(position, Blocks.CHEST.defaultBlockState());
restored.loadWithComponents(saved, registries);
assertEquals(1, countItem(restored, Items.DIAMOND));
}
@Test
public void generatedSignSpawnerAndBannerGetNativeBlockEntities() {
ProtoChunk chunk = newChunk();
BlockState signState = Blocks.OAK_SIGN.defaultBlockState();
BlockState spawnerState = Blocks.SPAWNER.defaultBlockState();
BlockState bannerState = BuiltInRegistries.BLOCK.get(
ResourceLocation.parse("minecraft:white_banner")).defaultBlockState();
BlockPos signPosition = new BlockPos(1, 70, 1);
BlockPos spawnerPosition = new BlockPos(2, 70, 1);
BlockPos bannerPosition = new BlockPos(3, 70, 1);
chunk.setBlockState(signPosition, signState, false);
chunk.setBlockState(spawnerPosition, spawnerState, false);
chunk.setBlockState(bannerPosition, bannerState, false);
NativeModdedChunkGenerator.createDefaultBlockEntity(chunk, signPosition, signState);
NativeModdedChunkGenerator.createDefaultBlockEntity(chunk, spawnerPosition, spawnerState);
NativeModdedChunkGenerator.createDefaultBlockEntity(chunk, bannerPosition, bannerState);
assertTrue(chunk.getBlockEntity(signPosition) instanceof SignBlockEntity);
assertTrue(chunk.getBlockEntity(spawnerPosition) instanceof SpawnerBlockEntity);
assertTrue(chunk.getBlockEntity(bannerPosition) instanceof BannerBlockEntity);
}
private static ProtoChunk newChunk() {
return new ProtoChunk(
new ChunkPos(0, 0),
UpgradeData.EMPTY,
LevelHeightAccessor.create(-64, 384),
biomeRegistry(),
null);
}
private static Registry<Biome> biomeRegistry() {
Biome biome = new Biome.BiomeBuilder()
.hasPrecipitation(false)
.temperature(0.8F)
.downfall(0.4F)
.specialEffects(new BiomeSpecialEffects.Builder().fogColor(0).waterColor(0x3F76E4)
.waterFogColor(0).skyColor(0).build())
.mobSpawnSettings(MobSpawnSettings.EMPTY)
.generationSettings(BiomeGenerationSettings.EMPTY)
.build();
MappedRegistry<Biome> registry = new MappedRegistry<>(Registries.BIOME, Lifecycle.stable());
registry.register(ResourceKey.create(Registries.BIOME, ResourceLocation.withDefaultNamespace("plains")),
biome, RegistrationInfo.BUILT_IN);
return registry.freeze();
}
private static int countItem(Container container, Item item) {
int count = 0;
for (int slot = 0; slot < container.getContainerSize(); slot++) {
ItemStack stack = container.getItem(slot);
if (stack.getItem() == item) {
count += stack.getCount();
}
}
return count;
}
}
@@ -0,0 +1,48 @@
package art.arcane.iris.modded;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeSpawnTableMerger;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.random.WeightedRandomList;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.level.biome.MobSpawnSettings;
import org.junit.Test;
import org.junit.BeforeClass;
import java.util.List;
import static org.junit.Assert.assertEquals;
public class IrisModdedChunkGeneratorSpawnTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void explicitSpawnsReplaceMatchingVanillaTypesAndPreserveOthers() {
MobSpawnSettings.SpawnerData zombie = new MobSpawnSettings.SpawnerData(EntityType.ZOMBIE, 100, 1, 4);
MobSpawnSettings.SpawnerData vanillaSlime = new MobSpawnSettings.SpawnerData(EntityType.SLIME, 1, 1, 1);
MobSpawnSettings.SpawnerData explicitSlime = new MobSpawnSettings.SpawnerData(EntityType.SLIME, 7, 2, 5);
MobSpawnSettings.SpawnerData explicitCow = new MobSpawnSettings.SpawnerData(EntityType.COW, 3, 2, 4);
WeightedRandomList<MobSpawnSettings.SpawnerData> vanilla = WeightedRandomList.create(List.of(
zombie,
vanillaSlime));
WeightedRandomList<MobSpawnSettings.SpawnerData> explicit = WeightedRandomList.create(List.of(
explicitSlime,
explicitCow));
WeightedRandomList<MobSpawnSettings.SpawnerData> merged = NativeSpawnTableMerger.merge(vanilla, explicit);
List<MobSpawnSettings.SpawnerData> entries = merged.unwrap();
assertEquals(3, entries.size());
assertEquals(EntityType.ZOMBIE, entries.get(0).type);
assertEquals(100, entries.get(0).getWeight().asInt());
assertEquals(explicitSlime, entries.get(1));
assertEquals(7, entries.get(1).getWeight().asInt());
assertEquals(explicitCow, entries.get(2));
assertEquals(3, entries.get(2).getWeight().asInt());
}
}
@@ -0,0 +1,134 @@
package art.arcane.iris.modded;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeEntityRuntime;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedPlatformWorld;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedLoader;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeEntitySpawns;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.world.entity.IrisEntity;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ServerChunkCache;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.Difficulty;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.MobSpawnType;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.level.chunk.LevelChunk;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ModdedEntitySpawnDifficultyTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void sharedPlatformSpawnReturnsFalseForPeacefulHostiles() {
ServerLevel level = mock(ServerLevel.class);
when(level.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
ModdedPlatform platform = new ModdedPlatform(mock(NativeModdedLoader.class));
assertFalse(platform.spawnEntity(new ModdedPlatformWorld(level), "minecraft:zombie", 0.5, 80, 0.5));
verify(level, never()).addFreshEntity(any(Entity.class));
verify(level, never()).enabledFeatures();
}
@Test
public void ambientSpawnReturnsNullForPeacefulHostilesBeforeApplyingConfiguration() {
ServerLevel level = mock(ServerLevel.class);
ServerChunkCache chunks = mock(ServerChunkCache.class);
when(level.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
when(level.getChunkSource()).thenReturn(chunks);
when(chunks.getChunkNow(anyInt(), anyInt())).thenReturn(mock(LevelChunk.class));
IrisEntity configured = new IrisEntity().setType("zombie").setSpawnEffectRiseOutOfGround(false);
assertNull(ModdedEntitySpawner.spawn(mock(Engine.class), configured, new NativeEntityRuntime(level), 0, 80, 0, new RNG(1L)));
verify(level, never()).addFreshEntity(any(Entity.class));
verify(level, never()).enabledFeatures();
}
@Test
public void peacefulRejectsNativeHostilesBeforeCreation() {
ServerLevel level = mock(ServerLevel.class);
when(level.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
for (EntityType<?> type : List.of(EntityType.ZOMBIE, EntityType.SKELETON, EntityType.CREEPER,
EntityType.SPIDER, EntityType.ENDERMAN, EntityType.BLAZE, EntityType.GHAST,
EntityType.PHANTOM, EntityType.SLIME, EntityType.MAGMA_CUBE, EntityType.WITHER,
EntityType.PIGLIN_BRUTE, EntityType.ZOGLIN, EntityType.WARDEN)) {
assertNull(NativeEntitySpawns.spawn(type, level, BlockPos.ZERO, MobSpawnType.NATURAL));
}
verify(level, never()).enabledFeatures();
verify(level, never()).addFreshEntityWithPassengers(any(Entity.class));
}
@Test
public void peacefulPreservesPassiveAndNativeMonsterCategoryExceptions() {
ServerLevel level = mock(ServerLevel.class);
when(level.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
IllegalStateException reachedNativeFactory = new IllegalStateException("Native entity factory reached");
when(level.enabledFeatures()).thenThrow(reachedNativeFactory);
for (EntityType<?> type : List.of(EntityType.COW, EntityType.VILLAGER, EntityType.WOLF,
EntityType.PIGLIN, EntityType.HOGLIN, EntityType.SHULKER, EntityType.ENDER_DRAGON)) {
assertSame(reachedNativeFactory, assertThrows(IllegalStateException.class,
() -> NativeEntitySpawns.spawn(type, level, BlockPos.ZERO, MobSpawnType.NATURAL)));
}
}
@Test
public void nonPeacefulDifficultiesPreserveHostileSpawns() {
ServerLevel level = mock(ServerLevel.class);
Entity created = mock(Entity.class);
for (Difficulty difficulty : List.of(Difficulty.EASY, Difficulty.NORMAL, Difficulty.HARD)) {
EntityType<?> type = spy(BuiltInRegistries.ENTITY_TYPE.get(ResourceLocation.parse("minecraft:zombie")));
when(level.getDifficulty()).thenReturn(difficulty);
doReturn(created).when(type).spawn(level, BlockPos.ZERO, MobSpawnType.NATURAL);
assertSame(created, NativeEntitySpawns.spawn(type, level, BlockPos.ZERO, MobSpawnType.NATURAL));
verify(type).spawn(level, BlockPos.ZERO, MobSpawnType.NATURAL);
}
}
@Test
public void unrelatedNativeSpawnFailuresStillPropagate() {
ServerLevel level = mock(ServerLevel.class);
when(level.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
EntityType<?> type = spy(BuiltInRegistries.ENTITY_TYPE.get(ResourceLocation.parse("minecraft:cow")));
IllegalStateException failure = new IllegalStateException("Spawn rejected by platform");
doThrow(failure).when(type).spawn(level, BlockPos.ZERO, MobSpawnType.NATURAL);
assertSame(failure, assertThrows(IllegalStateException.class,
() -> NativeEntitySpawns.spawn(type, level, BlockPos.ZERO, MobSpawnType.NATURAL)));
}
}
@@ -0,0 +1,167 @@
package art.arcane.iris.modded;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.core.registries.Registries;
import net.minecraft.core.RegistrationInfo;
import net.minecraft.core.Registry;
import net.minecraft.core.MappedRegistry;
import com.mojang.serialization.Lifecycle;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedStructureStage;
import com.mojang.serialization.Codec;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.IdMapper;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.entity.ai.village.poi.PoiSection;
import net.minecraft.world.entity.ai.village.poi.PoiManager;
import net.minecraft.world.entity.ai.village.poi.PoiType;
import net.minecraft.world.entity.ai.village.poi.PoiTypes;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeGenerationSettings;
import net.minecraft.world.level.biome.BiomeSpecialEffects;
import net.minecraft.world.level.biome.MobSpawnSettings;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.PalettedContainer;
import net.minecraft.world.level.chunk.PalettedContainerRO;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.UpgradeData;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class ModdedNativeStructurePoiRegistrationTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void visitsOnlyExistingPoiBlocksBeforeNativePlacement() {
ProtoChunk chunk = newChunk();
BlockPos poiPosition = new BlockPos(3, 70, 5);
BlockPos ordinaryPosition = new BlockPos(4, 70, 5);
BlockState poiState = Blocks.BARREL.defaultBlockState();
chunk.setBlockState(poiPosition, poiState, false);
chunk.setBlockState(ordinaryPosition, Blocks.STONE.defaultBlockState(), false);
Map<BlockPos, BlockState> visited = new LinkedHashMap<>();
NativeModdedStructureStage.visitExistingPois(
chunk, (position, state) -> visited.put(position.immutable(), state));
assertEquals(poiState, visited.get(poiPosition));
assertFalse(visited.containsKey(ordinaryPosition));
assertEquals(1, visited.size());
}
@Test
public void duplicatePrimingIsIdempotentForAnAlreadyRegisteredPoi() {
BlockPos position = new BlockPos(3, 70, 5);
Holder<PoiType> type = PoiTypes.forState(
Blocks.BARREL.defaultBlockState()).orElseThrow();
PoiSection section = new PoiSection(() -> { });
section.add(position, type);
section.add(position, type);
assertEquals(1L, section.getRecords(candidate -> true, PoiManager.Occupancy.ANY).count());
assertEquals(type, section.getType(position).orElseThrow());
section.remove(position);
assertTrue(section.getType(position).isEmpty());
}
@Test
public void worldgenPrimingQueuesBeforeTheLaterVanillaRemoval() {
ProtoChunk chunk = newChunk();
BlockPos position = new BlockPos(3, 70, 5);
BlockState poiState = Blocks.BARREL.defaultBlockState();
chunk.setBlockState(position, poiState, false);
PoiSection section = new PoiSection(() -> { });
Deque<Runnable> serverQueue = new ArrayDeque<>();
AtomicInteger missingRemovals = new AtomicInteger();
NativeModdedStructureStage.visitExistingPois(chunk,
(poiPosition, state) -> queuePoiTransition(
serverQueue, section, poiPosition,
Blocks.AIR.defaultBlockState(), state, missingRemovals));
assertTrue(section.getType(position).isEmpty());
queuePoiTransition(serverQueue, section, position,
poiState, Blocks.AIR.defaultBlockState(), missingRemovals);
while (!serverQueue.isEmpty()) {
serverQueue.removeFirst().run();
}
assertEquals(0, missingRemovals.get());
assertTrue(section.getType(position).isEmpty());
}
private static void queuePoiTransition(Deque<Runnable> serverQueue,
PoiSection section,
BlockPos position,
BlockState oldState,
BlockState newState,
AtomicInteger missingRemovals) {
Optional<Holder<PoiType>> oldType = PoiTypes.forState(oldState);
Optional<Holder<PoiType>> newType = PoiTypes.forState(newState);
if (Objects.equals(oldType, newType)) {
return;
}
BlockPos immutablePosition = position.immutable();
oldType.ifPresent(type -> serverQueue.addLast(() -> {
if (section.getType(immutablePosition).isEmpty()) {
missingRemovals.incrementAndGet();
return;
}
section.remove(immutablePosition);
}));
newType.ifPresent(type -> serverQueue.addLast(
() -> section.add(immutablePosition, type)));
}
private static ProtoChunk newChunk() {
return new ProtoChunk(
new ChunkPos(0, 0),
UpgradeData.EMPTY,
LevelHeightAccessor.create(-64, 384),
biomeRegistry(),
null);
}
private static Registry<Biome> biomeRegistry() {
Biome biome = new Biome.BiomeBuilder()
.hasPrecipitation(false)
.temperature(0.8F)
.downfall(0.4F)
.specialEffects(new BiomeSpecialEffects.Builder().fogColor(0).waterColor(0x3F76E4)
.waterFogColor(0).skyColor(0).build())
.mobSpawnSettings(MobSpawnSettings.EMPTY)
.generationSettings(BiomeGenerationSettings.EMPTY)
.build();
MappedRegistry<Biome> registry = new MappedRegistry<>(Registries.BIOME, Lifecycle.stable());
registry.register(ResourceKey.create(Registries.BIOME, ResourceLocation.withDefaultNamespace("plains")),
biome, RegistrationInfo.BUILT_IN);
return registry.freeze();
}
}
@@ -0,0 +1,45 @@
package art.arcane.iris.modded;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.mojang.serialization.JsonOps;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.packs.PackType;
import net.minecraft.server.packs.metadata.pack.PackMetadataSection;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class ModdedPackMetadataTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void metadataLoadsClientResourcesAndServerDataForCurrentMinecraft() throws IOException {
try (InputStream resource = ModdedPackMetadataTest.class.getResourceAsStream("/pack.mcmeta")) {
assertNotNull(resource);
try (InputStreamReader reader = new InputStreamReader(resource, StandardCharsets.UTF_8)) {
JsonObject metadata = JsonParser.parseReader(reader).getAsJsonObject();
for (PackType type : PackType.values()) {
PackMetadataSection section = PackMetadataSection.CODEC
.parse(JsonOps.INSTANCE, metadata.get("pack")).getOrThrow();
assertEquals("Iris client resources and mod data", section.description().getString());
assertTrue(type.toString(), section.supportedFormats().orElseThrow().isValueInRange(
SharedConstants.getCurrentVersion().getPackVersion(type)));
}
}
}
}
}
@@ -0,0 +1,253 @@
package art.arcane.iris.modded;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeStructureSetFrequencyOverrides;
import art.arcane.iris.structure.nativegen.IrisImportedStructureControl;
import art.arcane.iris.structure.placement.IrisStructureSetFrequencyOverride;
import art.arcane.volmlib.util.collection.KList;
import net.minecraft.SharedConstants;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderOwner;
import net.minecraft.core.HolderSet;
import net.minecraft.core.Vec3i;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.biome.Climate;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadType;
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacementType;
import com.mojang.serialization.MapCodec;
import java.util.stream.Stream;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.util.List;
import java.util.Optional;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
public class ModdedStructureSetFrequencyOverridesTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void createStateReplacementScalesOnlyTheExactSetAndRetainsEntries() throws Exception {
Holder<StructureSet> complexes = structureSet(
"minecraft:nether_complexes", 27, 4, 30084232);
Holder<StructureSet> fossils = structureSet(
"minecraft:nether_fossils", 2, 1, 14357921);
ChunkGeneratorStructureState state = state(List.of(complexes, fossils));
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
overrides.add(new IrisStructureSetFrequencyOverride()
.setStructureSet("minecraft:nether_complexes")
.setMultiplier(1.1D));
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
ChunkGeneratorStructureState returned =
NativeStructureSetFrequencyOverrides.apply(state, control);
assertSame(state, returned);
List<Holder<StructureSet>> scaledSets = state.possibleStructureSets();
RandomSpreadStructurePlacement scaledComplexes =
(RandomSpreadStructurePlacement) scaledSets.get(0).value().placement();
assertEquals(26, scaledComplexes.spacing());
assertEquals(4, scaledComplexes.separation());
assertSame(complexes.value().structures(), scaledSets.get(0).value().structures());
assertSame(fossils, scaledSets.get(1));
}
@Test
public void unrelatedCustomPlacementTypeRemainsUntouched() {
Holder<StructureSet> custom = new BoundStructureSetHolder(
"example:custom",
new StructureSet(List.of(), new UnsupportedPlacement()));
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
overrides.add(new IrisStructureSetFrequencyOverride()
.setStructureSet("minecraft:nether_complexes")
.setMultiplier(1.1D));
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
List<Holder<StructureSet>> scaled =
NativeStructureSetFrequencyOverrides.scaleSets(List.of(custom), control);
assertSame(custom, scaled.getFirst());
}
@Test
public void unrelatedExclusionCycleRemainsUntouched() {
BoundStructureSetHolder first = new BoundStructureSetHolder("example:first");
BoundStructureSetHolder second = new BoundStructureSetHolder("example:second");
first.bind(new StructureSet(List.of(), placement(32, 8, 1, second)));
second.bind(new StructureSet(List.of(), placement(40, 10, 2, first)));
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
overrides.add(new IrisStructureSetFrequencyOverride()
.setStructureSet("minecraft:nether_complexes")
.setMultiplier(1.1D));
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
List<Holder<StructureSet>> scaled = NativeStructureSetFrequencyOverrides.scaleSets(
List.of(first, second), control);
assertSame(first, scaled.get(0));
assertSame(second, scaled.get(1));
}
@Test
public void affectedExclusionCycleUsesBoundScaledHolders() {
BoundStructureSetHolder first = new BoundStructureSetHolder("example:first");
BoundStructureSetHolder second = new BoundStructureSetHolder("example:second");
first.bind(new StructureSet(List.of(), placement(32, 8, 1, second)));
second.bind(new StructureSet(List.of(), placement(40, 10, 2, first)));
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
overrides.add(new IrisStructureSetFrequencyOverride()
.setStructureSet("example:first")
.setMultiplier(1.1D));
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
List<Holder<StructureSet>> scaled = NativeStructureSetFrequencyOverrides.scaleSets(
List.of(first, second), control);
assertEquals(31, ((RandomSpreadStructurePlacement)
scaled.get(0).value().placement()).spacing());
assertEquals(40, ((RandomSpreadStructurePlacement)
scaled.get(1).value().placement()).spacing());
}
@Test
public void affectedRandomSpreadSubclassFailsInsteadOfLosingSubtypeBehavior() {
Holder<StructureSet> custom = new BoundStructureSetHolder(
"example:custom",
new StructureSet(List.of(), new CustomRandomSpreadPlacement()));
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
overrides.add(new IrisStructureSetFrequencyOverride()
.setStructureSet("example:custom")
.setMultiplier(1.1D));
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
assertThrows(IllegalStateException.class, () ->
NativeStructureSetFrequencyOverrides.scaleSets(List.of(custom), control));
}
private static Holder<StructureSet> structureSet(
String key,
int spacing,
int separation,
int salt
) {
StructureSet value = new StructureSet(
List.of(),
new RandomSpreadStructurePlacement(
spacing, separation, RandomSpreadType.LINEAR, salt));
return new BoundStructureSetHolder(key, value);
}
private static RandomSpreadStructurePlacement placement(
int spacing,
int separation,
int salt,
Holder<StructureSet> exclusionTarget
) {
return new RandomSpreadStructurePlacement(
Vec3i.ZERO,
StructurePlacement.FrequencyReductionMethod.DEFAULT,
1F,
salt,
Optional.of(new StructurePlacement.ExclusionZone(exclusionTarget, 1)),
spacing,
separation,
RandomSpreadType.LINEAR);
}
private static ChunkGeneratorStructureState state(
List<Holder<StructureSet>> sets
) throws Exception {
Constructor<ChunkGeneratorStructureState> constructor =
ChunkGeneratorStructureState.class.getDeclaredConstructor(
RandomState.class,
BiomeSource.class,
long.class,
long.class,
List.class);
constructor.setAccessible(true);
return constructor.newInstance(
null,
new EmptyBiomeSource(),
1L,
1L,
sets);
}
private static final class BoundStructureSetHolder extends Holder.Reference<StructureSet> {
private BoundStructureSetHolder(String key) {
this(key, null);
}
private BoundStructureSetHolder(String key, StructureSet value) {
super(Type.STAND_ALONE, new HolderOwner<>() {
}, ResourceKey.create(Registries.STRUCTURE_SET, ResourceLocation.parse(key)), value);
}
private void bind(StructureSet value) {
bindValue(value);
}
}
private static final class EmptyBiomeSource extends BiomeSource {
@Override
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
return null;
}
@Override
protected MapCodec<? extends BiomeSource> codec() {
throw new UnsupportedOperationException();
}
@Override
protected Stream<Holder<Biome>> collectPossibleBiomes() {
return HolderSet.<Biome>empty().stream();
}
}
private static final class UnsupportedPlacement extends StructurePlacement {
private UnsupportedPlacement() {
super(Vec3i.ZERO, FrequencyReductionMethod.DEFAULT, 1F, 0, Optional.empty());
}
@Override
protected boolean isPlacementChunk(ChunkGeneratorStructureState state, int x, int z) {
return false;
}
@Override
public StructurePlacementType<?> type() {
return null;
}
}
private static final class CustomRandomSpreadPlacement extends RandomSpreadStructurePlacement {
private CustomRandomSpreadPlacement() {
super(27, 4, RandomSpreadType.LINEAR, 30084232);
}
}
}
@@ -0,0 +1,71 @@
package art.arcane.volmlib.nativelib.minecraft26_2.modded;
import art.arcane.volmlib.nativelib.terrain.NativePregenRuntime;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerChunkCache;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ChunkResult;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.server.level.TicketType;
import net.minecraft.world.level.ChunkPos;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import java.util.concurrent.CompletableFuture;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
public class NativePregenRuntimeTest {
@BeforeClass
public static void bootstrapRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void chunkTicketAdmissionAndReleaseRunThroughTheServerExecutor() {
MinecraftServer server = mock(MinecraftServer.class);
ServerLevel level = mock(ServerLevel.class);
ServerChunkCache chunks = mock(ServerChunkCache.class);
when(level.getServer()).thenReturn(server);
when(level.getChunkSource()).thenReturn(chunks);
CompletableFuture<ChunkResult<ChunkAccess>> loaded = new CompletableFuture<>();
doReturn(loaded).when(chunks).getChunkFutureMainThread(-2, 7, ChunkStatus.FULL, true);
NativePregenRuntime runtime = NativeModdedPregenRuntime.from(new ModdedPlatformWorld(level));
CompletableFuture<NativePregenRuntime.ChunkLoad> result = runtime.loadChunk(-2, 7);
verify(chunks, never()).addRegionTicket(any(), any(), eq(0), any());
verify(chunks, never()).getChunkFutureMainThread(-2, 7, ChunkStatus.FULL, true);
ArgumentCaptor<Runnable> requests = ArgumentCaptor.forClass(Runnable.class);
verify(server).execute(requests.capture());
requests.getValue().run();
assertFalse(result.isDone());
verify(chunks).getChunkFutureMainThread(-2, 7, ChunkStatus.FULL, true);
loaded.complete(ChunkResult.of(mock(ChunkAccess.class)));
assertTrue(result.join().successful());
ArgumentCaptor<TicketType> ticket = ArgumentCaptor.forClass(TicketType.class);
verify(chunks).addRegionTicket(ticket.capture(), eq(new ChunkPos(-2, 7)), eq(0), eq(new ChunkPos(-2, 7)));
runtime.releaseChunk(-2, 7);
verify(chunks, never()).removeRegionTicket(any(), any(), eq(0), any());
verify(server, times(2)).execute(requests.capture());
requests.getAllValues().getLast().run();
ArgumentCaptor<TicketType> released = ArgumentCaptor.forClass(TicketType.class);
verify(chunks).removeRegionTicket(released.capture(), eq(new ChunkPos(-2, 7)), eq(0), eq(new ChunkPos(-2, 7)));
assertSame(ticket.getValue(), released.getValue());
}
}
@@ -0,0 +1,130 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.volmlib.nativelib.minecraft26_2.modded;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenSnapshot.WorldgenBiome;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenSnapshot.WorldgenPlacedFeature;
import com.google.gson.JsonParser;
import com.mojang.serialization.JsonOps;
import com.mojang.serialization.Lifecycle;
import net.minecraft.SharedConstants;
import net.minecraft.core.Holder;
import net.minecraft.core.MappedRegistry;
import net.minecraft.core.RegistrationInfo;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.carver.ConfiguredWorldCarver;
import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class NativeWorldgenScanTest {
private static final String BIOME_JSON = "{\"has_precipitation\":true,\"temperature\":0.8,\"downfall\":0.4,"
+ "\"effects\":{\"fog_color\":12638463,\"water_color\":4159204,\"water_fog_color\":329011,\"sky_color\":7907327},"
+ "\"carvers\":{},\"features\":[[],[],[],[],[],[],[\"examplemod:ore_glimmer\"]],"
+ "\"spawners\":{},\"spawn_costs\":{}}";
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void scanReadsLiveFeatureStepsAndEncodesDefinitions() {
NativeWorldgenSnapshot snapshot = NativeWorldgenScan.scan(server(), loader()).encode();
WorldgenBiome biome = snapshot.biomes().getFirst();
assertEquals("examplemod:glade", biome.key());
assertEquals(0.8F, biome.temperature(), 0F);
assertEquals(0.4F, biome.downfall(), 0.0001F);
assertTrue(biome.hasPrecipitation());
assertEquals(GenerationStep.Decoration.values().length, biome.featureSteps().size());
assertEquals(List.of("examplemod:ore_glimmer"),
biome.featureSteps().get(GenerationStep.Decoration.UNDERGROUND_ORES.ordinal()));
assertNotNull(biome.definition());
assertEquals(0.4F, biome.definition().getAsJsonObject().get("downfall").getAsFloat(), 0.0001F);
}
@Test
public void unencodableFeaturesStayInTheCatalogWithoutJson() {
NativeWorldgenSnapshot snapshot = NativeWorldgenScan.scan(server(), loader()).encode();
WorldgenPlacedFeature feature = snapshot.placedFeatures().getFirst();
assertEquals("examplemod:ore_glimmer", feature.key());
assertEquals("", feature.feature());
assertNull(feature.config());
assertTrue(feature.placement().isJsonArray());
assertEquals(1, snapshot.failures().count());
assertTrue(snapshot.failures().samples().getFirst().startsWith("feature of examplemod:ore_glimmer"));
assertTrue(snapshot.levelStems().isEmpty());
}
private static NativeModdedServer server() {
MappedRegistry<PlacedFeature> features = new MappedRegistry<>(Registries.PLACED_FEATURE, Lifecycle.stable());
features.register(ResourceKey.create(Registries.PLACED_FEATURE, ResourceLocation.parse("examplemod:ore_glimmer")),
new PlacedFeature(Holder.direct(mock(ConfiguredFeature.class)), List.of()), RegistrationInfo.BUILT_IN);
features.freeze();
MappedRegistry<ConfiguredWorldCarver<?>> carvers = new MappedRegistry<>(Registries.CONFIGURED_CARVER, Lifecycle.stable());
carvers.freeze();
MappedRegistry<Biome> biomes = new MappedRegistry<>(Registries.BIOME, Lifecycle.stable());
MappedRegistry<Structure> structures = new MappedRegistry<>(Registries.STRUCTURE, Lifecycle.stable());
structures.freeze();
MappedRegistry<StructureSet> structureSets = new MappedRegistry<>(Registries.STRUCTURE_SET, Lifecycle.stable());
structureSets.freeze();
MappedRegistry<DimensionType> dimensionTypes = new MappedRegistry<>(Registries.DIMENSION_TYPE, Lifecycle.stable());
dimensionTypes.freeze();
RegistryAccess access = new RegistryAccess.ImmutableRegistryAccess(
List.of(biomes, features, carvers, structures, structureSets, dimensionTypes));
Biome biome = Biome.DIRECT_CODEC.parse(RegistryOps.create(JsonOps.INSTANCE, access),
JsonParser.parseString(BIOME_JSON)).getOrThrow();
biomes.register(ResourceKey.create(Registries.BIOME, ResourceLocation.parse("examplemod:glade")), biome, RegistrationInfo.BUILT_IN);
biomes.freeze();
MinecraftServer server = mock(MinecraftServer.class);
when(server.registryAccess()).thenReturn(access.freeze());
return NativeModdedServer.fromHandle(server);
}
private static NativeModdedLoader loader() {
NativeModdedLoader loader = mock(NativeModdedLoader.class);
when(loader.unmodifiedBiome(any())).thenAnswer(invocation -> invocation.getArgument(0));
return loader;
}
}
@@ -0,0 +1,68 @@
package art.arcane.volmlib.nativelib.minecraft26_2.terrain;
import org.junit.Test;
import java.util.OptionalInt;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
public class NativeJigsawBoundsTest {
@Test
public void verticalDistanceUsesInclusiveBlocksWithinExclusiveHeightLimits() {
try (NativeJigsawBounds bounds = NativeJigsawBounds.open(OptionalInt.of(12))) {
NativeJigsawBounds.heightLimits(-64, 320);
NativeJigsawBounds.center(100);
assertEquals(88, NativeJigsawBounds.minimum(40), 0);
assertEquals(113, NativeJigsawBounds.maximum(160), 0);
NativeJigsawBounds.center(-60);
assertEquals(-64, NativeJigsawBounds.minimum(-120), 0);
assertEquals(-47, NativeJigsawBounds.maximum(0), 0);
NativeJigsawBounds.center(315);
assertEquals(303, NativeJigsawBounds.minimum(255), 0);
assertEquals(320, NativeJigsawBounds.maximum(375), 0);
}
}
@Test
public void nestedScopesRestoreTheOuterDistanceAndCenter() {
try (NativeJigsawBounds outer = NativeJigsawBounds.open(OptionalInt.of(10))) {
NativeJigsawBounds.heightLimits(-64, 320);
NativeJigsawBounds.center(80);
try (NativeJigsawBounds inner = NativeJigsawBounds.open(OptionalInt.of(2))) {
NativeJigsawBounds.heightLimits(0, 256);
NativeJigsawBounds.center(200);
assertEquals(198, NativeJigsawBounds.minimum(0), 0);
assertEquals(203, NativeJigsawBounds.maximum(256), 0);
}
assertEquals(70, NativeJigsawBounds.minimum(0), 0);
assertEquals(91, NativeJigsawBounds.maximum(256), 0);
try (NativeJigsawBounds unbounded = NativeJigsawBounds.open(OptionalInt.empty())) {
assertEquals(-20, NativeJigsawBounds.minimum(-20), 0);
assertEquals(400, NativeJigsawBounds.maximum(400), 0);
}
assertEquals(70, NativeJigsawBounds.minimum(0), 0);
}
assertEquals(-20, NativeJigsawBounds.minimum(-20), 0);
assertEquals(400, NativeJigsawBounds.maximum(400), 0);
}
@Test
public void failedGenerationClosesTheScopeAndRejectsNegativeDistance() {
assertThrows(IllegalStateException.class, () -> {
try (NativeJigsawBounds bounds = NativeJigsawBounds.open(OptionalInt.of(0))) {
NativeJigsawBounds.heightLimits(-64, 320);
NativeJigsawBounds.center(40);
assertEquals(40, NativeJigsawBounds.minimum(-100), 0);
assertEquals(41, NativeJigsawBounds.maximum(100), 0);
throw new IllegalStateException("generation failed");
}
});
assertEquals(-100, NativeJigsawBounds.minimum(-100), 0);
assertEquals(100, NativeJigsawBounds.maximum(100), 0);
assertThrows(IllegalArgumentException.class, () -> NativeJigsawBounds.open(OptionalInt.of(-1)));
assertEquals(-100, NativeJigsawBounds.minimum(-100), 0);
}
}
@@ -0,0 +1,41 @@
package art.arcane.volmlib.nativelib.minecraft26_2.terrain;
import com.mojang.datafixers.util.Either;
import net.minecraft.SharedConstants;
import net.minecraft.core.Holder;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import java.util.Optional;
import static org.junit.Assert.assertSame;
public class NativeStructureTemplateAccessTest {
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void resolvesRuntimeTemplateThroughWidenedPoolField() {
StructureTemplate template = new StructureTemplate();
SinglePoolElement element = new RuntimePoolElement(template);
assertSame(template, NativeStructureReflection.resolveTemplate(element, () -> {
throw new AssertionError("Runtime templates must not require a template manager");
}));
}
private static final class RuntimePoolElement extends SinglePoolElement {
private RuntimePoolElement(StructureTemplate template) {
super(Either.right(template), Holder.direct(new StructureProcessorList(List.of())),
StructureTemplatePool.Projection.RIGID, Optional.empty());
}
}
}
@@ -0,0 +1,430 @@
{
"global": [
{
"pattern": "\\bIdentifier\\b",
"replacement": "ResourceLocation"
},
{
"pattern": "\\bEntitySpawnReason\\b",
"replacement": "MobSpawnType"
},
{
"pattern": "net\\.minecraft\\.world\\.entity\\.animal\\.panda\\.Panda",
"replacement": "net.minecraft.world.entity.animal.Panda"
},
{
"pattern": "net\\.minecraft\\.world\\.entity\\.npc\\.villager\\.Villager",
"replacement": "net.minecraft.world.entity.npc.Villager"
},
{
"pattern": "net\\.minecraft\\.world\\.level\\.gamerules\\.GameRules",
"replacement": "net.minecraft.world.level.GameRules"
},
{
"pattern": "\\bSpeleothemBlock\\b",
"replacement": "PointedDripstoneBlock"
},
{
"pattern": "\\bSpeleothemThickness\\b",
"replacement": "DripstoneThickness"
},
{
"pattern": "\\bEntityTypes\\b",
"replacement": "EntityType"
},
{
"pattern": "\\bAbstractSpreadingStructurePlacement\\b",
"replacement": "StructurePlacement"
},
{
"pattern": "\\bDirectPoolAlias\\b",
"replacement": "Direct"
},
{
"pattern": "\\bRandomGroupPoolAlias\\b",
"replacement": "RandomGroup"
},
{
"pattern": "\\bRandomPoolAlias\\b",
"replacement": "Random"
},
{
"pattern": "\\.identifier\\(\\)",
"replacement": ".location()"
},
{
"pattern": "ChunkPos\\.pack\\(",
"replacement": "ChunkPos.asLong("
},
{
"pattern": "\\.registryAccess\\(\\)\\.lookupOrThrow\\(",
"replacement": ".registryAccess().registryOrThrow("
},
{
"pattern": "\\.getPos\\(\\)\\.x\\(\\)",
"replacement": ".getPos().x"
},
{
"pattern": "\\.getPos\\(\\)\\.z\\(\\)",
"replacement": ".getPos().z"
},
{
"pattern": "\\.getChunkPos\\(\\)\\.x\\(\\)",
"replacement": ".getChunkPos().x"
},
{
"pattern": "\\.getChunkPos\\(\\)\\.z\\(\\)",
"replacement": ".getChunkPos().z"
},
{
"pattern": "IdentifierArgument",
"replacement": "ResourceLocationArgument"
},
{
"pattern": "IdentifierArgument\\.identifier\\(\\)",
"replacement": "ResourceLocationArgument.id()"
},
{
"pattern": "BuiltInRegistries\\.([A-Z_]+)\\.getValue\\(",
"replacement": "BuiltInRegistries.$1.get("
},
{
"pattern": "\\blevel\\.getMinY\\(",
"replacement": "level.getMinBuildHeight("
},
{
"pattern": "(?ms)^ if \\(!Items\\.[A-Z_]+\\.builtInRegistryHolder\\(\\)\\.areComponentsBound\\(\\)\\) \\{\\n.*?^ \\}\\n",
"replacement": ""
}
],
"files": {
"art/arcane/iris/modded/command/NativeTaggedItemsTest.java": [
{
"pattern": "\\.getBooleanOr\\(\"selection_tool\", false\\)",
"replacement": ".getBoolean(\"selection_tool\")"
},
{
"pattern": "assertTrue\\(item\\.stack\\(\\)\\.get\\(DataComponents\\.TOOLTIP_DISPLAY\\)\\.hiddenComponents\\(\\)\\.contains\\(DataComponents\\.UNBREAKABLE\\)\\);",
"replacement": "assertEquals(false, item.stack().get(DataComponents.UNBREAKABLE).showInTooltip());"
}
],
"art/arcane/iris/modded/service/ModdedPackUpdateServiceTest.java": [
{
"pattern": "import net\\.minecraft\\.server\\.permissions\\.LevelBasedPermissionSet;\\n",
"replacement": ""
},
{
"pattern": "when\\(handle\\.permissions\\(\\)\\)\\.thenReturn\\(LevelBasedPermissionSet\\.ALL\\);",
"replacement": "when(handle.hasPermission(2)).thenReturn(false);"
},
{
"pattern": "handle\\.hasPermission\\(",
"replacement": "handle.hasPermissions("
},
{
"pattern": "handle\\.nameAndId\\(",
"replacement": "handle.getGameProfile("
},
{
"pattern": "handle\\.level\\(\\)",
"replacement": "handle.serverLevel()"
}
],
"art/arcane/iris/modded/service/ModdedChunkUpdateServiceTest.java": [
{
"pattern": "when\\(level\\.getMaxY\\(\\)\\)\\.thenReturn\\(319\\);",
"replacement": "when(level.getMaxBuildHeight()).thenReturn(320);"
}
],
"art/arcane/volmlib/nativelib/minecraft26_2/modded/NativeSpawnPopulationTest.java": [
{
"pattern": "when\\(level\\.getMaxY\\(\\)\\)\\.thenReturn\\(320\\);",
"replacement": "when(level.getMaxBuildHeight()).thenReturn(321);"
},
{
"pattern": "rules\\.get\\(GameRules\\.SPAWN_MOBS\\)",
"replacement": "rules.getBoolean(GameRules.RULE_DOMOBSPAWNING)"
}
],
"art/arcane/volmlib/nativelib/minecraft26_2/modded/NativeTeleportSafetyTest.java": [
{
"pattern": "when\\(level\\.getMaxY\\(\\)\\)\\.thenReturn\\(16\\);",
"replacement": "when(level.getMaxBuildHeight()).thenReturn(17);"
},
{
"pattern": "import net.minecraft.server.level.ServerLevel;",
"replacement": "import net.minecraft.server.level.ServerLevel;\nimport net.minecraft.server.level.ChunkResult;\nimport net.minecraft.world.level.chunk.ChunkAccess;"
},
{
"pattern": "CompletableFuture<Void> loaded",
"replacement": "CompletableFuture<ChunkResult<ChunkAccess>> loaded"
},
{
"pattern": "\\.addTicketAndLoadWithRadius\\(any\\(\\), any\\(\\), anyInt\\(\\)\\)",
"replacement": ".getChunkFutureMainThread(anyInt(), anyInt(), any(), anyBoolean())"
},
{
"pattern": "loaded\\.complete\\(null\\)",
"replacement": "loaded.complete(ChunkResult.of(mock(ChunkAccess.class)))"
},
{
"pattern": "\\.removeTicketWithRadius\\(any\\(\\), any\\(\\), anyInt\\(\\)\\)",
"replacement": ".removeRegionTicket(any(), any(), anyInt(), any())"
},
{
"pattern": "import static org.mockito.ArgumentMatchers.anyInt;",
"replacement": "import static org.mockito.ArgumentMatchers.anyInt;\nimport static org.mockito.ArgumentMatchers.anyBoolean;"
}
],
"art/arcane/volmlib/nativelib/minecraft26_2/modded/NativeStructureOperationsTest.java": [
{
"pattern": "import net\\.minecraft\\.world\\.level\\.levelgen\\.feature\\.FallenTreeFeature;\\n",
"replacement": ""
},
{
"pattern": " assertEquals\\(\"fallen_trees\", NativeStructureOperations\\.classifyFeature\\(mock\\(FallenTreeFeature\\.class\\)\\)\\);\\n",
"replacement": ""
}
],
"art/arcane/iris/nativegen/NativeStructureReferenceRepairTest.java": [
{
"pattern": "\\borigin\\.x\\(\\)",
"replacement": "origin.x"
},
{
"pattern": "\\borigin\\.z\\(\\)",
"replacement": "origin.z"
},
{
"pattern": "\\bcandidate\\.x\\(\\)",
"replacement": "candidate.x"
},
{
"pattern": "\\bcandidate\\.z\\(\\)",
"replacement": "candidate.z"
},
{
"pattern": "\\borigin\\.pack\\(",
"replacement": "origin.toLong("
},
{
"pattern": "\"lookupOrThrow\"",
"replacement": "\"registryOrThrow\""
},
{
"pattern": "\"lookup\"",
"replacement": "\"registry\""
}
],
"art/arcane/iris/modded/ModdedNativeStructureWorldgenAccessTest.java": [
{
"pattern": "\\bgenerationCenter\\.x\\(\\)",
"replacement": "generationCenter.x"
},
{
"pattern": "\\bgenerationCenter\\.z\\(\\)",
"replacement": "generationCenter.z"
},
{
"pattern": "\\borigin\\.pack\\(",
"replacement": "origin.toLong("
},
{
"pattern": "\\.updateNeighborsAt\\(",
"replacement": ".blockUpdated("
},
{
"pattern": "(Direction\\.(?:NORTH|EAST)), position, position\\.(north|east)\\(\\),\\s*Blocks\\.DIRT\\.defaultBlockState\\(\\),",
"replacement": "$1, Blocks.DIRT.defaultBlockState(), position, position.$2(),"
},
{
"pattern": "\"getMinY\"",
"replacement": "\"getMinBuildHeight\""
}
],
"art/arcane/iris/modded/ModdedTileParityTest.java": [
{
"pattern": "import net\\.minecraft\\.world\\.level\\.block\\.entity\\.SignTextSlot;\\n",
"replacement": ""
},
{
"pattern": "import net\\.minecraft\\.world\\.level\\.storage\\.TagValueInput;\\n",
"replacement": ""
},
{
"pattern": "\\.loadWithComponents\\(TagValueInput\\.create\\(\\s*ProblemReporter\\.DISCARDING, registries, (tile\\.nativeData\\(\\)\\.payload\\(\\)|\\(\\(ModdedTileData\\) tile\\)\\.nativeData\\(\\)\\.payload\\(\\))\\)\\)",
"replacement": ".loadWithComponents($1, registries)"
},
{
"pattern": "\\.getText\\(SignTextSlot\\.FRONT\\)",
"replacement": ".getFrontText()"
},
{
"pattern": "\\.getText\\(SignTextSlot\\.BACK\\)",
"replacement": ".getBackText()"
},
{
"pattern": "\\.getMessages\\(false\\)\\.get\\((\\d+)\\)",
"replacement": ".getMessages(false)[$1]"
},
{
"pattern": "out\\.writeShort\\(28\\);",
"replacement": "out.writeShort(37);"
}
],
"art/arcane/iris/modded/ModdedEntitySpawnerParityTest.java": [
{
"pattern": "MobSpawnType\\.SPAWN_ITEM_USE",
"replacement": "MobSpawnType.SPAWN_EGG"
},
{
"pattern": "MobSpawnType\\.LOAD",
"replacement": "MobSpawnType.EVENT"
}
],
"art/arcane/iris/modded/service/ModdedWorldBoundaryTest.java": [
{
"pattern": "\\.setSafeZone\\(",
"replacement": ".setDamageSafeZone("
},
{
"pattern": "\\.getSafeZone\\(",
"replacement": ".getDamageSafeZone("
}
],
"art/arcane/volmlib/nativelib/minecraft26_2/modded/NativeSpawnBiomeTablesTest.java": [
{
"pattern": "import net.minecraft.resources.ResourceLocation;",
"replacement": "import net.minecraft.resources.ResourceLocation;\nimport net.minecraft.resources.ResourceKey;\nimport net.minecraft.core.registries.Registries;"
},
{
"pattern": "registry\\.get\\((ResourceLocation\\.parse\\(\"[^\"]+\"\\))\\)",
"replacement": "registry.getHolder(ResourceKey.create(Registries.BIOME, $1))"
}
],
"art/arcane/iris/modded/NativeItemTranslatorTest.java": [
{
"pattern": "assertEquals\\(73F, stack\\.get\\(DataComponents\\.CUSTOM_MODEL_DATA\\)\\.floats\\(\\)\\.getFirst\\(\\), 0F\\);",
"replacement": "assertEquals(73, stack.get(DataComponents.CUSTOM_MODEL_DATA).value());"
},
{
"pattern": "\\.getString\\(\"marker\"\\)\\.orElseThrow\\(\\)",
"replacement": ".getString(\"marker\")"
}
],
"art/arcane/volmlib/nativelib/minecraft26_2/modded/NativeHarvestSessionTest.java": [
{
"pattern": "\\.getSelectedItem\\(",
"replacement": ".getSelected("
},
{
"pattern": " doAnswer\\(invocation -> \\{ item\\.set\\(invocation\\.getArgument\\(0\\)\\); return null; \\}\\)\\n \\.when\\(inventory\\)\\.setSelectedItem\\(any\\(ItemStack\\.class\\)\\);\\n",
"replacement": ""
}
],
"art/arcane/iris/modded/command/IrisModdedCommandsTeleportTest.java": [
{
"pattern": "\\.addTicketAndLoadWithRadius\\(any\\(\\), any\\(\\), anyInt\\(\\)\\)",
"replacement": ".getChunkFutureMainThread(anyInt(), anyInt(), any(), anyBoolean())"
},
{
"pattern": "import static org.mockito.ArgumentMatchers.anyInt;",
"replacement": "import static org.mockito.ArgumentMatchers.anyInt;\nimport static org.mockito.ArgumentMatchers.anyBoolean;"
}
],
"art/arcane/iris/modded/NativeRegistryAccessTest.java": [
{
"pattern": "\\.registryAccess\\(\\)\\.registryOrThrow\\(",
"replacement": ".registryAccess().lookupOrThrow("
},
{
"pattern": "\\.reloadableRegistries\\(\\)\\.lookup\\(",
"replacement": ".reloadableRegistries().get("
},
{
"pattern": "import net.minecraft.core.HolderLookup;",
"replacement": "import net.minecraft.core.HolderLookup;\nimport net.minecraft.core.RegistryAccess;"
},
{
"pattern": "HolderLookup\\.Provider reloadedRegistries = mock\\(HolderLookup\\.Provider.class",
"replacement": "RegistryAccess.Frozen reloadedRegistries = mock(RegistryAccess.Frozen.class"
}
],
"art/arcane/iris/modded/ModdedEntityPersistenceTest.java": [
{
"pattern": "\\.entityTags\\(",
"replacement": ".getTags("
}
],
"art/arcane/iris/modded/ModdedLootApplierTest.java": [
{
"pattern": "import net.minecraft.core.BlockPos;",
"replacement": "import net.minecraft.core.BlockPos;\nimport net.minecraft.core.Direction;"
},
{
"pattern": "ChestBlock\\.getConnectedBlockPos\\(leftPos, leftState\\)\\)\\.thenReturn\\(rightPos\\)",
"replacement": "ChestBlock.getConnectedDirection(leftState)).thenReturn(Direction.EAST)"
}
],
"art/arcane/iris/modded/ModdedBlockResolutionContractTest.java": [
{
"pattern": ", Blocks\\.PALE_MOSS_BLOCK",
"replacement": ""
},
{
"pattern": "Blocks\\.SULFUR_SPIKE, ",
"replacement": ""
},
{
"pattern": "\"minecraft:sulfur_spike\", ",
"replacement": ""
},
{
"pattern": "copperChestVariantsReceiveIrisLootAndPostLoadUpdates",
"replacement": "chestsReceiveIrisLootAndPostLoadUpdates"
},
{
"pattern": "\"copper_chest\", \"exposed_copper_chest\", \"weathered_copper_chest\",\\s*\"oxidized_copper_chest\", \"waxed_copper_chest\", \"waxed_exposed_copper_chest\",\\s*\"waxed_weathered_copper_chest\", \"waxed_oxidized_copper_chest\"",
"replacement": "\"chest\", \"trapped_chest\""
},
{
"pattern": "sulfurAndDripstoneTipsReceivePostLoadUpdates",
"replacement": "dripstoneTipsReceivePostLoadUpdates"
}
],
"art/arcane/iris/modded/ModdedWaterEntitySpawnTest.java": [
{
"pattern": "(?ms)^ BuiltInRegistries\\.BLOCK\\.prepareTagReload\\(new TagLoader\\.LoadResult<>.*?\\)\\)\\)\\)\\.apply\\(\\);\\n",
"replacement": ""
}
],
"art/arcane/iris/modded/command/ModdedLanguageCommandsTest.java": [
{
"pattern": "new ClickEvent\\.RunCommand\\(",
"replacement": "new ClickEvent(ClickEvent.Action.RUN_COMMAND, "
}
],
"art/arcane/volmlib/nativelib/minecraft26_2/modded/NativeCommandTextTest.java": [
{
"pattern": "\\(\\(ClickEvent\\.RunCommand\\) component\\.getStyle\\(\\)\\.getClickEvent\\(\\)\\)\\.command\\(\\)",
"replacement": "component.getStyle().getClickEvent().getValue()"
},
{
"pattern": "\\(\\(HoverEvent\\.ShowText\\) component\\.getStyle\\(\\)\\.getHoverEvent\\(\\)\\)\\.value\\(\\)",
"replacement": "component.getStyle().getHoverEvent().getValue(HoverEvent.Action.SHOW_TEXT)"
}
],
"art/arcane/volmlib/nativelib/minecraft26_2/modded/NativeChunkRegenerationTest.java": [
{
"pattern": "\\.markUnsaved\\(\\)",
"replacement": ".setUnsaved(true)"
}
],
"art/arcane/iris/modded/ModdedSavedBiomeConsumersTest.java": [
{
"pattern": "player\\.level\\(\\)",
"replacement": "player.serverLevel()"
}
]
}
}
+22 -6
View File
@@ -22,6 +22,7 @@ import org.gradle.api.file.FileCopyDetails
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.file.FileTree
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.api.attributes.Bundling
import org.gradle.jvm.toolchain.JavaLanguageVersion
import javax.inject.Inject
@@ -42,6 +43,8 @@ plugins {
alias(libs.plugins.shadow)
}
apply from: file('../../gradle/mod-target.gradle')
apply plugin: 'art.arcane.volmit-packaging'
interface IrisFileOperations {
@@ -58,7 +61,7 @@ Properties rootProperties = new Properties()
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.3'))
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse(rootProperties.getProperty('neoForgeVersion', '26.3.0.33-beta'))
String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse(irisModTarget.neoForge.toString())
Closure<String> irisArtifactName = { String platform, String targetVersion ->
return "Iris v${project.version} [${platform}] ${targetVersion}.jar"
}
@@ -144,7 +147,7 @@ dependencies {
}
testRuntimeOnly('org.junit.platform:junit-platform-launcher:6.1.2')
testRuntimeOnly('org.junit.vintage:junit-vintage-engine:6.1.2')
compileOnly('org.slf4j:slf4j-api:2.0.17')
compileOnly("org.slf4j:slf4j-api:${irisModTarget.slf4j}")
compileOnly(libs.spigot) {
transitive = false
}
@@ -173,8 +176,15 @@ dependencies {
libs.jaxen
]
shared.each { Object notation ->
add('bundle', notation)
add('devBundle', notation)
['bundle', 'devBundle'].each { String configuration ->
add(configuration, notation) {
if (group == 'art.arcane' && name == 'core') {
attributes {
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL))
}
}
}
}
}
}
@@ -197,7 +207,7 @@ sourceSets.modLibraries.output.classesDirs.setFrom(stageDevelopmentModLibraries)
neoForge {
version = neoForgeVersion
accessTransformers.from(art.arcane.volmit.packaging.NativeAccessRules.prepare(nativeSourceImplementation, "neoforge", layout.buildDirectory.dir("native-access").get().asFile))
accessTransformers.from(art.arcane.volmit.packaging.NativeAccessRules.prepare(irisModTarget.nativeAccess.toString(), "neoforge", layout.buildDirectory.dir("native-access").get().asFile))
runs {
client {
@@ -242,7 +252,7 @@ neoForge {
tasks.named('compileJava', JavaCompile).configure {
options.encoding = 'UTF-8'
options.release.set(25)
options.release.set((int) irisModTarget.java)
}
tasks.named('test').configure {
@@ -277,6 +287,10 @@ tasks.named('shadowJar', ShadowJar).configure {
configurations = [project.configurations.named('bundle').get()]
from(sourceSets.main.output)
mergeServiceFiles()
if ((int) irisModTarget.java == 21) {
exclude('art/arcane/volmlib/util/noise/NativeBillowLoader*.class')
exclude('art/arcane/volmlib/util/noise/BillowBatch*.class')
}
exclude('META-INF/maven/**')
exclude('META-INF/proguard/**')
exclude('META-INF/*.SF')
@@ -342,3 +356,5 @@ pluginPackaging {
}
}
}
apply from: file('../../gradle/modded-version-sources.gradle')
@@ -0,0 +1,29 @@
package art.arcane.iris.neoforge;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedLifecycleCallbacks;
import art.arcane.iris.modded.service.ModdedTreeFellerService;
import art.arcane.volmlib.nativelib.modded.NativeLoaderOptions;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeChunkGeneratorDefinition;
import art.arcane.volmlib.nativelib.minecraft26_2.neoforge.NativeNeoForgeLoader;
import art.arcane.volmlib.nativelib.minecraft26_2.neoforge.NativeNeoForgeBootstrap;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.common.Mod;
import net.neoforged.fml.loading.FMLLoader;
@Mod("irisworldgen")
public final class IrisNeoForgeBootstrap {
public IrisNeoForgeBootstrap(IEventBus modBus) {
NativeNeoForgeLoader loader = new NativeNeoForgeLoader(new NativeLoaderOptions(
"irisworldgen", "treefeller", ModdedTreeFellerService::runBreakProbe));
NativeChunkGeneratorDefinition generator = IrisModdedChunkGenerator.DEFINITION;
ModdedEngineBootstrap.bootCommon(loader, "NeoForge " + FMLLoader.versionInfo().neoForgeVersion(),
() -> NativeNeoForgeBootstrap.registerGenerator(modBus, generator));
NeoForgeProtocolNetworking.register(modBus);
if (loader.clientEnvironment()) {
IrisNeoForgeClient.init(modBus);
}
NativeNeoForgeBootstrap.install(modBus, loader, ModdedLifecycleCallbacks.create());
}
}
@@ -0,0 +1,37 @@
modLoader = "javafml"
loaderVersion = "[3,)"
license = "GPL-3.0"
issueTrackerURL = "https://github.com/VolmitSoftware/Iris/issues"
[[mixins]]
config = "volmlib.entity.mixins.json"
[[mixins]]
config = "volmlib.client.mixins.json"
[[accessTransformers]]
file = "META-INF/accesstransformer.cfg"
[[mods]]
modId = "irisworldgen"
version = "${version}"
displayName = "Iris"
description = "Iris World Generation Engine (NeoForge adapter - native chunk generator, explicit Iris world datapack workflow, engine lifecycle)"
authors = "Arcane Arts (Volmit Software)"
credits = "cyberpwn, NextdoorPsycho, Vatuu"
displayURL = "https://docs.volmit.com/iris/"
logoFile = "assets/irisworldgen/icon.png"
[[dependencies.irisworldgen]]
modId = "neoforge"
type = "required"
versionRange = "[21.1,21.2)"
ordering = "NONE"
side = "BOTH"
[[dependencies.irisworldgen]]
modId = "minecraft"
type = "required"
versionRange = "[${minecraftVersion}]"
ordering = "NONE"
side = "BOTH"
+121 -20
View File
@@ -6,6 +6,8 @@ import io.papermc.paperweight.userdev.PaperweightUserDependenciesExtension
import io.papermc.paperweight.userdev.PaperweightUserExtension
import org.gradle.api.artifacts.component.ComponentIdentifier
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.provider.Provider
import org.gradle.jvm.toolchain.JavaLauncher
import org.gradle.jvm.toolchain.JavaToolchainService
import org.gradle.api.file.CopySpec
import org.gradle.api.file.DeleteSpec
@@ -71,12 +73,14 @@ project(':core') {
group = 'art.arcane'
version = rootProject.version
}
apply from: file('gradle/mod-target.gradle')
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse('26.3')
// The Bukkit jar retains 26.1.2 and 26.2 alongside 26.3; loader jars are 26.3-only.
// The Bukkit jar retains 26.1.2 and 26.2 alongside 26.3.
String bukkitMinecraftRange = providers.gradleProperty('bukkitMinecraftRange').getOrElse('26.1.2-26.3')
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse('0.19.5')
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse('26.3-66.0.8')
String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse('26.3.0.33-beta')
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(irisModTarget.fabricLoader.toString())
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(irisModTarget.forge.toString())
String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse(irisModTarget.neoForge.toString())
String volmLibShared = 'com.github.VolmitSoftware.VolmLib:volmlib-shared:master-SNAPSHOT'
File localVolmLibCheckout = (File) gradle.extensions.extraProperties.get('volmLibDirectory')
IncludedBuild localVolmLibBuild = localVolmLibCheckout == null ? null : gradle.includedBuilds.find { IncludedBuild includedBuild ->
@@ -104,18 +108,18 @@ File pluginOutDirectory = file(providers.gradleProperty('pluginOutDirectory').ge
File legacyBukkitJar = layout.buildDirectory.file("libs/Iris-${project.version}.jar").get().asFile
File bukkitArtifact = layout.buildDirectory.file("libs/${bukkitArtifactName}").get().asFile
File consumerBukkitArtifact = layout.buildDirectory.file("consumer/${bukkitArtifactName}").get().asFile
File fabricArtifact = layout.projectDirectory.file("adapters/fabric/build/libs/${fabricArtifactName}").asFile
File forgeArtifact = layout.projectDirectory.file("adapters/forge/build/libs/${forgeArtifactName}").asFile
File neoForgeArtifact = layout.projectDirectory.file("adapters/neoforge/build/libs/${neoForgeArtifactName}").asFile
File fabricArtifact = layout.projectDirectory.file("adapters/fabric/build/${minecraftVersion}/libs/${fabricArtifactName}").asFile
File forgeArtifact = layout.projectDirectory.file("adapters/forge/build/${minecraftVersion}/libs/${forgeArtifactName}").asFile
File neoForgeArtifact = layout.projectDirectory.file("adapters/neoforge/build/${minecraftVersion}/libs/${neoForgeArtifactName}").asFile
File consumerDirectory = layout.buildDirectory.dir('consumer').get().asFile
FileTree staleConsumerBukkitJars = fileTree(consumerDirectory) {
include('Iris v* [CraftBukkit] *.jar')
}
Map<String, FileTree> staleOutJars = [
CraftBukkit: 'Iris v* [CraftBukkit] *.jar',
Fabric : 'Iris v* [Fabric] *.jar',
Forge : 'Iris v* [Forge] *.jar',
NeoForge : 'Iris v* [NeoForge] *.jar'
Fabric : "Iris v* [Fabric] ${minecraftVersion}+*.jar",
Forge : "Iris v* [Forge] ${minecraftVersion}+*.jar",
NeoForge : "Iris v* [NeoForge] ${minecraftVersion}+*.jar"
].collectEntries { String platform, String pattern ->
[(platform): fileTree(pluginOutDirectory) { include(pattern) }]
}
@@ -323,7 +327,10 @@ List<Object> moddedSharedInputs = []
'gradle/volmlib-stamp.gradle',
'gradle/generation-build-revision.gradle',
'gradle/modded-generation-build-revision.gradle',
'gradle/native-minecraft-sources.gradle'
'gradle/native-minecraft-sources.gradle',
'gradle/mod-target.gradle',
'gradle/mod-targets.json',
'gradle/modded-version-sources.gradle'
].each { String path -> moddedSharedInputs.add(layout.projectDirectory.file(path).asFile) }
if (localVolmLibCheckout != null) {
moddedSharedInputs.add(fileTree(localVolmLibCheckout) {
@@ -358,7 +365,7 @@ Closure<Void> declareModdedJarIo = { Exec task, String adapter, String artifactN
).withPropertyName('adapterSources')
versions.each { String key, String value -> task.inputs.property(key, value) }
declareVolmLibInputs(task)
task.outputs.file(layout.projectDirectory.file("adapters/${adapter}/build/libs/${artifactName}"))
task.outputs.file(layout.projectDirectory.file("adapters/${adapter}/build/${minecraftVersion}/libs/${artifactName}"))
return null
}
@@ -368,7 +375,7 @@ tasks.register('fabricJar', Exec) {
String wrapperScript = System.getProperty('os.name').toLowerCase().contains('windows') ? 'gradlew.bat' : 'gradlew'
List<String> command = [
layout.projectDirectory.file(wrapperScript).asFile.absolutePath,
'shadowJar',
'assemble',
'--console=plain',
"-PirisVersion=${project.version}",
"-PminecraftVersion=${minecraftVersion}",
@@ -389,7 +396,7 @@ tasks.register('fabricJar', Exec) {
tasks.register('buildFabric', Copy) {
group = 'iris'
dependsOn('fabricJar')
from(layout.projectDirectory.file("adapters/fabric/build/libs/${fabricArtifactName}"))
from(layout.projectDirectory.file("adapters/fabric/build/${minecraftVersion}/libs/${fabricArtifactName}"))
into(pluginOutDirectory)
File legacyOutJar = new File(pluginOutDirectory, "Iris-${project.version}+mc${minecraftVersion}-fabric.jar")
doFirst {
@@ -405,7 +412,7 @@ tasks.register('forgeJar', Exec) {
String wrapperScript = System.getProperty('os.name').toLowerCase().contains('windows') ? 'gradlew.bat' : 'gradlew'
List<String> command = [
layout.projectDirectory.file(wrapperScript).asFile.absolutePath,
'shadowJar',
'assemble',
'--console=plain',
"-PirisVersion=${project.version}",
"-PminecraftVersion=${minecraftVersion}",
@@ -426,7 +433,7 @@ tasks.register('forgeJar', Exec) {
tasks.register('buildForge', Copy) {
group = 'iris'
dependsOn('forgeJar')
from(layout.projectDirectory.file("adapters/forge/build/libs/${forgeArtifactName}"))
from(layout.projectDirectory.file("adapters/forge/build/${minecraftVersion}/libs/${forgeArtifactName}"))
into(pluginOutDirectory)
File legacyOutJar = new File(pluginOutDirectory, "Iris-${project.version}+mc${minecraftVersion}-forge.jar")
doFirst {
@@ -442,7 +449,7 @@ tasks.register('neoforgeJar', Exec) {
String wrapperScript = System.getProperty('os.name').toLowerCase().contains('windows') ? 'gradlew.bat' : 'gradlew'
List<String> command = [
layout.projectDirectory.file(wrapperScript).asFile.absolutePath,
'shadowJar',
'assemble',
'--console=plain',
"-PirisVersion=${project.version}",
"-PminecraftVersion=${minecraftVersion}",
@@ -463,7 +470,7 @@ tasks.register('neoforgeJar', Exec) {
tasks.register('buildNeoforge', Copy) {
group = 'iris'
dependsOn('neoforgeJar')
from(layout.projectDirectory.file("adapters/neoforge/build/libs/${neoForgeArtifactName}"))
from(layout.projectDirectory.file("adapters/neoforge/build/${minecraftVersion}/libs/${neoForgeArtifactName}"))
into(pluginOutDirectory)
File legacyOutJar = new File(pluginOutDirectory, "Iris-${project.version}+mc${minecraftVersion}-neoforge.jar")
doFirst {
@@ -521,7 +528,7 @@ tasks.register('moddedTest', Exec) {
inputs.property('minecraftVersion', minecraftVersion)
inputs.property('fabricLoaderVersion', fabricLoaderVersion)
declareVolmLibInputs(it)
outputs.dir(layout.projectDirectory.dir('adapters/fabric/build/test-results/test'))
outputs.dir(layout.projectDirectory.dir("adapters/fabric/build/${minecraftVersion}/test-results/test"))
mustRunAfter(tasks.named('jar'))
mustRunAfter(project(':core').tasks.named('check'))
mustRunAfter(project(':spi').tasks.named('jar'))
@@ -549,6 +556,7 @@ tasks.named('check').configure {
}
List<String> commonModdedArtifactEntries = [
'agent.jar',
'pack.mcmeta',
'com/fulcrumgenomics/jlibdeflate/LibdeflateDecompressor.class',
'com/fulcrumgenomics/jlibdeflate/LibdeflateException.class',
@@ -601,6 +609,7 @@ tasks.register('verifyFabricArtifact') {
'art/arcane/iris/fabric/IrisFabricBootstrap.class'
]
ModdedArtifactVerifier.verify(artifact, requiredEntries, bukkitCoupledCoreClasses)
ModdedArtifactVerifier.verifyJavaRuntime(artifact, (int) irisModTarget.java)
logger.lifecycle("Verified ${artifact.name} packaging, mixin compat, and nested jars")
}
}
@@ -618,6 +627,7 @@ tasks.register('verifyForgeArtifact') {
'art/arcane/iris/forge/IrisForgeBootstrap.class'
]
ModdedArtifactVerifier.verify(artifact, requiredEntries, bukkitCoupledCoreClasses)
ModdedArtifactVerifier.verifyJavaRuntime(artifact, (int) irisModTarget.java)
logger.lifecycle("Verified ${artifact.name} packaging, mixin compat, and nested jars")
}
}
@@ -635,6 +645,7 @@ tasks.register('verifyNeoforgeArtifact') {
'art/arcane/iris/neoforge/IrisNeoForgeBootstrap.class'
]
ModdedArtifactVerifier.verify(artifact, requiredEntries, bukkitCoupledCoreClasses)
ModdedArtifactVerifier.verifyJavaRuntime(artifact, (int) irisModTarget.java)
logger.lifecycle("Verified ${artifact.name} packaging, mixin compat, and nested jars")
}
}
@@ -644,6 +655,59 @@ tasks.register('verifyModdedArtifacts') {
dependsOn('verifyFabricArtifact', 'verifyForgeArtifact', 'verifyNeoforgeArtifact')
}
TaskProvider<Task> verifyModdedMatrix = tasks.register('verifyModdedMatrix') {
group = 'verification'
description = 'Builds and verifies every supported Minecraft mod target and runs the shared mod tests.'
}
TaskProvider<Exec> previousMatrixBuild = null
irisModTargets.each { String targetMinecraft, Map<String, Object> target ->
['fabric': 'Fabric', 'forge': 'Forge', 'neoforge': 'NeoForge'].each { String loader, String label ->
String pinKey = loader == 'fabric' ? 'fabricLoader' : loader == 'neoforge' ? 'neoForge' : 'forge'
String loaderPin = target.get(pinKey).toString()
String displayedLoader = loaderPin.startsWith(targetMinecraft + '-')
? loaderPin.substring(targetMinecraft.length() + 1) : loaderPin
String artifactName = irisArtifactName(label, "${targetMinecraft}+${displayedLoader}")
File artifact = file("adapters/${loader}/build/${targetMinecraft}/libs/${artifactName}")
TaskProvider<Exec> buildTarget = tasks.register("verify${label}${targetMinecraft.replace('.', '_')}", Exec) {
group = 'verification'
workingDir = file("adapters/${loader}")
String wrapperScript = System.getProperty('os.name').toLowerCase().contains('windows') ? 'gradlew.bat' : 'gradlew'
List<String> command = [file(wrapperScript).absolutePath, 'assemble', 'compileTestJava',
'verifyLoggingPolicy', 'verifyNativeClassBoundary', '--console=plain',
"-PirisVersion=${project.version}", "-PminecraftVersion=${targetMinecraft}",
"-PuseLocalVolmLib=${localVolmLibCheckout != null}"]
if (loader == 'fabric') {
command.add('test')
}
commandLine(command)
if (localVolmLibCheckout != null) {
environment('ORG_GRADLE_PROJECT_localVolmLibDirectory', localVolmLibCheckout.absolutePath)
}
inputs.files(moddedSharedInputs, file("adapters/${loader}/src"), file("adapters/${loader}/build.gradle"),
file("adapters/${loader}/settings.gradle"))
inputs.properties(target)
declareVolmLibInputs(it)
outputs.file(artifact)
if (loader == 'fabric') {
outputs.dir(file("adapters/${loader}/build/${targetMinecraft}/test-results/test"))
}
doLast {
String metadata = loader == 'fabric' ? 'fabric.mod.json'
: loader == 'forge' ? 'META-INF/mods.toml' : 'META-INF/neoforge.mods.toml'
ModdedArtifactVerifier.verify(artifact, commonModdedArtifactEntries + [metadata], bukkitCoupledCoreClasses)
ModdedArtifactVerifier.verifyJavaRuntime(artifact, (int) target.java)
logger.lifecycle("Verified ${artifact.name}")
}
}
if (previousMatrixBuild != null) {
TaskProvider<Exec> preceding = previousMatrixBuild
buildTarget.configure { mustRunAfter(preceding) }
}
previousMatrixBuild = buildTarget
verifyModdedMatrix.configure { dependsOn(buildTarget) }
}
}
// Entries the Bukkit artifact only ever reaches by name - NMS binding lookup, the SIMD kernels, the
// instrumentation agent, the JarScanner-driven command and service discovery, the Matter codec - plus
// the two plugin descriptors and the slimjar manifests. Nothing in the reference graph points at
@@ -805,9 +869,18 @@ tasks.register('irisDev', Copy) {
int defaultTestForks = Math.min(8, Math.max(1, Runtime.runtime.availableProcessors().intdiv(2)))
int testForks = Math.max(1, Integer.parseInt(providers.gradleProperty('testForks')
.getOrElse(String.valueOf(defaultTestForks))))
int irisRuntimeJava = providers.gradleProperty('irisRuntimeJava').getOrElse(irisModTarget.java.toString()).toInteger()
if (!(irisRuntimeJava in [21, 25])) {
throw new GradleException('irisRuntimeJava must be 21 or 25')
}
Set<String> sharedRuntimeProjects = [':core', ':core:agent', ':spi'] as Set<String>
allprojects {
apply plugin: 'java'
boolean java21Runtime = irisRuntimeJava == 21 && sharedRuntimeProjects.contains(project.path)
if (java21Runtime) {
layout.buildDirectory.set(layout.projectDirectory.dir('build/java21'))
}
tasks.withType(Test).configureEach {
int moduleForks = project.path.startsWith(':adapters:bukkit:') ? Math.min(2, testForks)
@@ -820,6 +893,32 @@ allprojects {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
}
if (java21Runtime) {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
}
if (java21Runtime) {
Provider<JavaLauncher> runtimeLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(21)
}
tasks.withType(JavaCompile).configureEach {
sourceCompatibility = '21'
targetCompatibility = '21'
if (project.path == ':core') {
options.compilerArgumentProviders.add({
['--system', runtimeLauncher.get().metadata.installationPath.asFile.absolutePath]
} as org.gradle.process.CommandLineArgumentProvider)
} else {
options.release.set(21)
}
}
if (project.path != ':core') {
tasks.withType(Test).configureEach {
javaLauncher.set(runtimeLauncher)
}
}
}
repositories {
@@ -881,7 +980,9 @@ allprojects {
options.compilerArgs.add('-parameters')
options.encoding = 'UTF-8'
options.debugOptions.debugLevel = 'none'
options.release.set(25)
if (!java21Runtime) {
options.release.set(25)
}
}
tasks.named('javadoc').configure {
@@ -2,6 +2,8 @@ import org.gradle.api.GradleException;
import org.objectweb.asm.ClassReader;
import java.io.File;
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
@@ -39,7 +41,8 @@ public final class ModdedArtifactVerifier {
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"
"art/arcane/iris/shadow/jna",
"art/arcane/iris/util/gson"
);
// ASM must always be relocated: the loaders put their own copy on the module layer and a second
// org.objectweb.asm package on the same layer is a boot failure.
@@ -66,6 +69,14 @@ public final class ModdedArtifactVerifier {
private ModdedArtifactVerifier() {
}
public static void verifyJavaRuntime(File artifact, int runtimeJava) {
try (JarInputStream archive = new JarInputStream(new FileInputStream(artifact))) {
verifyJavaRuntime(archive, runtimeJava, artifact.getName());
} catch (IOException error) {
throw new GradleException("Could not verify Java " + runtimeJava + " compatibility of " + artifact, error);
}
}
public static void verify(File artifact, List<String> requiredEntries) {
verify(artifact, requiredEntries, Set.of());
}
@@ -192,6 +203,38 @@ public final class ModdedArtifactVerifier {
}
}
private static void verifyJavaRuntime(JarInputStream archive, int runtimeJava, String location) throws IOException {
JarEntry entry;
while ((entry = archive.getNextJarEntry()) != null) {
String name = entry.getName();
if (entry.isDirectory()) {
continue;
}
if (name.startsWith("META-INF/versions/")) {
String version = name.substring("META-INF/versions/".length()).split("/", 2)[0];
if (Integer.parseInt(version) > runtimeJava) {
continue;
}
}
if (name.endsWith(".jar")) {
try (JarInputStream nested = new JarInputStream(new ByteArrayInputStream(archive.readAllBytes()))) {
verifyJavaRuntime(nested, runtimeJava, location + "!/" + name);
}
} else if (name.endsWith(".class")) {
byte[] header = archive.readNBytes(8);
if (header.length != 8 || header[0] != (byte) 0xca || header[1] != (byte) 0xfe
|| header[2] != (byte) 0xba || header[3] != (byte) 0xbe) {
throw new GradleException("Invalid class file " + location + "!/" + name);
}
int major = (header[6] & 0xff) << 8 | header[7] & 0xff;
if (major > runtimeJava + 44) {
throw new GradleException(location + "!/" + name + " requires Java " + (major - 44)
+ "; target runtime is Java " + runtimeJava);
}
}
}
}
private static void verifyNestedJarDeclarations(File artifact, JarFile jar, Set<String> shippedNestedJars)
throws IOException {
String metadata = readEntryText(jar, FABRIC_METADATA);
@@ -15,7 +15,10 @@ public final class GenerationRevisionScope {
"adapters/client-common/src/main/java",
"adapters/fabric/src/main/java",
"adapters/forge/src/main/java",
"adapters/neoforge/src/main/java"
"adapters/neoforge/src/main/java",
"adapters/forge/src/versions/1.21.1/java",
"adapters/neoforge/src/versions/1.21.1/java",
"adapters/modded-common/src/versions/1.21.1/java"
);
private static final List<String> OPERATIONAL_EXCLUSIONS = List.of(
"core/src/main/java/art/arcane/iris/generation/runtime/EngineDiagnostics.java",
@@ -11,6 +11,7 @@ import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -38,6 +39,24 @@ public class ModdedArtifactVerifierTest {
@Rule
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void rejectsClassesNewerThanSelectedRuntimeIncludingNestedJars() throws Exception {
ClassWriter writer = new ClassWriter(0);
writer.visit(Opcodes.V25, Opcodes.ACC_PUBLIC, "example/NewRuntime", null, "java/lang/Object", null);
writer.visitEnd();
Map<String, byte[]> entries = new LinkedHashMap<>();
entries.put("example/NewRuntime.class", writer.toByteArray());
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verifyJavaRuntime(artifact, 21));
assertTrue(failure.getMessage().contains("requires Java 25"));
ModdedArtifactVerifier.verifyJavaRuntime(artifact, 25);
File nested = createArtifact(Map.of("META-INF/jars/library.jar", Files.readAllBytes(artifact.toPath())));
GradleException nestedFailure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verifyJavaRuntime(nested, 21));
assertTrue(nestedFailure.getMessage().contains("library.jar!/example/NewRuntime.class"));
}
@Test
public void acceptsMinecraftRuntimeReferences() throws Exception {
Map<String, byte[]> entries = validEntries();
@@ -166,6 +185,16 @@ public class ModdedArtifactVerifierTest {
assertTrue(failure.getMessage().contains("Iris-private runtime references"));
}
@Test
public void rejectsBukkitRelocatedGsonReferences() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.put("art/arcane/iris/Test.class", "art/arcane/iris/util/gson/Gson"
.getBytes(StandardCharsets.ISO_8859_1));
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(createArtifact(entries), REQUIRED_ENTRIES));
assertTrue(failure.getMessage().contains("Iris-private runtime references"));
}
@Test
public void rejectsBundledMinecraftRuntimeLibrary() throws Exception {
Map<String, byte[]> entries = validEntries();
+39 -4
View File
@@ -1,4 +1,5 @@
import io.github.slimjar.resolver.data.Mirror
import org.gradle.api.attributes.java.TargetJvmVersion
import org.gradle.api.file.FileTreeElement
import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.TaskProvider
@@ -197,6 +198,11 @@ slimJar {
}
def embeddedAgentJar = project(':core:agent').tasks.named('jar', Jar)
tasks.named('processResources').configure {
from(embeddedAgentJar.map { it.archiveFile }) {
rename { String ignored -> 'agent.jar' }
}
}
def templateSource = file('src/main/templates')
def templateDest = layout.buildDirectory.dir('generated/sources/templates')
def generateTemplates = tasks.register('generateTemplates', Copy) {
@@ -263,7 +269,35 @@ tasks.named('compileJava', JavaCompile).configure {
tasks.named('test', Test).configure {
jvmArgs('--add-modules', 'jdk.incubator.vector')
dependsOn(languageSourceManifestTask)
classpath = files(layout.buildDirectory.dir('generated/language-source')) + files('src/main/resources') + sourceSets.main.output + classpath
classpath = (files(layout.buildDirectory.dir('generated/language-source')) + files('src/main/resources')
+ sourceSets.main.output + classpath) - files(tasks.named('shadowJar'))
}
if (providers.gradleProperty('irisRuntimeJava').getOrElse(rootProject.irisModTarget.java.toString()) == '21') {
configurations.configureEach {
if (name == 'compileClasspath' || name.endsWith('CompileClasspath')
|| name in ['testRuntimeClasspath', 'externalProviderTestRuntimeClasspath']) {
attributes.attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 25)
}
}
tasks.register('java21RuntimeTest', Test) {
group = 'verification'
description = 'Verifies generation kernels and pack updates on the Minecraft 1.21.1 Java runtime.'
testClassesDirs = sourceSets.test.output.classesDirs
classpath = (files(layout.buildDirectory.dir('generated/language-source')) + files('src/main/resources')
+ sourceSets.main.output + sourceSets.test.runtimeClasspath) - files(tasks.named('shadowJar'))
javaLauncher.set(javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(21)
})
jvmArgs('--add-modules', 'jdk.incubator.vector')
filter {
includeTestsMatching('art.arcane.iris.generation.simd.*')
includeTestsMatching('art.arcane.iris.pack.BuiltInPackUpdatesTest')
}
}
tasks.named('check').configure {
dependsOn('java21RuntimeTest')
}
}
configurations.matching { it.name.startsWith('slim') }.all { }
@@ -417,6 +451,10 @@ tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.Shadow
inputs.property('languageTomlSupportClassPattern', languageTomlSupportClassPattern)
dependsOn(embeddedAgentJar)
mergeServiceFiles()
if (providers.gradleProperty('irisRuntimeJava').getOrElse(rootProject.irisModTarget.java.toString()) == '21') {
exclude('art/arcane/volmlib/util/noise/NativeBillowLoader*.class')
exclude('art/arcane/volmlib/util/noise/BillowBatch*.class')
}
// minimize() stays off. Caffeine reaches its 520 generated cache and node classes through
// MethodHandles.Lookup.findClass on a computed name, and the VolmLib command, GUI and hotload
// surfaces are only referenced from :adapters:bukkit:plugin, which is merged into the artifact
@@ -434,9 +472,6 @@ tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.Shadow
exclude(unusedVolmLibClassEntries)
exclude(annotationOnlyArtifacts)
exclude(dependencyBuildMetadata)
from(embeddedAgentJar.map { it.archiveFile }) {
rename { String ignored -> 'agent.jar' }
}
}
rootProject.tasks.matching {
@@ -22,7 +22,6 @@ import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.Strictness;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.platform.reflect.KeyedType;
import art.arcane.volmlib.util.collection.KMap;
@@ -51,7 +50,7 @@ import java.util.concurrent.ConcurrentHashMap;
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class TileData implements Cloneable {
private static final Gson gson = new GsonBuilder().disableHtmlEscaping().setStrictness(Strictness.LENIENT).setObjectToNumberStrategy(com.google.gson.ToNumberPolicy.LONG_OR_DOUBLE).create();
private static final Gson gson = new GsonBuilder().disableHtmlEscaping().setLenient().setObjectToNumberStrategy(com.google.gson.ToNumberPolicy.LONG_OR_DOUBLE).create();
/**
* Memoized {@link #resolveMaterial()}. Pasting one tile resolves its material at least twice
* ({@link #isApplicable(BlockData)} then {@link #toBukkit(Block)}), and matchMaterial normalizes the string and
@@ -1360,11 +1360,12 @@ public class IrisDimension extends IrisRegistrant {
"pack": {
"description": "Iris Data Pack. This pack contains all installed Iris Packs' resources.",
"pack_format": %d,
"supported_formats": [%d, %d],
"min_format": %d,
"max_format": %d
}
}
""".formatted(maxFormat, minFormat, maxFormat);
""".formatted(maxFormat, minFormat, maxFormat, minFormat, maxFormat);
for (File datapackRoot : datapackRoots) {
File mcm = new File(datapackRoot, "pack.mcmeta");
@@ -467,7 +467,7 @@ public class IrisRegion extends IrisRegistrant implements Rarity {
}
/**
* Cascade: a region that lost every land biome to the gate cannot generate, so it leaves the dimension's region
* Cascade: a region that lost every biome in a declared surface pool cannot generate, so it leaves the dimension's region
* pool too. Loading biomes here is safe - biomes never load regions.
*/
@Override
@@ -489,17 +489,9 @@ public class IrisRegion extends IrisRegistrant implements Rarity {
"structures[" + index + "].caveBiomes");
}
if (getLandBiomes().isEmpty()) {
return base;
}
List<CompatFinding> drops = new ArrayList<>();
if (!resolvePool(() -> data, getLandBiomes(), "landBiomes", drops).isEmpty()) {
return base;
}
return CompatPools.cascade(data, base, drops, "region", getLoadKey(), "no land biomes remain");
base = evaluateSurfacePoolCompat(data, base, getLandBiomes(), "land");
base = evaluateSurfacePoolCompat(data, base, getSeaBiomes(), "sea");
return evaluateSurfacePoolCompat(data, base, getShoreBiomes(), "shore");
}
@Override
@@ -511,4 +503,17 @@ public class IrisRegion extends IrisRegistrant implements Rarity {
public String getTypeName() {
return "Region";
}
private CompatStatus evaluateSurfacePoolCompat(IrisData data, CompatStatus base, KList<String> keys, String role) {
if (base.excluded() || keys.isEmpty()) {
return base;
}
List<CompatFinding> drops = new ArrayList<>();
if (!resolvePool(() -> data, keys, role + "Biomes", drops).isEmpty()) {
return base;
}
return CompatPools.cascade(data, base, drops, "region", getLoadKey(), "no " + role + " biomes remain");
}
}
@@ -4,16 +4,20 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.IOException;
import java.math.BigInteger;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Flow;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.regex.Pattern;
@@ -48,7 +52,7 @@ public final class BuiltInPackUpdates {
.GET()
.build();
CompletableFuture<HttpResponse<String>> response = client.sendAsync(request,
HttpResponse.BodyHandlers.limiting(HttpResponse.BodyHandlers.ofString(), 1024L * 1024L));
info -> new ReleaseBodySubscriber(HttpResponse.BodyHandlers.ofString().apply(info)));
requests.put(pack, response.copy().orTimeout(REQUEST_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS)
.whenComplete((result, error) -> {
if (error != null) {
@@ -109,4 +113,58 @@ public final class BuiltInPackUpdates {
: "";
}
}
private static final class ReleaseBodySubscriber implements HttpResponse.BodySubscriber<String> {
private final HttpResponse.BodySubscriber<String> body;
private Flow.Subscription subscription;
private int remaining = 1024 * 1024;
private boolean complete;
private ReleaseBodySubscriber(HttpResponse.BodySubscriber<String> body) {
this.body = body;
}
@Override
public CompletionStage<String> getBody() {
return body.getBody();
}
@Override
public void onSubscribe(Flow.Subscription subscription) {
this.subscription = subscription;
body.onSubscribe(subscription);
}
@Override
public void onNext(List<ByteBuffer> buffers) {
if (complete) {
return;
}
for (ByteBuffer buffer : buffers) {
if (buffer.remaining() > remaining) {
subscription.cancel();
onError(new IOException("Release metadata exceeds 1 MiB"));
return;
}
remaining -= buffer.remaining();
}
body.onNext(buffers);
}
@Override
public void onError(Throwable error) {
if (!complete) {
complete = true;
body.onError(error);
}
}
@Override
public void onComplete() {
if (!complete) {
complete = true;
body.onComplete();
}
}
}
}
@@ -2,6 +2,7 @@ package art.arcane.iris.pack.datapack;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.pack.datapack.v1217.DataFixerV1217;
import art.arcane.iris.pack.datapack.v1206.DataFixerV1206;
import art.arcane.volmlib.util.collection.KMap;
import lombok.AccessLevel;
import lombok.Getter;
@@ -13,6 +14,7 @@ import art.arcane.iris.pack.datapack.v263.DataFixerV263;
@Getter
public enum DataVersion {
UNSUPPORTED("0.0.0", 0, () -> null),
V1_21_1("1.21.1", 48, DataFixerV1206::new),
V26_1_2("26.1.2", 101, DataFixerV1217::new),
V26_2("26.2", 107, DataFixerV1217::new),
V26_3("26.3", 121, DataFixerV263::new);
@@ -41,6 +43,7 @@ public enum DataVersion {
return UNSUPPORTED;
}
return switch (version) {
case "1.21.1" -> V1_21_1;
case "26.1.2" -> V26_1_2;
case "26.2", "26.2.0" -> V26_2;
case "26.3", "26.3.0" -> V26_3;
@@ -25,7 +25,6 @@ import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.Strictness;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
@@ -676,7 +675,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
builder = new GsonBuilder()
.addDeserializationExclusionStrategy(this)
.addSerializationExclusionStrategy(this)
.setStrictness(Strictness.LENIENT)
.setLenient()
.registerTypeAdapterFactory(this)
.registerTypeAdapter(MantleFlag.class, new MantleFlagAdapter())
.setPrettyPrinting();
@@ -718,7 +717,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
builder = new GsonBuilder()
.addDeserializationExclusionStrategy(this)
.addSerializationExclusionStrategy(this)
.setStrictness(Strictness.LENIENT)
.setLenient()
.registerTypeAdapterFactory(this)
.registerTypeAdapter(MantleFlag.class, new MantleFlagAdapter())
.setPrettyPrinting();
@@ -1,6 +1,7 @@
package art.arcane.iris.structure.export;
import art.arcane.iris.structure.graph.CompiledStructureGraph;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.structure.graph.PlanarJigsawWorkcellResolver;
import art.arcane.iris.structure.graph.StructureGraphCompilation;
import art.arcane.iris.structure.graph.StructureGraphCompiler;
@@ -214,6 +215,12 @@ final class VanillaJigsawExportCompiler {
"Minecraft jigsaw size must be within 0..20; Iris export requires 1..20.");
}
long horizontalDistance = (long) structure.getMaxSizeChunks() * 16L;
if (DataVersion.getRuntime() == DataVersion.V1_21_1 && horizontalDistance != settings.maxDistanceVertical()) {
addError(diagnostics,
VanillaJigsawExportDiagnostic.Code.INVALID_MAX_DISTANCE,
"max_distance_from_center.vertical",
"Minecraft 1.21.1 vanilla export requires equal horizontal and vertical maximum distances.");
}
if (horizontalDistance < 1L || horizontalDistance > MAX_HORIZONTAL_DISTANCE) {
addError(diagnostics,
VanillaJigsawExportDiagnostic.Code.INVALID_MAX_DISTANCE,
@@ -576,6 +583,12 @@ final class VanillaJigsawExportCompiler {
private JsonObject packMetadata(VanillaJigsawExportRequest request) {
JsonObject pack = new JsonObject();
pack.addProperty("description", request.description());
if (DataVersion.getRuntime() == DataVersion.V1_21_1) {
pack.addProperty("pack_format", DataVersion.V1_21_1.getPackFormat());
JsonObject root = new JsonObject();
root.add("pack", pack);
return root;
}
JsonArray minimum = new JsonArray();
boolean current = VanillaStructureTemplateEncoder.targets263();
int packFormat = current ? 121 : 107;
@@ -61,7 +61,8 @@ final class VanillaStructureTemplateEncoder {
root.put("palette", palette(paletteStates, format));
root.put("blocks", blocks(orderedBlocks, paletteIndexes));
root.put("entities", new ListTag<>(CompoundTag.class));
root.putInt("DataVersion", lowercase ? 5023 : DATA_VERSION_26_2);
root.putInt("DataVersion", "1.21.1".equals(IrisPlatforms.get().minecraftVersion())
? 3955 : lowercase ? 5023 : DATA_VERSION_26_2);
ByteArrayOutputStream output = new ByteArrayOutputStream();
NBTUtil.write(new NamedTag("", root), output, true);
@@ -7,8 +7,7 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.google.gson.Strictness;
import art.arcane.volmlib.util.json.StrictJsonReader;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
@@ -229,9 +228,9 @@ public final class GenerationHistoryStore {
throw new IOException("Generation manifest size changed while reading: " + manifestPath);
}
try (Reader input = new StringReader(new String(encoded, StandardCharsets.UTF_8));
JsonReader reader = new JsonReader(input)) {
reader.setStrictness(Strictness.STRICT);
JsonElement parsed = JsonParser.parseReader(reader);
JsonReader reader = new JsonReader(new StrictJsonReader(input))) {
reader.setLenient(false);
JsonElement parsed = GSON.getAdapter(JsonElement.class).read(reader);
if (parsed == null || !parsed.isJsonObject()) {
throw new IllegalArgumentException("Generation manifest must be a JSON object.");
}
@@ -314,9 +313,9 @@ public final class GenerationHistoryStore {
if (Files.size(metadata) > MAX_MANIFEST_BYTES) {
throw new IOException("Generation epoch metadata exceeds the supported size: " + metadata);
}
try (JsonReader reader = new JsonReader(Files.newBufferedReader(metadata, StandardCharsets.UTF_8))) {
reader.setStrictness(Strictness.STRICT);
JsonElement parsed = JsonParser.parseReader(reader);
try (JsonReader reader = new JsonReader(new StrictJsonReader(Files.newBufferedReader(metadata, StandardCharsets.UTF_8)))) {
reader.setLenient(false);
JsonElement parsed = GSON.getAdapter(JsonElement.class).read(reader);
if (!parsed.isJsonObject() || reader.peek() != JsonToken.END_DOCUMENT) {
throw new IOException("Invalid generation epoch metadata: " + metadata);
}
@@ -23,6 +23,7 @@ import static art.arcane.iris.testsupport.PackCompatFixtures.find;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -132,6 +133,133 @@ public class IrisRegionCompatPoolTest {
assertFalse(region.evaluateCompat(gate(report)).excluded());
}
@Test
public void evaluateCompatExcludesRegionWithNoShoreBiomesLeft() {
PackCompatReport report = new PackCompatReport();
IrisBiome plains = biome("land/plains");
IrisBiome beach = PackCompatFixtures.exclude(biome("swamp/shore/beach"),
CompatRegistry.BLOCK, "minecraft:firefly_bush");
IrisData data = dataWith(report, plains, beach);
IrisRegion region = PackCompatFixtures.region("swamp");
region.setLoader(data);
region.setLandBiomes(new KList<>("land/plains"));
region.setShoreBiomes(new KList<>("swamp/shore/beach"));
CompatStatus status = region.evaluateCompat(gate(report));
assertTrue(status.excluded());
assertSame(plains, region.getRealLandBiomes(() -> data).getFirst());
CompatFinding cascade = find(report, CompatAction.EXCLUDED, "region", "swamp");
assertNotNull(cascade);
assertEquals("minecraft:firefly_bush", cascade.key());
assertEquals("no shore biomes remain", cascade.detail());
CompatFinding dropped = find(report, CompatAction.DROPPED, "region", "swamp");
assertNotNull(dropped);
assertEquals("shoreBiomes[0] swamp/shore/beach", dropped.detail());
}
@Test
public void evaluateCompatExcludesRegionWithNoSeaBiomesLeft() {
PackCompatReport report = new PackCompatReport();
IrisBiome plains = biome("land/plains");
IrisBiome sea = excludeBlock(biome("sea/sulfur"));
IrisData data = dataWith(report, plains, sea);
IrisRegion region = PackCompatFixtures.region("overworld");
region.setLoader(data);
region.setLandBiomes(new KList<>("land/plains"));
region.setSeaBiomes(new KList<>("sea/sulfur"));
assertTrue(region.evaluateCompat(gate(report)).excluded());
CompatFinding cascade = find(report, CompatAction.EXCLUDED, "region", "overworld");
assertNotNull(cascade);
assertEquals(MISSING_BLOCK, cascade.key());
assertEquals("no sea biomes remain", cascade.detail());
}
@Test
public void evaluateCompatPreservesSurvivingSurfacePoolOrder() {
PackCompatReport report = new PackCompatReport();
IrisBiome plains = biome("land/plains");
IrisBiome sea = biome("sea/ocean");
IrisBiome sand = biome("shore/sand");
IrisBiome gravel = biome("shore/gravel");
IrisBiome sulfur = excludeBlock(biome("shore/sulfur"));
IrisData data = dataWith(report, plains, sea, sand, gravel, sulfur);
IrisRegion region = PackCompatFixtures.region("overworld");
region.setLoader(data);
region.setLandBiomes(new KList<>("land/plains"));
region.setSeaBiomes(new KList<>("shore/sulfur", "sea/ocean"));
region.setShoreBiomes(new KList<>("shore/sand", "shore/sulfur", "shore/gravel"));
assertFalse(region.evaluateCompat(gate(report)).excluded());
KList<IrisBiome> shores = region.getRealShoreBiomes(() -> data);
assertEquals(2, shores.size());
assertSame(sand, shores.get(0));
assertSame(gravel, shores.get(1));
assertSame(sea, region.getRealSeaBiomes(() -> data).getFirst());
}
@Test
public void evaluateCompatAllowsOmittedSurfacePoolsAndDepletedCavePool() {
PackCompatReport report = new PackCompatReport();
IrisBiome plains = biome("land/plains");
IrisBiome sulfur = excludeBlock(biome("cave/sulfur"));
IrisData data = dataWith(report, plains, sulfur);
IrisRegion region = PackCompatFixtures.region("highlands");
region.setLoader(data);
region.setLandBiomes(new KList<>("land/plains"));
region.setCaveBiomes(new KList<>("cave/sulfur"));
assertFalse(region.evaluateCompat(gate(report)).excluded());
assertTrue(region.getRealSeaBiomes(() -> data).isEmpty());
assertTrue(region.getRealShoreBiomes(() -> data).isEmpty());
assertTrue(region.getRealCaveBiomes(() -> data).isEmpty());
}
@Test
public void evaluateCompatDoesNotMisattributeUnresolvedShorePoolToLandDrop() {
PackCompatReport report = new PackCompatReport();
IrisBiome plains = biome("land/plains");
IrisBiome sulfur = excludeBlock(biome("land/sulfur"));
IrisData data = dataWith(report, plains, sulfur);
IrisRegion region = PackCompatFixtures.region("overworld");
region.setLoader(data);
region.setLandBiomes(new KList<>("land/plains", "land/sulfur"));
region.setShoreBiomes(new KList<>("shore/missing"));
assertFalse(region.evaluateCompat(gate(report)).excluded());
}
@Test
@SuppressWarnings("unchecked")
public void dimensionDropsRegionWhoseDeclaredShorePoolWasExcluded() {
PackCompatReport report = new PackCompatReport();
IrisBiome plains = biome("land/plains");
IrisBiome beach = excludeBlock(biome("shore/sulfur"));
IrisData data = dataWith(report, plains, beach);
IrisRegion swamp = PackCompatFixtures.region("swamp");
swamp.setLoader(data);
swamp.setLandBiomes(new KList<>("land/plains"));
swamp.setShoreBiomes(new KList<>("shore/sulfur"));
swamp.setCompat(swamp.evaluateCompat(gate(report)));
IrisRegion highlands = PackCompatFixtures.region("highlands");
ResourceLoader<IrisRegion> regions = mock(ResourceLoader.class);
when(data.getRegionLoader()).thenReturn(regions);
when(regions.load("swamp")).thenReturn(swamp);
when(regions.load("highlands")).thenReturn(highlands);
IrisDimension dimension = new IrisDimension().setRegions(new KList<>("swamp", "highlands"));
dimension.setLoadKey("overworld");
dimension.setLoader(data);
assertFalse(dimension.evaluateCompat(gate(report)).excluded());
KList<IrisRegion> surviving = dimension.getAllRegions(() -> data);
assertEquals(1, surviving.size());
assertSame(highlands, surviving.getFirst());
CompatFinding drop = find(report, CompatAction.DROPPED, "dimension", "overworld");
assertNotNull(drop);
assertEquals("regions[0] swamp", drop.detail());
}
@Test
public void evaluateCompatDoesNotExcludeRegionThatNeverDeclaredLandBiomes() {
PackCompatReport report = new PackCompatReport();
@@ -71,7 +71,15 @@ public class BuiltInPackUpdatesTest {
@Test
public void boundsReleaseMetadataSize() throws Exception {
assertEquals(BuiltInPackUpdates.Update.unavailable(), checkResponse(" ".repeat(1024 * 1024 + 1)));
String release = release("4010", "overworld.zip", false);
assertEquals(BuiltInPackUpdates.Update.unavailable(),
checkResponse(release + " ".repeat(1024 * 1024 - release.length() + 1)));
}
@Test
public void acceptsReleaseMetadataAtSizeLimit() throws Exception {
String release = release("4010", "overworld.zip", false);
assertEquals("4010", checkResponse(release + " ".repeat(1024 * 1024 - release.length())).version());
}
@Test
@@ -58,7 +58,7 @@ public class DefaultPackBootstrapProvisionerTest {
Path dataDirectory = root.resolve("plugins/Iris");
writePack(dataDirectory.resolve("packs/overworld"), "overworld", "runtime");
BukkitStartupPaths startupPaths = BukkitStartupPaths.resolve(root, new String[0]);
for (DataVersion version : new DataVersion[]{DataVersion.V26_1_2, DataVersion.V26_2, DataVersion.V26_3}) {
for (DataVersion version : new DataVersion[]{DataVersion.V1_21_1, DataVersion.V26_1_2, DataVersion.V26_2, DataVersion.V26_3}) {
DefaultPackBootstrapProvisioner.ProvisionResult result = DefaultPackBootstrapProvisioner.provision(
new DefaultPackBootstrapProvisioner.BootstrapRequest(dataDirectory, ignored -> {}, startupPaths, version));
Path biomeRoot = result.datapackRoot().resolve("data/iris/worldgen/biome/biomes");
@@ -9,6 +9,7 @@ import static org.junit.Assert.assertSame;
public class DataVersionTest {
@Test
public void runtimeSelectionPreservesEverySupportedVersion() {
assertSame(DataVersion.V1_21_1, DataVersion.forMinecraftVersion("1.21.1"));
assertSame(DataVersion.V26_1_2, DataVersion.forMinecraftVersion("26.1.2"));
assertSame(DataVersion.V26_2, DataVersion.forMinecraftVersion("26.2"));
assertSame(DataVersion.V26_2, DataVersion.forMinecraftVersion("26.2.0"));
@@ -24,6 +25,7 @@ public class DataVersionTest {
@Test
public void packFormatsMatchServerVersionJson() {
assertEquals(48, DataVersion.V1_21_1.getPackFormat());
assertEquals(101, DataVersion.V26_1_2.getPackFormat());
assertEquals(107, DataVersion.V26_2.getPackFormat());
assertEquals(121, DataVersion.V26_3.getPackFormat());
@@ -31,7 +33,7 @@ public class DataVersionTest {
@Test
public void minSupportedPackFormatIsTheOldestSupportedRuntime() {
assertEquals(101, DataVersion.minSupportedPackFormat());
assertEquals(48, DataVersion.minSupportedPackFormat());
}
@Test
@@ -142,6 +142,47 @@ public class VanillaJigsawDatapackExporterTest {
assertEquals("north_up", paletteEntry.getCompoundTag("Properties").getString("orientation"));
}
@Test
public void exports1211RuntimeBlockStatesAndPackFormat() throws Exception {
when(IrisPlatforms.get().minecraftVersion()).thenReturn("1.21.1");
try {
TestGraph graph = connectorGraph();
Path output = temporaryFolder.getRoot().toPath().resolve("village-1211");
VanillaJigsawExportSettings settings = VanillaJigsawExportSettings.builder()
.maxDistanceVertical(graph.structure.getMaxSizeChunks() * 16).build();
VanillaJigsawExportResult result = new VanillaJigsawDatapackExporter().export(
request(graph, output).namespace("studio").resourcePath("village/test").settings(settings).build());
assertTrue(result.diagnostics().toString(), result.isSuccess());
JsonObject pack = json(output.resolve("pack.mcmeta")).getAsJsonObject("pack");
assertEquals(48, pack.get("pack_format").getAsInt());
JsonObject structure = json(output.resolve("data/studio/worldgen/structure/village/test.json"));
assertTrue(structure.get("max_distance_from_center").isJsonPrimitive());
CompoundTag template = (CompoundTag) NBTUtil.read(output.resolve(
"data/studio/structure/village/test/piece/pieces/start.nbt").toFile()).getTag();
assertEquals(3955, template.getInt("DataVersion"));
CompoundTag palette = (CompoundTag) template.getListTag("palette").get(0);
assertEquals("minecraft:jigsaw", palette.getString("Name"));
} finally {
when(IrisPlatforms.get().minecraftVersion()).thenReturn("26.2");
}
}
@Test
public void rejects1211VanillaExportWithDifferentVerticalDistance() throws Exception {
when(IrisPlatforms.get().minecraftVersion()).thenReturn("1.21.1");
try {
Path output = temporaryFolder.getRoot().toPath().resolve("unequal-1211");
VanillaJigsawExportResult result = new VanillaJigsawDatapackExporter().export(
request(connectorGraph(), output).settings(VanillaJigsawExportSettings.builder()
.maxDistanceVertical(127).build()).build());
assertFalse(result.isSuccess());
assertTrue(result.diagnostics().stream().anyMatch(diagnostic ->
diagnostic.code() == VanillaJigsawExportDiagnostic.Code.INVALID_MAX_DISTANCE));
} finally {
when(IrisPlatforms.get().minecraftVersion()).thenReturn("26.2");
}
}
@Test
public void exports26_3RuntimeBlockStatesAndPackFormat() throws Exception {
when(IrisPlatforms.get().minecraftVersion()).thenReturn("26.3.0");
-3
View File
@@ -25,6 +25,3 @@ irisVersion=4.3.3
minecraftVersion=26.3
# Bukkit plugin api-version: lowest supported Minecraft release line so one artifact loads on 26.1.2 through 26.3
apiVersion=26.1
fabricLoaderVersion=0.19.5
forgeVersion=26.3-66.0.8
neoForgeVersion=26.3.0.33-beta
+15
View File
@@ -0,0 +1,15 @@
import groovy.json.JsonSlurper
File irisDirectory = project.name.startsWith('iris-') ? file('../..') : rootDir
String targetMinecraft = providers.gradleProperty('minecraftVersion').getOrElse('26.3')
Map<String, Map<String, Object>> targets = (Map<String, Map<String, Object>>) new JsonSlurper()
.parse(new File(irisDirectory, 'gradle/mod-targets.json'))
Map<String, Object> target = targets.get(targetMinecraft)
if (target == null) {
throw new GradleException("Unsupported Iris mod target ${targetMinecraft}; choose ${targets.keySet().join(', ')}")
}
ext.irisModTarget = target
ext.irisModTargets = targets
if (project.name.startsWith('iris-')) {
layout.buildDirectory.set(layout.projectDirectory.dir("build/${targetMinecraft}"))
}
+32
View File
@@ -0,0 +1,32 @@
{
"1.21.1": {
"java": 21,
"fabricRemap": true,
"fabricLoader": "0.19.5",
"forge": "1.21.1-52.1.20",
"neoForge": "21.1.262",
"nativeAccess": "minecraft1_21_1",
"fabricModules": {
"fabric-api-base": "0.4.42+6573ed8c19",
"fabric-registry-sync-v0": "5.3.2+e3eddc2119",
"fabric-resource-loader-v0": "1.3.1+5b5275af19",
"fabric-lifecycle-events-v1": "2.6.0+0865547519",
"fabric-command-api-v2": "2.2.28+6ced4dd919",
"fabric-events-interaction-v0": "0.7.14+ba9dae0619",
"fabric-networking-api-v1": "4.3.1+d30f6a7919",
"fabric-rendering-v1": "5.2.1+f44c2eea19",
"fabric-key-binding-api-v1": "1.0.47+0af3f5a719"
},
"fabricPermissions": "0.3.1",
"slf4j": "2.0.9"
},
"26.3": {
"java": 25,
"fabricRemap": false,
"fabricLoader": "0.19.5",
"forge": "26.3-66.0.8",
"neoForge": "26.3.0.33-beta",
"nativeAccess": "minecraft26_2",
"slf4j": "2.0.17"
}
}
@@ -18,7 +18,8 @@ TaskProvider<JavaExec> resolvedRevisionVerification = tasks.register('verifyReso
classpath = files('../../buildSrc/build/libs/buildSrc.jar')
mainClass.set('art.arcane.iris.buildtools.GenerationBuildRevision')
doFirst {
List<String> arguments = ['verify-dependencies', file('../../core/build/generated/generation-revision/META-INF/iris/generation-kernels/abi-1.revision').absolutePath]
String runtimeVariant = providers.gradleProperty('irisRuntimeJava').getOrElse(irisModTarget.java.toString()) == '21' ? 'java21/' : ''
List<String> arguments = ['verify-dependencies', file("../../core/build/${runtimeVariant}generated/generation-revision/META-INF/iris/generation-kernels/abi-1.revision").absolutePath]
for (ResolvedArtifact artifact : configurations.bundle.resolvedConfiguration.resolvedArtifacts) {
String group = artifact.moduleVersion.id.group
String name = artifact.moduleVersion.id.name
+80
View File
@@ -0,0 +1,80 @@
import groovy.json.JsonSlurper
String sourceMinecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse('26.3')
List<File> adapterSourceRoots = [projectDir, file('../modded-common'), file('../client-common')]
tasks.named('compileTestJava', JavaCompile).configure {
options.release.set((int) irisModTarget.java)
}
tasks.withType(Test).configureEach {
javaLauncher.set(javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of((int) irisModTarget.java)
})
}
['java', 'resources'].each { String kind ->
List<File> overlays = adapterSourceRoots.collect { File adapter ->
new File(adapter, "src/versions/${sourceMinecraftVersion}/${kind}")
}
if (!overlays.any { File directory -> directory.isDirectory() }) {
return
}
TaskProvider<Sync> prepareSources = tasks.register("prepareVersion${kind.capitalize()}", Sync) {
adapterSourceRoots.each { File adapter ->
File overlay = new File(adapter, "src/versions/${sourceMinecraftVersion}/${kind}")
from(new File(adapter, "src/main/${kind}")) {
exclude { FileTreeElement entry -> new File(overlay, entry.path).isFile() }
}
from(overlay)
}
includeEmptyDirs = false
into(layout.buildDirectory.dir("generated/sources/iris/${kind}"))
}
Set<File> replacedRoots = adapterSourceRoots.collect { File adapter ->
new File(adapter, "src/main/${kind}").canonicalFile
}.toSet()
List<Object> retainedRoots = sourceSets.main."${kind}".srcDirs.findAll { File source ->
!replacedRoots.contains(source.canonicalFile)
}.toList()
sourceSets.main."${kind}".setSrcDirs(retainedRoots + [prepareSources])
tasks.named('verifyLoggingPolicy').configure { dependsOn(prepareSources) }
tasks.named('verifyNativeBoundary').configure { dependsOn(prepareSources) }
}
File testRulesFile = file("../modded-common/src/versions/${sourceMinecraftVersion}/test/source-replacements.json")
if (testRulesFile.isFile()) {
Map<String, Object> testRules = (Map<String, Object>) new JsonSlurper().parse(testRulesFile)
TaskProvider<Sync> prepareTests = tasks.register('prepareVersionTests', Sync) {
inputs.file(testRulesFile)
adapterSourceRoots.each { File adapter ->
File overlay = new File(adapter, "src/versions/${sourceMinecraftVersion}/test/java")
from(new File(adapter, 'src/test/java')) {
exclude { FileTreeElement entry -> new File(overlay, entry.path).isFile() }
}
from(overlay)
}
includeEmptyDirs = false
into(layout.buildDirectory.dir('generated/sources/iris/test'))
doLast {
List<Map<String, String>> rules = (List<Map<String, String>>) (testRules.get('global') ?: [])
Map<String, List<Map<String, String>>> fileRules = (Map<String, List<Map<String, String>>>) (testRules.get('files') ?: [:])
project.fileTree(destinationDir).matching { include('**/*.java') }.each { File source ->
String path = destinationDir.toPath().relativize(source.toPath()).toString().replace(File.separator, '/')
boolean overridden = adapterSourceRoots.any { File adapter ->
new File(adapter, "src/versions/${sourceMinecraftVersion}/test/java/${path}").isFile()
}
if (!overridden) {
String content = source.getText('UTF-8')
String rewritten = content
for (Map<String, String> rule : rules + (fileRules.get(path) ?: [])) {
rewritten = rewritten.replaceAll(rule.pattern, rule.replacement)
}
if (content != rewritten) {
source.setText(rewritten, 'UTF-8')
}
}
}
}
}
sourceSets.test.java.setSrcDirs([prepareTests])
}
+59 -11
View File
@@ -1,3 +1,6 @@
import groovy.json.JsonSlurper
import java.util.zip.ZipFile
Configuration nativeMinecraftSources = configurations.create('nativeMinecraftSources') {
canBeConsumed = false
canBeResolved = true
@@ -6,41 +9,86 @@ Configuration nativeMinecraftSources = configurations.create('nativeMinecraftSou
String nativeSourceLoader = extensions.extraProperties.get('nativeSourceLoader')
String nativeSourceImplementation = 'minecraft26_2'
String nativeSourceMinecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse('26.3')
String versionPrefix = "versions/${nativeSourceMinecraftVersion}/"
extensions.extraProperties.set('nativeSourceImplementation', nativeSourceImplementation)
dependencies.add(nativeMinecraftSources.name,
"com.github.VolmitSoftware.VolmLib:native-${nativeSourceImplementation}-sources:master-SNAPSHOT")
Set<String> overrides = new HashSet<>()
Map<String, Object> replacements = [:]
TaskProvider<Sync> unpackNativeMinecraftSources = tasks.register('unpackNativeMinecraftSources', Sync) {
dependsOn(nativeMinecraftSources)
from(provider { nativeMinecraftSources.collect { File artifact -> zipTree(artifact) } }) {
include('art/**', 'modded/**', "${nativeSourceLoader}/**",
'resources/modded/**', "resources/${nativeSourceLoader}/**", "versions/${nativeSourceMinecraftVersion}/**")
eachFile { FileCopyDetails source ->
String versionPrefix = "versions/${nativeSourceMinecraftVersion}/"
if (source.path.startsWith(versionPrefix)) {
source.path = source.path.substring(versionPrefix.length())
inputs.property('minecraftVersion', nativeSourceMinecraftVersion)
inputs.property('loader', nativeSourceLoader)
doFirst {
overrides.clear()
for (File artifact : nativeMinecraftSources) {
new ZipFile(artifact).withCloseable { ZipFile archive ->
archive.entries().each { entry ->
if (entry.name.startsWith(versionPrefix)) {
overrides.add(entry.name.substring(versionPrefix.length()))
}
}
java.util.zip.ZipEntry rules = archive.getEntry(versionPrefix + 'source-replacements.json')
if (rules != null) {
archive.getInputStream(rules).withCloseable { InputStream stream ->
replacements = (Map<String, Object>) new JsonSlurper().parse(stream)
}
}
}
}
}
from(provider { nativeMinecraftSources.collect { File artifact -> zipTree(artifact) } }) {
include('art/**', 'modded/**', "${nativeSourceLoader}/**",
'resources/modded/**', "resources/${nativeSourceLoader}/**")
eachFile { FileCopyDetails source ->
if (overrides.contains(source.path)) {
source.exclude()
}
}
}
from(provider { nativeMinecraftSources.collect { File artifact -> zipTree(artifact) } }) {
include("${versionPrefix}art/**", "${versionPrefix}modded/**", "${versionPrefix}${nativeSourceLoader}/**",
"${versionPrefix}resources/modded/**", "${versionPrefix}resources/${nativeSourceLoader}/**")
eachFile { FileCopyDetails source -> source.path = source.path.substring(versionPrefix.length()) }
}
includeEmptyDirs = false
into(layout.buildDirectory.dir('generated/sources/native-minecraft'))
doLast {
List<Map<String, String>> rules = (List<Map<String, String>>) (replacements.get('global') ?: [])
Map<String, List<Map<String, String>>> fileRules = (Map<String, List<Map<String, String>>>) (replacements.get('files') ?: [:])
project.fileTree(destinationDir).matching { include('**/*.java') }.each { File source ->
String path = destinationDir.toPath().relativize(source.toPath()).toString().replace(File.separator, '/')
if (!overrides.contains(path)) {
String content = source.getText('UTF-8')
String rewritten = content
for (Map<String, String> rule : rules + (fileRules.get(path) ?: [])) {
rewritten = rewritten.replaceAll(rule.pattern, rule.replacement)
}
if (content != rewritten) {
source.setText(rewritten, 'UTF-8')
}
}
}
}
}
sourceSets.main.java.srcDir(unpackNativeMinecraftSources)
tasks.named('compileJava').configure { dependsOn(unpackNativeMinecraftSources) }
sourceSets.main.resources.srcDir(layout.buildDirectory.dir('generated/sources/native-minecraft/resources/modded'))
sourceSets.main.resources.srcDir(layout.buildDirectory.dir("generated/sources/native-minecraft/resources/${nativeSourceLoader}"))
tasks.named('processResources').configure {
dependsOn(unpackNativeMinecraftSources)
}
tasks.named('verifyLoggingPolicy').configure {
dependsOn(unpackNativeMinecraftSources)
}
tasks.named('verifyNativeBoundary').configure {
dependsOn(unpackNativeMinecraftSources)
}
tasks.named('verifyNativeClassBoundary').configure {
dependsOn(tasks.named('classes'))
}
tasks.matching { Task task -> task.name == 'createMinecraftArtifacts' }.configureEach {
dependsOn(unpackNativeMinecraftSources)
}