feat: rewrite instance class with a config system similar to vite, with better errors and cleaner code

This commit is contained in:
HerozDotExe 2026-03-19 18:06:58 +01:00
parent ad91a39e09
commit 66967c955c
15 changed files with 707 additions and 502 deletions

View file

@ -1,41 +1,39 @@
import path from "path";
import { isNeeded } from "../utils/rules";
import { Argument, Auth, Version } from "../utils/types";
import { Argument, Version } from "../utils/types";
import { getLibraries } from "./libraries";
import { version as packageVersion } from "../../package.json";
import { getArgument } from "./log4j";
import { Config } from "../launcher/instance";
// Fill templates
function fillArguments(
arg: string,
versionManifest: Version,
assetsPath: string,
instancePath: string,
librariesPath: string,
classPaths: string,
auth: Auth,
config: Config,
classPaths: string
) {
const argumentsToFill: { [key: string]: string } = {
"${natives_directory}": path.join(instancePath, "natives"),
"${natives_directory}": path.join(config.paths.instance, "natives"),
"${launcher_name}": "nlk",
"${launcher_version}": packageVersion,
"${classpath}": classPaths,
"${library_directory}": librariesPath,
"${game_directory}": instancePath,
"${library_directory}": config.paths.libraries,
"${game_directory}": config.paths.instance,
"${version_name}": versionManifest.id,
"${version_type}": versionManifest.type,
"${assets_index_name}": versionManifest.assets,
"${assets_root}": assetsPath,
"${game_assets}": assetsPath,
"${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",
"${assets_root}": config.paths.assets,
"${game_assets}": config.paths.assets,
"${auth_access_token}": config.auth.access_token,
"${auth_session}": config.auth.access_token,
"${auth_player_name}": config.auth.name,
"${auth_uuid}": config.auth.uuid,
"${auth_xuid}": config.auth.meta?.xuid || config.auth.access_token,
"${user_properties}": config.auth.user_properties,
"${user_type}": config.auth.meta?.type || "msa",
"${clientid}":
auth.meta?.clientId || auth.client_token || auth.access_token,
config.auth.meta?.clientId || config.auth.client_token || config.auth.access_token,
"${classpath_separator}": path.delimiter,
};
@ -54,11 +52,8 @@ function parseArg(
this: string[],
arg: Argument,
versionManifest: Version,
assetsPath: string,
instancePath: string,
librariesPath: string,
classPaths: string,
auth: Auth,
config: Config,
classPaths: string
) {
if (isNeeded(arg)) {
if (typeof arg === "string") {
@ -66,11 +61,8 @@ function parseArg(
fillArguments(
arg,
versionManifest,
assetsPath,
instancePath,
librariesPath,
classPaths,
auth,
config,
classPaths
),
);
} else if (typeof arg.value === "string") {
@ -78,24 +70,18 @@ function parseArg(
fillArguments(
arg.value,
versionManifest,
assetsPath,
instancePath,
librariesPath,
classPaths,
auth,
),
);
config,
classPaths
)
)
} else if (arg.value.length > 1) {
for (const e of arg.value) {
this.push(
fillArguments(
e,
versionManifest,
assetsPath,
instancePath,
librariesPath,
classPaths,
auth,
config,
classPaths
),
);
}
@ -121,23 +107,16 @@ function generateClassPaths(
export async function generateLaunchArguments(
versionManifest: Version,
javaExecutable: string,
instancePath: string,
librariesPath: string,
assetsPath: string,
versionRoot: string,
auth: Auth,
customArgs: { java: string; game: string },
ram: { max: string; min: string },
config: Config
) {
const jvm: string[] = [];
let game: string[] = [];
const versionJar = path.join(versionRoot, `${versionManifest.id}.jar`);
const versionJar = path.join(config.paths.versions, versionManifest.id, `${versionManifest.id}.jar`);
const classPaths = generateClassPaths(
versionManifest,
librariesPath,
config.paths.libraries,
versionJar,
);
@ -145,11 +124,8 @@ export async function generateLaunchArguments(
parseArg.apply(args, [
arg,
versionManifest,
assetsPath,
instancePath,
librariesPath,
config,
classPaths,
auth,
]);
}
@ -161,45 +137,41 @@ export async function generateLaunchArguments(
p(game, arg);
}
} else {
console.log(versionManifest.minecraftArguments)
game = fillArguments(
versionManifest.minecraftArguments!,
versionManifest,
assetsPath,
instancePath,
librariesPath,
classPaths,
auth,
config,
classPaths
).split(" ");
jvm.push(`-Djava.library.path=${path.join(instancePath, "natives")}`);
jvm.push(`-Djava.library.path=${path.join(config.paths.instance, "natives")}`);
jvm.push(`-Dminecraft.launcher.brand=nlk`);
jvm.push(`-Dminecraft.launcher.version=${packageVersion}`);
jvm.push(
`-Dminecraft.client.jar=${path.join(versionRoot, `${versionManifest.id}.jar`)}`,
`-Dminecraft.client.jar=${path.join(config.paths.versions, versionManifest.id, `${versionManifest.id}.jar`)}`,
);
jvm.push("-cp");
jvm.push(classPaths)
}
if (customArgs.java !== "") {
for (const arg of customArgs.java.split(" ")) {
if (config.args.java !== "") {
for (const arg of config.args.java.split(" ")) {
jvm.push(arg);
}
}
if (customArgs.game !== "") {
for (const arg of customArgs.game.split(" ")) {
if (config.args.game !== "") {
for (const arg of config.args.game.split(" ")) {
game.push(arg);
}
}
const log4j = await getArgument(versionManifest, versionRoot);
const log4j = await getArgument(versionManifest, config.paths.versions);
let launchArguments = [
...jvm,
`-Xms${ram.min}`,
`-Xmx${ram.max}`,
`-Xms${config.ram.min}`,
`-Xmx${config.ram.max}`,
"-XX:+UnlockExperimentalVMOptions",
"-XX:+UseG1GC",
"-XX:G1NewSizePercent=20",
@ -215,7 +187,7 @@ export async function generateLaunchArguments(
launchArguments = [...launchArguments, versionManifest.mainClass, ...game,]
return {
command: javaExecutable,
command: config.javaExecutable,
args: launchArguments,
};
}

View file

@ -2,6 +2,6 @@ export { AssetsDownloader } from "./assets";
export { LibrariesDownloader } from "./libraries";
export { NativesDownloader } from "./natives";
export * as version from "./version";
export * as arguments from "./arguments";
export * as argumentsGenerator from "./arguments";
export * as log4j from "./log4j";
export { launch } from "./launch";

View file

@ -8,57 +8,57 @@ import { version as packageVersion } from "../../package.json";
import { ensureDir, exists, readJson } from "../utils/fs";
import { InstallError } from "../utils/errors";
import AdmZip from "adm-zip";
import { Config } from "../launcher/instance";
const versionWithDoubleName = ["1.9.4", "1.9.0", "1.8.9", "1.8.8", "1.8", "1.7.10"]
export function fixVersionWithDoubleName(version: string, modloader: Modloader) {
let modLoaderVersion = modloader.version
if (versionWithDoubleName.includes(version)) {
// these versions have a different file name on forge's maven
modloader.version = modloader.version + `-${version}`
modLoaderVersion = modloader.version + `-${version}`
}
return modloader
return modLoaderVersion
}
async function downloadJar(
minecraftVersion: string,
modloader: Modloader,
config: ModloaderConfig,
type: "universal" | "installer",
destination: string,
) {
let filePath = "";
try {
if (modloader.name === "forge") {
if (config.modloader.name === "forge") {
if (type === "universal") {
filePath = path.join(
destination,
`forge-${minecraftVersion}-${modloader.version}-universal.jar`,
`forge-${config.version}-${config.modloader.version}-universal.jar`,
);
await downloadFile({
url: `https://maven.minecraftforge.net/net/minecraftforge/forge/${minecraftVersion}-${modloader.version}/forge-${minecraftVersion}-${modloader.version}-universal.jar`,
url: `https://maven.minecraftforge.net/net/minecraftforge/forge/${config.version}-${config.modloader.version}/forge-${config.version}-${config.modloader.version}-universal.jar`,
path: filePath,
});
} else {
filePath = path.join(
destination,
`forge-${minecraftVersion}-${modloader.version}-installer.jar`,
`forge-${config.version}-${config.modloader.version}-installer.jar`,
);
await downloadFile({
url: `https://maven.minecraftforge.net/net/minecraftforge/forge/${minecraftVersion}-${modloader.version}/forge-${minecraftVersion}-${modloader.version}-installer.jar`,
url: `https://maven.minecraftforge.net/net/minecraftforge/forge/${config.version}-${config.modloader.version}/forge-${config.version}-${config.modloader.version}-installer.jar`,
path: filePath,
});
}
} else if (modloader.name === "neoforge") {
} else if (config.modloader.name === "neoforge") {
filePath = path.join(
destination,
`neoforge-${modloader.version}-installer.jar`,
`neoforge-${config.modloader.version}-installer.jar`,
);
await downloadFile({
url: `https://maven.neoforged.net/releases/net/neoforged/neoforge/${modloader.version}/neoforge-${modloader.version}-installer.jar`,
url: `https://maven.neoforged.net/releases/net/neoforged/neoforge/${config.modloader.version}/neoforge-${config.modloader.version}-installer.jar`,
path: filePath,
});
}
} catch (original) {
const error = new InstallError("modloader", null, original, "Check that the version of the modloader exists.");
error.throw();
} catch (error) {
throw new InstallError("An error occured while downloadng natives", "natives", config, { cause: error })
}
return filePath;
@ -112,32 +112,30 @@ function isModernForge(version: string) {
} else return false
}
export interface ModloaderConfig extends Config {
modloader: Modloader
}
export async function installForge(
versionManifest: Version,
modloader: Modloader,
javaExecutable: string,
root: string,
instanceLocation: string,
librariesPath: string,
versionsPath: string
config: ModloaderConfig,
versionManifest: Version
) {
const forgeLibDir = path.join(root, "libraries", "net", "minecraftforge", "forge", `${versionManifest.id}-${modloader.version}`)
const neoForgeLibDir = path.join(root, "libraries", "net", "neoforged", "neoforge", `${modloader.version}`)
const forgeLibDir = path.join(config.paths.root, "libraries", "net", "minecraftforge", "forge", `${versionManifest.id}-${config.modloader.version}`)
const neoForgeLibDir = path.join(config.paths.root, "libraries", "net", "neoforged", "neoforge", `${config.modloader.version}`)
// older forge jar path
const universalDestination = path.join(forgeLibDir, `forge-${versionManifest.id}-${modloader.version}.jar`)
const universalDestination = path.join(forgeLibDir, `forge-${versionManifest.id}-${config.modloader.version}.jar`)
// newer forge jar path
const forgeClientDestination = path.join(forgeLibDir, `forge-${versionManifest.id}-${modloader.version}-client.jar`)
const forgeClientDestination = path.join(forgeLibDir, `forge-${versionManifest.id}-${config.modloader.version}-client.jar`)
// neoforge jar path
const neoForgeClientDestination = path.join(neoForgeLibDir, `neoforge-${modloader.version}-client.jar`)
const neoForgeClientDestination = path.join(neoForgeLibDir, `neoforge-${config.modloader.version}-client.jar`)
if (await exists(universalDestination) || await exists(forgeClientDestination) || await exists(neoForgeClientDestination)) return;
const tempFolder = await getTempFolder("forge");
// Download and extract installer file
const forgeInstallerPath = await downloadJar(
versionManifest.id,
modloader,
config,
"installer",
tempFolder,
);
@ -149,13 +147,13 @@ export async function installForge(
const modernInstaller = isModernForge(versionManifest.id)
await runForgeInstaller(javaExecutable, forgeInstallerPath, fakeLauncher, modernInstaller ? "Client" : "Server");
await runForgeInstaller(config.javaExecutable, forgeInstallerPath, fakeLauncher, modernInstaller ? "Client" : "Server");
// Copy libraries and versions files
for (const file of await fs.readdir(path.join(fakeLauncher, "libraries"))) {
await fs.cp(
path.join(fakeLauncher, "libraries", file),
path.join(librariesPath, file),
path.join(config.paths.libraries, file),
{
recursive: true,
},
@ -173,17 +171,15 @@ export async function installForge(
await fs.cp(
path.join(fakeLauncher, "versions", originalVersionId),
path.join(versionsPath, originalVersionId),
path.join(config.paths.versions, originalVersionId),
{ recursive: true },
);
} else {
// Older versions (from 1.5.2 to 1.12.2, even older versions are not supported)
//await runForgeInstaller(javaExecutable, forgeInstallerPath, fakeLauncher, "Server");
const universalPath = path.join(fakeLauncher, `forge-${versionManifest.id}-${modloader.version}-universal.jar`)
const universalPath = path.join(fakeLauncher, `forge-${versionManifest.id}-${config.modloader.version}-universal.jar`)
// Copy forge jar
await ensureDir(path.join(root, "libraries", "net", "minecraftforge", "forge", `${versionManifest.id}-${modloader.version}`), true)
await ensureDir(path.join(config.paths.root, "libraries", "net", "minecraftforge", "forge", `${versionManifest.id}-${config.modloader.version}`), true)
await fs.cp(
universalPath,
universalDestination,
@ -192,7 +188,7 @@ export async function installForge(
// Copy version json
const zip = new AdmZip(universalPath)
zip.extractEntryTo("version.json", path.join(versionsPath, `${versionManifest.id}-forge-${modloader.version}`))
await fs.rename(path.join(versionsPath, `${versionManifest.id}-forge-${modloader.version}`, "version.json"), path.join(versionsPath, `${versionManifest.id}-forge-${modloader.version}`, `${versionManifest.id}-forge-${modloader.version}.json`))
zip.extractEntryTo("version.json", path.join(config.paths.versions, `${versionManifest.id}-forge-${config.modloader.version}`))
await fs.rename(path.join(config.paths.versions, `${versionManifest.id}-forge-${config.modloader.version}`, "version.json"), path.join(config.paths.versions, `${versionManifest.id}-forge-${config.modloader.version}`, `${versionManifest.id}-forge-${config.modloader.version}.json`))
}
}

View file

@ -1,3 +1,3 @@
export { offlineAuth } from "./auth";
export { Instance } from "./instance";
export { Instance, defineConfig } from "./instance";
export { RuntimeManager, getJavaComponent } from "./java";

View file

@ -0,0 +1,274 @@
import {
Auth,
InstanceEvents,
Modloader,
Paths,
SupportedModloaders,
Version,
} from "../utils/types";
import * as core from "../core";
import path from "path";
import { EventEmitter } from "node:events";
import { throwLaunch, throwInstall } from "../utils/errors";
import { fixVersionWithDoubleName, installForge } from "../core/modloaders";
import { readJson } from "../utils/fs";
import { mergeManifests } from "../core/mergeManifests";
import { checkJava } from "./java";
import { ChildProcessWithoutNullStreams } from "node:child_process";
export class Instance extends EventEmitter<InstanceEvents> {
version?: string;
modloader?: Modloader;
auth?: Auth;
paths?: Paths;
args: { java: string; game: string };
ram: { max: string; min: string };
versionManifest?: Version;
versionLocation?: string;
instanceLocation?: string;
ready: boolean;
javaExecutable?: string;
forgeVersionPatched: boolean;
constructor() {
super();
this.args = { java: "", game: "" };
this.ram = { max: "2G", min: "2G" };
this.ready = false;
this.forgeVersionPatched = false;
}
setVersion(version: string) {
this.version = version;
}
setModLoader(name: SupportedModloaders, version: string) {
this.modloader = { name, version };
}
setJavaExecutable(path: string) {
this.javaExecutable = path;
}
setPaths(paths: Paths | string): void {
if (typeof paths === "string") {
this.paths = {
root: paths,
versions: path.join(paths, "versions"),
assets: path.join(paths, "assets"),
libraries: path.join(paths, "libraries"),
instances: path.join(paths, "instances"),
};
} else {
this.paths = {
root: paths.root,
versions: paths.versions || path.join(paths.root, "versions"),
assets: paths.assets || path.join(paths.root, "assets"),
libraries: paths.libraries || path.join(paths.root, "libraries"),
instances: paths.instances || path.join(paths.root, "instances"),
};
}
}
setAuth(auth: Auth) {
this.auth = auth;
}
setArgs({ java, game }: { java?: string; game?: string }) {
this.args.java = java || "";
this.args.game = game || "";
}
setRAM({ min, max }: { min?: string; max?: string }) {
this.ram.min = min || "2G";
this.ram.max = max || "2G";
}
private async initialize() {
if (this.modloader && !this.forgeVersionPatched) {
this.modloader = fixVersionWithDoubleName(this.version!, this.modloader)
this.forgeVersionPatched = true
}
if (!this.javaExecutable || !this.paths?.root || !this.version || !this.auth) {
throw new Error("Missing options")
}
this.versionLocation = path.join(this.paths.versions!, this.version);
this.instanceLocation = path.join(this.paths.instances!, this.version);
this.versionManifest = await core.version.getVersionManifest(
this.version,
this.versionLocation,
);
}
async install() {
try {
await this.initialize();
this.ready = true;
await core.version.downloadJar(
this.versionManifest!,
this.versionLocation!,
);
} catch (original) {
throwInstall("installInit", original, this)
}
try {
const librariesDownloader = await core.LibrariesDownloader(
this.paths!.libraries!,
this.versionManifest!,
);
librariesDownloader.on("completed", () => {
this.emit(
"progress",
"libraries",
librariesDownloader.done,
librariesDownloader.total,
librariesDownloader.doneSize,
librariesDownloader.totalSize,
);
});
await librariesDownloader.run();
} catch (original) {
throwInstall("libraries", original, this)
}
try {
const assetsDownloader = await core.AssetsDownloader(
this.paths!.assets!,
this.versionManifest!,
);
assetsDownloader.on("completed", () => {
this.emit(
"progress",
"assets",
assetsDownloader.done,
assetsDownloader.total,
assetsDownloader.doneSize,
assetsDownloader.totalSize,
);
});
await assetsDownloader.run();
} catch (original) {
throwInstall("assets", original, this)
}
try {
const nativesDownloader = await core.NativesDownloader(
path.join(this.instanceLocation!, "natives"),
this.versionManifest!,
);
nativesDownloader.on("completed", () => {
this.emit(
"progress",
"natives",
nativesDownloader.done,
nativesDownloader.total,
nativesDownloader.doneSize,
nativesDownloader.totalSize,
);
});
await nativesDownloader.run();
} catch (original) {
throwInstall("natives", original, this)
}
const javaError = await checkJava(this.javaExecutable!)
if (javaError) {
throwInstall("java", javaError, this)
}
if (this.modloader) {
try {
switch (this.modloader.name) {
case "forge":
case "neoforge":
await installForge(
this.versionManifest!,
this.modloader,
this.javaExecutable!,
this.paths!.root,
this.instanceLocation!,
this.paths!.libraries!,
this.paths!.versions!
);
break;
default:
throw new Error("Unknown modloader");
}
} catch (original) {
throwInstall("modloader", original, this)
}
}
}
async launch(): Promise<ChildProcessWithoutNullStreams> {
try {
if (!this.ready) {
try {
await this.initialize();
} catch (original) {
throw throwLaunch(original, this)
}
}
if (this.modloader) {
switch (this.modloader.name) {
case "forge": {
const forgeVersionManifest = await readJson<Version>(
path.join(
this.paths!.versions!,
`${this.version}-${this.modloader.name}-${this.modloader.version}`,
`${this.version}-${this.modloader.name}-${this.modloader.version}.json`,
),
);
this.versionManifest = mergeManifests(
this.versionManifest!,
forgeVersionManifest,
);
break;
}
case "neoforge":
{
const neoForgeVersionManifest = await readJson<Version>(
path.join(
this.paths!.versions!,
`${this.modloader.name}-${this.modloader.version}`,
`${this.modloader.name}-${this.modloader.version}.json`,
),
);
this.versionManifest = mergeManifests(
this.versionManifest!,
neoForgeVersionManifest,
);
}
break;
default:
throw new Error("Unknown modloader");
}
}
const args = await core.arguments.generateLaunchArguments(
this.versionManifest!,
this.javaExecutable!,
this.instanceLocation!,
this.paths!.libraries!,
this.paths!.assets!,
this.versionLocation!,
this.auth!,
this.args,
this.ram
);
const process = core.launch(args, this.instanceLocation!);
return process;
} catch (original) {
throw throwLaunch(original, this)
}
}
}

View file

@ -1,274 +1,249 @@
import {
Auth,
InstanceEvents,
Modloader,
Paths,
SupportedModloaders,
Version,
} from "../utils/types";
import * as core from "../core";
import path from "path";
import { EventEmitter } from "node:events";
import { throwLaunch, throwInstall } from "../utils/errors";
import { fixVersionWithDoubleName, installForge } from "../core/modloaders";
import { EventEmitter } from "node:stream";
import { Auth, InstanceEvents, Modloader, Paths, ProcessArgs, ProcessRam, Version } from "../utils/types";
import path from "node:path";
import { argumentsGenerator, AssetsDownloader, launch, LibrariesDownloader, NativesDownloader, version } from "../core";
import { ConfigError, InstallError, LaunchError } from "../utils/errors";
import { checkJava } from "./java";
import { fixVersionWithDoubleName, installForge, ModloaderConfig } from "../core/modloaders";
import { ChildProcessWithoutNullStreams } from "node:child_process";
import { readJson } from "../utils/fs";
import { mergeManifests } from "../core/mergeManifests";
import { checkJava } from "./java";
import { ChildProcessWithoutNullStreams } from "node:child_process";
export interface BaseConfig {
version: string;
auth: Auth;
paths: Paths
javaExecutable: string
modloader?: Modloader;
args?: ProcessArgs;
ram?: { max?: string; min?: string }
}
export interface Config extends BaseConfig {
paths: Required<Paths>
args: Required<ProcessArgs>
ram: Required<ProcessRam>
}
export function defineConfig(...layers: Partial<BaseConfig>[]) {
let config: Partial<BaseConfig> = {} as Partial<BaseConfig>;
for (const layer of layers) {
config = { ...config, ...layer }
}
if (!config.auth || !config.paths?.root || !config.paths?.instance || !config.version || !config.javaExecutable) {
throw new ConfigError("Invalid config provided", config)
}
config.paths = {
root: config.paths.root,
instance: config.paths.instance,
versions: config.paths.versions ?? path.join(config.paths.root, "versions"),
assets: config.paths.assets ?? path.join(config.paths.root, "assets"),
libraries: config.paths.libraries ?? path.join(config.paths.root, "libraries"),
};
config.args = { java: config.args?.java ?? "", game: config.args?.game ?? "" };
config.ram = { max: config.ram?.max ?? "2G", min: config.ram?.min ?? "2G" };
return config as Config
}
export class Instance extends EventEmitter<InstanceEvents> {
version?: string;
modloader?: Modloader;
auth?: Auth;
paths?: Paths;
args: { java: string; game: string };
ram: { max: string; min: string };
versionManifest?: Version;
versionLocation?: string;
instanceLocation?: string;
ready: boolean;
javaExecutable?: string;
forgeVersionPatched: boolean;
ready: boolean
config: Config
versionLocation: string
versionManifest: Version
constructor() {
super();
this.args = { java: "", game: "" };
this.ram = { max: "2G", min: "2G" };
this.ready = false;
this.forgeVersionPatched = false;
}
setVersion(version: string) {
this.version = version;
}
setModLoader(name: SupportedModloaders, version: string) {
this.modloader = { name, version };
}
setJavaExecutable(path: string) {
this.javaExecutable = path;
}
setPaths(paths: Paths | string): void {
if (typeof paths === "string") {
this.paths = {
root: paths,
versions: path.join(paths, "versions"),
assets: path.join(paths, "assets"),
libraries: path.join(paths, "libraries"),
instances: path.join(paths, "instances"),
};
} else {
this.paths = {
root: paths.root,
versions: paths.versions || path.join(paths.root, "versions"),
assets: paths.assets || path.join(paths.root, "assets"),
libraries: paths.libraries || path.join(paths.root, "libraries"),
instances: paths.instances || path.join(paths.root, "instances"),
};
}
}
setAuth(auth: Auth) {
this.auth = auth;
}
setArgs({ java, game }: { java?: string; game?: string }) {
this.args.java = java || "";
this.args.game = game || "";
}
setRAM({ min, max }: { min?: string; max?: string }) {
this.ram.min = min || "2G";
this.ram.max = max || "2G";
}
private async initialize() {
if (this.modloader && !this.forgeVersionPatched) {
this.modloader = fixVersionWithDoubleName(this.version!, this.modloader)
this.forgeVersionPatched = true
}
if (!this.javaExecutable || !this.paths?.root || !this.version || !this.auth) {
throw new Error("Missing options")
constructor(...layers: Partial<BaseConfig>[]) {
super()
this.config = defineConfig(...layers)
this.ready = false
this.versionManifest = {} as Version
this.versionLocation = ""
}
this.versionLocation = path.join(this.paths.versions!, this.version);
this.instanceLocation = path.join(this.paths.instances!, this.version);
private async init() {
this.versionLocation = path.join(this.config.paths.versions, this.config.version);
this.versionManifest = await core.version.getVersionManifest(
this.version,
this.versionLocation,
);
}
async install() {
try {
await this.initialize();
this.ready = true;
await core.version.downloadJar(
this.versionManifest!,
this.versionLocation!,
);
} catch (original) {
throwInstall("installInit", original, this)
}
try {
const librariesDownloader = await core.LibrariesDownloader(
this.paths!.libraries!,
this.versionManifest!,
);
librariesDownloader.on("completed", () => {
this.emit(
"progress",
"libraries",
librariesDownloader.done,
librariesDownloader.total,
librariesDownloader.doneSize,
librariesDownloader.totalSize,
);
});
await librariesDownloader.run();
} catch (original) {
throwInstall("libraries", original, this)
}
try {
const assetsDownloader = await core.AssetsDownloader(
this.paths!.assets!,
this.versionManifest!,
);
assetsDownloader.on("completed", () => {
this.emit(
"progress",
"assets",
assetsDownloader.done,
assetsDownloader.total,
assetsDownloader.doneSize,
assetsDownloader.totalSize,
);
});
await assetsDownloader.run();
} catch (original) {
throwInstall("assets", original, this)
}
try {
const nativesDownloader = await core.NativesDownloader(
path.join(this.instanceLocation!, "natives"),
this.versionManifest!,
);
nativesDownloader.on("completed", () => {
this.emit(
"progress",
"natives",
nativesDownloader.done,
nativesDownloader.total,
nativesDownloader.doneSize,
nativesDownloader.totalSize,
);
});
await nativesDownloader.run();
} catch (original) {
throwInstall("natives", original, this)
}
const javaError = await checkJava(this.javaExecutable!)
if (javaError) {
throwInstall("java", javaError, this)
}
if (this.modloader) {
try {
switch (this.modloader.name) {
case "forge":
case "neoforge":
await installForge(
this.versionManifest!,
this.modloader,
this.javaExecutable!,
this.paths!.root,
this.instanceLocation!,
this.paths!.libraries!,
this.paths!.versions!
);
break;
default:
throw new Error("Unknown modloader");
if (this.config.modloader) {
this.config.modloader.version = fixVersionWithDoubleName(this.config.version, this.config.modloader)
}
} catch (original) {
throwInstall("modloader", original, this)
}
}
}
async launch(): Promise<ChildProcessWithoutNullStreams> {
try {
if (!this.ready) {
this.versionManifest = await version.getVersionManifest(
this.config.version,
this.versionLocation,
);
this.ready = true
}
async install() {
try {
await this.initialize();
} catch (original) {
throw throwLaunch(original, this)
if (!this.ready) await this.init()
await version.downloadJar(
this.versionManifest,
this.versionLocation,
);
} catch (error) {
throw new InstallError("An error occured while initializing instance", "install-init", this.config, { cause: error })
}
}
if (this.modloader) {
switch (this.modloader.name) {
case "forge": {
const forgeVersionManifest = await readJson<Version>(
path.join(
this.paths!.versions!,
`${this.version}-${this.modloader.name}-${this.modloader.version}`,
`${this.version}-${this.modloader.name}-${this.modloader.version}.json`,
),
try {
const librariesDownloader = await LibrariesDownloader(
this.config.paths.libraries,
this.versionManifest,
);
this.versionManifest = mergeManifests(
this.versionManifest!,
forgeVersionManifest,
);
break;
}
case "neoforge":
{
const neoForgeVersionManifest = await readJson<Version>(
path.join(
this.paths!.versions!,
`${this.modloader.name}-${this.modloader.version}`,
`${this.modloader.name}-${this.modloader.version}.json`,
),
);
librariesDownloader.on("completed", () => {
this.emit(
"progress",
"libraries",
librariesDownloader.done,
librariesDownloader.total,
librariesDownloader.doneSize,
librariesDownloader.totalSize,
);
});
this.versionManifest = mergeManifests(
await librariesDownloader.run();
} catch (error) {
throw new InstallError("An error occured while downloading libraries", "libraries", this.config, { cause: error })
}
try {
const assetsDownloader = await AssetsDownloader(
this.config.paths.assets,
this.versionManifest,
);
assetsDownloader.on("completed", () => {
this.emit(
"progress",
"assets",
assetsDownloader.done,
assetsDownloader.total,
assetsDownloader.doneSize,
assetsDownloader.totalSize,
);
});
await assetsDownloader.run();
} catch (error) {
throw new InstallError("An error occured while downloading assets", "assets", this.config, { cause: error })
}
try {
const nativesDownloader = await NativesDownloader(
path.join(this.config.paths.instance, "natives"),
this.versionManifest!,
neoForgeVersionManifest,
);
}
break;
default:
throw new Error("Unknown modloader");
);
nativesDownloader.on("completed", () => {
this.emit(
"progress",
"natives",
nativesDownloader.done,
nativesDownloader.total,
nativesDownloader.doneSize,
nativesDownloader.totalSize,
);
});
await nativesDownloader.run();
} catch (error) {
throw new InstallError("An error occured while downloadng natives", "natives", this.config, { cause: error })
}
}
const args = await core.arguments.generateLaunchArguments(
this.versionManifest!,
this.javaExecutable!,
this.instanceLocation!,
this.paths!.libraries!,
this.paths!.assets!,
this.versionLocation!,
this.auth!,
this.args,
this.ram
);
const javaError = await checkJava(this.config.javaExecutable)
if (javaError) {
throw new InstallError("Invalid java provieded", "java", this.config, { cause: javaError })
}
const process = core.launch(args, this.instanceLocation!);
return process;
} catch (original) {
throw throwLaunch(original, this)
if (this.config.modloader) {
try {
switch (this.config.modloader.name) {
case "forge":
case "neoforge":
await installForge(
this.config as ModloaderConfig,
this.versionManifest
);
break;
default:
throw new Error("Unknown modloader");
}
} catch (error) {
throw new InstallError("An error occured while installing the modloader", "modloader", this.config, { cause: error })
}
}
}
}
}
async launch(): Promise<ChildProcessWithoutNullStreams> {
try {
if (!this.ready) await this.init()
} catch (error) {
throw new LaunchError("An error occured while initializing instance", "launch-init", this.config, { cause: error })
}
if (this.config.modloader) {
try {
switch (this.config.modloader.name) {
case "forge": {
const forgeVersionManifest = await readJson<Version>(
path.join(
this.config.paths.versions,
`${this.config.version}-${this.config.modloader.name}-${this.config.modloader.version}`,
`${this.config.version}-${this.config.modloader.name}-${this.config.modloader.version}.json`,
),
);
this.versionManifest = mergeManifests(
this.versionManifest!,
forgeVersionManifest,
);
break;
}
case "neoforge":
{
const neoForgeVersionManifest = await readJson<Version>(
path.join(
this.config.paths.versions,
`${this.config.modloader.name}-${this.config.modloader.version}`,
`${this.config.modloader.name}-${this.config.modloader.version}.json`,
),
);
this.versionManifest = mergeManifests(
this.versionManifest!,
neoForgeVersionManifest,
);
}
break;
default:
throw new Error("Unknown modloader");
}
} catch (error) {
throw new LaunchError("An error occured while preparing the modloader", "modloader", this.config, { cause: error })
}
}
let args;
try {
args = await argumentsGenerator.generateLaunchArguments(
this.versionManifest,
this.config
);
} catch (error) {
throw new LaunchError("An error occured while generating launch arguments", "arguments", this.config, { cause: error })
}
try {
const process = launch(args!, this.config.paths.instance);
return process;
} catch (error) {
throw new LaunchError("An error occured while launching minecraft", "launch-process", this.config, { cause: error })
}
}
}

View file

@ -13,7 +13,6 @@ import { ensureDir } from "../utils/fs";
import { EventEmitter } from "stream";
import { exec } from "child_process";
import { arch, os } from "../utils/systemInfo";
import { InstallError } from "../utils/errors";
import { getVersionManifest } from "../core/version";
import { getTempFolder } from "../utils/temp";
@ -114,12 +113,12 @@ async function JavaDownloader(
}
export async function getJavaComponent(mcVersion: string, versionsRoot?: string) {
if(!versionsRoot) {
if (!versionsRoot) {
versionsRoot = await getTempFolder("nlk-versions-cache")
}
const versionManifest = await getVersionManifest(mcVersion, versionsRoot);
return versionManifest.javaVersion.component
}
@ -156,8 +155,8 @@ export class RuntimeManager extends EventEmitter<InstanceEvents> {
);
});
await javaDownloader.run();
} catch (original) {
new InstallError("java", null, original, "An error happened while downloading java.")
} catch (error) {
throw new Error("An error occured while downloading java", { cause: error })
}
}

View file

@ -1,87 +1,19 @@
import { version as packageVersion } from "../../package.json";
import { Instance } from "../launcher";
import { LaunchErrorConfig } from "./types";
import { BaseConfig, Config } from "../launcher/instance"
function redactToken(config: LaunchErrorConfig) {
config.auth.access_token = "token"
config.auth.client_token = "token"
return config
export class ConfigError extends Error {
constructor(message: string, config: Partial<BaseConfig>, options?: ErrorOptions) {
super(message, options)
console.log("Provided config was:")
console.log(config)
}
}
export class InstallError extends Error {
config: LaunchErrorConfig | null;
step: string;
original: Error;
moreInfo?: string;
constructor(step: string, config: LaunchErrorConfig | null, original: unknown, moreInfo?: string) {
super();
this.step = step;
this.original = original as Error;
this.config = config;
this.moreInfo = moreInfo;
}
throw() {
console.error(
`[nlk ${packageVersion}] Error occured when installing ${this.step}: ${this.original.message}`,
);
console.error(`[nlk ${packageVersion}] Original error is:`, this.original);
if (this.moreInfo) {
console.error(this.moreInfo);
}
if (this.config) console.dir(redactToken(this.config));
throw this;
constructor(message: string, step: string, config: Config, options?: ErrorOptions) {
super(`[${step}] ${message}`, options)
console.log("Provided config was:")
console.log(config)
}
}
export class LaunchError extends Error {
config: LaunchErrorConfig;
original: Error;
constructor(config: LaunchErrorConfig, original: Error) {
super();
this.config = config;
this.original = original;
}
throw() {
console.error(
`[nlk ${packageVersion}] Error occured when launching the game: ${this.original.message}`,
);
console.error(`[nlk ${packageVersion}] Original error is:`, this.original);
console.dir(redactToken(this.config));
throw this;
}
}
export function throwInstall(step: string, original: unknown, config: Instance) {
const error = new InstallError(step,
{
version: config.version!,
auth: config.auth!,
paths: config.paths!,
customGameArgs: config.args.game!,
customJvmArgs: config.args.java!,
versionManifest: config.versionManifest!,
modloader: config.modloader!,
},
original);
error.throw();
}
export function throwLaunch(original: unknown, config: Instance) {
const error = new LaunchError({
version: config.version!,
auth: config.auth!,
paths: config.paths!,
customGameArgs: config.args.game!,
customJvmArgs: config.args.java!,
versionManifest: config.versionManifest!,
modloader: config.modloader!,
},
original as Error);
throw error.throw();
}
export class LaunchError extends InstallError {}

View file

@ -184,11 +184,10 @@ export type LaunchArguments = { command: string; args: string[] };
export type Paths = {
root: string;
instance: string;
versions?: string;
assets?: string;
libraries?: string;
instances?: string;
javaRoot?: string;
};
export type LaunchErrorConfig = {
@ -231,4 +230,14 @@ export interface InstanceEvents {
doneSize: number,
totalSize: number,
];
}
export type ProcessArgs = {
game?: string,
java?: string
}
export type ProcessRam = {
max?: string,
min?: string
}

56
tests/config.test.ts Normal file
View file

@ -0,0 +1,56 @@
import { test } from "vitest";
import { Instance } from "../dist/";
import path from "node:path";
const temp = path.join(import.meta.dirname, "temp")
test("instance", async () => {
const instance = new Instance({
version: "1.21.1",
auth: {
access_token: "token",
client_token: "token",
name: "player",
user_properties: "{}",
uuid: "uuid"
},
paths: { root: temp, instance: temp },
javaExecutable: "java"
},
{
modloader: {
name: "forge",
version: "52.1.12"
}
},
{
modloader: {
name: "neoforge",
version: "21.1.220"
}
})
await instance.install();
instance.on("progress", console.log);
// const p = await instance.launch();
// p.stdout.on("data", (d: Buffer) => {
// console.log(d.toString());
// });
// p.stderr.on("data", (d: Buffer) => {
// console.log(d.toString());
// });
// await new Promise<void>((res) => {
// p.on("error", (d: Buffer) => {
// console.log(d.toString());
// res();
// });
// p.on("close", () => {
// console.log("closed");
// res();
// });
// });
})

View file

@ -7,21 +7,21 @@ const gameRoot = path.join(import.meta.dirname, "..", "..", "temp");
// await fs.rm(gameRoot, { recursive: true, force: true });
// await fs.mkdir(gameRoot, { recursive: true });
test("launch game", { timeout: 0, tags:["full", "forge"] }, async () => {
const instance = new Instance();
test("launch game", { timeout: 0, tags: ["full", "forge"] }, async () => {
const javaManager = new RuntimeManager(path.join(gameRoot, "java"));
javaManager.on("progress", console.log)
// const java = await javaManager.use(await getJavaComponent("${version}"))
const auth = offlineAuth("player");
instance.setVersion("${version}");
instance.setPaths(gameRoot);
instance.setAuth(auth);
instance.setJavaExecutable("${java}")
instance.setModLoader("${modloader}", "${modloader_version}");
const instance = new Instance({
version: "${version}",
auth: offlineAuth("player"),
paths: { root: gameRoot, instance: path.join(gameRoot, "instances", "${version}") },
javaExecutable: "${java}",
modloader: {
name: "${modloader}",
version: "${modloader_version}"
}
});
instance.on("progress", console.log);

View file

@ -7,21 +7,21 @@ const gameRoot = path.join(import.meta.dirname, "..", "..", "temp");
// await fs.rm(gameRoot, { recursive: true, force: true });
// await fs.mkdir(gameRoot, { recursive: true });
test("launch game", { timeout: 0, tags:["full", "forge"] }, async () => {
const instance = new Instance();
test("launch game", { timeout: 0, tags: ["full", "vanilla"] }, async () => {
const javaManager = new RuntimeManager(path.join(gameRoot, "java"));
javaManager.on("progress", console.log)
const java = await javaManager.use(await getJavaComponent("${version}"))
const auth = offlineAuth("player");
instance.setVersion("${version}");
instance.setPaths(gameRoot);
instance.setAuth(auth);
instance.setJavaExecutable(java)
instance.setModLoader("${modloader}", "${modloader_version}");
const instance = new Instance({
version: "${version}",
auth: offlineAuth("player"),
paths: { root: gameRoot, instance: path.join(gameRoot, "instances", "${version}") },
javaExecutable: java,
modloader: {
name: "${modloader}",
version: "${modloader_version}"
}
});
instance.on("progress", console.log);

View file

@ -68,10 +68,10 @@ await fs.mkdir(path.join(tempFolder, "modloaders/neoforge"))
let result = ""
if (java) {
const template = await fs.readFile(path.join(tempFolder, "forge-java.test.ts.template"), { encoding: "utf-8" })
result = template.replaceAll("${version}", version).replace("${modloader}", "forge").replace("${modloader_version}", forgeVersion).replace("${java}", await getJava(version))
result = template.replaceAll("${version}", version).replaceAll("${modloader}", "forge").replaceAll("${modloader_version}", forgeVersion).replaceAll("${java}", await getJava(version))
} else {
const template = await fs.readFile(path.join(tempFolder, "forge.test.ts.template"), { encoding: "utf-8" })
result = template.replaceAll("${version}", version).replace("${modloader}", "forge").replace("${modloader_version}", forgeVersion)
result = template.replaceAll("${version}", version).replaceAll("${modloader}", "forge").replaceAll("${modloader_version}", forgeVersion)
}
await fs.writeFile(path.join(tempFolder, `modloaders/forge/forge-${version}-${forgeVersion}.test.ts`), result)
console.log(`Done forge ${forgeVersion}`)
@ -100,10 +100,10 @@ await fs.mkdir(path.join(tempFolder, "modloaders/neoforge"))
let result = ""
if (java) {
const template = await fs.readFile(path.join(tempFolder, "forge-java.test.ts.template"), { encoding: "utf-8" })
result = template.replaceAll("${version}", mcVersion).replace("${modloader}", "neoforge").replace("${modloader_version}", neoForgeVersion).replace("${java}", await getJava(mcVersion))
result = template.replaceAll("${version}", mcVersion).replaceAll("${modloader}", "neoforge").replaceAll("${modloader_version}", neoForgeVersion).replaceAll("${java}", await getJava(mcVersion))
} else {
const template = await fs.readFile(path.join(tempFolder, "forge.test.ts.template"), { encoding: "utf-8" })
result = template.replaceAll("${version}", mcVersion).replace("${modloader}", "neoforge").replace("${modloader_version}", neoForgeVersion)
result = template.replaceAll("${version}", mcVersion).replaceAll("${modloader}", "neoforge").replaceAll("${modloader_version}", neoForgeVersion)
}
await fs.writeFile(path.join(tempFolder, `modloaders/neoforge/neoforge-${mcVersion}-${neoForgeVersion}.test.ts`), result)
console.log(`Done neoforge ${neoForgeVersion}`)
@ -118,7 +118,7 @@ await fs.mkdir(path.join(tempFolder, "modloaders/neoforge"))
let result = ""
if (java) {
const template = await fs.readFile(path.join(tempFolder, "vanilla-java.test.ts.template"), { encoding: "utf-8" })
result = template.replaceAll("${version}", v.id).replace("${java}", await getJava(v.id))
result = template.replaceAll("${version}", v.id).replaceAll("${java}", await getJava(v.id))
} else {
const template = await fs.readFile(path.join(tempFolder, "vanilla.test.ts.template"), { encoding: "utf-8" })
result = template.replaceAll("${version}", v.id)

View file

@ -1,27 +1,23 @@
import { test } from "vitest";
import { Instance, offlineAuth, RuntimeManager } from "../../dist/index.js";
import { Instance, offlineAuth, RuntimeManager, getJavaComponent } from "../../dist/index.js";
import path from "path";
import fs from "fs/promises";
import { getJavaComponent } from "../../src/index.js";
const gameRoot = path.join(import.meta.dirname, "..", "temp");
// await fs.rm(gameRoot, { recursive: true, force: true });
// await fs.mkdir(gameRoot, { recursive: true });
test("launch game", { timeout: 0, tags:["full", "vanilla"] }, async () => {
const instance = new Instance();
test("launch game", { timeout: 0, tags: ["full", "forge"] }, async () => {
const javaManager = new RuntimeManager(path.join(gameRoot, "java"));
javaManager.on("progress", console.log)
// const java = await javaManager.use(await getJavaComponent("${version}"))
const auth = offlineAuth("player");
instance.setVersion("${version}");
instance.setPaths(gameRoot);
instance.setJavaExecutable("${java}")
instance.setAuth(auth);
const instance = new Instance({
version: "${version}",
auth: offlineAuth("player"),
paths: { root: gameRoot, instance: path.join(gameRoot, "instances", "${version}") },
javaExecutable: "${java}"
});
instance.on("progress", console.log);

View file

@ -1,27 +1,23 @@
import { test } from "vitest";
import { Instance, offlineAuth, RuntimeManager } from "../../dist/index.js";
import { Instance, offlineAuth, RuntimeManager, getJavaComponent } from "../../dist/index.js";
import path from "path";
import fs from "fs/promises";
import { getJavaComponent } from "../../src/index.js";
const gameRoot = path.join(import.meta.dirname, "..", "temp");
// await fs.rm(gameRoot, { recursive: true, force: true });
// await fs.mkdir(gameRoot, { recursive: true });
test("launch game", { timeout: 0, tags: ["full", "vanilla"] }, async () => {
const instance = new Instance();
test("launch game", { timeout: 0, tags: ["full", "forge"] }, async () => {
const javaManager = new RuntimeManager(path.join(gameRoot, "java"));
javaManager.on("progress", console.log)
const java = await javaManager.use(await getJavaComponent("${version}"))
const auth = offlineAuth("player");
instance.setVersion("${version}");
instance.setPaths(gameRoot);
instance.setJavaExecutable(java)
instance.setAuth(auth);
const instance = new Instance({
version: "${version}",
auth: offlineAuth("player"),
paths: { root: gameRoot, instance: path.join(gameRoot, "instances", "${version}") },
javaExecutable: java
});
instance.on("progress", console.log);