Chat about this codebase

AI-powered code exploration

Online

Project Overview

OpenLauncherLib is a Java library for programmatically launching external Java applications and Minecraft instances. It streamlines classpath management, JVM argument configuration, profile handling and localization, while providing utilities for splash screens, data persistence and RAM selection. You can embed it in desktop clients, server tools or custom launchers to automate pre- and post-launch tasks.

What Problems It Solves

  • Centralizes JVM argument and classpath configuration for any Java application
  • Simplifies Minecraft launch integration without manual dependency handling
  • Provides built-in localization (i18n) support for multi-language UIs
  • Offers utilities for splash screens, configuration storage and memory management

High-Level Capabilities

  • External Java launching: full control over main class, JVM options, environment variables
  • Minecraft launching: auto-resolve Mojang libraries, assets, versions and authentication
  • Configuration profiles: create, load and persist multiple launch setups
  • Internationalization: load and switch locale bundles at runtime
  • Utilities: data storage helpers, RAM selector components, customizable splash displays

Licensing

You may choose between two licensing options:

  • GNU General Public License v3 (GPLv3)
    • Requires derivative works to remain open source under GPLv3
    • Include the full LICENSE text in your distribution
  • GNU Lesser General Public License v3 (LGPLv3)
    • Permits linking from proprietary software
    • Include the LICENSE.LESSER text and notices in your distribution

Select the license that matches your project’s distribution and linking requirements.

Quick Start

Below is a basic example demonstrating how to launch a Minecraft instance with custom JVM arguments and RAM settings.

import com.flowpowered.launcher.Launcher;
import com.flowpowered.launcher.profile.LaunchProfile;

public class MinecraftLauncherExample {
    public static void main(String[] args) {
        // Configure launch profile
        LaunchProfile profile = new LaunchProfile()
            .setMainClass("net.minecraft.client.main.Main")
            .addJvmArgument("-Xms512M")
            .addJvmArgument("-Xmx2G")
            .addProgramArgument("--username", "Player123")
            .setWorkingDirectory("game-directory");

        // Create and run launcher
        Launcher launcher = new Launcher(profile);
        launcher.launch();
    }
}

This snippet:

  • Sets the Minecraft main class and working directory
  • Adds JVM flags for minimum and maximum RAM
  • Passes the --username argument to the game
  • Initializes and starts the launcher with one method call

Getting Started

This guide shows how to add OpenLauncherLib to your Gradle project, bootstrap the Gradle wrapper, and launch an external Java application in minutes.

1. Initialize a Java application project

mkdir my-launcher
cd my-launcher
gradle init --type java-application

2. Configure build.gradle

Replace your build.gradle with:

plugins {
    id 'java'
    id 'application'
    id 'com.github.johnrengelman.shadow' version '7.1.2'
}

group = 'com.example'
version = '1.0.0'
application {
    mainClass = 'com.example.AppLauncher'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'fr.theshark34.openlauncherlib:openlauncherlib:VERSION'
}

– Replace VERSION with the latest release (check Maven Central).
– The Shadow plugin builds a fat JAR for distribution.

3. Bootstrap & build with the Gradle Wrapper

Generate the wrapper (if not already committed):

gradle wrapper --gradle-version 8.10 --distribution-type all

Build the project:

./gradlew clean build

4. Create your launcher

  1. Create a libs/ directory and place the target JAR there (for this example, HelloWorld.jar containing com.example.HelloWorld).
  2. Add src/main/java/com/example/AppLauncher.java:
package com.example;

import fr.theshark34.openlauncherlib.external.ExternalLaunchProfile;
import fr.theshark34.openlauncherlib.external.ExternalLauncher;
import fr.theshark34.openlauncherlib.LaunchException;

import java.nio.file.Paths;
import java.util.Collections;

