diff --git a/src/main/java/net/fabricmc/loader/impl/FabricLoaderImpl.java b/src/main/java/net/fabricmc/loader/impl/FabricLoaderImpl.java index d1a945372..4244da746 100644 --- a/src/main/java/net/fabricmc/loader/impl/FabricLoaderImpl.java +++ b/src/main/java/net/fabricmc/loader/impl/FabricLoaderImpl.java @@ -43,6 +43,7 @@ import net.fabricmc.loader.api.MappingResolver; import net.fabricmc.loader.api.SemanticVersion; import net.fabricmc.loader.api.entrypoint.EntrypointContainer; +import net.fabricmc.loader.impl.discovery.ArgumentModCandidateFinder; import net.fabricmc.loader.impl.discovery.ClasspathModCandidateFinder; import net.fabricmc.loader.impl.discovery.DirectoryModCandidateFinder; import net.fabricmc.loader.impl.discovery.ModCandidate; @@ -179,9 +180,11 @@ public void load() { } private void setup() throws ModResolutionException { + boolean remapRegularMods = isDevelopmentEnvironment(); ModDiscoverer discoverer = new ModDiscoverer(); discoverer.addCandidateFinder(new ClasspathModCandidateFinder()); - discoverer.addCandidateFinder(new DirectoryModCandidateFinder(gameDir.resolve("mods"), isDevelopmentEnvironment())); + discoverer.addCandidateFinder(new DirectoryModCandidateFinder(gameDir.resolve("mods"), remapRegularMods)); + discoverer.addCandidateFinder(new ArgumentModCandidateFinder(remapRegularMods)); List mods = ModResolver.resolve(discoverer.discoverMods(this)); @@ -201,7 +204,7 @@ private void setup() throws ModResolutionException { // runtime mod remapping - if (isDevelopmentEnvironment()) { + if (remapRegularMods) { if (System.getProperty(SystemProperties.REMAP_CLASSPATH_FILE) == null) { Log.warn(LogCategory.MOD_REMAP, "Runtime mod remapping disabled due to no fabric.remapClasspathFile being specified. You may need to update loom."); } else { diff --git a/src/main/java/net/fabricmc/loader/impl/discovery/ArgumentModCandidateFinder.java b/src/main/java/net/fabricmc/loader/impl/discovery/ArgumentModCandidateFinder.java new file mode 100644 index 000000000..990c30f9c --- /dev/null +++ b/src/main/java/net/fabricmc/loader/impl/discovery/ArgumentModCandidateFinder.java @@ -0,0 +1,147 @@ +/* + * Copyright 2016 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.loader.impl.discovery; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.List; + +import net.fabricmc.loader.impl.FabricLoaderImpl; +import net.fabricmc.loader.impl.util.Arguments; +import net.fabricmc.loader.impl.util.SystemProperties; +import net.fabricmc.loader.impl.util.log.Log; +import net.fabricmc.loader.impl.util.log.LogCategory; + +public class ArgumentModCandidateFinder implements ModCandidateFinder { + private final boolean requiresRemap; + + public ArgumentModCandidateFinder(boolean requiresRemap) { + this.requiresRemap = requiresRemap; + } + + @Override + public void findCandidates(ModCandidateConsumer out) { + String list = System.getProperty(SystemProperties.ADD_MODS); + if (list != null) addMods(list, "system property", out); + + list = FabricLoaderImpl.INSTANCE.getGameProvider().getArguments().remove(Arguments.ADD_MODS); + if (list != null) addMods(list, "argument", out); + } + + private void addMods(String list, String source, ModCandidateConsumer out) { + for (String pathStr : list.split(File.pathSeparator)) { + if (pathStr.isEmpty()) continue; + + if (pathStr.startsWith("@")) { + Path path = Paths.get(pathStr.substring(1)); + + if (!Files.isRegularFile(path)) { + Log.warn(LogCategory.DISCOVERY, "Missing/invalid %s provided mod list file %s", source, path); + continue; + } + + try (BufferedReader reader = Files.newBufferedReader(path)) { + String fileSource = String.format("%s file %s", source, path); + String line; + + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) continue; + + addMod(line, fileSource, out); + } + } catch (IOException e) { + throw new RuntimeException(String.format("Error reading %s provided mod list file %s", source, path), e); + } + } else { + addMod(pathStr, source, out); + } + } + } + + private void addMod(String pathStr, String source, ModCandidateConsumer out) { + Path path = Paths.get(pathStr).toAbsolutePath().normalize(); + + if (!Files.exists(path)) { // missing + Log.warn(LogCategory.DISCOVERY, "Missing %s provided mod path %s", source, path); + } else if (Files.isDirectory(path)) { // directory for extracted mod (in-dev usually) or jars (like mods, but recursive) + if (isHidden(path)) { + Log.warn(LogCategory.DISCOVERY, "Ignoring hidden %s provided mod path %s", source, path); + return; + } + + if (Files.exists(path.resolve("fabric.mod.json"))) { // extracted mod + out.accept(path, requiresRemap); + } else { // dir containing jars + try { + List skipped = new ArrayList<>(); + + Files.walkFileTree(path, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (DirectoryModCandidateFinder.isValidFile(file)) { + out.accept(file, requiresRemap); + } else { + skipped.add(path.relativize(file).toString()); + } + + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + if (isHidden(dir)) { + return FileVisitResult.SKIP_SUBTREE; + } else { + return FileVisitResult.CONTINUE; + } + } + }); + + if (!skipped.isEmpty()) { + Log.warn(LogCategory.DISCOVERY, "Incompatible files in %s provided mod directory %s (non-jar or hidden): %s", source, path, String.join(", ", skipped)); + } + } catch (IOException e) { + Log.warn(LogCategory.DISCOVERY, "Error processing %s provided mod path %s: %s", source, path, e); + } + } + } else { // single file + if (!DirectoryModCandidateFinder.isValidFile(path)) { + Log.warn(LogCategory.DISCOVERY, "Incompatible file in %s provided mod path %s (non-jar or hidden)", source, path); + } else { + out.accept(path, requiresRemap); + } + } + } + + private static boolean isHidden(Path path) { + try { + return path.getFileName().toString().startsWith(".") || Files.isHidden(path); + } catch (IOException e) { + Log.warn(LogCategory.DISCOVERY, "Error determining whether %s is hidden: %s", path, e); + return true; + } + } +} diff --git a/src/main/java/net/fabricmc/loader/impl/discovery/DirectoryModCandidateFinder.java b/src/main/java/net/fabricmc/loader/impl/discovery/DirectoryModCandidateFinder.java index 6c005f2bd..eefcd4989 100644 --- a/src/main/java/net/fabricmc/loader/impl/discovery/DirectoryModCandidateFinder.java +++ b/src/main/java/net/fabricmc/loader/impl/discovery/DirectoryModCandidateFinder.java @@ -25,6 +25,9 @@ import java.nio.file.attribute.BasicFileAttributes; import java.util.EnumSet; +import net.fabricmc.loader.impl.util.log.Log; +import net.fabricmc.loader.impl.util.log.LogCategory; + public class DirectoryModCandidateFinder implements ModCandidateFinder { private final Path path; private final boolean requiresRemap; @@ -52,18 +55,7 @@ public void findCandidates(ModCandidateConsumer out) { Files.walkFileTree(this.path, EnumSet.of(FileVisitOption.FOLLOW_LINKS), 1, new SimpleFileVisitor() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - /* - * We only propose a file as a possible mod in the following scenarios: - * General: Must be a jar file - * - * Some OSes Generate metadata so consider the following because of OSes: - * UNIX: Exclude if file is hidden; this occurs when starting a file name with `.` - * MacOS: Exclude hidden + startsWith "." since Mac OS names their metadata files in the form of `.mod.jar` - */ - - String fileName = file.getFileName().toString(); - - if (fileName.endsWith(".jar") && !fileName.startsWith(".") && !Files.isHidden(file)) { + if (isValidFile(file)) { out.accept(file, requiresRemap); } @@ -74,4 +66,28 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO throw new RuntimeException("Exception while searching for mods in '" + path + "'!", e); } } + + static boolean isValidFile(Path path) { + /* + * We only propose a file as a possible mod in the following scenarios: + * General: Must be a jar file + * + * Some OSes Generate metadata so consider the following because of OSes: + * UNIX: Exclude if file is hidden; this occurs when starting a file name with `.` + * MacOS: Exclude hidden + startsWith "." since Mac OS names their metadata files in the form of `.mod.jar` + */ + + if (!Files.isRegularFile(path)) return false; + + try { + if (Files.isHidden(path)) return false; + } catch (IOException e) { + Log.warn(LogCategory.DISCOVERY, "Error checking if file %s is hidden", path, e); + return false; + } + + String fileName = path.getFileName().toString(); + + return fileName.endsWith(".jar") && !fileName.startsWith("."); + } } diff --git a/src/main/java/net/fabricmc/loader/impl/discovery/ModDiscoverer.java b/src/main/java/net/fabricmc/loader/impl/discovery/ModDiscoverer.java index d1b2c8094..9a9caec2b 100644 --- a/src/main/java/net/fabricmc/loader/impl/discovery/ModDiscoverer.java +++ b/src/main/java/net/fabricmc/loader/impl/discovery/ModDiscoverer.java @@ -70,10 +70,15 @@ public void addCandidateFinder(ModCandidateFinder f) { public Collection discoverMods(FabricLoaderImpl loader) throws ModResolutionException { long startTime = System.nanoTime(); ForkJoinPool pool = new ForkJoinPool(); + Set paths = new HashSet<>(); // suppresses duplicate paths List> futures = new ArrayList<>(); ModCandidateConsumer taskSubmitter = (path, requiresRemap) -> { - futures.add(pool.submit(new ModScanTask(path, requiresRemap))); + path = path.toAbsolutePath().normalize(); + + if (paths.add(path)) { + futures.add(pool.submit(new ModScanTask(path, requiresRemap))); + } }; for (ModCandidateFinder finder : candidateFinders) { diff --git a/src/main/java/net/fabricmc/loader/impl/game/GameProvider.java b/src/main/java/net/fabricmc/loader/impl/game/GameProvider.java index 62425732a..499fd6753 100644 --- a/src/main/java/net/fabricmc/loader/impl/game/GameProvider.java +++ b/src/main/java/net/fabricmc/loader/impl/game/GameProvider.java @@ -24,6 +24,7 @@ import net.fabricmc.api.EnvType; import net.fabricmc.loader.api.metadata.ModMetadata; import net.fabricmc.loader.impl.game.patch.GameTransformer; +import net.fabricmc.loader.impl.util.Arguments; public interface GameProvider { String getGameId(); @@ -42,6 +43,7 @@ public interface GameProvider { GameTransformer getEntrypointTransformer(); void launch(ClassLoader loader); + Arguments getArguments(); String[] getLaunchArguments(boolean sanitize); default boolean canOpenErrorGui() { diff --git a/src/main/java/net/fabricmc/loader/impl/game/minecraft/MinecraftGameProvider.java b/src/main/java/net/fabricmc/loader/impl/game/minecraft/MinecraftGameProvider.java index f6e047e95..83b917976 100644 --- a/src/main/java/net/fabricmc/loader/impl/game/minecraft/MinecraftGameProvider.java +++ b/src/main/java/net/fabricmc/loader/impl/game/minecraft/MinecraftGameProvider.java @@ -36,7 +36,6 @@ import net.fabricmc.loader.impl.game.minecraft.patch.EntrypointPatch; import net.fabricmc.loader.impl.game.minecraft.patch.EntrypointPatchFML125; import net.fabricmc.loader.impl.game.patch.GameTransformer; -import net.fabricmc.loader.impl.launch.FabricLauncherBase; import net.fabricmc.loader.impl.metadata.BuiltinModMetadata; import net.fabricmc.loader.impl.metadata.ModDependencyImpl; import net.fabricmc.loader.impl.util.Arguments; @@ -110,7 +109,7 @@ public Path getLaunchDirectory() { return new File(".").toPath(); } - return FabricLauncherBase.getLaunchDirectory(arguments).toPath(); + return getLaunchDirectory(arguments).toPath(); } @Override @@ -166,38 +165,75 @@ public boolean locateGame(EnvType envType, String[] args, ClassLoader loader) { if (version == null) version = System.getProperty(SystemProperties.GAME_VERSION); versionData = McVersionLookup.getVersion(gameJar, entrypointClasses, version); - FabricLauncherBase.processArgumentMap(arguments, envType); + processArgumentMap(arguments, envType); return true; } + private static void processArgumentMap(Arguments argMap, EnvType envType) { + switch (envType) { + case CLIENT: + if (!argMap.containsKey("accessToken")) { + argMap.put("accessToken", "FabricMC"); + } + + if (!argMap.containsKey("version")) { + argMap.put("version", "Fabric"); + } + + String versionType = ""; + + if (argMap.containsKey("versionType") && !argMap.get("versionType").equalsIgnoreCase("release")) { + versionType = argMap.get("versionType") + "/"; + } + + argMap.put("versionType", versionType + "Fabric"); + + if (!argMap.containsKey("gameDir")) { + argMap.put("gameDir", getLaunchDirectory(argMap).getAbsolutePath()); + } + + break; + case SERVER: + argMap.remove("version"); + argMap.remove("gameDir"); + argMap.remove("assetsDir"); + break; + } + } + + private static File getLaunchDirectory(Arguments argMap) { + return new File(argMap.getOrDefault("gameDir", ".")); + } + @Override - public String[] getLaunchArguments(boolean sanitize) { - if (arguments != null) { - List list = new ArrayList<>(Arrays.asList(arguments.toArray())); + public Arguments getArguments() { + return arguments; + } - if (sanitize) { - int remove = 0; - Iterator iterator = list.iterator(); + @Override + public String[] getLaunchArguments(boolean sanitize) { + if (arguments == null) return new String[0]; + if (!sanitize) return arguments.toArray(); - while (iterator.hasNext()) { - String next = iterator.next(); + List list = new ArrayList<>(Arrays.asList(arguments.toArray())); + int remove = 0; + Iterator iterator = list.iterator(); - if ("--accessToken".equals(next)) { - remove = 2; - } + while (iterator.hasNext()) { + String next = iterator.next(); - if (remove > 0) { - iterator.remove(); - remove--; - } - } + if ("--accessToken".equals(next)) { + remove = 2; } - return list.toArray(new String[0]); + if (remove > 0) { + iterator.remove(); + remove--; + } } - return new String[0]; + return list.toArray(new String[0]); } @Override diff --git a/src/main/java/net/fabricmc/loader/impl/game/minecraft/launchwrapper/FabricTweaker.java b/src/main/java/net/fabricmc/loader/impl/game/minecraft/launchwrapper/FabricTweaker.java index c286c3239..20a5cd991 100644 --- a/src/main/java/net/fabricmc/loader/impl/game/minecraft/launchwrapper/FabricTweaker.java +++ b/src/main/java/net/fabricmc/loader/impl/game/minecraft/launchwrapper/FabricTweaker.java @@ -88,8 +88,6 @@ public void acceptOptions(List localArgs, File gameDir, File assetsDir, if (getEnvironmentType() == EnvType.CLIENT && !arguments.containsKey("assetsDir") && assetsDir != null) { arguments.put("assetsDir", assetsDir.getAbsolutePath()); } - - FabricLauncherBase.processArgumentMap(arguments, getEnvironmentType()); } @Override @@ -118,6 +116,7 @@ public void injectIntoClassLoader(LaunchClassLoader launchClassLoader) { throw new RuntimeException("Could not locate Minecraft: provider locate failed"); } + arguments = null; FabricLoaderImpl loader = FabricLoaderImpl.INSTANCE; loader.setGameProvider(provider); loader.load(); @@ -127,7 +126,6 @@ public void injectIntoClassLoader(LaunchClassLoader launchClassLoader) { if (!isDevelopment) { // Obfuscated environment - Launch.blackboard.put(SystemProperties.DEVELOPMENT, false); try { String target = getLaunchTarget(); @@ -164,7 +162,7 @@ public void injectIntoClassLoader(LaunchClassLoader launchClassLoader) { @Override public String[] getLaunchArguments() { - return isPrimaryTweaker ? arguments.toArray() : new String[0]; + return isPrimaryTweaker ? FabricLoaderImpl.INSTANCE.getGameProvider().getLaunchArguments(false) : new String[0]; } @Override diff --git a/src/main/java/net/fabricmc/loader/impl/launch/FabricLauncherBase.java b/src/main/java/net/fabricmc/loader/impl/launch/FabricLauncherBase.java index e13877d49..bd97ebd43 100644 --- a/src/main/java/net/fabricmc/loader/impl/launch/FabricLauncherBase.java +++ b/src/main/java/net/fabricmc/loader/impl/launch/FabricLauncherBase.java @@ -16,7 +16,6 @@ package net.fabricmc.loader.impl.launch; -import java.io.File; import java.io.IOException; import java.lang.reflect.Method; import java.net.URI; @@ -32,9 +31,7 @@ import org.spongepowered.asm.mixin.MixinEnvironment; -import net.fabricmc.api.EnvType; import net.fabricmc.loader.impl.FabricLoaderImpl; -import net.fabricmc.loader.impl.util.Arguments; import net.fabricmc.loader.impl.util.UrlUtil; import net.fabricmc.loader.impl.util.log.Log; import net.fabricmc.loader.impl.util.log.LogCategory; @@ -55,10 +52,6 @@ protected FabricLauncherBase() { setLauncher(this); } - public static File getLaunchDirectory(Arguments argMap) { - return new File(argMap.getOrDefault("gameDir", ".")); - } - public static Class getClass(String className) throws ClassNotFoundException { return Class.forName(className, true, getLauncher().getTargetClassLoader()); } @@ -219,38 +212,6 @@ private static void deobfuscate0(Path jarFile, Path deobfJarFile, Path deobfJarF } } - public static void processArgumentMap(Arguments argMap, EnvType envType) { - switch (envType) { - case CLIENT: - if (!argMap.containsKey("accessToken")) { - argMap.put("accessToken", "FabricMC"); - } - - if (!argMap.containsKey("version")) { - argMap.put("version", "Fabric"); - } - - String versionType = ""; - - if (argMap.containsKey("versionType") && !argMap.get("versionType").equalsIgnoreCase("release")) { - versionType = argMap.get("versionType") + "/"; - } - - argMap.put("versionType", versionType + "Fabric"); - - if (!argMap.containsKey("gameDir")) { - argMap.put("gameDir", getLaunchDirectory(argMap).getAbsolutePath()); - } - - break; - case SERVER: - argMap.remove("version"); - argMap.remove("gameDir"); - argMap.remove("assetsDir"); - break; - } - } - protected static void setProperties(Map propertiesA) { if (properties != null && properties != propertiesA) { throw new RuntimeException("Duplicate setProperties call!"); diff --git a/src/main/java/net/fabricmc/loader/impl/util/Arguments.java b/src/main/java/net/fabricmc/loader/impl/util/Arguments.java index efc248bf9..2e7907612 100644 --- a/src/main/java/net/fabricmc/loader/impl/util/Arguments.java +++ b/src/main/java/net/fabricmc/loader/impl/util/Arguments.java @@ -25,7 +25,10 @@ import java.util.Map; public final class Arguments { - public static final String GAME_VERSION = "fabric.gameVersion"; + // set the game version for the builtin game mod/dependencies, bypassing auto-detection + public static final String GAME_VERSION = SystemProperties.GAME_VERSION; + // additional mods to load (path separator separated paths, @ prefix for meta-file with each line referencing an actual file) + public static final String ADD_MODS = SystemProperties.ADD_MODS; private final Map values; private final List extraArgs; diff --git a/src/main/java/net/fabricmc/loader/impl/util/SystemProperties.java b/src/main/java/net/fabricmc/loader/impl/util/SystemProperties.java index c0a65fec2..486b2af83 100644 --- a/src/main/java/net/fabricmc/loader/impl/util/SystemProperties.java +++ b/src/main/java/net/fabricmc/loader/impl/util/SystemProperties.java @@ -17,14 +17,18 @@ package net.fabricmc.loader.impl.util; public final class SystemProperties { + // whether fabric loader is running in a development environment / mode, affects class path mod discovery, remapping, logging, ... public static final String DEVELOPMENT = "fabric.development"; public static final String SIDE = "fabric.side"; public static final String GAME_JAR_PATH = "fabric.gameJarPath"; + // set the game version for the builtin game mod/dependencies, bypassing auto-detection public static final String GAME_VERSION = "fabric.gameVersion"; // fallback log file for the builtin log handler (dumped on exit if not replaced with another handler) public static final String LOG_FILE = "fabric.log.file"; // minimum log level for builtin log handler public static final String LOG_LEVEL = "fabric.log.level"; + // additional mods to load (path separator separated paths, @ prefix for meta-file with each line referencing an actual file) + public static final String ADD_MODS = "fabric.addMods"; // file containing the class path for in-dev runtime mod remapping public static final String REMAP_CLASSPATH_FILE = "fabric.remapClasspathFile"; // throw exceptions from entrypoints, discovery etc. directly instead of gathering and attaching as suppressed