From 3ceac958f6197a90156598b661469f170bcb9cfc Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Sun, 26 Oct 2025 12:22:25 +0100 Subject: [PATCH 01/29] Start working on auto installer. --- gradle.properties | 2 +- .../net/neoforged/fml/VersionChecker.java | 2 +- .../fml/loading/ClassLoaderStack.java | 164 ++++++++ .../net/neoforged/fml/loading/FMLLoader.java | 375 ++++++------------ .../fml/loading/ImmediateWindowHandler.java | 8 +- .../fml/loading/LanguageProviderLoader.java | 7 +- .../neoforged/fml/loading/VersionInfo.java | 12 - .../fml/loading/VersionSupportMatrix.java | 4 +- .../GameDiscovery.java} | 239 +++++------ .../fml/loading/game/GameDiscoveryResult.java | 13 + .../locators => game}/MinecraftModInfo.java | 2 +- .../RequiredSystemFiles.java | 22 +- .../fml/loading/game/package-info.java | 9 + .../loading/moddiscovery/ModDiscoverer.java | 12 +- .../neoforged/fml/util/ServiceLoaderUtil.java | 26 +- .../neoforged/neoforgespi/ILaunchContext.java | 25 +- .../neoforged/neoforgespi/LocatedPaths.java | 22 + 17 files changed, 499 insertions(+), 445 deletions(-) create mode 100644 loader/src/main/java/net/neoforged/fml/loading/ClassLoaderStack.java delete mode 100644 loader/src/main/java/net/neoforged/fml/loading/VersionInfo.java rename loader/src/main/java/net/neoforged/fml/loading/{moddiscovery/locators/GameLocator.java => game/GameDiscovery.java} (66%) create mode 100644 loader/src/main/java/net/neoforged/fml/loading/game/GameDiscoveryResult.java rename loader/src/main/java/net/neoforged/fml/loading/{moddiscovery/locators => game}/MinecraftModInfo.java (96%) rename loader/src/main/java/net/neoforged/fml/loading/{moddiscovery/locators => game}/RequiredSystemFiles.java (91%) create mode 100644 loader/src/main/java/net/neoforged/fml/loading/game/package-info.java create mode 100644 loader/src/main/java/net/neoforged/neoforgespi/LocatedPaths.java diff --git a/gradle.properties b/gradle.properties index c596eb655..36e0732cb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ gradleutils_version=5.0.1 -test_neoforge_version=21.10.20-beta +test_neoforge_version=21.10.37-beta test_minecraft_version=1.21.10 mergetool_version=2.0.0 diff --git a/loader/src/main/java/net/neoforged/fml/VersionChecker.java b/loader/src/main/java/net/neoforged/fml/VersionChecker.java index cc3a1cd70..0b45663ff 100644 --- a/loader/src/main/java/net/neoforged/fml/VersionChecker.java +++ b/loader/src/main/java/net/neoforged/fml/VersionChecker.java @@ -169,7 +169,7 @@ private void process(IModInfo mod) { Map promos = (Map) json.get("promos"); display_url = (String) json.get("homepage"); - var mcVersion = FMLLoader.getCurrent().getVersionInfo().mcVersion(); + var mcVersion = FMLLoader.getCurrent().getMinecraftVersion(); String rec = promos.get(mcVersion + "-recommended"); String lat = promos.get(mcVersion + "-latest"); ComparableVersion current = new ComparableVersion(mod.getVersion().toString()); diff --git a/loader/src/main/java/net/neoforged/fml/loading/ClassLoaderStack.java b/loader/src/main/java/net/neoforged/fml/loading/ClassLoaderStack.java new file mode 100644 index 000000000..f5ff7ae8f --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/loading/ClassLoaderStack.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading; + +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import net.neoforged.fml.classloading.JarContentsModule; +import net.neoforged.fml.classloading.ResourceMaskingClassLoader; +import net.neoforged.fml.jarcontents.CompositeJarContents; +import net.neoforged.fml.jarcontents.EmptyJarContents; +import net.neoforged.fml.jarcontents.FolderJarContents; +import net.neoforged.fml.jarcontents.JarContents; +import net.neoforged.fml.jarcontents.JarFileContents; +import net.neoforged.fml.util.ClasspathResourceUtils; +import net.neoforged.fml.util.PathPrettyPrinting; +import net.neoforged.neoforgespi.LocatedPaths; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class ClassLoaderStack implements AutoCloseable { + private static final Logger LOGGER = LoggerFactory.getLogger(ClassLoaderStack.class); + + private final List ownedResources = new ArrayList<>(); + private final LocatedPaths locatedPaths; + /** + * The context class-loader that will be restored when the loader is closed. + */ + @Nullable + private final ClassLoader originalClassLoader; + /** + * The current tail of the class-loader chain. It is moved whenever a new set of Jars is loaded. + */ + private ClassLoader currentClassLoader; + + public ClassLoaderStack(ClassLoader initialLoader, LocatedPaths locatedPaths) { + this.currentClassLoader = initialLoader; + this.locatedPaths = locatedPaths; + this.originalClassLoader = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(currentClassLoader); + } + + /** + * Loads the given services into a URL classloader. + */ + public void appendLoader(String loaderName, List jars) { + if (jars.isEmpty()) { + LOGGER.info("No additional classpath items for {} were found.", loaderName); + return; + } + + LOGGER.info("Loading {}:", loaderName); + + List rootUrls = new ArrayList<>(jars.size()); + for (var jar : jars) { + if (jar instanceof CompositeJarContents compositeJarContents && compositeJarContents.isFiltered()) { + throw new IllegalArgumentException("Cannot use simple URLClassLoader for filtered content " + jar); + } + + // TODO: Order on the classpath matters, we need to double-check the content roots are in the right order here + for (var contentRoot : jar.getContentRoots()) { + LOGGER.info(" - {}", PathPrettyPrinting.prettyPrint(contentRoot)); + try { + rootUrls.add(contentRoot.toUri().toURL()); + } catch (MalformedURLException e) { + throw new RuntimeException(e); // This should not happen for file URLs + } + locatedPaths.addLocated(contentRoot); // Prevents it from getting picked up again + } + } + + var loader = new URLClassLoader(loaderName, rootUrls.toArray(URL[]::new), currentClassLoader); + ownedResources.add(loader); + currentClassLoader = loader; + Thread.currentThread().setContextClassLoader(loader); + } + + public ClassLoader getCurrentClassLoader() { + return currentClassLoader; + } + + public void append(ClassLoader loader) { + currentClassLoader = loader; + } + + /** + * If any location being added is already on the classpath, we add a masking classloader to ensure + * that resources are not double-reported when using getResources/getResource. + *

+ * The primary purpose of this is in mod and NeoForge development environments, where IDEs put the mod + * on the app classpath, but we also add it as content to the game layer. This method is responsible + * for setting up a classloader that prevents getResource/getResources from reporting Jar resources + * for both the jar on the App classpath and on the transforming classloader. + */ + public void maskContentAlreadyOnClasspath(List content) { + var classpathItems = ClasspathResourceUtils.getAllClasspathItems(getCurrentClassLoader()); + + // Collect all paths that make up the game content, which are already on the classpath + Set needsMasking = new HashSet<>(); + for (var secureJar : content) { + for (var basePath : getBasePaths(secureJar.contents(), true)) { + if (classpathItems.contains(basePath)) { + needsMasking.add(basePath); + } + } + } + + if (!needsMasking.isEmpty()) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Masking classpath elements: {}", needsMasking.stream().map(PathPrettyPrinting::prettyPrint).toList()); + } + + var maskedLoader = new ResourceMaskingClassLoader(currentClassLoader, needsMasking); + if (Thread.currentThread().getContextClassLoader() == currentClassLoader) { + Thread.currentThread().setContextClassLoader(maskedLoader); + } + currentClassLoader = maskedLoader; + } + } + + private static List getBasePaths(JarContents contents, boolean ignoreFilter) { + var result = new ArrayList(); + switch (contents) { + case CompositeJarContents compositeModContainer -> { + if (!ignoreFilter && compositeModContainer.isFiltered()) { + throw new IllegalStateException("Cannot load filtered Jar content into a URL classloader"); + } + for (var delegate : compositeModContainer.getDelegates()) { + result.addAll(getBasePaths(delegate, ignoreFilter)); + } + } + case EmptyJarContents ignored -> {} + case FolderJarContents folderModContainer -> result.add(folderModContainer.getPrimaryPath()); + case JarFileContents jarModContainer -> result.add(jarModContainer.getPrimaryPath()); + default -> throw new IllegalStateException("Don't know how to handle " + contents); + } + return result; + } + + @Override + public void close() { + for (var ownedResource : ownedResources) { + try { + ownedResource.close(); + } catch (Exception e) { + LOGGER.error("Failed to close resource {} owned by class loader stack", ownedResource, e); + } + } + ownedResources.clear(); + + if (Thread.currentThread().getContextClassLoader() == currentClassLoader) { + Thread.currentThread().setContextClassLoader(originalClassLoader); + } + } +} diff --git a/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java b/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java index 85a1bb9df..88cda64d4 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java +++ b/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java @@ -6,36 +6,6 @@ package net.neoforged.fml.loading; import com.mojang.logging.LogUtils; -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.lang.instrument.Instrumentation; -import java.lang.module.Configuration; -import java.lang.module.ModuleDescriptor; -import java.lang.module.ModuleFinder; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.ServiceLoader; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.Stream; import net.neoforged.accesstransformer.api.AccessTransformerEngine; import net.neoforged.api.distmarker.Dist; import net.neoforged.fml.FMLVersion; @@ -46,7 +16,6 @@ import net.neoforged.fml.ModLoadingIssue; import net.neoforged.fml.classloading.JarContentsModule; import net.neoforged.fml.classloading.JarContentsModuleFinder; -import net.neoforged.fml.classloading.ResourceMaskingClassLoader; import net.neoforged.fml.classloading.transformation.ClassProcessorAuditLog; import net.neoforged.fml.classloading.transformation.ClassProcessorAuditSource; import net.neoforged.fml.classloading.transformation.ClassProcessorSet; @@ -55,17 +24,14 @@ import net.neoforged.fml.common.asm.SimpleProcessorsGroup; import net.neoforged.fml.common.asm.enumextension.RuntimeEnumExtender; import net.neoforged.fml.i18n.FMLTranslations; -import net.neoforged.fml.jarcontents.CompositeJarContents; -import net.neoforged.fml.jarcontents.EmptyJarContents; -import net.neoforged.fml.jarcontents.FolderJarContents; import net.neoforged.fml.jarcontents.JarContents; -import net.neoforged.fml.jarcontents.JarFileContents; import net.neoforged.fml.jarcontents.JarResource; +import net.neoforged.fml.loading.game.GameDiscovery; +import net.neoforged.fml.loading.game.GameDiscoveryResult; import net.neoforged.fml.loading.mixin.MixinFacade; import net.neoforged.fml.loading.moddiscovery.ModDiscoverer; import net.neoforged.fml.loading.moddiscovery.ModFile; import net.neoforged.fml.loading.moddiscovery.ModFileInfo; -import net.neoforged.fml.loading.moddiscovery.locators.GameLocator; import net.neoforged.fml.loading.moddiscovery.locators.InDevFolderLocator; import net.neoforged.fml.loading.moddiscovery.locators.InDevJarLocator; import net.neoforged.fml.loading.moddiscovery.locators.ModsFolderLocator; @@ -74,10 +40,10 @@ import net.neoforged.fml.loading.progress.StartupNotificationManager; import net.neoforged.fml.startup.InstrumentationHelper; import net.neoforged.fml.startup.StartupArgs; -import net.neoforged.fml.util.ClasspathResourceUtils; import net.neoforged.fml.util.PathPrettyPrinting; import net.neoforged.fml.util.ServiceLoaderUtil; import net.neoforged.neoforgespi.ILaunchContext; +import net.neoforged.neoforgespi.LocatedPaths; import net.neoforged.neoforgespi.language.IModFileInfo; import net.neoforged.neoforgespi.language.IModInfo; import net.neoforged.neoforgespi.locating.IModFile; @@ -91,23 +57,43 @@ import org.slf4j.Logger; import org.slf4j.event.Level; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.instrument.Instrumentation; +import java.lang.module.Configuration; +import java.lang.module.ModuleDescriptor; +import java.lang.module.ModuleFinder; +import java.net.URI; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + public final class FMLLoader implements AutoCloseable { private static final Logger LOGGER = LogUtils.getLogger(); private static final AtomicReference<@Nullable FMLLoader> current = new AtomicReference<>(); + private final ClassLoaderStack classLoaderStack; + /** - * The context class-loader that will be restored when the loader is closed. - */ - @Nullable - private final ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader(); - /** - * The current tail of the class-loader chain. It is moved whenever a new set of Jars is loaded. - */ - private ClassLoader currentClassLoader; - /** - * Resources owned by this loader, such as opened URL classloaders, which - * will be closed alongside the loader. + * Resources owned by this loader, which will be closed alongside the loader. */ private final List ownedResources = new ArrayList<>(); /** @@ -122,13 +108,12 @@ public final class FMLLoader implements AutoCloseable { private final Path gameDir; private final Set locatedPaths = new HashSet<>(); - private VersionInfo versionInfo; private VersionSupportMatrix versionSupportMatrix; public BackgroundScanHandler backgroundScanHandler; - private final boolean production; @Nullable private ModuleLayer gameLayer; private final List earlyServicesJars = new ArrayList<>(); + private final GameDiscoveryResult discoveredGame; @VisibleForTesting DiscoveryResult discoveryResult; private final ClassProcessorAuditLog classTransformerAuditLog = new ClassProcessorAuditLog(); @@ -169,26 +154,13 @@ public boolean hasErrors() { } } - private FMLLoader(ClassLoader currentClassLoader, String[] programArgs, Dist dist, boolean production, Path gameDir) { - this.currentClassLoader = currentClassLoader; - this.programArgs = ProgramArgs.from(programArgs); + private FMLLoader(ClassLoaderStack classLoaderStack, GameDiscoveryResult discoveredGame, ProgramArgs programArgs, Dist dist, Path gameDir) { + this.classLoaderStack = classLoaderStack; + this.discoveredGame = discoveredGame; + this.programArgs = programArgs; this.dist = dist; - this.production = production; this.gameDir = gameDir; - versionInfo = new VersionInfo( - this.programArgs.remove("fml.neoForgeVersion"), - this.programArgs.remove("fml.mcVersion"), - this.programArgs.remove("fml.neoFormVersion")); - - LOGGER.info( - "Starting FancyModLoader version {} ({} in {})", - FMLVersion.getVersion(), - dist, - production ? "PROD" : "DEV"); - - LOGGER.info("Game directory: {}", gameDir); - makeCurrent(); } @@ -259,9 +231,7 @@ public void close() { } ownedResources.clear(); - if (Thread.currentThread().getContextClassLoader() == currentClassLoader) { - Thread.currentThread().setContextClassLoader(originalClassLoader); - } + classLoaderStack.close(); } private void makeCurrent() { @@ -272,7 +242,7 @@ private void makeCurrent() { } public ClassLoader getCurrentClassLoader() { - return currentClassLoader; + return classLoaderStack.getCurrentClassLoader(); } public ProgramArgs getProgramArgs() { @@ -307,45 +277,48 @@ public static FMLLoader create(StartupArgs startupArgs) { } public static FMLLoader create(@Nullable Instrumentation instrumentation, StartupArgs startupArgs) { - // If a client class is available, then it's client, otherwise DEDICATED_SERVER - // The auto-detection never detects JOINED since it's impossible to do so var initialLoader = Objects.requireNonNullElse(startupArgs.parentClassLoader(), ClassLoader.getSystemClassLoader()); + var dist = Objects.requireNonNullElseGet(startupArgs.dist(), () -> GameDiscovery.detectDist(initialLoader)); + LOGGER.info("Starting FancyModLoader {} ({}) in {}", FMLVersion.getVersion(), dist, startupArgs.gameDirectory()); PathPrettyPrinting.addRoot(startupArgs.gameDirectory()); - var loader = new FMLLoader( - initialLoader, - startupArgs.programArgs(), - Objects.requireNonNullElseGet(startupArgs.dist(), () -> detectDist(initialLoader)), - detectProduction(initialLoader), - startupArgs.gameDirectory()); + var programArgs = ProgramArgs.from(startupArgs.programArgs()); - try { - FMLPaths.loadAbsolutePaths(startupArgs.gameDirectory()); - FMLConfig.load(); + FMLPaths.loadAbsolutePaths(startupArgs.gameDirectory()); + FMLConfig.load(); - var launchContext = loader.new LaunchContextAdapter(); - for (var claimedFile : startupArgs.claimedFiles()) { - launchContext.addLocated(claimedFile.toPath()); - } - - loader.loadEarlyServices(startupArgs); + var locatedPaths = new LocatedPaths() { + final Set paths = new HashSet<>(startupArgs.claimedFiles().stream().map(File::toPath).toList()); - ImmediateWindowHandler.load(launchContext, startupArgs.headless(), loader.programArgs); - // Report known versions no - if (loader.versionInfo.neoForgeVersion() != null) { - ImmediateWindowHandler.setNeoForgeVersion(loader.versionInfo.neoForgeVersion()); - } - if (loader.versionInfo.mcVersion() != null) { - ImmediateWindowHandler.setMinecraftVersion(loader.versionInfo.mcVersion()); + @Override + public boolean isLocated(Path path) { + return paths.contains(path); } - DiscoveryResult discoveryResult; - if (startupArgs.headless()) { - discoveryResult = loader.runDiscovery(); - } else { - discoveryResult = runOffThread(loader::runDiscovery); + @Override + public boolean addLocated(Path path) { + return paths.add(path); } + }; + + var classLoaderStack = new ClassLoaderStack(initialLoader, locatedPaths); + + loadEarlyServices(classLoaderStack, startupArgs); + + ImmediateWindowHandler.load(locatedPaths, startupArgs.headless(), programArgs); + + var discoveredGame = runLongRunning(startupArgs, () -> GameDiscovery.discoverGame(programArgs, locatedPaths, dist)); + var neoForgeVersion = discoveredGame.neoforge().getModFileInfo().versionString(); + var minecraftVersion = discoveredGame.minecraft().getModFileInfo().versionString(); + + LOGGER.info("Discovered NeoForge {} and Minecraft {} ({})", neoForgeVersion, minecraftVersion, discoveredGame.production() ? "production" : "development"); + ImmediateWindowHandler.setNeoForgeVersion(neoForgeVersion); + ImmediateWindowHandler.setMinecraftVersion(minecraftVersion); + + var loader = new FMLLoader(classLoaderStack, discoveredGame, programArgs, dist, startupArgs.gameDirectory()); + try { + var discoveryResult = runLongRunning(startupArgs, loader::runDiscovery); for (var issue : discoveryResult.discoveryIssues()) { LOGGER.atLevel(issue.severity() == ModLoadingIssue.Severity.ERROR ? Level.ERROR : Level.WARN) .setCause(issue.cause()) @@ -371,7 +344,7 @@ public static FMLLoader create(@Nullable Instrumentation instrumentation, Startu loader.loadPlugins(loader.loadingModList.getPlugins()); // Now go and build the language providers and let mods discover theirs - loader.languageProviderLoader = new LanguageProviderLoader(launchContext); + loader.languageProviderLoader = new LanguageProviderLoader(); for (var modFile : discoveryResult.gameContent) { modFile.identifyLanguage(); } @@ -384,7 +357,7 @@ public static FMLLoader create(@Nullable Instrumentation instrumentation, Startu modFile.getModuleDescriptor())); } - var classProcessorSet = createClassProcessorSet(startupArgs, launchContext, discoveryResult, mixinFacade); + var classProcessorSet = createClassProcessorSet(startupArgs, discoveryResult, mixinFacade); if (!classProcessorSet.getGeneratedPackages().isEmpty()) { var descriptor = ModuleDescriptor.newAutomaticModule(ClassProcessor.GENERATED_PACKAGE_MODULE) .packages(classProcessorSet.getGeneratedPackages()) @@ -420,9 +393,8 @@ public static FMLLoader create(@Nullable Instrumentation instrumentation, Startu } private static ClassProcessorSet createClassProcessorSet(StartupArgs startupArgs, - LaunchContextAdapter launchContext, - DiscoveryResult discoveryResult, - MixinFacade mixinFacade) { + DiscoveryResult discoveryResult, + MixinFacade mixinFacade) { // Add our own launch plugins explicitly. var builtInProcessors = new ArrayList(); builtInProcessors.add(createAccessTransformerService(discoveryResult)); @@ -445,8 +417,8 @@ private static ClassProcessorSet createClassProcessorSet(StartupArgs startupArgs return ClassProcessorSet.builder() .markMarker(ClassProcessorIds.SIMPLE_PROCESSORS_GROUP) .markMarker(ClassProcessorIds.COMPUTING_FRAMES) - .addProcessors(ServiceLoaderUtil.loadServices(launchContext, ClassProcessor.class, builtInProcessors)) - .addProcessorProviders(ServiceLoaderUtil.loadServices(launchContext, ClassProcessorProvider.class)) + .addProcessors(ServiceLoaderUtil.loadServices(ClassProcessor.class, builtInProcessors)) + .addProcessorProviders(ServiceLoaderUtil.loadServices(ClassProcessorProvider.class)) .build(); } @@ -471,9 +443,9 @@ private static ClassProcessor createAccessTransformerService(DiscoveryResult dis } private TransformingClassLoader buildTransformingLoader(ClassProcessorSet classProcessorSet, - ClassProcessorAuditLog auditTrail, - List content) { - maskContentAlreadyOnClasspath(content); + ClassProcessorAuditLog auditTrail, + List content) { + classLoaderStack.maskContentAlreadyOnClasspath(content); long start = System.currentTimeMillis(); @@ -487,7 +459,7 @@ private TransformingClassLoader buildTransformingLoader(ClassProcessorSet classP var moduleNames = getModuleNameList(cf, content); LOGGER.info("Building game content classloader:\n{}", moduleNames); - var loader = new TransformingClassLoader(classProcessorSet, auditTrail, cf, parentLayers, currentClassLoader); + var loader = new TransformingClassLoader(classProcessorSet, auditTrail, cf, parentLayers, getCurrentClassLoader()); var layer = ModuleLayer.defineModules( cf, @@ -497,69 +469,15 @@ private TransformingClassLoader buildTransformingLoader(ClassProcessorSet classP var elapsed = System.currentTimeMillis() - start; LOGGER.info("Built game content classloader in {}ms", elapsed); - loader.setFallbackClassLoader(currentClassLoader); + loader.setFallbackClassLoader(getCurrentClassLoader()); gameLayer = layer; ownedResources.add(loader); - currentClassLoader = loader; + classLoaderStack.append(loader); Thread.currentThread().setContextClassLoader(loader); return loader; } - /** - * If any location being added is already on the classpath, we add a masking classloader to ensure - * that resources are not double-reported when using getResources/getResource. - *

- * The primary purpose of this is in mod and NeoForge development environments, where IDEs put the mod - * on the app classpath, but we also add it as content to the game layer. This method is responsible - * for setting up a classloader that prevents getResource/getResources from reporting Jar resources - * for both the jar on the App classpath and on the transforming classloader. - */ - private void maskContentAlreadyOnClasspath(List content) { - var classpathItems = ClasspathResourceUtils.getAllClasspathItems(currentClassLoader); - - // Collect all paths that make up the game content, which are already on the classpath - Set needsMasking = new HashSet<>(); - for (var secureJar : content) { - for (var basePath : getBasePaths(secureJar.contents(), true)) { - if (classpathItems.contains(basePath)) { - needsMasking.add(basePath); - } - } - } - - if (!needsMasking.isEmpty()) { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Masking classpath elements: {}", needsMasking.stream().map(PathPrettyPrinting::prettyPrint).toList()); - } - - var maskedLoader = new ResourceMaskingClassLoader(currentClassLoader, needsMasking); - if (Thread.currentThread().getContextClassLoader() == currentClassLoader) { - Thread.currentThread().setContextClassLoader(maskedLoader); - } - currentClassLoader = maskedLoader; - } - } - - private static List getBasePaths(JarContents contents, boolean ignoreFilter) { - var result = new ArrayList(); - switch (contents) { - case CompositeJarContents compositeModContainer -> { - if (!ignoreFilter && compositeModContainer.isFiltered()) { - throw new IllegalStateException("Cannot load filtered Jar content into a URL classloader"); - } - for (var delegate : compositeModContainer.getDelegates()) { - result.addAll(getBasePaths(delegate, ignoreFilter)); - } - } - case EmptyJarContents ignored -> {} - case FolderJarContents folderModContainer -> result.add(folderModContainer.getPrimaryPath()); - case JarFileContents jarModContainer -> result.add(jarModContainer.getPrimaryPath()); - default -> throw new IllegalStateException("Don't know how to handle " + contents); - } - return result; - } - private static String getModuleNameList(Configuration cf, List content) { var jarsById = content.stream().collect(Collectors.toMap(JarContentsModule::moduleName, Function.identity())); @@ -573,63 +491,16 @@ private static String getModuleNameList(Configuration cf, List(EarlyServiceDiscovery.findEarlyServiceJars(startupArgs, FMLPaths.MODSDIR.get())); if (!earlyServicesJars.isEmpty()) { - appendLoader("FML Early Services", earlyServicesJars.stream().map(IModFile::getContents).toList()); + classLoaderStack.appendLoader("FML Early Services", earlyServicesJars.stream().map(IModFile::getContents).toList()); } } private void loadPlugins(List plugins) { - appendLoader("FML Plugins", plugins.stream().map(mfi -> mfi.getFile().getContents()).toList()); - } - - /** - * Loads the given services into a URL classloader. - */ - private void appendLoader(String loaderName, List jars) { - if (jars.isEmpty()) { - LOGGER.info("No additional classpath items for {} were found.", loaderName); - return; - } - - LOGGER.info("Loading {}:", loaderName); - - List rootUrls = new ArrayList<>(jars.size()); - for (var jar : jars) { - if (jar instanceof CompositeJarContents compositeJarContents && compositeJarContents.isFiltered()) { - throw new IllegalArgumentException("Cannot use simple URLClassLoader for filtered content " + jar); - } - - // TODO: Order on the classpath matters, we need to double-check the content roots are in the right order here - for (var contentRoot : jar.getContentRoots()) { - LOGGER.info(" - {}", PathPrettyPrinting.prettyPrint(contentRoot)); - try { - rootUrls.add(contentRoot.toUri().toURL()); - } catch (MalformedURLException e) { - throw new RuntimeException(e); // This should not happen for file URLs - } - locatedPaths.add(contentRoot); // Prevents it from getting picked up again - } - } - - var loader = new URLClassLoader(loaderName, rootUrls.toArray(URL[]::new), currentClassLoader); - ownedResources.add(loader); - currentClassLoader = loader; - Thread.currentThread().setContextClassLoader(loader); + classLoaderStack.appendLoader("FML Plugins", plugins.stream().map(mfi -> mfi.getFile().getContents()).toList()); } private DiscoveryResult runDiscovery() { @@ -637,38 +508,16 @@ private DiscoveryResult runDiscovery() { var additionalLocators = new ArrayList(); - additionalLocators.add(new GameLocator()); additionalLocators.add(new InDevFolderLocator()); additionalLocators.add(new InDevJarLocator()); additionalLocators.add(new ModsFolderLocator()); - var modDiscoverer = new ModDiscoverer(new LaunchContextAdapter(), additionalLocators); + var modDiscoverer = new ModDiscoverer(new LaunchContextAdapter(), discoveredGame, additionalLocators); var discoveryResult = modDiscoverer.discoverMods(earlyServicesJars); - // Now we should have a mod for "minecraft" and "neoforge" allowing us to fill in the versions - var neoForgeVersion = versionInfo.neoForgeVersion(); - var minecraftVersion = versionInfo.mcVersion(); - for (var modFile : discoveryResult.modFiles()) { - var mods = modFile.getModFileInfo().getMods(); - if (mods.isEmpty()) { - continue; - } - var mainMod = mods.getFirst(); - switch (modFile.getId()) { - case "minecraft" -> minecraftVersion = mainMod.getVersion().toString(); - case "neoforge" -> neoForgeVersion = mainMod.getVersion().toString(); - } - } - versionInfo = new VersionInfo( - neoForgeVersion, - minecraftVersion, - getVersionInfo().neoFormVersion()); - versionSupportMatrix = new VersionSupportMatrix(versionInfo); + versionSupportMatrix = new VersionSupportMatrix(getMinecraftVersion()); progress.complete(); - ImmediateWindowHandler.setMinecraftVersion(versionInfo.mcVersion()); - ImmediateWindowHandler.setNeoForgeVersion(versionInfo.neoForgeVersion()); - loadingModList = ModSorter.sort(discoveryResult.modFiles(), discoveryResult.discoveryIssues()); Map enumExtensionsByMod = new HashMap<>(); @@ -697,6 +546,14 @@ private DiscoveryResult runDiscovery() { loadingModList.getModLoadingIssues()); } + private static T runLongRunning(StartupArgs startupArgs, Supplier supplier) { + if (startupArgs.headless()) { + return supplier.get(); + } else { + return runOffThread(supplier); + } + } + private static T runOffThread(Supplier supplier) { var cl = Thread.currentThread().getContextClassLoader(); var future = CompletableFuture.supplyAsync(() -> { @@ -721,7 +578,8 @@ private static T runOffThread(Supplier supplier) { } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("Interrupted while waiting for future", e); - } catch (TimeoutException ignored) {} + } catch (TimeoutException ignored) { + } } } @@ -761,7 +619,7 @@ public Path getGameDir() { } public boolean isProduction() { - return production; + return discoveredGame.production(); } public ModuleLayer getGameLayer() { @@ -771,12 +629,12 @@ public ModuleLayer getGameLayer() { return gameLayer; } - /** - * Please note that the returned version information can be incomplete until mod discovery has been completed. - * This is only relevant for early FML services. - */ - public VersionInfo getVersionInfo() { - return versionInfo; + public String getMinecraftVersion() { + return discoveredGame.minecraft().getJarVersion().toString(); + } + + public String getNeoForgeVersion() { + return discoveredGame.neoforge().getJarVersion().toString(); } VersionSupportMatrix getVersionSupportMatrix() { @@ -798,24 +656,23 @@ public Path gameDirectory() { } @Override - public Stream> loadServices(Class serviceClass) { - // We simply rely on thread context classloader to be correct - return ServiceLoader.load(serviceClass).stream(); + public boolean isLocated(Path path) { + return locatedPaths.contains(path); } @Override - public boolean isLocated(Path path) { - return FMLLoader.this.locatedPaths.contains(path); + public boolean addLocated(Path path) { + return locatedPaths.add(path); } @Override - public boolean addLocated(Path path) { - return FMLLoader.this.locatedPaths.add(path); + public String getMinecraftVersion() { + return FMLLoader.this.getMinecraftVersion(); } @Override - public VersionInfo getVersions() { - return versionInfo; + public String getNeoForgeVersion() { + return FMLLoader.this.getNeoForgeVersion(); } } } diff --git a/loader/src/main/java/net/neoforged/fml/loading/ImmediateWindowHandler.java b/loader/src/main/java/net/neoforged/fml/loading/ImmediateWindowHandler.java index ae8b430e0..7e190d0b3 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/ImmediateWindowHandler.java +++ b/loader/src/main/java/net/neoforged/fml/loading/ImmediateWindowHandler.java @@ -10,7 +10,7 @@ import java.util.Objects; import net.neoforged.fml.ModLoadingIssue; import net.neoforged.fml.util.ServiceLoaderUtil; -import net.neoforged.neoforgespi.ILaunchContext; +import net.neoforged.neoforgespi.LocatedPaths; import net.neoforged.neoforgespi.earlywindow.GraphicsBootstrapper; import net.neoforged.neoforgespi.earlywindow.ImmediateWindowProvider; import org.apache.logging.log4j.LogManager; @@ -25,8 +25,8 @@ public class ImmediateWindowHandler { @Nullable static ImmediateWindowProvider provider; - public static void load(ILaunchContext context, boolean headless, ProgramArgs arguments) { - ServiceLoaderUtil.loadEarlyServices(context, GraphicsBootstrapper.class, List.of()) + public static void load(LocatedPaths located, boolean headless, ProgramArgs arguments) { + ServiceLoaderUtil.loadEarlyServices(located, GraphicsBootstrapper.class, List.of()) .forEach(bootstrap -> { LOGGER.info("Running graphics bootstrap plugin {}", bootstrap.name()); bootstrap.bootstrap(arguments.getArguments()); // TODO: Should take ProgramArgs so it can *remove* args @@ -44,7 +44,7 @@ public static void load(ILaunchContext context, boolean headless, ProgramArgs ar } else { var providername = FMLConfig.getConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_PROVIDER); LOGGER.info("Loading ImmediateWindowProvider {}", providername); - var maybeProvider = ServiceLoaderUtil.loadEarlyServices(context, ImmediateWindowProvider.class, List.of()) + var maybeProvider = ServiceLoaderUtil.loadEarlyServices(located, ImmediateWindowProvider.class, List.of()) .stream() .filter(p -> Objects.equals(p.name(), providername)) .findFirst(); diff --git a/loader/src/main/java/net/neoforged/fml/loading/LanguageProviderLoader.java b/loader/src/main/java/net/neoforged/fml/loading/LanguageProviderLoader.java index a4d18dc55..0b8f93d9e 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/LanguageProviderLoader.java +++ b/loader/src/main/java/net/neoforged/fml/loading/LanguageProviderLoader.java @@ -17,14 +17,15 @@ import net.neoforged.fml.javafmlmod.FMLJavaModLanguageProvider; import net.neoforged.fml.loading.moddiscovery.ModFile; import net.neoforged.fml.util.ServiceLoaderUtil; -import net.neoforged.neoforgespi.ILaunchContext; import net.neoforged.neoforgespi.language.IModLanguageLoader; import org.apache.maven.artifact.versioning.ArtifactVersion; import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import org.apache.maven.artifact.versioning.VersionRange; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; +@ApiStatus.Internal public class LanguageProviderLoader { private static final Logger LOGGER = LogUtils.getLogger(); private final List languageProviders; @@ -40,8 +41,8 @@ public Stream applyForEach(Function function) { private record ModLanguageWrapper(IModLanguageLoader modLanguageProvider, ArtifactVersion version) {} - LanguageProviderLoader(ILaunchContext launchContext) { - languageProviders = ServiceLoaderUtil.loadServices(launchContext, IModLanguageLoader.class); + LanguageProviderLoader() { + languageProviders = ServiceLoaderUtil.loadServices(IModLanguageLoader.class); ImmediateWindowHandler.updateProgress("Loading language providers"); languageProviders.forEach(lp -> { String version = lp.version(); diff --git a/loader/src/main/java/net/neoforged/fml/loading/VersionInfo.java b/loader/src/main/java/net/neoforged/fml/loading/VersionInfo.java deleted file mode 100644 index fde6441b1..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/VersionInfo.java +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading; - -public record VersionInfo(String neoForgeVersion, String mcVersion, String neoFormVersion) { - public String mcAndNeoFormVersion() { - return mcVersion + "-" + neoFormVersion; - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/VersionSupportMatrix.java b/loader/src/main/java/net/neoforged/fml/loading/VersionSupportMatrix.java index a3577a11a..9015ab865 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/VersionSupportMatrix.java +++ b/loader/src/main/java/net/neoforged/fml/loading/VersionSupportMatrix.java @@ -17,8 +17,8 @@ class VersionSupportMatrix { private static final HashMap> overrideVersions = new HashMap<>(); - public VersionSupportMatrix(VersionInfo versionInfo) { - var mcVersion = new DefaultArtifactVersion(versionInfo.mcVersion()); + public VersionSupportMatrix(String minecraftVersion) { + var mcVersion = new DefaultArtifactVersion(minecraftVersion); // If the MC version is 1.21.8 and any default version constraint fails, // we'll also pass the version check if the versions below match if (MavenVersionAdapter.createFromVersionSpec("[1.21.8]").containsVersion(mcVersion)) { diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/GameLocator.java b/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java similarity index 66% rename from loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/GameLocator.java rename to loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java index dc9de140e..599199476 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/GameLocator.java +++ b/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java @@ -3,10 +3,11 @@ * SPDX-License-Identifier: LGPL-2.1-only */ -package net.neoforged.fml.loading.moddiscovery.locators; +package net.neoforged.fml.loading.game; import com.google.gson.Gson; import com.google.gson.JsonObject; +import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; @@ -16,77 +17,119 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Properties; import java.util.stream.Collectors; import net.neoforged.api.distmarker.Dist; import net.neoforged.fml.ModLoadingException; import net.neoforged.fml.ModLoadingIssue; import net.neoforged.fml.jarcontents.JarContents; -import net.neoforged.fml.loading.LibraryFinder; import net.neoforged.fml.loading.MavenCoordinate; +import net.neoforged.fml.loading.ProgramArgs; +import net.neoforged.fml.loading.moddiscovery.ModFile; import net.neoforged.fml.loading.moddiscovery.ModJarMetadata; +import net.neoforged.fml.loading.moddiscovery.locators.NeoForgeDevDistCleaner; import net.neoforged.fml.loading.moddiscovery.readers.JarModsDotTomlModFileReader; import net.neoforged.fml.util.ClasspathResourceUtils; import net.neoforged.fml.util.PathPrettyPrinting; -import net.neoforged.neoforgespi.ILaunchContext; -import net.neoforged.neoforgespi.locating.IDiscoveryPipeline; +import net.neoforged.neoforgespi.LocatedPaths; +import net.neoforged.neoforgespi.language.IModInfo; import net.neoforged.neoforgespi.locating.IModFile; -import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; -import net.neoforged.neoforgespi.locating.IncompatibleFileReporting; import net.neoforged.neoforgespi.locating.ModFileDiscoveryAttributes; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class GameLocator implements IModFileCandidateLocator { - private static final Logger LOG = LoggerFactory.getLogger(GameLocator.class); +/** + * This class is responsible for finding the minecraft and neoforge "mods". + */ +public final class GameDiscovery { + private static final Logger LOG = LoggerFactory.getLogger(GameDiscovery.class); public static final String LIBRARIES_DIRECTORY_PROPERTY = "libraryDirectory"; public static final String[] NEOFORGE_SPECIFIC_PATH_PREFIXES = { "net/neoforged/neoforge/", "META-INF/services/", JarModsDotTomlModFileReader.MODS_TOML }; - @Override - public void findCandidates(ILaunchContext context, IDiscoveryPipeline pipeline) { + private GameDiscovery() {} + + public static GameDiscoveryResult discoverGame(ProgramArgs programArgs, LocatedPaths locatedPaths, Dist requiredDist) { var ourCl = Thread.currentThread().getContextClassLoader(); + var neoForgeVersion = getNeoForgeVersion(programArgs, ourCl); + + programArgs.remove("fml.neoForgeVersion"); + programArgs.remove("fml.neoFormVersion"); // Remove legacy arguments + programArgs.remove("fml.mcVersion"); // Remove legacy arguments + // 0) Vanilla Launcher puts the obfuscated jar on the classpath. We mark it as claimed to prevent it from // being hoisted into a module, occupying the entrypoint packages. - preventLoadingOfObfuscatedClientJar(context, ourCl); + preventLoadingOfObfuscatedClientJar(locatedPaths, ourCl); // We look for a class present in both Minecraft distributions on the classpath, which would be obfuscated in production (DetectedVersion) // If that is present, we assume we're launching in dev (NeoDev or ModDev). - try (var systemFiles = RequiredSystemFiles.find(context, ourCl)) { + try (var systemFiles = RequiredSystemFiles.find(locatedPaths::isLocated, ourCl)) { if (!systemFiles.isEmpty()) { // If we've only been able to find some of the required files, we need to error - systemFiles.checkForMissingMinecraftFiles(context.getRequiredDistribution() == Dist.CLIENT); + systemFiles.checkForMissingMinecraftFiles(requiredDist == Dist.CLIENT); - handleMergedMinecraftAndNeoForgeJar(context, pipeline, systemFiles); - return; + return handleMergedMinecraftAndNeoForgeJar(requiredDist, locatedPaths, systemFiles); } else { LOG.info("Failed to find common Minecraft classes and resources on the classpath. Assuming we're launching production."); } } // In production, it's in the libraries directory, and we're passed the version to look for on the commandline - locateProductionMinecraft(context, pipeline); + return locateProductionMinecraft(neoForgeVersion, requiredDist); + } + + private static String getNeoForgeVersion(ProgramArgs programArgs, ClassLoader ourCl) { + var neoForgeVersion = getNeoForgeVersionFromClasspath(ourCl); + if (neoForgeVersion == null) { + neoForgeVersion = programArgs.get("fml.neoForgeVersion"); + if (neoForgeVersion == null) { + LOG.error("NeoForge version must be known to launch FML, in normal environments this is set as a command-line option (--fml.neoForgeVersion)"); + throw new ModLoadingException(ModLoadingIssue.error("fml.modloadingissue.corrupted_installation")); + } + LOG.debug("Using NeoForge version found on commandline: {}", neoForgeVersion); + } else { + LOG.debug("Using NeoForge version found on classpath: {}", neoForgeVersion); + } + return neoForgeVersion; + } + + @Nullable + private static String getNeoForgeVersionFromClasspath(ClassLoader classLoader) { + try (var in = classLoader.getResourceAsStream("net/neoforged/neoforge/common/version.properties")) { + if (in == null) { + return null; + } + + Properties p = new Properties(); + p.load(new BufferedInputStream(in)); + return p.getProperty("neoforge_version"); + } catch (IOException ignored) { + return null; + } + } + + @Nullable + private static ModFile readModFile(JarContents jarContents) { + return (ModFile) new JarModsDotTomlModFileReader().read(jarContents, ModFileDiscoveryAttributes.DEFAULT); } - private static void handleMergedMinecraftAndNeoForgeJar(ILaunchContext context, IDiscoveryPipeline pipeline, RequiredSystemFiles systemFiles) { + private static GameDiscoveryResult handleMergedMinecraftAndNeoForgeJar(Dist requiredDist, LocatedPaths locatedPaths, RequiredSystemFiles systemFiles) { LOG.info("Detected a joined NeoForge and Minecraft configuration. Applying filtering..."); - var mcJarContents = getCombinedMinecraftJar(context, systemFiles); - IModFile minecraftModFile; + var mcJarContents = getCombinedMinecraftJar(requiredDist, systemFiles); + ModFile minecraftModFile; if (mcJarContents.containsFile("META-INF/neoforged.mods.toml")) { // In this branch, the jar already has a neoforge.mods.toml - minecraftModFile = pipeline.addJarContent(mcJarContents, ModFileDiscoveryAttributes.DEFAULT, IncompatibleFileReporting.IGNORE).orElse(null); + minecraftModFile = readModFile(mcJarContents); if (minecraftModFile == null) { throw new ModLoadingException(ModLoadingIssue.error("fml.modloadingissue.corrupted_minecraft_jar")); } } else { var minecraftVersion = detectMinecraftVersion(mcJarContents); var mcJarMetadata = new ModJarMetadata(); - minecraftModFile = IModFile.create(mcJarContents, mcJarMetadata, new MinecraftModInfo(minecraftVersion)::buildMinecraftModInfo); + minecraftModFile = (ModFile) IModFile.create(mcJarContents, mcJarMetadata, new MinecraftModInfo(minecraftVersion)::buildMinecraftModInfo); mcJarMetadata.setModFile(minecraftModFile); - if (!pipeline.addModFile(minecraftModFile)) { - throw new ModLoadingException(ModLoadingIssue.error("fml.modloadingissue.corrupted_minecraft_jar")); - } } if (!minecraftModFile.getId().equals("minecraft")) { LOG.error("The mod id for the Minecraft jar is not 'minecraft': {}", minecraftModFile.getId()); @@ -117,24 +160,25 @@ private static void handleMergedMinecraftAndNeoForgeJar(ILaunchContext context, throw new ModLoadingException(ModLoadingIssue.error("fml.modloadingissue.corrupted_neoforge_jar").withCause(e)); } - var modFile = JarModsDotTomlModFileReader.createModFile(nfJarContents, ModFileDiscoveryAttributes.DEFAULT); + var modFile = (ModFile) JarModsDotTomlModFileReader.createModFile(nfJarContents, ModFileDiscoveryAttributes.DEFAULT); if (modFile == null) { LOG.error("Failed to construct NeoForge mod file from {}", nfJarContents); throw new ModLoadingException(ModLoadingIssue.error("fml.modloadingissue.corrupted_neoforge_jar")); } - pipeline.addModFile(modFile); - systemFiles.getAll().stream().map(JarContents::getPrimaryPath).forEach(context::addLocated); + systemFiles.getAll().stream().map(JarContents::getPrimaryPath).forEach(locatedPaths::addLocated); + + return new GameDiscoveryResult(modFile, minecraftModFile, false); } - private static JarContents getCombinedMinecraftJar(ILaunchContext context, RequiredSystemFiles systemFiles) { + private static JarContents getCombinedMinecraftJar(Dist requiredDist, RequiredSystemFiles systemFiles) { if (systemFiles.getCommonResources() == systemFiles.getNeoForgeResources()) { throw new IllegalStateException("The Minecraft and NeoForge resources cannot come from the same jar: " + systemFiles.getCommonResources() + " and " + systemFiles.getNeoForgeResources()); } var mcJarRoots = new ArrayList(); - mcJarRoots.addAll(getMinecraftResourcesRoots(context, systemFiles)); + mcJarRoots.addAll(getMinecraftResourcesRoots(requiredDist, systemFiles)); JarContents.PathFilter mcClassesFilter = relativePath -> { if (relativePath.endsWith(".class")) { @@ -186,16 +230,16 @@ private static String detectMinecraftVersion(JarContents mcJarContents) { return minecraftVersion; } - private static List getMinecraftResourcesRoots(ILaunchContext context, RequiredSystemFiles systemFiles) { + private static List getMinecraftResourcesRoots(Dist requiredDist, RequiredSystemFiles systemFiles) { JarContents commonResources = systemFiles.getCommonResources(); JarContents clientResources = systemFiles.getClientResources(); // If the resource roots are separate from classes, no filter needs to be applied. List result = new ArrayList<>(); - result.add(buildFilteredMinecraftResourcesFilteredPath(commonResources, context.getRequiredDistribution(), systemFiles)); + result.add(buildFilteredMinecraftResourcesFilteredPath(commonResources, requiredDist, systemFiles)); if (clientResources != null && clientResources != commonResources) { - result.add(buildFilteredMinecraftResourcesFilteredPath(clientResources, context.getRequiredDistribution(), systemFiles)); + result.add(buildFilteredMinecraftResourcesFilteredPath(clientResources, requiredDist, systemFiles)); } return result; @@ -246,94 +290,76 @@ private static void addContentRoot(List roots, JarCont /** * In production, the client and neoforge jars are assembled from partial jars in the libraries folder. */ - private static void locateProductionMinecraft(ILaunchContext context, IDiscoveryPipeline pipeline) { + private static GameDiscoveryResult locateProductionMinecraft(String neoForgeVersion, Dist requiredDist) { // 2) It's neither, but a libraries directory and desired versions are given on the commandline var librariesDirectory = System.getProperty(LIBRARIES_DIRECTORY_PROPERTY); if (librariesDirectory == null) { LOG.error("When launching in production, the system property {} must point to the libraries directory.", LIBRARIES_DIRECTORY_PROPERTY); - pipeline.addIssue(ModLoadingIssue.error("fml.modloadingissue.corrupted_installation")); - return; + throw new ModLoadingException(ModLoadingIssue.error("fml.modloadingissue.corrupted_installation")); } var librariesRoot = Path.of(librariesDirectory); if (!Files.isDirectory(librariesRoot)) { LOG.error("Libraries directory is not readable: {}", librariesRoot); - pipeline.addIssue(ModLoadingIssue.error("fml.modloadingissue.corrupted_installation")); - return; + throw new ModLoadingException(ModLoadingIssue.error("fml.modloadingissue.corrupted_installation")); } PathPrettyPrinting.addSubstitution(librariesRoot, "~libraries/", ""); - // The versions for Minecraft and NeoForge, etc. must be given on the CLI - var versions = context.getVersions(); - var minecraftVersion = versions.mcVersion(); - if (minecraftVersion == null) { - LOG.error("When launching in production, --fml.mcVersion must be present as a command-line argument"); - pipeline.addIssue(ModLoadingIssue.error("fml.modloadingissue.corrupted_installation")); - return; - } - var neoForgeVersion = versions.neoForgeVersion(); - if (neoForgeVersion == null) { - LOG.error("When launching in production, --fml.neoForgeVersion must be present as a command-line argument"); - pipeline.addIssue(ModLoadingIssue.error("fml.modloadingissue.corrupted_installation")); - return; - } - var neoFormVersion = versions.neoFormVersion(); - if (neoFormVersion == null) { - LOG.error("When launching in production, --fml.neoFormVersion must be present as a command-line argument"); - pipeline.addIssue(ModLoadingIssue.error("fml.modloadingissue.corrupted_installation")); - return; + // The versions for Minecraft and NeoForm can be read from the NeoForge jar + var neoforgeCoordinate = new MavenCoordinate("net.neoforged", "neoforge", "", "universal", neoForgeVersion); + var neoforgeJar = librariesRoot.resolve(neoforgeCoordinate.toRelativeRepositoryPath()); + if (!Files.exists(neoforgeJar)) { + throw new ModLoadingException(ModLoadingIssue.error("fml.modloadingissue.missing_neoforge_jar").withAffectedPath(neoforgeJar)); } // Detect if the newer Minecraft installation method is available. If not, we assume the old method. - var patchedMinecraftPath = librariesRoot.resolve((switch (context.getRequiredDistribution()) { - case CLIENT -> new MavenCoordinate("net.neoforged", "minecraft-client-patched", "", "", versions.neoForgeVersion()); - case DEDICATED_SERVER -> new MavenCoordinate("net.neoforged", "minecraft-server-patched", "", "", versions.neoForgeVersion()); + var patchedMinecraftPath = librariesRoot.resolve((switch (requiredDist) { + case CLIENT -> new MavenCoordinate("net.neoforged", "minecraft-client-patched", "", "", neoForgeVersion); + case DEDICATED_SERVER -> new MavenCoordinate("net.neoforged", "minecraft-server-patched", "", "", neoForgeVersion); }).toRelativeRepositoryPath()); - if (Files.isRegularFile(patchedMinecraftPath)) { - if (pipeline.addPath(patchedMinecraftPath, ModFileDiscoveryAttributes.DEFAULT, IncompatibleFileReporting.IGNORE).isEmpty()) { - throw new ModLoadingException(ModLoadingIssue.error("fml.modloadingissue.corrupted_minecraft_jar").withAffectedPath(patchedMinecraftPath)); + var nfModFile = openModFile(neoforgeJar, "neoforge", "fml.modloadingissue.corrupted_neoforge_jar"); + ModFile minecraftModFile; + try { + minecraftModFile = openModFile(patchedMinecraftPath, "minecraft", "fml.modloadingissue.corrupted_minecraft_jar"); + } catch (Exception e) { + nfModFile.close(); + throw e; + } + + return new GameDiscoveryResult(nfModFile, minecraftModFile, true); + } + + private static ModFile openModFile(Path path, String expectedModId, String errorTranslation) { + JarContents jarContents; + try { + jarContents = JarContents.ofPath(path); + } catch (IOException e) { + throw new ModLoadingException(ModLoadingIssue.error(errorTranslation).withAffectedPath(path).withCause(e)); + } + try { + var modFile = JarModsDotTomlModFileReader.createModFile(jarContents, ModFileDiscoveryAttributes.DEFAULT); + if (modFile == null || modFile.getType() != IModFile.Type.MOD) { + throw new ModLoadingException(ModLoadingIssue.error(errorTranslation).withAffectedPath(path)); } - } else { - var content = new ArrayList(); - switch (context.getRequiredDistribution()) { - case CLIENT -> { - addRequiredLibrary(new MavenCoordinate("net.minecraft", "client", "", "srg", versions.mcAndNeoFormVersion()), content); - addRequiredLibrary(new MavenCoordinate("net.minecraft", "client", "", "extra", versions.mcAndNeoFormVersion()), content); - addRequiredLibrary(new MavenCoordinate("net.neoforged", "neoforge", "", "client", versions.neoForgeVersion()), content); - } - case DEDICATED_SERVER -> { - addRequiredLibrary(new MavenCoordinate("net.minecraft", "server", "", "srg", versions.mcAndNeoFormVersion()), content); - addRequiredLibrary(new MavenCoordinate("net.minecraft", "server", "", "extra", versions.mcAndNeoFormVersion()), content); - addRequiredLibrary(new MavenCoordinate("net.neoforged", "neoforge", "", "server", versions.neoForgeVersion()), content); - } + var containedModIds = modFile.getModInfos().stream().map(IModInfo::getModId).toList(); + if (!containedModIds.equals(List.of(expectedModId))) { + LOG.error("The mod file {} does not contain only the expected mod '{}': {}", path, expectedModId, containedModIds); + throw new ModLoadingException(ModLoadingIssue.error(errorTranslation).withAffectedPath(path)); } - + return (ModFile) modFile; + } catch (Exception e) { try { - var mcJarContents = JarContents.ofPaths(content); - - var mcJarMetadata = new ModJarMetadata(); - var mcjar = IModFile.create(mcJarContents, mcJarMetadata, new MinecraftModInfo(minecraftVersion)::buildMinecraftModInfo); - mcJarMetadata.setModFile(mcjar); - - pipeline.addModFile(mcjar); - } catch (Exception e) { - pipeline.addIssue(ModLoadingIssue.error("fml.modloadingissue.corrupted_installation").withCause(e)); + jarContents.close(); + } catch (IOException ex) { + e.addSuppressed(ex); } - } - - var neoforgeCoordinate = new MavenCoordinate("net.neoforged", "neoforge", "", "universal", versions.neoForgeVersion()); - var neoforgeJar = librariesRoot.resolve(neoforgeCoordinate.toRelativeRepositoryPath()); - if (!Files.exists(neoforgeJar)) { - throw new ModLoadingException(ModLoadingIssue.error("fml.modloadingissue.missing_neoforge_jar").withAffectedPath(neoforgeJar)); - } - if (pipeline.addPath(neoforgeJar, ModFileDiscoveryAttributes.DEFAULT, IncompatibleFileReporting.IGNORE).isEmpty()) { - throw new ModLoadingException(ModLoadingIssue.error("fml.modloadingissue.corrupted_neoforge_jar").withAffectedPath(neoforgeJar)); + throw e; } } - private void preventLoadingOfObfuscatedClientJar(ILaunchContext context, ClassLoader ourCl) { + private static void preventLoadingOfObfuscatedClientJar(LocatedPaths locatedPaths, ClassLoader ourCl) { try { var jarsWithEntrypoint = new HashSet(); @@ -350,29 +376,14 @@ private void preventLoadingOfObfuscatedClientJar(ILaunchContext context, ClassLo for (Path path : jarsWithEntrypoint) { LOG.info("Marking obfuscated client jar as claimed to prevent loading: {}", path); - context.addLocated(path); + locatedPaths.addLocated(path); } } catch (IOException ignored) {} } - private static void addRequiredLibrary(MavenCoordinate coordinate, List content) { - var path = LibraryFinder.findPathForMaven(coordinate); - if (!Files.exists(path)) { - LOG.error("Failed to find required file {}", path); - throw new ModLoadingException(ModLoadingIssue.error("fml.modloadingissue.corrupted_installation").withAffectedPath(path)); - } else { - content.add(path); - } - } - - @Override - public int getPriority() { - return HIGHEST_SYSTEM_PRIORITY; - } - - @Override - public String toString() { - return "game locator"; + public static Dist detectDist(ClassLoader classLoader) { + var clientAvailable = classLoader.getResource("net/minecraft/client/main/Main.class") != null; + return clientAvailable ? Dist.CLIENT : Dist.DEDICATED_SERVER; } } diff --git a/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscoveryResult.java b/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscoveryResult.java new file mode 100644 index 000000000..2d95066dd --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscoveryResult.java @@ -0,0 +1,13 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading.game; + +import net.neoforged.fml.loading.moddiscovery.ModFile; + +/** + * The result of discovering NeoForge and Minecraft. + */ +public record GameDiscoveryResult(ModFile neoforge, ModFile minecraft, boolean production) {} diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/MinecraftModInfo.java b/loader/src/main/java/net/neoforged/fml/loading/game/MinecraftModInfo.java similarity index 96% rename from loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/MinecraftModInfo.java rename to loader/src/main/java/net/neoforged/fml/loading/game/MinecraftModInfo.java index 5f9f8b9e0..8e6ea2f47 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/MinecraftModInfo.java +++ b/loader/src/main/java/net/neoforged/fml/loading/game/MinecraftModInfo.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: LGPL-2.1-only */ -package net.neoforged.fml.loading.moddiscovery.locators; +package net.neoforged.fml.loading.game; import com.electronwill.nightconfig.core.Config; import java.util.List; diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/RequiredSystemFiles.java b/loader/src/main/java/net/neoforged/fml/loading/game/RequiredSystemFiles.java similarity index 91% rename from loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/RequiredSystemFiles.java rename to loader/src/main/java/net/neoforged/fml/loading/game/RequiredSystemFiles.java index e34184c72..bd288d636 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/RequiredSystemFiles.java +++ b/loader/src/main/java/net/neoforged/fml/loading/game/RequiredSystemFiles.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: LGPL-2.1-only */ -package net.neoforged.fml.loading.moddiscovery.locators; +package net.neoforged.fml.loading.game; import java.io.BufferedInputStream; import java.io.IOException; @@ -13,6 +13,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.function.Predicate; import java.util.jar.Manifest; import java.util.stream.Stream; import net.neoforged.fml.ModLoadingException; @@ -20,7 +21,6 @@ import net.neoforged.fml.jarcontents.JarContents; import net.neoforged.fml.loading.moddiscovery.readers.JarModsDotTomlModFileReader; import net.neoforged.fml.util.ClasspathResourceUtils; -import net.neoforged.neoforgespi.ILaunchContext; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -89,17 +89,17 @@ public void checkForMissingMinecraftFiles(boolean clientRequired) { } } - public static RequiredSystemFiles find(ILaunchContext context, ClassLoader loader) { + public static RequiredSystemFiles find(Predicate ignorePath, ClassLoader loader) { var locatedRoots = new ArrayList(); var result = new RequiredSystemFiles(); try { - result.commonClasses = findAndOpen(context, loader, locatedRoots, COMMON_CLASS); - result.commonResources = findAndOpen(context, loader, locatedRoots, COMMON_RESOURCE_ROOT); - result.clientClasses = findAndOpen(context, loader, locatedRoots, CLIENT_CLASS); - result.clientResources = findAndOpen(context, loader, locatedRoots, CLIENT_RESOURCE_ROOT); - result.neoForgeCommonClasses = findAndOpen(context, loader, locatedRoots, NEOFORGE_COMMON_CLASS); - result.neoForgeClientClasses = findAndOpen(context, loader, locatedRoots, NEOFORGE_CLIENT_CLASS); + result.commonClasses = findAndOpen(ignorePath, loader, locatedRoots, COMMON_CLASS); + result.commonResources = findAndOpen(ignorePath, loader, locatedRoots, COMMON_RESOURCE_ROOT); + result.clientClasses = findAndOpen(ignorePath, loader, locatedRoots, CLIENT_CLASS); + result.clientResources = findAndOpen(ignorePath, loader, locatedRoots, CLIENT_RESOURCE_ROOT); + result.neoForgeCommonClasses = findAndOpen(ignorePath, loader, locatedRoots, NEOFORGE_COMMON_CLASS); + result.neoForgeClientClasses = findAndOpen(ignorePath, loader, locatedRoots, NEOFORGE_CLIENT_CLASS); result.neoForgeResources = findNeoForgeResources(locatedRoots, loader); } catch (Exception e) { closeAll(locatedRoots); @@ -136,7 +136,7 @@ private static JarContents findNeoForgeResources(List locatedRoots, } @Nullable - private static JarContents findAndOpen(ILaunchContext context, + private static JarContents findAndOpen(Predicate ignorePath, ClassLoader loader, List alreadyOpened, String relativePath) { @@ -152,7 +152,7 @@ private static JarContents findAndOpen(ILaunchContext context, for (var path : roots) { // The obfuscated client jar is on the classpath in production, and we mark it as located earlier. // This check prevents us trying to load our files from it. - if (!context.isLocated(path)) { + if (!ignorePath.test(path)) { var jar = openOrThrow(path); alreadyOpened.add(jar); return jar; diff --git a/loader/src/main/java/net/neoforged/fml/loading/game/package-info.java b/loader/src/main/java/net/neoforged/fml/loading/game/package-info.java new file mode 100644 index 000000000..e57073dab --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/loading/game/package-info.java @@ -0,0 +1,9 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +@ApiStatus.Internal +package net.neoforged.fml.loading.game; + +import org.jetbrains.annotations.ApiStatus; diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModDiscoverer.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModDiscoverer.java index d82d09db8..ff1e54b05 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModDiscoverer.java +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModDiscoverer.java @@ -26,6 +26,7 @@ import net.neoforged.fml.loading.ImmediateWindowHandler; import net.neoforged.fml.loading.LogMarkers; import net.neoforged.fml.loading.UniqueModListBuilder; +import net.neoforged.fml.loading.game.GameDiscoveryResult; import net.neoforged.fml.util.ServiceLoaderUtil; import net.neoforged.neoforgespi.ILaunchContext; import net.neoforged.neoforgespi.locating.IDependencyLocator; @@ -35,23 +36,24 @@ import net.neoforged.neoforgespi.locating.IModFileReader; import net.neoforged.neoforgespi.locating.IncompatibleFileReporting; import net.neoforged.neoforgespi.locating.ModFileDiscoveryAttributes; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; +@ApiStatus.Internal public class ModDiscoverer { private static final Logger LOGGER = LogUtils.getLogger(); private final List modFileLocators; private final List dependencyLocators; private final List modFileReaders; private final ILaunchContext launchContext; - - public ModDiscoverer(ILaunchContext launchContext) { - this(launchContext, List.of()); - } + private final GameDiscoveryResult gameDiscoveryResult; public ModDiscoverer(ILaunchContext launchContext, + GameDiscoveryResult gameDiscoveryResult, Collection additionalModFileLocators) { this.launchContext = launchContext; + this.gameDiscoveryResult = gameDiscoveryResult; modFileLocators = ServiceLoaderUtil.loadEarlyServices(launchContext, IModFileCandidateLocator.class, additionalModFileLocators); modFileReaders = ServiceLoaderUtil.loadEarlyServices(launchContext, IModFileReader.class, List.of()); @@ -65,6 +67,8 @@ public record Result( public Result discoverMods(List additionalDependencySources) { LOGGER.debug(LogMarkers.SCAN, "Scanning for mods and other resources to load. We know {} ways to find mods", modFileLocators.size()); List loadedFiles = new ArrayList<>(); + loadedFiles.add(gameDiscoveryResult.minecraft()); + loadedFiles.add(gameDiscoveryResult.neoforge()); List discoveryIssues = new ArrayList<>(); boolean successfullyLoadedMods = true; ImmediateWindowHandler.updateProgress("Discovering mod files"); diff --git a/loader/src/main/java/net/neoforged/fml/util/ServiceLoaderUtil.java b/loader/src/main/java/net/neoforged/fml/util/ServiceLoaderUtil.java index 14fae2005..397a843bc 100644 --- a/loader/src/main/java/net/neoforged/fml/util/ServiceLoaderUtil.java +++ b/loader/src/main/java/net/neoforged/fml/util/ServiceLoaderUtil.java @@ -14,10 +14,11 @@ import java.util.Locale; import java.util.Objects; import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; import java.util.function.Predicate; import java.util.stream.Stream; import net.neoforged.fml.loading.LogMarkers; -import net.neoforged.neoforgespi.ILaunchContext; +import net.neoforged.neoforgespi.LocatedPaths; import net.neoforged.neoforgespi.locating.IOrderedProvider; import org.jetbrains.annotations.ApiStatus; import org.slf4j.Logger; @@ -29,30 +30,30 @@ public final class ServiceLoaderUtil { private ServiceLoaderUtil() {} - public static List loadServices(ILaunchContext context, Class serviceClass) { - return loadServices(context, serviceClass, List.of()); + public static List loadServices(Class serviceClass) { + return loadServices(serviceClass, List.of()); } - public static List loadServices(ILaunchContext context, Class serviceClass, Predicate> filter) { - return loadServices(context, serviceClass, List.of(), filter); + public static List loadServices(Class serviceClass, Predicate> filter) { + return loadServices(serviceClass, List.of(), filter); } - public static List loadServices(ILaunchContext context, Class serviceClass, Collection additionalServices) { - return loadServices(context, serviceClass, additionalServices, ignored -> true); + public static List loadServices(Class serviceClass, Collection additionalServices) { + return loadServices(serviceClass, additionalServices, ignored -> true); } /** * Same as {@link #loadServices}, but it also marks any jar file that provided such services as located to prevent it * from being located again as a mod-file or library later. */ - public static List loadEarlyServices(ILaunchContext context, Class serviceClass, Collection additionalServices) { - var services = loadServices(context, serviceClass, additionalServices, ignored -> true); + public static List loadEarlyServices(LocatedPaths located, Class serviceClass, Collection additionalServices) { + var services = loadServices(serviceClass, additionalServices, ignored -> true); for (var service : services) { var codeSource = service.getClass().getProtectionDomain().getCodeSource(); if (codeSource != null && codeSource.getLocation() != null) { try { - context.addLocated(Path.of(codeSource.getLocation().toURI())); + located.addLocated(Path.of(codeSource.getLocation().toURI())); } catch (IllegalArgumentException | FileSystemNotFoundException | URISyntaxException ignored) {} } } @@ -63,11 +64,10 @@ public static List loadEarlyServices(ILaunchContext context, Class ser /** * @param serviceClass If the service class implements {@link IOrderedProvider}, the services will automatically be sorted. */ - public static List loadServices(ILaunchContext context, - Class serviceClass, + public static List loadServices(Class serviceClass, Collection additionalServices, Predicate> filter) { - var serviceLoaderServices = context.loadServices(serviceClass) + var serviceLoaderServices = ServiceLoader.load(serviceClass).stream() .filter(p -> { if (!filter.test(p.type())) { LOGGER.debug("Filtering out service provider {} for service class {}", p.type(), serviceClass); diff --git a/loader/src/main/java/net/neoforged/neoforgespi/ILaunchContext.java b/loader/src/main/java/net/neoforged/neoforgespi/ILaunchContext.java index 32910a862..850e0c954 100644 --- a/loader/src/main/java/net/neoforged/neoforgespi/ILaunchContext.java +++ b/loader/src/main/java/net/neoforged/neoforgespi/ILaunchContext.java @@ -6,19 +6,14 @@ package net.neoforged.neoforgespi; import java.nio.file.Path; -import java.util.ServiceLoader; -import java.util.stream.Stream; import net.neoforged.api.distmarker.Dist; -import net.neoforged.fml.loading.VersionInfo; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.jetbrains.annotations.ApiStatus; /** * Provides context for various FML plugins about the current launch operation. */ -public interface ILaunchContext { - Logger LOGGER = LoggerFactory.getLogger(ILaunchContext.class); - +@ApiStatus.NonExtendable +public interface ILaunchContext extends LocatedPaths { Dist getRequiredDistribution(); /** @@ -26,17 +21,7 @@ public interface ILaunchContext { */ Path gameDirectory(); - Stream> loadServices(Class serviceClass); - - /** - * Checks if a given path was already found by a previous locator, or may be already loaded. - */ - boolean isLocated(Path path); - - /** - * Marks a path as being located and returns true if it was not previously located. - */ - boolean addLocated(Path path); + String getMinecraftVersion(); - VersionInfo getVersions(); + String getNeoForgeVersion(); } diff --git a/loader/src/main/java/net/neoforged/neoforgespi/LocatedPaths.java b/loader/src/main/java/net/neoforged/neoforgespi/LocatedPaths.java new file mode 100644 index 000000000..b6d9582ef --- /dev/null +++ b/loader/src/main/java/net/neoforged/neoforgespi/LocatedPaths.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforgespi; + +import java.nio.file.Path; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.NonExtendable +public interface LocatedPaths { + /** + * Checks if a given path was already found by a previous locator, or may be already loaded. + */ + boolean isLocated(Path path); + + /** + * Marks a path as being located and returns true if it was not previously located. + */ + boolean addLocated(Path path); +} From 383f458f6a4b7b6e1424b2aa3f3a9720f78fc625 Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Sun, 26 Oct 2025 15:26:41 +0100 Subject: [PATCH 02/29] Fix tests --- .../net/neoforged/fml/loading/FMLLoader.java | 89 ++++++++++--------- .../jarcontents/FolderJarContentsTest.java | 3 +- .../fml/jarcontents/JarFileContentsTest.java | 3 +- .../neoforged/fml/loading/FMLLoaderTest.java | 51 +++-------- .../neoforged/fml/loading/LauncherTest.java | 12 --- .../fml/testlib/SimulatedInstallation.java | 18 ---- 6 files changed, 63 insertions(+), 113 deletions(-) diff --git a/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java b/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java index 88cda64d4..e7c25b13b 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java +++ b/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java @@ -6,6 +6,33 @@ package net.neoforged.fml.loading; import com.mojang.logging.LogUtils; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.instrument.Instrumentation; +import java.lang.module.Configuration; +import java.lang.module.ModuleDescriptor; +import java.lang.module.ModuleFinder; +import java.net.URI; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; import net.neoforged.accesstransformer.api.AccessTransformerEngine; import net.neoforged.api.distmarker.Dist; import net.neoforged.fml.FMLVersion; @@ -57,34 +84,6 @@ import org.slf4j.Logger; import org.slf4j.event.Level; -import java.io.BufferedInputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.lang.instrument.Instrumentation; -import java.lang.module.Configuration; -import java.lang.module.ModuleDescriptor; -import java.lang.module.ModuleFinder; -import java.net.URI; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.ServiceLoader; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Collectors; - public final class FMLLoader implements AutoCloseable { private static final Logger LOGGER = LogUtils.getLogger(); @@ -106,7 +105,7 @@ public final class FMLLoader implements AutoCloseable { private final Dist dist; private LoadingModList loadingModList; private final Path gameDir; - private final Set locatedPaths = new HashSet<>(); + private final LocatedPaths locatedPaths; private VersionSupportMatrix versionSupportMatrix; public BackgroundScanHandler backgroundScanHandler; @@ -154,7 +153,13 @@ public boolean hasErrors() { } } - private FMLLoader(ClassLoaderStack classLoaderStack, GameDiscoveryResult discoveredGame, ProgramArgs programArgs, Dist dist, Path gameDir) { + private FMLLoader(LocatedPaths locatedPaths, + ClassLoaderStack classLoaderStack, + GameDiscoveryResult discoveredGame, + ProgramArgs programArgs, + Dist dist, + Path gameDir) { + this.locatedPaths = locatedPaths; this.classLoaderStack = classLoaderStack; this.discoveredGame = discoveredGame; this.programArgs = programArgs; @@ -304,7 +309,7 @@ public boolean addLocated(Path path) { var classLoaderStack = new ClassLoaderStack(initialLoader, locatedPaths); - loadEarlyServices(classLoaderStack, startupArgs); + var earlyServiceJars = loadEarlyServices(classLoaderStack, startupArgs); ImmediateWindowHandler.load(locatedPaths, startupArgs.headless(), programArgs); @@ -316,8 +321,10 @@ public boolean addLocated(Path path) { ImmediateWindowHandler.setNeoForgeVersion(neoForgeVersion); ImmediateWindowHandler.setMinecraftVersion(minecraftVersion); - var loader = new FMLLoader(classLoaderStack, discoveredGame, programArgs, dist, startupArgs.gameDirectory()); + var loader = new FMLLoader(locatedPaths, classLoaderStack, discoveredGame, programArgs, dist, startupArgs.gameDirectory()); try { + loader.earlyServicesJars.addAll(earlyServiceJars); + var discoveryResult = runLongRunning(startupArgs, loader::runDiscovery); for (var issue : discoveryResult.discoveryIssues()) { LOGGER.atLevel(issue.severity() == ModLoadingIssue.Severity.ERROR ? Level.ERROR : Level.WARN) @@ -393,8 +400,8 @@ public boolean addLocated(Path path) { } private static ClassProcessorSet createClassProcessorSet(StartupArgs startupArgs, - DiscoveryResult discoveryResult, - MixinFacade mixinFacade) { + DiscoveryResult discoveryResult, + MixinFacade mixinFacade) { // Add our own launch plugins explicitly. var builtInProcessors = new ArrayList(); builtInProcessors.add(createAccessTransformerService(discoveryResult)); @@ -443,8 +450,8 @@ private static ClassProcessor createAccessTransformerService(DiscoveryResult dis } private TransformingClassLoader buildTransformingLoader(ClassProcessorSet classProcessorSet, - ClassProcessorAuditLog auditTrail, - List content) { + ClassProcessorAuditLog auditTrail, + List content) { classLoaderStack.maskContentAlreadyOnClasspath(content); long start = System.currentTimeMillis(); @@ -491,12 +498,13 @@ private static String getModuleNameList(Configuration cf, List loadEarlyServices(ClassLoaderStack classLoaderStack, StartupArgs startupArgs) { // Search for early services var earlyServicesJars = new ArrayList<>(EarlyServiceDiscovery.findEarlyServiceJars(startupArgs, FMLPaths.MODSDIR.get())); if (!earlyServicesJars.isEmpty()) { classLoaderStack.appendLoader("FML Early Services", earlyServicesJars.stream().map(IModFile::getContents).toList()); } + return earlyServicesJars; } private void loadPlugins(List plugins) { @@ -578,8 +586,7 @@ private static T runOffThread(Supplier supplier) { } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("Interrupted while waiting for future", e); - } catch (TimeoutException ignored) { - } + } catch (TimeoutException ignored) {} } } @@ -657,12 +664,12 @@ public Path gameDirectory() { @Override public boolean isLocated(Path path) { - return locatedPaths.contains(path); + return locatedPaths.isLocated(path); } @Override public boolean addLocated(Path path) { - return locatedPaths.add(path); + return locatedPaths.addLocated(path); } @Override diff --git a/loader/src/test/java/net/neoforged/fml/jarcontents/FolderJarContentsTest.java b/loader/src/test/java/net/neoforged/fml/jarcontents/FolderJarContentsTest.java index b4588c475..90fc980e0 100644 --- a/loader/src/test/java/net/neoforged/fml/jarcontents/FolderJarContentsTest.java +++ b/loader/src/test/java/net/neoforged/fml/jarcontents/FolderJarContentsTest.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.List; import java.util.jar.Manifest; +import net.neoforged.fml.util.PathPrettyPrinting; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -201,7 +202,7 @@ void testGetManifestReturnsEmptyManifestIfManifestIsMissing() { @Test void testToString() { - assertEquals("folder(" + tempDir + ")", contents.toString()); + assertEquals("folder(" + PathPrettyPrinting.prettyPrint(tempDir) + ")", contents.toString()); } @Nested diff --git a/loader/src/test/java/net/neoforged/fml/jarcontents/JarFileContentsTest.java b/loader/src/test/java/net/neoforged/fml/jarcontents/JarFileContentsTest.java index 141c0f4a6..016ac3b5a 100644 --- a/loader/src/test/java/net/neoforged/fml/jarcontents/JarFileContentsTest.java +++ b/loader/src/test/java/net/neoforged/fml/jarcontents/JarFileContentsTest.java @@ -30,6 +30,7 @@ import java.util.function.Consumer; import java.util.jar.Manifest; import java.util.zip.ZipOutputStream; +import net.neoforged.fml.util.PathPrettyPrinting; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AutoClose; import org.junit.jupiter.api.BeforeEach; @@ -254,7 +255,7 @@ void testGetManifestReturnsEmptyManifestIfManifestIsMissing() throws IOException @Test void testToString() throws IOException { JarContents jarContents = getJarContents(); - assertEquals("jar(" + jarFilePath + ")", jarContents.toString()); + assertEquals("jar(" + PathPrettyPrinting.prettyPrint(jarFilePath) + ")", jarContents.toString()); } /** diff --git a/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java b/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java index 255a935c7..d436ee793 100644 --- a/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java +++ b/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java @@ -31,8 +31,8 @@ import net.neoforged.jarjar.metadata.ContainedJarIdentifier; import net.neoforged.jarjar.metadata.ContainedJarMetadata; import net.neoforged.jarjar.metadata.ContainedVersion; -import net.neoforged.neoforgespi.earlywindow.GraphicsBootstrapper; import net.neoforged.neoforgespi.locating.IModFile; +import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; import net.neoforged.neoforgespi.locating.IModFileReader; import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import org.apache.maven.artifact.versioning.VersionRange; @@ -65,19 +65,6 @@ void testProductionClientDiscovery() throws Exception { assertNeoForgeJar(result); } - @Test - void testProductionClientDiscoveryLegacyApproach() throws Exception { - installation.setupProductionClientLegacy(); - - var result = launchAndLoad("neoforgeclient"); - assertThat(result.loadedMods()).containsOnlyKeys("minecraft", "neoforge"); - assertThat(result.gameLayerModules()).containsOnlyKeys("minecraft", "neoforge"); - assertThat(result.pluginLayerModules()).isEmpty(); - - assertLegacyMinecraftClientJar(result, true); - assertNeoForgeJar(result); - } - @Test void testProductionServerDiscovery() throws Exception { installation.setupProductionServer(); @@ -92,20 +79,6 @@ void testProductionServerDiscovery() throws Exception { assertNeoForgeJar(result); } - @Test - void testProductionServerDiscoveryLegacyApproach() throws Exception { - installation.setupProductionServerLegacy(); - - var result = launchAndLoad("neoforgeserver"); - assertThat(result.issues()).isEmpty(); - assertThat(result.loadedMods()).containsOnlyKeys("minecraft", "neoforge"); - assertThat(result.gameLayerModules()).containsOnlyKeys("minecraft", "neoforge"); - assertThat(result.pluginLayerModules()).isEmpty(); - - assertLegacyMinecraftServerJar(result); - assertNeoForgeJar(result); - } - @Test void testNeoForgeDevServerDiscovery() throws Exception { var result = launchAndLoadInNeoForgeDevEnvironment("neoforgeserverdev"); @@ -647,9 +620,9 @@ void testMissingMinecraftJarInClientInstallation() throws Exception { var clientPath = installation.getLibrariesDir().resolve("net/neoforged/minecraft-client-patched/20.4.9999/minecraft-client-patched-20.4.9999.jar"); Files.delete(clientPath); - var e = assertThrows(ModLoadingException.class, () -> launchAndLoad("neoforgeclient")); + var e = assertThrows(ModLoadingException.class, () -> launchInstalledDist()); assertThat(getTranslatedIssues(e.getIssues())).containsOnly( - "ERROR: Your NeoForge installation is corrupted. Please try to reinstall NeoForge."); + "ERROR: The patched Minecraft jar is corrupted. Please try to reinstall NeoForge."); } @Test @@ -699,7 +672,7 @@ void testMissingMinecraftJarInServerInstallation() throws Exception { var e = assertThrows(ModLoadingException.class, () -> launchAndLoad("neoforgeserver")); assertThat(getTranslatedIssues(e.getIssues())).containsOnly( - "ERROR: Your NeoForge installation is corrupted. Please try to reinstall NeoForge."); + "ERROR: The patched Minecraft jar is corrupted. Please try to reinstall NeoForge."); } @Test @@ -984,15 +957,13 @@ public class NotLoadedYet { public static boolean dummy; } """) - .addClass("test.Bootstrapper", """ - public class Bootstrapper implements net.neoforged.neoforgespi.earlywindow.GraphicsBootstrapper { - @Override - public String name() { - return "dummy"; - } - + .addClass("test.CandidateLocator", """ + import net.neoforged.neoforgespi.IModFileCandidateLocator; + import net.neoforged.neoforgespi.IDiscoveryPipeline; + import net.neoforged.neoforgespi.locating.ILaunchContext; + public class CandidateLocator implements IModFileCandidateLocator { @Override - public void bootstrap(String[] arguments) { + public void findCandidates(ILaunchContext context, IDiscoveryPipeline pipeline) { net.neoforged.fml.loading.FMLLoader.getCurrent().addCloseCallback(() -> { NotLoadedYet.dummy = true; // This will fail if the CL is already closed net.neoforged.fml.loading.FMLLoaderTest.closeCallbackCalled = true; @@ -1000,7 +971,7 @@ public void bootstrap(String[] arguments) { } } """) - .addService(GraphicsBootstrapper.class, "test.Bootstrapper")); + .addService(IModFileCandidateLocator.class, "test.CandidateLocator")); launchClient(); diff --git a/loader/src/test/java/net/neoforged/fml/loading/LauncherTest.java b/loader/src/test/java/net/neoforged/fml/loading/LauncherTest.java index 8262a7b78..36ca225b7 100644 --- a/loader/src/test/java/net/neoforged/fml/loading/LauncherTest.java +++ b/loader/src/test/java/net/neoforged/fml/loading/LauncherTest.java @@ -370,18 +370,6 @@ public void assertMinecraftServerJar(LaunchResult launchResult) throws IOExcepti assertModContent(launchResult, "minecraft", expectedContent); } - /** - * Asserts a Minecraft Jar in the legacy installation mode where the Minecraft jar is assembled in-memory from different individual pieces. - * The only noticeable difference is that the Minecraft jar does not have a neoforge.mods.toml. - */ - public void assertLegacyMinecraftServerJar(LaunchResult launchResult) throws IOException { - var expectedContent = new ArrayList(); - Collections.addAll(expectedContent, SimulatedInstallation.SERVER_EXTRA_JAR_CONTENT); - expectedContent.add(SimulatedInstallation.PATCHED_SHARED); - - assertModContent(launchResult, "minecraft", expectedContent); - } - /** * Asserts a Minecraft Jar in the legacy installation mode where the Minecraft jar is assembled in-memory from different individual pieces. * The only noticeable difference is that the Minecraft jar does not have a neoforge.mods.toml. diff --git a/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedInstallation.java b/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedInstallation.java index 8336aa9c4..04a0af60c 100644 --- a/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedInstallation.java +++ b/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedInstallation.java @@ -286,28 +286,10 @@ public void setupProductionClient() throws IOException { setup(Type.PRODUCTION_CLIENT); } - public void setupProductionClientLegacy() throws IOException { - System.setProperty(LIBRARIES_DIRECTORY_PROPERTY, librariesDir.toString()); - - writeLibrary("net.minecraft", "client", MC_VERSION + "-" + NEOFORM_VERSION, "srg", RENAMED_CLIENT, RENAMED_SHARED); - writeLibrary("net.minecraft", "client", MC_VERSION + "-" + NEOFORM_VERSION, "extra", CLIENT_ASSETS, SHARED_ASSETS, MINECRAFT_VERSION_JSON); - writeLibrary("net.neoforged", "neoforge", NEOFORGE_VERSION, "client", PATCHED_CLIENT); - writeLibrary("net.neoforged", "neoforge", NEOFORGE_VERSION, "universal", NEOFORGE_UNIVERSAL_JAR_CONTENT); - } - public void setupProductionServer() throws IOException { setup(Type.PRODUCTION_SERVER); } - public void setupProductionServerLegacy() throws IOException { - System.setProperty(LIBRARIES_DIRECTORY_PROPERTY, librariesDir.toString()); - - writeLibrary("net.minecraft", "server", MC_VERSION + "-" + NEOFORM_VERSION, "srg", RENAMED_SHARED); - writeLibrary("net.minecraft", "server", MC_VERSION + "-" + NEOFORM_VERSION, "extra", SERVER_EXTRA_JAR_CONTENT); - writeLibrary("net.neoforged", "neoforge", NEOFORGE_VERSION, "server", PATCHED_SHARED); - writeLibrary("net.neoforged", "neoforge", NEOFORGE_VERSION, "universal", NEOFORGE_UNIVERSAL_JAR_CONTENT); - } - // The classes directory in a NeoForge development environment will contain both the Minecraft // and the NeoForge classes. This is due to both calling each other and having to be compiled in // the same javac compilation as a result. From 881a66da68326ccddb569916f1354c4024f7f257 Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Sun, 26 Oct 2025 15:31:05 +0100 Subject: [PATCH 03/29] Fix tests --- .../test/java/net/neoforged/fml/loading/FMLLoaderTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java b/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java index d436ee793..263198198 100644 --- a/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java +++ b/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java @@ -958,9 +958,9 @@ public class NotLoadedYet { } """) .addClass("test.CandidateLocator", """ - import net.neoforged.neoforgespi.IModFileCandidateLocator; - import net.neoforged.neoforgespi.IDiscoveryPipeline; - import net.neoforged.neoforgespi.locating.ILaunchContext; + import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; + import net.neoforged.neoforgespi.locating.IDiscoveryPipeline; + import net.neoforged.neoforgespi.ILaunchContext; public class CandidateLocator implements IModFileCandidateLocator { @Override public void findCandidates(ILaunchContext context, IDiscoveryPipeline pipeline) { From 8de2cce48e1930db335d7096dd974ca98d826b42 Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Sun, 26 Oct 2025 18:28:22 +0100 Subject: [PATCH 04/29] Remove unused --- .../main/java/net/neoforged/fml/util/ServiceLoaderUtil.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/loader/src/main/java/net/neoforged/fml/util/ServiceLoaderUtil.java b/loader/src/main/java/net/neoforged/fml/util/ServiceLoaderUtil.java index 397a843bc..79df72ca9 100644 --- a/loader/src/main/java/net/neoforged/fml/util/ServiceLoaderUtil.java +++ b/loader/src/main/java/net/neoforged/fml/util/ServiceLoaderUtil.java @@ -34,10 +34,6 @@ public static List loadServices(Class serviceClass) { return loadServices(serviceClass, List.of()); } - public static List loadServices(Class serviceClass, Predicate> filter) { - return loadServices(serviceClass, List.of(), filter); - } - public static List loadServices(Class serviceClass, Collection additionalServices) { return loadServices(serviceClass, additionalServices, ignored -> true); } From 1e3b62e2a405f7db3d3304830c5f1c57a01e78a9 Mon Sep 17 00:00:00 2001 From: Marc Hermans Date: Fri, 28 Nov 2025 21:06:43 +0100 Subject: [PATCH 05/29] Add dynamic game installation discovery service --- .../fml/loading/EarlyServiceDiscovery.java | 4 +- .../net/neoforged/fml/loading/FMLLoader.java | 40 +++- .../fml/loading/game/GameDiscovery.java | 18 +- .../fml/loading/game/RequiredSystemFiles.java | 4 + .../GameDiscoveryOrInstallationService.java | 54 +++++ .../neoforged/fml/loading/FMLLoaderTest.java | 13 ++ .../neoforged/fml/loading/LauncherTest.java | 4 +- ...tedGameAndInstallationServiceProvider.java | 208 ++++++++++++++++++ .../fml/testlib/SimulatedInstallation.java | 46 +++- 9 files changed, 381 insertions(+), 10 deletions(-) create mode 100644 loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java create mode 100644 testlib/src/main/java/net/neoforged/fml/testlib/SimulatedGameAndInstallationServiceProvider.java diff --git a/loader/src/main/java/net/neoforged/fml/loading/EarlyServiceDiscovery.java b/loader/src/main/java/net/neoforged/fml/loading/EarlyServiceDiscovery.java index 8ea03f613..fa98752fc 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/EarlyServiceDiscovery.java +++ b/loader/src/main/java/net/neoforged/fml/loading/EarlyServiceDiscovery.java @@ -24,6 +24,7 @@ import net.neoforged.fml.startup.StartupArgs; import net.neoforged.neoforgespi.earlywindow.GraphicsBootstrapper; import net.neoforged.neoforgespi.earlywindow.ImmediateWindowProvider; +import net.neoforged.neoforgespi.installation.GameDiscoveryOrInstallationService; import net.neoforged.neoforgespi.locating.IDependencyLocator; import net.neoforged.neoforgespi.locating.IModFile; import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; @@ -39,7 +40,8 @@ final class EarlyServiceDiscovery { IModFileReader.class, IDependencyLocator.class, GraphicsBootstrapper.class, - ImmediateWindowProvider.class); + ImmediateWindowProvider.class, + GameDiscoveryOrInstallationService.class); private EarlyServiceDiscovery() {} diff --git a/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java b/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java index e7c25b13b..c90b49fa9 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java +++ b/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java @@ -71,6 +71,7 @@ import net.neoforged.fml.util.ServiceLoaderUtil; import net.neoforged.neoforgespi.ILaunchContext; import net.neoforged.neoforgespi.LocatedPaths; +import net.neoforged.neoforgespi.installation.GameDiscoveryOrInstallationService; import net.neoforged.neoforgespi.language.IModFileInfo; import net.neoforged.neoforgespi.language.IModInfo; import net.neoforged.neoforgespi.locating.IModFile; @@ -313,7 +314,9 @@ public boolean addLocated(Path path) { ImmediateWindowHandler.load(locatedPaths, startupArgs.headless(), programArgs); - var discoveredGame = runLongRunning(startupArgs, () -> GameDiscovery.discoverGame(programArgs, locatedPaths, dist)); + var gameInstallationService = discoverGameInstaller(locatedPaths, programArgs); + + var discoveredGame = runLongRunning(startupArgs, () -> GameDiscovery.discoverGame(programArgs, locatedPaths, dist, gameInstallationService)); var neoForgeVersion = discoveredGame.neoforge().getModFileInfo().versionString(); var minecraftVersion = discoveredGame.minecraft().getModFileInfo().versionString(); @@ -399,6 +402,41 @@ public boolean addLocated(Path path) { } } + @Nullable + private static GameDiscoveryOrInstallationService discoverGameInstaller(LocatedPaths located, ProgramArgs programArgs) { + if (programArgs.hasValue("fml.disableInstaller")) + return null; + + var providers = ServiceLoaderUtil.loadEarlyServices(located, GameDiscoveryOrInstallationService.class, List.of()) + .stream() + .toList(); + + if (providers.size() == 1) + return providers.getFirst(); + + if (providers.isEmpty()) { + LOGGER.error("No installation provider found!"); + return null; + } + + if (!programArgs.hasValue("fml.installer")) { + LOGGER.warn("Failed to find game installer, multiple are found, but no selector is provided!"); + return null; + } + + var installerName = programArgs.get("fml.installer"); + var installer = providers.stream() + .filter(p -> Objects.equals(p.name(), installerName)) + .findFirst(); + + if (installer.isEmpty()) { + LOGGER.error("Requested installer: {} was not found in: {}", installerName, providers.stream().map(GameDiscoveryOrInstallationService::name).collect(Collectors.joining(", "))); + return null; + } + + return installer.get(); + } + private static ClassProcessorSet createClassProcessorSet(StartupArgs startupArgs, DiscoveryResult discoveryResult, MixinFacade mixinFacade) { diff --git a/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java b/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java index 599199476..c968d2617 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java +++ b/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java @@ -32,6 +32,7 @@ import net.neoforged.fml.util.ClasspathResourceUtils; import net.neoforged.fml.util.PathPrettyPrinting; import net.neoforged.neoforgespi.LocatedPaths; +import net.neoforged.neoforgespi.installation.GameDiscoveryOrInstallationService; import net.neoforged.neoforgespi.language.IModInfo; import net.neoforged.neoforgespi.locating.IModFile; import net.neoforged.neoforgespi.locating.ModFileDiscoveryAttributes; @@ -49,7 +50,7 @@ public final class GameDiscovery { private GameDiscovery() {} - public static GameDiscoveryResult discoverGame(ProgramArgs programArgs, LocatedPaths locatedPaths, Dist requiredDist) { + public static GameDiscoveryResult discoverGame(ProgramArgs programArgs, LocatedPaths locatedPaths, Dist requiredDist, @Nullable GameDiscoveryOrInstallationService gameDiscoveryOrInstallationService) { var ourCl = Thread.currentThread().getContextClassLoader(); var neoForgeVersion = getNeoForgeVersion(programArgs, ourCl); @@ -65,7 +66,7 @@ public static GameDiscoveryResult discoverGame(ProgramArgs programArgs, LocatedP // We look for a class present in both Minecraft distributions on the classpath, which would be obfuscated in production (DetectedVersion) // If that is present, we assume we're launching in dev (NeoDev or ModDev). try (var systemFiles = RequiredSystemFiles.find(locatedPaths::isLocated, ourCl)) { - if (!systemFiles.isEmpty()) { + if (!systemFiles.isEmpty() && systemFiles.hasMinecraft()) { // If we've only been able to find some of the required files, we need to error systemFiles.checkForMissingMinecraftFiles(requiredDist == Dist.CLIENT); @@ -76,7 +77,7 @@ public static GameDiscoveryResult discoverGame(ProgramArgs programArgs, LocatedP } // In production, it's in the libraries directory, and we're passed the version to look for on the commandline - return locateProductionMinecraft(neoForgeVersion, requiredDist); + return locateProductionMinecraft(neoForgeVersion, requiredDist, gameDiscoveryOrInstallationService); } private static String getNeoForgeVersion(ProgramArgs programArgs, ClassLoader ourCl) { @@ -290,7 +291,7 @@ private static void addContentRoot(List roots, JarCont /** * In production, the client and neoforge jars are assembled from partial jars in the libraries folder. */ - private static GameDiscoveryResult locateProductionMinecraft(String neoForgeVersion, Dist requiredDist) { + private static GameDiscoveryResult locateProductionMinecraft(String neoForgeVersion, Dist requiredDist, @Nullable GameDiscoveryOrInstallationService gameDiscoveryOrInstallationService) { // 2) It's neither, but a libraries directory and desired versions are given on the commandline var librariesDirectory = System.getProperty(LIBRARIES_DIRECTORY_PROPERTY); if (librariesDirectory == null) { @@ -320,6 +321,15 @@ private static GameDiscoveryResult locateProductionMinecraft(String neoForgeVers }).toRelativeRepositoryPath()); var nfModFile = openModFile(neoforgeJar, "neoforge", "fml.modloadingissue.corrupted_neoforge_jar"); + + if (!Files.exists(patchedMinecraftPath)) { + if (gameDiscoveryOrInstallationService != null) { + LOG.info("Patched minecraft does not exist. Triggering external discovery or installation service!"); + var result = gameDiscoveryOrInstallationService.discoverOrInstall(); + patchedMinecraftPath = result.minecraft(); + } + } + ModFile minecraftModFile; try { minecraftModFile = openModFile(patchedMinecraftPath, "minecraft", "fml.modloadingissue.corrupted_minecraft_jar"); diff --git a/loader/src/main/java/net/neoforged/fml/loading/game/RequiredSystemFiles.java b/loader/src/main/java/net/neoforged/fml/loading/game/RequiredSystemFiles.java index bd288d636..8931dc815 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/game/RequiredSystemFiles.java +++ b/loader/src/main/java/net/neoforged/fml/loading/game/RequiredSystemFiles.java @@ -270,4 +270,8 @@ private static List uniqueAndNonNull(JarContents... contents) { } return result; } + + public boolean hasMinecraft() { + return !getMinecraftJarComponents().isEmpty(); + } } diff --git a/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java b/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java new file mode 100644 index 000000000..47f7e383d --- /dev/null +++ b/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java @@ -0,0 +1,54 @@ +package net.neoforged.neoforgespi.installation; + +import java.nio.file.Path; + +/** + * A service which can be used to discover or install the game at runtime. + *

+ * When the game launches it tries to discover the NeoForge universal jar and the patched minecraft + * jar from the folder which contains the libraries for the game. + *

+ *

+ * The first step will be to validate the libraries folder, and then find the relevant neoforge jar + * in their. Secondly it will then parse its neoforge.mods.toml and prepare the launch. + *

+ *

+ * When that all succeeds the game tries to find the relevant patched minecraft jar in the folder as well, + * as it expects this jar to be put there by the installer. + * If the jar exists it will continue and load the neoforge.mods.toml from that jar and continue the normal + * loading procedure. + *

+ *

+ * If it can not find the patched minecraft jar (because the installer did not create the file), then it will + * check if a service of this type is found as an early loader service (if multiple are found a launch argument + * 'fml.installer' is used to differentiate and select the requested instance). Then it will call + * {@link GameDiscoveryOrInstallationService#discoverOrInstall()} to handle the discovery or installation. + *

+ *

+ * Each implementation of this type is responsible on its own for caching its results. + *

+ *

+ * If the launch argument 'fml.disableInstaller' is provided then this entire subsystem is disabled and the loader + * will not try to invoke or even discover and instantiate implementation of this type. + *

+ */ +public interface GameDiscoveryOrInstallationService { + /** + * {@return The name of the service} + */ + String name(); + + /** + * Invoked to discover or install the game when it is not found in the libraries folder. + * + * @return The {@link Result} of discovery or installation. + */ + Result discoverOrInstall(); + + /** + * The result of the discovery or installation. + * + * @param minecraft The path to the patched minecraft jar. + */ + record Result(Path minecraft) {} +} diff --git a/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java b/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java index 263198198..9e2d77df2 100644 --- a/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java +++ b/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java @@ -65,6 +65,19 @@ void testProductionClientDiscovery() throws Exception { assertNeoForgeJar(result); } + @Test + void testProductionClientWithDynamicInstallationDiscovery() throws Exception { + installation.setupProductionClientWithDynamicInstallation(); + + var result = launchAndLoad("neoforgeclient"); + assertThat(result.loadedMods()).containsOnlyKeys("minecraft", "neoforge"); + assertThat(result.gameLayerModules()).containsOnlyKeys("minecraft", "neoforge"); + assertThat(result.pluginLayerModules()).isEmpty(); + + assertMinecraftClientJar(result, true); + assertNeoForgeJar(result); + } + @Test void testProductionServerDiscovery() throws Exception { installation.setupProductionServer(); diff --git a/loader/src/test/java/net/neoforged/fml/loading/LauncherTest.java b/loader/src/test/java/net/neoforged/fml/loading/LauncherTest.java index 36ca225b7..cc2c85794 100644 --- a/loader/src/test/java/net/neoforged/fml/loading/LauncherTest.java +++ b/loader/src/test/java/net/neoforged/fml/loading/LauncherTest.java @@ -119,7 +119,7 @@ final void cleanupLoaderAndInstallation() throws Exception { */ LaunchResult launchInstalledDist() throws Exception { var supportedDist = switch (installation.getType()) { - case PRODUCTION_CLIENT, USERDEV_FOLDERS, USERDEV_JAR, USERDEV_LEGACY_FOLDERS, USERDEV_LEGACY_JAR -> Dist.CLIENT; + case PRODUCTION_CLIENT, PRODUCTION_CLIENT_INSTALLED_AT_RUNTIME, USERDEV_FOLDERS, USERDEV_JAR, USERDEV_LEGACY_FOLDERS, USERDEV_LEGACY_JAR -> Dist.CLIENT; case PRODUCTION_SERVER -> Dist.DEDICATED_SERVER; }; return launchAndLoad(supportedDist, true, List.of()); @@ -236,7 +236,7 @@ private LaunchResult launch(Dist launchDist, boolean cleanDist, List addit // TODO: We can pass less in certain scenarios and should (i.e. development) "--fml.mcVersion", SimulatedInstallation.MC_VERSION, "--fml.neoForgeVersion", SimulatedInstallation.NEOFORGE_VERSION, - "--fml.neoFormVersion", SimulatedInstallation.NEOFORM_VERSION + "--fml.neoFormVersion", SimulatedInstallation.NEOFORM_VERSION, }, locatedPaths.stream().map(Path::toFile).collect(Collectors.toSet()), additionalClassPath.stream().map(Path::toFile).toList(), diff --git a/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedGameAndInstallationServiceProvider.java b/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedGameAndInstallationServiceProvider.java new file mode 100644 index 000000000..b84c5692f --- /dev/null +++ b/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedGameAndInstallationServiceProvider.java @@ -0,0 +1,208 @@ +package net.neoforged.fml.testlib; + +import static org.objectweb.asm.Opcodes.ACC_FINAL; +import static org.objectweb.asm.Opcodes.ACC_PUBLIC; +import static org.objectweb.asm.Opcodes.ACC_RECORD; +import static org.objectweb.asm.Opcodes.ACC_STATIC; +import static org.objectweb.asm.Opcodes.ACC_SUPER; +import static org.objectweb.asm.Opcodes.ALOAD; +import static org.objectweb.asm.Opcodes.ANEWARRAY; +import static org.objectweb.asm.Opcodes.ARETURN; +import static org.objectweb.asm.Opcodes.ASTORE; +import static org.objectweb.asm.Opcodes.DUP; +import static org.objectweb.asm.Opcodes.ICONST_0; +import static org.objectweb.asm.Opcodes.INVOKEINTERFACE; +import static org.objectweb.asm.Opcodes.INVOKESPECIAL; +import static org.objectweb.asm.Opcodes.INVOKESTATIC; +import static org.objectweb.asm.Opcodes.IRETURN; +import static org.objectweb.asm.Opcodes.NEW; +import static org.objectweb.asm.Opcodes.RETURN; +import static org.objectweb.asm.Opcodes.V21; + +import java.util.Arrays; +import java.util.stream.Collectors; +import org.apache.commons.lang3.ArrayUtils; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.RecordComponentVisitor; +import org.objectweb.asm.Type; + +public class SimulatedGameAndInstallationServiceProvider { + private static IdentifiableContent createInstallerClass( + InstallerInstance installerInstance) { + return createInstallerClass( + installerInstance.className, + installerInstance.packageName, + installerInstance.relativePath); + } + + private static IdentifiableContent createInstallerClass( + String className, + String packageName, + String relativePath) { + ClassWriter classWriter = new ClassWriter(0); + FieldVisitor fieldVisitor; + RecordComponentVisitor recordComponentVisitor; + MethodVisitor methodVisitor; + AnnotationVisitor annotationVisitor0; + + String packagePath = packageName.replace(".", "/"); + + classWriter.visit(V21, ACC_PUBLIC | ACC_FINAL | ACC_SUPER | ACC_RECORD, "%s/%s".formatted(packagePath, className), null, "java/lang/Record", new String[] { "net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService" }); + + classWriter.visitSource("%s.java".formatted(className), null); + + classWriter.visitInnerClass("net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService$Result", "net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService", "Result", ACC_PUBLIC | ACC_FINAL | ACC_STATIC); + + classWriter.visitInnerClass("java/lang/invoke/MethodHandles$Lookup", "java/lang/invoke/MethodHandles", "Lookup", ACC_PUBLIC | ACC_FINAL | ACC_STATIC); + + { + methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "", "()V", null, null); + methodVisitor.visitCode(); + Label label0 = new Label(); + methodVisitor.visitLabel(label0); + methodVisitor.visitLineNumber(10, label0); + methodVisitor.visitVarInsn(ALOAD, 0); + methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Record", "", "()V", false); + methodVisitor.visitInsn(RETURN); + Label label1 = new Label(); + methodVisitor.visitLabel(label1); + methodVisitor.visitLocalVariable("this", "L%s/%s;".formatted(packagePath, className), null, label0, label1, 0); + methodVisitor.visitMaxs(1, 1); + methodVisitor.visitEnd(); + } + { + methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "name", "()Ljava/lang/String;", null, null); + methodVisitor.visitCode(); + Label label0 = new Label(); + methodVisitor.visitLabel(label0); + methodVisitor.visitLineNumber(13, label0); + methodVisitor.visitLdcInsn("simulatedInstallation"); + methodVisitor.visitInsn(ARETURN); + Label label1 = new Label(); + methodVisitor.visitLabel(label1); + methodVisitor.visitLocalVariable("this", "L%s/%s;".formatted(packagePath, className), null, label0, label1, 0); + methodVisitor.visitMaxs(1, 1); + methodVisitor.visitEnd(); + } + { + methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "discoverOrInstall", "()Lnet/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService$Result;", null, null); + methodVisitor.visitCode(); + Label label0 = new Label(); + methodVisitor.visitLabel(label0); + methodVisitor.visitLineNumber(18, label0); + methodVisitor.visitLdcInsn("libraryDirectory"); + methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "getProperty", "(Ljava/lang/String;)Ljava/lang/String;", false); + methodVisitor.visitVarInsn(ASTORE, 1); + Label label1 = new Label(); + methodVisitor.visitLabel(label1); + methodVisitor.visitLineNumber(19, label1); + methodVisitor.visitVarInsn(ALOAD, 1); + methodVisitor.visitInsn(ICONST_0); + methodVisitor.visitTypeInsn(ANEWARRAY, "java/lang/String"); + methodVisitor.visitMethodInsn(INVOKESTATIC, "java/nio/file/Path", "of", "(Ljava/lang/String;[Ljava/lang/String;)Ljava/nio/file/Path;", true); + methodVisitor.visitVarInsn(ASTORE, 2); + Label label2 = new Label(); + methodVisitor.visitLabel(label2); + methodVisitor.visitLineNumber(20, label2); + methodVisitor.visitTypeInsn(NEW, "net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService$Result"); + methodVisitor.visitInsn(DUP); + methodVisitor.visitVarInsn(ALOAD, 2); + methodVisitor.visitLdcInsn(relativePath); + Label label3 = new Label(); + methodVisitor.visitLabel(label3); + methodVisitor.visitLineNumber(21, label3); + methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/nio/file/Path", "resolve", "(Ljava/lang/String;)Ljava/nio/file/Path;", true); + methodVisitor.visitMethodInsn(INVOKESPECIAL, "net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService$Result", "", "(Ljava/nio/file/Path;)V", false); + Label label4 = new Label(); + methodVisitor.visitLabel(label4); + methodVisitor.visitLineNumber(20, label4); + methodVisitor.visitInsn(ARETURN); + Label label5 = new Label(); + methodVisitor.visitLabel(label5); + methodVisitor.visitLocalVariable("this", "L%s/%s;".formatted(packagePath, className), null, label0, label5, 0); + methodVisitor.visitLocalVariable("librariesDirectory", "Ljava/lang/String;", null, label1, label5, 1); + methodVisitor.visitLocalVariable("librariesRoot", "Ljava/nio/file/Path;", null, label2, label5, 2); + methodVisitor.visitMaxs(4, 3); + methodVisitor.visitEnd(); + } + { + methodVisitor = classWriter.visitMethod(ACC_PUBLIC | ACC_FINAL, "toString", "()Ljava/lang/String;", null, null); + methodVisitor.visitCode(); + Label label0 = new Label(); + methodVisitor.visitLabel(label0); + methodVisitor.visitLineNumber(10, label0); + methodVisitor.visitVarInsn(ALOAD, 0); + methodVisitor.visitInvokeDynamicInsn("toString", "(L%s/%s;)Ljava/lang/String;".formatted(packagePath, className), new Handle(Opcodes.H_INVOKESTATIC, "java/lang/runtime/ObjectMethods", "bootstrap", "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;", false), new Object[] { Type.getType("L%s/%s;".formatted(packagePath, className)), "" }); + methodVisitor.visitInsn(ARETURN); + Label label1 = new Label(); + methodVisitor.visitLabel(label1); + methodVisitor.visitLocalVariable("this", "L%s/%s;".formatted(packagePath, className), null, label0, label1, 0); + methodVisitor.visitMaxs(1, 1); + methodVisitor.visitEnd(); + } + { + methodVisitor = classWriter.visitMethod(ACC_PUBLIC | ACC_FINAL, "hashCode", "()I", null, null); + methodVisitor.visitCode(); + Label label0 = new Label(); + methodVisitor.visitLabel(label0); + methodVisitor.visitLineNumber(10, label0); + methodVisitor.visitVarInsn(ALOAD, 0); + methodVisitor.visitInvokeDynamicInsn("hashCode", "(L%s/%s;)I".formatted(packagePath, className), new Handle(Opcodes.H_INVOKESTATIC, "java/lang/runtime/ObjectMethods", "bootstrap", "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;", false), new Object[] { Type.getType("L%s/%s;".formatted(packagePath, className)), "" }); + methodVisitor.visitInsn(IRETURN); + Label label1 = new Label(); + methodVisitor.visitLabel(label1); + methodVisitor.visitLocalVariable("this", "L%s/%s;".formatted(packagePath, className), null, label0, label1, 0); + methodVisitor.visitMaxs(1, 1); + methodVisitor.visitEnd(); + } + { + methodVisitor = classWriter.visitMethod(ACC_PUBLIC | ACC_FINAL, "equals", "(Ljava/lang/Object;)Z", null, null); + methodVisitor.visitCode(); + Label label0 = new Label(); + methodVisitor.visitLabel(label0); + methodVisitor.visitLineNumber(10, label0); + methodVisitor.visitVarInsn(ALOAD, 0); + methodVisitor.visitVarInsn(ALOAD, 1); + methodVisitor.visitInvokeDynamicInsn("equals", "(L%s/%s;Ljava/lang/Object;)Z".formatted(packagePath, className), new Handle(Opcodes.H_INVOKESTATIC, "java/lang/runtime/ObjectMethods", "bootstrap", "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;", false), new Object[] { Type.getType("L%s/%s;".formatted(packagePath, className)), "" }); + methodVisitor.visitInsn(IRETURN); + Label label1 = new Label(); + methodVisitor.visitLabel(label1); + methodVisitor.visitLocalVariable("this", "L%s/%s;".formatted(packagePath, className), null, label0, label1, 0); + methodVisitor.visitLocalVariable("o", "Ljava/lang/Object;", null, label0, label1, 1); + methodVisitor.visitMaxs(2, 2); + methodVisitor.visitEnd(); + } + classWriter.visitEnd(); + + return new IdentifiableContent("%s/%s.class".formatted(packageName, className), "%s/%s.class".formatted(packagePath, className), classWriter.toByteArray()); + } + + public record InstallerInstance( + String className, + String packageName, + String relativePath) {} + + public static IdentifiableContent[] create( + InstallerInstance... installers) { + var installerClasses = Arrays.stream(installers) + .map(SimulatedGameAndInstallationServiceProvider::createInstallerClass) + .toArray(IdentifiableContent[]::new); + + var serviceFileContent = Arrays.stream(installers) + .map(installer -> "%s.%s".formatted(installer.packageName, installer.className)) + .collect(Collectors.joining("\n")); + + var serviceFile = new IdentifiableContent( + "servicefile:net.neoforged.neoforgespi.installation.GameDiscoveryOrInstallationService", + "META-INF/services/net.neoforged.neoforgespi.installation.GameDiscoveryOrInstallationService", + serviceFileContent.getBytes()); + + return ArrayUtils.addAll(installerClasses, serviceFile); + } +} diff --git a/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedInstallation.java b/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedInstallation.java index 04a0af60c..2df4f024a 100644 --- a/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedInstallation.java +++ b/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedInstallation.java @@ -38,6 +38,7 @@ import net.neoforged.jarjar.metadata.Metadata; import net.neoforged.jarjar.metadata.MetadataIOHandler; import net.neoforged.jarjar.selection.util.Constants; +import org.apache.commons.lang3.ArrayUtils; import org.jetbrains.annotations.Nullable; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Opcodes; @@ -52,6 +53,11 @@ public class SimulatedInstallation implements AutoCloseable { public enum Type { PRODUCTION_CLIENT, + /** + * A special variant of the production client where the installation happens + * when the game loads through an early loader service. + */ + PRODUCTION_CLIENT_INSTALLED_AT_RUNTIME, PRODUCTION_SERVER, /** * Used by NeoGradle and ModDevGradle currently. @@ -59,7 +65,7 @@ public enum Type { * - A jar with all Minecraft Classes, NeoForge Classes and Resources * - A second jar with the original non-class content of the Minecraft jar * The Minecraft classes and resources are merged from server+client distributions. - * + *

* The difference between FOLDERS and JAR relates to how the "installation appropriate" mod project * is put onto the classpath (as folders, or built as a jar file). */ @@ -70,7 +76,7 @@ public enum Type { * It puts two jars on the classpath: * - The merged, patched Minecraft jar, including classes and resources from both distributions * - The unmodified NeoForge universal jar - * + *

* The difference between FOLDERS and JAR relates to how the "installation appropriate" mod project * is put onto the classpath (as folders, or built as a jar file). */ @@ -134,8 +140,10 @@ public boolean isProduction() { public static final IdentifiableContent[] USERDEV_CLIENT_JAR_CONTENT = { PATCHED_CLIENT, PATCHED_SHARED }; private static final String GAV_PATCHED_CLIENT = "net.neoforged:minecraft-client-patched:" + NEOFORGE_VERSION; + private static final String GAV_DYNAMIC_PATCHED_CLIENT = "net.neoforged-dynamic-install:minecraft-client-patched:" + NEOFORGE_VERSION; private static final String GAV_PATCHED_SERVER = "net.neoforged:minecraft-server-patched:" + NEOFORGE_VERSION; private static final String GAV_NEOFORGE_UNIVERSAL = "net.neoforged:neoforge:" + NEOFORGE_VERSION + ":universal"; + private static final String GAV_NEOFORGE_DYNAMIC_INSTALLER = "net.neoforged:neoforge:" + NEOFORGE_VERSION + ":dynamic-installer"; private static byte[] buildVersionJson(String mcVersion) { var obj = new JsonObject(); @@ -197,6 +205,36 @@ public void setup(Type type) throws IOException { componentRoots = InstallationComponents.productionJars(patchedClientJar, universalJar); } + case PRODUCTION_CLIENT_INSTALLED_AT_RUNTIME -> { + System.setProperty(LIBRARIES_DIRECTORY_PROPERTY, librariesDir.toString()); + + var patchedClientJar = writeLibrary(GAV_DYNAMIC_PATCHED_CLIENT, PATCHED_CLIENT, RENAMED_SHARED, CLIENT_ASSETS, SHARED_ASSETS, MINECRAFT_MODS_TOML, MINECRAFT_VERSION_JSON); + var universalJar = writeLibrary(GAV_NEOFORGE_UNIVERSAL, NEOFORGE_UNIVERSAL_JAR_CONTENT); + var dynamicInstallerJar = writeLibrary(GAV_NEOFORGE_DYNAMIC_INSTALLER, SimulatedGameAndInstallationServiceProvider + .create( + new SimulatedGameAndInstallationServiceProvider.InstallerInstance( + "DynamicInstaller", + "net.neoforged.neoforge.installer", + librariesDir.relativize(patchedClientJar).toString()))); + + // For the production client, the Vanilla launcher puts the original, obfuscated client jar on the classpath + // Since this can influence our detection logic, let's make sure it's included for the tests. + Path obfuscatedClientJar = versionsDir.resolve(MC_VERSION).resolve(MC_VERSION + ".jar"); + writeJarFile( + obfuscatedClientJar, + generateClass("CLIENT_MAIN", "net/minecraft/client/main/Main.class"), + generateClass("CLIENT_DATA_MAIN", "net/minecraft/client/data/Main.class"), + generateClass("SERVER_MAIN", "net/minecraft/server/Main.class"), + generateClass("SERVER_DATA_MAIN", "net/minecraft/data/Main.class"), + generateClass("GAMETEST_MAIN", "net/minecraft/gametest/Main.class"), + generateClass("MINECRAFT_SERVER", "net/minecraft/server/MinecraftServer.class"), + MINECRAFT_VERSION_JSON, + SHARED_ASSETS, + CLIENT_ASSETS); + launchClasspath.add(dynamicInstallerJar); + + componentRoots = InstallationComponents.productionJars(obfuscatedClientJar, universalJar); + } case PRODUCTION_SERVER -> { System.setProperty(LIBRARIES_DIRECTORY_PROPERTY, librariesDir.toString()); @@ -286,6 +324,10 @@ public void setupProductionClient() throws IOException { setup(Type.PRODUCTION_CLIENT); } + public void setupProductionClientWithDynamicInstallation() throws IOException { + setup(Type.PRODUCTION_CLIENT_INSTALLED_AT_RUNTIME); + } + public void setupProductionServer() throws IOException { setup(Type.PRODUCTION_SERVER); } From 5d8e7e2e732051a41798a5359fbcc5afd155601d Mon Sep 17 00:00:00 2001 From: Marc Hermans Date: Fri, 28 Nov 2025 22:19:54 +0100 Subject: [PATCH 06/29] Add parameters for better detection of the game discovery or installation result. Allow the external discovery service to return null as a result, which indicates failure or disinterest with the discovery. --- .../fml/loading/game/GameDiscovery.java | 8 ++++-- .../GameDiscoveryOrInstallationService.java | 12 ++++++-- ...tedGameAndInstallationServiceProvider.java | 28 +++++++++++++------ 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java b/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java index c968d2617..ce1f4ea85 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java +++ b/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java @@ -325,8 +325,12 @@ private static GameDiscoveryResult locateProductionMinecraft(String neoForgeVers if (!Files.exists(patchedMinecraftPath)) { if (gameDiscoveryOrInstallationService != null) { LOG.info("Patched minecraft does not exist. Triggering external discovery or installation service!"); - var result = gameDiscoveryOrInstallationService.discoverOrInstall(); - patchedMinecraftPath = result.minecraft(); + var result = gameDiscoveryOrInstallationService.discoverOrInstall(neoForgeVersion, requiredDist); + if (result != null) { + patchedMinecraftPath = result.minecraft(); + } else { + LOG.info("Game discovery or installation service: {} did not return a result. Skipping.", gameDiscoveryOrInstallationService.name()); + } } } diff --git a/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java b/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java index 47f7e383d..92e2256b8 100644 --- a/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java +++ b/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java @@ -1,5 +1,8 @@ package net.neoforged.neoforgespi.installation; +import net.neoforged.api.distmarker.Dist; +import org.jetbrains.annotations.Nullable; + import java.nio.file.Path; /** @@ -22,7 +25,7 @@ * If it can not find the patched minecraft jar (because the installer did not create the file), then it will * check if a service of this type is found as an early loader service (if multiple are found a launch argument * 'fml.installer' is used to differentiate and select the requested instance). Then it will call - * {@link GameDiscoveryOrInstallationService#discoverOrInstall()} to handle the discovery or installation. + * {@link GameDiscoveryOrInstallationService#discoverOrInstall(String, Dist)} to handle the discovery or installation. *

*

* Each implementation of this type is responsible on its own for caching its results. @@ -41,9 +44,12 @@ public interface GameDiscoveryOrInstallationService { /** * Invoked to discover or install the game when it is not found in the libraries folder. * - * @return The {@link Result} of discovery or installation. + * @param neoForgeVersion The version of neoforge for which the discovery or installation should be started. + * @param requiredDist The distribution which should be discovered or installed. + * @return The {@link Result} of discovery or installation. {@code null} if not found or installed. */ - Result discoverOrInstall(); + @Nullable + Result discoverOrInstall(String neoForgeVersion, Dist requiredDist); /** * The result of the discovery or installation. diff --git a/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedGameAndInstallationServiceProvider.java b/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedGameAndInstallationServiceProvider.java index b84c5692f..129a874d3 100644 --- a/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedGameAndInstallationServiceProvider.java +++ b/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedGameAndInstallationServiceProvider.java @@ -91,28 +91,36 @@ private static IdentifiableContent createInstallerClass( methodVisitor.visitEnd(); } { - methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "discoverOrInstall", "()Lnet/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService$Result;", null, null); + methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "discoverOrInstall", "(Ljava/lang/String;Lnet/neoforged/api/distmarker/Dist;)Lnet/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService$Result;", null, null); + { + annotationVisitor0 = methodVisitor.visitAnnotation("Lorg/jetbrains/annotations/Nullable;", false); + annotationVisitor0.visitEnd(); + } + { + annotationVisitor0 = methodVisitor.visitTypeAnnotation(335544320, null, "Lorg/jetbrains/annotations/Nullable;", false); + annotationVisitor0.visitEnd(); + } methodVisitor.visitCode(); Label label0 = new Label(); methodVisitor.visitLabel(label0); methodVisitor.visitLineNumber(18, label0); methodVisitor.visitLdcInsn("libraryDirectory"); methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "getProperty", "(Ljava/lang/String;)Ljava/lang/String;", false); - methodVisitor.visitVarInsn(ASTORE, 1); + methodVisitor.visitVarInsn(ASTORE, 3); Label label1 = new Label(); methodVisitor.visitLabel(label1); methodVisitor.visitLineNumber(19, label1); - methodVisitor.visitVarInsn(ALOAD, 1); + methodVisitor.visitVarInsn(ALOAD, 3); methodVisitor.visitInsn(ICONST_0); methodVisitor.visitTypeInsn(ANEWARRAY, "java/lang/String"); methodVisitor.visitMethodInsn(INVOKESTATIC, "java/nio/file/Path", "of", "(Ljava/lang/String;[Ljava/lang/String;)Ljava/nio/file/Path;", true); - methodVisitor.visitVarInsn(ASTORE, 2); + methodVisitor.visitVarInsn(ASTORE, 4); Label label2 = new Label(); methodVisitor.visitLabel(label2); methodVisitor.visitLineNumber(20, label2); methodVisitor.visitTypeInsn(NEW, "net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService$Result"); methodVisitor.visitInsn(DUP); - methodVisitor.visitVarInsn(ALOAD, 2); + methodVisitor.visitVarInsn(ALOAD, 4); methodVisitor.visitLdcInsn(relativePath); Label label3 = new Label(); methodVisitor.visitLabel(label3); @@ -125,10 +133,12 @@ private static IdentifiableContent createInstallerClass( methodVisitor.visitInsn(ARETURN); Label label5 = new Label(); methodVisitor.visitLabel(label5); - methodVisitor.visitLocalVariable("this", "L%s/%s;".formatted(packagePath, className), null, label0, label5, 0); - methodVisitor.visitLocalVariable("librariesDirectory", "Ljava/lang/String;", null, label1, label5, 1); - methodVisitor.visitLocalVariable("librariesRoot", "Ljava/nio/file/Path;", null, label2, label5, 2); - methodVisitor.visitMaxs(4, 3); + methodVisitor.visitLocalVariable("this", "L%s/%s;".formatted(packagePath,className), null, label0, label5, 0); + methodVisitor.visitLocalVariable("neoForgeVersion", "Ljava/lang/String;", null, label0, label5, 1); + methodVisitor.visitLocalVariable("requiredDist", "Lnet/neoforged/api/distmarker/Dist;", null, label0, label5, 2); + methodVisitor.visitLocalVariable("librariesDirectory", "Ljava/lang/String;", null, label1, label5, 3); + methodVisitor.visitLocalVariable("librariesRoot", "Ljava/nio/file/Path;", null, label2, label5, 4); + methodVisitor.visitMaxs(4, 5); methodVisitor.visitEnd(); } { From 279e2a2da93aa3946141ea4388760a498c1c1360 Mon Sep 17 00:00:00 2001 From: Marc Hermans Date: Sun, 30 Nov 2025 10:42:43 +0100 Subject: [PATCH 07/29] Introduce a better design for the running of the GameDiscovery Test --- .../neoforged/fml/loading/FMLLoaderTest.java | 74 ++++-- ...tedGameAndInstallationServiceProvider.java | 218 ------------------ .../fml/testlib/SimulatedInstallation.java | 75 +++--- 3 files changed, 99 insertions(+), 268 deletions(-) delete mode 100644 testlib/src/main/java/net/neoforged/fml/testlib/SimulatedGameAndInstallationServiceProvider.java diff --git a/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java b/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java index 9e2d77df2..9c330d3d1 100644 --- a/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java +++ b/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java @@ -5,18 +5,6 @@ package net.neoforged.fml.loading; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.nio.file.Files; -import java.util.List; -import java.util.Map; -import java.util.ServiceLoader; -import java.util.zip.ZipOutputStream; import net.neoforged.fml.FMLVersion; import net.neoforged.fml.ModLoader; import net.neoforged.fml.ModLoadingException; @@ -31,6 +19,7 @@ import net.neoforged.jarjar.metadata.ContainedJarIdentifier; import net.neoforged.jarjar.metadata.ContainedJarMetadata; import net.neoforged.jarjar.metadata.ContainedVersion; +import net.neoforged.neoforgespi.installation.GameDiscoveryOrInstallationService; import net.neoforged.neoforgespi.locating.IModFile; import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; import net.neoforged.neoforgespi.locating.IModFileReader; @@ -43,6 +32,27 @@ import org.junit.jupiter.params.provider.ValueSource; import org.lwjgl.system.FunctionProvider; +import java.nio.file.Files; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.zip.ZipOutputStream; + +import static net.neoforged.fml.testlib.SimulatedInstallation.CLIENT_ASSETS; +import static net.neoforged.fml.testlib.SimulatedInstallation.GAV_DYNAMIC_PATCHED_CLIENT; +import static net.neoforged.fml.testlib.SimulatedInstallation.GAV_NEOFORGE_DYNAMIC_INSTALLER; +import static net.neoforged.fml.testlib.SimulatedInstallation.MINECRAFT_MODS_TOML; +import static net.neoforged.fml.testlib.SimulatedInstallation.MINECRAFT_VERSION_JSON; +import static net.neoforged.fml.testlib.SimulatedInstallation.PATCHED_CLIENT; +import static net.neoforged.fml.testlib.SimulatedInstallation.RENAMED_SHARED; +import static net.neoforged.fml.testlib.SimulatedInstallation.SHARED_ASSETS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class FMLLoaderTest extends LauncherTest { private static final ContainedVersion JIJ_V1 = new ContainedVersion(VersionRange.createFromVersion("1.0"), new DefaultArtifactVersion("1.0")); @@ -69,6 +79,26 @@ void testProductionClientDiscovery() throws Exception { void testProductionClientWithDynamicInstallationDiscovery() throws Exception { installation.setupProductionClientWithDynamicInstallation(); + var patchedClientJar = installation.writeLibrary(GAV_DYNAMIC_PATCHED_CLIENT, PATCHED_CLIENT, RENAMED_SHARED, CLIENT_ASSETS, SHARED_ASSETS, MINECRAFT_MODS_TOML, MINECRAFT_VERSION_JSON); + var autoInstaller = installation.createLibraryProject(GAV_NEOFORGE_DYNAMIC_INSTALLER, builder -> { + builder.addClass("autoinstall.TestAutoInstaller", """ + import net.neoforged.api.distmarker.Dist; + import net.neoforged.neoforgespi.installation.GameDiscoveryOrInstallationService; + import java.nio.file.Path; + + public record TestAutoInstaller() implements GameDiscoveryOrInstallationService { + @Override public String name() { + return "test"; + } + @Override public Result discoverOrInstall(String neoForgeVersion, Dist requiredDist) { + return new Result(Path.of("%s")); + } + } + """.formatted(patchedClientJar.toAbsolutePath().toString())) + .addService(GameDiscoveryOrInstallationService.class, "autoinstall.TestAutoInstaller"); + }); + installation.getLaunchClasspath().add(autoInstaller); + var result = launchAndLoad("neoforgeclient"); assertThat(result.loadedMods()).containsOnlyKeys("minecraft", "neoforge"); assertThat(result.gameLayerModules()).containsOnlyKeys("minecraft", "neoforge"); @@ -369,8 +399,8 @@ void testLibraryIsLoaded(SimulatedInstallation.Type type) throws Exception { installation.setup(type); installation.buildInstallationAppropriateModProject("lib", "lib.jar", builder -> { builder.withManifest(Map.of( - "Automatic-Module-Name", "lib", - "FMLModType", "LIBRARY")) + "Automatic-Module-Name", "lib", + "FMLModType", "LIBRARY")) .addClass("lib.TestClass", "class TestClass {}"); }); var result = launchClient(); @@ -391,8 +421,8 @@ void testGameLibraryIsLoaded(SimulatedInstallation.Type type) throws Exception { installation.setup(type); installation.buildInstallationAppropriateModProject("gamelib", "gamelib.jar", builder -> { builder.withManifest(Map.of( - "Automatic-Module-Name", "gamelib", - "FMLModType", "GAMELIBRARY")) + "Automatic-Module-Name", "gamelib", + "FMLModType", "GAMELIBRARY")) .addClass("gamelib.TestClass", "class TestClass {}"); }); var result = launchClient(); @@ -543,7 +573,7 @@ public class Test implements org.lwjgl.system.FunctionProvider { public static java.util.ServiceLoader test() { return java.util.ServiceLoader.load(org.lwjgl.system.FunctionProvider.class); } - + @Override public long getFunctionAddress(java.nio.ByteBuffer functionName) { return 123; @@ -560,7 +590,7 @@ public class Test implements org.lwjgl.system.FunctionProvider { public static java.util.ServiceLoader test() { return java.util.ServiceLoader.load(org.lwjgl.system.FunctionProvider.class); } - + @Override public long getFunctionAddress(java.nio.ByteBuffer functionName) { return 123; @@ -871,7 +901,8 @@ public Thrower(net.neoforged.bus.api.IEventBus modEventBus) { var launchResult = launchInstalledDist(); assertThat(launchResult.loadedMods()).containsKey("testmod"); - var e = assertThrows(ModLoadingException.class, () -> ModLoader.dispatchParallelEvent("test", ModWorkManager.syncExecutor(), ModWorkManager.parallelExecutor(), () -> {}, FMLClientSetupEvent::new)); + var e = assertThrows(ModLoadingException.class, () -> ModLoader.dispatchParallelEvent("test", ModWorkManager.syncExecutor(), ModWorkManager.parallelExecutor(), () -> { + }, FMLClientSetupEvent::new)); assertThat(getTranslatedIssues(e.getIssues())).containsOnly( "ERROR: testmod (testmod) encountered an error while dispatching the net.neoforged.fml.event.lifecycle.FMLClientSetupEvent event\n" + exceptionType.getName() + ": Exception Message"); @@ -892,7 +923,8 @@ void testExceptionInInitTaskIsCollectedAsModLoadingIssue(Class ModLoader.runInitTask("test", ModWorkManager.syncExecutor(), () -> {}, () -> { + var e = assertThrows(ModLoadingException.class, () -> ModLoader.runInitTask("test", ModWorkManager.syncExecutor(), () -> { + }, () -> { Throwable exception; try { exception = exceptionType.getConstructor(String.class).newInstance("Exception Message"); @@ -944,7 +976,7 @@ public class DummyReader implements net.neoforged.neoforgespi.locating.IModFileR public DummyReader() throws Exception { Class.forName("net.minecraft.DetectedVersion"); } - + @Override public net.neoforged.neoforgespi.locating.IModFile read(net.neoforged.fml.jarcontents.JarContents jar, net.neoforged.neoforgespi.locating.ModFileDiscoveryAttributes attributes) { return null; diff --git a/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedGameAndInstallationServiceProvider.java b/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedGameAndInstallationServiceProvider.java deleted file mode 100644 index 129a874d3..000000000 --- a/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedGameAndInstallationServiceProvider.java +++ /dev/null @@ -1,218 +0,0 @@ -package net.neoforged.fml.testlib; - -import static org.objectweb.asm.Opcodes.ACC_FINAL; -import static org.objectweb.asm.Opcodes.ACC_PUBLIC; -import static org.objectweb.asm.Opcodes.ACC_RECORD; -import static org.objectweb.asm.Opcodes.ACC_STATIC; -import static org.objectweb.asm.Opcodes.ACC_SUPER; -import static org.objectweb.asm.Opcodes.ALOAD; -import static org.objectweb.asm.Opcodes.ANEWARRAY; -import static org.objectweb.asm.Opcodes.ARETURN; -import static org.objectweb.asm.Opcodes.ASTORE; -import static org.objectweb.asm.Opcodes.DUP; -import static org.objectweb.asm.Opcodes.ICONST_0; -import static org.objectweb.asm.Opcodes.INVOKEINTERFACE; -import static org.objectweb.asm.Opcodes.INVOKESPECIAL; -import static org.objectweb.asm.Opcodes.INVOKESTATIC; -import static org.objectweb.asm.Opcodes.IRETURN; -import static org.objectweb.asm.Opcodes.NEW; -import static org.objectweb.asm.Opcodes.RETURN; -import static org.objectweb.asm.Opcodes.V21; - -import java.util.Arrays; -import java.util.stream.Collectors; -import org.apache.commons.lang3.ArrayUtils; -import org.objectweb.asm.AnnotationVisitor; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.FieldVisitor; -import org.objectweb.asm.Handle; -import org.objectweb.asm.Label; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.RecordComponentVisitor; -import org.objectweb.asm.Type; - -public class SimulatedGameAndInstallationServiceProvider { - private static IdentifiableContent createInstallerClass( - InstallerInstance installerInstance) { - return createInstallerClass( - installerInstance.className, - installerInstance.packageName, - installerInstance.relativePath); - } - - private static IdentifiableContent createInstallerClass( - String className, - String packageName, - String relativePath) { - ClassWriter classWriter = new ClassWriter(0); - FieldVisitor fieldVisitor; - RecordComponentVisitor recordComponentVisitor; - MethodVisitor methodVisitor; - AnnotationVisitor annotationVisitor0; - - String packagePath = packageName.replace(".", "/"); - - classWriter.visit(V21, ACC_PUBLIC | ACC_FINAL | ACC_SUPER | ACC_RECORD, "%s/%s".formatted(packagePath, className), null, "java/lang/Record", new String[] { "net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService" }); - - classWriter.visitSource("%s.java".formatted(className), null); - - classWriter.visitInnerClass("net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService$Result", "net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService", "Result", ACC_PUBLIC | ACC_FINAL | ACC_STATIC); - - classWriter.visitInnerClass("java/lang/invoke/MethodHandles$Lookup", "java/lang/invoke/MethodHandles", "Lookup", ACC_PUBLIC | ACC_FINAL | ACC_STATIC); - - { - methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "", "()V", null, null); - methodVisitor.visitCode(); - Label label0 = new Label(); - methodVisitor.visitLabel(label0); - methodVisitor.visitLineNumber(10, label0); - methodVisitor.visitVarInsn(ALOAD, 0); - methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Record", "", "()V", false); - methodVisitor.visitInsn(RETURN); - Label label1 = new Label(); - methodVisitor.visitLabel(label1); - methodVisitor.visitLocalVariable("this", "L%s/%s;".formatted(packagePath, className), null, label0, label1, 0); - methodVisitor.visitMaxs(1, 1); - methodVisitor.visitEnd(); - } - { - methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "name", "()Ljava/lang/String;", null, null); - methodVisitor.visitCode(); - Label label0 = new Label(); - methodVisitor.visitLabel(label0); - methodVisitor.visitLineNumber(13, label0); - methodVisitor.visitLdcInsn("simulatedInstallation"); - methodVisitor.visitInsn(ARETURN); - Label label1 = new Label(); - methodVisitor.visitLabel(label1); - methodVisitor.visitLocalVariable("this", "L%s/%s;".formatted(packagePath, className), null, label0, label1, 0); - methodVisitor.visitMaxs(1, 1); - methodVisitor.visitEnd(); - } - { - methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "discoverOrInstall", "(Ljava/lang/String;Lnet/neoforged/api/distmarker/Dist;)Lnet/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService$Result;", null, null); - { - annotationVisitor0 = methodVisitor.visitAnnotation("Lorg/jetbrains/annotations/Nullable;", false); - annotationVisitor0.visitEnd(); - } - { - annotationVisitor0 = methodVisitor.visitTypeAnnotation(335544320, null, "Lorg/jetbrains/annotations/Nullable;", false); - annotationVisitor0.visitEnd(); - } - methodVisitor.visitCode(); - Label label0 = new Label(); - methodVisitor.visitLabel(label0); - methodVisitor.visitLineNumber(18, label0); - methodVisitor.visitLdcInsn("libraryDirectory"); - methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "getProperty", "(Ljava/lang/String;)Ljava/lang/String;", false); - methodVisitor.visitVarInsn(ASTORE, 3); - Label label1 = new Label(); - methodVisitor.visitLabel(label1); - methodVisitor.visitLineNumber(19, label1); - methodVisitor.visitVarInsn(ALOAD, 3); - methodVisitor.visitInsn(ICONST_0); - methodVisitor.visitTypeInsn(ANEWARRAY, "java/lang/String"); - methodVisitor.visitMethodInsn(INVOKESTATIC, "java/nio/file/Path", "of", "(Ljava/lang/String;[Ljava/lang/String;)Ljava/nio/file/Path;", true); - methodVisitor.visitVarInsn(ASTORE, 4); - Label label2 = new Label(); - methodVisitor.visitLabel(label2); - methodVisitor.visitLineNumber(20, label2); - methodVisitor.visitTypeInsn(NEW, "net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService$Result"); - methodVisitor.visitInsn(DUP); - methodVisitor.visitVarInsn(ALOAD, 4); - methodVisitor.visitLdcInsn(relativePath); - Label label3 = new Label(); - methodVisitor.visitLabel(label3); - methodVisitor.visitLineNumber(21, label3); - methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/nio/file/Path", "resolve", "(Ljava/lang/String;)Ljava/nio/file/Path;", true); - methodVisitor.visitMethodInsn(INVOKESPECIAL, "net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService$Result", "", "(Ljava/nio/file/Path;)V", false); - Label label4 = new Label(); - methodVisitor.visitLabel(label4); - methodVisitor.visitLineNumber(20, label4); - methodVisitor.visitInsn(ARETURN); - Label label5 = new Label(); - methodVisitor.visitLabel(label5); - methodVisitor.visitLocalVariable("this", "L%s/%s;".formatted(packagePath,className), null, label0, label5, 0); - methodVisitor.visitLocalVariable("neoForgeVersion", "Ljava/lang/String;", null, label0, label5, 1); - methodVisitor.visitLocalVariable("requiredDist", "Lnet/neoforged/api/distmarker/Dist;", null, label0, label5, 2); - methodVisitor.visitLocalVariable("librariesDirectory", "Ljava/lang/String;", null, label1, label5, 3); - methodVisitor.visitLocalVariable("librariesRoot", "Ljava/nio/file/Path;", null, label2, label5, 4); - methodVisitor.visitMaxs(4, 5); - methodVisitor.visitEnd(); - } - { - methodVisitor = classWriter.visitMethod(ACC_PUBLIC | ACC_FINAL, "toString", "()Ljava/lang/String;", null, null); - methodVisitor.visitCode(); - Label label0 = new Label(); - methodVisitor.visitLabel(label0); - methodVisitor.visitLineNumber(10, label0); - methodVisitor.visitVarInsn(ALOAD, 0); - methodVisitor.visitInvokeDynamicInsn("toString", "(L%s/%s;)Ljava/lang/String;".formatted(packagePath, className), new Handle(Opcodes.H_INVOKESTATIC, "java/lang/runtime/ObjectMethods", "bootstrap", "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;", false), new Object[] { Type.getType("L%s/%s;".formatted(packagePath, className)), "" }); - methodVisitor.visitInsn(ARETURN); - Label label1 = new Label(); - methodVisitor.visitLabel(label1); - methodVisitor.visitLocalVariable("this", "L%s/%s;".formatted(packagePath, className), null, label0, label1, 0); - methodVisitor.visitMaxs(1, 1); - methodVisitor.visitEnd(); - } - { - methodVisitor = classWriter.visitMethod(ACC_PUBLIC | ACC_FINAL, "hashCode", "()I", null, null); - methodVisitor.visitCode(); - Label label0 = new Label(); - methodVisitor.visitLabel(label0); - methodVisitor.visitLineNumber(10, label0); - methodVisitor.visitVarInsn(ALOAD, 0); - methodVisitor.visitInvokeDynamicInsn("hashCode", "(L%s/%s;)I".formatted(packagePath, className), new Handle(Opcodes.H_INVOKESTATIC, "java/lang/runtime/ObjectMethods", "bootstrap", "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;", false), new Object[] { Type.getType("L%s/%s;".formatted(packagePath, className)), "" }); - methodVisitor.visitInsn(IRETURN); - Label label1 = new Label(); - methodVisitor.visitLabel(label1); - methodVisitor.visitLocalVariable("this", "L%s/%s;".formatted(packagePath, className), null, label0, label1, 0); - methodVisitor.visitMaxs(1, 1); - methodVisitor.visitEnd(); - } - { - methodVisitor = classWriter.visitMethod(ACC_PUBLIC | ACC_FINAL, "equals", "(Ljava/lang/Object;)Z", null, null); - methodVisitor.visitCode(); - Label label0 = new Label(); - methodVisitor.visitLabel(label0); - methodVisitor.visitLineNumber(10, label0); - methodVisitor.visitVarInsn(ALOAD, 0); - methodVisitor.visitVarInsn(ALOAD, 1); - methodVisitor.visitInvokeDynamicInsn("equals", "(L%s/%s;Ljava/lang/Object;)Z".formatted(packagePath, className), new Handle(Opcodes.H_INVOKESTATIC, "java/lang/runtime/ObjectMethods", "bootstrap", "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;", false), new Object[] { Type.getType("L%s/%s;".formatted(packagePath, className)), "" }); - methodVisitor.visitInsn(IRETURN); - Label label1 = new Label(); - methodVisitor.visitLabel(label1); - methodVisitor.visitLocalVariable("this", "L%s/%s;".formatted(packagePath, className), null, label0, label1, 0); - methodVisitor.visitLocalVariable("o", "Ljava/lang/Object;", null, label0, label1, 1); - methodVisitor.visitMaxs(2, 2); - methodVisitor.visitEnd(); - } - classWriter.visitEnd(); - - return new IdentifiableContent("%s/%s.class".formatted(packageName, className), "%s/%s.class".formatted(packagePath, className), classWriter.toByteArray()); - } - - public record InstallerInstance( - String className, - String packageName, - String relativePath) {} - - public static IdentifiableContent[] create( - InstallerInstance... installers) { - var installerClasses = Arrays.stream(installers) - .map(SimulatedGameAndInstallationServiceProvider::createInstallerClass) - .toArray(IdentifiableContent[]::new); - - var serviceFileContent = Arrays.stream(installers) - .map(installer -> "%s.%s".formatted(installer.packageName, installer.className)) - .collect(Collectors.joining("\n")); - - var serviceFile = new IdentifiableContent( - "servicefile:net.neoforged.neoforgespi.installation.GameDiscoveryOrInstallationService", - "META-INF/services/net.neoforged.neoforgespi.installation.GameDiscoveryOrInstallationService", - serviceFileContent.getBytes()); - - return ArrayUtils.addAll(installerClasses, serviceFile); - } -} diff --git a/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedInstallation.java b/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedInstallation.java index 2df4f024a..62867886d 100644 --- a/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedInstallation.java +++ b/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedInstallation.java @@ -8,6 +8,14 @@ import com.google.common.io.MoreFiles; import com.google.common.io.RecursiveDeleteOption; import com.google.gson.JsonObject; +import net.neoforged.jarjar.metadata.ContainedJarMetadata; +import net.neoforged.jarjar.metadata.Metadata; +import net.neoforged.jarjar.metadata.MetadataIOHandler; +import net.neoforged.jarjar.selection.util.Constants; +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; + import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.File; @@ -25,6 +33,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; import java.util.jar.Attributes; import java.util.jar.JarEntry; import java.util.jar.JarFile; @@ -34,14 +43,6 @@ import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; -import net.neoforged.jarjar.metadata.ContainedJarMetadata; -import net.neoforged.jarjar.metadata.Metadata; -import net.neoforged.jarjar.metadata.MetadataIOHandler; -import net.neoforged.jarjar.selection.util.Constants; -import org.apache.commons.lang3.ArrayUtils; -import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.Opcodes; /** * Simulates various installation types for NeoForge @@ -134,16 +135,16 @@ public boolean isProduction() { public static final String NEOFORM_VERSION = "202401020304"; public static final IdentifiableContent MINECRAFT_VERSION_JSON = new IdentifiableContent("MC_VERSION_JSON", "version.json", buildVersionJson(MC_VERSION)); - public static final IdentifiableContent[] SERVER_EXTRA_JAR_CONTENT = { SHARED_ASSETS, MINECRAFT_VERSION_JSON }; - public static final IdentifiableContent[] CLIENT_EXTRA_JAR_CONTENT = { CLIENT_ASSETS, SHARED_ASSETS, RESOURCES_MANIFEST, MINECRAFT_VERSION_JSON }; - public static final IdentifiableContent[] NEOFORGE_UNIVERSAL_JAR_CONTENT = { NEOFORGE_ASSETS, NEOFORGE_CLIENT_CLASSES, NEOFORGE_CLASSES, NEOFORGE_MODS_TOML, NEOFORGE_MANIFEST }; - public static final IdentifiableContent[] USERDEV_CLIENT_JAR_CONTENT = { PATCHED_CLIENT, PATCHED_SHARED }; + public static final IdentifiableContent[] SERVER_EXTRA_JAR_CONTENT = {SHARED_ASSETS, MINECRAFT_VERSION_JSON}; + public static final IdentifiableContent[] CLIENT_EXTRA_JAR_CONTENT = {CLIENT_ASSETS, SHARED_ASSETS, RESOURCES_MANIFEST, MINECRAFT_VERSION_JSON}; + public static final IdentifiableContent[] NEOFORGE_UNIVERSAL_JAR_CONTENT = {NEOFORGE_ASSETS, NEOFORGE_CLIENT_CLASSES, NEOFORGE_CLASSES, NEOFORGE_MODS_TOML, NEOFORGE_MANIFEST}; + public static final IdentifiableContent[] USERDEV_CLIENT_JAR_CONTENT = {PATCHED_CLIENT, PATCHED_SHARED}; private static final String GAV_PATCHED_CLIENT = "net.neoforged:minecraft-client-patched:" + NEOFORGE_VERSION; - private static final String GAV_DYNAMIC_PATCHED_CLIENT = "net.neoforged-dynamic-install:minecraft-client-patched:" + NEOFORGE_VERSION; + public static final String GAV_DYNAMIC_PATCHED_CLIENT = "net.neoforged-dynamic-install:minecraft-client-patched:" + NEOFORGE_VERSION; private static final String GAV_PATCHED_SERVER = "net.neoforged:minecraft-server-patched:" + NEOFORGE_VERSION; private static final String GAV_NEOFORGE_UNIVERSAL = "net.neoforged:neoforge:" + NEOFORGE_VERSION + ":universal"; - private static final String GAV_NEOFORGE_DYNAMIC_INSTALLER = "net.neoforged:neoforge:" + NEOFORGE_VERSION + ":dynamic-installer"; + public static final String GAV_NEOFORGE_DYNAMIC_INSTALLER = "net.neoforged:neoforge:" + NEOFORGE_VERSION + ":dynamic-installer"; private static byte[] buildVersionJson(String mcVersion) { var obj = new JsonObject(); @@ -208,14 +209,7 @@ public void setup(Type type) throws IOException { case PRODUCTION_CLIENT_INSTALLED_AT_RUNTIME -> { System.setProperty(LIBRARIES_DIRECTORY_PROPERTY, librariesDir.toString()); - var patchedClientJar = writeLibrary(GAV_DYNAMIC_PATCHED_CLIENT, PATCHED_CLIENT, RENAMED_SHARED, CLIENT_ASSETS, SHARED_ASSETS, MINECRAFT_MODS_TOML, MINECRAFT_VERSION_JSON); var universalJar = writeLibrary(GAV_NEOFORGE_UNIVERSAL, NEOFORGE_UNIVERSAL_JAR_CONTENT); - var dynamicInstallerJar = writeLibrary(GAV_NEOFORGE_DYNAMIC_INSTALLER, SimulatedGameAndInstallationServiceProvider - .create( - new SimulatedGameAndInstallationServiceProvider.InstallerInstance( - "DynamicInstaller", - "net.neoforged.neoforge.installer", - librariesDir.relativize(patchedClientJar).toString()))); // For the production client, the Vanilla launcher puts the original, obfuscated client jar on the classpath // Since this can influence our detection logic, let's make sure it's included for the tests. @@ -231,7 +225,6 @@ public void setup(Type type) throws IOException { MINECRAFT_VERSION_JSON, SHARED_ASSETS, CLIENT_ASSETS); - launchClasspath.add(dynamicInstallerJar); componentRoots = InstallationComponents.productionJars(obfuscatedClientJar, universalJar); } @@ -369,7 +362,8 @@ protected record NeoForgeDevFolders( Path clientClassesDir, Path commonClassesDir, Path commonResourcesDir, - Path clientExtraJar) {} + Path clientExtraJar) { + } // Emulate the layout of a NeoForge development environment // In dev, the NeoForge sources itself are joined, but the Minecraft sources are not @@ -463,7 +457,7 @@ public static void addFilesToJar(Path jarFile, IdentifiableContent... content) t Set written = new HashSet<>(); var newJarFile = jarFile.resolveSibling(jarFile.getFileName() + ".new"); try (var jarIn = new JarFile(jarFile.toFile()); - var jarOut = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(newJarFile)))) { + var jarOut = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(newJarFile)))) { // Ensure the manifest is written first if (newManifest != null) { jarOut.putNextEntry(new ZipEntry(JarFile.MANIFEST_NAME)); @@ -525,7 +519,7 @@ private static byte[] writeNeoForgeManifest() { private static byte[] writeNeoForgeModsToml() { return """ license = "LICENSE" - + [[mods]] modId="neoforge" """.getBytes(); @@ -535,7 +529,7 @@ private static byte[] writeMinecraftModsToml() { return """ loader = "minecraft" license = "See Minecraft EULA" - + [[mods]] modId="minecraft" """.getBytes(); @@ -559,7 +553,7 @@ public static IdentifiableContent createModsToml(String modId, String version) { modLoader = "javafml" loaderVersion = "[3,]" license = "LICENSE" - + [[mods]] modId="%s" version="%s" @@ -572,11 +566,11 @@ public static IdentifiableContent createMultiModsToml(String modId, String versi modLoader = "javafml" loaderVersion = "[3,]" license = "LICENSE" - + [[mods]] modId="%s" version="%s" - + [[mods]] modId="%s" version="%s" @@ -688,6 +682,29 @@ public ModFileBuilder.ModJarBuilder buildModJar(String filename) throws IOExcept return ModFileBuilder.toJar(path); } + public Path createLibraryProject(String groupArtifactVersion, ModFileBuilder.ModJarCustomizer configurator) throws IOException { + String[] parts = groupArtifactVersion.split(":", 4); + return createLibraryProject(parts[0], parts[1], parts[2], parts.length > 3 ? parts[3] : null, configurator); + } + + public Path createLibraryProject(String group, String artifact, String version, @Nullable String classifier, ModFileBuilder.ModJarCustomizer configurator) throws IOException { + var folder = librariesDir.resolve(group.replace('.', '/')) + .resolve(artifact) + .resolve(version); + Files.createDirectories(folder); + + var filename = artifact + "-" + version; + if (classifier != null) { + filename += "-" + classifier; + } + filename += ".jar"; + var file = folder.resolve(filename); + + var builder = ModFileBuilder.toJar(file); + configurator.customize(builder); + return builder.build(); + } + /** * @param projectSubfolder Can be null to place output into the root project, but can also be a path relative * to the root project referring to the submodule. From 58a237282c88e0404bb4875961ae81a45605a04e Mon Sep 17 00:00:00 2001 From: Marc Hermans Date: Sun, 30 Nov 2025 10:54:42 +0100 Subject: [PATCH 08/29] Add exception handling to game discovery and installation process --- .../neoforged/fml/loading/game/GameDiscovery.java | 15 ++++++++++----- .../GameDiscoveryOrInstallationService.java | 2 +- .../net/neoforged/fml/loading/FMLLoaderTest.java | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java b/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java index ce1f4ea85..590ee9c65 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java +++ b/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java @@ -325,11 +325,16 @@ private static GameDiscoveryResult locateProductionMinecraft(String neoForgeVers if (!Files.exists(patchedMinecraftPath)) { if (gameDiscoveryOrInstallationService != null) { LOG.info("Patched minecraft does not exist. Triggering external discovery or installation service!"); - var result = gameDiscoveryOrInstallationService.discoverOrInstall(neoForgeVersion, requiredDist); - if (result != null) { - patchedMinecraftPath = result.minecraft(); - } else { - LOG.info("Game discovery or installation service: {} did not return a result. Skipping.", gameDiscoveryOrInstallationService.name()); + try { + var result = gameDiscoveryOrInstallationService.discoverOrInstall(neoForgeVersion, requiredDist); + if (result != null) { + patchedMinecraftPath = result.minecraft(); + } else { + LOG.info("Game discovery or installation service: {} did not return a result. Skipping.", gameDiscoveryOrInstallationService.name()); + } + } catch (Exception e) { + nfModFile.close(); + throw e; } } } diff --git a/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java b/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java index 92e2256b8..afaab00e5 100644 --- a/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java +++ b/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java @@ -49,7 +49,7 @@ public interface GameDiscoveryOrInstallationService { * @return The {@link Result} of discovery or installation. {@code null} if not found or installed. */ @Nullable - Result discoverOrInstall(String neoForgeVersion, Dist requiredDist); + Result discoverOrInstall(String neoForgeVersion, Dist requiredDist) throws Exception; /** * The result of the discovery or installation. diff --git a/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java b/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java index 9c330d3d1..39f13a8b2 100644 --- a/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java +++ b/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java @@ -90,7 +90,7 @@ public record TestAutoInstaller() implements GameDiscoveryOrInstallationService @Override public String name() { return "test"; } - @Override public Result discoverOrInstall(String neoForgeVersion, Dist requiredDist) { + @Override public Result discoverOrInstall(String neoForgeVersion, Dist requiredDist) throws Exception { return new Result(Path.of("%s")); } } From eb4f549005e5e7aba799b04c0b1309f861de7a0e Mon Sep 17 00:00:00 2001 From: Marc Hermans Date: Sun, 30 Nov 2025 10:58:04 +0100 Subject: [PATCH 09/29] Properly handle the exception. --- .../java/net/neoforged/fml/loading/game/GameDiscovery.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java b/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java index 590ee9c65..ce6a701c8 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java +++ b/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java @@ -332,9 +332,9 @@ private static GameDiscoveryResult locateProductionMinecraft(String neoForgeVers } else { LOG.info("Game discovery or installation service: {} did not return a result. Skipping.", gameDiscoveryOrInstallationService.name()); } - } catch (Exception e) { + } catch (Throwable e) { nfModFile.close(); - throw e; + throw new ModLoadingException(ModLoadingIssue.error("fml.modloadingissue.discovery_service_failure").withCause(e).withSeverity(ModLoadingIssue.Severity.ERROR)); } } } From 090bd38ee299e1448de3dab9d3fcc83d0a5e052b Mon Sep 17 00:00:00 2001 From: Marc Hermans Date: Sun, 30 Nov 2025 11:44:53 +0100 Subject: [PATCH 10/29] Fix the license --- .../installation/GameDiscoveryOrInstallationService.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java b/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java index afaab00e5..40bc93c5d 100644 --- a/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java +++ b/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.neoforgespi.installation; import net.neoforged.api.distmarker.Dist; From ea954747a7f034322fe297460e8c0f43aad3465c Mon Sep 17 00:00:00 2001 From: Marc Hermans Date: Sun, 30 Nov 2025 11:48:38 +0100 Subject: [PATCH 11/29] Update the documentation. --- .../installation/GameDiscoveryOrInstallationService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java b/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java index 40bc93c5d..f045d4704 100644 --- a/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java +++ b/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java @@ -18,7 +18,7 @@ *

*

* The first step will be to validate the libraries folder, and then find the relevant neoforge jar - * in their. Secondly it will then parse its neoforge.mods.toml and prepare the launch. + * in there. Secondly it will then parse its neoforge.mods.toml and prepare the launch. *

*

* When that all succeeds the game tries to find the relevant patched minecraft jar in the folder as well, @@ -37,7 +37,7 @@ *

*

* If the launch argument 'fml.disableInstaller' is provided then this entire subsystem is disabled and the loader - * will not try to invoke or even discover and instantiate implementation of this type. + * will not try to invoke or even discover and instantiate implementations of this type. *

*/ public interface GameDiscoveryOrInstallationService { From 693c1dbe6a347057007ee126a066279ac0920c87 Mon Sep 17 00:00:00 2001 From: Marc Hermans Date: Sun, 30 Nov 2025 11:50:21 +0100 Subject: [PATCH 12/29] Fix immaculate. --- .../GameDiscoveryOrInstallationService.java | 3 +- .../neoforged/fml/loading/FMLLoaderTest.java | 87 +++++++++---------- .../fml/testlib/SimulatedInstallation.java | 39 ++++----- 3 files changed, 61 insertions(+), 68 deletions(-) diff --git a/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java b/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java index f045d4704..bf74938ad 100644 --- a/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java +++ b/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java @@ -5,11 +5,10 @@ package net.neoforged.neoforgespi.installation; +import java.nio.file.Path; import net.neoforged.api.distmarker.Dist; import org.jetbrains.annotations.Nullable; -import java.nio.file.Path; - /** * A service which can be used to discover or install the game at runtime. *

diff --git a/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java b/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java index 39f13a8b2..6c6e81be1 100644 --- a/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java +++ b/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java @@ -5,6 +5,26 @@ package net.neoforged.fml.loading; +import static net.neoforged.fml.testlib.SimulatedInstallation.CLIENT_ASSETS; +import static net.neoforged.fml.testlib.SimulatedInstallation.GAV_DYNAMIC_PATCHED_CLIENT; +import static net.neoforged.fml.testlib.SimulatedInstallation.GAV_NEOFORGE_DYNAMIC_INSTALLER; +import static net.neoforged.fml.testlib.SimulatedInstallation.MINECRAFT_MODS_TOML; +import static net.neoforged.fml.testlib.SimulatedInstallation.MINECRAFT_VERSION_JSON; +import static net.neoforged.fml.testlib.SimulatedInstallation.PATCHED_CLIENT; +import static net.neoforged.fml.testlib.SimulatedInstallation.RENAMED_SHARED; +import static net.neoforged.fml.testlib.SimulatedInstallation.SHARED_ASSETS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.zip.ZipOutputStream; import net.neoforged.fml.FMLVersion; import net.neoforged.fml.ModLoader; import net.neoforged.fml.ModLoadingException; @@ -32,27 +52,6 @@ import org.junit.jupiter.params.provider.ValueSource; import org.lwjgl.system.FunctionProvider; -import java.nio.file.Files; -import java.util.List; -import java.util.Map; -import java.util.ServiceLoader; -import java.util.zip.ZipOutputStream; - -import static net.neoforged.fml.testlib.SimulatedInstallation.CLIENT_ASSETS; -import static net.neoforged.fml.testlib.SimulatedInstallation.GAV_DYNAMIC_PATCHED_CLIENT; -import static net.neoforged.fml.testlib.SimulatedInstallation.GAV_NEOFORGE_DYNAMIC_INSTALLER; -import static net.neoforged.fml.testlib.SimulatedInstallation.MINECRAFT_MODS_TOML; -import static net.neoforged.fml.testlib.SimulatedInstallation.MINECRAFT_VERSION_JSON; -import static net.neoforged.fml.testlib.SimulatedInstallation.PATCHED_CLIENT; -import static net.neoforged.fml.testlib.SimulatedInstallation.RENAMED_SHARED; -import static net.neoforged.fml.testlib.SimulatedInstallation.SHARED_ASSETS; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - public class FMLLoaderTest extends LauncherTest { private static final ContainedVersion JIJ_V1 = new ContainedVersion(VersionRange.createFromVersion("1.0"), new DefaultArtifactVersion("1.0")); @@ -82,19 +81,19 @@ void testProductionClientWithDynamicInstallationDiscovery() throws Exception { var patchedClientJar = installation.writeLibrary(GAV_DYNAMIC_PATCHED_CLIENT, PATCHED_CLIENT, RENAMED_SHARED, CLIENT_ASSETS, SHARED_ASSETS, MINECRAFT_MODS_TOML, MINECRAFT_VERSION_JSON); var autoInstaller = installation.createLibraryProject(GAV_NEOFORGE_DYNAMIC_INSTALLER, builder -> { builder.addClass("autoinstall.TestAutoInstaller", """ - import net.neoforged.api.distmarker.Dist; - import net.neoforged.neoforgespi.installation.GameDiscoveryOrInstallationService; - import java.nio.file.Path; - - public record TestAutoInstaller() implements GameDiscoveryOrInstallationService { - @Override public String name() { - return "test"; - } - @Override public Result discoverOrInstall(String neoForgeVersion, Dist requiredDist) throws Exception { - return new Result(Path.of("%s")); - } - } - """.formatted(patchedClientJar.toAbsolutePath().toString())) + import net.neoforged.api.distmarker.Dist; + import net.neoforged.neoforgespi.installation.GameDiscoveryOrInstallationService; + import java.nio.file.Path; + + public record TestAutoInstaller() implements GameDiscoveryOrInstallationService { + @Override public String name() { + return "test"; + } + @Override public Result discoverOrInstall(String neoForgeVersion, Dist requiredDist) throws Exception { + return new Result(Path.of("%s")); + } + } + """.formatted(patchedClientJar.toAbsolutePath().toString())) .addService(GameDiscoveryOrInstallationService.class, "autoinstall.TestAutoInstaller"); }); installation.getLaunchClasspath().add(autoInstaller); @@ -399,8 +398,8 @@ void testLibraryIsLoaded(SimulatedInstallation.Type type) throws Exception { installation.setup(type); installation.buildInstallationAppropriateModProject("lib", "lib.jar", builder -> { builder.withManifest(Map.of( - "Automatic-Module-Name", "lib", - "FMLModType", "LIBRARY")) + "Automatic-Module-Name", "lib", + "FMLModType", "LIBRARY")) .addClass("lib.TestClass", "class TestClass {}"); }); var result = launchClient(); @@ -421,8 +420,8 @@ void testGameLibraryIsLoaded(SimulatedInstallation.Type type) throws Exception { installation.setup(type); installation.buildInstallationAppropriateModProject("gamelib", "gamelib.jar", builder -> { builder.withManifest(Map.of( - "Automatic-Module-Name", "gamelib", - "FMLModType", "GAMELIBRARY")) + "Automatic-Module-Name", "gamelib", + "FMLModType", "GAMELIBRARY")) .addClass("gamelib.TestClass", "class TestClass {}"); }); var result = launchClient(); @@ -573,7 +572,7 @@ public class Test implements org.lwjgl.system.FunctionProvider { public static java.util.ServiceLoader test() { return java.util.ServiceLoader.load(org.lwjgl.system.FunctionProvider.class); } - + @Override public long getFunctionAddress(java.nio.ByteBuffer functionName) { return 123; @@ -590,7 +589,7 @@ public class Test implements org.lwjgl.system.FunctionProvider { public static java.util.ServiceLoader test() { return java.util.ServiceLoader.load(org.lwjgl.system.FunctionProvider.class); } - + @Override public long getFunctionAddress(java.nio.ByteBuffer functionName) { return 123; @@ -901,8 +900,7 @@ public Thrower(net.neoforged.bus.api.IEventBus modEventBus) { var launchResult = launchInstalledDist(); assertThat(launchResult.loadedMods()).containsKey("testmod"); - var e = assertThrows(ModLoadingException.class, () -> ModLoader.dispatchParallelEvent("test", ModWorkManager.syncExecutor(), ModWorkManager.parallelExecutor(), () -> { - }, FMLClientSetupEvent::new)); + var e = assertThrows(ModLoadingException.class, () -> ModLoader.dispatchParallelEvent("test", ModWorkManager.syncExecutor(), ModWorkManager.parallelExecutor(), () -> {}, FMLClientSetupEvent::new)); assertThat(getTranslatedIssues(e.getIssues())).containsOnly( "ERROR: testmod (testmod) encountered an error while dispatching the net.neoforged.fml.event.lifecycle.FMLClientSetupEvent event\n" + exceptionType.getName() + ": Exception Message"); @@ -923,8 +921,7 @@ void testExceptionInInitTaskIsCollectedAsModLoadingIssue(Class ModLoader.runInitTask("test", ModWorkManager.syncExecutor(), () -> { - }, () -> { + var e = assertThrows(ModLoadingException.class, () -> ModLoader.runInitTask("test", ModWorkManager.syncExecutor(), () -> {}, () -> { Throwable exception; try { exception = exceptionType.getConstructor(String.class).newInstance("Exception Message"); @@ -976,7 +973,7 @@ public class DummyReader implements net.neoforged.neoforgespi.locating.IModFileR public DummyReader() throws Exception { Class.forName("net.minecraft.DetectedVersion"); } - + @Override public net.neoforged.neoforgespi.locating.IModFile read(net.neoforged.fml.jarcontents.JarContents jar, net.neoforged.neoforgespi.locating.ModFileDiscoveryAttributes attributes) { return null; diff --git a/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedInstallation.java b/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedInstallation.java index 62867886d..5f68442e0 100644 --- a/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedInstallation.java +++ b/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedInstallation.java @@ -8,14 +8,6 @@ import com.google.common.io.MoreFiles; import com.google.common.io.RecursiveDeleteOption; import com.google.gson.JsonObject; -import net.neoforged.jarjar.metadata.ContainedJarMetadata; -import net.neoforged.jarjar.metadata.Metadata; -import net.neoforged.jarjar.metadata.MetadataIOHandler; -import net.neoforged.jarjar.selection.util.Constants; -import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.Opcodes; - import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.File; @@ -33,7 +25,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.Consumer; import java.util.jar.Attributes; import java.util.jar.JarEntry; import java.util.jar.JarFile; @@ -43,6 +34,13 @@ import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; +import net.neoforged.jarjar.metadata.ContainedJarMetadata; +import net.neoforged.jarjar.metadata.Metadata; +import net.neoforged.jarjar.metadata.MetadataIOHandler; +import net.neoforged.jarjar.selection.util.Constants; +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; /** * Simulates various installation types for NeoForge @@ -135,10 +133,10 @@ public boolean isProduction() { public static final String NEOFORM_VERSION = "202401020304"; public static final IdentifiableContent MINECRAFT_VERSION_JSON = new IdentifiableContent("MC_VERSION_JSON", "version.json", buildVersionJson(MC_VERSION)); - public static final IdentifiableContent[] SERVER_EXTRA_JAR_CONTENT = {SHARED_ASSETS, MINECRAFT_VERSION_JSON}; - public static final IdentifiableContent[] CLIENT_EXTRA_JAR_CONTENT = {CLIENT_ASSETS, SHARED_ASSETS, RESOURCES_MANIFEST, MINECRAFT_VERSION_JSON}; - public static final IdentifiableContent[] NEOFORGE_UNIVERSAL_JAR_CONTENT = {NEOFORGE_ASSETS, NEOFORGE_CLIENT_CLASSES, NEOFORGE_CLASSES, NEOFORGE_MODS_TOML, NEOFORGE_MANIFEST}; - public static final IdentifiableContent[] USERDEV_CLIENT_JAR_CONTENT = {PATCHED_CLIENT, PATCHED_SHARED}; + public static final IdentifiableContent[] SERVER_EXTRA_JAR_CONTENT = { SHARED_ASSETS, MINECRAFT_VERSION_JSON }; + public static final IdentifiableContent[] CLIENT_EXTRA_JAR_CONTENT = { CLIENT_ASSETS, SHARED_ASSETS, RESOURCES_MANIFEST, MINECRAFT_VERSION_JSON }; + public static final IdentifiableContent[] NEOFORGE_UNIVERSAL_JAR_CONTENT = { NEOFORGE_ASSETS, NEOFORGE_CLIENT_CLASSES, NEOFORGE_CLASSES, NEOFORGE_MODS_TOML, NEOFORGE_MANIFEST }; + public static final IdentifiableContent[] USERDEV_CLIENT_JAR_CONTENT = { PATCHED_CLIENT, PATCHED_SHARED }; private static final String GAV_PATCHED_CLIENT = "net.neoforged:minecraft-client-patched:" + NEOFORGE_VERSION; public static final String GAV_DYNAMIC_PATCHED_CLIENT = "net.neoforged-dynamic-install:minecraft-client-patched:" + NEOFORGE_VERSION; @@ -362,8 +360,7 @@ protected record NeoForgeDevFolders( Path clientClassesDir, Path commonClassesDir, Path commonResourcesDir, - Path clientExtraJar) { - } + Path clientExtraJar) {} // Emulate the layout of a NeoForge development environment // In dev, the NeoForge sources itself are joined, but the Minecraft sources are not @@ -457,7 +454,7 @@ public static void addFilesToJar(Path jarFile, IdentifiableContent... content) t Set written = new HashSet<>(); var newJarFile = jarFile.resolveSibling(jarFile.getFileName() + ".new"); try (var jarIn = new JarFile(jarFile.toFile()); - var jarOut = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(newJarFile)))) { + var jarOut = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(newJarFile)))) { // Ensure the manifest is written first if (newManifest != null) { jarOut.putNextEntry(new ZipEntry(JarFile.MANIFEST_NAME)); @@ -519,7 +516,7 @@ private static byte[] writeNeoForgeManifest() { private static byte[] writeNeoForgeModsToml() { return """ license = "LICENSE" - + [[mods]] modId="neoforge" """.getBytes(); @@ -529,7 +526,7 @@ private static byte[] writeMinecraftModsToml() { return """ loader = "minecraft" license = "See Minecraft EULA" - + [[mods]] modId="minecraft" """.getBytes(); @@ -553,7 +550,7 @@ public static IdentifiableContent createModsToml(String modId, String version) { modLoader = "javafml" loaderVersion = "[3,]" license = "LICENSE" - + [[mods]] modId="%s" version="%s" @@ -566,11 +563,11 @@ public static IdentifiableContent createMultiModsToml(String modId, String versi modLoader = "javafml" loaderVersion = "[3,]" license = "LICENSE" - + [[mods]] modId="%s" version="%s" - + [[mods]] modId="%s" version="%s" From 34ee39eb4d62c47f497e18a1bc7ea4d48f9ec693 Mon Sep 17 00:00:00 2001 From: Marc Hermans Date: Mon, 1 Dec 2025 11:07:54 +0100 Subject: [PATCH 13/29] Introduce support for cause listing in the log when a mod loading exception with a cause is thrown. --- .../neoforged/fml/ModLoadingException.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/loader/src/main/java/net/neoforged/fml/ModLoadingException.java b/loader/src/main/java/net/neoforged/fml/ModLoadingException.java index f51990040..afaf2d3a1 100644 --- a/loader/src/main/java/net/neoforged/fml/ModLoadingException.java +++ b/loader/src/main/java/net/neoforged/fml/ModLoadingException.java @@ -6,6 +6,8 @@ package net.neoforged.fml; import java.util.List; +import java.util.Objects; + import net.neoforged.fml.i18n.FMLTranslations; public class ModLoadingException extends RuntimeException { @@ -57,4 +59,30 @@ private void appendIssue(ModLoadingIssue issue, StringBuilder result) { result.append("\t- ").append(translation).append("\n"); } + + @Override + public synchronized Throwable getCause() { + //First get all issues which are errored, and get their first cause. + for (ModLoadingIssue i : issues) { + if (i.severity() == ModLoadingIssue.Severity.ERROR) { + Throwable cause = i.cause(); + if (cause != null) { + return cause; + } + } + } + + //If we have no errors then check the warnings. + for (ModLoadingIssue i : issues) { + if (i.severity() == ModLoadingIssue.Severity.WARNING) { + Throwable cause = i.cause(); + if (cause != null) { + return cause; + } + } + } + + //No cause known. + return null; + } } From 2b41bfc32d85f7e33dff8386745c94a37e9bf696 Mon Sep 17 00:00:00 2001 From: Marc Hermans Date: Mon, 1 Dec 2025 11:08:47 +0100 Subject: [PATCH 14/29] Remove the NeoForge version from the game discovery and installation interface. --- .../java/net/neoforged/fml/loading/game/GameDiscovery.java | 2 +- .../installation/GameDiscoveryOrInstallationService.java | 3 +-- .../src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java b/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java index ce6a701c8..c27313393 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java +++ b/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java @@ -326,7 +326,7 @@ private static GameDiscoveryResult locateProductionMinecraft(String neoForgeVers if (gameDiscoveryOrInstallationService != null) { LOG.info("Patched minecraft does not exist. Triggering external discovery or installation service!"); try { - var result = gameDiscoveryOrInstallationService.discoverOrInstall(neoForgeVersion, requiredDist); + var result = gameDiscoveryOrInstallationService.discoverOrInstall(requiredDist); if (result != null) { patchedMinecraftPath = result.minecraft(); } else { diff --git a/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java b/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java index bf74938ad..9505f18c5 100644 --- a/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java +++ b/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java @@ -48,12 +48,11 @@ public interface GameDiscoveryOrInstallationService { /** * Invoked to discover or install the game when it is not found in the libraries folder. * - * @param neoForgeVersion The version of neoforge for which the discovery or installation should be started. * @param requiredDist The distribution which should be discovered or installed. * @return The {@link Result} of discovery or installation. {@code null} if not found or installed. */ @Nullable - Result discoverOrInstall(String neoForgeVersion, Dist requiredDist) throws Exception; + Result discoverOrInstall(Dist requiredDist) throws Exception; /** * The result of the discovery or installation. diff --git a/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java b/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java index 6c6e81be1..16f0cddea 100644 --- a/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java +++ b/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java @@ -89,7 +89,7 @@ public record TestAutoInstaller() implements GameDiscoveryOrInstallationService @Override public String name() { return "test"; } - @Override public Result discoverOrInstall(String neoForgeVersion, Dist requiredDist) throws Exception { + @Override public Result discoverOrInstall(Dist requiredDist) throws Exception { return new Result(Path.of("%s")); } } From 0b7358579f131bdb343900d09bc3bcedeb4a8803 Mon Sep 17 00:00:00 2001 From: Marc Hermans Date: Mon, 1 Dec 2025 11:19:21 +0100 Subject: [PATCH 15/29] Fix formatting and introduce better logging for ModLoadingExceptions when logged to the console or log files by logging a cause. --- loader/src/main/java/net/neoforged/fml/ModLoadingException.java | 2 -- .../installation/GameDiscoveryOrInstallationService.java | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/loader/src/main/java/net/neoforged/fml/ModLoadingException.java b/loader/src/main/java/net/neoforged/fml/ModLoadingException.java index afaf2d3a1..c5b79ea4f 100644 --- a/loader/src/main/java/net/neoforged/fml/ModLoadingException.java +++ b/loader/src/main/java/net/neoforged/fml/ModLoadingException.java @@ -6,8 +6,6 @@ package net.neoforged.fml; import java.util.List; -import java.util.Objects; - import net.neoforged.fml.i18n.FMLTranslations; public class ModLoadingException extends RuntimeException { diff --git a/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java b/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java index 9505f18c5..8c9d9c7e7 100644 --- a/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java +++ b/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java @@ -48,7 +48,7 @@ public interface GameDiscoveryOrInstallationService { /** * Invoked to discover or install the game when it is not found in the libraries folder. * - * @param requiredDist The distribution which should be discovered or installed. + * @param requiredDist The distribution which should be discovered or installed. * @return The {@link Result} of discovery or installation. {@code null} if not found or installed. */ @Nullable From 805b0ef8ee3a0d7cff45bb0b3ac809ed770b79be Mon Sep 17 00:00:00 2001 From: Marc Hermans Date: Fri, 2 Jan 2026 15:10:11 +0100 Subject: [PATCH 16/29] Address needed and requested Changes. --- .../net/neoforged/fml/loading/FMLLoader.java | 20 +++++----- .../fml/loading/game/GameDiscovery.java | 38 +++++++------------ 2 files changed, 23 insertions(+), 35 deletions(-) diff --git a/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java b/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java index c90b49fa9..f2672cbab 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java +++ b/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java @@ -155,17 +155,19 @@ public boolean hasErrors() { } private FMLLoader(LocatedPaths locatedPaths, - ClassLoaderStack classLoaderStack, - GameDiscoveryResult discoveredGame, - ProgramArgs programArgs, - Dist dist, - Path gameDir) { + ClassLoaderStack classLoaderStack, + List earlyServiceJars, + GameDiscoveryResult discoveredGame, + ProgramArgs programArgs, + Dist dist, + Path gameDir) { this.locatedPaths = locatedPaths; this.classLoaderStack = classLoaderStack; this.discoveredGame = discoveredGame; this.programArgs = programArgs; this.dist = dist; this.gameDir = gameDir; + this.earlyServicesJars.addAll(earlyServiceJars); makeCurrent(); } @@ -316,7 +318,7 @@ public boolean addLocated(Path path) { var gameInstallationService = discoverGameInstaller(locatedPaths, programArgs); - var discoveredGame = runLongRunning(startupArgs, () -> GameDiscovery.discoverGame(programArgs, locatedPaths, dist, gameInstallationService)); + var discoveredGame = runLongRunning(startupArgs, () -> GameDiscovery.discoverGame(locatedPaths, dist, gameInstallationService)); var neoForgeVersion = discoveredGame.neoforge().getModFileInfo().versionString(); var minecraftVersion = discoveredGame.minecraft().getModFileInfo().versionString(); @@ -324,10 +326,8 @@ public boolean addLocated(Path path) { ImmediateWindowHandler.setNeoForgeVersion(neoForgeVersion); ImmediateWindowHandler.setMinecraftVersion(minecraftVersion); - var loader = new FMLLoader(locatedPaths, classLoaderStack, discoveredGame, programArgs, dist, startupArgs.gameDirectory()); + var loader = new FMLLoader(locatedPaths, classLoaderStack, earlyServiceJars, discoveredGame, programArgs, dist, startupArgs.gameDirectory()); try { - loader.earlyServicesJars.addAll(earlyServiceJars); - var discoveryResult = runLongRunning(startupArgs, loader::runDiscovery); for (var issue : discoveryResult.discoveryIssues()) { LOGGER.atLevel(issue.severity() == ModLoadingIssue.Severity.ERROR ? Level.ERROR : Level.WARN) @@ -538,7 +538,7 @@ private static String getModuleNameList(Configuration cf, List loadEarlyServices(ClassLoaderStack classLoaderStack, StartupArgs startupArgs) { // Search for early services - var earlyServicesJars = new ArrayList<>(EarlyServiceDiscovery.findEarlyServiceJars(startupArgs, FMLPaths.MODSDIR.get())); + var earlyServicesJars = EarlyServiceDiscovery.findEarlyServiceJars(startupArgs, FMLPaths.MODSDIR.get()); if (!earlyServicesJars.isEmpty()) { classLoaderStack.appendLoader("FML Early Services", earlyServicesJars.stream().map(IModFile::getContents).toList()); } diff --git a/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java b/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java index 082a37b91..615f9345b 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java +++ b/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java @@ -24,7 +24,6 @@ import net.neoforged.fml.ModLoadingIssue; import net.neoforged.fml.jarcontents.JarContents; import net.neoforged.fml.loading.MavenCoordinate; -import net.neoforged.fml.loading.ProgramArgs; import net.neoforged.fml.loading.moddiscovery.ModFile; import net.neoforged.fml.loading.moddiscovery.ModJarMetadata; import net.neoforged.fml.loading.moddiscovery.locators.NeoForgeDevDistCleaner; @@ -35,6 +34,7 @@ import net.neoforged.neoforgespi.installation.GameDiscoveryOrInstallationService; import net.neoforged.neoforgespi.language.IModInfo; import net.neoforged.neoforgespi.locating.IModFile; +import net.neoforged.neoforgespi.locating.IncompatibleFileReporting; import net.neoforged.neoforgespi.locating.ModFileDiscoveryAttributes; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -50,14 +50,10 @@ public final class GameDiscovery { private GameDiscovery() {} - public static GameDiscoveryResult discoverGame(ProgramArgs programArgs, LocatedPaths locatedPaths, Dist requiredDist, @Nullable GameDiscoveryOrInstallationService gameDiscoveryOrInstallationService) { + public static GameDiscoveryResult discoverGame(LocatedPaths locatedPaths, Dist requiredDist, @Nullable GameDiscoveryOrInstallationService gameDiscoveryOrInstallationService) { var ourCl = Thread.currentThread().getContextClassLoader(); - var neoForgeVersion = getNeoForgeVersion(programArgs, ourCl); - - programArgs.remove("fml.neoForgeVersion"); - programArgs.remove("fml.neoFormVersion"); // Remove legacy arguments - programArgs.remove("fml.mcVersion"); // Remove legacy arguments + var neoForgeVersion = getNeoForgeVersion(ourCl); // 0) Vanilla Launcher puts the obfuscated jar on the classpath. We mark it as claimed to prevent it from // being hoisted into a module, occupying the entrypoint packages. @@ -80,18 +76,16 @@ public static GameDiscoveryResult discoverGame(ProgramArgs programArgs, LocatedP return locateProductionMinecraft(neoForgeVersion, requiredDist, gameDiscoveryOrInstallationService); } - private static String getNeoForgeVersion(ProgramArgs programArgs, ClassLoader ourCl) { + private static String getNeoForgeVersion(ClassLoader ourCl) { var neoForgeVersion = getNeoForgeVersionFromClasspath(ourCl); + if (neoForgeVersion == null) { - neoForgeVersion = programArgs.get("fml.neoForgeVersion"); - if (neoForgeVersion == null) { - LOG.error("NeoForge version must be known to launch FML, in normal environments this is set as a command-line option (--fml.neoForgeVersion)"); - throw new ModLoadingException(ModLoadingIssue.error("fml.modloadingissue.corrupted_installation")); - } - LOG.debug("Using NeoForge version found on commandline: {}", neoForgeVersion); - } else { - LOG.debug("Using NeoForge version found on classpath: {}", neoForgeVersion); + LOG.error("Could not load the NeoForge version from the version properties file."); + throw new ModLoadingException(ModLoadingIssue.error("fml.modloadingissue.corrupted_installation")); } + + LOG.debug("Locate NeoForge version: {}", neoForgeVersion); + return neoForgeVersion; } @@ -110,11 +104,6 @@ private static String getNeoForgeVersionFromClasspath(ClassLoader classLoader) { } } - @Nullable - private static ModFile readModFile(JarContents jarContents) { - return (ModFile) new JarModsDotTomlModFileReader().read(jarContents, ModFileDiscoveryAttributes.DEFAULT); - } - private static GameDiscoveryResult handleMergedMinecraftAndNeoForgeJar(Dist requiredDist, LocatedPaths locatedPaths, RequiredSystemFiles systemFiles) { LOG.info("Detected a joined NeoForge and Minecraft configuration. Applying filtering..."); @@ -122,7 +111,7 @@ private static GameDiscoveryResult handleMergedMinecraftAndNeoForgeJar(Dist requ ModFile minecraftModFile; if (mcJarContents.containsFile("META-INF/neoforged.mods.toml")) { // In this branch, the jar already has a neoforge.mods.toml - minecraftModFile = readModFile(mcJarContents); + minecraftModFile = (ModFile) new JarModsDotTomlModFileReader().read(mcJarContents, ModFileDiscoveryAttributes.DEFAULT); if (minecraftModFile == null) { throw new ModLoadingException(ModLoadingIssue.error("fml.modloadingissue.corrupted_minecraft_jar")); } @@ -178,8 +167,7 @@ private static JarContents getCombinedMinecraftJar(Dist requiredDist, RequiredSy + systemFiles.getCommonResources() + " and " + systemFiles.getNeoForgeResources()); } - var mcJarRoots = new ArrayList(); - mcJarRoots.addAll(getMinecraftResourcesRoots(requiredDist, systemFiles)); + var mcJarRoots = new ArrayList<>(getMinecraftResourcesRoots(requiredDist, systemFiles)); JarContents.PathFilter mcClassesFilter = relativePath -> { if (relativePath.endsWith(".class")) { @@ -401,7 +389,7 @@ private static void preventLoadingOfObfuscatedClientJar(LocatedPaths locatedPath } LOG.info("Marking unmodified client jar as claimed to prevent loading: {}", jarPath); - context.addLocated(jarPath); + locatedPaths.addLocated(jarPath); return; } catch (IOException ignored) {} } From 0ae4b67b8c913bb253cde87f28a2b068993ee9f7 Mon Sep 17 00:00:00 2001 From: Marc Hermans Date: Fri, 2 Jan 2026 19:41:07 +0100 Subject: [PATCH 17/29] Port to a direct variant --- gradle.properties | 1 + loader/build.gradle | 1 + .../fml/loading/EarlyServiceDiscovery.java | 4 +- .../net/neoforged/fml/loading/FMLLoader.java | 40 +---- .../loading/game/AutoInstallationService.java | 140 ++++++++++++++++++ .../fml/loading/game/GameDiscovery.java | 30 ++-- .../GameDiscoveryOrInstallationService.java | 63 -------- 7 files changed, 157 insertions(+), 122 deletions(-) create mode 100644 loader/src/main/java/net/neoforged/fml/loading/game/AutoInstallationService.java delete mode 100644 loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java diff --git a/gradle.properties b/gradle.properties index 8ac4b79f3..d8dc74beb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,6 +19,7 @@ jupiter_version=5.13.4 mockito_version=5.11.0 assertj_version=3.26.0 jmh_version=1.37 +installer_tools_version=4.0.14 mojang_logging_version=1.1.1 log4j_version=2.22.1 diff --git a/loader/build.gradle b/loader/build.gradle index 7db41e6a4..5171997c3 100644 --- a/loader/build.gradle +++ b/loader/build.gradle @@ -30,6 +30,7 @@ dependencies { api "net.neoforged:JarJarSelector:${jarjar_version}" api "net.neoforged:JarJarMetadata:${jarjar_version}" api("net.neoforged:bus:${eventbus_version}") + api("net.neoforged.installertools:binarypatch-applier:${installer_tools_version}") implementation("net.sf.jopt-simple:jopt-simple:${jopt_simple_version}") implementation("net.neoforged:accesstransformers:${accesstransformers_version}") diff --git a/loader/src/main/java/net/neoforged/fml/loading/EarlyServiceDiscovery.java b/loader/src/main/java/net/neoforged/fml/loading/EarlyServiceDiscovery.java index fa98752fc..8ea03f613 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/EarlyServiceDiscovery.java +++ b/loader/src/main/java/net/neoforged/fml/loading/EarlyServiceDiscovery.java @@ -24,7 +24,6 @@ import net.neoforged.fml.startup.StartupArgs; import net.neoforged.neoforgespi.earlywindow.GraphicsBootstrapper; import net.neoforged.neoforgespi.earlywindow.ImmediateWindowProvider; -import net.neoforged.neoforgespi.installation.GameDiscoveryOrInstallationService; import net.neoforged.neoforgespi.locating.IDependencyLocator; import net.neoforged.neoforgespi.locating.IModFile; import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; @@ -40,8 +39,7 @@ final class EarlyServiceDiscovery { IModFileReader.class, IDependencyLocator.class, GraphicsBootstrapper.class, - ImmediateWindowProvider.class, - GameDiscoveryOrInstallationService.class); + ImmediateWindowProvider.class); private EarlyServiceDiscovery() {} diff --git a/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java b/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java index f2672cbab..639363714 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java +++ b/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java @@ -71,7 +71,6 @@ import net.neoforged.fml.util.ServiceLoaderUtil; import net.neoforged.neoforgespi.ILaunchContext; import net.neoforged.neoforgespi.LocatedPaths; -import net.neoforged.neoforgespi.installation.GameDiscoveryOrInstallationService; import net.neoforged.neoforgespi.language.IModFileInfo; import net.neoforged.neoforgespi.language.IModInfo; import net.neoforged.neoforgespi.locating.IModFile; @@ -316,9 +315,7 @@ public boolean addLocated(Path path) { ImmediateWindowHandler.load(locatedPaths, startupArgs.headless(), programArgs); - var gameInstallationService = discoverGameInstaller(locatedPaths, programArgs); - - var discoveredGame = runLongRunning(startupArgs, () -> GameDiscovery.discoverGame(locatedPaths, dist, gameInstallationService)); + var discoveredGame = runLongRunning(startupArgs, () -> GameDiscovery.discoverGame(locatedPaths, dist)); var neoForgeVersion = discoveredGame.neoforge().getModFileInfo().versionString(); var minecraftVersion = discoveredGame.minecraft().getModFileInfo().versionString(); @@ -402,41 +399,6 @@ public boolean addLocated(Path path) { } } - @Nullable - private static GameDiscoveryOrInstallationService discoverGameInstaller(LocatedPaths located, ProgramArgs programArgs) { - if (programArgs.hasValue("fml.disableInstaller")) - return null; - - var providers = ServiceLoaderUtil.loadEarlyServices(located, GameDiscoveryOrInstallationService.class, List.of()) - .stream() - .toList(); - - if (providers.size() == 1) - return providers.getFirst(); - - if (providers.isEmpty()) { - LOGGER.error("No installation provider found!"); - return null; - } - - if (!programArgs.hasValue("fml.installer")) { - LOGGER.warn("Failed to find game installer, multiple are found, but no selector is provided!"); - return null; - } - - var installerName = programArgs.get("fml.installer"); - var installer = providers.stream() - .filter(p -> Objects.equals(p.name(), installerName)) - .findFirst(); - - if (installer.isEmpty()) { - LOGGER.error("Requested installer: {} was not found in: {}", installerName, providers.stream().map(GameDiscoveryOrInstallationService::name).collect(Collectors.joining(", "))); - return null; - } - - return installer.get(); - } - private static ClassProcessorSet createClassProcessorSet(StartupArgs startupArgs, DiscoveryResult discoveryResult, MixinFacade mixinFacade) { diff --git a/loader/src/main/java/net/neoforged/fml/loading/game/AutoInstallationService.java b/loader/src/main/java/net/neoforged/fml/loading/game/AutoInstallationService.java new file mode 100644 index 000000000..b2b074719 --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/loading/game/AutoInstallationService.java @@ -0,0 +1,140 @@ +package net.neoforged.fml.loading.game; + +import net.neoforged.api.distmarker.Dist; +import net.neoforged.fml.loading.MavenCoordinate; +import net.neoforged.fml.loading.progress.StartupNotificationManager; +import net.neoforged.fml.util.ClasspathResourceUtils; +import net.neoforged.internal.binarypatchapplier.PatchBase; +import net.neoforged.internal.binarypatchapplier.Patcher; +import org.jetbrains.annotations.Nullable; + +import java.io.BufferedInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; + +import static net.neoforged.fml.loading.game.GameDiscovery.LIBRARIES_DIRECTORY_PROPERTY; + +class AutoInstallationService { + + private static final String AUTOINSTALL_PATCH_LOCATION = "autoinstall.patches"; + + @Nullable + static Path discoverOrInstall(Dist requiredDist, String neoForgeVersion, ClassLoader classLoader) throws IOException { + if (!requiredDist.isClient()) + return null; + + var progress = StartupNotificationManager.addProgressBar("Installation", 3); + progress.label("Installation - Extracting resources..."); + + var tempDir = Files.createTempDirectory("fml-auto-installer"); + var patchesPath = extractPatchesPath(classLoader, tempDir); + if (patchesPath == null) { + progress.complete(); + return null; + } + + var minecraftJar = getRawMinecraftClientJar(); + var output = tempDir.resolve("client.jar"); + + + progress.increment(); + progress.label("Installation - Installing NeoForge..."); + + Patcher.patch( + minecraftJar.toFile(), + PatchBase.CLIENT, + List.of(patchesPath.toFile()), + output.toFile(), + (ignored) -> {} //We are not outputting debugging information at the moment. + ); + + progress.increment(); + progress.label("Installation - Finalizing changes..."); + + var patchedMinecraftPath = copyToLibraries(neoForgeVersion, requiredDist, output); + + progress.increment(); + progress.complete(); + + return patchedMinecraftPath; + } + + private static Path copyToLibraries(final String neoForgeVersion, final Dist dist, final Path output) throws IOException { + var librariesDirectory = System.getProperty(LIBRARIES_DIRECTORY_PROPERTY); + var patchedMinecraftPath = Path.of(librariesDirectory).resolve((switch (dist) { + case CLIENT -> new MavenCoordinate("net.neoforged", "minecraft-client-patched", "", "", neoForgeVersion); + case DEDICATED_SERVER -> new MavenCoordinate("net.neoforged", "minecraft-server-patched", "", "", neoForgeVersion); + }).toRelativeRepositoryPath()); + + Files.createDirectories(patchedMinecraftPath.getParent()); + Files.copy(output, patchedMinecraftPath); + return patchedMinecraftPath; + } + + @Nullable + private static Path extractPatchesPath(ClassLoader classLoader, Path tempDir) { + try (var in = classLoader.getResourceAsStream("net/neoforged/neoforge/common/version.properties")) { + if (in == null) { + return null; + } + + Properties properties = new Properties(); + properties.load(new BufferedInputStream(in)); + + if (!properties.contains(AUTOINSTALL_PATCH_LOCATION)) + return null; + + var patchesInnerPath = properties.get(AUTOINSTALL_PATCH_LOCATION).toString(); + return extractFrom(tempDir, "patchers.lzma", patchesInnerPath); + } catch (IOException ignored) { + return null; + } + } + + private static Path getRawMinecraftClientJar() throws IOException { + var jarsWithEntrypoint = new HashSet(); + + var ourCl = Thread.currentThread().getContextClassLoader(); + var resources = ourCl.getResources("net/minecraft/client/main/Main.class"); + while (resources.hasMoreElements()) { + jarsWithEntrypoint.add(ClasspathResourceUtils.findJarPathFor("net/minecraft/client/main/Main.class", "minecraft jar", resources.nextElement())); + } + + // This class would only be present in deobfuscated jars + resources = ourCl.getResources("net/minecraft/client/Minecraft.class"); + while (resources.hasMoreElements()) { + jarsWithEntrypoint.remove(ClasspathResourceUtils.findJarPathFor("net/minecraft/client/Minecraft.class", "minecraft jar", resources.nextElement())); + } + + if (jarsWithEntrypoint.size() != 1) { + throw new IllegalStateException("Failed to find the raw minecraft client from the classpath"); + } + + //Get the minecraft jar (currently obfuscated) + return jarsWithEntrypoint.iterator().next(); + } + + private static Path extractFrom(final Path tempDir, final String targetName, final String packagedName) throws IOException { + var targetFile = tempDir.resolve(targetName); + var patchResource = AutoInstallationService.class.getResource(packagedName); + if (patchResource == null) { + throw new IllegalStateException("Could not find %s in the auto installer.".formatted(packagedName)); + } + + try (BufferedInputStream in = new BufferedInputStream(patchResource.openStream()); + FileOutputStream fileOutputStream = new FileOutputStream(targetFile.toFile())) { + byte[] dataBuffer = new byte[1024]; + int bytesRead; + while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) { + fileOutputStream.write(dataBuffer, 0, bytesRead); + } + } + + return targetFile; + } +} \ No newline at end of file diff --git a/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java b/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java index 615f9345b..c386ef42e 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java +++ b/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java @@ -31,10 +31,8 @@ import net.neoforged.fml.util.ClasspathResourceUtils; import net.neoforged.fml.util.PathPrettyPrinting; import net.neoforged.neoforgespi.LocatedPaths; -import net.neoforged.neoforgespi.installation.GameDiscoveryOrInstallationService; import net.neoforged.neoforgespi.language.IModInfo; import net.neoforged.neoforgespi.locating.IModFile; -import net.neoforged.neoforgespi.locating.IncompatibleFileReporting; import net.neoforged.neoforgespi.locating.ModFileDiscoveryAttributes; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -50,7 +48,7 @@ public final class GameDiscovery { private GameDiscovery() {} - public static GameDiscoveryResult discoverGame(LocatedPaths locatedPaths, Dist requiredDist, @Nullable GameDiscoveryOrInstallationService gameDiscoveryOrInstallationService) { + public static GameDiscoveryResult discoverGame(LocatedPaths locatedPaths, Dist requiredDist) { var ourCl = Thread.currentThread().getContextClassLoader(); var neoForgeVersion = getNeoForgeVersion(ourCl); @@ -73,7 +71,7 @@ public static GameDiscoveryResult discoverGame(LocatedPaths locatedPaths, Dist r } // In production, it's in the libraries directory, and we're passed the version to look for on the commandline - return locateProductionMinecraft(neoForgeVersion, requiredDist, gameDiscoveryOrInstallationService); + return locateProductionMinecraft(neoForgeVersion, requiredDist, ourCl); } private static String getNeoForgeVersion(ClassLoader ourCl) { @@ -279,7 +277,7 @@ private static void addContentRoot(List roots, JarCont /** * In production, the client and neoforge jars are assembled from partial jars in the libraries folder. */ - private static GameDiscoveryResult locateProductionMinecraft(String neoForgeVersion, Dist requiredDist, @Nullable GameDiscoveryOrInstallationService gameDiscoveryOrInstallationService) { + private static GameDiscoveryResult locateProductionMinecraft(String neoForgeVersion, Dist requiredDist, ClassLoader classLoader) { // 2) It's neither, but a libraries directory and desired versions are given on the commandline var librariesDirectory = System.getProperty(LIBRARIES_DIRECTORY_PROPERTY); if (librariesDirectory == null) { @@ -311,19 +309,17 @@ private static GameDiscoveryResult locateProductionMinecraft(String neoForgeVers var nfModFile = openModFile(neoforgeJar, "neoforge", "fml.modloadingissue.corrupted_neoforge_jar"); if (!Files.exists(patchedMinecraftPath)) { - if (gameDiscoveryOrInstallationService != null) { - LOG.info("Patched minecraft does not exist. Triggering external discovery or installation service!"); - try { - var result = gameDiscoveryOrInstallationService.discoverOrInstall(requiredDist); - if (result != null) { - patchedMinecraftPath = result.minecraft(); - } else { - LOG.info("Game discovery or installation service: {} did not return a result. Skipping.", gameDiscoveryOrInstallationService.name()); - } - } catch (Throwable e) { - nfModFile.close(); - throw new ModLoadingException(ModLoadingIssue.error("fml.modloadingissue.discovery_service_failure").withCause(e).withSeverity(ModLoadingIssue.Severity.ERROR)); + LOG.info("Patched minecraft does not exist. Triggering external discovery or installation service!"); + try { + var result = AutoInstallationService.discoverOrInstall(requiredDist, classLoader); + if (result != null) { + patchedMinecraftPath = result.minecraft(); + } else { + LOG.info("Game discovery or installation service did not return a result. Skipping."); } + } catch (Throwable e) { + nfModFile.close(); + throw new ModLoadingException(ModLoadingIssue.error("fml.modloadingissue.discovery_service_failure").withCause(e).withSeverity(ModLoadingIssue.Severity.ERROR)); } } diff --git a/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java b/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java deleted file mode 100644 index 8c9d9c7e7..000000000 --- a/loader/src/main/java/net/neoforged/neoforgespi/installation/GameDiscoveryOrInstallationService.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (c) NeoForged and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.neoforgespi.installation; - -import java.nio.file.Path; -import net.neoforged.api.distmarker.Dist; -import org.jetbrains.annotations.Nullable; - -/** - * A service which can be used to discover or install the game at runtime. - *

- * When the game launches it tries to discover the NeoForge universal jar and the patched minecraft - * jar from the folder which contains the libraries for the game. - *

- *

- * The first step will be to validate the libraries folder, and then find the relevant neoforge jar - * in there. Secondly it will then parse its neoforge.mods.toml and prepare the launch. - *

- *

- * When that all succeeds the game tries to find the relevant patched minecraft jar in the folder as well, - * as it expects this jar to be put there by the installer. - * If the jar exists it will continue and load the neoforge.mods.toml from that jar and continue the normal - * loading procedure. - *

- *

- * If it can not find the patched minecraft jar (because the installer did not create the file), then it will - * check if a service of this type is found as an early loader service (if multiple are found a launch argument - * 'fml.installer' is used to differentiate and select the requested instance). Then it will call - * {@link GameDiscoveryOrInstallationService#discoverOrInstall(String, Dist)} to handle the discovery or installation. - *

- *

- * Each implementation of this type is responsible on its own for caching its results. - *

- *

- * If the launch argument 'fml.disableInstaller' is provided then this entire subsystem is disabled and the loader - * will not try to invoke or even discover and instantiate implementations of this type. - *

- */ -public interface GameDiscoveryOrInstallationService { - /** - * {@return The name of the service} - */ - String name(); - - /** - * Invoked to discover or install the game when it is not found in the libraries folder. - * - * @param requiredDist The distribution which should be discovered or installed. - * @return The {@link Result} of discovery or installation. {@code null} if not found or installed. - */ - @Nullable - Result discoverOrInstall(Dist requiredDist) throws Exception; - - /** - * The result of the discovery or installation. - * - * @param minecraft The path to the patched minecraft jar. - */ - record Result(Path minecraft) {} -} From dca21677052c7a6bfda8d6b1ba54570661281527 Mon Sep 17 00:00:00 2001 From: Marc Hermans Date: Fri, 2 Jan 2026 19:46:15 +0100 Subject: [PATCH 18/29] Fix formatting and properly wire the neoforge version --- .../net/neoforged/fml/loading/FMLLoader.java | 12 ++++----- .../loading/game/AutoInstallationService.java | 25 ++++++++----------- .../fml/loading/game/GameDiscovery.java | 4 +-- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java b/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java index 639363714..45c48d952 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java +++ b/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java @@ -154,12 +154,12 @@ public boolean hasErrors() { } private FMLLoader(LocatedPaths locatedPaths, - ClassLoaderStack classLoaderStack, - List earlyServiceJars, - GameDiscoveryResult discoveredGame, - ProgramArgs programArgs, - Dist dist, - Path gameDir) { + ClassLoaderStack classLoaderStack, + List earlyServiceJars, + GameDiscoveryResult discoveredGame, + ProgramArgs programArgs, + Dist dist, + Path gameDir) { this.locatedPaths = locatedPaths; this.classLoaderStack = classLoaderStack; this.discoveredGame = discoveredGame; diff --git a/loader/src/main/java/net/neoforged/fml/loading/game/AutoInstallationService.java b/loader/src/main/java/net/neoforged/fml/loading/game/AutoInstallationService.java index b2b074719..481fd32fc 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/game/AutoInstallationService.java +++ b/loader/src/main/java/net/neoforged/fml/loading/game/AutoInstallationService.java @@ -1,12 +1,6 @@ package net.neoforged.fml.loading.game; -import net.neoforged.api.distmarker.Dist; -import net.neoforged.fml.loading.MavenCoordinate; -import net.neoforged.fml.loading.progress.StartupNotificationManager; -import net.neoforged.fml.util.ClasspathResourceUtils; -import net.neoforged.internal.binarypatchapplier.PatchBase; -import net.neoforged.internal.binarypatchapplier.Patcher; -import org.jetbrains.annotations.Nullable; +import static net.neoforged.fml.loading.game.GameDiscovery.LIBRARIES_DIRECTORY_PROPERTY; import java.io.BufferedInputStream; import java.io.FileOutputStream; @@ -16,11 +10,15 @@ import java.util.HashSet; import java.util.List; import java.util.Properties; - -import static net.neoforged.fml.loading.game.GameDiscovery.LIBRARIES_DIRECTORY_PROPERTY; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.fml.loading.MavenCoordinate; +import net.neoforged.fml.loading.progress.StartupNotificationManager; +import net.neoforged.fml.util.ClasspathResourceUtils; +import net.neoforged.internal.binarypatchapplier.PatchBase; +import net.neoforged.internal.binarypatchapplier.Patcher; +import org.jetbrains.annotations.Nullable; class AutoInstallationService { - private static final String AUTOINSTALL_PATCH_LOCATION = "autoinstall.patches"; @Nullable @@ -41,7 +39,6 @@ static Path discoverOrInstall(Dist requiredDist, String neoForgeVersion, ClassLo var minecraftJar = getRawMinecraftClientJar(); var output = tempDir.resolve("client.jar"); - progress.increment(); progress.label("Installation - Installing NeoForge..."); @@ -51,7 +48,7 @@ static Path discoverOrInstall(Dist requiredDist, String neoForgeVersion, ClassLo List.of(patchesPath.toFile()), output.toFile(), (ignored) -> {} //We are not outputting debugging information at the moment. - ); + ); progress.increment(); progress.label("Installation - Finalizing changes..."); @@ -127,7 +124,7 @@ private static Path extractFrom(final Path tempDir, final String targetName, fin } try (BufferedInputStream in = new BufferedInputStream(patchResource.openStream()); - FileOutputStream fileOutputStream = new FileOutputStream(targetFile.toFile())) { + FileOutputStream fileOutputStream = new FileOutputStream(targetFile.toFile())) { byte[] dataBuffer = new byte[1024]; int bytesRead; while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) { @@ -137,4 +134,4 @@ private static Path extractFrom(final Path tempDir, final String targetName, fin return targetFile; } -} \ No newline at end of file +} diff --git a/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java b/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java index c386ef42e..e0959b8cc 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java +++ b/loader/src/main/java/net/neoforged/fml/loading/game/GameDiscovery.java @@ -311,9 +311,9 @@ private static GameDiscoveryResult locateProductionMinecraft(String neoForgeVers if (!Files.exists(patchedMinecraftPath)) { LOG.info("Patched minecraft does not exist. Triggering external discovery or installation service!"); try { - var result = AutoInstallationService.discoverOrInstall(requiredDist, classLoader); + var result = AutoInstallationService.discoverOrInstall(requiredDist, neoForgeVersion, classLoader); if (result != null) { - patchedMinecraftPath = result.minecraft(); + patchedMinecraftPath = result; } else { LOG.info("Game discovery or installation service did not return a result. Skipping."); } From fcc6ce860a5d43b71265801ff78febd4b5d0a5ad Mon Sep 17 00:00:00 2001 From: Marc Hermans Date: Fri, 2 Jan 2026 19:59:43 +0100 Subject: [PATCH 19/29] Fix license violations --- .../neoforged/fml/loading/game/AutoInstallationService.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/loader/src/main/java/net/neoforged/fml/loading/game/AutoInstallationService.java b/loader/src/main/java/net/neoforged/fml/loading/game/AutoInstallationService.java index 481fd32fc..b385ab8b0 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/game/AutoInstallationService.java +++ b/loader/src/main/java/net/neoforged/fml/loading/game/AutoInstallationService.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.loading.game; import static net.neoforged.fml.loading.game.GameDiscovery.LIBRARIES_DIRECTORY_PROPERTY; From d6a327a3e46ac414a0959ca3f48680afddc9be44 Mon Sep 17 00:00:00 2001 From: Marc Hermans Date: Fri, 2 Jan 2026 22:27:56 +0100 Subject: [PATCH 20/29] Fix tests --- .../loading/game/AutoInstallationService.java | 28 ++- loader/src/main/resources/lang/en_us.json | 2 +- .../neoforged/fml/loading/FMLLoaderTest.java | 37 +--- .../neoforged/fml/loading/LauncherTest.java | 9 +- testlib/build.gradle | 1 + .../fml/testlib/SimulatedInstallation.java | 195 +++++++++++++----- 6 files changed, 165 insertions(+), 107 deletions(-) diff --git a/loader/src/main/java/net/neoforged/fml/loading/game/AutoInstallationService.java b/loader/src/main/java/net/neoforged/fml/loading/game/AutoInstallationService.java index b385ab8b0..12f21cdc6 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/game/AutoInstallationService.java +++ b/loader/src/main/java/net/neoforged/fml/loading/game/AutoInstallationService.java @@ -10,6 +10,7 @@ import java.io.BufferedInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashSet; @@ -24,7 +25,7 @@ import org.jetbrains.annotations.Nullable; class AutoInstallationService { - private static final String AUTOINSTALL_PATCH_LOCATION = "autoinstall.patches"; + private static final String AUTOINSTALL_PATCH_LOCATION = "autoinstall_patches"; @Nullable static Path discoverOrInstall(Dist requiredDist, String neoForgeVersion, ClassLoader classLoader) throws IOException { @@ -47,6 +48,9 @@ static Path discoverOrInstall(Dist requiredDist, String neoForgeVersion, ClassLo progress.increment(); progress.label("Installation - Installing NeoForge..."); + //An empty file is never patchable. + //Unit tests for this area have an empty patch file. + //We assume that the patcher is well tested and does not need testing. Patcher.patch( minecraftJar.toFile(), PatchBase.CLIENT, @@ -88,11 +92,11 @@ private static Path extractPatchesPath(ClassLoader classLoader, Path tempDir) { Properties properties = new Properties(); properties.load(new BufferedInputStream(in)); - if (!properties.contains(AUTOINSTALL_PATCH_LOCATION)) + if (!properties.containsKey(AUTOINSTALL_PATCH_LOCATION)) return null; var patchesInnerPath = properties.get(AUTOINSTALL_PATCH_LOCATION).toString(); - return extractFrom(tempDir, "patchers.lzma", patchesInnerPath); + return extractFrom(classLoader, tempDir, "patchers.lzma", patchesInnerPath); } catch (IOException ignored) { return null; } @@ -107,12 +111,6 @@ private static Path getRawMinecraftClientJar() throws IOException { jarsWithEntrypoint.add(ClasspathResourceUtils.findJarPathFor("net/minecraft/client/main/Main.class", "minecraft jar", resources.nextElement())); } - // This class would only be present in deobfuscated jars - resources = ourCl.getResources("net/minecraft/client/Minecraft.class"); - while (resources.hasMoreElements()) { - jarsWithEntrypoint.remove(ClasspathResourceUtils.findJarPathFor("net/minecraft/client/Minecraft.class", "minecraft jar", resources.nextElement())); - } - if (jarsWithEntrypoint.size() != 1) { throw new IllegalStateException("Failed to find the raw minecraft client from the classpath"); } @@ -121,18 +119,16 @@ private static Path getRawMinecraftClientJar() throws IOException { return jarsWithEntrypoint.iterator().next(); } - private static Path extractFrom(final Path tempDir, final String targetName, final String packagedName) throws IOException { + private static Path extractFrom(ClassLoader classLoader, final Path tempDir, final String targetName, final String packagedName) throws IOException { var targetFile = tempDir.resolve(targetName); - var patchResource = AutoInstallationService.class.getResource(packagedName); - if (patchResource == null) { - throw new IllegalStateException("Could not find %s in the auto installer.".formatted(packagedName)); - } - try (BufferedInputStream in = new BufferedInputStream(patchResource.openStream()); + try (InputStream stream = classLoader.getResourceAsStream(packagedName); + BufferedInputStream bufferedIn = new BufferedInputStream(stream); FileOutputStream fileOutputStream = new FileOutputStream(targetFile.toFile())) { byte[] dataBuffer = new byte[1024]; int bytesRead; - while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) { + + while ((bytesRead = bufferedIn.read(dataBuffer, 0, 1024)) != -1) { fileOutputStream.write(dataBuffer, 0, bytesRead); } } diff --git a/loader/src/main/resources/lang/en_us.json b/loader/src/main/resources/lang/en_us.json index 7c075c342..09613a9bc 100644 --- a/loader/src/main/resources/lang/en_us.json +++ b/loader/src/main/resources/lang/en_us.json @@ -30,7 +30,7 @@ "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.", "fml.modloadingissue.missing_neoforge_jar": "The NeoForge jar is missing. Please try to reinstall NeoForge.", - "fml.modloadingissue.corrupted_neoforge_jar": "The NeoForge jar is corrupted. Please try to reinstall NeoForge.", + "fml.modloadingissue.corrupted_neoforge_jar": "The NeoForge jar is corrupted or missing. Please try to reinstall NeoForge.", "fml.modloadingissue.cycle": "Detected a mod dependency cycle: {0}", "fml.modloading.failedtoprocesswork": "{0,modinfo,name} ({0,modinfo,id}) encountered an error processing deferred work\n§7{2,exc,msg}", "fml.modloadingissue.brokenfile": "File {101} is not a valid mod file", diff --git a/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java b/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java index 16f0cddea..e00f3ecd9 100644 --- a/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java +++ b/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java @@ -5,14 +5,6 @@ package net.neoforged.fml.loading; -import static net.neoforged.fml.testlib.SimulatedInstallation.CLIENT_ASSETS; -import static net.neoforged.fml.testlib.SimulatedInstallation.GAV_DYNAMIC_PATCHED_CLIENT; -import static net.neoforged.fml.testlib.SimulatedInstallation.GAV_NEOFORGE_DYNAMIC_INSTALLER; -import static net.neoforged.fml.testlib.SimulatedInstallation.MINECRAFT_MODS_TOML; -import static net.neoforged.fml.testlib.SimulatedInstallation.MINECRAFT_VERSION_JSON; -import static net.neoforged.fml.testlib.SimulatedInstallation.PATCHED_CLIENT; -import static net.neoforged.fml.testlib.SimulatedInstallation.RENAMED_SHARED; -import static net.neoforged.fml.testlib.SimulatedInstallation.SHARED_ASSETS; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -39,7 +31,6 @@ import net.neoforged.jarjar.metadata.ContainedJarIdentifier; import net.neoforged.jarjar.metadata.ContainedJarMetadata; import net.neoforged.jarjar.metadata.ContainedVersion; -import net.neoforged.neoforgespi.installation.GameDiscoveryOrInstallationService; import net.neoforged.neoforgespi.locating.IModFile; import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; import net.neoforged.neoforgespi.locating.IModFileReader; @@ -78,26 +69,6 @@ void testProductionClientDiscovery() throws Exception { void testProductionClientWithDynamicInstallationDiscovery() throws Exception { installation.setupProductionClientWithDynamicInstallation(); - var patchedClientJar = installation.writeLibrary(GAV_DYNAMIC_PATCHED_CLIENT, PATCHED_CLIENT, RENAMED_SHARED, CLIENT_ASSETS, SHARED_ASSETS, MINECRAFT_MODS_TOML, MINECRAFT_VERSION_JSON); - var autoInstaller = installation.createLibraryProject(GAV_NEOFORGE_DYNAMIC_INSTALLER, builder -> { - builder.addClass("autoinstall.TestAutoInstaller", """ - import net.neoforged.api.distmarker.Dist; - import net.neoforged.neoforgespi.installation.GameDiscoveryOrInstallationService; - import java.nio.file.Path; - - public record TestAutoInstaller() implements GameDiscoveryOrInstallationService { - @Override public String name() { - return "test"; - } - @Override public Result discoverOrInstall(Dist requiredDist) throws Exception { - return new Result(Path.of("%s")); - } - } - """.formatted(patchedClientJar.toAbsolutePath().toString())) - .addService(GameDiscoveryOrInstallationService.class, "autoinstall.TestAutoInstaller"); - }); - installation.getLaunchClasspath().add(autoInstaller); - var result = launchAndLoad("neoforgeclient"); assertThat(result.loadedMods()).containsOnlyKeys("minecraft", "neoforge"); assertThat(result.gameLayerModules()).containsOnlyKeys("minecraft", "neoforge"); @@ -689,7 +660,7 @@ void testMissingNeoForgeJarInClientInstallation() throws Exception { var e = assertThrows(ModLoadingException.class, () -> launchAndLoad("neoforgeclient")); assertThat(getTranslatedIssues(e.getIssues())).containsOnly( - "ERROR: The NeoForge jar is missing. Please try to reinstall NeoForge."); + "ERROR: Your NeoForge installation is corrupted. Please try to reinstall NeoForge."); } @Test @@ -702,7 +673,7 @@ void testCorruptedNeoForgeJarInClientInstallation() throws Exception { var e = assertThrows(ModLoadingException.class, () -> launchAndLoad("neoforgeclient")); assertThat(getTranslatedIssues(e.getIssues())).containsOnly( - "ERROR: The NeoForge jar is corrupted. Please try to reinstall NeoForge."); + "ERROR: Your NeoForge installation is corrupted. Please try to reinstall NeoForge."); } @Test @@ -739,7 +710,7 @@ void testMissingNeoForgeJarInServerInstallation() throws Exception { var e = assertThrows(ModLoadingException.class, () -> launchAndLoad("neoforgeserver")); assertThat(getTranslatedIssues(e.getIssues())).containsOnly( - "ERROR: The NeoForge jar is missing. Please try to reinstall NeoForge."); + "ERROR: Your NeoForge installation is corrupted. Please try to reinstall NeoForge."); } @Test @@ -752,7 +723,7 @@ void testCorruptedNeoForgeJarInServerInstallation() throws Exception { var e = assertThrows(ModLoadingException.class, () -> launchAndLoad("neoforgeserver")); assertThat(getTranslatedIssues(e.getIssues())).containsOnly( - "ERROR: The NeoForge jar is corrupted. Please try to reinstall NeoForge."); + "ERROR: Your NeoForge installation is corrupted. Please try to reinstall NeoForge."); } /** diff --git a/loader/src/test/java/net/neoforged/fml/loading/LauncherTest.java b/loader/src/test/java/net/neoforged/fml/loading/LauncherTest.java index cc2c85794..3c6e601c6 100644 --- a/loader/src/test/java/net/neoforged/fml/loading/LauncherTest.java +++ b/loader/src/test/java/net/neoforged/fml/loading/LauncherTest.java @@ -8,6 +8,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertNotNull; +import com.google.common.collect.Lists; import java.io.IOException; import java.io.UncheckedIOException; import java.lang.invoke.MethodHandles; @@ -406,12 +407,16 @@ public void assertMinecraftClientJar(LaunchResult launchResult, boolean producti } public void assertNeoForgeJar(LaunchResult launchResult) throws IOException { - var expectedContent = List.of( + var expectedContent = Lists.newArrayList( SimulatedInstallation.NEOFORGE_ASSETS, SimulatedInstallation.NEOFORGE_CLASSES, SimulatedInstallation.NEOFORGE_CLIENT_CLASSES, SimulatedInstallation.NEOFORGE_MODS_TOML, - SimulatedInstallation.NEOFORGE_MANIFEST); + SimulatedInstallation.NEOFORGE_MANIFEST, + installation.getNeoForgeVersionProperties()); + + if (installation.getPatches() != null) + expectedContent.add(installation.getPatches()); assertModContent(launchResult, "neoforge", expectedContent); } diff --git a/testlib/build.gradle b/testlib/build.gradle index 619d17022..310181c24 100644 --- a/testlib/build.gradle +++ b/testlib/build.gradle @@ -19,4 +19,5 @@ dependencies { implementation "net.neoforged:JarJarMetadata:${jarjar_version}" implementation "org.assertj:assertj-core:${assertj_version}" implementation "org.junit.jupiter:junit-jupiter-api:$jupiter_version" + implementation("net.neoforged.installertools:binarypatcher:${installer_tools_version}") } diff --git a/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedInstallation.java b/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedInstallation.java index 12b0472a9..891c09949 100644 --- a/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedInstallation.java +++ b/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedInstallation.java @@ -8,6 +8,19 @@ import com.google.common.io.MoreFiles; import com.google.common.io.RecursiveDeleteOption; import com.google.gson.JsonObject; +import net.neoforged.binarypatcher.DiffOptions; +import net.neoforged.binarypatcher.Generator; +import net.neoforged.binarypatcher.PatchBase; +import net.neoforged.jarjar.metadata.ContainedJarMetadata; +import net.neoforged.jarjar.metadata.Metadata; +import net.neoforged.jarjar.metadata.MetadataIOHandler; +import net.neoforged.jarjar.selection.util.Constants; +import org.apache.commons.lang3.ArrayUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; + import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.File; @@ -34,13 +47,6 @@ import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; -import net.neoforged.jarjar.metadata.ContainedJarMetadata; -import net.neoforged.jarjar.metadata.Metadata; -import net.neoforged.jarjar.metadata.MetadataIOHandler; -import net.neoforged.jarjar.selection.util.Constants; -import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.Opcodes; /** * Simulates various installation types for NeoForge @@ -84,7 +90,7 @@ public enum Type { ; public boolean isProduction() { - return this == PRODUCTION_CLIENT || this == PRODUCTION_SERVER; + return this == PRODUCTION_CLIENT || this == PRODUCTION_SERVER || this == PRODUCTION_CLIENT_INSTALLED_AT_RUNTIME; } } @@ -97,6 +103,12 @@ public boolean isProduction() { } } + public static final String LIBRARIES_DIRECTORY_PROPERTY = "libraryDirectory"; + public static final String MOD_FOLDERS_PROPERTIES = "fml.modFolders"; + public static final String NEOFORGE_VERSION = "20.4.9999"; + public static final String MC_VERSION = "1.20.4"; + public static final String NEOFORM_VERSION = "202401020304"; + /** * A class that is contained in both client and dedicated server distribution, renamed to official mappings. */ @@ -125,19 +137,17 @@ public boolean isProduction() { public static final IdentifiableContent NEOFORGE_CLASSES = generateClass("NEOFORGE_CLASSES", "net/neoforged/neoforge/common/NeoForgeMod.class"); public static final IdentifiableContent NEOFORGE_MODS_TOML = new IdentifiableContent("NEOFORGE_MODS_TOML", "META-INF/neoforge.mods.toml", writeNeoForgeModsToml()); public static final IdentifiableContent NEOFORGE_MANIFEST = new IdentifiableContent("NEOFORGE_MANIFEST", JarFile.MANIFEST_NAME, writeNeoForgeManifest()); + public static final IdentifiableContent NEOFORGE_VERSION_PROPERTIES = new IdentifiableContent("NEOFORGE_VERSION_PROPERTIES", "net/neoforged/neoforge/common/version.properties", writeNeoForgeVersionProperties()); + public static final IdentifiableContent NEOFORGE_VERSION_PROPERTIES_WITH_PATCHES = new IdentifiableContent("NEOFORGE_VERSION_PROPERTIES", "net/neoforged/neoforge/common/version.properties", writeDynamicPatchedNeoForgeVersionProperties()); public static final IdentifiableContent NEOFORGE_ASSETS = new IdentifiableContent("NEOFORGE_ASSETS", "neoforged_logo.png"); - - public static final String LIBRARIES_DIRECTORY_PROPERTY = "libraryDirectory"; - public static final String MOD_FOLDERS_PROPERTIES = "fml.modFolders"; - public static final String NEOFORGE_VERSION = "20.4.9999"; - public static final String MC_VERSION = "1.20.4"; - public static final String NEOFORM_VERSION = "202401020304"; + public static final IdentifiableContent NEOFORGE_MOCK_PATCHES = new IdentifiableContent("NEOFORGE_PATCHES", "META-INF/net/neoforged/patches/patches.lzma", new byte[0]); public static final IdentifiableContent MINECRAFT_VERSION_JSON = new IdentifiableContent("MC_VERSION_JSON", "version.json", buildVersionJson(MC_VERSION)); - public static final IdentifiableContent[] SERVER_EXTRA_JAR_CONTENT = { SHARED_ASSETS, MINECRAFT_VERSION_JSON }; - public static final IdentifiableContent[] CLIENT_EXTRA_JAR_CONTENT = { CLIENT_ASSETS, SHARED_ASSETS, RESOURCES_MANIFEST, MINECRAFT_VERSION_JSON }; - public static final IdentifiableContent[] NEOFORGE_UNIVERSAL_JAR_CONTENT = { NEOFORGE_ASSETS, NEOFORGE_CLIENT_CLASSES, NEOFORGE_CLASSES, NEOFORGE_MODS_TOML, NEOFORGE_MANIFEST }; - public static final IdentifiableContent[] USERDEV_CLIENT_JAR_CONTENT = { PATCHED_CLIENT, PATCHED_SHARED }; + public static final IdentifiableContent[] SERVER_EXTRA_JAR_CONTENT = {SHARED_ASSETS, MINECRAFT_VERSION_JSON}; + public static final IdentifiableContent[] CLIENT_EXTRA_JAR_CONTENT = {CLIENT_ASSETS, SHARED_ASSETS, RESOURCES_MANIFEST, MINECRAFT_VERSION_JSON}; + public static final IdentifiableContent[] NEOFORGE_UNIVERSAL_JAR_CONTENT_WITHOUT_PROPS = {NEOFORGE_ASSETS, NEOFORGE_CLIENT_CLASSES, NEOFORGE_CLASSES, NEOFORGE_MODS_TOML, NEOFORGE_MANIFEST}; + public static final IdentifiableContent[] NEOFORGE_UNIVERSAL_JAR_CONTENT = ArrayUtils.addAll(NEOFORGE_UNIVERSAL_JAR_CONTENT_WITHOUT_PROPS, NEOFORGE_VERSION_PROPERTIES); + public static final IdentifiableContent[] USERDEV_CLIENT_JAR_CONTENT = {PATCHED_CLIENT, PATCHED_SHARED}; private static final String GAV_PATCHED_CLIENT = "net.neoforged:minecraft-client-patched:" + NEOFORGE_VERSION; public static final String GAV_DYNAMIC_PATCHED_CLIENT = "net.neoforged-dynamic-install:minecraft-client-patched:" + NEOFORGE_VERSION; @@ -168,6 +178,10 @@ private static byte[] buildVersionJson(String mcVersion) { // As the installation is setup, we record where which components are private InstallationComponents componentRoots; + // As the installation is seutp, we record the patches we include. + private IdentifiableContent patches; + private IdentifiableContent neoforgeVersionProperties = NEOFORGE_VERSION_PROPERTIES; + // For a production client: Simulates the "libraries" directory found in the Vanilla Minecraft installation directory (".minecraft") // For a production server: The NF installer creates a "libraries" directory in the server root @@ -184,48 +198,47 @@ public void setup(Type type) throws IOException { case PRODUCTION_CLIENT -> { System.setProperty(LIBRARIES_DIRECTORY_PROPERTY, librariesDir.toString()); - var patchedClientJar = writeLibrary(GAV_PATCHED_CLIENT, PATCHED_CLIENT, RENAMED_SHARED, CLIENT_ASSETS, SHARED_ASSETS, MINECRAFT_MODS_TOML, MINECRAFT_VERSION_JSON); + var patchedClientJar = createPatchedClient(); var universalJar = writeLibrary(GAV_NEOFORGE_UNIVERSAL, NEOFORGE_UNIVERSAL_JAR_CONTENT); // For the production client, the Vanilla launcher puts the original, obfuscated client jar on the classpath // Since this can influence our detection logic, let's make sure it's included for the tests. - Path obfuscatedClientJar = versionsDir.resolve(MC_VERSION).resolve(MC_VERSION + ".jar"); - writeJarFile( - obfuscatedClientJar, - generateClass("CLIENT_MAIN", "net/minecraft/client/main/Main.class"), - generateClass("CLIENT_DATA_MAIN", "net/minecraft/client/data/Main.class"), - generateClass("SERVER_MAIN", "net/minecraft/server/Main.class"), - generateClass("SERVER_DATA_MAIN", "net/minecraft/data/Main.class"), - generateClass("GAMETEST_MAIN", "net/minecraft/gametest/Main.class"), - generateClass("MINECRAFT_SERVER", "net/minecraft/server/MinecraftServer.class"), - UNPATCHED_CLIENT, - RENAMED_SHARED, - MINECRAFT_VERSION_JSON, - SHARED_ASSETS, - CLIENT_ASSETS); + Path obfuscatedClientJar = createObfuscatedClient(); + launchClasspath.add(obfuscatedClientJar); + launchClasspath.add(universalJar); componentRoots = InstallationComponents.productionJars(patchedClientJar, universalJar); } case PRODUCTION_CLIENT_INSTALLED_AT_RUNTIME -> { System.setProperty(LIBRARIES_DIRECTORY_PROPERTY, librariesDir.toString()); - var universalJar = writeLibrary(GAV_NEOFORGE_UNIVERSAL, NEOFORGE_UNIVERSAL_JAR_CONTENT); - // For the production client, the Vanilla launcher puts the original, obfuscated client jar on the classpath // Since this can influence our detection logic, let's make sure it's included for the tests. - Path obfuscatedClientJar = versionsDir.resolve(MC_VERSION).resolve(MC_VERSION + ".jar"); - writeJarFile( - obfuscatedClientJar, - generateClass("CLIENT_MAIN", "net/minecraft/client/main/Main.class"), - generateClass("CLIENT_DATA_MAIN", "net/minecraft/client/data/Main.class"), - generateClass("SERVER_MAIN", "net/minecraft/server/Main.class"), - generateClass("SERVER_DATA_MAIN", "net/minecraft/data/Main.class"), - generateClass("GAMETEST_MAIN", "net/minecraft/gametest/Main.class"), - generateClass("MINECRAFT_SERVER", "net/minecraft/server/MinecraftServer.class"), - MINECRAFT_VERSION_JSON, - SHARED_ASSETS, - CLIENT_ASSETS); + Path obfuscatedClientJar = createObfuscatedClient(); + Path patchedClientJar = createPatchedClient("-target"); + + patches = new IdentifiableContent( + "NEOFORGE_PATCHES", + "META-INF/net/neoforged/patches/patches.lzma", + writePatches( + obfuscatedClientJar, + patchedClientJar + ) + ); + neoforgeVersionProperties = NEOFORGE_VERSION_PROPERTIES_WITH_PATCHES; + + var universalJar = writeLibrary( + GAV_NEOFORGE_UNIVERSAL, + ArrayUtils.addAll( + NEOFORGE_UNIVERSAL_JAR_CONTENT_WITHOUT_PROPS, + neoforgeVersionProperties, + patches + ) + ); + + launchClasspath.add(universalJar); + launchClasspath.add(obfuscatedClientJar); componentRoots = InstallationComponents.productionJars(obfuscatedClientJar, universalJar); } @@ -235,6 +248,10 @@ public void setup(Type type) throws IOException { var patchedServerJar = writeLibrary(GAV_PATCHED_SERVER, PATCHED_SHARED, SHARED_ASSETS, MINECRAFT_MODS_TOML, MINECRAFT_VERSION_JSON); var universalJar = writeLibrary(GAV_NEOFORGE_UNIVERSAL, NEOFORGE_UNIVERSAL_JAR_CONTENT); + //Not sure yet if this is correct, but the loader always required the universal jar to be on the CP + //when running so this should emulate even when running with the server starter jar. + launchClasspath.add(universalJar); + componentRoots = InstallationComponents.productionJars(patchedServerJar, universalJar); } case USERDEV_LEGACY_FOLDERS, USERDEV_LEGACY_JAR -> { @@ -271,6 +288,32 @@ public void setup(Type type) throws IOException { this.type = type; } + private @NotNull Path createObfuscatedClient() throws IOException { + Path obfuscatedClientJar = versionsDir.resolve(MC_VERSION).resolve(MC_VERSION + ".jar"); + writeJarFile( + obfuscatedClientJar, + generateClass("CLIENT_MAIN", "net/minecraft/client/main/Main.class"), + generateClass("CLIENT_DATA_MAIN", "net/minecraft/client/data/Main.class"), + generateClass("SERVER_MAIN", "net/minecraft/server/Main.class"), + generateClass("SERVER_DATA_MAIN", "net/minecraft/data/Main.class"), + generateClass("GAMETEST_MAIN", "net/minecraft/gametest/Main.class"), + generateClass("MINECRAFT_SERVER", "net/minecraft/server/MinecraftServer.class"), + UNPATCHED_CLIENT, + RENAMED_SHARED, + MINECRAFT_VERSION_JSON, + SHARED_ASSETS, + CLIENT_ASSETS); + return obfuscatedClientJar; + } + + private Path createPatchedClient() throws IOException { + return createPatchedClient(""); + } + + private Path createPatchedClient(String gavSuffix) throws IOException { + return writeLibrary(GAV_PATCHED_CLIENT + gavSuffix, PATCHED_CLIENT, RENAMED_SHARED, CLIENT_ASSETS, SHARED_ASSETS, MINECRAFT_MODS_TOML, MINECRAFT_VERSION_JSON); + } + public Path getModsFolder() throws IOException { var modsFolder = gameDir.resolve("mods"); Files.createDirectories(modsFolder); @@ -363,7 +406,8 @@ protected record NeoForgeDevFolders( Path clientClassesDir, Path commonClassesDir, Path commonResourcesDir, - Path clientExtraJar) {} + Path clientExtraJar) { + } // Emulate the layout of a NeoForge development environment // In dev, the NeoForge sources itself are joined, but the Minecraft sources are not @@ -375,7 +419,7 @@ private NeoForgeDevFolders createNeoForgeDevFolders() throws IOException { writeFiles(commonClassesDir, PATCHED_SHARED, NEOFORGE_CLASSES); var resourcesDir = projectRoot.resolve("projects/neoforge/build/resources/main"); - writeFiles(resourcesDir, NEOFORGE_ASSETS, NEOFORGE_MODS_TOML, NEOFORGE_MANIFEST); + writeFiles(resourcesDir, NEOFORGE_ASSETS, NEOFORGE_MODS_TOML, NEOFORGE_MANIFEST, NEOFORGE_VERSION_PROPERTIES); var clientExtraJar = projectRoot.resolve("client-extra.jar"); writeJarFile(clientExtraJar, CLIENT_EXTRA_JAR_CONTENT); @@ -432,6 +476,22 @@ public InstallationComponents getComponentRoots() { return componentRoots; } + /** + * {@returns the content which represents the patches used for the installation, might be null if not needed for this installation} + */ + @Nullable + public IdentifiableContent getPatches() { + return patches; + } + + /** + * {@returns the version properties file included in neoforge} + */ + @NotNull + public IdentifiableContent getNeoForgeVersionProperties() { + return neoforgeVersionProperties; + } + /** * {@returns the type of installation that was setup} */ @@ -457,7 +517,7 @@ public static void addFilesToJar(Path jarFile, IdentifiableContent... content) t Set written = new HashSet<>(); var newJarFile = jarFile.resolveSibling(jarFile.getFileName() + ".new"); try (var jarIn = new JarFile(jarFile.toFile()); - var jarOut = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(newJarFile)))) { + var jarOut = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(newJarFile)))) { // Ensure the manifest is written first if (newManifest != null) { jarOut.putNextEntry(new ZipEntry(JarFile.MANIFEST_NAME)); @@ -519,22 +579,47 @@ private static byte[] writeNeoForgeManifest() { private static byte[] writeNeoForgeModsToml() { return """ license = "LICENSE" - + [[mods]] modId="neoforge" """.getBytes(); } + private static byte[] writeNeoForgeVersionProperties() { + return """ + neoforge_version=%s + """.formatted(NEOFORGE_VERSION).getBytes(); + } + + private static byte[] writeDynamicPatchedNeoForgeVersionProperties() { + return """ + neoforge_version=%s + autoinstall_patches=META-INF/net/neoforged/patches/patches.lzma + """.formatted(NEOFORGE_VERSION).getBytes(); + } + private static byte[] writeMinecraftModsToml() { return """ loader = "minecraft" license = "See Minecraft EULA" - + [[mods]] modId="minecraft" """.getBytes(); } + private byte[] writePatches(Path obfuscatedClientJar, Path patchedClientJar) throws IOException { + Path patchBundle = versionsDir.resolve(MC_VERSION).resolve(MC_VERSION + ".lzma"); + Generator.createPatchBundle( + Map.of(PatchBase.CLIENT, obfuscatedClientJar.toFile()), + Map.of(PatchBase.CLIENT, patchedClientJar.toFile()), + patchBundle.toFile(), + new DiffOptions() + ); + + return Files.readAllBytes(patchBundle); + } + public static IdentifiableContent createManifest(String name, Map attributes) throws IOException { Manifest manifest = new Manifest(); // If no manifest version is written, nothing is written. @@ -553,7 +638,7 @@ public static IdentifiableContent createModsToml(String modId, String version) { modLoader = "javafml" loaderVersion = "[3,]" license = "LICENSE" - + [[mods]] modId="%s" version="%s" @@ -566,11 +651,11 @@ public static IdentifiableContent createMultiModsToml(String modId, String versi modLoader = "javafml" loaderVersion = "[3,]" license = "LICENSE" - + [[mods]] modId="%s" version="%s" - + [[mods]] modId="%s" version="%s" From c5a21cb57003c6b289c7e4b1b6fa5f272ccdc865 Mon Sep 17 00:00:00 2001 From: Marc Hermans Date: Fri, 2 Jan 2026 22:28:45 +0100 Subject: [PATCH 21/29] Fix formatting --- .../fml/testlib/SimulatedInstallation.java | 62 ++++++++----------- 1 file changed, 26 insertions(+), 36 deletions(-) diff --git a/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedInstallation.java b/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedInstallation.java index 891c09949..1ae737608 100644 --- a/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedInstallation.java +++ b/testlib/src/main/java/net/neoforged/fml/testlib/SimulatedInstallation.java @@ -8,19 +8,6 @@ import com.google.common.io.MoreFiles; import com.google.common.io.RecursiveDeleteOption; import com.google.gson.JsonObject; -import net.neoforged.binarypatcher.DiffOptions; -import net.neoforged.binarypatcher.Generator; -import net.neoforged.binarypatcher.PatchBase; -import net.neoforged.jarjar.metadata.ContainedJarMetadata; -import net.neoforged.jarjar.metadata.Metadata; -import net.neoforged.jarjar.metadata.MetadataIOHandler; -import net.neoforged.jarjar.selection.util.Constants; -import org.apache.commons.lang3.ArrayUtils; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.Opcodes; - import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.File; @@ -47,6 +34,17 @@ import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; +import net.neoforged.binarypatcher.DiffOptions; +import net.neoforged.binarypatcher.Generator; +import net.neoforged.binarypatcher.PatchBase; +import net.neoforged.jarjar.metadata.ContainedJarMetadata; +import net.neoforged.jarjar.metadata.Metadata; +import net.neoforged.jarjar.metadata.MetadataIOHandler; +import net.neoforged.jarjar.selection.util.Constants; +import org.apache.commons.lang3.ArrayUtils; +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; /** * Simulates various installation types for NeoForge @@ -143,11 +141,11 @@ public boolean isProduction() { public static final IdentifiableContent NEOFORGE_MOCK_PATCHES = new IdentifiableContent("NEOFORGE_PATCHES", "META-INF/net/neoforged/patches/patches.lzma", new byte[0]); public static final IdentifiableContent MINECRAFT_VERSION_JSON = new IdentifiableContent("MC_VERSION_JSON", "version.json", buildVersionJson(MC_VERSION)); - public static final IdentifiableContent[] SERVER_EXTRA_JAR_CONTENT = {SHARED_ASSETS, MINECRAFT_VERSION_JSON}; - public static final IdentifiableContent[] CLIENT_EXTRA_JAR_CONTENT = {CLIENT_ASSETS, SHARED_ASSETS, RESOURCES_MANIFEST, MINECRAFT_VERSION_JSON}; - public static final IdentifiableContent[] NEOFORGE_UNIVERSAL_JAR_CONTENT_WITHOUT_PROPS = {NEOFORGE_ASSETS, NEOFORGE_CLIENT_CLASSES, NEOFORGE_CLASSES, NEOFORGE_MODS_TOML, NEOFORGE_MANIFEST}; + public static final IdentifiableContent[] SERVER_EXTRA_JAR_CONTENT = { SHARED_ASSETS, MINECRAFT_VERSION_JSON }; + public static final IdentifiableContent[] CLIENT_EXTRA_JAR_CONTENT = { CLIENT_ASSETS, SHARED_ASSETS, RESOURCES_MANIFEST, MINECRAFT_VERSION_JSON }; + public static final IdentifiableContent[] NEOFORGE_UNIVERSAL_JAR_CONTENT_WITHOUT_PROPS = { NEOFORGE_ASSETS, NEOFORGE_CLIENT_CLASSES, NEOFORGE_CLASSES, NEOFORGE_MODS_TOML, NEOFORGE_MANIFEST }; public static final IdentifiableContent[] NEOFORGE_UNIVERSAL_JAR_CONTENT = ArrayUtils.addAll(NEOFORGE_UNIVERSAL_JAR_CONTENT_WITHOUT_PROPS, NEOFORGE_VERSION_PROPERTIES); - public static final IdentifiableContent[] USERDEV_CLIENT_JAR_CONTENT = {PATCHED_CLIENT, PATCHED_SHARED}; + public static final IdentifiableContent[] USERDEV_CLIENT_JAR_CONTENT = { PATCHED_CLIENT, PATCHED_SHARED }; private static final String GAV_PATCHED_CLIENT = "net.neoforged:minecraft-client-patched:" + NEOFORGE_VERSION; public static final String GAV_DYNAMIC_PATCHED_CLIENT = "net.neoforged-dynamic-install:minecraft-client-patched:" + NEOFORGE_VERSION; @@ -182,7 +180,6 @@ private static byte[] buildVersionJson(String mcVersion) { private IdentifiableContent patches; private IdentifiableContent neoforgeVersionProperties = NEOFORGE_VERSION_PROPERTIES; - // For a production client: Simulates the "libraries" directory found in the Vanilla Minecraft installation directory (".minecraft") // For a production server: The NF installer creates a "libraries" directory in the server root // In both cases, the location of this directory is passed via a System property "libraryDirectory" @@ -223,9 +220,7 @@ public void setup(Type type) throws IOException { "META-INF/net/neoforged/patches/patches.lzma", writePatches( obfuscatedClientJar, - patchedClientJar - ) - ); + patchedClientJar)); neoforgeVersionProperties = NEOFORGE_VERSION_PROPERTIES_WITH_PATCHES; var universalJar = writeLibrary( @@ -233,9 +228,7 @@ public void setup(Type type) throws IOException { ArrayUtils.addAll( NEOFORGE_UNIVERSAL_JAR_CONTENT_WITHOUT_PROPS, neoforgeVersionProperties, - patches - ) - ); + patches)); launchClasspath.add(universalJar); launchClasspath.add(obfuscatedClientJar); @@ -288,7 +281,7 @@ public void setup(Type type) throws IOException { this.type = type; } - private @NotNull Path createObfuscatedClient() throws IOException { + private Path createObfuscatedClient() throws IOException { Path obfuscatedClientJar = versionsDir.resolve(MC_VERSION).resolve(MC_VERSION + ".jar"); writeJarFile( obfuscatedClientJar, @@ -406,8 +399,7 @@ protected record NeoForgeDevFolders( Path clientClassesDir, Path commonClassesDir, Path commonResourcesDir, - Path clientExtraJar) { - } + Path clientExtraJar) {} // Emulate the layout of a NeoForge development environment // In dev, the NeoForge sources itself are joined, but the Minecraft sources are not @@ -487,7 +479,6 @@ public IdentifiableContent getPatches() { /** * {@returns the version properties file included in neoforge} */ - @NotNull public IdentifiableContent getNeoForgeVersionProperties() { return neoforgeVersionProperties; } @@ -517,7 +508,7 @@ public static void addFilesToJar(Path jarFile, IdentifiableContent... content) t Set written = new HashSet<>(); var newJarFile = jarFile.resolveSibling(jarFile.getFileName() + ".new"); try (var jarIn = new JarFile(jarFile.toFile()); - var jarOut = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(newJarFile)))) { + var jarOut = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(newJarFile)))) { // Ensure the manifest is written first if (newManifest != null) { jarOut.putNextEntry(new ZipEntry(JarFile.MANIFEST_NAME)); @@ -579,7 +570,7 @@ private static byte[] writeNeoForgeManifest() { private static byte[] writeNeoForgeModsToml() { return """ license = "LICENSE" - + [[mods]] modId="neoforge" """.getBytes(); @@ -602,7 +593,7 @@ private static byte[] writeMinecraftModsToml() { return """ loader = "minecraft" license = "See Minecraft EULA" - + [[mods]] modId="minecraft" """.getBytes(); @@ -614,8 +605,7 @@ private byte[] writePatches(Path obfuscatedClientJar, Path patchedClientJar) thr Map.of(PatchBase.CLIENT, obfuscatedClientJar.toFile()), Map.of(PatchBase.CLIENT, patchedClientJar.toFile()), patchBundle.toFile(), - new DiffOptions() - ); + new DiffOptions()); return Files.readAllBytes(patchBundle); } @@ -638,7 +628,7 @@ public static IdentifiableContent createModsToml(String modId, String version) { modLoader = "javafml" loaderVersion = "[3,]" license = "LICENSE" - + [[mods]] modId="%s" version="%s" @@ -651,11 +641,11 @@ public static IdentifiableContent createMultiModsToml(String modId, String versi modLoader = "javafml" loaderVersion = "[3,]" license = "LICENSE" - + [[mods]] modId="%s" version="%s" - + [[mods]] modId="%s" version="%s" From fa707a9724932caf257ff17878c731068725d330 Mon Sep 17 00:00:00 2001 From: Marc Hermans Date: Sat, 3 Jan 2026 14:59:43 +0100 Subject: [PATCH 22/29] Mark the neoforge and minecraft jar as located by the game discovery process before discovering mods. --- .../net/neoforged/fml/loading/moddiscovery/ModDiscoverer.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModDiscoverer.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModDiscoverer.java index ff1e54b05..32cb35977 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModDiscoverer.java +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModDiscoverer.java @@ -69,6 +69,10 @@ public Result discoverMods(List additionalDependencySources) { List loadedFiles = new ArrayList<>(); loadedFiles.add(gameDiscoveryResult.minecraft()); loadedFiles.add(gameDiscoveryResult.neoforge()); + + launchContext.addLocated(gameDiscoveryResult.minecraft().getFilePath()); + launchContext.addLocated(gameDiscoveryResult.neoforge().getFilePath()); + List discoveryIssues = new ArrayList<>(); boolean successfullyLoadedMods = true; ImmediateWindowHandler.updateProgress("Discovering mod files"); From ad0a515483e39cb24f6578db107b0230437a1ae0 Mon Sep 17 00:00:00 2001 From: Marc Hermans Date: Sat, 3 Jan 2026 15:31:13 +0100 Subject: [PATCH 23/29] Handle the case where two different path objects are not equal because they are not absolute --- loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java b/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java index 45c48d952..36587dedd 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java +++ b/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java @@ -300,12 +300,12 @@ public static FMLLoader create(@Nullable Instrumentation instrumentation, Startu @Override public boolean isLocated(Path path) { - return paths.contains(path); + return paths.contains(path.toAbsolutePath()); } @Override public boolean addLocated(Path path) { - return paths.add(path); + return paths.add(path.toAbsolutePath()); } }; From fd62efaaadef71d374c45a9d1c0f3e57b17fe7fa Mon Sep 17 00:00:00 2001 From: Marc Hermans Date: Sat, 3 Jan 2026 15:48:44 +0100 Subject: [PATCH 24/29] Also normalize both paths --- loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java b/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java index 36587dedd..b163e5fb4 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java +++ b/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java @@ -300,12 +300,12 @@ public static FMLLoader create(@Nullable Instrumentation instrumentation, Startu @Override public boolean isLocated(Path path) { - return paths.contains(path.toAbsolutePath()); + return paths.contains(path.toAbsolutePath().normalize()); } @Override public boolean addLocated(Path path) { - return paths.add(path.toAbsolutePath()); + return paths.add(path.toAbsolutePath().normalize()); } }; From 5b9dc7c3148a971e4baf8d2a664e243d371ffde1 Mon Sep 17 00:00:00 2001 From: Marc Hermans Date: Fri, 9 Jan 2026 17:03:01 +0100 Subject: [PATCH 25/29] Update to first review, add support for purging the cache dir to the ELS Error Screen --- .../fml/earlydisplay/error/Button.java | 18 +++-- .../error/ErrorDisplayWindow.java | 57 ++++++++++---- gradle.properties | 2 +- .../net/neoforged/fml/loading/FMLPaths.java | 4 +- .../fml/loading/cache/CacheUtils.java | 13 ++++ .../loading/game/AutoInstallationService.java | 76 +++++++++++-------- .../fml/loading/game/GameDiscovery.java | 30 +++++--- loader/src/main/resources/lang/en_us.json | 4 +- 8 files changed, 140 insertions(+), 64 deletions(-) create mode 100644 loader/src/main/java/net/neoforged/fml/loading/cache/CacheUtils.java diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/error/Button.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/error/Button.java index 9b7170083..d5d933aa0 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/error/Button.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/error/Button.java @@ -9,6 +9,8 @@ import java.nio.ByteBuffer; import java.nio.IntBuffer; import java.util.List; +import java.util.function.Supplier; + import net.neoforged.fml.earlydisplay.render.RenderContext; import net.neoforged.fml.earlydisplay.render.SimpleFont; import net.neoforged.fml.earlydisplay.render.Texture; @@ -25,11 +27,15 @@ final class Button { private final int width; private final int height; private final String text; - private final boolean active; + private final Supplier active; private final Runnable onPress; private boolean focused; Button(ErrorDisplayWindow window, int x, int y, int width, int height, String text, boolean active, Runnable onPress) { + this(window, x, y, width, height, text, () -> active, onPress); + } + + Button(ErrorDisplayWindow window, int x, int y, int width, int height, String text, Supplier active, Runnable onPress) { this.window = window; this.x = x; this.y = y; @@ -41,13 +47,13 @@ final class Button { } void render(RenderContext ctx, SimpleFont font, double mouseX, double mouseY) { - boolean highlighted = active && (focused || isMouseOver(mouseX, mouseY)); - Texture texture = active ? (highlighted ? window.buttonTextureHover : window.buttonTexture) : window.buttonTextureInactive; + boolean highlighted = isActive() && (focused || isMouseOver(mouseX, mouseY)); + Texture texture = isActive() ? (highlighted ? window.buttonTextureHover : window.buttonTexture) : window.buttonTextureInactive; ctx.blitTexture(texture, x, y, width, height); int w = font.stringWidth(text); float tx = x + width / 2F - w / 2F; - int textColor = active ? 0xFFFFFFFF : 0xFFA0A0A0; + int textColor = isActive() ? 0xFFFFFFFF : 0xFFA0A0A0; ctx.renderTextWithShadow(tx, y + 2, font, List.of(new SimpleFont.DisplayText(text, textColor))); } @@ -56,7 +62,7 @@ boolean isMouseOver(double mouseX, double mouseY) { } boolean isActive() { - return active; + return active.get(); } boolean isFocused() { @@ -72,7 +78,7 @@ void unfocus() { } void press() { - if (this.active) { + if (isActive()) { this.onPress.run(); } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/error/ErrorDisplayWindow.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/error/ErrorDisplayWindow.java index 76c67511a..b4838eba0 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/error/ErrorDisplayWindow.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/error/ErrorDisplayWindow.java @@ -5,6 +5,7 @@ package net.neoforged.fml.earlydisplay.error; +import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; @@ -21,6 +22,8 @@ import net.neoforged.fml.earlydisplay.render.Texture; import net.neoforged.fml.earlydisplay.theme.Theme; import net.neoforged.fml.i18n.FMLTranslations; +import net.neoforged.fml.loading.FMLPaths; +import net.neoforged.fml.loading.cache.CacheUtils; import org.jetbrains.annotations.Nullable; import org.lwjgl.glfw.GLFW; import org.lwjgl.opengl.GL11C; @@ -28,7 +31,8 @@ final class ErrorDisplayWindow { private static final int DISPLAY_WIDTH = 854; private static final int DISPLAY_HEIGHT = 480; - private static final int BUTTON_WIDTH = 320; + private static final int SMALL_BUTTON_WIDTH = 320; + private static final int LARGE_BUTTON_WIDTH = 2 * SMALL_BUTTON_WIDTH + 20; private static final int BUTTON_HEIGHT = 40; private static final int LIST_BORDER_HEIGHT = 2; private static final int SCROLLER_WIDTH = 15; @@ -36,14 +40,15 @@ final class ErrorDisplayWindow { private static final int ENTRY_PADDING = 10; private static final int HEADER_Y = 10; private static final int HEADER_LINE_HEIGHT = 18; - private static final int LEFT_BTN_X = DISPLAY_WIDTH / 2 - 10 - BUTTON_WIDTH; + private static final int LEFT_BTN_X = DISPLAY_WIDTH / 2 - 10 - SMALL_BUTTON_WIDTH; private static final int RIGHT_BTN_X = DISPLAY_WIDTH / 2 + 10; - private static final int TOP_BTN_Y = DISPLAY_HEIGHT - 92; + private static final int TOP_BTN_Y = DISPLAY_HEIGHT - 137; + private static final int MIDDLE_BTN_Y = DISPLAY_HEIGHT - 92; private static final int BOTTOM_BTN_Y = DISPLAY_HEIGHT - 47; private static final int LIST_Y_TOP = 70; - private static final int LIST_Y_BOTTOM = DISPLAY_HEIGHT - 100; + private static final int LIST_Y_BOTTOM = DISPLAY_HEIGHT - 145; private static final int LIST_CONTENT_Y_TOP = 74; - private static final int LIST_CONTENT_Y_BOTTOM = DISPLAY_HEIGHT - 102; + private static final int LIST_CONTENT_Y_BOTTOM = DISPLAY_HEIGHT - 147; private static final int LIST_BORDER_TOP_Y2 = LIST_Y_TOP - LIST_BORDER_HEIGHT; private static final int LIST_BORDER_TOP_Y1 = LIST_BORDER_TOP_Y2 - LIST_BORDER_HEIGHT; private static final int LIST_BORDER_BOTTOM_Y1 = LIST_Y_BOTTOM; @@ -64,9 +69,9 @@ final class ErrorDisplayWindow { final Texture buttonTextureHover; final Texture buttonTextureInactive; private final List