diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:42:14 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:42:14 +0300 |
| commit | 9affe6b2ee9d4bd1c474d1d7c32ea2cc81ce5cee (patch) | |
| tree | 0dcfc70783bc623b016d05515aee309e0ca5c2d3 /forgewrapper/src | |
| parent | 3d2121f5d6555744ce5aa502088fc2b34dc26d38 (diff) | |
| parent | 959f7668509a90d524438903ad8bda55d9e24e9d (diff) | |
| download | Project-Tick-9affe6b2ee9d4bd1c474d1d7c32ea2cc81ce5cee.tar.gz Project-Tick-9affe6b2ee9d4bd1c474d1d7c32ea2cc81ce5cee.zip | |
Add 'forgewrapper/' from commit '959f7668509a90d524438903ad8bda55d9e24e9d'
git-subtree-dir: forgewrapper
git-subtree-mainline: 3d2121f5d6555744ce5aa502088fc2b34dc26d38
git-subtree-split: 959f7668509a90d524438903ad8bda55d9e24e9d
Diffstat (limited to 'forgewrapper/src')
8 files changed, 558 insertions, 0 deletions
diff --git a/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Bootstrap.java b/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Bootstrap.java new file mode 100644 index 0000000000..88c3a00742 --- /dev/null +++ b/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Bootstrap.java @@ -0,0 +1,75 @@ +package io.github.zekerzhayard.forgewrapper.installer; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import io.github.zekerzhayard.forgewrapper.installer.util.ModuleUtil; + +public class Bootstrap { + public static void bootstrap(String[] jvmArgs, String minecraftJar, String libraryDir) throws Throwable { + // Replace all placeholders + String[] replacedJvmArgs = new String[jvmArgs.length]; + for (int i = 0; i < jvmArgs.length; i++) { + String arg = jvmArgs[i]; + replacedJvmArgs[i] = arg.replace("${classpath}", System.getProperty("java.class.path").replace(File.separator, "/")).replace("${classpath_separator}", File.pathSeparator).replace("${library_directory}", libraryDir).replace("${version_name}", minecraftJar.substring(0, minecraftJar.lastIndexOf('.'))); + } + jvmArgs = replacedJvmArgs; + + // Remove NewLaunch.jar from property to prevent Forge from adding it to the module path + StringBuilder newCP = new StringBuilder(); + for (String path : System.getProperty("java.class.path").split(File.pathSeparator)) { + if (!path.endsWith("NewLaunch.jar")) { + newCP.append(path).append(File.pathSeparator); + } + } + System.setProperty("java.class.path", newCP.substring(0, newCP.length() - 1)); + + String modulePath = null; + List<String> addExports = new ArrayList<>(); + List<String> addOpens = new ArrayList<>(); + for (int i = 0; i < jvmArgs.length; i++) { + String arg = jvmArgs[i]; + + if (arg.equals("-p") || arg.equals("--module-path")) { + modulePath = jvmArgs[i + 1]; + } else if (arg.startsWith("--module-path=")) { + modulePath = arg.split("=", 2)[1]; + } + + if (arg.equals("--add-exports")) { + addExports.add(jvmArgs[i + 1]); + } else if (arg.startsWith("--add-exports=")) { + addExports.add(arg.split("=", 2)[1]); + } + + if (arg.equals("--add-opens")) { + addOpens.add(jvmArgs[i + 1]); + } else if (arg.startsWith("--add-opens=")) { + addOpens.add(arg.split("=", 2)[1]); + } + + // Java properties + if (arg.startsWith("-D")) { + String[] prop = arg.substring(2).split("=", 2); + + if (prop[0].equals("ignoreList")) { + // The default ignoreList may cause some problems, so we define it more precisely. + System.setProperty(prop[0], prop[1] + ",NewLaunch.jar,ForgeWrapper-," + minecraftJar); + } else { + System.setProperty(prop[0], prop[1]); + } + } + } + + if (System.getProperty("libraryDirectory") == null) { + System.setProperty("libraryDirectory", libraryDir); + } + + if (modulePath != null) { + ModuleUtil.addModules(modulePath); + } + ModuleUtil.addExports(addExports); + ModuleUtil.addOpens(addOpens); + } +} diff --git a/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Installer.java b/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Installer.java new file mode 100644 index 0000000000..98b9d1afa3 --- /dev/null +++ b/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Installer.java @@ -0,0 +1,182 @@ +package io.github.zekerzhayard.forgewrapper.installer; + +import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import net.minecraftforge.installer.DownloadUtils; +import net.minecraftforge.installer.actions.PostProcessors; +import net.minecraftforge.installer.actions.ProgressCallback; +import net.minecraftforge.installer.json.Artifact; +import net.minecraftforge.installer.json.Install; +import net.minecraftforge.installer.json.InstallV1; +import net.minecraftforge.installer.json.Util; +import net.minecraftforge.installer.json.Version; + +public class Installer { + private static InstallV1Wrapper wrapper; + private static InstallV1Wrapper getWrapper(File librariesDir) { + if (wrapper == null) { + wrapper = new InstallV1Wrapper(Util.loadInstallProfile(), librariesDir); + } + return wrapper; + } + + public static Map<String, Object> getData(File librariesDir) { + Map<String, Object> data = new HashMap<>(); + Version0 version = Version0.loadVersion(getWrapper(librariesDir)); + data.put("mainClass", version.getMainClass()); + data.put("jvmArgs", version.getArguments().getJvm()); + data.put("extraLibraries", getExtraLibraries(version)); + return data; + } + + public static boolean install(File libraryDir, File minecraftJar, File installerJar) throws Throwable { + ProgressCallback monitor = ProgressCallback.withOutputs(System.out); + if (System.getProperty("java.net.preferIPv4Stack") == null) { + System.setProperty("java.net.preferIPv4Stack", "true"); + } + String vendor = System.getProperty("java.vendor", "missing vendor"); + String javaVersion = System.getProperty("java.version", "missing java version"); + String jvmVersion = System.getProperty("java.vm.version", "missing jvm version"); + monitor.message(String.format("JVM info: %s - %s - %s", vendor, javaVersion, jvmVersion)); + monitor.message("java.net.preferIPv4Stack=" + System.getProperty("java.net.preferIPv4Stack")); + monitor.message("Current Time: " + new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(new Date())); + + // MinecraftForge has removed all old installers since 2024/2/27, but they still exist in NeoForge. + PostProcessors processors = new PostProcessors(wrapper, true, monitor); + Method processMethod = PostProcessors.class.getMethod("process", File.class, File.class, File.class, File.class); + if (boolean.class.equals(processMethod.getReturnType())) { + return (boolean) processMethod.invoke(processors, libraryDir, minecraftJar, libraryDir.getParentFile(), installerJar); + } else { + return processMethod.invoke(processors, libraryDir, minecraftJar, libraryDir.getParentFile(), installerJar) != null; + } + } + + // Some libraries in the version json are not available via direct download, + // so they are not available in the MultiMC meta json, + // so wee need to get them manually. + private static List<String> getExtraLibraries(Version0 version) { + List<String> paths = new ArrayList<>(); + for (Version.Library library : version.getLibraries()) { + Version.LibraryDownload artifact = library.getDownloads().getArtifact(); + if (artifact.getUrl().isEmpty()) { + paths.add(artifact.getPath()); + } + } + return paths; + } + + public static class InstallV1Wrapper extends InstallV1 { + protected Map<String, List<Processor>> processors = new HashMap<>(); + protected File librariesDir; + + public InstallV1Wrapper(InstallV1 v1, File librariesDir) { + super(v1); + this.serverJarPath = v1.getServerJarPath(); + this.librariesDir = librariesDir; + } + + @Override + public List<Processor> getProcessors(String side) { + List<Processor> processor = this.processors.get(side); + if (processor == null) { + checkProcessorFiles(processor = super.getProcessors(side), super.getData("client".equals(side)), this.librariesDir); + this.processors.put(side, processor); + } + // It can also be defined by JVM argument "-Dforgewrapper.skipHashCheck=true". + if (Boolean.getBoolean("forgewrapper.skipHashCheck")){ + processor.forEach(proc -> proc.getOutputs().clear()); + } + return processor; + } + + private static void checkProcessorFiles(List<Processor> processors, Map<String, String> data, File base) { + Map<String, File> artifactData = new HashMap<>(); + for (Map.Entry<String, String> entry : data.entrySet()) { + String value = entry.getValue(); + if (value.charAt(0) == '[' && value.charAt(value.length() - 1) == ']') { + artifactData.put("{" + entry.getKey() + "}", Artifact.from(value.substring(1, value.length() - 1)).getLocalPath(base)); + } + } + + Map<Processor, Map<String, String>> outputsMap = new HashMap<>(); + label: + for (Processor processor : processors) { + Map<String, String> outputs = new HashMap<>(); + if (processor.getOutputs().isEmpty()) { + String[] args = processor.getArgs(); + for (int i = 0; i < args.length; i++) { + for (Map.Entry<String, File> entry : artifactData.entrySet()) { + if (args[i].contains(entry.getKey())) { + // We assume that all files that exist but don't have the sha1 checksum are valid. + if (entry.getValue().exists()) { + outputs.put(entry.getKey(), DownloadUtils.getSha1(entry.getValue())); + } else { + outputsMap.clear(); + break label; + } + } + } + } + outputsMap.put(processor, outputs); + } + } + for (Map.Entry<Processor, Map<String, String>> entry : outputsMap.entrySet()) { + setOutputs(entry.getKey(), entry.getValue()); + } + } + + private static Field outputsField; + private static void setOutputs(Processor processor, Map<String, String> outputs) { + try { + if (outputsField == null) { + outputsField = Processor.class.getDeclaredField("outputs"); + outputsField.setAccessible(true); + } + outputsField.set(processor, outputs); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + } + + public static class Version0 extends Version { + + public static Version0 loadVersion(Install profile) { + try (InputStream stream = Util.class.getResourceAsStream(profile.getJson())) { + return Util.GSON.fromJson(new InputStreamReader(stream, StandardCharsets.UTF_8), Version0.class); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + protected String mainClass; + protected Version0.Arguments arguments; + + public String getMainClass() { + return mainClass; + } + + public Version0.Arguments getArguments() { + return arguments; + } + + public static class Arguments { + protected String[] jvm; + + public String[] getJvm() { + return jvm; + } + } + } +} diff --git a/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Main.java b/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Main.java new file mode 100644 index 0000000000..2c0f4cfc29 --- /dev/null +++ b/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Main.java @@ -0,0 +1,74 @@ +package io.github.zekerzhayard.forgewrapper.installer; + +import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.github.zekerzhayard.forgewrapper.installer.detector.DetectorLoader; +import io.github.zekerzhayard.forgewrapper.installer.detector.IFileDetector; +import io.github.zekerzhayard.forgewrapper.installer.util.ModuleUtil; + +public class Main { + @SuppressWarnings("unchecked") + public static void main(String[] args) throws Throwable { + // --fml.neoForgeVersion 20.2.20-beta --fml.fmlVersion 1.0.2 --fml.mcVersion 1.20.2 --fml.neoFormVersion 20231019.002635 --launchTarget forgeclient + + List<String> argsList = Stream.of(args).collect(Collectors.toList()); + // NOTE: this is only true for NeoForge versions past 20.2.x + // early versions of NeoForge (for 1.20.1) are not supposed to be covered here + boolean isNeoForge = argsList.contains("--fml.neoForgeVersion"); + + String mcVersion = argsList.get(argsList.indexOf("--fml.mcVersion") + 1); + String forgeGroup = argsList.contains("--fml.forgeGroup") ? argsList.get(argsList.indexOf("--fml.forgeGroup") + 1) : "net.neoforged"; + String forgeArtifact = isNeoForge ? "neoforge" : "forge"; + String forgeVersionKey = isNeoForge ? "--fml.neoForgeVersion" : "--fml.forgeVersion"; + String forgeVersion = argsList.get(argsList.indexOf(forgeVersionKey) + 1); + String forgeFullVersion = isNeoForge ? forgeVersion : mcVersion + "-" + forgeVersion; + + IFileDetector detector = DetectorLoader.loadDetector(); + // Check installer jar. + Path installerJar = detector.getInstallerJar(forgeGroup, forgeArtifact, forgeFullVersion); + if (!isFile(installerJar)) { + throw new RuntimeException("Unable to detect the forge installer!"); + } + + // Check vanilla Minecraft jar. + Path minecraftJar = detector.getMinecraftJar(mcVersion); + if (!isFile(minecraftJar)) { + throw new RuntimeException("Unable to detect the Minecraft jar!"); + } + + try (URLClassLoader ucl = URLClassLoader.newInstance(new URL[] { + Main.class.getProtectionDomain().getCodeSource().getLocation(), + installerJar.toUri().toURL() + }, ModuleUtil.getPlatformClassLoader())) { + Class<?> installer = ucl.loadClass("io.github.zekerzhayard.forgewrapper.installer.Installer"); + + Map<String, Object> data = (Map<String, Object>) installer.getMethod("getData", File.class).invoke(null, detector.getLibraryDir().toFile()); + try { + Bootstrap.bootstrap((String[]) data.get("jvmArgs"), detector.getMinecraftJar(mcVersion).getFileName().toString(), detector.getLibraryDir().toAbsolutePath().toString()); + } catch (Throwable t) { + // Avoid this bunch of hacks that nuke the whole wrapper. + t.printStackTrace(); + } + + if (!((boolean) installer.getMethod("install", File.class, File.class, File.class).invoke(null, detector.getLibraryDir().toFile(), minecraftJar.toFile(), installerJar.toFile()))) { + return; + } + + ModuleUtil.setupClassPath(detector.getLibraryDir(), (List<String>) data.get("extraLibraries")); + Class<?> mainClass = ModuleUtil.setupBootstrapLauncher(Class.forName((String) data.get("mainClass"))); + mainClass.getMethod("main", String[].class).invoke(null, new Object[] {args}); + } + } + + private static boolean isFile(Path path) { + return path != null && Files.isRegularFile(path); + } +} diff --git a/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/DetectorLoader.java b/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/DetectorLoader.java new file mode 100644 index 0000000000..369c4b555d --- /dev/null +++ b/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/DetectorLoader.java @@ -0,0 +1,35 @@ +package io.github.zekerzhayard.forgewrapper.installer.detector; + +import java.util.HashMap; +import java.util.Map; +import java.util.ServiceLoader; + +public class DetectorLoader { + public static IFileDetector loadDetector() { + ServiceLoader<IFileDetector> sl = ServiceLoader.load(IFileDetector.class); + HashMap<String, IFileDetector> detectors = new HashMap<>(); + for (IFileDetector detector : sl) { + detectors.put(detector.name(), detector); + } + + boolean enabled = false; + IFileDetector temp = null; + for (Map.Entry<String, IFileDetector> detector : detectors.entrySet()) { + HashMap<String, IFileDetector> others = new HashMap<>(detectors); + others.remove(detector.getKey()); + if (!enabled) { + enabled = detector.getValue().enabled(others); + if (enabled) { + temp = detector.getValue(); + } + } else if (detector.getValue().enabled(others)) { + throw new RuntimeException("There are two or more file detectors are enabled! (" + temp.toString() + ", " + detector.toString() + ")"); + } + } + + if (temp == null) { + throw new RuntimeException("No file detector is enabled!"); + } + return temp; + } +} diff --git a/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/IFileDetector.java b/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/IFileDetector.java new file mode 100644 index 0000000000..6f695ff884 --- /dev/null +++ b/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/IFileDetector.java @@ -0,0 +1,99 @@ +package io.github.zekerzhayard.forgewrapper.installer.detector; + +import java.net.URL; +import java.net.URISyntaxException; +import java.net.MalformedURLException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; + +public interface IFileDetector { + /** + * @return The name of the detector. + */ + String name(); + + /** + * If there are two or more detectors are enabled, an exception will be thrown. Removing anything from the map is in vain. + * @param others Other detectors. + * @return True represents enabled. + */ + boolean enabled(HashMap<String, IFileDetector> others); + + /** + * @return The ".minecraft/libraries" folder for normal. It can also be defined by JVM argument "-Dforgewrapper.librariesDir=<libraries-path>". + */ + default Path getLibraryDir() { + String libraryDir = System.getProperty("forgewrapper.librariesDir"); + if (libraryDir != null) { + return Paths.get(libraryDir).toAbsolutePath(); + } + try { + URL launcherLocation = null; + String[] classNames = { + "cpw/mods/modlauncher/Launcher.class", + "net/neoforged/fml/loading/FMLLoader.class" + }; + + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + for (String classResource : classNames) { + URL url = cl.getResource(classResource); + if (url != null) { + String path = url.toString(); + if (path.startsWith("jar:") && path.contains("!")) { + path = path.substring(4, path.indexOf('!')); + try { + launcherLocation = new URL(path); + break; + } catch (MalformedURLException e) { + // ignore and try next + } + } + } + } + + if (launcherLocation == null) { + throw new UnsupportedOperationException("Could not detect the libraries folder - it can be manually specified with `-Dforgewrapper.librariesDir=` (Java runtime argument)"); + } + Path launcher = Paths.get(launcherLocation.toURI()); + + while (!launcher.getFileName().toString().equals("libraries")) { + launcher = launcher.getParent(); + + if (launcher == null || launcher.getFileName() == null) { + throw new UnsupportedOperationException("Could not detect the libraries folder - it can be manually specified with `-Dforgewrapper.librariesDir=` (Java runtime argument)"); + } + } + + return launcher.toAbsolutePath(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + /** + * @param forgeGroup Forge package group (e.g. net.minecraftforge). + * @param forgeArtifact Forge package artifact (e.g. forge). + * @param forgeFullVersion Forge full version (e.g. 1.14.4-28.2.0). + * @return The forge installer jar path. It can also be defined by JVM argument "-Dforgewrapper.installer=<installer-path>". + */ + default Path getInstallerJar(String forgeGroup, String forgeArtifact, String forgeFullVersion) { + String installer = System.getProperty("forgewrapper.installer"); + if (installer != null) { + return Paths.get(installer).toAbsolutePath(); + } + return null; + } + + /** + * @param mcVersion Minecraft version (e.g. 1.14.4). + * @return The minecraft client jar path. It can also be defined by JVM argument "-Dforgewrapper.minecraft=<minecraft-path>". + */ + default Path getMinecraftJar(String mcVersion) { + String minecraft = System.getProperty("forgewrapper.minecraft"); + if (minecraft != null) { + return Paths.get(minecraft).toAbsolutePath(); + } + return null; + } +}
\ No newline at end of file diff --git a/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/MultiMCFileDetector.java b/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/MultiMCFileDetector.java new file mode 100644 index 0000000000..a1216f0947 --- /dev/null +++ b/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/MultiMCFileDetector.java @@ -0,0 +1,52 @@ +package io.github.zekerzhayard.forgewrapper.installer.detector; + +import java.nio.file.Path; +import java.util.HashMap; + +public class MultiMCFileDetector implements IFileDetector { + protected Path libraryDir = null; + protected Path installerJar = null; + protected Path minecraftJar = null; + + @Override + public String name() { + return "MultiMC"; + } + + @Override + public boolean enabled(HashMap<String, IFileDetector> others) { + return others.size() == 0; + } + + @Override + public Path getLibraryDir() { + if (this.libraryDir == null) { + this.libraryDir = IFileDetector.super.getLibraryDir(); + } + return this.libraryDir; + } + + @Override + public Path getInstallerJar(String forgeGroup, String forgeArtifact, String forgeFullVersion) { + Path path = IFileDetector.super.getInstallerJar(forgeGroup, forgeArtifact, forgeFullVersion); + if (path == null) { + if (this.installerJar == null) { + Path installerBase = this.getLibraryDir(); + for (String dir : forgeGroup.split("\\.")) + installerBase = installerBase.resolve(dir); + this.installerJar = installerBase.resolve(forgeArtifact).resolve(forgeFullVersion).resolve(forgeArtifact + "-" + forgeFullVersion + "-installer.jar").toAbsolutePath(); + } + return this.installerJar; + } + return path; + } + + @Override + public Path getMinecraftJar(String mcVersion) { + Path path = IFileDetector.super.getMinecraftJar(mcVersion); + if (path == null) { + return this.minecraftJar != null ? this.minecraftJar : (this.minecraftJar = this.getLibraryDir().resolve("com").resolve("mojang").resolve("minecraft").resolve(mcVersion).resolve("minecraft-" + mcVersion + "-client.jar").toAbsolutePath()); + } + return path; + } +} diff --git a/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/util/ModuleUtil.java b/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/util/ModuleUtil.java new file mode 100644 index 0000000000..9eee574cf3 --- /dev/null +++ b/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/util/ModuleUtil.java @@ -0,0 +1,40 @@ +package io.github.zekerzhayard.forgewrapper.installer.util; + +import java.io.File; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.util.List; + +public class ModuleUtil { + public static void addModules(String modulePath) { + // nothing to do with Java 8 + } + + public static void addExports(List<String> exports) { + // nothing to do with Java 8 + } + + public static void addOpens(List<String> opens) { + // nothing to do with Java 8 + } + + public static void setupClassPath(Path libraryDir, List<String> paths) throws Throwable { + Method addURLMethod = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); + addURLMethod.setAccessible(true); + for (String path : paths) { + addURLMethod.invoke(ClassLoader.getSystemClassLoader(), libraryDir.resolve(path).toUri().toURL()); + } + } + + public static Class<?> setupBootstrapLauncher(Class<?> mainClass) { + // nothing to do with Java 8 + return mainClass; + } + + public static ClassLoader getPlatformClassLoader() { + // PlatformClassLoader does not exist in Java 8 + return null; + } +} diff --git a/forgewrapper/src/main/resources/META-INF/services/io.github.zekerzhayard.forgewrapper.installer.detector.IFileDetector b/forgewrapper/src/main/resources/META-INF/services/io.github.zekerzhayard.forgewrapper.installer.detector.IFileDetector new file mode 100644 index 0000000000..31f2c4e711 --- /dev/null +++ b/forgewrapper/src/main/resources/META-INF/services/io.github.zekerzhayard.forgewrapper.installer.detector.IFileDetector @@ -0,0 +1 @@ +io.github.zekerzhayard.forgewrapper.installer.detector.MultiMCFileDetector |
