bump: version 1.0.10 → 1.0.11, fix Forge/NeoForge launch
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>me.sashegdev</groupId>
|
||||
<artifactId>ZernMCLauncher</artifactId>
|
||||
<version>1.0.10</version>
|
||||
<version>1.0.11</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>zernmc-bootstrap</artifactId>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>me.sashegdev</groupId>
|
||||
<artifactId>ZernMCLauncher</artifactId>
|
||||
<version>1.0.10</version>
|
||||
<version>1.0.11</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>zernmclauncher</artifactId>
|
||||
|
||||
+14
-6
@@ -1,8 +1,7 @@
|
||||
package me.sashegdev.zernmc.launcher.minecraft;
|
||||
|
||||
import me.sashegdev.zernmc.launcher.minecraft.installer.FabricInstaller;
|
||||
import me.sashegdev.zernmc.launcher.minecraft.installer.ForgeInstaller;
|
||||
import me.sashegdev.zernmc.launcher.minecraft.installer.NeoForgeInstaller;
|
||||
import me.sashegdev.zernmc.launcher.minecraft.installer.ModLoaderInstaller;
|
||||
import me.sashegdev.zernmc.launcher.minecraft.installer.VersionInstaller;
|
||||
import me.sashegdev.zernmc.launcher.minecraft.launch.LaunchCommandBuilder;
|
||||
import me.sashegdev.zernmc.launcher.minecraft.model.LaunchOptions;
|
||||
@@ -45,13 +44,13 @@ public class MinecraftLib {
|
||||
}
|
||||
|
||||
public boolean installForge(String minecraftVersion, String forgeVersion) throws Exception {
|
||||
ForgeInstaller installer = new ForgeInstaller(instance);
|
||||
return installer.install(minecraftVersion, forgeVersion);
|
||||
return new ModLoaderInstaller(instance)
|
||||
.install(minecraftVersion, forgeVersion, ModLoaderInstaller.LoaderType.FORGE);
|
||||
}
|
||||
|
||||
public boolean installNeoForge(String minecraftVersion, String neoforgeVersion) throws Exception {
|
||||
NeoForgeInstaller installer = new NeoForgeInstaller(instance);
|
||||
return installer.install(minecraftVersion, neoforgeVersion);
|
||||
return new ModLoaderInstaller(instance)
|
||||
.install(minecraftVersion, neoforgeVersion, ModLoaderInstaller.LoaderType.NEOFORGE);
|
||||
}
|
||||
|
||||
public boolean installFabric(String minecraftVersion, String loaderVersion) throws Exception {
|
||||
@@ -123,6 +122,15 @@ public class MinecraftLib {
|
||||
ProcessBuilder pb = new ProcessBuilder(command);
|
||||
pb.directory(instance.getPath().toFile());
|
||||
|
||||
// Write launch command to file for debugging
|
||||
Path cmdLogFile = instance.getPath().resolve("launch-command.log");
|
||||
try {
|
||||
Files.writeString(cmdLogFile, String.join(" \\\n ", command));
|
||||
System.out.println(ZAnsi.green(" Launch command written to " + cmdLogFile.toAbsolutePath()));
|
||||
} catch (Exception e) {
|
||||
System.out.println(ZAnsi.yellow(" Failed to write launch command log: " + e.getMessage()));
|
||||
}
|
||||
|
||||
System.out.println(ZAnsi.brightGreen("\nStarting Minecraft...\n"));
|
||||
ConsoleUtils.clearScreen();
|
||||
|
||||
|
||||
+319
@@ -0,0 +1,319 @@
|
||||
package me.sashegdev.zernmc.launcher.minecraft.installer;
|
||||
|
||||
import me.sashegdev.zernmc.launcher.minecraft.Instance;
|
||||
import me.sashegdev.zernmc.launcher.utils.ProgressBar;
|
||||
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
|
||||
public class ModLoaderInstaller {
|
||||
|
||||
public enum LoaderType {
|
||||
FORGE("forge", "net.minecraftforge", "forge", "https://maven.minecraftforge.net"),
|
||||
NEOFORGE("neoforge", "net.neoforged", null, "https://maven.neoforged.net/releases");
|
||||
|
||||
final String loaderName;
|
||||
final String mavenGroup;
|
||||
final String fixedArtifact;
|
||||
final String mavenBase;
|
||||
|
||||
LoaderType(String loaderName, String mavenGroup, String fixedArtifact, String mavenBase) {
|
||||
this.loaderName = loaderName;
|
||||
this.mavenGroup = mavenGroup;
|
||||
this.fixedArtifact = fixedArtifact;
|
||||
this.mavenBase = mavenBase;
|
||||
}
|
||||
|
||||
public String artifact(String mcVersion) {
|
||||
if (this == NEOFORGE && !"1.20.1".equals(mcVersion)) return "neoforge";
|
||||
return "forge";
|
||||
}
|
||||
}
|
||||
|
||||
private final Instance instance;
|
||||
private final HttpClient httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(30))
|
||||
.build();
|
||||
|
||||
public ModLoaderInstaller(Instance instance) {
|
||||
this.instance = instance;
|
||||
}
|
||||
|
||||
public boolean install(String mcVersion, String loaderVersion, LoaderType type) throws Exception {
|
||||
System.out.println(ZAnsi.cyan("Installing " + type.loaderName + " " + loaderVersion + " for Minecraft " + mcVersion));
|
||||
|
||||
System.out.println(ZAnsi.cyan("Installing base Minecraft version " + mcVersion + "..."));
|
||||
VersionInstaller vanillaInstaller = new VersionInstaller(instance.getPath());
|
||||
String assetIndex = vanillaInstaller.install(mcVersion);
|
||||
|
||||
if (assetIndex == null || assetIndex.isEmpty()) {
|
||||
System.out.println(ZAnsi.brightRed("Failed to install base Minecraft version"));
|
||||
return false;
|
||||
}
|
||||
|
||||
instance.setAssetIndex(assetIndex);
|
||||
createLauncherProfile();
|
||||
|
||||
String installerUrl = buildInstallerUrl(mcVersion, loaderVersion, type);
|
||||
String jarName = type.loaderName + "-installer.jar";
|
||||
Path installerJar = instance.getPath().resolve(jarName);
|
||||
|
||||
System.out.println(ZAnsi.cyan("Downloading " + type.loaderName + " Installer..."));
|
||||
downloadFileWithProgress(installerUrl, installerJar, type);
|
||||
|
||||
System.out.println(ZAnsi.cyan("Running " + type.loaderName + " Installer..."));
|
||||
System.out.println(ZAnsi.yellow("This may take a few minutes. Please wait...\n"));
|
||||
|
||||
boolean success = runInstaller(installerJar, type);
|
||||
|
||||
if (success) {
|
||||
try {
|
||||
downloadMissingLibraries(type);
|
||||
} catch (Exception e) {
|
||||
System.out.println(ZAnsi.yellow("Warning: could not download some libraries: " + e.getMessage()));
|
||||
}
|
||||
|
||||
System.out.println(ZAnsi.brightGreen("\n" + type.loaderName + " " + loaderVersion + " installed successfully!"));
|
||||
instance.setMinecraftVersion(mcVersion);
|
||||
instance.setLoaderType(type.loaderName);
|
||||
instance.setLoaderVersion(loaderVersion);
|
||||
|
||||
Files.deleteIfExists(installerJar);
|
||||
return true;
|
||||
} else {
|
||||
System.out.println(ZAnsi.brightRed("\nError installing " + type.loaderName + "!"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private String buildInstallerUrl(String mcVersion, String loaderVersion, LoaderType type) {
|
||||
if (type == LoaderType.FORGE) {
|
||||
return type.mavenBase + "/" + type.mavenGroup.replace('.', '/')
|
||||
+ "/" + type.fixedArtifact + "/" + mcVersion + "-" + loaderVersion
|
||||
+ "/" + type.fixedArtifact + "-" + mcVersion + "-" + loaderVersion + "-installer.jar";
|
||||
}
|
||||
|
||||
String artifact = type.artifact(mcVersion);
|
||||
return type.mavenBase + "/" + type.mavenGroup.replace('.', '/')
|
||||
+ "/" + artifact + "/" + loaderVersion
|
||||
+ "/" + artifact + "-" + loaderVersion + "-installer.jar";
|
||||
}
|
||||
|
||||
private void createLauncherProfile() throws IOException {
|
||||
Path profilePath = instance.getPath().resolve("launcher_profiles.json");
|
||||
if (Files.exists(profilePath)) return;
|
||||
|
||||
String minimalProfile = """
|
||||
{
|
||||
"profiles": {},
|
||||
"selectedProfile": "Default"
|
||||
}
|
||||
""";
|
||||
Files.writeString(profilePath, minimalProfile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
System.out.println(ZAnsi.yellow("Created launcher_profiles.json"));
|
||||
}
|
||||
|
||||
private void downloadFileWithProgress(String url, Path target, LoaderType type) throws Exception {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url))
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
HttpResponse<InputStream> response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
|
||||
|
||||
if (response.statusCode() != 200) {
|
||||
throw new IOException("HTTP " + response.statusCode());
|
||||
}
|
||||
|
||||
long contentLength = response.headers().firstValueAsLong("Content-Length").orElse(-1);
|
||||
|
||||
try (InputStream in = response.body();
|
||||
FileOutputStream out = new FileOutputStream(target.toFile())) {
|
||||
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
long totalRead = 0;
|
||||
int lastPercent = -1;
|
||||
|
||||
while ((bytesRead = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, bytesRead);
|
||||
totalRead += bytesRead;
|
||||
|
||||
if (contentLength > 0) {
|
||||
int percent = (int) ((totalRead * 100) / contentLength);
|
||||
if (percent != lastPercent) {
|
||||
ProgressBar.show(type.loaderName + " Installer", percent, 100,
|
||||
"% (" + ProgressBar.formatBytes(totalRead) + "/" + ProgressBar.formatBytes(contentLength) + ")");
|
||||
lastPercent = percent;
|
||||
}
|
||||
} else {
|
||||
char[] spinner = {'|', '/', '-', '\\'};
|
||||
int idx = (int) (totalRead / 1024) % 4;
|
||||
System.out.print("\rDownloading " + type.loaderName + " Installer: " + ProgressBar.formatBytes(totalRead) + " " + spinner[idx]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ProgressBar.finish(type.loaderName + " Installer (" + ProgressBar.formatBytes(Files.size(target)) + ")");
|
||||
}
|
||||
|
||||
private boolean runInstaller(Path installerJar, LoaderType type) throws IOException, InterruptedException {
|
||||
int maxRetries = 3;
|
||||
int attempt = 1;
|
||||
|
||||
while (attempt <= maxRetries) {
|
||||
System.out.println(ZAnsi.cyan("Attempt " + attempt + " of " + maxRetries));
|
||||
|
||||
ProcessBuilder pb = new ProcessBuilder(
|
||||
"java",
|
||||
"-jar",
|
||||
installerJar.toAbsolutePath().toString(),
|
||||
"--installClient",
|
||||
instance.getPath().toAbsolutePath().toString()
|
||||
);
|
||||
|
||||
pb.environment().put("JAVA_OPTS", "-Dhttp.connectionTimeout=60000 -Dhttp.socketTimeout=60000");
|
||||
pb.directory(instance.getPath().toFile());
|
||||
pb.redirectErrorStream(true);
|
||||
|
||||
Process process = pb.start();
|
||||
|
||||
StringBuilder output = new StringBuilder();
|
||||
boolean hasErrors = false;
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
output.append(line).append("\n");
|
||||
|
||||
if (line.contains("Downloading") || line.contains("Extracting")) {
|
||||
System.out.println(ZAnsi.blue(" -> " + line));
|
||||
} else if (line.contains("SUCCESS") || line.contains("successfully")) {
|
||||
System.out.println(ZAnsi.brightGreen(" + " + line));
|
||||
} else if (line.contains("WARNING") || line.contains("warning")) {
|
||||
System.out.println(ZAnsi.yellow(" ! " + line));
|
||||
} else if (line.contains("ERROR") || line.contains("error") || line.contains("failed") || line.contains("timed out")) {
|
||||
System.out.println(ZAnsi.brightRed(" X " + line));
|
||||
if (line.contains("timed out") || line.contains("failed to download")) {
|
||||
hasErrors = true;
|
||||
}
|
||||
} else if (!line.isBlank()) {
|
||||
System.out.println(" " + line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int exitCode = process.waitFor();
|
||||
|
||||
if (exitCode == 0 && !hasErrors) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
System.out.println(ZAnsi.yellow("Install error. Retrying in 5 seconds..."));
|
||||
Thread.sleep(5000);
|
||||
|
||||
Path librariesDir = instance.getPath().resolve("libraries");
|
||||
if (Files.exists(librariesDir)) {
|
||||
try (var stream = Files.walk(librariesDir)) {
|
||||
stream.filter(p -> p.toString().contains("asm") && p.toString().endsWith(".jar"))
|
||||
.forEach(p -> {
|
||||
try { Files.deleteIfExists(p); }
|
||||
catch (IOException e) { /* ignore */ }
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
System.out.println(ZAnsi.brightRed(type.loaderName + " Installer exited with error code: " + exitCode));
|
||||
|
||||
if (output.toString().contains("timed out")) {
|
||||
System.out.println(ZAnsi.yellow("\nPossible solutions:"));
|
||||
System.out.println(ZAnsi.yellow("1. Check your internet connection"));
|
||||
System.out.println(ZAnsi.yellow("2. Run the launcher as administrator"));
|
||||
System.out.println(ZAnsi.yellow("3. Temporarily disable antivirus/firewall"));
|
||||
System.out.println(ZAnsi.yellow("4. Try installing a different version"));
|
||||
}
|
||||
}
|
||||
|
||||
attempt++;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void downloadMissingLibraries(LoaderType type) throws Exception {
|
||||
System.out.println(ZAnsi.cyan("Checking and downloading missing libraries..."));
|
||||
|
||||
Map<String, List<String>> alternativeUrls = new LinkedHashMap<>();
|
||||
alternativeUrls.put("org/ow2/asm/asm/9.6/asm-9.6.jar", List.of(
|
||||
"https://repo1.maven.org/maven2/org/ow2/asm/asm/9.6/asm-9.6.jar",
|
||||
"https://mirrors.huaweicloud.com/repository/maven/org/ow2/asm/asm/9.6/asm-9.6.jar"
|
||||
));
|
||||
|
||||
if (type == LoaderType.NEOFORGE) {
|
||||
alternativeUrls.put("org/ow2/asm/asm-commons/9.6/asm-commons-9.6.jar", List.of(
|
||||
"https://repo1.maven.org/maven2/org/ow2/asm/asm-commons/9.6/asm-commons-9.6.jar"
|
||||
));
|
||||
alternativeUrls.put("org/ow2/asm/asm-tree/9.6/asm-tree-9.6.jar", List.of(
|
||||
"https://repo1.maven.org/maven2/org/ow2/asm/asm-tree/9.6/asm-tree-9.6.jar"
|
||||
));
|
||||
}
|
||||
|
||||
Path librariesDir = instance.getPath().resolve("libraries");
|
||||
|
||||
for (Map.Entry<String, List<String>> entry : alternativeUrls.entrySet()) {
|
||||
Path target = librariesDir.resolve(entry.getKey());
|
||||
if (!Files.exists(target)) {
|
||||
Files.createDirectories(target.getParent());
|
||||
System.out.println(ZAnsi.yellow("Downloading: " + target.getFileName()));
|
||||
|
||||
boolean downloaded = false;
|
||||
for (String mirrorUrl : entry.getValue()) {
|
||||
for (int attempt = 1; attempt <= 3; attempt++) {
|
||||
try {
|
||||
downloadDirect(mirrorUrl, target);
|
||||
downloaded = true;
|
||||
break;
|
||||
} catch (Exception e) {
|
||||
if (attempt == 3 && mirrorUrl.equals(entry.getValue().get(entry.getValue().size() - 1))) throw e;
|
||||
System.out.println(ZAnsi.yellow("Retry " + attempt + "/3..."));
|
||||
Thread.sleep(2000);
|
||||
}
|
||||
}
|
||||
if (downloaded) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void downloadDirect(String url, Path target) throws Exception {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url))
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
HttpResponse<InputStream> response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
|
||||
|
||||
if (response.statusCode() != 200) {
|
||||
throw new IOException("HTTP " + response.statusCode());
|
||||
}
|
||||
|
||||
try (InputStream in = response.body();
|
||||
FileOutputStream out = new FileOutputStream(target.toFile())) {
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
while ((bytesRead = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-19
@@ -85,23 +85,14 @@ public class LaunchCommandBuilder {
|
||||
|
||||
String cpFile = writeClasspathFile(classpath);
|
||||
|
||||
// DEBUG: print classpath entries to identify split-package sources
|
||||
String sep = System.getProperty("os.name").toLowerCase().contains("win") ? ";" : ":";
|
||||
System.out.println(ZAnsi.cyan(" === CLASSPATH ENTRIES (" + classpath.split(sep.replace("\\","\\\\")).length + " jars) ==="));
|
||||
for (String entry : classpath.split(sep.replace("\\","\\\\"))) {
|
||||
String fileName = entry.contains("/") ? entry.substring(entry.lastIndexOf('/') + 1) : entry.substring(entry.lastIndexOf('\\') + 1);
|
||||
System.out.println(ZAnsi.cyan(" CP: " + fileName));
|
||||
}
|
||||
System.out.println(ZAnsi.cyan(" === END CLASSPATH ==="));
|
||||
|
||||
// Build variable map for placeholder substitution
|
||||
Map<String, String> vars = buildVariableMap(options);
|
||||
vars.put("classpath", cpFile);
|
||||
|
||||
// Parse ALL version.json JVM args (merged with parent) with placeholder
|
||||
// substitution. This includes -Djava.library.path, -cp, --add-modules,
|
||||
// --add-opens, etc. — everything the launcher needs.
|
||||
List<String> allJvmArgs = manifest != null ? manifest.getAllJvmArguments() : new ArrayList<>();
|
||||
// Parse version.json JVM args (child only). Forge/NeoForge defines its own
|
||||
// -p, --add-modules, --add-opens. -Djava.library.path is NOT in child args,
|
||||
// so it's added manually below.
|
||||
List<String> allJvmArgs = manifest != null ? manifest.getJvmArguments() : new ArrayList<>();
|
||||
if (!allJvmArgs.isEmpty()) {
|
||||
for (String arg : allJvmArgs) {
|
||||
String resolved = resolveVariable(arg, vars);
|
||||
@@ -113,11 +104,11 @@ public class LaunchCommandBuilder {
|
||||
} else {
|
||||
System.out.println(ZAnsi.yellow(" WARNING: No JVM args in version.json, using manual fallback"));
|
||||
command.addAll(getJvmArguments(options));
|
||||
command.add("-Djava.library.path=" + nativesDir.toAbsolutePath());
|
||||
command.add("-cp");
|
||||
command.add(cpFile);
|
||||
}
|
||||
|
||||
// Forge/NeoForge child version.json doesn't include -Djava.library.path
|
||||
command.add("-Djava.library.path=" + nativesDir.toAbsolutePath());
|
||||
|
||||
// Append memory/GC args (always after version.json args, like AstralRinth)
|
||||
int ramMB = options.getMaxMemory() > 0 ? options.getMaxMemory() : 4096;
|
||||
command.add("-Xmx" + ramMB + "M");
|
||||
@@ -147,9 +138,9 @@ public class LaunchCommandBuilder {
|
||||
}
|
||||
command.add(mainClass);
|
||||
|
||||
// Parse ALL version.json game args (merged with parent) with placeholder
|
||||
// substitution. This includes --launchTarget, --fml.forgeVersion,
|
||||
// --fml.mcVersion, --version, --gameDir, --assetsDir, etc.
|
||||
// Parse version.json game args (merged with parent) with placeholder
|
||||
// substitution. Parent provides --username, --version, etc.;
|
||||
// child provides --launchTarget, --fml.*.
|
||||
List<String> allGameArgs = manifest != null ? manifest.getAllGameArguments() : new ArrayList<>();
|
||||
if (!allGameArgs.isEmpty()) {
|
||||
for (String arg : allGameArgs) {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="brand-title">ZernMC</h1>
|
||||
<p class="brand-sub">Launcher <span id="version" data-i18n="version">1.0.9</span></p>
|
||||
<p class="brand-sub">Launcher <span id="version" data-i18n="version">1.0.11</span></p>
|
||||
</div>
|
||||
|
||||
<form id="login-form" class="login-form">
|
||||
@@ -73,7 +73,7 @@
|
||||
</svg>
|
||||
<div class="sidebar-brand-text">
|
||||
<span class="sidebar-brand-name">ZernMC</span>
|
||||
<span class="sidebar-brand-ver">v<span id="header-version">1.0.9</span></span>
|
||||
<span class="sidebar-brand-ver">v<span id="header-version">1.0.11</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
package me.sashegdev.zernmc.launcher.integration;
|
||||
|
||||
import me.sashegdev.zernmc.launcher.minecraft.Instance;
|
||||
import me.sashegdev.zernmc.launcher.minecraft.installer.ModLoaderInstaller;
|
||||
import me.sashegdev.zernmc.launcher.minecraft.installer.VersionInstaller;
|
||||
import me.sashegdev.zernmc.launcher.minecraft.launch.LaunchCommandBuilder;
|
||||
import me.sashegdev.zernmc.launcher.minecraft.model.LaunchOptions;
|
||||
import org.junit.jupiter.api.*;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.net.URL;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.junit.jupiter.api.Assumptions.assumeTrue;
|
||||
|
||||
@Tag("integration")
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
class ForgeInstallIntegrationTest {
|
||||
|
||||
private static final String MC_VERSION = "1.20.1";
|
||||
private static final String FORGE_VERSION = "47.4.22";
|
||||
|
||||
private static Path instanceDir;
|
||||
private static Instance instance;
|
||||
private static boolean installSuccess = false;
|
||||
|
||||
@BeforeAll
|
||||
static void setup() throws Exception {
|
||||
// Check network
|
||||
assumeTrue(networkReachable("https://maven.minecraftforge.net"),
|
||||
"Forge maven not reachable, skipping");
|
||||
assumeTrue(networkReachable("https://piston-meta.mojang.com"),
|
||||
"Mojang meta not reachable, skipping");
|
||||
|
||||
instanceDir = Files.createTempDirectory("zernmc-forge-test-");
|
||||
instance = new Instance("forge-test", instanceDir);
|
||||
instance.setMinecraftVersion(MC_VERSION);
|
||||
instance.setLoaderType("forge");
|
||||
instance.setLoaderVersion(FORGE_VERSION);
|
||||
instance.setAssetIndex(MC_VERSION);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void cleanup() throws Exception {
|
||||
if (instanceDir != null) {
|
||||
try (var stream = Files.walk(instanceDir)) {
|
||||
stream.sorted((a, b) -> b.compareTo(a))
|
||||
.forEach(p -> {
|
||||
try { Files.deleteIfExists(p); } catch (Exception ignored) {}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
@DisplayName("Install Minecraft vanilla 1.20.1")
|
||||
void installMinecraft() throws Exception {
|
||||
System.out.println("\n=== Installing Minecraft " + MC_VERSION + " ===");
|
||||
VersionInstaller versionInstaller = new VersionInstaller(instanceDir);
|
||||
String assetIndex = versionInstaller.install(MC_VERSION);
|
||||
assertNotNull(assetIndex, "assetIndex must not be null after install");
|
||||
instance.setAssetIndex(assetIndex);
|
||||
|
||||
// Verify version.json exists
|
||||
Path versionJson = instanceDir.resolve("versions/" + MC_VERSION + "/" + MC_VERSION + ".json");
|
||||
assertTrue(Files.exists(versionJson), "version.json must exist after vanilla install");
|
||||
|
||||
// Verify client jar exists
|
||||
Path clientJar = instanceDir.resolve("versions/" + MC_VERSION + "/" + MC_VERSION + ".jar");
|
||||
assertTrue(Files.exists(clientJar), "client jar must exist after vanilla install");
|
||||
|
||||
// Verify libraries exist
|
||||
Path libsDir = instanceDir.resolve("libraries");
|
||||
assertTrue(Files.exists(libsDir) && Files.list(libsDir).findAny().isPresent(),
|
||||
"At least one library must exist after vanilla install");
|
||||
|
||||
System.out.println("=== Minecraft install OK ===\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
@DisplayName("Install Forge 1.20.1-47.4.22")
|
||||
void installForge() throws Exception {
|
||||
System.out.println("\n=== Installing Forge " + MC_VERSION + "-" + FORGE_VERSION + " ===");
|
||||
ModLoaderInstaller installer = new ModLoaderInstaller(instance);
|
||||
boolean success = installer.install(MC_VERSION, FORGE_VERSION, ModLoaderInstaller.LoaderType.FORGE);
|
||||
assertTrue(success, "Forge installation must succeed");
|
||||
installSuccess = true;
|
||||
|
||||
// Verify Forge version.json exists
|
||||
String forgeVersionId = MC_VERSION + "-forge-" + FORGE_VERSION;
|
||||
Path forgeJson = instanceDir.resolve("versions/" + forgeVersionId + "/" + forgeVersionId + ".json");
|
||||
assertTrue(Files.exists(forgeJson), "Forge version.json must exist at " + forgeJson);
|
||||
System.out.println(" Forge version.json size: " + Files.size(forgeJson) + " bytes\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
@DisplayName("Build launch command and verify structure")
|
||||
void buildAndVerifyCommand() throws Exception {
|
||||
assumeTrue(installSuccess, "Skipping: Forge install did not succeed");
|
||||
System.out.println("\n=== Building launch command ===");
|
||||
|
||||
LaunchOptions options = new LaunchOptions();
|
||||
options.setUsername("TestPlayer");
|
||||
options.setUuid("00000000-0000-0000-0000-000000000000");
|
||||
options.setAccessToken("test-token");
|
||||
options.setMaxMemory(4096);
|
||||
options.setWidth(854);
|
||||
options.setHeight(480);
|
||||
|
||||
LaunchCommandBuilder builder = new LaunchCommandBuilder(instance);
|
||||
List<String> command = builder.build(options);
|
||||
|
||||
// === JVM args checks ===
|
||||
assertFalse(command.contains("-cp"),
|
||||
"Forge must NOT have -cp (causes split-package)");
|
||||
|
||||
// Find -Djava.library.path (added manually, not from forge child)
|
||||
String libPath = null;
|
||||
for (String arg : command) {
|
||||
if (arg != null && arg.startsWith("-Djava.library.path=")) {
|
||||
libPath = arg;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertNotNull(libPath, "Must have -Djava.library.path");
|
||||
assertTrue(libPath.contains("natives"), "-Djava.library.path must point to natives dir");
|
||||
|
||||
// Must have -p (module path) from forge child
|
||||
int pIdx = command.indexOf("-p");
|
||||
assertTrue(pIdx >= 0, "Must have -p");
|
||||
String modulePath = command.get(pIdx + 1);
|
||||
assertNotNull(modulePath, "-p must have a value");
|
||||
assertTrue(modulePath.contains("bootstraplauncher"), "-p must include bootstraplauncher");
|
||||
|
||||
// Must have --add-modules + ALL-MODULE-PATH as two args (forge format)
|
||||
int amIdx = command.indexOf("--add-modules");
|
||||
assertTrue(amIdx >= 0, "Must have --add-modules");
|
||||
assertEquals("ALL-MODULE-PATH", command.get(amIdx + 1),
|
||||
"--add-modules must be followed by ALL-MODULE-PATH");
|
||||
|
||||
// Must have --add-opens (at least one) from forge child
|
||||
boolean hasAddOpens = command.stream().anyMatch(a -> a != null && a.startsWith("--add-opens"));
|
||||
assertTrue(hasAddOpens, "Must have at least one --add-opens");
|
||||
|
||||
// Main class
|
||||
assertTrue(command.contains("cpw.mods.bootstraplauncher.BootstrapLauncher"),
|
||||
"Main class must be BootstrapLauncher");
|
||||
|
||||
// Game args: parent (--username) + child (--launchTarget)
|
||||
assertTrue(command.contains("--username"), "Game args must include --username from parent");
|
||||
assertTrue(command.contains("--launchTarget"), "Game args must include --launchTarget from child");
|
||||
|
||||
// Memory/GC args
|
||||
assertTrue(command.contains("-Xmx4096M"), "Must have -Xmx");
|
||||
assertTrue(command.contains("-XX:+UseG1GC"), "Must have GC flags");
|
||||
|
||||
System.out.println("=== Command structure OK (" + command.size() + " args) ===\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(4)
|
||||
@DisplayName("Module path is valid (headless smoke test)")
|
||||
void modulePathSmokeTest() throws Exception {
|
||||
assumeTrue(installSuccess, "Skipping: Forge install did not succeed");
|
||||
System.out.println("\n=== Module path smoke test ===");
|
||||
|
||||
LaunchOptions options = new LaunchOptions();
|
||||
options.setUsername("TestPlayer");
|
||||
LaunchCommandBuilder builder = new LaunchCommandBuilder(instance);
|
||||
List<String> command = builder.build(options);
|
||||
|
||||
// Extract key args
|
||||
String libPath = null;
|
||||
String modulePath = null;
|
||||
|
||||
for (int i = 0; i < command.size(); i++) {
|
||||
String arg = command.get(i);
|
||||
if (arg != null) {
|
||||
if (arg.startsWith("-Djava.library.path=")) libPath = arg;
|
||||
}
|
||||
}
|
||||
int pIdx = command.indexOf("-p");
|
||||
if (pIdx >= 0) modulePath = command.get(pIdx + 1);
|
||||
|
||||
// Find --add-modules + ALL-MODULE-PATH as two args
|
||||
int amIdx = command.indexOf("--add-modules");
|
||||
String addModules = null;
|
||||
if (amIdx >= 0 && amIdx + 1 < command.size()) {
|
||||
addModules = command.get(amIdx + 1);
|
||||
}
|
||||
|
||||
assertNotNull(libPath, "-Djava.library.path must exist");
|
||||
assertNotNull(modulePath, "-p must exist");
|
||||
assertNotNull(addModules, "--add-modules must exist");
|
||||
assertEquals("ALL-MODULE-PATH", addModules, "--add-modules must be ALL-MODULE-PATH");
|
||||
|
||||
// Run: java -Djava.library.path=<path> -p <mp> --add-modules ALL-MODULE-PATH -version
|
||||
ProcessBuilder pb = new ProcessBuilder(
|
||||
"java",
|
||||
libPath,
|
||||
"-p", modulePath,
|
||||
"--add-modules", addModules,
|
||||
"-version"
|
||||
);
|
||||
pb.redirectErrorStream(true);
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
Process proc = pb.start();
|
||||
String output = new String(proc.getInputStream().readAllBytes());
|
||||
int exit = proc.waitFor();
|
||||
long elapsed = System.currentTimeMillis() - start;
|
||||
|
||||
System.out.println(" JVM accepted module path in " + elapsed + "ms");
|
||||
System.out.println(" Output: " + output.trim().replace('\n', ' '));
|
||||
assertEquals(0, exit,
|
||||
"JVM must accept module path without errors.\nExit: " + exit + "\nOutput: " + output);
|
||||
System.out.println("=== Module path smoke test OK ===\n");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Helper
|
||||
// ================================================================
|
||||
|
||||
private static boolean networkReachable(String urlStr) {
|
||||
try {
|
||||
URL url = new URL(urlStr);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setConnectTimeout(5000);
|
||||
conn.setRequestMethod("GET");
|
||||
int code = conn.getResponseCode();
|
||||
conn.disconnect();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
System.out.println(" Network check failed for " + urlStr + ": " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+414
@@ -0,0 +1,414 @@
|
||||
package me.sashegdev.zernmc.launcher.minecraft.launch;
|
||||
|
||||
import me.sashegdev.zernmc.launcher.minecraft.Instance;
|
||||
import me.sashegdev.zernmc.launcher.minecraft.model.LaunchOptions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class LaunchCommandBuilderTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
// ================================================================
|
||||
// Helpers
|
||||
// ================================================================
|
||||
|
||||
private Path createJar(Path path) throws Exception {
|
||||
Files.createDirectories(path.getParent());
|
||||
try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(path))) {
|
||||
zos.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF"));
|
||||
zos.write("Manifest-Version: 1.0\n".getBytes());
|
||||
zos.closeEntry();
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
private Path createJarWithModule(Path path, String moduleName) throws Exception {
|
||||
Files.createDirectories(path.getParent());
|
||||
try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(path))) {
|
||||
zos.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF"));
|
||||
zos.write(("Manifest-Version: 1.0\nAutomatic-Module-Name: " + moduleName + "\n").getBytes());
|
||||
zos.closeEntry();
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
private void writeVersionJson(Path path, String content) throws Exception {
|
||||
Files.createDirectories(path.getParent());
|
||||
Files.writeString(path, content);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Fixture setup
|
||||
// ================================================================
|
||||
|
||||
private Instance createForgeFixture(Path dir) throws Exception {
|
||||
String mcVer = "1.20.1";
|
||||
String forgeVer = "47.3.0";
|
||||
String versionId = mcVer + "-forge-" + forgeVer;
|
||||
|
||||
// Vanilla parent
|
||||
String vanillaJson = """
|
||||
{
|
||||
"id": "1.20.1",
|
||||
"mainClass": "net.minecraft.client.main.Main",
|
||||
"arguments": {
|
||||
"jvm": ["-Djava.library.path=${natives_directory}", "-cp", "${classpath}"],
|
||||
"game": ["--username", "${auth_player_name}", "--version", "${version_name}", "--gameDir", "${game_directory}", "--assetsDir", "${assets_root}", "--assetIndex", "${assets_index_name}", "--uuid", "${auth_uuid}", "--accessToken", "${auth_access_token}", "--userType", "${user_type}", "--versionType", "${version_type}"]
|
||||
},
|
||||
"assetIndex": {"id": "1.20.1"},
|
||||
"libraries": [{"name": "net.minecraft:client:1.20.1", "downloads": {"artifact": {"path": "net/minecraft/client/1.20.1/client-1.20.1.jar"}}}]
|
||||
}
|
||||
""";
|
||||
writeVersionJson(dir.resolve("versions/1.20.1/1.20.1.json"), vanillaJson);
|
||||
createJarWithModule(dir.resolve("versions/1.20.1/1.20.1.jar"), "minecraft");
|
||||
createJar(dir.resolve("libraries/net/minecraft/client/1.20.1/client-1.20.1.jar"));
|
||||
|
||||
// Forge child
|
||||
String forgeJson = """
|
||||
{
|
||||
"id": "1.20.1-forge-47.3.0",
|
||||
"inheritsFrom": "1.20.1",
|
||||
"mainClass": "cpw.mods.bootstraplauncher.BootstrapLauncher",
|
||||
"arguments": {
|
||||
"jvm": ["-p", "${classpath}", "--add-modules=ALL-MODULE-PATH", "--add-opens=java.base/java.util.jar=ALL-UNNAMED", "--add-opens=java.base/java.lang.invoke=ALL-UNNAMED"],
|
||||
"game": ["--launchTarget", "forgeclient", "--fml.forgeVersion", "47.3.0", "--fml.mcVersion", "1.20.1", "--fml.forgeGroup", "net.minecraftforge"]
|
||||
},
|
||||
"libraries": [{"name": "net.minecraftforge:forge:1.20.1-47.3.0", "downloads": {"artifact": {"path": "net/minecraftforge/forge/1.20.1-47.3.0/forge-1.20.1-47.3.0.jar"}}}]
|
||||
}
|
||||
""";
|
||||
writeVersionJson(dir.resolve("versions/" + versionId + "/" + versionId + ".json"), forgeJson);
|
||||
createJar(dir.resolve("libraries/net/minecraftforge/forge/1.20.1-47.3.0/forge-1.20.1-47.3.0.jar"));
|
||||
|
||||
Files.createDirectories(dir.resolve("natives"));
|
||||
|
||||
Instance instance = new Instance("test-forge", dir);
|
||||
instance.setMinecraftVersion(mcVer);
|
||||
instance.setLoaderType("forge");
|
||||
instance.setLoaderVersion(forgeVer);
|
||||
instance.setAssetIndex(mcVer);
|
||||
return instance;
|
||||
}
|
||||
|
||||
private Instance createNeoForgeFixture(Path dir) throws Exception {
|
||||
String mcVer = "1.20.1";
|
||||
String neoVer = "47.1.106";
|
||||
String versionId = mcVer + "-neoforge-" + neoVer;
|
||||
|
||||
String vanillaJson = """
|
||||
{
|
||||
"id": "1.20.1",
|
||||
"mainClass": "net.minecraft.client.main.Main",
|
||||
"arguments": {
|
||||
"jvm": ["-Djava.library.path=${natives_directory}", "-cp", "${classpath}"],
|
||||
"game": ["--username", "${auth_player_name}", "--version", "${version_name}"]
|
||||
},
|
||||
"assetIndex": {"id": "1.20.1"},
|
||||
"libraries": [{"name": "net.minecraft:client:1.20.1", "downloads": {"artifact": {"path": "net/minecraft/client/1.20.1/client-1.20.1.jar"}}}]
|
||||
}
|
||||
""";
|
||||
writeVersionJson(dir.resolve("versions/1.20.1/1.20.1.json"), vanillaJson);
|
||||
createJarWithModule(dir.resolve("versions/1.20.1/1.20.1.jar"), "minecraft");
|
||||
createJar(dir.resolve("libraries/net/minecraft/client/1.20.1/client-1.20.1.jar"));
|
||||
|
||||
String neoJson = """
|
||||
{
|
||||
"id": "1.20.1-neoforge-47.1.106",
|
||||
"inheritsFrom": "1.20.1",
|
||||
"mainClass": "cpw.mods.bootstraplauncher.BootstrapLauncher",
|
||||
"arguments": {
|
||||
"jvm": ["-p", "${classpath}", "--add-modules=ALL-MODULE-PATH", "--add-opens=java.base/java.util.jar=ALL-UNNAMED"],
|
||||
"game": ["--launchTarget", "neoforgeclient", "--fml.neoForgeVersion", "47.1.106", "--fml.mcVersion", "1.20.1"]
|
||||
},
|
||||
"libraries": [{"name": "net.neoforged:neoforge:1.20.1-47.1.106", "downloads": {"artifact": {"path": "net/neoforged/neoforge/1.20.1-47.1.106/neoforge-1.20.1-47.1.106.jar"}}}]
|
||||
}
|
||||
""";
|
||||
writeVersionJson(dir.resolve("versions/" + versionId + "/" + versionId + ".json"), neoJson);
|
||||
createJar(dir.resolve("libraries/net/neoforged/neoforge/1.20.1-47.1.106/neoforge-1.20.1-47.1.106.jar"));
|
||||
|
||||
Files.createDirectories(dir.resolve("natives"));
|
||||
|
||||
Instance instance = new Instance("test-neoforge", dir);
|
||||
instance.setMinecraftVersion(mcVer);
|
||||
instance.setLoaderType("neoforge");
|
||||
instance.setLoaderVersion(neoVer);
|
||||
instance.setAssetIndex(mcVer);
|
||||
instance.setFabricVersionId(versionId);
|
||||
return instance;
|
||||
}
|
||||
|
||||
private Instance createFabricFixture(Path dir) throws Exception {
|
||||
String mcVer = "1.20.1";
|
||||
String loaderVer = "0.16.10";
|
||||
String versionId = "fabric-loader-" + loaderVer + "-" + mcVer;
|
||||
|
||||
String vanillaJson = """
|
||||
{
|
||||
"id": "1.20.1",
|
||||
"mainClass": "net.minecraft.client.main.Main",
|
||||
"arguments": {
|
||||
"game": ["--username", "${auth_player_name}", "--version", "${version_name}"]
|
||||
},
|
||||
"assetIndex": {"id": "1.20.1"},
|
||||
"libraries": [{"name": "net.minecraft:client:1.20.1", "downloads": {"artifact": {"path": "net/minecraft/client/1.20.1/client-1.20.1.jar"}}}]
|
||||
}
|
||||
""";
|
||||
writeVersionJson(dir.resolve("versions/1.20.1/1.20.1.json"), vanillaJson);
|
||||
createJar(dir.resolve("versions/1.20.1/1.20.1.jar"));
|
||||
|
||||
String fabricJson = """
|
||||
{
|
||||
"id": "fabric-loader-0.16.10-1.20.1",
|
||||
"inheritsFrom": "1.20.1",
|
||||
"mainClass": "net.fabricmc.loader.impl.launch.knot.KnotClient",
|
||||
"arguments": {
|
||||
"game": ["--username", "${auth_player_name}"]
|
||||
},
|
||||
"libraries": [{"name": "net.fabricmc:fabric-loader:0.16.10", "downloads": {"artifact": {"path": "net/fabricmc/fabric-loader/0.16.10/fabric-loader-0.16.10.jar"}}}]
|
||||
}
|
||||
""";
|
||||
writeVersionJson(dir.resolve("versions/" + versionId + "/" + versionId + ".json"), fabricJson);
|
||||
createJar(dir.resolve("libraries/net/minecraft/client/1.20.1/client-1.20.1.jar"));
|
||||
createJar(dir.resolve("libraries/net/fabricmc/fabric-loader/0.16.10/fabric-loader-0.16.10.jar"));
|
||||
|
||||
Files.createDirectories(dir.resolve("natives"));
|
||||
|
||||
Instance instance = new Instance("test-fabric", dir);
|
||||
instance.setMinecraftVersion(mcVer);
|
||||
instance.setLoaderType("fabric");
|
||||
instance.setLoaderVersion(loaderVer);
|
||||
instance.setAssetIndex(mcVer);
|
||||
instance.setFabricVersionId(versionId);
|
||||
return instance;
|
||||
}
|
||||
|
||||
private Instance createVanillaFixture(Path dir) throws Exception {
|
||||
Files.createDirectories(dir.resolve("natives"));
|
||||
// No version.json — forces vanilla fallback
|
||||
Instance instance = new Instance("test-vanilla", dir);
|
||||
instance.setMinecraftVersion("1.21");
|
||||
instance.setLoaderType("vanilla");
|
||||
instance.setAssetIndex("1.21");
|
||||
return instance;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Forge tests
|
||||
// ================================================================
|
||||
|
||||
@Test
|
||||
void forge_noParentJvmArgs() throws Exception {
|
||||
Instance instance = createForgeFixture(tempDir);
|
||||
LaunchOptions options = new LaunchOptions();
|
||||
LaunchCommandBuilder builder = new LaunchCommandBuilder(instance);
|
||||
|
||||
List<String> command = builder.build(options);
|
||||
|
||||
// Parent's -cp must NOT appear (causes split-package with child's -p)
|
||||
assertFalse(command.contains("-cp"), "Forge must not have -cp from parent");
|
||||
|
||||
// -Djava.library.path must be added manually (not in child args)
|
||||
String expectedLibPath = "-Djava.library.path=" + tempDir.resolve("natives").toAbsolutePath();
|
||||
assertTrue(command.contains(expectedLibPath), "Forge must have -Djava.library.path");
|
||||
|
||||
// Child's -p (module path) must be present
|
||||
int pIndex = command.indexOf("-p");
|
||||
assertTrue(pIndex >= 0, "Forge must have -p");
|
||||
assertTrue(pIndex + 1 < command.size(), "-p must have a value");
|
||||
assertNotNull(command.get(pIndex + 1), "-p value must not be null");
|
||||
|
||||
// Child's --add-modules must be present
|
||||
assertTrue(command.contains("--add-modules=ALL-MODULE-PATH"), "Forge must have --add-modules=ALL-MODULE-PATH");
|
||||
|
||||
// Memory/GC args
|
||||
assertTrue(command.contains("-Xmx4096M"), "Forge must have memory args");
|
||||
assertTrue(command.contains("-XX:+UseG1GC"), "Forge must have GC args");
|
||||
|
||||
// Main class
|
||||
assertTrue(command.contains("cpw.mods.bootstraplauncher.BootstrapLauncher"), "Forge main class must be BootstrapLauncher");
|
||||
|
||||
// Game args: parent + child
|
||||
int launchTargetIdx = command.indexOf("--launchTarget");
|
||||
assertTrue(launchTargetIdx >= 0, "Game args must include child's --launchTarget");
|
||||
assertEquals("forgeclient", command.get(launchTargetIdx + 1), "Launch target must be forgeclient");
|
||||
|
||||
int usernameIdx = command.indexOf("--username");
|
||||
assertTrue(usernameIdx >= 0, "Game args must include parent's --username");
|
||||
}
|
||||
|
||||
@Test
|
||||
void forge_classpathContainsBothVanillaAndForgeLibraries() throws Exception {
|
||||
Instance instance = createForgeFixture(tempDir);
|
||||
LaunchOptions options = new LaunchOptions();
|
||||
LaunchCommandBuilder builder = new LaunchCommandBuilder(instance);
|
||||
|
||||
List<String> command = builder.build(options);
|
||||
|
||||
// Find -p's classpath value
|
||||
int pIdx = command.indexOf("-p");
|
||||
String cpValue = command.get(pIdx + 1);
|
||||
|
||||
// Version jar from ensureVersionJarForForge
|
||||
assertTrue(cpValue.contains("1.20.1-forge-47.3.0.jar") || cpValue.contains("1.20.1.jar"),
|
||||
"Classpath must include version jar");
|
||||
|
||||
// Forge library
|
||||
assertTrue(cpValue.contains("forge-1.20.1-47.3.0.jar"),
|
||||
"Classpath must include Forge library");
|
||||
|
||||
// Vanilla library
|
||||
assertTrue(cpValue.contains("client-1.20.1.jar"),
|
||||
"Classpath must include vanilla client library");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// NeoForge tests
|
||||
// ================================================================
|
||||
|
||||
@Test
|
||||
void neoforge_noParentJvmArgs() throws Exception {
|
||||
Instance instance = createNeoForgeFixture(tempDir);
|
||||
LaunchOptions options = new LaunchOptions();
|
||||
LaunchCommandBuilder builder = new LaunchCommandBuilder(instance);
|
||||
|
||||
List<String> command = builder.build(options);
|
||||
|
||||
// Same checks as Forge
|
||||
assertFalse(command.contains("-cp"), "NeoForge must not have -cp from parent");
|
||||
String expectedLibPath = "-Djava.library.path=" + tempDir.resolve("natives").toAbsolutePath();
|
||||
assertTrue(command.contains(expectedLibPath), "NeoForge must have -Djava.library.path");
|
||||
assertTrue(command.contains("-p"), "NeoForge must have -p");
|
||||
assertTrue(command.contains("--add-modules=ALL-MODULE-PATH"), "NeoForge must have --add-modules=ALL-MODULE-PATH");
|
||||
assertTrue(command.contains("cpw.mods.bootstraplauncher.BootstrapLauncher"), "NeoForge main class must be BootstrapLauncher");
|
||||
|
||||
// Game args: parent + child
|
||||
int launchTargetIdx = command.indexOf("--launchTarget");
|
||||
assertTrue(launchTargetIdx >= 0, "Game args must include child's --launchTarget");
|
||||
assertEquals("neoforgeclient", command.get(launchTargetIdx + 1), "Launch target must be neoforgeclient");
|
||||
assertTrue(command.contains("--username"), "Game args must include parent's --username");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Fabric tests
|
||||
// ================================================================
|
||||
|
||||
@Test
|
||||
void fabric_followsVanillaArgPattern() throws Exception {
|
||||
Instance instance = createFabricFixture(tempDir);
|
||||
LaunchOptions options = new LaunchOptions();
|
||||
LaunchCommandBuilder builder = new LaunchCommandBuilder(instance);
|
||||
|
||||
List<String> command = builder.build(options);
|
||||
|
||||
// Fabric uses -cp (vanilla pattern), not -p
|
||||
assertTrue(command.contains("-cp"), "Fabric must have -cp");
|
||||
assertFalse(command.contains("-p"), "Fabric must not have -p");
|
||||
|
||||
// -Djava.library.path present
|
||||
String expectedLibPath = "-Djava.library.path=" + tempDir.resolve("natives").toAbsolutePath();
|
||||
assertTrue(command.contains(expectedLibPath), "Fabric must have -Djava.library.path");
|
||||
|
||||
// Main class is KnotClient
|
||||
assertTrue(command.contains("net.fabricmc.loader.impl.launch.knot.KnotClient"),
|
||||
"Fabric main class must be KnotClient");
|
||||
|
||||
// Game args from version.json (merged with parent)
|
||||
assertTrue(command.contains("--username"), "Fabric must have game args");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Vanilla tests
|
||||
// ================================================================
|
||||
|
||||
@Test
|
||||
void vanilla_usesVanillaArgs() throws Exception {
|
||||
Instance instance = createVanillaFixture(tempDir);
|
||||
LaunchOptions options = new LaunchOptions();
|
||||
LaunchCommandBuilder builder = new LaunchCommandBuilder(instance);
|
||||
|
||||
List<String> command = builder.build(options);
|
||||
|
||||
// Vanilla uses -cp
|
||||
assertTrue(command.contains("-cp"), "Vanilla must have -cp");
|
||||
assertFalse(command.contains("-p"), "Vanilla must not have -p");
|
||||
|
||||
// -Djava.library.path present
|
||||
String expectedLibPath = "-Djava.library.path=" + tempDir.resolve("natives").toAbsolutePath();
|
||||
assertTrue(command.contains(expectedLibPath), "Vanilla must have -Djava.library.path");
|
||||
|
||||
// Memory/GC args
|
||||
assertTrue(command.contains("-Xmx4096M"), "Vanilla must have memory args");
|
||||
assertTrue(command.contains("-XX:+UseG1GC"), "Vanilla must have GC args");
|
||||
|
||||
// Main class
|
||||
assertTrue(command.contains("net.minecraft.client.main.Main"),
|
||||
"Vanilla main class must be net.minecraft.client.main.Main");
|
||||
|
||||
// Game args
|
||||
assertTrue(command.contains("--username"), "Vanilla must have --username");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Headless smoke tests — run Java with constructed args to verify
|
||||
// module path validity. No display needed, uses -version.
|
||||
// ================================================================
|
||||
|
||||
@Test
|
||||
void forge_modulePathIsValid_smoke() throws Exception {
|
||||
Instance instance = createForgeFixture(tempDir);
|
||||
LaunchOptions options = new LaunchOptions();
|
||||
LaunchCommandBuilder builder = new LaunchCommandBuilder(instance);
|
||||
|
||||
List<String> command = builder.build(options);
|
||||
|
||||
// Extract -p (module path) and its value
|
||||
int pIdx = command.indexOf("-p");
|
||||
assertTrue(pIdx >= 0, "Forge must have -p");
|
||||
String modulePath = command.get(pIdx + 1);
|
||||
|
||||
// Extract --add-modules, -Djava.library.path
|
||||
String addModules = null;
|
||||
String libPath = null;
|
||||
for (String arg : command) {
|
||||
if (arg.startsWith("-Djava.library.path=")) {
|
||||
libPath = arg;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < command.size(); i++) {
|
||||
if ("--add-modules=ALL-MODULE-PATH".equals(command.get(i))) {
|
||||
addModules = command.get(i);
|
||||
}
|
||||
}
|
||||
assertNotNull(addModules, "Must have --add-modules=ALL-MODULE-PATH");
|
||||
|
||||
// Build minimal Java command that validates the module path
|
||||
// java -Djava.library.path=<path> -p <classpath> --add-modules=ALL-MODULE-PATH -version
|
||||
ProcessBuilder pb = new ProcessBuilder(
|
||||
"java",
|
||||
libPath,
|
||||
"-p", modulePath,
|
||||
"--add-modules=ALL-MODULE-PATH",
|
||||
"-version"
|
||||
);
|
||||
pb.redirectErrorStream(true);
|
||||
|
||||
Process process = pb.start();
|
||||
String output = new String(process.getInputStream().readAllBytes());
|
||||
int exitCode = process.waitFor();
|
||||
|
||||
assertTrue(exitCode == 0,
|
||||
"Java should accept module path without errors. Exit code: " + exitCode
|
||||
+ "\nOutput: " + output);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>me.sashegdev</groupId>
|
||||
<artifactId>ZernMCLauncher</artifactId>
|
||||
<version>1.0.10</version>
|
||||
<version>1.0.11</version>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<name>ZernMC Launcher Parent</name>
|
||||
|
||||
Reference in New Issue
Block a user