Inceptum/launcher/src/main/java/io/gitlab/jfronny/inceptum/launcher/system/launch/InstanceLauncher.java

245 lines
12 KiB
Java

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.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 ProcessState());
// Prepare arguments
List<String> 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<Process> proc = new AtomicReference<>();
Runnable starterRunner = () -> {
try {
proc.set(pb.start());
instance.runningLock = proc.get().pid();
} catch (IOException e) {
Utils.LOGGER.error("Could not start " + launchType.name, e);
}
};
if (restart) {
new Thread(() -> {
while (true) {
starterRunner.run();
if (!proc.get().isAlive) {
Utils.LOGGER.error("Could not create server process");
instance.isRunningLocked();
return;
}
try {
proc.get().waitFor();
} catch (InterruptedException e) {
Utils.LOGGER.error("Could not wait for server to finish", e);
}
Utils.LOGGER.info("Restarting server");
}
}).start();
} else {
var th = new Thread(() -> {
starterRunner.run();
try {
proc.get().waitFor();
} catch (InterruptedException e) {
Utils.LOGGER.error("Could not wait for thread", e);
return;
}
instance.isRunningLocked();
});
th.isDaemon = true;
th.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<String> parse(List<MinecraftArgument> arguments, VersionInfo info, Instance instance, String classPath, AuthInfo authInfo) {
List<String> 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}", "{}");
}
}