Implement non-deobf for NeoForge 26.1 (albeit ugly, will fix), add integration test for NF 26.1-snapshot-11

This commit is contained in:
shedaniel
2026-03-21 23:28:24 +09:00
parent c6002c2d06
commit 9e04ddb5d0
15 changed files with 263 additions and 40 deletions

View File

@@ -23,7 +23,7 @@ access-transformers = "3.0.1"
access-transformers-new = "8.0.5"
access-transformers-neo = "10.0.2"
unprotect = "2.0.2"
asm = "9.7"
asm = "9.9.1"
access-transformers-log4j = "2.17.1"
forge-installer-tools = "1.2.0"
neoforge-installer-tools = "4.0.6"

View File

@@ -29,6 +29,7 @@ import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Nested;
import org.gradle.api.tasks.Optional;
import org.jspecify.annotations.Nullable;
import net.fabricmc.loom.LoomGradleExtension;
@@ -55,6 +56,7 @@ public final class ForgeSourcesService extends Service<ForgeSourcesService.Optio
@InputFiles
ConfigurableFileCollection getForgeSourceJars();
@Optional
@Nested
Property<SourceRemapperService.Options> getSourceRemapperService();
@@ -74,25 +76,27 @@ public final class ForgeSourcesService extends Service<ForgeSourcesService.Optio
final String sourceDependency = extension.getForgeUserdevProvider().getConfig().sources();
options.getForgeSourceJars().from(DependencyDownloader.download(project, sourceDependency));
options.getSourceRemapperService().set(SourceRemapperService.TYPE.create(project, sro -> {
final MappingsNamespace sourceNamespace = IntermediaryNamespaces.intermediaryNamespace(project);
final String targetNamespace = MappingsNamespace.NAMED.toString();
if (!extension.isUnobfuscatedForge()) {
options.getSourceRemapperService().set(SourceRemapperService.TYPE.create(project, sro -> {
final MappingsNamespace sourceNamespace = IntermediaryNamespaces.intermediaryNamespace(project);
final String targetNamespace = MappingsNamespace.NAMED.toString();
sro.getMappings().set(MappingsService.createOptionsWithProjectMappings(
project,
project.provider(sourceNamespace::toString),
project.provider(() -> targetNamespace)
));
sro.getJavaCompileRelease().set(SourceRemapperService.getJavaCompileRelease(project));
sro.getClasspath().from(DependencyDownloader.download(project, LoomVersions.JETBRAINS_ANNOTATIONS.mavenNotation()));
sro.getClasspath().from(extension.getMinecraftJars(sourceNamespace));
sro.getClasspath().from(project.getConfigurations().getByName(Constants.Configurations.MINECRAFT_COMPILE_LIBRARIES));
sro.getMappings().set(MappingsService.createOptionsWithProjectMappings(
project,
project.provider(sourceNamespace::toString),
project.provider(() -> targetNamespace)
));
sro.getJavaCompileRelease().set(SourceRemapperService.getJavaCompileRelease(project));
sro.getClasspath().from(DependencyDownloader.download(project, LoomVersions.JETBRAINS_ANNOTATIONS.mavenNotation()));
sro.getClasspath().from(extension.getMinecraftJars(sourceNamespace));
sro.getClasspath().from(project.getConfigurations().getByName(Constants.Configurations.MINECRAFT_COMPILE_LIBRARIES));
TinyRemapperHelper.JSR_TO_JETBRAINS.forEach((from, to) -> {
Pair<String, String> mapping = new Pair<>(from, to);
sro.getAdditionalClassMappings().add(mapping);
});
}));
TinyRemapperHelper.JSR_TO_JETBRAINS.forEach((from, to) -> {
Pair<String, String> mapping = new Pair<>(from, to);
sro.getAdditionalClassMappings().add(mapping);
});
}));
}
options.getShouldShowVerboseStderr().set(ForgeToolExecutor.shouldShowVerboseStderr(project));
@@ -174,8 +178,10 @@ public final class ForgeSourcesService extends Service<ForgeSourcesService.Optio
forgeSources.keySet().removeIf(classFilter.negate());
LOGGER.lifecycle(":extracted {} forge source classes", forgeSources.size());
try (var tempFiles = new TempFiles()) {
remapSources(tempFiles, forgeSources);
if (getOptions().getSourceRemapperService().isPresent()) {
try (var tempFiles = new TempFiles()) {
remapSources(tempFiles, forgeSources);
}
}
forgeSources.forEach(consumer);

View File

@@ -46,6 +46,7 @@ import org.gradle.api.artifacts.ModuleDependency;
import org.gradle.api.artifacts.ModuleVersionIdentifier;
import org.gradle.api.artifacts.ResolvedArtifact;
import org.gradle.api.artifacts.ResolvedConfiguration;
import org.jspecify.annotations.Nullable;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.api.mappings.layered.MappingContext;
@@ -82,7 +83,7 @@ public class ForgeLibrariesProvider {
private static final String NEOFORGE_GAME_LOCATOR_FILE = "net/neoforged/fml/loading/moddiscovery/locators/GameLocator.class";
private static final String NEOFORGE_REQUIRED_SYSTEM_FILES_FILE = "net/neoforged/fml/loading/moddiscovery/locators/RequiredSystemFiles.class";
public static void provide(MappingConfiguration mappingConfiguration, Project project) throws Exception {
public static void provide(@Nullable MappingConfiguration mappingConfiguration, Project project) throws Exception {
LoomGradleExtension extension = LoomGradleExtension.get(project);
final List<Dependency> dependencies = new ArrayList<>();
@@ -145,7 +146,7 @@ public class ForgeLibrariesProvider {
isFancyModLoader10OrNewer = true;
}
if (isFML || isFancyML) {
if ((isFML || isFancyML) && mappingConfiguration != null) {
// If FML, remap it.
try (var serviceFactory = new ScopedServiceFactory()) {
if (isFML) {

View File

@@ -157,7 +157,9 @@ public class MinecraftPatchedProvider {
minecraftProvider.setJarPrefix(patchId);
final String intermediateId = getExtension().isNeoForge() ? "mojang" : "srg";
final String intermediateId = getExtension().isNeoForge()
? (getExtension().isUnobfuscatedForge() ? "official" : "mojang")
: "srg";
minecraftIntermediateJar = forgeWorkingDir.resolve("minecraft-" + type.id + "-" + intermediateId + ".jar");
minecraftPatchedIntermediateJar = forgeWorkingDir.resolve("minecraft-" + type.id + "-" + intermediateId + "-patched.jar");
minecraftPatchedIntermediateAtJar = forgeWorkingDir.resolve("minecraft-" + type.id + "-" + intermediateId + "-at-patched.jar");
@@ -231,14 +233,40 @@ public class MinecraftPatchedProvider {
public void remapJar(ServiceFactory serviceFactory) throws Exception {
if (dirty) {
remapPatchedJar(serviceFactory);
if (getExtension().isUnobfuscatedForge()) {
mergeUnobfuscatedPatchedJar();
} else {
remapPatchedJar(serviceFactory);
}
fillClientExtraJar(serviceFactory);
}
if (getExtension().isUnobfuscatedForge()) {
DependencyProvider.addDependency(project, getForgeJar(), Constants.Configurations.FORGE_EXTRA);
}
DependencyProvider.addDependency(project, minecraftClientExtra, Constants.Configurations.FORGE_EXTRA);
}
private void mergeUnobfuscatedPatchedJar() throws IOException {
logger.lifecycle(":merging userdev into minecraft");
Path mcOutput = minecraftPatchedJar;
Path forgeUserdevJar = getForgeUserdevJar().toPath();
Files.deleteIfExists(mcOutput);
Files.copy(minecraftPatchedIntermediateAtJar, mcOutput);
copyUserdevFiles(forgeUserdevJar, mcOutput);
applyLoomPatchVersion(mcOutput);
}
private void createPrePatchJar() throws IOException {
if (getExtension().isUnobfuscatedForge()) {
createUnobfuscatedPrePatchJar();
return;
}
if (shouldUseNeoForgeInstallerToolsToCreatePrePatchJar()) {
createNeoForgeInstallerToolsPrePatchJar();
return;
@@ -253,6 +281,16 @@ public class MinecraftPatchedProvider {
}
}
private void createUnobfuscatedPrePatchJar() throws IOException {
try (var tempFiles = new TempFiles(); var serviceFactory = new ScopedServiceFactory()) {
McpExecutorBuilder builder = createMcpExecutor(tempFiles.directory("loom-mcp"));
builder.enqueue("preProcessJar");
McpExecutor executor = serviceFactory.get(builder.build());
Path output = executor.execute();
Files.copy(output, minecraftIntermediateJar, StandardCopyOption.REPLACE_EXISTING);
}
}
private void createNeoForgeInstallerToolsPrePatchJar() throws IOException {
try (var tempFiles = new TempFiles()) {
final Path mappings = tempFiles.file("mappings", ".txt");
@@ -300,7 +338,11 @@ public class MinecraftPatchedProvider {
// The manifest includes a Minecraft-Dists attribute that specifies the dists in the current dev env,
// as well as Minecraft-Dist attributes on every dist-only file.
private void generateNeoForgeDistManifest(ServiceFactory serviceFactory, Path manifestPath) throws IOException {
MemoryMappingTree mappings = getMappingTree(serviceFactory);
// For unobfuscated NeoForge the classes are already in official namespace; an empty tree is safe
// because SidedJarIndexGenerator falls back to the original name when no mapping is found.
MemoryMappingTree mappings = getExtension().isUnobfuscatedForge()
? new MemoryMappingTree()
: getMappingTree(serviceFactory);
Manifest manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
@@ -527,7 +569,7 @@ public class MinecraftPatchedProvider {
copyMissingClasses(minecraftIntermediateJar, minecraftPatchedIntermediateJar);
deleteParameterNames(minecraftPatchedIntermediateJar);
if (getExtension().isForgeLikeAndNotOfficial()) {
if (getExtension().isForgeLikeAndNotOfficial() && !getExtension().isUnobfuscatedForge()) {
fixParameterAnnotation(minecraftPatchedIntermediateJar);
}

View File

@@ -31,13 +31,14 @@ import java.util.Map;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.jspecify.annotations.Nullable;
/**
* Data extracted from the MCPConfig JSON file.
*
* @param version the Minecraft version - the value of the {@code version} property
* @param data the value of the {@code data} property
* @param mappingsPath the path to srg mappings inside the MCP zip
* @param mappingsPath the path to srg mappings inside the MCP zip, or {@code null} if absent (spec 6+)
* @param official the value of the {@code official} property
* @param steps the MCP step definitions by environment type
* @param functions the MCP function definitions by name
@@ -45,15 +46,19 @@ import com.google.gson.JsonObject;
public record McpConfigData(
String version,
JsonObject data,
String mappingsPath,
@Nullable String mappingsPath,
boolean official,
Map<String, List<McpConfigStep>> steps,
Map<String, McpConfigFunction> functions
) {
public boolean hasMappings() {
return mappingsPath != null;
}
public static McpConfigData fromJson(JsonObject json) {
String version = json.get("version").getAsString();
JsonObject data = json.getAsJsonObject("data");
String mappingsPath = data.get("mappings").getAsString();
@Nullable String mappingsPath = data.has("mappings") ? data.get("mappings").getAsString() : null;
boolean official = json.has("official") && json.getAsJsonPrimitive("official").getAsBoolean();
JsonObject stepsJson = json.getAsJsonObject("steps");

View File

@@ -47,6 +47,7 @@ import org.jspecify.annotations.Nullable;
*/
public record McpConfigFunction(String version, List<ConfigValue> args, List<ConfigValue> jvmArgs, @Nullable String repo) implements Serializable {
private static final String VERSION_KEY = "version";
private static final String CLASSPATH_KEY = "classpath";
private static final String ARGS_KEY = "args";
private static final String JVM_ARGS_KEY = "jvmargs";
private static final String REPO_KEY = "repo";
@@ -82,11 +83,21 @@ public record McpConfigFunction(String version, List<ConfigValue> args, List<Con
}
public static McpConfigFunction fromJson(JsonObject json) {
String version = json.get(VERSION_KEY).getAsString();
String version;
if (json.has(VERSION_KEY)) {
version = json.get(VERSION_KEY).getAsString();
} else if (json.has(CLASSPATH_KEY)) {
// Spec 6+: uses classpath array instead of version string
version = json.getAsJsonArray(CLASSPATH_KEY).get(0).getAsString();
} else {
throw new IllegalArgumentException("MCP config function has neither 'version' nor 'classpath'");
}
List<ConfigValue> args = json.has(ARGS_KEY) ? configValuesFromJson(json.getAsJsonArray(ARGS_KEY)) : List.of();
List<ConfigValue> jvmArgs = json.has(JVM_ARGS_KEY) ? configValuesFromJson(json.getAsJsonArray(JVM_ARGS_KEY)) : List.of();
JsonElement repoJson = json.get(REPO_KEY);
@Nullable String repo = repoJson.isJsonPrimitive() ? repoJson.getAsString() : null;
@Nullable JsonElement repoJson = json.get(REPO_KEY);
@Nullable String repo = repoJson != null && repoJson.isJsonPrimitive() ? repoJson.getAsString() : null;
return new McpConfigFunction(version, args, jvmArgs, repo);
}

View File

@@ -84,7 +84,15 @@ public class McpConfigProvider extends DependencyProvider {
configJson = unpacked.resolve("config.json");
}
public boolean hasMappings() {
return data.hasMappings();
}
public Path getMappings() {
if (!hasMappings()) {
throw new UnsupportedOperationException("MCP config has no mappings (spec 6+ unobfuscated)");
}
return unpacked.resolve(getMappingsPath());
}

View File

@@ -51,6 +51,7 @@ import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.Nested;
import org.gradle.api.tasks.Optional;
import org.jspecify.annotations.Nullable;
import net.fabricmc.loom.util.download.Download;
@@ -90,7 +91,9 @@ public final class McpExecutor extends Service<McpExecutor.Options> {
/**
* Mappings extracted from {@code data.mappings} in the MCPConfig JSON.
* Optional for spec 6+ where mappings are absent.
*/
@Optional
@InputFile
RegularFileProperty getMappings();
@@ -217,6 +220,10 @@ public final class McpExecutor extends Service<McpExecutor.Options> {
@Override
public Path mappings() {
if (!getOptions().getMappings().isPresent()) {
throw new UnsupportedOperationException("Mappings are not available (spec 6+ unobfuscated)");
}
return getOptions().getMappings().get().getAsFile().toPath();
}

View File

@@ -36,6 +36,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.SortedSet;
@@ -194,7 +195,11 @@ public final class McpExecutorBuilder {
}
options.getStepsToExecute().set(toExecute);
options.getMappings().set(extension.getMcpConfigProvider().getMappings().toFile());
if (extension.getMcpConfigProvider().hasMappings()) {
options.getMappings().set(extension.getMcpConfigProvider().getMappings().toFile());
}
options.getInitialConfig().set(config);
options.getOffline().set(project.getGradle().getStartParameter().isOffline());
options.getManualRefreshDeps().set(extension.manualRefreshDeps());
@@ -228,8 +233,12 @@ public final class McpExecutorBuilder {
case "downloadServer" -> ConstantLogic.createOptions(setupContext, () -> minecraftProvider.getMinecraftServerJar().toPath());
case "strip" -> StripLogic.createOptions(setupContext);
case "listLibraries" -> ListLibrariesLogic.createOptions(setupContext);
case "downloadClientMappings" -> DownloadManifestFileLogic.createOptions(setupContext, minecraftProvider.getVersionInfo().download("client_mappings"));
case "downloadServerMappings" -> DownloadManifestFileLogic.createOptions(setupContext, minecraftProvider.getVersionInfo().download("server_mappings"));
case "downloadClientMappings" -> DownloadManifestFileLogic.createOptions(setupContext,
Objects.requireNonNull(minecraftProvider.getVersionInfo().download("client_mappings"),
"client_mappings download is not available for this Minecraft version"));
case "downloadServerMappings" -> DownloadManifestFileLogic.createOptions(setupContext,
Objects.requireNonNull(minecraftProvider.getVersionInfo().download("server_mappings"),
"server_mappings download is not available for this Minecraft version"));
case "inject" -> InjectLogic.createOptions(setupContext);
case "patch" -> PatchLogic.createOptions(setupContext);
default -> {

View File

@@ -188,6 +188,10 @@ public interface LoomGradleExtension extends LoomGradleExtensionAPI {
return isForgeLike() && !getMcpConfigProvider().isOfficial();
}
default boolean isUnobfuscatedForge() {
return isForgeLike() && getProductionNamespace().get().equals(MappingsNamespace.OFFICIAL.toString());
}
DependencyProviders getDependencyProviders();
void setDependencyProviders(DependencyProviders dependencyProviders);

View File

@@ -242,9 +242,15 @@ public abstract class CompileConfiguration implements Runnable {
throw new UnsupportedOperationException("Using %s with split jars is not supported!".formatted(extension.getPlatform().get().displayName()));
}
if (extension.isForgeLike() && extension.disableObfuscation()) {
// TODO: Allow setting up Forge and NeoForge without obfuscation
throw new UnsupportedOperationException("Using %s without obfuscation is not supported!".formatted(extension.getPlatform().get().displayName()));
// TODO: Re-evaluate if isUnobfuscatedForge() should even exist, or if the checks below should be removed
if (extension.isForgeLike() && extension.disableObfuscation() && !extension.isUnobfuscatedForge()) {
throw new UnsupportedOperationException(("Architectury Loom: The dev.architectury.loom-no-remap plugin was applied, but the Minecraft version '%s' is obfuscated. " +
"Forge / NeoForge support for obfuscated Minecraft is through the regular dev.architectury.loom plugin instead.").formatted(metadataProvider.getMinecraftVersion()));
}
if (extension.isForgeLike() && !extension.disableObfuscation() && extension.isUnobfuscatedForge()) {
throw new UnsupportedOperationException(("Architectury Loom: The Minecraft version '%s' is unobfuscated (no mappings). " +
"Forge / NeoForge support for unobfuscated Minecraft is through the dev.architectury.loom-no-remap plugin instead.").formatted(metadataProvider.getMinecraftVersion()));
}
extension.setMinecraftProvider(minecraftProvider);
@@ -273,9 +279,14 @@ public abstract class CompileConfiguration implements Runnable {
mappingConfiguration.setupPost(project);
mappingConfiguration.applyToProject(getProject(), mappingsDep);
} else if (extension.isUnobfuscatedForge()) {
// Unobfuscated NeoForge: run the forge patch pipeline without requiring user-provided mappings.
setupDependencyProviders(project, extension);
ForgeLibrariesProvider.provide(null, project);
((ForgeMinecraftProvider) minecraftProvider).getPatchedProvider().provide();
}
if (extension.isForgeLike() && extension.getForgeProvider().usesMojangAtRuntime()) {
if (extension.isForgeLike() && extension.getForgeProvider().usesMojangAtRuntime() && !extension.isUnobfuscatedForge()) {
extension.getRuntimeIntermediaryNamespace().set(MappingsNamespace.MOJANG.toString());
}
@@ -315,7 +326,7 @@ public abstract class CompileConfiguration implements Runnable {
srgMinecraftProvider.provide(provideContext);
}
if (extension.isForgeLike() && extension.getForgeProvider().usesMojangAtRuntime()) {
if (extension.isForgeLike() && extension.getForgeProvider().usesMojangAtRuntime() && !extension.isUnobfuscatedForge()) {
final MojangMappedMinecraftProvider<?> mojangMappedMinecraftProvider = jarConfiguration.createMojangMappedMinecraftProvider(project);
extension.setMojangMappedMinecraftProvider(mojangMappedMinecraftProvider);
mojangMappedMinecraftProvider.provide(provideContext);

View File

@@ -0,0 +1,58 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2025 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.test.integration.neoforge
import spock.lang.Specification
import spock.lang.Unroll
import net.fabricmc.loom.test.util.GradleProjectTestTrait
import static net.fabricmc.loom.test.LoomTestConstants.DEFAULT_GRADLE
import static org.gradle.testkit.runner.TaskOutcome.SUCCESS
class NeoForge261Test extends Specification implements GradleProjectTestTrait {
@Unroll
def "build #mcVersion #neoforgeVersion"() {
if (Integer.valueOf(System.getProperty("java.version").split("\\.")[0]) < 21) {
println("This test requires Java 21. Currently you have Java ${System.getProperty("java.version")}.")
return
}
setup:
def gradle = gradleProject(project: "neoforge/261", version: DEFAULT_GRADLE)
gradle.buildGradle.text = gradle.buildGradle.text.replace('@MCVERSION@', mcVersion)
.replace('@NEOFORGEVERSION@', neoforgeVersion)
when:
def result = gradle.run(task: "build")
then:
result.task(":build").outcome == SUCCESS
where:
mcVersion | neoforgeVersion
'26.1-snapshot-11' | '26.1.0.0-alpha.14+snapshot-11'
}
}

View File

@@ -0,0 +1,50 @@
plugins {
id 'dev.architectury.loom-no-remap'
id 'maven-publish'
}
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
base {
archivesName = project.archives_base_name
}
version = project.mod_version
group = project.maven_group
def mcVersion = "@MCVERSION@"
def neoforgeVersion = "@NEOFORGEVERSION@"
repositories {
maven { url = "https://maven.neoforged.net/releases/" }
}
dependencies {
minecraft "com.mojang:minecraft:$mcVersion"
neoForge "net.neoforged:neoforge:$neoforgeVersion"
}
tasks.withType(JavaCompile).configureEach {
it.options.release = 21
}
java {
withSourcesJar()
}
jar {
from("LICENSE") {
rename { "${it}_${project.archivesBaseName}"}
}
}
publishing {
publications {
mavenJava(MavenPublication) {
from components.java
}
}
}

View File

@@ -0,0 +1,10 @@
# Done to increase the memory available to gradle.
org.gradle.jvmargs=-Xmx1G
# Mod Properties
mod_version = 1.0.0
maven_group = com.example
archives_base_name = fabric-example-mod
# Dependencies
loom.platform = neoforge

View File

@@ -0,0 +1 @@
rootProject.name = "fabric-example-mod"