Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Provide better errors when game-layer mod files try to provide class …
…processors
  • Loading branch information
shartte committed Oct 5, 2025
commit 0c55478833385e63ac5182b1c7a680ce25297aad
31 changes: 26 additions & 5 deletions loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import net.neoforged.fml.IBindingsProvider;
import net.neoforged.fml.ModList;
import net.neoforged.fml.ModLoader;
import net.neoforged.fml.ModLoadingException;
import net.neoforged.fml.ModLoadingIssue;
import net.neoforged.fml.classloading.JarModuleFinder;
import net.neoforged.fml.classloading.ResourceMaskingClassLoader;
Expand Down Expand Up @@ -141,7 +142,15 @@ record DiscoveryResult(
List<ModFile> pluginContent,
List<ModFile> gameContent,
List<ModFile> gameLibraryContent,
List<ModLoadingIssue> discoveryIssues) {}
List<ModLoadingIssue> discoveryIssues) {
public List<ModFile> allGameContent() {
var content = new ArrayList<ModFile>(
gameContent.size() + gameLibraryContent.size());
content.addAll(gameContent);
content.addAll(gameLibraryContent);
return content;
}
}

private FMLLoader(ClassLoader currentClassLoader, String[] programArgs, Dist dist, boolean production, Path gameDir) {
this.currentClassLoader = currentClassLoader;
Expand Down Expand Up @@ -323,10 +332,7 @@ public static FMLLoader create(@Nullable Instrumentation instrumentation, Startu
// BUILD GAME LAYER
// NOTE: This is where Mixin contributes its synthetic SecureJar to ensure it's generated classes are handled by the TCL
var gameContent = new ArrayList<SecureJar>();
for (var modFile : discoveryResult.gameLibraryContent) {
gameContent.add(modFile.getSecureJar());
}
for (var modFile : discoveryResult.gameContent) {
for (var modFile : discoveryResult.allGameContent()) {
gameContent.add(modFile.getSecureJar());
}

Expand Down Expand Up @@ -364,6 +370,21 @@ private static ClassProcessorSet createClassProcessorSet(StartupArgs startupArgs
LaunchContextAdapter launchContext,
DiscoveryResult discoveryResult,
MixinFacade mixinFacade) {
// Validate that the modder didn't try to provide transformation services from game content
var issues = new ArrayList<ModLoadingIssue>();
for (var modFile : discoveryResult.allGameContent()) {
var descriptor = modFile.getSecureJar().moduleDataProvider().descriptor();
for (var provides : descriptor.provides()) {
if (provides.service().equals(ClassProcessorProvider.class.getName())
|| provides.service().equals(ClassProcessor.class.getName())) {
issues.add(ModLoadingIssue.error("fml.modloadingissue.classprocessor_in_game_content").withAffectedModFile(modFile));
}
}
}
if (!issues.isEmpty()) {
throw new ModLoadingException(issues);
}

// Add our own launch plugins explicitly.
var builtInProcessors = new ArrayList<ClassProcessor>();
builtInProcessors.add(createAccessTransformerService(discoveryResult));
Expand Down
1 change: 1 addition & 0 deletions loader/src/main/resources/lang/en_us.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"fml.modloadingissue.dependencyloading.conflictingdependencies": "Some mods have requested conflicting versions of: §6{0}§r. Requested by: §e{1}§r.",
"fml.modloadingissue.dependencyloading.mismatchedcontaineddependencies": "Some mods have agreed upon an acceptable version range for : §6{0}§r, but no jar was provided which matched the range. Requested by: §e{1}§r.",
"fml.modloadingissue.coremod_error": "An error occurred while loading core-mod {0} from {1}",
"fml.modloadingissue.classprocessor_in_game_content": "Mod file {101} is trying to provide a ClassProcessor service. Mods and game libraries cannot provide such services, only libraries can.",
"fml.modloadingissue.corrupted_installation": "Your NeoForge installation is corrupted. Please try to reinstall NeoForge.",
"fml.modloadingissue.missing_minecraft_jar": "The patched Minecraft jar is missing. Please try to reinstall NeoForge.",
"fml.modloadingissue.corrupted_minecraft_jar": "The patched Minecraft jar is corrupted. Please try to reinstall NeoForge.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,6 @@ private static ModuleDescriptor getJdkModuleDescriptor(Path testJar) {

@FunctionalInterface
interface ModFileCustomizer {
void customize(ModFileBuilder builder) throws IOException;
void customize(ModFileBuilder<?> builder) throws IOException;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.io.IOException;
import java.util.Set;
import net.neoforged.fml.ModLoadingException;
import net.neoforged.fml.testlib.ModFileBuilder;
import net.neoforged.fml.testlib.SimulatedInstallation;
import net.neoforged.fml.testlib.args.ClientInstallationTypesSource;
import net.neoforged.jarjar.metadata.ContainedJarIdentifier;
import net.neoforged.neoforgespi.locating.IModFile;
import net.neoforged.neoforgespi.transformation.ClassProcessor;
Expand All @@ -19,6 +23,7 @@
import net.neoforged.neoforgespi.transformation.SimpleClassProcessor;
import net.neoforged.neoforgespi.transformation.SimpleTransformationContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.objectweb.asm.tree.ClassNode;

/**
Expand Down Expand Up @@ -119,4 +124,45 @@ public void createProcessors(Context context, Collector collector) {

assertEquals("fml:test", loader.getClassTransformerAuditLog().getAuditString("testmod.TestClass"));
}

/**
* Class processors shouldn't be able to be loaded from mod jars, and the modder should get a proper error
* for this case.
*/
@ParameterizedTest
@ClientInstallationTypesSource
public void testProcessorProvidedByModJar(SimulatedInstallation.Type type) throws Exception {
testProcessorProvidedByGameContent(type, ModFileBuilder::withTestmodModsToml);
}

/**
* Class processors shouldn't be able to be loaded from game libraries, and the modder should get a proper error
* for this case.
*/
@ParameterizedTest
@ClientInstallationTypesSource
public void testProcessorProvidedByGameLibrary(SimulatedInstallation.Type type) throws Exception {
testProcessorProvidedByGameContent(type, builder -> builder.withModTypeManifest("GAMELIBRARY"));
}

private void testProcessorProvidedByGameContent(SimulatedInstallation.Type type, ModFileBuilder.ModJarCustomizer customizer) throws IOException {
installation.setup(type);
installation.buildInstallationAppropriateModProject("testmod", "testmod.jar", builder -> {
customizer.customize(builder);
builder.addClass("testmod.TestProvider", """
public class TestProvider implements net.neoforged.neoforgespi.transformation.ClassProcessorProvider {
@Override
public void createProcessors(Context context, Collector collector) {
throw new RuntimeException();
}
}""")
.addService(ClassProcessorProvider.class, "testmod.TestProvider");
});

var e = assertThrows(ModLoadingException.class, () -> launchAndLoad("neoforgeclient"));
var issues = getTranslatedIssues(e.getIssues());
assertThat(issues).hasSize(1);
assertThat(issues.getFirst()).matches(
"ERROR: Mod file .* is trying to provide a ClassProcessor service. Mods and game libraries cannot provide such services, only libraries can.");
}
}