public class AppLauncher {
    public static void main(String[] args) {
        // 1. Build classpath string pointing to your JAR
        String classpath = Paths.get("libs/HelloWorld.jar").toString();
        // 2. Create a launch profile (main class, classpath, no extra args)
        ExternalLaunchProfile profile = new ExternalLaunchProfile(
            "com.example.HelloWorld",
            classpath,
            Collections.emptyList(),
            Collections.emptyList()
        );
        try {
            // 3. Launch and wait for exit
            Process process = new ExternalLauncher(profile).launch();
            int exitCode = process.waitFor();
            System.out.println("External process exited with code " + exitCode);
        } catch (LaunchException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

5. Run your launcher

Invoke via the Gradle wrapper:

./gradlew run

You should see the output from HelloWorld.jar followed by
External process exited with code 0.

6. Build a fat JAR (optional)

Produce a single executable JAR including OpenLauncherLib:

./gradlew shadowJar

Run:

java -jar build/libs/my-launcher-1.0.0-all.jar

Your launcher is now ready for distribution!

Core Concepts & API Guide

This section explores OpenLauncherLib’s primary APIs and design patterns: launching external processes, building launch profiles, managing JSON configurations, and handling translations with fallback.

External Process Launching

Purpose: Configure and launch an external Java process with custom classpath, JVM/program arguments, working directory, and ProcessBuilder tweaks.

1. Build the classpath

Use ClasspathConstructor to collect JARs/paths and generate the platform-specific classpath string.

import io.github.opencubicchunks.launcher.ClasspathConstructor;
import java.nio.file.Paths;

// Collect library JARs and main application JAR
ClasspathConstructor cp = new ClasspathConstructor();
cp.add(Paths.get("libs/dependency1.jar"));
cp.add(Paths.get("libs/dependency2.jar"));
cp.add(Paths.get("myapp.jar"));

String classPath = cp.make();  // handles OS-specific separators

2. Create an ExternalLaunchProfile

Supply the main class name, classpath, VM args, program args, stderr redirection, macOS dock name, and working directory.

import io.github.opencubicchunks.launcher.ExternalLaunchProfile;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;

List<String> vmArgs  = Arrays.asList("-Xmx1G", "-Dconfig=prod");
List<String> appArgs = Arrays.asList("--port", "8080");
Path workDir         = Paths.get("run");

ExternalLaunchProfile profile = new ExternalLaunchProfile(
    "com.example.Main",
    classPath,
    vmArgs,
    appArgs,
    true,               // redirect stderr to stdout
    "MyApp",            // macOS dock name
    workDir
);

3. (Optional) Customize the ProcessBuilder

Implement BeforeLaunchingEvent to tweak environment variables, I/O redirection, or other settings.

import io.github.opencubicchunks.launcher.BeforeLaunchingEvent;
import java.io.File;

BeforeLaunchingEvent hook = builder -> {
    builder.environment().put("MY_ENV", "value");
    builder.redirectOutput(new File("logs/out.log"));
};

4. Instantiate and launch

Use ExternalLauncher to start the process and optionally stream logs.

import io.github.opencubicchunks.launcher.ExternalLauncher;
import io.github.opencubicchunks.launcher.LaunchException;

ExternalLauncher launcher = new ExternalLauncher(profile, hook);
launcher.setLogsEnabled(true);

try {
    Process process = launcher.launch();
    int exitCode = process.waitFor();
    System.out.println("Exited with code " + exitCode);
} catch (LaunchException | InterruptedException e) {
    e.printStackTrace();
}

Practical guidance:

  • Always call ClasspathConstructor.make() to handle OS-specific separators.
  • Use redirectErrorStream(true) for unified output.
  • Omit BeforeLaunchingEvent if no custom tweaks are needed.
  • Call launcher.setLogsEnabled(false) to suppress automatic console logs.
  • Handle the returned Process to monitor exit codes or implement IPC.

Generating an External Launch Profile

Purpose: Build an ExternalLaunchProfile for launching Minecraft using GameInfos, GameFolder, and AuthInfos.

1. Prepare your inputs

  • AuthInfos: holds username, access token, UUID, and optional client token.
  • GameFolder: directory layout for assets, libraries, natives, and the main JAR.
  • GameVersion: version identifier and GameType (vanilla, Forge, Fabric, etc.).
  • GameInfos: server name, GameVersion, optional GameTweak array.

2. Construct AuthInfos and GameFolder

import io.github.opencubicchunks.launcher.minecraft.AuthInfos;
import io.github.opencubicchunks.launcher.minecraft.GameFolder;

// Fill with values from your auth flow
AuthInfos auth = new AuthInfos(
    "Notch",
    "abcd-1234-access-token",
    "uuid-5678",
    "clientToken-0000"
);

GameFolder folder = new GameFolder(
    "assets",
    "libraries",
    "natives",
    "versions/1.16.5/1.16.5.jar"
);

3. Define GameVersion and (for Forge 1.13+) NFVD

import io.github.opencubicchunks.launcher.minecraft.GameType;
import io.github.opencubicchunks.launcher.minecraft.GameVersion;
import io.github.opencubicchunks.launcher.minecraft.NewForgeVersionDiscriminator;

// Vanilla or older Forge
GameVersion version = new GameVersion("1.16.5", GameType.V1_8_HIGHER);

// Forge 1.13+ requires an NFVD implementation
NewForgeVersionDiscriminator nfvd = new YourNFVDImplementation(...);
GameType.V1_13_HIGHER_FORGE.setNFVD(nfvd);
GameVersion forgeVersion = new GameVersion("1.16.5-forge", GameType.V1_13_HIGHER_FORGE);

4. Build GameInfos with optional tweaks

import io.github.opencubicchunks.launcher.minecraft.GameInfos;
import io.github.opencubicchunks.launcher.minecraft.GameTweak;

// Vanilla launch
GameInfos vanillaInfos = new GameInfos("MyServer", version, new GameTweak[0]);

// With Optifine and Shaders
GameInfos tweakedInfos = new GameInfos(
    "MyServer",
    version,
    new GameTweak[]{ GameTweak.OPTIFINE, GameTweak.SHADER }
);

5. Create the ExternalLaunchProfile

import io.github.opencubicchunks.launcher.minecraft.MinecraftLauncher;

ExternalLaunchProfile profile = MinecraftLauncher.createExternalProfile(
    tweakedInfos,
    folder,
    auth
);

Internally this verifies folders and JARs, builds classpath, chooses mainClass, assembles VM/program args, and handles tweak classes.

6. Launch Minecraft

ProcessBuilder pb = profile.getProcessBuilder();
Process gameProcess = pb.start();

Practical tips:

  • Match GameFolder paths to your unpacked assets and libraries.
  • For Forge 1.13+, omit GameTweak.FORGE—the GameType handles Forge.
  • OpenLauncherLib filters incompatible tweaks; check logs (LogUtil) for “support-forge” or “tweak-deprec” messages.

SimpleConfiguration: JSON-based Configuration Storage

Purpose: Provide a lightweight JSON-backed Configuration with nested keys, default values, and on-disk persistence.

Usage Overview

  1. Obtain a Configuration via ConfigurationManager.
  2. Read values with get(...) or getOrSet(...).
  3. Mutate values with set(...).
  4. Persist changes with save() (or auto-save via set(..., save=true)).

Retrieving a Configuration

import io.github.opencubicchunks.launcher.config.Configuration;
import io.github.opencubicchunks.launcher.config.ConfigurationManager;
import io.github.opencubicchunks.launcher.config.DefaultConfigurationManager;
import java.util.logging.Logger;

Logger logger = Logger.getLogger("cfg");
ConfigurationManager cfgMgr = new DefaultConfigurationManager(logger);
Configuration cfg = cfgMgr.getConfiguration("config.json");

Reading Values

int port        = cfg.get(25565, "server", "port");
String host     = cfg.get("localhost", "server", "host");
boolean enabled = cfg.get(false, "features", "coolFeatureEnabled");

Auto-inserting Defaults

String theme = cfg.getOrSet("dark", "ui", "theme");
// To immediately write default to disk:
String lang = cfg.getOrSet("en_US", true, "ui", "language");

Setting and Removing Values

// Auto-saves
cfg.set(1024, "window", "width");

// Batch update without saving
cfg.set("v1.2.3", false, "app", "version");

// Remove a key
cfg.set(null, true, "obsolete", "entry");

Manually Saving

try {
    cfg.save();
} catch (IOException e) {
    logger.severe("Failed to save config: " + e.getMessage());
}

Nested keys (String... path) create intermediate JSON objects automatically. Setting at root (cfg.set(obj)) replaces the entire config.

Best practices:

  • Use getOrSet(default, true, …) for first-run defaults.
  • Batch multiple set(...) calls then call save() once.
  • Catch and log IOException on save() to avoid silent failures.

Retrieving Translated Strings with Fallback

Purpose: Use Language.get(...) to fetch translations and automatically fall back to the default language.

Initialization and Registration

import io.github.opencubicchunks.launcher.i18n.DefaultLanguageManager;
import io.github.opencubicchunks.launcher.i18n.LanguageInfo;
import io.github.opencubicchunks.launcher.config.ConfigurationManager;
import java.util.logging.Logger;

Logger logger = Logger.getLogger("lang");
ConfigurationManager cfgMgr = new DefaultConfigurationManager(logger);
DefaultLanguageManager langMgr = new DefaultLanguageManager(logger, cfgMgr);

// Register English and French JSON files from classpath
langMgr.registerLanguage(LanguageInfo.IDENTIFY, LanguageInfo.EN, "/assets/languages/");
langMgr.registerLanguage(LanguageInfo.IDENTIFY, LanguageInfo.FR, "/assets/languages/");

// Set default (fallback) language
langMgr.setDefaultLanguage(langMgr.getLanguage(LanguageInfo.EN));

Fetching Translations

import io.github.opencubicchunks.launcher.i18n.Language;

// Retrieve French translation
Language french = langMgr.getLanguage(LanguageInfo.FR);
String optionsFr = french.get(LanguageInfo.OPTIONS);  

// Missing key in French: falls back to English or returns raw key
String undefinedInFr = french.get(LanguageInfo.SOME_NEW_KEY);

// Nested lookup (e.g., { "menu": { "exit": "Quit" } })
String exitLabel = french.get(LanguageInfo.MENU, "exit");

Practical guidance:

  • Always call setDefaultLanguage(...) before fetching translations.
  • Use varargs nodes to traverse nested JSON structures.
  • Registering the same language twice merges JSON files.
  • Missing keys return the default-language value or the raw key path.

These core APIs and patterns form the foundation of OpenLauncherLib’s extensible and practical design.

Utility Modules

A collection of optional helpers that simplify common launcher tasks, from RAM selection to crash reporting and logging.

Customizing the Ram Selector Frame

Show how to provide a bespoke RAM‐selection UI by subclassing AbstractOptionFrame and plugging it into RamSelector.

  1. Implement your custom frame
    • Extend AbstractOptionFrame
    • Provide a public constructor taking a single RamSelector parameter
    • Override getSelectedIndex() / setSelectedIndex(int) to bind your UI components
import fr.theshark34.openlauncherlib.util.ramselector.AbstractOptionFrame;
import fr.theshark34.openlauncherlib.util.ramselector.RamSelector;
import javax.swing.*;
import java.awt.*;

public class MyRamOptionFrame extends AbstractOptionFrame {
    private final JSlider slider;
    private final JLabel valueLabel;

    public MyRamOptionFrame(RamSelector selector) {
        super(selector);
        setTitle("Custom RAM Selector");
        setSize(300, 120);
        setLayout(new BorderLayout());
        setResizable(false);
        setLocationRelativeTo(null);

        slider = new JSlider(0, RamSelector.RAM_ARRAY.length - 1, readInitialIndex());
        slider.setMajorTickSpacing(1);
        slider.setPaintTicks(true);
        slider.addChangeListener(e -> valueLabel.setText(RamSelector.RAM_ARRAY[slider.getValue()]));

        valueLabel = new JLabel(RamSelector.RAM_ARRAY[slider.getValue()], SwingConstants.CENTER);

        add(valueLabel, BorderLayout.NORTH);
        add(slider, BorderLayout.CENTER);
    }

    private int readInitialIndex() {
        return getSelector().getFrame() == null
            ? getSelector().readRam()
            : getSelector().getFrame().getSelectedIndex();
    }

    @Override
    public int getSelectedIndex() {
        return slider.getValue();
    }

    @Override
    public void setSelectedIndex(int index) {
        slider.setValue(index);
        valueLabel.setText(RamSelector.RAM_ARRAY[index]);
    }
}
  1. Register your frame with RamSelector
import java.nio.file.Paths;
import fr.theshark34.openlauncherlib.util.ramselector.RamSelector;

Path configFile = Paths.get("config/ram.txt");
RamSelector selector = new RamSelector(configFile);

// Use your custom frame
selector.setFrameClass(MyRamOptionFrame.class);
selector.display();
  1. Retrieve JVM arguments and persist choice
String[] vmArgs = selector.getRamArguments(); // ["-Xms1024M", "-Xmx2048M"]
selector.save();
MyLauncher.launch(vmArgs);

Practical tips:

  • Constructor must accept only RamSelector for reflective instantiation.
  • Call selector.save() on user confirmation.
  • Saved index (0–9) is stored as a plain integer.

CrashReporter: Automatic Crash Report Generation

Catch unexpected exceptions, write detailed stack traces to timestamped files, notify the user, and exit cleanly.

Key API

  • new CrashReporter(String projectName, Path crashDir)
  • catchError(Exception e, String userMessage)
  • writeError(Exception e) → Path
  • makeCrashReport(String projectName, Exception e) → String
import fr.theshark34.openlauncherlib.util.CrashReporter;
import java.nio.file.Paths;
import java.nio.file.Path;

Path crashDir = Paths.get("logs/crashes");
CrashReporter reporter = new CrashReporter("MyLauncher", crashDir);

try {
    // startup logic...
} catch(Exception e) {
    reporter.catchError(e, "Oops! The launcher failed to start.");
}

Practical guidance:

  • Ensure crashDir is writable; directories are created automatically.
  • Use reporter.setDir(newDir) to change output at runtime.
  • Override or copy makeCrashReport() to customize report format.

LogUtil: Localized and Prefixed Logging

Centralize console logging with automatic language translation and a consistent [OpenLauncherLib] prefix.

Key Methods

  • LogUtil.info(String… keys) / LogUtil.err(String… keys)
  • LogUtil.rawInfo(String message) / LogUtil.rawErr(String message)
  • LogUtil.getLanguageManager() / LogUtil.getIdentifier()
import fr.theshark34.openlauncherlib.util.LogUtil;

LogUtil.info("launching", "version-info");
// Console: [OpenLauncherLib] Launching… Version 1.0.0

Practical guidance:

  • Place your .properties files under /assets/languages/.
  • Use rawInfo for dynamic or debug messages.
  • Force a language via LogUtil.getLanguageManager().setDefaultLanguage("fr").

ProcessLogManager: Capturing Subprocess Output

Stream stdout or stderr from a Process into console and optionally to a file.

Core API

  • new ProcessLogManager(InputStream in)
  • new ProcessLogManager(InputStream in, Path logFile)
  • setPrint(boolean) / setToWrite(Path)
  • Call start() to begin reading in a thread.
import fr.theshark34.openlauncherlib.util.ProcessLogManager;
import fr.theshark34.openlauncherlib.util.LogUtil;
import java.nio.file.Paths;
import java.nio.file.Path;

ProcessBuilder pb = new ProcessBuilder("java", "-jar", "game.jar");
Process game = pb.start();

Path logPath = Paths.get("logs/game-output.txt");
ProcessLogManager outLogger = new ProcessLogManager(game.getInputStream(), logPath);
outLogger.setPrint(true);
outLogger.start();

ProcessLogManager errLogger = new ProcessLogManager(game.getErrorStream());
errLogger.setPrint(true);
errLogger.start();

int exitCode = game.waitFor();
LogUtil.info("game-exited", String.valueOf(exitCode));

Practical guidance:

  • Use separate managers for stdout and stderr.
  • Call setPrint(false) to disable console output.
  • Ensure log file’s parent directories exist or catch write errors.

Directory Exploration with Explorer

Navigate, list and filter files and folders using Explorer, ExploredDirectory, and FileList.

Initializing an Explorer

import fr.theshark34.openlauncherlib.util.explorer.Explorer;
import fr.theshark34.openlauncherlib.util.explorer.ExploredDirectory;
import java.nio.file.Paths;

ExploredDirectory root = Explorer.dir("path/to/myDir");
Explorer explorer = new Explorer(Paths.get("path/to/myDir"));

Changing Directories

explorer.cd(Paths.get("anotherDir"));
explorer.cd("subFolder"); // throws FailException if not found

Listing Contents

FileList entries = explorer.list();
FileList subdirs = explorer.subs();
FileList filesOnly = explorer.files();
FileList all = explorer.allRecursive();

Accessing Specific Files or Subfolders

ExploredDirectory images = root.sub("images");
Path config = root.get("config.json");

Filtering with FileList

FileList jars = root.allRecursive().match(".*\\.jar$");

Practical tips:

  • All methods throw FailException on missing targets.
  • Chain calls fluently:
List<Path> pngs = Explorer
  .dir("assets")
  .sub("images")
  .allRecursive()
  .match(".*\\.png$")
  .get();
  • Accumulate multiple directories via FileList.add(...).

JavaUtil: Managing Java Execution and Native Library Paths

Provide and override the Java executable command and adjust java.library.path at runtime.

Key Methods

  • getJavaCommand()
  • setJavaCommand(String)
  • setLibraryPath(String)
import fr.theshark34.openlauncherlib.util.JavaUtil;

// Default java command
String javaCmd = JavaUtil.getJavaCommand();

// Override with custom JRE
JavaUtil.setJavaCommand("/opt/my_jre/bin/java");

// Update native library path
try {
    JavaUtil.setLibraryPath("/path/to/native/libs");
} catch (Exception e) {
    e.printStackTrace();
}
System.loadLibrary("my_native_lib");

Practical guidance:

  • Call setJavaCommand(...) before launching subprocesses.
  • Use setLibraryPath(...) early if bundling native libraries.
  • Reflection-based path changes may be restricted under some JVM security settings.

Saver: Simple Key-Value Persistence via Properties Files

Read, write and remove string key/value pairs from a disk-backed Java Properties file seamlessly.

Core Features

  • Loads or creates the file and parent directories on instantiation
  • set(key, value) / get(key) / get(key, default) / remove(key)
import fr.theshark34.openlauncherlib.util.Saver;
import java.nio.file.Paths;

Saver saver = new Saver(Paths.get(System.getProperty("user.home"), "myapp", "config.properties"));

// Store
saver.set("username", "player42");

// Retrieve with default
String user = saver.get("username", "guest");

// Remove
saver.remove("username");
String guest = saver.get("username", "guest");

Practical guidance:

  • Mutating operations save immediately; avoid in tight loops.
  • Catch FailException to handle I/O or permission errors.

SplashScreen: Lightweight Image-Driven Launcher Splash

Display an undecorated, optionally transparent, splash window with a background image.

Constructor

  • new SplashScreen(String title, Image image)

Display Methods

  • display()
  • displayFor(long ms) → returns Thread
  • stop()
  • setTransparent()
import fr.theshark34.openlauncherlib.util.SplashScreen;
import java.awt.Image;
import java.awt.Toolkit;

// Load image
Image img = Toolkit.getDefaultToolkit().getImage("assets/splash.png");
SplashScreen splash = new SplashScreen("MyApp", img);
splash.setTransparent();
Thread splashThread = splash.displayFor(3000);

// Initialize resources off the EDT
initializeGame();

splashThread.join();

Practical guidance:

  • Call setTransparent() before display() for per-pixel transparency.
  • Use displayFor(...) for fire-and-forget; join the returned Thread to wait.
  • Run heavy initialization off the Event Dispatch Thread to keep UI responsive.

Advanced Usage & Customization

This section shows how to integrate Forge arguments, use NoFramework for custom launch flows, and hook into the launch process with callbacks.

Forge Launch Argument Provider

Generate and supply standard launch arguments for a Forge-based instance by implementing IForgeArgumentsProvider and using NewForgeVersionDiscriminator to extract version metadata.

How It Works

  • NewForgeVersionDiscriminator parses a Forge JSON (e.g. 1.15.2-forge-32.0.2.json) to extract:
    • forgeVersion (e.g. “32.0.2”)
    • mcVersion (e.g. “1.15.2”)
    • forgeGroup (e.g. “net.minecraftforge”)
    • mcpVersion (e.g. “20200625.160719”)
  • IForgeArgumentsProvider’s default getForgeArguments() builds:
    --launchTarget fmlclient
    --fml.forgeVersion <forgeVersion>
    --fml.mcVersion    <mcVersion>
    --fml.forgeGroup   <forgeGroup>
    --fml.mcpVersion   <mcpVersion>
    

Implementing IForgeArgumentsProvider

import fr.flowarg.openlauncherlib.NewForgeVersionDiscriminator;
import fr.flowarg.openlauncherlib.IForgeArgumentsProvider;
import java.nio.file.Path;
import java.util.List;
import java.util.ArrayList;

public class MyForgeArgsProvider implements IForgeArgumentsProvider {
    private final NewForgeVersionDiscriminator nfvd;

    // Load from JSON: versionsDir/1.15.2-forge-32.0.2.json
    public MyForgeArgsProvider(Path versionsDir, String mcVersion, String forgeVersion) throws Exception {
        this.nfvd = new NewForgeVersionDiscriminator(versionsDir, mcVersion, forgeVersion);
    }

    @Override
    public NewForgeVersionDiscriminator getNFVD() {
        return nfvd;
    }
}

Retrieve and apply the arguments:

MyForgeArgsProvider provider = new MyForgeArgsProvider(versionsDir, "1.15.2", "32.0.2");
List<String> forgeArgs = provider.getForgeArguments();

List<String> cmd = new ArrayList<>();
cmd.add(javaPath.toString());
cmd.addAll(forgeArgs);
cmd.addAll(userDefinedArgs);

ProcessBuilder pb = new ProcessBuilder(cmd);
pb.directory(gameDirectory.toFile());
Process process = pb.start();

Customizing Arguments

Override getForgeArguments() to inject extra flags:

@Override
public List<String> getForgeArguments() {
    List<String> args = new ArrayList<>(IForgeArgumentsProvider.super.getForgeArguments());
    args.add("--fml.log.level");
    args.add("DEBUG");
    return args;
}

Tips

  • Match JSON file name to mcVersion-forge-forgeVersion.json.
  • Keep the official Forge JSON structure; the parser relies on its layout.
  • For testing, use the no-IO constructor:
    NewForgeVersionDiscriminator mockNfvd =
        new NewForgeVersionDiscriminator("32.0.2", "1.15.2", "net.minecraftforge", "20200625.160719");
    
  • Call getForgeArguments() at launch time to pick up dynamic changes.

Launching Minecraft with NoFramework

Configure and start a vanilla or mod-loaded Minecraft instance using NoFramework, with custom JSONs, extra arguments, and launch callbacks.

  1. Instantiate NoFramework
import fr.flowarg.openlauncherlib.NoFramework;
import fr.flowarg.openlauncherlib.auth.AuthInfos;
import fr.flowarg.openlauncherlib.GameFolder;

Path gameDir = Paths.get("C:/Minecraft/instances/MyPack");
AuthInfos auth = new AuthInfos("playerName", "uuid", "accessToken", "clientId", "authXUID");
GameFolder folder = new GameFolder("libraries", "minecraft.jar", "assets", "natives");

// Without extra args:
NoFramework noFramework = new NoFramework(gameDir, auth, folder);

// Or supply initial VM args:
// List<String> vmArgs = List.of("-Xmx2G", "-Xms1G");
// NoFramework noFramework = new NoFramework(gameDir, auth, folder, vmArgs, NoFramework.Type.VM);
  1. Override JSON filenames
noFramework.setCustomVanillaJsonFileName("custom/1.18.2-custom.json");
noFramework.setCustomModLoaderJsonFileName("custom/forge-1.18.2-40.0.50.json");
  1. Add extra JVM and game arguments
noFramework.setAdditionalVmArgs(List.of("-Xmx4G", "-XX:+UseG1GC"));
noFramework.setAdditionalArgs(List.of("--width=1280", "--height=720", "--server", "play.example.com"));
  1. Register a pre-launch callback
noFramework.setLastCallback(launcher -> {
    launcher.getProcessBuilder()
            .inheritIO()
            .directory(gameDir.toFile());
});
  1. Launch the process
Process gameProcess = noFramework.launch(
    "1.18.2",                    // Minecraft version
    "40.0.50",                   // Forge loader version (ignored for VANILLA)
    NoFramework.ModLoader.FORGE  // or ModLoader.VANILLA
);
int exitCode = gameProcess.waitFor();

Tips

  • For vanilla, use ModLoader.VANILLA and pass "" as loader version.
  • Additional args append after JSON-specified ones; avoid duplicates.
  • Custom JSON paths are relative to gameDir.
  • Use the callback to set env vars or redirect I/O.

BeforeLaunchingEvent

Provide a hook just before the process starts to tweak the ProcessBuilder—add JVM args, set environment variables, change working directory, or redirect streams.

Definition

package fr.flowarg.openlauncherlib.external;

public interface BeforeLaunchingEvent {
    /**
     * Called after the ProcessBuilder has been configured by the launcher,
     * but before the process is actually started.
     * @param builder the ProcessBuilder to customize
     */
    void onLaunching(ProcessBuilder builder);
}

Usage

  1. Implement the interface (or use a lambda).
  2. Register with your launcher instance.
  3. Inside onLaunching, call builder.command(), builder.environment(), builder.directory(), or any other ProcessBuilder API.

Example: Inject JVM Arg, Env Var & Redirect Output

import fr.flowarg.openlauncherlib.external.BeforeLaunchingEvent;
import fr.flowarg.openlauncherlib.launcher.ExternalLauncher;

ExternalLauncher launcher = new ExternalLauncher(profile, settings);

launcher.setBeforeLaunchingEvent(builder -> {
    // Insert debug flag after 'java'
    List<String> cmd = builder.command();
    cmd.add(1, "-Dexample.debug=true");
    builder.command(cmd);

    // Set an environment variable
    builder.environment().put("MY_LAUNCHER_MODE", "debug");

    // Change working directory
    builder.directory(new File("C:/mygame"));

    // Redirect stdout and stderr
    builder.redirectOutput(ProcessBuilder.Redirect.appendTo(new File("logs/output.log")));
    builder.redirectErrorStream(true);
});

launcher.launch();

Tips

  • JVM argument order matters: insert flags before the main class or jar.
  • Use builder.environment() to set or override variables (e.g. java.library.path).
  • Redirecting streams early helps capture startup logs.
  • Changing builder.directory() allows relative mods/resources in custom folders.

Development & Contribution Guide

This guide explains how to build, test, sign, publish OpenLauncherLib and contribute fixes or enhancements.

Prerequisites

  • Java 8 (JDK 8)
  • Git client
  • GPG key for signing (with passphrase)
  • GitHub account with repository write access
  • OSSRH/Maven Central credentials (OSSRH_USERNAME, OSSRH_PASSWORD, SONATYPE_USERNAME, SONATYPE_TOKEN)

1. Gradle Wrapper Setup & Usage

Ensure every build uses Gradle 8.10 without a local install.

Commit these four files:

Invoke tasks:

# UNIX/macOS
./gradlew clean build --parallel
./gradlew test

# Windows
gradlew.bat clean build

Override JVM options:

export GRADLE_OPTS="-Xmx2g"
./gradlew build

Upgrade wrapper:

./gradlew wrapper --gradle-version 8.10 --distribution-type all
git add gradlew gradlew.bat gradle/wrapper/gradle-wrapper.properties
git commit -m "Upgrade Gradle wrapper to 8.10"

2. Building & Testing

Run full build and tests locally:

./gradlew clean build
  • build compiles, runs tests, creates shadow JAR.
  • test runs unit tests only.
  • shadowJar produces a fat JAR in build/libs/OpenLauncherLib-all.jar.

Inspect test reports: build/reports/tests/test/index.html.

3. Signing & Publishing Artifacts

Local Dry Run

Simulate publishing without pushing:

./gradlew publish \
  -PossrhUsername=$OSSRH_USERNAME \
  -PossrhPassword=$OSSRH_PASSWORD \
  -PgpgPrivateKey="$GPG_PRIVATE_KEY" \
  -PgpgPassphrase="$GPG_PASSPHRASE" \
  -PsonatypeUsername=$SONATYPE_USERNAME \
  -PsonatypeToken=$SONATYPE_TOKEN

GitHub Actions Release

On each GitHub release, CI uses .github/workflows/gradle-publish.yml:

name: Publish OpenLauncherLib
on:
  release:
    types: [published]
jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - name: Set up JDK 8
        uses: actions/setup-java@v4
        with:
          java-version: '8'
          distribution: 'zulu'
      - name: Publish to Maven Central
        run: ./gradlew publish
        env:
          OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}
          OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
          SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
          SONATYPE_TOKEN: ${{ secrets.SONATYPE_TOKEN }}
          GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
          GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}

Secrets to configure in repo Settings → Secrets:

  • OSSRH_USERNAME / OSSRH_PASSWORD
  • SONATYPE_USERNAME / SONATYPE_TOKEN
  • GPG_PRIVATE_KEY (base64-encoded)
  • GPG_PASSPHRASE

Verify publication via Maven Central staging interface and GitHub Actions logs.

4. Contributing Code

  1. Fork the repository and clone your fork.
  2. Create a feature branch:
    git checkout -b feature/your-description
    
  3. Implement code and corresponding tests under src/main/java and src/test/java.
  4. Ensure code style compliance:
    ./gradlew check
    
  5. Commit with descriptive message:
    git add .
    git commit -m "Add feature X: explain benefit"
    
  6. Push and open a Pull Request against main.
  7. Pass CI checks (build, tests, lint).
  8. Address review feedback; squash or rebase as needed.

5. License Compliance

  • All source files must include the appropriate license header:
    • Use GPLv3 header for application code (see LICENSE).
    • Use LGPLv3 header for library components (see LICENSE.LESSER).
  • Place headers at the very top of each file before package/imports.
  • Update year and author in each new file.
  • Do not remove warranty disclaimer or license pointers.

By following this guide, you ensure consistent builds, secure publication, and smooth collaboration.