From 754ce2216ed852b84fda59cfa9b95bd43742e386 Mon Sep 17 00:00:00 2001 From: HerozDotExe Date: Thu, 28 May 2026 21:21:05 +0200 Subject: [PATCH] feat: add progress for non tracked task, standardize step name in progress event --- src/launcher/instance.ts | 588 +++++++++++++++++++++++---------------- 1 file changed, 351 insertions(+), 237 deletions(-) diff --git a/src/launcher/instance.ts b/src/launcher/instance.ts index 95a62d2..8adbfdb 100644 --- a/src/launcher/instance.ts +++ b/src/launcher/instance.ts @@ -1,272 +1,386 @@ import { EventEmitter } from "events"; -import { BaseConfig, Config, InstanceEvents, logger, PoolFile, Version, ModloaderConfig } from "../utils/types"; +import { + BaseConfig, + Config, + InstanceEvents, + logger, + PoolFile, + Version, + ModloaderConfig, +} from "../utils/types"; import path from "node:path"; -import fs from "fs/promises" -import { argumentsGenerator, AssetsDownloader, launch, LibrariesDownloader, NativesDownloader, version } from "../core"; +import fs from "fs/promises"; +import { + argumentsGenerator, + AssetsDownloader, + launch, + LibrariesDownloader, + NativesDownloader, + version, +} from "../core"; import { ConfigError, InstallError, LaunchError } from "../utils/errors"; import { checkJava } from "./java"; -import { fixVersionWithDoubleName, installFabric, installForge } from "../core/modloaders"; +import { + fixVersionWithDoubleName, + installFabric, + installForge, +} from "../core/modloaders"; import { ChildProcessWithoutNullStreams } from "node:child_process"; import { prepareManifest } from "../core/mergeManifests"; import { DownloadPool } from "../utils/fetch"; export function defineConfig(logger: logger, ...layers: Partial[]) { - let config: Partial = {} as Partial; - 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, logger) - } + let config: Partial = {} as Partial; + 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, logger); + } - 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.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.args = { + java: config.args?.java ?? "", + game: config.args?.game ?? "", + }; - config.ram = { max: config.ram?.max ?? "2G", min: config.ram?.min ?? "2G" }; + config.ram = { max: config.ram?.max ?? "2G", min: config.ram?.min ?? "2G" }; - if (!config.files) { - config.files = [] - } + if (!config.files) { + config.files = []; + } - return config as Config + return config as Config; } export class Instance extends EventEmitter { - ready: boolean - config: Config - versionLocation: string - versionManifest: Version - logger: logger + ready: boolean; + config: Config; + versionLocation: string; + versionManifest: Version; + logger: logger; - constructor(...layers: Partial[]) { - super() - this.logger = (step: string, message: unknown) => { - this.emit("log", step, message) - } - this.config = defineConfig(this.logger, ...layers) - this.ready = false - this.versionManifest = {} as Version - this.versionLocation = "" + constructor(...layers: Partial[]) { + super(); + this.logger = (step: string, message: unknown) => { + this.emit("log", step, message); + }; + this.config = defineConfig(this.logger, ...layers); + this.ready = false; + this.versionManifest = {} as Version; + this.versionLocation = ""; + } + + private async log(step: string, message: unknown) { + this.emit("log", step, message); + } + + private async init() { + this.log("init", "Initializing instance"); + + this.versionLocation = path.join( + this.config.paths.versions, + this.config.version, + ); + + if (this.config.modloader?.name === "forge") { + this.config.modloader.version = fixVersionWithDoubleName( + this.config.version, + this.config.modloader, + ); } - private async log(step: string, message: unknown) { - this.emit("log", step, message) + this.versionManifest = await version.getVersionManifest( + this.config.version, + this.versionLocation, + ); + + this.ready = true; + } + + async install() { + try { + 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, + this.logger, + { cause: error }, + ); } - private async init() { - this.log("init", "Initializing instance") + this.log("install-libraries", "Downloading libraries"); - this.versionLocation = path.join(this.config.paths.versions, this.config.version); + try { + const librariesDownloader = await LibrariesDownloader( + this.config.paths.libraries, + this.versionManifest, + ); - if (this.config.modloader?.name === "forge") { - this.config.modloader.version = fixVersionWithDoubleName(this.config.version, this.config.modloader) - } - - this.versionManifest = await version.getVersionManifest( - this.config.version, - this.versionLocation, + librariesDownloader.on("completed", () => { + this.emit( + "progress", + "install-libraries", + librariesDownloader.done, + librariesDownloader.total, + librariesDownloader.doneSize, + librariesDownloader.totalSize, ); + }); - this.ready = true + await librariesDownloader.run(); + } catch (error) { + throw new InstallError( + "An error occured while downloading libraries", + "install-libraries", + this.config, + this.logger, + { cause: error }, + ); } - async install() { - try { - if (!this.ready) await this.init() + this.log("install-assets", "Downloading assets"); - await version.downloadJar( - this.versionManifest, - this.versionLocation, - ); - } catch (error) { - throw new InstallError("An error occured while initializing instance", "install-init", this.config, this.logger, { cause: error }) - } + try { + const assetsDownloader = await AssetsDownloader( + this.config.paths.assets, + this.versionManifest, + ); + assetsDownloader.on("completed", () => { + this.emit( + "progress", + "install-assets", + assetsDownloader.done, + assetsDownloader.total, + assetsDownloader.doneSize, + assetsDownloader.totalSize, + ); + }); - this.log("libraries", "Downloading libraries") - - try { - const librariesDownloader = await LibrariesDownloader( - this.config.paths.libraries, - this.versionManifest, - ); - - librariesDownloader.on("completed", () => { - this.emit( - "progress", - "libraries", - librariesDownloader.done, - librariesDownloader.total, - librariesDownloader.doneSize, - librariesDownloader.totalSize, - ); - }); - - await librariesDownloader.run(); - } catch (error) { - throw new InstallError("An error occured while downloading libraries", "libraries", this.config, this.logger, { cause: error }) - } - - this.log("assets", "Downloading assets") - - 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, this.logger, { cause: error }) - } - - this.log("natives", "Downloading natives") - - try { - const nativesDownloader = await NativesDownloader( - path.join(this.config.paths.instance, "natives"), - this.versionManifest!, - ); - 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 downloading natives", "natives", this.config, this.logger, { cause: error }) - } - - this.log("java", "Checking java") - - const javaError = await checkJava(this.config.javaExecutable) - if (javaError) { - throw new InstallError("Invalid java provided", "java", this.config, this.logger, { cause: javaError }) - } - - if (this.config.modloader) { - this.log("install-modloader", "Installing modloader") - try { - switch (this.config.modloader.name) { - case "forge": - case "neoforge": - await installForge( - this.config as ModloaderConfig, - this.logger - ); - break; - case "fabric": - await installFabric(this.config as ModloaderConfig, this.logger) - break; - default: - throw new Error("Unknown modloader"); - } - } catch (error) { - throw new InstallError("An error occured while installing the modloader", "install-modloader", this.config, this.logger, { cause: error }) - } - } - - if (this.config.files.length > 0) { - this.log("install-modpack", "Downloading modpack's files") - try { - const files = this.config.files.map(f => { - return { url: f.url, path: path.join(this.config.paths.instance, f.path), size: f.size } - }) - const pool = new DownloadPool(files, { pQueueOptions: { concurrency: 5 }, overwrite: false }) - pool.on("completed", () => { - this.emit( - "progress", - "modpack-files", - pool.done, - pool.total, - pool.doneSize, - pool.totalSize, - ); - }); - await pool.run(); - } catch (error) { - throw new InstallError("An error occured while downloading modpack's files", "install-modpack", this.config, this.logger, { cause: error }) - } - } - - if (this.config.overridesPath) { - this.log("install-modpack", "Copying modpack's overrides") - try { - await fs.cp(this.config.overridesPath, this.config.paths.instance, {recursive: true}) - } catch (error) { - throw new InstallError("An error occured while copying modpack's overrides", "install-modpack", this.config, this.logger, { cause: error }) - } - } + await assetsDownloader.run(); + } catch (error) { + throw new InstallError( + "An error occured while downloading assets", + "install-assets", + this.config, + this.logger, + { cause: error }, + ); } - async launch(): Promise { - try { - if (!this.ready) await this.init() - } catch (error) { - throw new LaunchError("An error occured while initializing instance", "launch-init", this.config, this.logger, { cause: error }) - } + this.log("install-natives", "Downloading natives"); - if (this.config.modloader) { - this.log("launch-modloader", "Preparing modloader") - try { - switch (this.config.modloader.name) { - case "forge": - case "neoforge": - case "fabric": - this.versionManifest = await prepareManifest(this.config as ModloaderConfig, this.versionManifest) - break - default: - throw new Error("Unknown modloader"); - } - } catch (error) { - throw new LaunchError("An error occured while preparing the modloader", "launch-modloader", this.config, this.logger, { cause: error }) - } - } - - this.log("arguments", "Preparing launch arguments") - - 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, this.logger, { cause: error }) - } - - - this.log("launch-process", "Preparing launch arguments") - try { - const process = launch(args!, this.config.paths.instance, this.logger); - - return process; - } catch (error) { - throw new LaunchError("An error occured while launching minecraft", "launch-process", this.config, this.logger, { cause: error }) - } + try { + const nativesDownloader = await NativesDownloader( + path.join(this.config.paths.instance, "natives"), + this.versionManifest!, + ); + nativesDownloader.on("completed", () => { + this.emit( + "progress", + "install-natives", + nativesDownloader.done, + nativesDownloader.total, + nativesDownloader.doneSize, + nativesDownloader.totalSize, + ); + }); + await nativesDownloader.run(); + } catch (error) { + throw new InstallError( + "An error occured while downloading natives", + "install-natives", + this.config, + this.logger, + { cause: error }, + ); } -} \ No newline at end of file + + this.log("install-java", "Checking java"); + + const javaError = await checkJava(this.config.javaExecutable); + if (javaError) { + throw new InstallError( + "Invalid java provided", + "install-java", + this.config, + this.logger, + { cause: javaError }, + ); + } + + if (this.config.modloader) { + this.log("install-modloader", "Installing modloader"); + this.emit("progress", "install-modloader", 0, 1, 0, 1); + try { + switch (this.config.modloader.name) { + case "forge": + case "neoforge": + await installForge(this.config as ModloaderConfig, this.logger); + break; + case "fabric": + await installFabric(this.config as ModloaderConfig, this.logger); + break; + default: + throw new Error("Unknown modloader"); + } + } catch (error) { + throw new InstallError( + "An error occured while installing the modloader", + "install-modloader", + this.config, + this.logger, + { cause: error }, + ); + } + this.emit("progress", "install-modloader", 1, 1, 1, 1); + } + + if (this.config.files.length > 0) { + this.log("install-modpack-files", "Downloading modpack's files"); + try { + const files = this.config.files.map((f) => { + return { + url: f.url, + path: path.join(this.config.paths.instance, f.path), + size: f.size, + }; + }); + const pool = new DownloadPool(files, { + pQueueOptions: { concurrency: 5 }, + overwrite: false, + }); + pool.on("completed", () => { + this.emit( + "progress", + "install-modpack-files", + pool.done, + pool.total, + pool.doneSize, + pool.totalSize, + ); + }); + await pool.run(); + } catch (error) { + throw new InstallError( + "An error occured while downloading modpack's files", + "install-modpack-files", + this.config, + this.logger, + { cause: error }, + ); + } + } + + if (this.config.overridesPath) { + this.log("install-modpack-overrides", "Copying modpack's overrides"); + this.emit("progress", "install-modpack-overrides", 0, 1, 0, 1); + try { + await fs.cp(this.config.overridesPath, this.config.paths.instance, { + recursive: true, + }); + } catch (error) { + throw new InstallError( + "An error occured while copying modpack's overrides", + "install-modpack-overrides", + this.config, + this.logger, + { cause: error }, + ); + } + this.emit("progress", "install-modpack-overrides", 1, 1, 1, 1); + } + } + + async launch(): Promise { + try { + if (!this.ready) await this.init(); + } catch (error) { + throw new LaunchError( + "An error occured while initializing instance", + "launch-init", + this.config, + this.logger, + { cause: error }, + ); + } + + if (this.config.modloader) { + this.log("launch-modloader", "Preparing modloader"); + try { + switch (this.config.modloader.name) { + case "forge": + case "neoforge": + case "fabric": + this.versionManifest = await prepareManifest( + this.config as ModloaderConfig, + this.versionManifest, + ); + break; + default: + throw new Error("Unknown modloader"); + } + } catch (error) { + throw new LaunchError( + "An error occured while preparing the modloader", + "launch-modloader", + this.config, + this.logger, + { cause: error }, + ); + } + } + + this.log("launch-arguments", "Preparing launch arguments"); + + let args; + try { + args = await argumentsGenerator.generateLaunchArguments( + this.versionManifest, + this.config, + ); + } catch (error) { + throw new LaunchError( + "An error occured while generating launch arguments", + "launch-arguments", + this.config, + this.logger, + { cause: error }, + ); + } + + this.log("launch-process", "Preparing launch arguments"); + try { + const process = launch(args!, this.config.paths.instance, this.logger); + + return process; + } catch (error) { + throw new LaunchError( + "An error occured while launching minecraft", + "launch-process", + this.config, + this.logger, + { cause: error }, + ); + } + } +}