diff --git a/.project b/.project
index b597f7a84..53346fb71 100644
--- a/.project
+++ b/.project
@@ -1,16 +1,21 @@
-
-
- MinecraftMineralogy
- Mineralogy
-
-
- org.eclipse.jdt.core.javanature
-
-
-
- org.eclipse.jdt.core.javabuilder
-
-
-
-
-
+
+
+ MinecraftMineralogy
+ Mineralogy
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.buildship.core.gradleprojectnature
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+ org.eclipse.buildship.core.gradleprojectbuilder
+
+
+
+
+
diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 8a7418757..13f7be11f 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -19,4 +19,6 @@ Mineralogy 6.0.1.110021 for Minecraft 1.10.2 Forge
- Documents OreSpawn's custom terrain dimensions and per-rock dimension membership as the resolution for issue #30, including Minecraft 1.10 numeric-ID mapping, dimension-specific rock sets and altitudes, and new-chunks-only behavior.
- Repairs saved rock-furnace loading so Minecraft can reconstruct the tile entity, restore its inventory and progress, and recover the rock-specific burn rate without changing historical NBT or tile IDs.
- Aligns novaculite with Mineralogy 6 hardness/resistance and makes double slabs drop two matching slabs normally or one matching full block with Silk Touch.
+- Repairs generated ForgeGradle 7 Eclipse launches by restoring their Buildship project/cache configuration, retiring obsolete ForgeGradle 2 launches, quoting Slime Launcher paths that contain spaces, and verifying the result before handoff.
+- Applies OreSpawn's released access-transformer contract to ForgeGradle's mapped development runtime and synchronizes processed metadata and bundled guides into Eclipse output without changing the production jar.
- Advances the functional bug version to 6.0.1 because this release includes fixes and compatibility improvements not present in Mineralogy 6.0.0.
diff --git a/build.gradle b/build.gradle
index 20b118e75..923306cf3 100644
--- a/build.gradle
+++ b/build.gradle
@@ -86,10 +86,17 @@ def normalizeArchiveLineEndings = { details ->
minecraft {
mappings channel: project.mappings_channel, version: project.mappings_version
+ // Forge applies OreSpawn's SRG FMLAT in an ordinary packaged installation,
+ // but ForgeGradle 7 does not remap a dependency transform for mapped
+ // development Minecraft. Mirror the exact two released rules for
+ // Gradle/Eclipse only. This file is outside main resources and is never
+ // packaged by Mineralogy.
+ accessTransformer.from(file('gradle/orespawn-development-access-transformer.cfg'))
+ String configuredRunDirectory = providers.gradleProperty('mineralogyRunDirectory').getOrElse('run')
runs {
configureEach {
- workingDir.convention layout.projectDirectory.dir('run')
+ workingDir.convention layout.projectDirectory.dir(configuredRunDirectory)
systemProperty 'forge.logging.console.level', 'debug'
}
@@ -174,6 +181,18 @@ tasks.named('processResources', ProcessResources) {
filesMatching(archiveTextPatterns, normalizeArchiveLineEndings)
}
+def prepareEclipseResources = tasks.register('prepareEclipseResources') {
+ group = 'ide'
+ description = 'Copies Gradle-processed production resources into Eclipse merged output.'
+ dependsOn tasks.named('processResources')
+ doLast {
+ project.copy {
+ from(layout.buildDirectory.dir('resources/main'))
+ into(layout.projectDirectory.dir('bin/main'))
+ }
+ }
+}
+
tasks.named('jar', Jar) {
archiveClassifier = 'deobf'
destinationDirectory = layout.buildDirectory.dir('libs-dev')
@@ -325,6 +344,29 @@ tasks.register('verifyReleaseDependencies') {
throw new GradleException('Resolved OreSpawn file does not contain version '
+ project.orespawn_version)
}
+
+ def transformerEntry = oreSpawn.getEntry('META-INF/accesstransformer.cfg')
+ if (transformerEntry == null) {
+ throw new GradleException('Resolved OreSpawn file is missing its access transformer')
+ }
+ List releasedTransformer = oreSpawn.getInputStream(transformerEntry)
+ .getText(StandardCharsets.UTF_8.name()).readLines()
+ .findAll { !it.trim().isEmpty() }
+ List expectedReleasedTransformer = [
+ 'public-f net.minecraft.world.WorldProvider field_76578_c # biomeProvider',
+ 'public-f net.minecraft.world.gen.ChunkProviderOverworld field_186001_t # oceanBlock'
+ ]
+ List developmentTransformer = file(
+ 'gradle/orespawn-development-access-transformer.cfg').readLines('UTF-8')
+ .findAll { !it.trim().isEmpty() }
+ List expectedDevelopmentTransformer = [
+ 'public-f net.minecraft.world.WorldProvider biomeProvider',
+ 'public-f net.minecraft.world.gen.ChunkProviderOverworld oceanBlock'
+ ]
+ if (releasedTransformer != expectedReleasedTransformer
+ || developmentTransformer != expectedDevelopmentTransformer) {
+ throw new GradleException('Development access transformer no longer matches released OreSpawn semantics')
+ }
} finally {
oreSpawn.close()
}
@@ -385,7 +427,9 @@ tasks.register('verifyReleaseArtifacts') {
}
}
['agent-notes/', 'src/test/', 'OilRuntimeProbe', 'org/junit/',
- 'org/mockito/', 'net/bytebuddy/'].each { forbidden ->
+ 'org/mockito/', 'net/bytebuddy/',
+ 'orespawn-development-access-transformer.cfg',
+ 'META-INF/accesstransformer.cfg'].each { forbidden ->
if (names.any { it.contains(forbidden) }) {
throw new GradleException("Release jar contains forbidden entry matching ${forbidden}")
}
@@ -594,27 +638,109 @@ eclipse {
synchronizationTasks 'genEclipseRuns'
}
+def configureEclipseBuildship = tasks.register('configureEclipseBuildship') {
+ group = 'ide'
+ description = 'Creates the Buildship project configuration required by ForgeGradle Eclipse launches.'
+
+ doLast {
+ File preferencesFile = file('.settings/org.eclipse.buildship.core.prefs')
+ Properties preferences = new Properties()
+ if (preferencesFile.isFile()) {
+ preferencesFile.withInputStream { preferences.load(it) }
+ }
+
+ Map requiredPreferences = [
+ 'eclipse.preferences.version' : '1',
+ 'connection.gradle.distribution' : 'GRADLE_DISTRIBUTION(WRAPPER)',
+ 'connection.gradle.user.home' : gradle.gradleUserHomeDir.canonicalPath,
+ 'connection.project.dir' : '',
+ 'gradle.user.home' : gradle.gradleUserHomeDir.canonicalPath,
+ 'override.workspace.settings' : 'true'
+ ]
+ if (!preferencesFile.isFile() || requiredPreferences.any { key, value ->
+ preferences.getProperty(key) != value
+ }) {
+ requiredPreferences.each { key, value -> preferences.setProperty(key, value) }
+ preferencesFile.parentFile.mkdirs()
+ preferencesFile.withOutputStream {
+ preferences.store(it,
+ 'Generated by configureEclipseBuildship; keep Eclipse and command-line caches aligned.')
+ }
+ }
+ }
+}
+
+tasks.named('genEclipseRuns') {
+ finalizedBy configureEclipseBuildship, 'isolateEclipseProductionRuns'
+}
+
tasks.register('verifyEclipseProductionClasspath') {
group = 'verification'
description = 'Verifies that ordinary generated Eclipse launches exclude test code and dependencies.'
- dependsOn 'isolateEclipseProductionRuns'
+ dependsOn tasks.named('genEclipseRuns')
doLast {
+ File buildshipPreferences = file('.settings/org.eclipse.buildship.core.prefs')
+ if (!buildshipPreferences.isFile()) {
+ throw new GradleException("Missing Eclipse Buildship project configuration: ${buildshipPreferences}")
+ }
+ Properties buildship = new Properties()
+ buildshipPreferences.withInputStream { buildship.load(it) }
+ if (buildship.getProperty('eclipse.preferences.version') != '1'
+ || buildship.getProperty('connection.gradle.distribution') != 'GRADLE_DISTRIBUTION(WRAPPER)') {
+ throw new GradleException('Eclipse Buildship project configuration does not use the Gradle wrapper')
+ }
+ String configuredGradleHome = buildship.getProperty('connection.gradle.user.home', '').trim()
+ if (!configuredGradleHome
+ || file(configuredGradleHome).canonicalFile != gradle.gradleUserHomeDir.canonicalFile) {
+ throw new GradleException("Eclipse Buildship Gradle home is not ${gradle.gradleUserHomeDir.canonicalPath}")
+ }
+ String projectMetadata = file('.project').getText('UTF-8')
+ if (!projectMetadata.contains('org.eclipse.buildship.core.gradleprojectnature')
+ || !projectMetadata.contains('org.eclipse.buildship.core.gradleprojectbuilder')) {
+ throw new GradleException('Eclipse project metadata is missing its Buildship nature or builder')
+ }
+
+ File eclipseModMetadata = file('bin/main/mcmod.info')
+ if (!eclipseModMetadata.isFile()) {
+ throw new GradleException('Eclipse output is missing processed mcmod.info')
+ }
+ def eclipseMetadata = new JsonSlurper().parseText(eclipseModMetadata.getText('UTF-8'))
+ def eclipseMod = eclipseMetadata instanceof List ? eclipseMetadata.first() : eclipseMetadata
+ if (eclipseMod.version != project.mod_version || eclipseMod.mcversion != project.mc_version) {
+ throw new GradleException('Eclipse output contains unexpanded or incorrect mod metadata')
+ }
+ ['README.md', 'PLAYER_GUIDE.md', 'DEVELOPER_GUIDE.md',
+ 'CONTENT_CONFIG.md', 'PROVIDER.md', 'VERSIONS.md'].each { guide ->
+ if (!file("bin/main/META-INF/mineralogy/docs/${guide}").isFile()) {
+ throw new GradleException("Eclipse output is missing bundled guide ${guide}")
+ }
+ }
+
+ ['MinecraftMineralogy_Client.launch', 'MinecraftMineralogy_Server.launch'].each { name ->
+ if (file(name).exists()) {
+ throw new GradleException("Obsolete ForgeGradle 2 Eclipse launch still exists: ${name}")
+ }
+ }
+
List launchFiles = fileTree(project.projectDir) {
include '*.launch'
include '.eclipse/*.launch'
}.files.findAll { file ->
- ['runClient', 'runServer', 'runGameTestServer', 'runData'].any { file.name.contains(it) }
+ ['runClient.launch', 'runServer.launch',
+ 'runTestClient.launch', 'runTestServer.launch'].contains(file.name)
} as List
- if (launchFiles.isEmpty()) {
- throw new GradleException('ForgeGradle did not generate ordinary Eclipse launch files')
+ if (launchFiles.size() != 4) {
+ throw new GradleException("ForgeGradle generated ${launchFiles.size()} of 4 expected Eclipse launch files")
}
List forbidden = [
'src/test', 'bin/test', 'build/classes/java/test',
'junit-', 'mockito-', 'byte-buddy', 'objenesis', 'OilRuntimeProbe'
]
- launchFiles.each { launch ->
+ launchFiles.findAll { launch ->
+ ['runClient.launch', 'runServer.launch'].contains(launch.name)
+ }.each { launch ->
String contents = launch.getText('UTF-8').replace('\\', '/')
List present = forbidden.findAll { contents.contains(it) }
if (!present.isEmpty()) {
@@ -628,25 +754,81 @@ tasks.register('verifyEclipseProductionClasspath') {
throw new GradleException("${launch.name} targets the wrong Eclipse project")
}
}
+ launchFiles.each { launch ->
+ String contents = launch.getText('UTF-8')
+ ['--cache', '--metadata', '--to-srg', '--to-obf'].each { option ->
+ String prefix = option + ' '
+ int valueStart = contents.indexOf(prefix)
+ if (valueStart < 0) {
+ throw new GradleException("${launch.name} is missing Slime Launcher argument ${option}")
+ }
+ valueStart += prefix.length()
+ int valueEnd = contents.indexOf(' --', valueStart)
+ if (valueEnd < 0) {
+ throw new GradleException("${launch.name} has a malformed Slime Launcher argument ${option}")
+ }
+ String value = contents.substring(valueStart, valueEnd)
+ boolean quoted = value.startsWith('"') && value.endsWith('"')
+ if (value.find(/\s/) != null && !quoted) {
+ throw new GradleException("${launch.name} leaves whitespace unquoted in ${option}: ${value}")
+ }
+ }
+ }
}
}
tasks.register('isolateEclipseProductionRuns') {
group = 'ide'
- description = 'Marks ordinary Eclipse launches as production-only after ForgeGradle generation.'
- dependsOn tasks.named('genEclipseRuns')
-
+ description = 'Normalizes ordinary ForgeGradle Eclipse launches for production-only execution.'
+ dependsOn prepareEclipseResources
doLast {
- ['runClient.launch', 'runServer.launch'].each { name ->
+ [
+ 'MinecraftMineralogy_Client.launch': 'MAIN_TYPE" value="GradleStart"',
+ 'MinecraftMineralogy_Server.launch': 'MAIN_TYPE" value="GradleStartServer"'
+ ].each { name, legacyMarker ->
+ File legacy = file(name)
+ if (legacy.isFile() && legacy.getText('UTF-8').contains(legacyMarker)) {
+ if (!legacy.delete()) {
+ throw new GradleException("Could not retire obsolete ForgeGradle 2 launch ${name}")
+ }
+ }
+ }
+
+ ['runClient.launch', 'runServer.launch',
+ 'runTestClient.launch', 'runTestServer.launch'].each { name ->
File launch = file(name)
if (!launch.isFile()) {
throw new GradleException("ForgeGradle did not generate ${name}")
}
String contents = launch.getText('UTF-8')
- if (!contents.contains('org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE')) {
+ boolean changed = false
+ boolean production = ['runClient.launch', 'runServer.launch'].contains(name)
+ if (production && !contents.contains('org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE')) {
String marker = ''
String attribute = ' '
contents = contents.replace(marker, attribute + System.lineSeparator() + marker)
+ changed = true
+ }
+ ['--cache', '--metadata', '--to-srg', '--to-obf'].each { option ->
+ String prefix = option + ' '
+ int valueStart = contents.indexOf(prefix)
+ if (valueStart < 0) {
+ throw new GradleException("${name} is missing Slime Launcher argument ${option}")
+ }
+ valueStart += prefix.length()
+ int valueEnd = contents.indexOf(' --', valueStart)
+ if (valueEnd < 0) {
+ throw new GradleException("${name} has a malformed Slime Launcher argument ${option}")
+ }
+ String value = contents.substring(valueStart, valueEnd)
+ if (value.find(/\s/) != null && !value.startsWith('"')) {
+ contents = (contents.substring(0, valueStart)
+ + '"' + value + '"'
+ + contents.substring(valueEnd))
+ changed = true
+ }
+ }
+ if (changed) {
launch.setText(contents, 'UTF-8')
}
}
diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md
index 389b68907..7644cb12e 100644
--- a/docs/DEVELOPER_GUIDE.md
+++ b/docs/DEVELOPER_GUIDE.md
@@ -55,18 +55,38 @@ recipes, so existing content remains loadable.
## Building
-Use JDK 8, Gradle 4.9, and ForgeGradle `2.2-SNAPSHOT` from the nested 1.10
-checkout:
+Use a JDK 21 Gradle runtime with the checked-in Gradle 9.6.1 wrapper and
+ForgeGradle `7.0.34`. The project itself still compiles and packages Java 8
+bytecode through its Java 8 toolchain:
```powershell
-$env:JAVA_HOME='C:\Program Files\Java\jdk1.8.0_202'
+$env:JAVA_HOME='C:\path\to\jdk-21'
$env:GRADLE_USER_HOME='D:\MinecraftMineralogy\.gradle-verify-cache'
.\gradlew.bat test compileJava processResources --no-daemon
.\gradlew.bat clean build javadoc --no-daemon
-.\gradlew.bat setupDecompWorkspace eclipse verifyEclipseProductionClasspath --no-daemon
+.\gradlew.bat genEclipseRuns isolateEclipseProductionRuns verifyEclipseProductionClasspath --no-daemon
.\gradlew.bat assemble --no-daemon
```
+Import the nested checkout into Eclipse as an existing Gradle project. Do not
+run the legacy `setupDecompWorkspace` task. Regenerate the Forge run
+configurations after changing the checkout path or Gradle cache; Mineralogy's
+normalization task keeps the ordinary client/server launches production-only
+and safely quotes Slime Launcher paths containing spaces. It also copies the
+Gradle-processed resources into Eclipse's merged `bin/main`, so expanded
+metadata and the bundled guide are identical to a Gradle development run.
+
+OreSpawn's released jar owns the production access transformer. ForgeGradle 7
+does not remap a dependency transform for Minecraft's mapped development
+classes, so this project mirrors OreSpawn's two rules with mapped field names
+in `gradle/orespawn-development-access-transformer.cfg`. The dependency audit
+keeps those rules aligned with the released jar, and the development-only file
+must never be packaged by Mineralogy.
+
+For a disposable Gradle runtime, pass
+`-PmineralogyRunDirectory=build/`; Eclipse continues to use the
+normal `run` directory unless that Gradle property is deliberately supplied.
+
Inspect complete client/server logs and test the reobfuscated jar with released
OreSpawn in a launcher-like Forge installation. The normal jar packages this
guide under `META-INF/mineralogy/docs/`.
diff --git a/gradle/orespawn-development-access-transformer.cfg b/gradle/orespawn-development-access-transformer.cfg
new file mode 100644
index 000000000..f6e960f5f
--- /dev/null
+++ b/gradle/orespawn-development-access-transformer.cfg
@@ -0,0 +1,2 @@
+public-f net.minecraft.world.WorldProvider biomeProvider
+public-f net.minecraft.world.gen.ChunkProviderOverworld oceanBlock
diff --git a/src/test/java/zone/moddev/mc/mineralogy/VersionContractTest.java b/src/test/java/zone/moddev/mc/mineralogy/VersionContractTest.java
index e6c98ff42..285db8dd0 100644
--- a/src/test/java/zone/moddev/mc/mineralogy/VersionContractTest.java
+++ b/src/test/java/zone/moddev/mc/mineralogy/VersionContractTest.java
@@ -8,6 +8,7 @@
import java.io.FileInputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
+import java.util.Arrays;
import java.util.Properties;
import org.junit.Test;
@@ -57,6 +58,9 @@ public void modernBuildAndReleaseMetadataKeepTheRequiredOreSpawnContract() throw
assertTrue(build.contains("id 'net.minecraftforge.renamer' version '1.1.5'"));
assertTrue(build.contains("JavaLanguageVersion.of(8)"));
assertTrue(build.contains("tasks.register('verifyReleaseDependencies')"));
+ assertTrue(build.contains("accessTransformer.from(file('gradle/orespawn-development-access-transformer.cfg'))"));
+ assertTrue(build.contains("Development access transformer no longer matches released OreSpawn semantics"));
+ assertTrue(build.contains("providers.gradleProperty('mineralogyRunDirectory').getOrElse('run')"));
assertTrue(build.contains("tasks.register('verifyPreparedReleaseArtifacts')"));
assertTrue(build.contains("providers.gradleProperty('preparedReleaseDir')"));
assertTrue(wrapper.contains("gradle-9.6.1-bin.zip"));
@@ -67,6 +71,30 @@ public void modernBuildAndReleaseMetadataKeepTheRequiredOreSpawnContract() throw
assertEquals("4.0.6.110021", properties.getProperty("orespawn_version"));
assertFalse(properties.containsKey("create_api_jar"));
assertFalse(properties.containsKey("create_deobf_jar"));
+
+ assertEquals(Arrays.asList(
+ "public-f net.minecraft.world.WorldProvider biomeProvider",
+ "public-f net.minecraft.world.gen.ChunkProviderOverworld oceanBlock"),
+ Files.readAllLines(new File("gradle/orespawn-development-access-transformer.cfg").toPath(),
+ StandardCharsets.UTF_8));
+ }
+
+ @Test
+ public void eclipseLaunchNormalizationQuotesWhitespacePathsAndVerifiesThem() throws Exception {
+ String build = read(new File("build.gradle"));
+ String project = read(new File(".project"));
+ assertTrue(build.contains("tasks.register('configureEclipseBuildship')"));
+ assertTrue(build.contains("tasks.register('isolateEclipseProductionRuns')"));
+ assertTrue(build.contains("tasks.register('prepareEclipseResources')"));
+ assertTrue(build.contains("dependsOn prepareEclipseResources"));
+ assertTrue(build.contains("Eclipse output contains unexpanded or incorrect mod metadata"));
+ assertTrue(build.contains("Eclipse output is missing bundled guide"));
+ assertTrue(build.contains("['--cache', '--metadata', '--to-srg', '--to-obf']"));
+ assertTrue(build.contains("+ '"' + value + '"'"));
+ assertTrue(build.contains("leaves whitespace unquoted"));
+ assertTrue(build.contains("finalizedBy configureEclipseBuildship, 'isolateEclipseProductionRuns'"));
+ assertTrue(project.contains("org.eclipse.buildship.core.gradleprojectnature"));
+ assertTrue(project.contains("org.eclipse.buildship.core.gradleprojectbuilder"));
}
@Test
diff --git a/src/test/java/zone/moddev/mc/mineralogy/documentation/DocumentationContractTest.java b/src/test/java/zone/moddev/mc/mineralogy/documentation/DocumentationContractTest.java
index 5d49e8aa7..0319ee777 100644
--- a/src/test/java/zone/moddev/mc/mineralogy/documentation/DocumentationContractTest.java
+++ b/src/test/java/zone/moddev/mc/mineralogy/documentation/DocumentationContractTest.java
@@ -45,6 +45,10 @@ public void guidesDescribeTheTargetNative110Contract() throws Exception {
assertTrue(all.contains("Minecraft 1.10.2"));
assertTrue(all.contains("Forge 12.18.3.2511"));
assertTrue(all.contains("OreSpawn 4.0.6"));
+ assertTrue(all.contains("JDK 21"));
+ assertTrue(all.contains("Gradle 9.6.1"));
+ assertTrue(all.contains("ForgeGradle `7.0.34`"));
+ assertTrue(all.contains("Java 8 toolchain"));
assertTrue(all.contains("schema 4"));
assertTrue(all.contains("provider revision 3"));
assertTrue(all.contains("assets/mineralogy/orespawn/provider.json"));
@@ -73,7 +77,8 @@ public void guidesDescribeTheTargetNative110Contract() throws Exception {
assertFalse(targetNativeGuides.contains("mineralogy-common.toml"));
assertFalse(targetNativeGuides.contains("data/mineralogy/orespawn/provider.json"));
assertFalse(targetNativeGuides.contains("Minecraft 1.18.2"));
- assertFalse(targetNativeGuides.contains("Java 17"));
+ assertFalse(targetNativeGuides.contains(".\\gradlew.bat setupDecompWorkspace"));
+ assertFalse(targetNativeGuides.contains("Gradle 4.9"));
assertFalse(targetNativeGuides.toLowerCase().contains("deepslate"));
assertFalse(targetNativeGuides.contains("Y -48"));
}