package io.gitlab.jfronny.inceptum.launcher.system.launch; import io.gitlab.jfronny.commons.OSUtils; import io.gitlab.jfronny.commons.ref.R; import io.gitlab.jfronny.inceptum.common.*; import io.gitlab.jfronny.inceptum.launcher.LauncherEnv; import io.gitlab.jfronny.inceptum.launcher.api.FabricMetaApi; import io.gitlab.jfronny.inceptum.launcher.api.McApi; import io.gitlab.jfronny.inceptum.launcher.api.account.AccountManager; import io.gitlab.jfronny.inceptum.launcher.api.account.AuthInfo; import io.gitlab.jfronny.inceptum.launcher.model.inceptum.ArtifactInfo; import io.gitlab.jfronny.inceptum.launcher.model.mojang.*; import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance; import io.gitlab.jfronny.inceptum.launcher.system.instance.Mod; import io.gitlab.jfronny.inceptum.launcher.system.setup.steps.DownloadLibrariesStep; import io.gitlab.jfronny.inceptum.launcher.util.ProcessState; import io.gitlab.jfronny.inceptum.launcher.util.VersionInfoLibraryResolver; import java.io.File; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.*; import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; public class InstanceLauncher { public static void launchClient(Instance instance) { if (AccountManager.accountMissing() && InceptumConfig.enforceAccount) { LauncherEnv.showError("You have not set up an account.\nDoing so is required to play Minecraft", "Not authenticated"); return; } AuthInfo authInfo = AccountManager.selectedAccount; if (authInfo.equals(AccountManager.NULL_AUTH)) { try { String sysUser = System.getProperty("user.name"); if (InceptumConfig.offlineAccountLastName == null) InceptumConfig.offlineAccountLastName = sysUser; LauncherEnv.getInput("User name", "Please enter the username to use for this session", InceptumConfig.offlineAccountLastName, name -> { InceptumConfig.offlineAccountLastName = name.equals(sysUser) ? null : name; InceptumConfig.saveConfig(); AuthInfo infoNew = new AuthInfo(name, authInfo.uuid, authInfo.accessToken, authInfo.userType); launchClient(instance, infoNew); }, R::nop); } catch (IOException e) { LauncherEnv.showError("Failed to request input", e); } } else launchClient(instance, authInfo); } private static void launchClient(Instance instance, AuthInfo authInfo) { try { launch(instance, LaunchType.Client, false, authInfo); } catch (LaunchException | IOException e) { LauncherEnv.showError("Could not launch client", e); } } public static void launch(Instance instance, LaunchType launchType, boolean restart, AuthInfo authInfo) throws LaunchException, IOException { if (authInfo == null) throw new LaunchException("authInfo is null"); VersionsListInfo versionDataSimple = getVersion(instance.gameVersion); VersionInfo versionInfo = McApi.getVersionInfo(versionDataSimple); // Add fabric metadata if using fabric if (instance.isFabric) { versionInfo = FabricMetaApi.addFabric(versionInfo, instance.loaderVersion, launchType.fabricMetaType); } // Ensure libs/assets are present DownloadLibrariesStep.execute(versionInfo, new AtomicBoolean(false), new ProcessState()); // Prepare arguments List args = new LinkedList<>(); // JVM path { final VersionInfo lambdaVersionInfo = versionInfo; args.add(Objects.requireNonNullElseGet(instance.meta.java, () -> OSUtils.getJvmBinary(MetaHolder.NATIVES_DIR .resolve(lambdaVersionInfo.javaVersion.component) .resolve(Integer.toString(lambdaVersionInfo.javaVersion.majorVersion))) .toAbsolutePath().toString())); } // Java classpath StringBuilder classPath = new StringBuilder(); for (ArtifactInfo artifact : VersionInfoLibraryResolver.getRelevant(versionInfo)) { classPath.append(MetaHolder.LIBRARIES_DIR.resolve(artifact.path)); classPath.append(File.pathSeparatorChar); } Path gameJar = MetaHolder.LIBRARIES_DIR.resolve("net/minecraft/" + launchType.name).resolve(versionDataSimple.id + ".jar"); classPath.append(gameJar); classPath.append(File.pathSeparatorChar); classPath.append(DownloadLibrariesStep.launchWrapperArtifact.localPath); // JVM arguments if (instance.meta.arguments != null && instance.meta.arguments.jvm != null) args.addAll(instance.meta.arguments.jvm); if (launchType == LaunchType.Client && versionInfo.arguments != null) args.addAll(parse(versionInfo.arguments.jvm, versionInfo, instance, classPath.toString(), authInfo)); if (instance.meta.minMem != null) args.add("-Xms" + instance.meta.minMem); if (instance.meta.maxMem != null) args.add("-Xmx" + instance.meta.maxMem); // Forceload natives if (Files.exists(MetaHolder.FORCE_LOAD_PATH)) { args.add("-Dinceptum.forceloadNatives=" + MetaHolder.FORCE_LOAD_PATH); } // Fabric imods if (instance.isFabric) { StringBuilder fabricAddMods = new StringBuilder("-Dfabric.addMods="); for (Mod mod : instance.mods) { if (mod.isEnabled && mod.needsInject) { fabricAddMods.append(mod.jarPath); fabricAddMods.append(File.pathSeparatorChar); } } args.add(fabricAddMods.substring(0, fabricAddMods.length() - 1)); } // Wrapper class (launched by vm, launches main class) args.add("io.gitlab.jfronny.inceptum.launchwrapper.Main"); // Main class args.add(resolveMainClass(instance, versionInfo, gameJar, launchType)); // Game arguments if (launchType == LaunchType.Client) { if (versionInfo.arguments != null) args.addAll(parse(versionInfo.arguments.game, versionInfo, instance, classPath.toString(), authInfo)); else if (versionInfo.minecraftArguments != null) { for (String s : versionInfo.minecraftArguments.split(" ")) { args.add(expandArg(s, versionInfo, instance, classPath.toString(), authInfo)); } } else throw new LaunchException("Could not launch: No valid source for client arguments found"); } if (instance.meta().arguments != null) { switch (launchType) { case Client -> { if (instance.meta().arguments.client != null) args.addAll(instance.meta.arguments.client); } case Server -> { if (instance.meta().arguments.server != null) args.addAll(instance.meta.arguments.server); } } } // Write launch time instance.meta.lastLaunched = System.currentTimeMillis() / 1000L; instance.writeMeta(); // Log command used to start Utils.LOGGER.info(String.join(" ", args)); // Create process ProcessBuilder pb = new ProcessBuilder(args.toArray(new String[0])); pb.directory(instance.path.toFile()); pb.redirectOutput(ProcessBuilder.Redirect.INHERIT); pb.redirectError(ProcessBuilder.Redirect.INHERIT); AtomicReference proc = new AtomicReference<>(); Runnable starterRunner = () -> { try { proc.set(pb.start()); instance.setRunningLock(proc.get().pid()); } catch (IOException e) { Utils.LOGGER.error("Could not start " + launchType.name, e); } }; starterRunner.run(); if (restart) { new Thread(() -> { while (true) { try { proc.get().waitFor(); } catch (InterruptedException e) { Utils.LOGGER.error("Could not wait for server to finish", e); } Utils.LOGGER.info("Restarting server"); starterRunner.run(); if (!proc.get().isAlive) { Utils.LOGGER.error("Could not restart server"); return; } } }).start(); } } private static String resolveMainClass(Instance instance, VersionInfo versionInfo, Path gameJar, LaunchType launchType) throws LaunchException { if (launchType == LaunchType.Client || instance.isFabric) return versionInfo.mainClass; // Identify main class using MANIFEST.MF final String linePrefix = "Main-Class: "; try (FileSystem fs = Utils.openZipFile(gameJar, false)) { for (String line : Files.readAllLines(fs.getPath("META-INF/MANIFEST.MF"))) { if (line.startsWith(linePrefix)) { return line.substring(linePrefix.length()); } } } catch (IOException | URISyntaxException e) { throw new LaunchException("IO Exception while trying to identify entrypoint", e); } throw new LaunchException("Could not identify entrypoint"); } private static VersionsListInfo getVersion(String minecraftVersion) throws LaunchException { for (VersionsListInfo version : McApi.getVersions().versions) { if (version.id.equals(minecraftVersion)) return version; } throw new LaunchException("Could not find data for minecraft version: " + minecraftVersion); } private static List parse(List arguments, VersionInfo info, Instance instance, String classPath, AuthInfo authInfo) { List res = new ArrayList<>(); for (MinecraftArgument argument : arguments) { for (String s : argument.arg()) { res.add(expandArg(s, info, instance, classPath, authInfo)); } } return res; } private static String expandArg(String arg, VersionInfo info, Instance instance, String classPath, AuthInfo authInfo) { return arg // game args .replace("${auth_player_name}", authInfo.name) .replace("${version_name}", "Inceptum") .replace("${game_directory}", instance.path.toString()) .replace("${assets_root}", MetaHolder.ASSETS_DIR.toString()) .replace("${assets_index_name}", info.assets) .replace("${auth_uuid}", authInfo.uuid) .replace("${auth_access_token}", authInfo.accessToken) .replace("${user_type}", authInfo.userType) .replace("${version_type}", info.type) .replace("${resolution_width}", "1920") //TODO has_custom_resolution .replace("${resolution_height}", "1080") //TODO has_custom_resolution // jvm args .replace("${natives_directory}", MetaHolder.NATIVES_DIR.resolve(instance.gameVersion).toString()) .replace("${launcher_name}", "Inceptum") .replace("${launcher_version}", BuildMetadata.VERSION) .replace("${classpath}", classPath) .replace("${user_properties}", "{}"); } }