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

263 lines
13 KiB
Java

package io.gitlab.jfronny.inceptum.launcher.system.launch;
import io.gitlab.jfronny.commons.OSUtils;
import io.gitlab.jfronny.commons.io.JFiles;
import io.gitlab.jfronny.inceptum.launcher.LauncherEnv;
import io.gitlab.jfronny.inceptum.common.*;
import io.gitlab.jfronny.inceptum.launcher.model.inceptum.*;
import io.gitlab.jfronny.inceptum.launcher.model.mojang.*;
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.system.install.steps.DownloadLibrariesStep;
import io.gitlab.jfronny.inceptum.launcher.system.source.ModSource;
import io.gitlab.jfronny.inceptum.launcher.util.*;
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(Path path, InstanceMeta 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.getSelectedAccount();
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(path, instance, infoNew);
}, R::nop);
} catch (IOException e) {
LauncherEnv.showError("Failed to request input", e);
}
} else launchClient(path, instance, authInfo);
}
private static void launchClient(Path path, InstanceMeta instance, AuthInfo authInfo) {
try {
launch(path, instance, LaunchType.Client, false, authInfo);
} catch (LaunchException | IOException e) {
LauncherEnv.showError("Could not launch client", e);
}
}
public static void launch(Path instancePath, InstanceMeta instance, LaunchType launchType, boolean restart, AuthInfo authInfo) throws LaunchException, IOException {
if (authInfo == null) throw new LaunchException("authInfo is null");
VersionsListInfo versionDataSimple = getVersion(instance.getMinecraftVersion());
VersionInfo versionInfo = McApi.getVersionInfo(versionDataSimple);
// Add fabric metadata if using fabric
if (instance.isFabric()) {
versionInfo = FabricMetaApi.addFabric(versionInfo, instance.getLoaderVersion(), launchType.fabricMetaType);
}
// Ensure libs/assets are present
DownloadLibrariesStep.execute(versionInfo, new AtomicBoolean(false), new ProcessState());
// Prepare arguments
List<String> args = new LinkedList<>();
// JVM path
{
final VersionInfo lambdaVersionInfo = versionInfo;
args.add(Objects.requireNonNullElseGet(instance.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).toAbsolutePath());
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(MetaHolder.LIBRARIES_DIR.resolve(DownloadLibrariesStep.getLaunchWrapperArtifact()));
// JVM arguments
if (launchType == LaunchType.Client && versionInfo.arguments != null)
args.addAll(parse(versionInfo.arguments.jvm, versionInfo, instance, classPath.toString(), instancePath.toAbsolutePath().toString(), authInfo));
if (instance.minMem != null) args.add("-Xms" + instance.minMem);
if (instance.maxMem != null) args.add("-Xmx" + instance.maxMem);
if (instance.arguments != null && instance.arguments.jvm != null) args.addAll(instance.arguments.jvm);
// Native library path
args.add("-Djava.library.path=" + MetaHolder.NATIVES_DIR.resolve(instance.getMinecraftVersion()));
// 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=");
Path mods = instancePath.resolve("mods");
if (Files.exists(mods)) {
for (Path imod : JFiles.list(mods, path -> ModPath.isImod(path) && ModPath.isEnabled(path))) {
String fn = imod.getFileName().toString();
if (Files.exists(imod.getParent().resolve(fn.substring(0, fn.length() - 5))))
continue;
Map<ModSource, Optional<ModSource>> sources = JFiles.readObject(imod, ModDescription.class).sources;
if (sources.isEmpty()) throw new LaunchException(".imod without attached jar contains no sources");
ModSource ms = List.copyOf(sources.keySet()).get(0);
Path p = ms.getJarPath().toAbsolutePath();
if (!Files.exists(p)) ms.download();
fabricAddMods.append(p);
fabricAddMods.append(File.pathSeparatorChar);
}
}
args.add(fabricAddMods.substring(0, fabricAddMods.length() - 1));
}
// Add classpath to args
args.add("-cp");
args.add(classPath.toString());
// 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(), instancePath.toAbsolutePath().toString(), authInfo));
else if (versionInfo.minecraftArguments != null) {
for (String s : versionInfo.minecraftArguments.split(" ")) {
args.add(expandArg(s, versionInfo, instance, classPath.toString(), instancePath.toAbsolutePath().toString(), authInfo));
}
} else throw new LaunchException("Could not launch: No valid source for client arguments found");
}
if (instance.arguments != null) {
switch (launchType) {
case Client -> {
if (instance.arguments.client != null)
args.addAll(instance.arguments.client);
}
case Server -> {
if (instance.arguments.server != null)
args.addAll(instance.arguments.server);
}
}
}
// Log command used to start
Utils.LOGGER.info(String.join(" ", args));
// Create process
ProcessBuilder pb = new ProcessBuilder(args.toArray(new String[0]));
pb.directory(instancePath.toFile());
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
AtomicReference<Process> proc = new AtomicReference<>();
Runnable starterRunner = () -> {
try {
proc.set(pb.start());
InstanceLock.setRunningLock(instancePath, 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(InstanceMeta 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, InstanceMeta instance, String classPath, String gameDirectory, AuthInfo authInfo) {
List<String> res = new ArrayList<>();
for (MinecraftArgument argument : arguments) {
for (String s : argument.arg()) {
res.add(expandArg(s, info, instance, classPath, gameDirectory, authInfo));
}
}
return res;
}
private static String expandArg(String arg, VersionInfo info, InstanceMeta instance, String classPath, String gameDirectory, AuthInfo authInfo) {
return arg
// game args
.replace("${auth_player_name}", authInfo.name())
.replace("${version_name}", instance.getMinecraftVersion())
.replace("${game_directory}", gameDirectory)
.replace("${assets_root}", MetaHolder.ASSETS_DIR.toAbsolutePath().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.getMinecraftVersion()).toAbsolutePath().toString())
.replace("${launcher_name}", "Inceptum")
.replace("${launcher_version}", BuildMetadata.VERSION.toString())
.replace("${classpath}", classPath)
.replace("${user_properties}", "{}");
}
public enum LaunchType {
Server("server", FabricMetaApi.FabricVersionInfoType.Server), Client("client", FabricMetaApi.FabricVersionInfoType.Client);
LaunchType(String name, FabricMetaApi.FabricVersionInfoType fabricMetaType) {
this.name = name;
this.fabricMetaType = fabricMetaType;
}
public final String name;
public final FabricMetaApi.FabricVersionInfoType fabricMetaType;
}
public static class LaunchException extends Exception {
public LaunchException(String message) {
super(message);
}
public LaunchException(String message, Throwable cause) {
super(message, cause);
}
}
}