diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltJoinedFileSystemProvider.java b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltJoinedFileSystemProvider.java index e376498b8..4034e0061 100644 --- a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltJoinedFileSystemProvider.java +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltJoinedFileSystemProvider.java @@ -59,10 +59,15 @@ @SuppressWarnings("unchecked") // TODO make more specific @QuiltLoaderInternal(QuiltLoaderInternalType.LEGACY_EXPOSED) public final class QuiltJoinedFileSystemProvider extends FileSystemProvider { - public QuiltJoinedFileSystemProvider() {} + public QuiltJoinedFileSystemProvider() { + if (instance == null) { + instance = this; + } + } public static final String SCHEME = "quilt.jfs"; + private static QuiltJoinedFileSystemProvider instance; private static final Map> ACTIVE_FILESYSTEMS = new HashMap<>(); static { @@ -108,12 +113,25 @@ static synchronized void closeFileSystem(QuiltJoinedFileSystem fs) { } public static QuiltJoinedFileSystemProvider instance() { + QuiltJoinedFileSystemProvider found = findInstance(); + if (found != null) { + return found; + } + throw new IllegalStateException("Unable to load QuiltJoinedFileSystemProvider via services!"); + } + + public static QuiltJoinedFileSystemProvider findInstance() { + if (instance != null) { + return instance; + } + for (FileSystemProvider provider : FileSystemProvider.installedProviders()) { if (provider instanceof QuiltJoinedFileSystemProvider) { return (QuiltJoinedFileSystemProvider) provider; } } - throw new IllegalStateException("Unable to load QuiltJoinedFileSystemProvider via services!"); + + return instance; } @Override diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMemoryFileSystemProvider.java b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMemoryFileSystemProvider.java index 0ec541b41..655d997ba 100644 --- a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMemoryFileSystemProvider.java +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltMemoryFileSystemProvider.java @@ -20,7 +20,6 @@ import java.net.URI; import java.nio.file.FileStore; import java.nio.file.FileSystem; -import java.nio.file.FileSystemNotFoundException; import java.nio.file.Path; import java.nio.file.spi.FileSystemProvider; import java.util.Map; @@ -30,20 +29,39 @@ @QuiltLoaderInternal(QuiltLoaderInternalType.LEGACY_EXPOSED) public final class QuiltMemoryFileSystemProvider extends QuiltMapFileSystemProvider { - public QuiltMemoryFileSystemProvider() {} + public QuiltMemoryFileSystemProvider() { + if (instance == null) { + instance = this; + } + } public static final String SCHEME = "quilt.mfs"; + private static QuiltMemoryFileSystemProvider instance; + static final String READ_ONLY_EXCEPTION = "This FileSystem is read-only"; static final QuiltFSP PROVIDER = new QuiltFSP<>(SCHEME); public static QuiltMemoryFileSystemProvider instance() { + QuiltMemoryFileSystemProvider found = findInstance(); + if (found != null) { + return found; + } + throw new IllegalStateException("Unable to load QuiltMemoryFileSystemProvider via services!"); + } + + public static QuiltMemoryFileSystemProvider findInstance() { + if (instance != null) { + return instance; + } + for (FileSystemProvider provider : FileSystemProvider.installedProviders()) { if (provider instanceof QuiltMemoryFileSystemProvider) { return (QuiltMemoryFileSystemProvider) provider; } } - throw new IllegalStateException("Unable to load QuiltMemoryFileSystemProvider via services!"); + + return instance; } @Override diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltUnifiedFileSystemProvider.java b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltUnifiedFileSystemProvider.java index 0be9874a6..e9ae5bdc5 100644 --- a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltUnifiedFileSystemProvider.java +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltUnifiedFileSystemProvider.java @@ -40,20 +40,39 @@ @QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) public class QuiltUnifiedFileSystemProvider extends QuiltMapFileSystemProvider { - public QuiltUnifiedFileSystemProvider() {} + public QuiltUnifiedFileSystemProvider() { + if (instance == null) { + instance = this; + } + } public static final String SCHEME = "quilt.ufs"; static final String READ_ONLY_EXCEPTION = "This FileSystem is read-only"; static final QuiltFSP PROVIDER = new QuiltFSP<>(SCHEME); + private static QuiltUnifiedFileSystemProvider instance; + public static QuiltUnifiedFileSystemProvider instance() { + QuiltUnifiedFileSystemProvider found = findInstance(); + if (found != null) { + return found; + } + throw new IllegalStateException("Unable to load QuiltUnifiedFileSystemProvider via services!"); + } + + public static QuiltUnifiedFileSystemProvider findInstance() { + if (instance != null) { + return instance; + } + for (FileSystemProvider provider : FileSystemProvider.installedProviders()) { if (provider instanceof QuiltUnifiedFileSystemProvider) { return (QuiltUnifiedFileSystemProvider) provider; } } - throw new IllegalStateException("Unable to load QuiltUnifiedFileSystemProvider via services!"); + + return instance; } @Override diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltZipFileSystemProvider.java b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltZipFileSystemProvider.java index 5aaba14a3..a8c65cfd6 100644 --- a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltZipFileSystemProvider.java +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltZipFileSystemProvider.java @@ -29,18 +29,37 @@ @QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) public class QuiltZipFileSystemProvider extends QuiltMapFileSystemProvider { + public QuiltZipFileSystemProvider() { + if (instance == null) { + instance = this; + } + } public static final String SCHEME = "quilt.zfs"; static final String READ_ONLY_EXCEPTION = "This FileSystem is read-only"; static final QuiltFSP PROVIDER = new QuiltFSP<>(SCHEME); + private static QuiltZipFileSystemProvider instance; public static QuiltZipFileSystemProvider instance() { + QuiltZipFileSystemProvider found = findInstance(); + if (found != null) { + return found; + } + throw new IllegalStateException("Unable to load QuiltZipFileSystemProvider via services!"); + } + + public static QuiltZipFileSystemProvider findInstance() { + if (instance != null) { + return instance; + } + for (FileSystemProvider provider : FileSystemProvider.installedProviders()) { if (provider instanceof QuiltZipFileSystemProvider) { return (QuiltZipFileSystemProvider) provider; } } - throw new IllegalStateException("Unable to load QuiltZipFileSystemProvider via services!"); + + return instance; } @Override diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/quilt/jfs/Handler.java b/src/main/java/org/quiltmc/loader/impl/filesystem/quilt/jfs/Handler.java index bd26fcb1d..6ff00f298 100644 --- a/src/main/java/org/quiltmc/loader/impl/filesystem/quilt/jfs/Handler.java +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/quilt/jfs/Handler.java @@ -23,8 +23,6 @@ import java.net.URL; import java.net.URLConnection; import java.net.URLStreamHandler; -import java.nio.file.Files; -import java.nio.file.Path; import org.quiltmc.loader.impl.filesystem.QuiltJoinedFileSystem; import org.quiltmc.loader.impl.filesystem.QuiltJoinedFileSystemProvider; @@ -36,7 +34,7 @@ @QuiltLoaderInternal(QuiltLoaderInternalType.LEGACY_EXPOSED) public class Handler extends URLStreamHandler { @Override - protected URLConnection openConnection(URL u) throws IOException { + public URLConnection openConnection(URL u) throws IOException { QuiltJoinedPath path; try { path = QuiltJoinedFileSystemProvider.instance().getPath(u.toURI()); @@ -58,7 +56,7 @@ public InputStream getInputStream() throws IOException { } @Override - protected InetAddress getHostAddress(URL u) { + public InetAddress getHostAddress(URL u) { return null; } } diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/quilt/mfs/Handler.java b/src/main/java/org/quiltmc/loader/impl/filesystem/quilt/mfs/Handler.java index 3deb2df08..6a66edb4a 100644 --- a/src/main/java/org/quiltmc/loader/impl/filesystem/quilt/mfs/Handler.java +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/quilt/mfs/Handler.java @@ -34,7 +34,7 @@ @QuiltLoaderInternal(QuiltLoaderInternalType.LEGACY_EXPOSED) public class Handler extends URLStreamHandler { @Override - protected URLConnection openConnection(URL u) throws IOException { + public URLConnection openConnection(URL u) throws IOException { QuiltMemoryPath path; try { path = QuiltMemoryFileSystemProvider.instance().getPath(u.toURI()); @@ -56,7 +56,7 @@ public InputStream getInputStream() throws IOException { } @Override - protected InetAddress getHostAddress(URL u) { + public InetAddress getHostAddress(URL u) { return null; } } diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/quilt/ufs/Handler.java b/src/main/java/org/quiltmc/loader/impl/filesystem/quilt/ufs/Handler.java index 56aa6b5a5..40b2a454c 100644 --- a/src/main/java/org/quiltmc/loader/impl/filesystem/quilt/ufs/Handler.java +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/quilt/ufs/Handler.java @@ -34,7 +34,7 @@ @QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) public class Handler extends URLStreamHandler { @Override - protected URLConnection openConnection(URL u) throws IOException { + public URLConnection openConnection(URL u) throws IOException { QuiltUnifiedPath path; try { path = QuiltUnifiedFileSystemProvider.instance().getPath(u.toURI()); @@ -56,7 +56,7 @@ public InputStream getInputStream() throws IOException { } @Override - protected InetAddress getHostAddress(URL u) { + public InetAddress getHostAddress(URL u) { return null; } } diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/quilt/zfs/Handler.java b/src/main/java/org/quiltmc/loader/impl/filesystem/quilt/zfs/Handler.java index 10f776124..ce89c5897 100644 --- a/src/main/java/org/quiltmc/loader/impl/filesystem/quilt/zfs/Handler.java +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/quilt/zfs/Handler.java @@ -34,7 +34,7 @@ @QuiltLoaderInternal(QuiltLoaderInternalType.LEGACY_EXPOSED) public class Handler extends URLStreamHandler { @Override - protected URLConnection openConnection(URL u) throws IOException { + public URLConnection openConnection(URL u) throws IOException { QuiltZipPath path; try { path = QuiltZipFileSystemProvider.instance().getPath(u.toURI()); @@ -56,7 +56,7 @@ public InputStream getInputStream() throws IOException { } @Override - protected InetAddress getHostAddress(URL u) { + public InetAddress getHostAddress(URL u) { return null; } } diff --git a/src/main/java/org/quiltmc/loader/impl/launch/boot/BootLauncher.java b/src/main/java/org/quiltmc/loader/impl/launch/boot/BootLauncher.java new file mode 100644 index 000000000..75f5d55d5 --- /dev/null +++ b/src/main/java/org/quiltmc/loader/impl/launch/boot/BootLauncher.java @@ -0,0 +1,246 @@ +/* + * Copyright 2022, 2023 QuiltMC + * + * 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 org.quiltmc.loader.impl.launch.boot; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.invoke.MethodType; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLStreamHandler; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.spi.FileSystemProvider; +import java.util.Arrays; +import java.util.function.Consumer; + +import org.quiltmc.loader.impl.filesystem.QuiltJoinedFileSystemProvider; +import org.quiltmc.loader.impl.filesystem.QuiltMemoryFileSystemProvider; +import org.quiltmc.loader.impl.filesystem.QuiltUnifiedFileSystemProvider; +import org.quiltmc.loader.impl.filesystem.QuiltZipFileSystemProvider; +import org.quiltmc.loader.impl.launch.boot.QuiltInstallerJson.QuiltInstallerLibrary; +import org.quiltmc.loader.impl.launch.knot.Knot; +import org.quiltmc.loader.impl.launch.knot.KnotClient; +import org.quiltmc.loader.impl.launch.knot.KnotServer; +import org.quiltmc.loader.impl.launch.server.QuiltServerLauncher; +import org.quiltmc.loader.impl.util.QuiltLoaderInternal; +import org.quiltmc.loader.impl.util.QuiltLoaderInternalType; +import org.quiltmc.loader.impl.util.SystemProperties; + +@QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) +public class BootLauncher { + /* + * IMPORTANT: + * + * This class is loaded BEFORE all of loaders libraries are on the class path. + * This means we can only use: + * - Classes from the standard java libraries + * - Classes in libraries which loader shades + * - Classes in loader itself which only use the above two classes. + */ + + public Path run(Object context, String[] args) { + + // TODO: Implement update mechanism! + // As in, this loader version should read a file with the *new* loader version, + // verify that it exists, and relaunch with it (return that path) + + final String env; + + // void #(char, FileSystemProvider, URLStreamHandler) + final FileSystemInit putFileSystemProvider; + + // void #(URL) + final Consumer addToClassPath; + + try { + Class ctxClass = context.getClass(); + env = (String) ctxClass.getMethod("environment").invoke(context); + Lookup lookup = MethodHandles.lookup(); + MethodHandle putFSP = lookup( + context, "putFileSystemProvider", void.class, char.class, FileSystemProvider.class, + URLStreamHandler.class + ); + + putFileSystemProvider = (letter, fsp, handler) -> { + try { + putFSP.invokeWithArguments(letter, fsp, handler); + } catch (Throwable e) { + throw rethrow(e); + } + }; + + MethodHandle addToClassPathMethod = lookup(context, "addToClassPath", void.class, URL.class); + + addToClassPath = url -> { + try { + addToClassPathMethod.invokeWithArguments(url); + } catch (Throwable e) { + throw rethrow(e); + } + }; + + } catch (ReflectiveOperationException e) { + throw new Error("Failed to read some methods from the bootstrap!", e); + } + + addLoaderLibraries(addToClassPath); + putFileSystems(putFileSystemProvider); + + switch (env) { + case "NONE": { + Knot.main(args); + break; + } + case "CLIENT": { + KnotClient.main(args); + break; + } + case "SERVER": { + KnotServer.main(args); + break; + } + case "SERVER_LAUNCHER": { + QuiltServerLauncher.main(args); + break; + } + default: { + throw new IllegalArgumentException("Unknown environment '" + env + "'"); + } + } + + // Relaunch not currently used. + return null; + } + + private static RuntimeException rethrow(Throwable e) throws Error { + if (e instanceof Error) { + throw (Error) e; + } + if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } + throw new RuntimeException(e); + } + + private static MethodHandle lookup(Object target, String methodName, Class returnType, Class... argTypes) { + try { + MethodType type = MethodType.methodType(returnType, argTypes); + return MethodHandles.publicLookup().findVirtual(target.getClass(), methodName, type).bindTo(target); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new IllegalStateException("Unable to find " + methodName + "!", e); + } + } + + private void addLoaderLibraries(Consumer to) { + QuiltInstallerJson installerInfo; + + try (InputStream stream = getClass().getResourceAsStream("/quilt_installer.json")) { + if (stream == null) { + throw new Error("Unable to find 'quilt_installer.json'"); + } + installerInfo = new QuiltInstallerJson(stream); + } catch (IOException e) { + throw new Error("Unable to read 'quilt_installer.json'!", e); + } + + String mavenRoot = System.getProperty(SystemProperties.BOOT_LIBRARY_ROOT); + if (mavenRoot == null) { + throw new Error("Missing maven root system property '" + SystemProperties.BOOT_LIBRARY_ROOT + "'"); + } + Path root = Paths.get(mavenRoot); + if (!Files.isDirectory(root)) { + throw new Error("Cannot bootstrap - missing maven root folder " + root); + } + + for (QuiltInstallerLibrary lib : installerInfo.libraries) { + String name = lib.name; + String[] sections = name.split(":"); + if (sections.length != 3) { + throw new Error("Bad maven name: " + name + ", resulting in " + Arrays.toString(sections)); + } + String group = sections[0].replace(".", root.getFileSystem().getSeparator()); + String artifact = sections[1]; + String version = sections[2]; + + Path jar = root.resolve(group).resolve(artifact).resolve(version)// + .resolve(artifact + "-" + version + ".jar"); + + if (!Files.exists(jar)) { + throw new Error("Missing required library: " + jar); + } + + try { + to.accept(jar.toUri().toURL()); + } catch (MalformedURLException e) { + throw new Error("Failed to convert " + jar + " to a URL!", e); + } + } + } + + @FunctionalInterface + @QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) + interface FileSystemInit { + void putFileSystemProvider(char letter, FileSystemProvider fsp, URLStreamHandler handler); + } + + private static void putFileSystems(FileSystemInit to) { + // Joined FS + { + QuiltJoinedFileSystemProvider jfs = QuiltJoinedFileSystemProvider.findInstance(); + if (jfs == null) { + jfs = new QuiltJoinedFileSystemProvider(); + } + // We don't care about creating lots of stream handlers. + to.putFileSystemProvider('j', jfs, new org.quiltmc.loader.impl.filesystem.quilt.jfs.Handler()); + } + + // Memory FS + { + QuiltMemoryFileSystemProvider mfs = QuiltMemoryFileSystemProvider.findInstance(); + if (mfs == null) { + mfs = new QuiltMemoryFileSystemProvider(); + } + // We don't care about creating lots of stream handlers. + to.putFileSystemProvider('m', mfs, new org.quiltmc.loader.impl.filesystem.quilt.mfs.Handler()); + } + + // Unified FS + { + QuiltUnifiedFileSystemProvider ufs = QuiltUnifiedFileSystemProvider.findInstance(); + if (ufs == null) { + ufs = new QuiltUnifiedFileSystemProvider(); + } + // We don't care about creating lots of stream handlers. + to.putFileSystemProvider('u', ufs, new org.quiltmc.loader.impl.filesystem.quilt.ufs.Handler()); + } + + // Zip FS + { + QuiltZipFileSystemProvider zfs = QuiltZipFileSystemProvider.findInstance(); + if (zfs == null) { + zfs = new QuiltZipFileSystemProvider(); + } + // We don't care about creating lots of stream handlers. + to.putFileSystemProvider('z', zfs, new org.quiltmc.loader.impl.filesystem.quilt.zfs.Handler()); + } + } +} diff --git a/src/main/java/org/quiltmc/loader/impl/launch/boot/QuiltInstallerJson.java b/src/main/java/org/quiltmc/loader/impl/launch/boot/QuiltInstallerJson.java new file mode 100644 index 000000000..619b37669 --- /dev/null +++ b/src/main/java/org/quiltmc/loader/impl/launch/boot/QuiltInstallerJson.java @@ -0,0 +1,71 @@ +/* + * Copyright 2022, 2023 QuiltMC + * + * 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 org.quiltmc.loader.impl.launch.boot; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import org.quiltmc.loader.api.LoaderValue; +import org.quiltmc.loader.api.LoaderValue.LObject; +import org.quiltmc.loader.api.LoaderValue.LType; +import org.quiltmc.loader.api.plugin.LoaderValueFactory; +import org.quiltmc.loader.impl.util.QuiltLoaderInternal; +import org.quiltmc.loader.impl.util.QuiltLoaderInternalType; + +@QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) +class QuiltInstallerJson { + + final List libraries = new ArrayList<>(); + + QuiltInstallerJson(InputStream stream) throws IOException { + LoaderValue lValue = LoaderValueFactory.getFactory().read(stream); + if (lValue.type() != LType.OBJECT) { + throw new IOException("Expected the document to be an object, but was " + lValue.type()); + } + LObject root = lValue.asObject(); + LoaderValue libs = root.get("libraries"); + if (libs == null || libs.type() != LType.OBJECT) { + throw new IOException("Expected to find 'libraries', but found " + libs); + } + // Loader doesn't have client or server-specific libraries + LoaderValue allLibs = libs.asObject().get("common"); + if (allLibs == null || allLibs.type() != LType.ARRAY) { + throw new IOException("Expected to find 'libraries.common', but found " + allLibs); + } + + for (LoaderValue lib : allLibs.asArray()) { + libraries.add(new QuiltInstallerLibrary(lib)); + } + } + + static class QuiltInstallerLibrary { + final String name; + + QuiltInstallerLibrary(LoaderValue from) throws IOException { + if (from.type() != LType.OBJECT) { + throw new IOException("Expected library to be an object, but got " + from); + } + LoaderValue nameValue = from.asObject().get("name"); + if (nameValue == null || nameValue.type() != LType.STRING) { + throw new IOException("Expected to find 'name', but found " + nameValue + " in " + from); + } + name = nameValue.asString(); + } + } +} diff --git a/src/main/java/org/quiltmc/loader/impl/util/SystemProperties.java b/src/main/java/org/quiltmc/loader/impl/util/SystemProperties.java index 113ca2d02..aa9afdf79 100644 --- a/src/main/java/org/quiltmc/loader/impl/util/SystemProperties.java +++ b/src/main/java/org/quiltmc/loader/impl/util/SystemProperties.java @@ -101,6 +101,7 @@ private SystemProperties() {} public static final String DISABLE_BUILTIN_MIXIN_EXTRAS = "loader.disable_builtin_mixin_extras"; /** whether the loader should display unsupported mods with the GUI or ignore and continue starting up the game. */ public static final String IGNORE_UNSUPPORTED_MODS = "loader.ignore_unsupported_mods"; + public static final String BOOT_LIBRARY_ROOT = "loader.boot.library_root"; /** Disables loader from registering its {@link URLStreamHandlerFactory} with * {@link URL#setURLStreamHandlerFactory(URLStreamHandlerFactory)}. This */ diff --git a/src/main/resources/META-INF/quilt-bootstrap/target.txt b/src/main/resources/META-INF/quilt-bootstrap/target.txt new file mode 100644 index 000000000..2de08e34e --- /dev/null +++ b/src/main/resources/META-INF/quilt-bootstrap/target.txt @@ -0,0 +1 @@ +org.quiltmc.loader.impl.launch.boot.BootLauncher