feat: add function to generate launch arguments with unfinished test

STILL WIP
This commit is contained in:
HerozDotExe 2025-10-28 10:48:58 +01:00
parent 8f2b5d6d4f
commit 976838bd3c
16 changed files with 324 additions and 63 deletions

View file

@ -31,6 +31,7 @@
"prettier": "3.6.2",
"tsup": "^8.5.0",
"typescript-eslint": "^8.44.1",
"uuid": "^13.0.0",
"vitest": "^3.2.4"
}
}

9
pnpm-lock.yaml generated
View file

@ -41,6 +41,9 @@ importers:
typescript-eslint:
specifier: ^8.44.1
version: 8.44.1(eslint@9.36.0)(typescript@5.9.2)
uuid:
specifier: ^13.0.0
version: 13.0.0
vitest:
specifier: ^3.2.4
version: 3.2.4(@types/node@24.5.2)
@ -1215,6 +1218,10 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
uuid@13.0.0:
resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==}
hasBin: true
vite-node@3.2.4:
resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@ -2396,6 +2403,8 @@ snapshots:
dependencies:
punycode: 2.3.1
uuid@13.0.0: {}
vite-node@3.2.4(@types/node@24.5.2):
dependencies:
cac: 6.7.14

119
src/core/arguments.ts Normal file
View file

@ -0,0 +1,119 @@
import path from "path";
import { isNeeded } from "../utils/rules";
import { Argument, Auth, PoolFile, Version } from "../utils/types";
import { getJavaExecutable } from "./java";
import { getLibraries } from "./libraries";
import { version as packageVersion } from "../../package.json";
function fillArguments(
arg: string,
versionManifest: Version,
gameRoot: string,
classPaths: PoolFile[],
auth: Auth,
) {
const argumentsToFill = {
"${natives_directory}": path.join(gameRoot, "natives"),
"${launcher_name}": "nlk",
"${launcher_version}": packageVersion,
"${classpath}": classPaths,
"${game_directory}": gameRoot,
"${version_name}": versionManifest.id,
"${version_type}": versionManifest.type,
"${assets_index_name}": versionManifest.assets,
"${assets_root}": path.join(gameRoot, "assets"),
"${auth_access_token}": auth.access_token,
"${auth_session}": auth.access_token,
"${auth_player_name}": auth.name,
"${auth_uuid}": auth.uuid,
"${auth_xuid}": auth.meta?.xuid || auth.access_token,
"${user_properties}": auth.user_properties,
"${user_type}": auth.meta?.type || "msa",
"${clientid}":
auth.meta?.clientId || auth.client_token || auth.access_token,
};
for (const key in argumentsToFill) {
if (Object.prototype.hasOwnProperty.call(argumentsToFill, key)) {
const value = argumentsToFill[key];
arg = arg.replace(key, value);
}
}
return arg;
}
function parseArg(
this: string[],
arg: Argument,
versionManifest: Version,
gameRoot: string,
classPaths: PoolFile[],
auth: Auth,
) {
if (isNeeded(arg)) {
if (typeof arg === "string") {
this.push(
fillArguments(arg, versionManifest, gameRoot, classPaths, auth),
);
} else if (typeof arg.value === "string") {
this.push(
fillArguments(arg.value, versionManifest, gameRoot, classPaths, auth),
);
} else if (arg.value.length > 1) {
for (const e of arg.value) {
this.push(
fillArguments(e, versionManifest, gameRoot, classPaths, auth),
);
}
}
}
return this;
}
function generateClassPaths(versionManifest: Version, librariesRoot: string) {
const libs = getLibraries(versionManifest, librariesRoot).map(
(lib) => lib.path,
);
libs.push(path.join(librariesRoot, "..", "versions", `${versionManifest.id}.jar`))
return libs.join(path.delimiter);
}
export function generateLaunchArguments(
versionManifest: Version,
javaRoot: string,
gameRoot: string,
auth: Auth,
options = {
minRam: "2G",
maxRam: "2G",
customJvm: "",
},
) {
// Log4j /!\
// Replace templates /!\
const jvm: string[] = [];
const game: string[] = [];
const classPaths = generateClassPaths(
versionManifest,
path.join(gameRoot, "libraries"),
);
function p(args: string[], arg: Argument) {
parseArg.apply(args, [arg, versionManifest, gameRoot, classPaths, auth]);
}
for (const arg of versionManifest.arguments.jvm) {
p(jvm, arg);
}
for (const arg of versionManifest.arguments.game) {
p(game, arg);
}
return `${getJavaExecutable(javaRoot)} ${jvm.join(" ")}${options.customJvm === "" ? "" : ` ${options.customJvm}`} -Xms${options.minRam} -Xmx${options.maxRam} -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC -XX:G1NewSizePercent=20 -XX:G1ReservePercent=20 -XX:MaxGCPauseMillis=50 -XX:G1HeapRegionSize=32M ${versionManifest.mainClass} ${game.join(" ")}`;
}

