/* SPDX-FileCopyrightText: 2026 Project Tick * SPDX-FileContributor: Project Tick * SPDX-License-Identifier: GPL-3.0-or-later * * MeshMC - A Custom Launcher for Minecraft * Copyright (C) 2026 Project Tick * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Linking this library statically or dynamically with other modules is * making a combined work based on this library. Thus, the terms and * conditions of the GNU General Public License cover the whole * combination. * * As a special exception, the copyright holders of this library give * you permission to link this library with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also meet, * for each linked independent module, the terms and conditions of the * license of that module. An independent module is a module which is * not derived from or based on this library. If you modify this * library, you may extend this exception to your version of the * library, but you are not obliged to do so. If you do not wish to do * so, delete this exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2012-2021 MultiMC Contributors * * 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.projecttick.modern; import org.projecttick.MeshMC; import org.projecttick.ParamBucket; import org.projecttick.Utils; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * Modern launcher implementation for Minecraft 1.0 and above. * * Unlike OneSixLauncher which loads the game in-process via reflection, * ModernLauncher spawns Minecraft as a separate child process using the * Java binary specified by MeshMC. This decouples the JVM version * running MeshMC library from the JVM version required by the game, * allowing Minecraft versions that require Java 21+ or newer (e.g., class * file version 69.0 / Java 25) to be launched correctly even when the * launcher itself runs on an older JVM. * * Parameters consumed from ParamBucket: * javaPath - Path to the Java binary for the game process (required) * cp - Classpath entries, one per entry (required) * mainClass - Main class to invoke (default: net.minecraft.client.main.Main) * param - Game arguments, one per entry * jvmArg - JVM arguments to forward to the game process, one per arg * natives - Path to the native libraries directory * serverAddress - Server address for direct-connect on launch (optional) * serverPort - Server port for direct-connect on launch (optional) */ public class ModernLauncher implements MeshMC { @Override public int launch(ParamBucket params) { try { return doLaunch(params); } catch (Exception e) { Utils.log("ModernLauncher encountered a fatal error: " + e.getMessage(), "Error"); e.printStackTrace(); return 1; } } private int doLaunch(ParamBucket params) throws Exception { // --- Java binary --- String javaPath = params.firstSafe("javaPath", "java"); Utils.log("Java binary: " + javaPath); // --- Main class --- String mainClass = params.firstSafe("mainClass", "net.minecraft.client.main.Main"); Utils.log("Main class: " + mainClass); // --- Classpath --- // "cp" = regular game libraries // "ext" = native-classifier JARs (e.g. lwjgl-3.x-natives-linux.jar) that // LWJGL 3's SharedLibraryLoader needs to find on the classpath. // OneSixLauncher works without these because it runs in-process and // MeshMC JVM already has them on its own classpath. For // ModernLauncher we must include them explicitly. List cpEntries = params.allSafe("cp", Collections.emptyList()); List extEntries = params.allSafe("ext", Collections.emptyList()); List allCpEntries = new ArrayList(cpEntries); allCpEntries.addAll(extEntries); if (allCpEntries.isEmpty()) { Utils.log("No classpath entries provided to ModernLauncher.", "Error"); return 1; } String classpath = buildClassPath(allCpEntries); // --- Native library path --- String natives = params.firstSafe("natives", ""); // --- JVM arguments (Xmx, Xms, GC flags, platform-specific flags, etc.) --- List jvmArgs = params.allSafe("jvmArg", Collections.emptyList()); // --- Game arguments --- // param entries are already pre-expanded by the C++ launcher (auth tokens, // game directory, asset index, resolution, etc.) List gameArgs = new ArrayList( params.allSafe("param", Collections.emptyList()) ); // Direct-connect: server address / port are passed as separate keys and // must be appended to game args manually (processMinecraftArgs skips them // when a launch script is used). String serverAddress = params.firstSafe("serverAddress", ""); String serverPort = params.firstSafe("serverPort", ""); if (serverAddress != null && !serverAddress.isEmpty()) { gameArgs.add("--server"); gameArgs.add(serverAddress); if (serverPort != null && !serverPort.isEmpty()) { gameArgs.add("--port"); gameArgs.add(serverPort); } } // --- Build the full command --- List command = buildCommand(javaPath, jvmArgs, natives, classpath, mainClass, gameArgs); // Log the full command for diagnostics (visible in launcher console) StringBuilder cmdLog = new StringBuilder("Spawning game process:\n CMD: "); for (String s : command) { cmdLog.append(s).append(" "); } Utils.log(cmdLog.toString().trim()); ProcessBuilder pb = new ProcessBuilder(command); // Let the game process inherit the working directory from us (set by LauncherPartLaunch) pb.directory(null); // Set JAVA_HOME so the game and any native launchers can locate Java correctly File javaFile = new File(javaPath); File javaParent = javaFile.getParentFile(); if (javaParent != null && javaParent.getParentFile() != null) { pb.environment().put("JAVA_HOME", javaParent.getParent()); } final Process process = pb.start(); // Pipe stdout and stderr from the child process to our own streams so // the C++ launcher's LoggedProcess can capture and display them. Thread outPipe = pipeStream(process.getInputStream(), System.out, "stdout"); Thread errPipe = pipeStream(process.getErrorStream(), System.err, "stderr"); outPipe.start(); errPipe.start(); // Shutdown hook: ensure the game process is terminated if MeshMC is // forcefully killed (e.g., SIGKILL from within MeshMC UI). Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { @Override public void run() { if (process.isAlive()) { process.destroyForcibly(); } } }, "ModernLauncher-ShutdownHook")); // Block until the game process exits int exitCode = process.waitFor(); outPipe.join(); errPipe.join(); if (exitCode != 0) { Utils.log("Game process exited with code " + exitCode, "Error"); } return exitCode; } private String buildClassPath(List entries) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < entries.size(); i++) { if (i > 0) sb.append(File.pathSeparator); sb.append(entries.get(i)); } return sb.toString(); } private List buildCommand( String javaPath, List jvmArgs, String natives, String classpath, String mainClass, List gameArgs) { List cmd = new ArrayList(); // Java executable cmd.add(javaPath); // JVM arguments (memory settings, GC options, platform flags, etc.) cmd.addAll(jvmArgs); // Native library path - needed for LWJGL and other native dependencies. // Pass both the standard JVM property and the LWJGL-specific property: // java.library.path - used by java.lang.System.loadLibrary() // org.lwjgl.librarypath - checked first by LWJGL 3.3+ before java.library.path if (natives != null && !natives.isEmpty()) { cmd.add("-Djava.library.path=" + natives); cmd.add("-Dorg.lwjgl.librarypath=" + natives); } // Classpath if (!classpath.isEmpty()) { cmd.add("-cp"); cmd.add(classpath); } // Main class cmd.add(mainClass); // Game arguments cmd.addAll(gameArgs); return cmd; } /** * Reads bytes from {@code src} and writes them to {@code dst} on a dedicated * daemon thread so neither stream blocks the main thread. */ private Thread pipeStream(final InputStream src, final PrintStream dst, final String name) { Thread t = new Thread(new Runnable() { @Override public void run() { byte[] buffer = new byte[8192]; int read; try { while ((read = src.read(buffer)) != -1) { dst.write(buffer, 0, read); dst.flush(); } } catch (IOException e) { // Stream closed - expected when the child process exits } } }, "ModernLauncher-" + name + "-pipe"); t.setDaemon(true); return t; } }