diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 53d8839..c525e85 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,10 @@ +Version 4.0.7.112021 + +* Fix Forge 1.12.2 packaged jars not declaring their access transformer, which + caused an IllegalAccessError as soon as a world was loaded. +* Add a release-artifact contract check and a reusable fresh/reload test that + launches the reobfuscated jar through the real Forge 14 server runtime. + Version 4.0.6.112021 * Adopt target-qualified four-component versions so Minecraft and loader compatibility can be identified from the mod version. diff --git a/README.md b/README.md index 474cbcd..976a00d 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ It gives mods and modpacks one place to configure ores, deposit shapes, optional rock strata and geomes, provider-owned underground fluid deposits, biome palettes and world materials, flat bedrock, and bounded ore retrogen. -This branch builds target-qualified version `4.0.6.112021`: the OreSpawn 4.0.6 +This branch builds target-qualified version `4.0.7.112021`: the OreSpawn 4.0.7 feature set for Minecraft 1.12.2 and Forge. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. diff --git a/build.gradle b/build.gradle index 0c2b1c7..26bbe73 100644 --- a/build.gradle +++ b/build.gradle @@ -103,12 +103,47 @@ jar { 'Implementation-Version' : version, 'Implementation-Vendor' : 'SkyBlade1978', 'Implementation-Timestamp' : new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"), - 'OreSpawn-API-Version' : '1' + 'OreSpawn-API-Version' : '1', + // Forge 1.12 discovers mod access transformers only through + // this manifest attribute. The development configuration + // above is not consulted when a packaged jar is loaded. + 'FMLAT' : 'accesstransformer.cfg' ]) } finalizedBy 'reobfJar' } +task verifyPublishedJarRuntimeContract(dependsOn: 'reobfJar') { + group = 'verification' + description = 'Verifies the reobfuscated jar advertises and contains its Forge 1.12 access transformer.' + doLast { + java.util.jar.JarFile published = new java.util.jar.JarFile(jar.archivePath) + try { + String declared = published.manifest.mainAttributes.getValue('FMLAT') + if (declared != 'accesstransformer.cfg') { + throw new GradleException("Published jar has invalid FMLAT manifest entry: ${declared}") + } + java.util.jar.JarEntry transformer = published.getJarEntry('META-INF/accesstransformer.cfg') + if (transformer == null) { + throw new GradleException('Published jar is missing META-INF/accesstransformer.cfg') + } + String rules = published.getInputStream(transformer).getText('UTF-8') + [ + 'public-f net.minecraft.world.WorldProvider field_76578_c', + 'public-f net.minecraft.world.gen.ChunkGeneratorOverworld field_186001_t' + ].each { String required -> + if (!rules.readLines().any { String line -> line.trim().startsWith(required) }) { + throw new GradleException("Published access transformer is missing rule: ${required}") + } + } + } finally { + published.close() + } + } +} + +check.dependsOn verifyPublishedJarRuntimeContract + processResources { inputs.property 'version', project.version inputs.property 'minecraft_version', minecraft_version @@ -188,22 +223,38 @@ def assertRuntimeLogsClean = { File runDirectory, String context -> } File logsDirectory = new File(runDirectory, 'logs') - if (!logsDirectory.isDirectory()) return def failures = [] - fileTree(logsDirectory) { include '**/*.log'; include '**/*.txt' }.files.each { File log -> + def runtimeLogs = [] as Set + if (logsDirectory.isDirectory()) { + runtimeLogs.addAll(fileTree(logsDirectory) { + include '**/*.log' + include '**/*.txt' + }.files) + } + runtimeLogs.addAll(fileTree(runDirectory) { + include '*-console.txt' + }.files) + runtimeLogs.each { File log -> int lineNumber = 0 log.eachLine('UTF-8') { String line -> lineNumber++ - boolean unexpectedSeverity = line ==~ /.*\/(?:ERROR|FATAL)\].*/ + boolean unexpectedSeverity = (line ==~ /.*\/(?:ERROR|FATAL)\].*/ + || line ==~ /.*\s(?:ERROR|FATAL)\s.*/) boolean knownNoise = acceptedForge14LogNoise.any { line =~ it } + boolean oreSpawnCascadingLoad = (line.contains('cascading worldgen lag') + && line.contains('OreSpawn loaded a new chunk')) boolean fatalText = (line.contains('Encountered an unexpected exception') || line.contains('Exception stopping the server') || line.contains('Migration audit failed') || line.contains('java.lang.Error:') + || line.contains('IllegalAccessError') + || line.contains('NoSuchFieldError') + || line.contains('LinkageError') || line.contains('NoSuchMethodError') || line.contains('NoClassDefFoundError') || line.contains('ExceptionInInitializerError') - || line.contains('Tried to assign a mutable BlockPos')) + || line.contains('Tried to assign a mutable BlockPos') + || oreSpawnCascadingLoad) if ((unexpectedSeverity && !knownNoise) || fatalText) { failures.add("${log.name}:${lineNumber}: ${line}") } @@ -238,6 +289,13 @@ task runtimeLogScannerTest { try { assertRuntimeLogsClean(probe, 'scanner-error-severity-probe') } catch (GradleException expected) { rejected = true } if (!rejected) throw new GradleException('Runtime log scanner accepted an unexpected ERROR line') + new File(probe, 'packaged-forge-fresh-console.txt').setText( + '[Server thread/ERROR] [FML]: java.lang.IllegalAccessError: tried to access field net.minecraft.world.WorldProvider.field_76578_c\n', + 'UTF-8') + rejected = false + try { assertRuntimeLogsClean(probe, 'scanner-packaged-console-probe') } + catch (GradleException expected) { rejected = true } + if (!rejected) throw new GradleException('Runtime log scanner ignored a packaged-launch IllegalAccessError') delete probe } } @@ -261,6 +319,19 @@ task surfaceIntegrationTestModJar(type: Jar, dependsOn: compileSurfaceIntegratio from 'src/biomeIntegrationTest/resources' } +// Keep the mapped development fixture separate from the copy transformed for +// a real packaged Forge runtime. +task packagedSurfaceIntegrationTestModJar(type: Jar, dependsOn: compileSurfaceIntegrationTestMod) { + archiveName = 'surfaceprobe-packaged.jar' + destinationDir = file("${buildDir}/surface-integration-fixture") + from surfaceIntegrationClasses + from 'src/biomeIntegrationTest/resources' +} + +reobf { + packagedSurfaceIntegrationTestModJar {} +} + def surfaceIntegrationRunDirectory = file("${buildDir}/surface-integration-run") def surfaceIntegrationMainOutput = file("${buildDir}/surface-integration-fixture/orespawn-main") task prepareSurfaceIntegrationTest(dependsOn: surfaceIntegrationTestModJar) { @@ -404,6 +475,157 @@ task surfaceIntegrationTest(dependsOn: surfaceIntegrationReloadProcess) { check.dependsOn surfaceIntegrationTest +// Release qualification must also exercise the reobfuscated jar through +// Forge's real packaged-mod discovery. Supply the official 1.12.2 dedicated +// server jar and an exact Forge 14 library root; no development output or +// mapped Forge jar is placed on this process's classpath. +def packagedForgeRunDirectory = file("${buildDir}/packaged-forge-runtime-run") +def packagedMinecraftServer = { + if (!project.hasProperty('packagedMinecraftServerJar')) { + throw new GradleException('packagedMinecraftServerJar is required for packagedForgeRuntimeTest') + } + File server = file(project.property('packagedMinecraftServerJar')) + if (!server.isFile()) { + throw new GradleException("Minecraft 1.12.2 server jar is missing: ${server}") + } + server +} +def packagedForgeLibraries = { + if (!project.hasProperty('packagedForgeLibrariesRoot')) { + throw new GradleException('packagedForgeLibrariesRoot is required for packagedForgeRuntimeTest') + } + File libraries = file(project.property('packagedForgeLibrariesRoot')) + if (!libraries.isDirectory()) { + throw new GradleException("Forge 14 library root is missing: ${libraries}") + } + libraries +} +def packagedForgeRuntimeLayout = { + File forge = forge14UniversalRuntime() + File server = packagedMinecraftServer() + File libraries = packagedForgeLibraries() + java.util.jar.JarFile runtime = new java.util.jar.JarFile(forge) + String declared + try { + declared = runtime.manifest.mainAttributes.getValue('Class-Path') + } finally { + runtime.close() + } + if (declared == null || declared.trim().isEmpty()) { + throw new GradleException("Forge 14 runtime has no Class-Path manifest entry: ${forge}") + } + def dependencies = declared.trim().split(/\s+/).findAll { String entry -> + entry != "minecraft_server.${minecraft_version}.jar" + }.collect { String entry -> + String relative = entry.startsWith('libraries/') ? entry.substring('libraries/'.length()) : entry + File dependency = new File(libraries, relative) + if (!dependency.isFile()) { + throw new GradleException("Forge 14 packaged-runtime dependency is missing: ${dependency}") + } + [source: dependency, relative: relative] + } + [forge: forge, server: server, dependencies: dependencies] +} + +task preparePackagedForgeRuntimeTest(dependsOn: [verifyPublishedJarRuntimeContract, + 'reobfPackagedSurfaceIntegrationTestModJar']) { + group = 'verification' + doLast { + delete packagedForgeRunDirectory + def runtime = packagedForgeRuntimeLayout() + copy { from runtime.forge; into packagedForgeRunDirectory } + copy { + from runtime.server + into packagedForgeRunDirectory + rename { "minecraft_server.${minecraft_version}.jar" } + } + runtime.dependencies.each { Map dependency -> + File destination = new File(packagedForgeRunDirectory, + "libraries/${dependency.relative}").parentFile + copy { from dependency.source; into destination } + } + File mods = new File(packagedForgeRunDirectory, 'mods') + mods.mkdirs() + copy { from jar.archivePath; into mods } + copy { from packagedSurfaceIntegrationTestModJar.archivePath; into mods } + new File(packagedForgeRunDirectory, 'server.properties').setText('''\ +level-name=surface-integration-world +level-seed=zsjpxah +level-type=default +online-mode=false +allow-nether=true +generate-structures=false +spawn-protection=0 +max-tick-time=-1 +''', 'UTF-8') + new File(packagedForgeRunDirectory, 'eula.txt').setText('eula=true\n', 'UTF-8') + } +} + +def createPackagedForgeProcess = { String phase, Object dependency -> + ByteArrayOutputStream console = new ByteArrayOutputStream() + File consoleFile = new File(packagedForgeRunDirectory, + "packaged-forge-${phase.toLowerCase()}-console.txt") + task("packagedForgeRuntime${phase}Process", type: Exec, dependsOn: dependency) { + group = 'verification' + workingDir packagedForgeRunDirectory + doFirst { + console.reset() + File javaExecutable = new File(System.getProperty('java.home'), + "bin/java${System.properties['os.name'].toLowerCase().contains('windows') ? '.exe' : ''}") + File forge = new File(packagedForgeRunDirectory, forge14UniversalRuntime().name) + commandLine javaExecutable, + '-Dforge.logging.console.level=info', + "-Dsurfaceprobe.integrationPhase=${phase.toLowerCase()}", + '-jar', forge, 'nogui' + standardOutput = console + errorOutput = console + } + doLast { + consoleFile.setText(console.toString('UTF-8'), 'UTF-8') + } + } +} + +def packagedForgeFreshProcess = createPackagedForgeProcess('Fresh', preparePackagedForgeRuntimeTest) +packagedForgeFreshProcess.doLast { + File marker = new File(packagedForgeRunDirectory, + 'surface-integration-world/surfaceprobe-integration.properties') + if (!marker.isFile()) { + throw new GradleException("Packaged Forge fresh marker is missing: ${marker}") + } + assertRuntimeLogsClean(packagedForgeRunDirectory, 'packaged Forge fresh phase') +} +def packagedForgeReloadProcess = createPackagedForgeProcess('Reload', packagedForgeFreshProcess) +packagedForgeReloadProcess.doLast { + assertRuntimeLogsClean(packagedForgeRunDirectory, 'packaged Forge reload phase') +} + +task packagedForgeRuntimeTest(dependsOn: packagedForgeReloadProcess) { + group = 'verification' + description = 'Creates and reloads a world using the reobfuscated OreSpawn jar under the real Forge 14 launcher.' + doLast { + File marker = new File(packagedForgeRunDirectory, + 'surface-integration-world/surfaceprobe-integration.properties') + Properties result = new Properties() + marker.withInputStream { result.load(it) } + if (result.getProperty('reload_verified') != 'true') { + throw new GradleException("Packaged Forge reload was not verified: ${marker}") + } + logger.lifecycle('Packaged Forge jar created and reloaded {} dimensions with {} audited columns each', + result.getProperty('dimensions'), result.getProperty('columns_per_dimension')) + } +} + +// The packaged launcher needs two machine-local inputs that cannot be checked +// into the repository. When callers supply them, make the real-runtime gate a +// mandatory part of check; otherwise the focused jar-contract verification +// still protects the manifest and embedded access-transformer contract. +if (project.hasProperty('packagedMinecraftServerJar') + && project.hasProperty('packagedForgeLibrariesRoot')) { + check.dependsOn packagedForgeRuntimeTest +} + def migrationIntegrationClasses = file("${buildDir}/migration-integration-fixture/classes") task compileMigrationIntegrationTestMod(type: JavaCompile, dependsOn: classes) { source fileTree('src/migrationIntegrationTest/java') diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md index 7eeb76f..16e2df6 100644 --- a/docs/DEVELOPER_GUIDE.md +++ b/docs/DEVELOPER_GUIDE.md @@ -208,3 +208,19 @@ registrar rejects duplicate and late declarations. Run `gradlew check` (or `gradlew build`, which includes it) before publishing any change to biome registration, palettes, surfaces, feature ordering, height handling, or profile persistence. + +Before publishing a Forge 1.12.2 jar, also run the packaged-runtime gate with +the official Minecraft 1.12.2 dedicated-server jar and the libraries installed +for Forge 14.23.5.2859: + +```text +gradlew packagedForgeRuntimeTest --offline --no-daemon \ + -PpackagedMinecraftServerJar= \ + -PpackagedForgeLibrariesRoot= +``` + +Unlike ForgeGradle's development launches, this gate places the reobfuscated +OreSpawn jar in `mods`, starts Forge's real `ServerLaunchWrapper`, generates a +fresh provider world, and reopens the same save. It deliberately excludes +mapped Forge jars, `sourceSets.main`, `MOD_CLASSES`, and LegacyDev so packaging +metadata such as `FMLAT` is tested exactly as players receive it. diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index ecee8a6..2bf01c0 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -49,10 +49,10 @@ version. Examples: -| Minecraft | Loader | Target | Full OreSpawn 4.0.6 version | +| Minecraft | Loader | Target | Example full OreSpawn version | | --- | --- | ---: | --- | | 1.10.2 | Forge | `110021` | `4.0.6.110021` | -| 1.12.2 | Forge | `112021` | `4.0.6.112021` | +| 1.12.2 | Forge | `112021` | `4.0.7.112021` | | 1.13.2 | Forge | `113021` | `4.0.6.113021` | | 1.20.6 | Forge | `120061` | `4.0.6.120061` | | 1.21.11 | Forge | `121111` | `4.0.6.121111` | diff --git a/gradle.properties b/gradle.properties index 34a6277..cba63c6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,7 @@ mapping_version=39-1.12 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.6.112021 +mod_version=4.0.7.112021 mod_group_id=zone.moddev.mc.orespawn mod_authors=SkyBlade1978, dshadowwolf, the MMD Team mod_description=Configurable, provider-driven terrain, ore, and deposit generation. diff --git a/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java b/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java index 71cd773..3c260c9 100644 --- a/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java +++ b/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java @@ -818,7 +818,7 @@ private static void writeHumanUpgradeReport(Path destination) throws IOException } } List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.6.112021 Upgrade Report"); + lines.add("OreSpawn 4.0.7.112021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Legacy OreSpawn configuration was consumed and translated for OS4."); diff --git a/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java b/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java index 3686db1..3ec4a7d 100644 --- a/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java +++ b/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java @@ -52,7 +52,7 @@ public class OreSpawn { public static final String MODID = "orespawn"; public static final String NAME = "OreSpawn"; - public static final String VERSION = "4.0.6.112021"; + public static final String VERSION = "4.0.7.112021"; private static final Logger LOGGER = LogManager.getLogger(); diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java index e1ecbc5..420a97d 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java @@ -152,7 +152,7 @@ private static void writeUpgradeReport(Path worldRoot, Path configDirectory, Path report = worldRoot.resolve("serverconfig/orespawn-upgrade-report.txt"); List missing = missingBlocks(igneous, metamorphic, sedimentary); List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.6.112021 Upgrade Report"); + lines.add("OreSpawn 4.0.7.112021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Existing Mineralogy " + identity.version + " world detected."); diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java index 30c31eb..c34590e 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java @@ -65,7 +65,7 @@ private static void writeInitialUpgradeReport(Path configDirectory, boolean forceRetrogen, boolean flatBedrock, boolean retrogenBedrock, int bedrockLayers) throws IOException { String newline = System.lineSeparator(); - String text = "OreSpawn 4.0.6.112021 Upgrade Report" + newline + String text = "OreSpawn 4.0.7.112021 Upgrade Report" + newline + "================================" + newline + newline + "RESULT: Legacy OreSpawn settings were imported into the OS4 profile." + newline + "- Manage vanilla ores: " + manageVanilla + newline