mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-22 16:17:44 +00:00
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:
1 parent
0ca33068f3
commit
62062c4a95
58 files changed
+3279
-133
No files matched your search
@@ -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": "*"
|
||||
}
|
||||
}
|
||||
+102
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
+100
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
@@ -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.
|
||||
|
||||
+1
@@ -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) {
|
||||
|
||||
+64
-31
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+61
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+139
-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);
|
||||
|
||||
+23
@@ -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));
|
||||
}
|
||||
}
|
||||
+141
@@ -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]
|
||||
}
|
||||
}
|
||||
+145
@@ -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;
|
||||
}
|
||||
}
|
||||
+48
@@ -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());
|
||||
}
|
||||
}
|
||||
+134
@@ -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)));
|
||||
}
|
||||
}
|
||||
+167
@@ -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();
|
||||
}
|
||||
}
|
||||
+45
@@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+253
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+71
@@ -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());
|
||||
}
|
||||
}
|
||||
+130
@@ -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;
|
||||
}
|
||||
}
|
||||
+68
@@ -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);
|
||||
}
|
||||
}
|
||||
+41
@@ -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 +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')
|
||||
+29
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
+41
@@ -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");
|
||||
|
||||
@@ -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
|
||||
@@ -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}"))
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user