View file

@ -3,3 +3,4 @@ export * as version from "./version";
export * as natives from "./natives";
export * as libraries from "./libraries";
export * as assets from "./assets";
export * as arguments from "./arguments"

View file

@ -8,6 +8,7 @@ import {
RuntimeComponent,
RuntimeOS,
} from "../utils/types";
import { os } from "../utils/systemInfo";
export async function download(
os: RuntimeOS,
@ -48,3 +49,8 @@ export async function download(
await fs.chmod(path.join(destination, "bin/java"), 0o777);
}
}
export function getJavaExecutable(javaRoot: string, show=false) {
const executable = `${os() === "windows" && show ? "javaw" : "java"}${os() === "windows" ? ".exe" : ""}`
return path.join(javaRoot, "bin", executable)
}

View file

@ -1,20 +1,32 @@
import path from "path";
import { isNeeded } from "../utils/rules";
import { Library, PoolFile, Version } from "../utils/types";
import { PoolFile, Version } from "../utils/types";
import { DownloadPool } from "../utils/fetch";
export async function download(destination: string, versionManifest: Version) {
// Download libraries
const files = versionManifest.libraries.map<PoolFile>((library: Library) => {
if (isNeeded(library)) {
return {
url: library.downloads.artifact.url,
path: path.join(destination, library.downloads.artifact.path),
};
}
});
export function getLibraries(versionManifest: Version, librariesRoot: string) {
const libs: PoolFile[] = [];
const dPool = new DownloadPool(files, 5);
for (const key in versionManifest.libraries) {
if (Object.prototype.hasOwnProperty.call(versionManifest.libraries, key)) {
const library = versionManifest.libraries[key];
if (isNeeded(library)) {
libs.push({
url: library.downloads.artifact.url,
path: path.join(librariesRoot, library.downloads.artifact.path),
});
}
}
}
return libs
}
export async function download(librariesRoot: string, versionManifest: Version) {
const libs = getLibraries(versionManifest, librariesRoot);
// Download libraries
const dPool = new DownloadPool(libs, 5);
await dPool.run();
return libs;
}

View file

@ -13,21 +13,26 @@ export async function download(destination: string, versionManifest: Version) {
const tempNativesPath = await getTempFolder("natives");
// Download archives to a temp folder
const files = versionManifest.libraries.map<PoolFile>((library: Library) => {
if (library.downloads && library.downloads.classifiers) {
if (!isNeeded(library)) return null;
const files: PoolFile[] = [];
const native: Native =
os() === "osx"
? library.downloads.classifiers["natives-osx"] ||
library.downloads.classifiers["natives-macos"]
: library.downloads.classifiers[`natives-${os()}`];
for (const key in versionManifest.libraries) {
if (Object.prototype.hasOwnProperty.call(versionManifest.libraries, key)) {
const library = versionManifest.libraries[key];
if (library.downloads && library.downloads.classifiers) {
if (!isNeeded(library)) continue;
const name = native.path.split("/").pop();
const native: Native =
os() === "osx"
? library.downloads.classifiers["natives-osx"] ||
library.downloads.classifiers["natives-macos"]
: library.downloads.classifiers[`natives-${os()}`];
return { url: native.url, path: path.join(tempNativesPath, name) };
const name = native.path.split("/").pop();
files.push({ url: native.url, path: path.join(tempNativesPath, name) });
}
}
});
}
const dPool = new DownloadPool(files, 5);

View file

@ -1,5 +1,7 @@
import { fetchJson } from "../utils/fetch";
import { downloadFile, fetchJson } from "../utils/fetch";
import { exists } from "../utils/fs";
import { Versions, Version } from "../utils/types";
import path from "path";
export async function getVersionManifest(versionString: string) {
const versions = await (
@ -22,3 +24,16 @@ export async function getVersionManifest(versionString: string) {
return versionManifest;
}
export async function downloadJar(versionManifest: Version, gameRoot: string) {
const destination = path.join(
gameRoot,
"versions",
`${versionManifest.id}.jar`,
);
if (!(await exists(destination)))
await downloadFile({
url: versionManifest.downloads.client.url,
path: destination,
});
}

View file

@ -11,7 +11,6 @@ export async function fetchJson<T>(url: string): Promise<T> {
export async function downloadFile(file: { url: string; path: string }) {
try {
if (!file) return;
await ensureDir(path.dirname(file.path), true);
const res = await fetch(file.url);

View file

@ -1,5 +1,5 @@
import { arch, os } from "./systemInfo";
import { Library } from "./types";
import { os } from "./systemInfo";
import { Argument, Library } from "./types";
// Copied from https://github.com/Pierce01/MinecraftLauncher-core/blob/f4ce947658e82218011d92c36d4d8a1b8c0c2429/components/handler.js#L237
// export function parseRule(library: Version["libraries"][number]): boolean {
@ -22,16 +22,18 @@ import { Library } from "./types";
// }
// }
export function isNeeded(library: Library): boolean {
if (library.rules) {
for (const rule of library.rules) {
export function isNeeded(element: Library | Argument): boolean {
if (typeof element !== "string" && element.rules) {
for (const rule of element.rules) {
if (rule.action === "allow") {
if (rule.features) return false; // quickplay, demo, resolution...
if (!rule.os) continue; // no os, check next rule
if (rule.os.name === os() || rule.os.arch === arch()) {
if (rule.os.arch) return true;
if (rule.os.name === os()) {
return true;
} else return false;
} else {
if (rule.os.name === os() || rule.os.arch === arch()) {
if (rule.os.name === os()) {
return false;
} else return true;
}

View file

@ -7,16 +7,4 @@ export function os() {
default:
return "linux";
}
}
export function arch() {
switch(process.arch) {
case "arm":
case "ia32":
return "x86"
case "arm64":
case "x64":
default:
return "x64"
}
}

View file

@ -83,20 +83,18 @@ export type Native = {
type NativeOS = "natives-linux" | "natives-windows" | "natives-macos";
type JVMRule = {
type Rules = {
action: "allow" | "disallow";
os: { name?: string; version?: string; arch?: string };
os?: { name?: string; version?: string; arch?: string };
features?: { [key: string]: boolean };
};
type Argument = {
rules: JVMRule[];
value: string[] | string;
};
type LibraryRule = {
action: "allow" | "disallow";
os?: { name?: string; arch?: string };
};
export type Argument =
| {
rules: Rules[];
value: string[] | string;
}
| string;
export type Library = {
downloads: {
@ -111,14 +109,14 @@ export type Library = {
};
};
name: string;
rules?: LibraryRule[];
rules?: Rules[];
natives?: { [key: string]: string };
};
export type Version = {
arguments: {
game: (Argument | string)[];
jvm: (Argument | string)[];
game: Argument[];
jvm: Argument[];
};
assetIndex: {
id: string;
@ -148,6 +146,11 @@ export type Version = {
type: string;
};
};
mainClass: string;
minimumLauncherVersion: number;
releaseTime: string;
time: string;
type: string;
};
export type AssetIndex = {
@ -158,3 +161,17 @@ export type AssetIndex = {
};
};
};
export type Auth = {
access_token: string;
client_token: string;
uuid: string;
name: string;
user_properties: string;
meta?: {
type: "mojang" | "msa";
demo: boolean;
xuid: string;
clientId: string;
};
};

54
tests/arguments.test.ts Normal file
View file

@ -0,0 +1,54 @@
import { expect, test, vi } from "vitest";
import * as nlk from "../dist/index.js";
import path from "path";
import fs from "fs/promises";
import { v3 } from "uuid";
function getUUID(value) {
return v3(value, v3.DNS);
}
// const javaPath = path.join(import.meta.dirname, "temp/java");
// await fs.rm(javaPath, { recursive: true });
// await fs.mkdir(javaPath, { recursive: true });
function offline() {
const uuid = getUUID("Heroz_0");
return {
access_token: uuid,
client_token: uuid,
uuid,
name: "Heroz_0",
user_properties: "{}",
};
}
async function exists(path: string) {
try {
await fs.access(path, fs.constants.F_OK);
return true;
} catch {
return false;
}
}
// mock os to linux for testing
vi.stubGlobal("process", { platform: "linux" });
test("parse arguments correctly for 1.21.8", { timeout: 10000 }, async () => {
const args = nlk.core.arguments.generateLaunchArguments(
await nlk.core.version.getVersionManifest("1.21.8"),
path.join(import.meta.dirname, "temp/java"),
path.join(import.meta.dirname, "temp"),
offline(),
);
console.log(args);
// expect(
// await fs.readFile(path.join(javaPath, "release"), { encoding: "utf-8" }),
// "check release file",
// ).toBe(`JAVA_VERSION="17.0.15"
// MODULES="java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.compiler jdk.crypto.ec jdk.crypto.cryptoki jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.foreign jdk.incubator.vector jdk.internal.le jdk.internal.opt jdk.internal.vm.ci jdk.internal.vm.compiler jdk.internal.vm.compiler.management jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jsobject jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.random jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom jdk.zipfs"
// `);
});

View file

@ -20,13 +20,13 @@ async function exists(path: string) {
vi.stubGlobal("process", { platform: "linux" });
test("install java-runtime-beta for linux correctly", {timeout: 10000}, async () => {
await nlk.core.java.download("linux", "java-runtime-beta", javaPath);
await nlk.core.java.download("linux", "java-runtime-delta", javaPath);
expect(
await fs.readFile(path.join(javaPath, "release"), { encoding: "utf-8" }),
"check release file"
).toBe(`JAVA_VERSION="17.0.15"
MODULES="java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.compiler jdk.crypto.ec jdk.crypto.cryptoki jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.foreign jdk.incubator.vector jdk.internal.le jdk.internal.opt jdk.internal.vm.ci jdk.internal.vm.compiler jdk.internal.vm.compiler.management jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jsobject jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.random jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom jdk.zipfs"
).toBe(`JAVA_VERSION="21.0.7"
MODULES="java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.ec jdk.crypto.cryptoki jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.vector jdk.internal.le jdk.internal.vm.ci jdk.internal.vm.compiler jdk.internal.vm.compiler.management jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jsobject jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.random jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom"
`);
expect(await exists(path.join(javaPath, "bin/java")), "bin/java exists").toBe(true);

View file

@ -20,8 +20,8 @@ async function exists(path: string) {
vi.stubGlobal("process", { platform: "linux" });
// old version because newer versions doesn't have natives
test("download natives for 1.15 correctly", { timeout: 10000 }, async () => {
const versionManifest = await nlk.core.version.getVersionManifest("1.15");
test("download natives for 1.21.8 correctly", { timeout: 10000 }, async () => {
const versionManifest = await nlk.core.version.getVersionManifest("1.21.8");
await nlk.core.natives.download(nativesPath, versionManifest);
expect(

33
tests/versionJar.test.ts Normal file
View file

@ -0,0 +1,33 @@
import { expect, test, vi } from "vitest";
import * as nlk from "../dist/index.js";
import path from "path";
import fs from "fs/promises";
const versionsPath = path.join(import.meta.dirname, "temp/versions");
await fs.rm(versionsPath, { recursive: true });
await fs.mkdir(versionsPath, { recursive: true });
async function exists(path: string) {
try {
await fs.access(path, fs.constants.F_OK);
return true;
} catch {
return false;
}
}
// mock os to linux for testing
vi.stubGlobal("process", { platform: "linux" });
test("parse arguments correctly for 1.21.8", { timeout: 10000 }, async () => {
const versionManifest = await nlk.core.version.getVersionManifest("1.21.8");
await nlk.core.version.downloadJar(
versionManifest,
path.join(versionsPath, ".."),
);
expect(
await exists(path.join(versionsPath, `${versionManifest.id}.jar`)),
"check jar file",
).toBe(true);
});