UNPKG

@xmcl/installer

Version:

The installers of Minecraft/Forge/Fabric/Liteloader/Quilt

4 lines 161 kB
{ "version": 3, "sources": ["../fabric.ts", "../utils.ts", "../liteloader.ts", "../forge.ts", "../downloadTask.ts", "../minecraft.ts", "../zipValdiator.ts", "../profile.ts", "../neoForged.ts", "../optifine.ts", "../java.ts", "../java-runtime.ts", "../diagnose.ts", "../quilt.ts", "../unzip.ts", "../labymod.ts"], "sourcesContent": ["import { MinecraftFolder, MinecraftLocation, Version } from '@xmcl/core'\nimport { writeFile } from 'fs/promises'\nimport { Dispatcher, request } from 'undici'\nimport { ensureFile, InstallOptions } from './utils'\n\nexport const YARN_MAVEN_URL = 'https://maven.fabricmc.net/net/fabricmc/yarn/maven-metadata.xml'\nexport const LOADER_MAVEN_URL = 'https://maven.fabricmc.net/net/fabricmc/fabric-loader/maven-metadata.xml'\n\nexport interface FabricArtifactVersion {\n gameVersion?: string // \"20w10a\",\n separator?: string\n build?: number\n maven: string // \"net.fabricmc:yarn:20w10a+build.7\",\n version: string // \"20w10a+build.7\",\n stable: boolean\n}\n\nexport interface FabricArtifacts {\n mappings: FabricArtifactVersion[]\n loader: FabricArtifactVersion[]\n}\n\nexport interface FabricLoaderArtifact {\n loader: FabricArtifactVersion\n intermediary: FabricArtifactVersion\n launcherMeta: {\n version: number\n libraries: {\n client: { name: string; url: string }[]\n common: { name: string; url: string }[]\n server: { name: string; url: string }[]\n }\n mainClass: {\n client: string\n server: string\n }\n }\n}\n\nexport interface FabricOptions {\n dispatcher?: Dispatcher\n}\n\n/**\n * Get all the artifacts provided by fabric\n * @param remote The fabric API host\n * @beta\n */\nexport async function getFabricArtifacts(options?: FabricOptions): Promise<FabricArtifacts> {\n const response = await request('https://meta.fabricmc.net/v2/versions', { throwOnError: true, dispatcher: options?.dispatcher })\n const body = response.body.json() as any\n return body\n}\n/**\n * Get fabric-yarn artifact list\n * @param remote The fabric API host\n * @beta\n */\nexport async function getYarnArtifactList(options?: FabricOptions): Promise<FabricArtifactVersion[]> {\n const response = await request('https://meta.fabricmc.net/v2/versions/yarn', { throwOnError: true, dispatcher: options?.dispatcher })\n const body = response.body.json() as any\n return body\n}\n/**\n * Get fabric-yarn artifact list by Minecraft version\n * @param minecraft The Minecraft version\n * @param remote The fabric API host\n * @beta\n */\nexport async function getYarnArtifactListFor(minecraft: string, options?: FabricOptions): Promise<FabricArtifactVersion[]> {\n const response = await request('https://meta.fabricmc.net/v2/versions/yarn/' + minecraft, { throwOnError: true, dispatcher: options?.dispatcher })\n const body = response.body.json() as any\n return body\n}\n/**\n * Get fabric-loader artifact list\n * @param remote The fabric API host\n * @beta\n */\nexport async function getLoaderArtifactList(options?: FabricOptions): Promise<FabricArtifactVersion[]> {\n const response = await request('https://meta.fabricmc.net/v2/versions/loader', { throwOnError: true, dispatcher: options?.dispatcher })\n const body = response.body.json() as any\n return body\n}\n/**\n * Get fabric-loader artifact list by Minecraft version\n * @param minecraft The minecraft version\n * @param remote The fabric API host\n * @beta\n */\nexport async function getLoaderArtifactListFor(minecraft: string, options?: FabricOptions): Promise<FabricLoaderArtifact[]> {\n const response = await request('https://meta.fabricmc.net/v2/versions/loader/' + minecraft, { throwOnError: true, dispatcher: options?.dispatcher })\n const body = response.body.json() as any\n return body\n}\n/**\n * Get fabric-loader artifact list by Minecraft version\n * @param minecraft The minecraft version\n * @param loader The yarn-loader version\n * @param remote The fabric API host\n * @beta\n */\nexport async function getFabricLoaderArtifact(minecraft: string, loader: string, options?: FabricOptions): Promise<FabricLoaderArtifact> {\n const response = await request('https://meta.fabricmc.net/v2/versions/loader/' + minecraft + '/' + loader, { throwOnError: true, dispatcher: options?.dispatcher })\n const body = response.body.json() as any\n return body\n}\n\n/**\n * Install the fabric to the client. Notice that this will only install the json.\n * You need to call `Installer.installDependencies` to get a full client.\n * @param yarnVersion The yarn version\n * @param loaderVersion The fabric loader version\n * @param minecraft The minecraft location\n * @returns The installed version id\n */\n// export async function installFabricYarnAndLoader(yarnVersion: string, loaderVersion: string, minecraft: MinecraftLocation, options: InstallOptions = {}) {\n// const folder = MinecraftFolder.from(minecraft);\n// const mcversion = yarnVersion.split(\"+\")[0];\n// const id = options.versionId || `${mcversion}-fabric${yarnVersion}-${loaderVersion}`;\n\n// const jsonFile = folder.getVersionJson(id);\n\n// const body: Version = constr esponse = await request(`https://fabricmc.net/download/technic/?yarn=${encodeURIComponent(yarnVersion)}&loader=${encodeURIComponent(loaderVersion)}`, { throwOnError: true, dispatcher: options?.dispatcher });\n// const body = response.body.json() as any;\n// return body;\n// body.id = id;\n// if (typeof options.inheritsFrom === \"string\") {\n// body.inheritsFrom = options.inheritsFrom;\n// }\n// await ensureFile(jsonFile);\n// await writeFile(jsonFile, JSON.stringify(body));\n\n// return id;\n// }\n\nexport interface FabricInstallOptions extends InstallOptions {\n side?: 'client' | 'server'\n yarnVersion?: string | FabricArtifactVersion\n}\n\n/**\n * Generate fabric version json to the disk according to yarn and loader\n * @param side Client or server\n * @param yarnVersion The yarn version string or artifact\n * @param loader The loader artifact\n * @param minecraft The Minecraft Location\n * @param options The options\n * @beta\n */\nexport async function installFabric(loader: FabricLoaderArtifact, minecraft: MinecraftLocation, options: FabricInstallOptions = {}) {\n const folder = MinecraftFolder.from(minecraft)\n\n let yarn: string | undefined\n const side = options.side ?? 'client'\n let id = options.versionId\n let mcversion: string\n if (options.yarnVersion) {\n const yarnVersion = options.yarnVersion\n if (typeof yarnVersion === 'string') {\n yarn = yarnVersion\n mcversion = yarn.split('+')[0]\n } else {\n yarn = yarnVersion.version\n mcversion = yarnVersion.gameVersion || yarn.split('+')[0]\n }\n } else {\n mcversion = loader.intermediary.version\n }\n\n if (!id) {\n id = mcversion\n if (yarn) {\n id += `-fabric${yarn}-loader${loader.loader.version}`\n } else {\n id += `-fabric${loader.loader.version}`\n }\n }\n const libraries = [\n { name: loader.loader.maven, url: 'https://maven.fabricmc.net/' },\n { name: loader.intermediary.maven, url: 'https://maven.fabricmc.net/' },\n ...(options.yarnVersion\n ? [{ name: `net.fabricmc:yarn:${yarn}`, url: 'https://maven.fabricmc.net/' }]\n : []),\n ...loader.launcherMeta.libraries.common,\n ...loader.launcherMeta.libraries[side],\n ]\n const mainClass = loader.launcherMeta.mainClass[side]\n const inheritsFrom = options.inheritsFrom || mcversion\n\n const jsonFile = folder.getVersionJson(id)\n\n await ensureFile(jsonFile)\n await writeFile(jsonFile, JSON.stringify({\n id,\n inheritsFrom,\n mainClass,\n libraries,\n arguments: {\n game: [],\n jvm: [],\n },\n releaseTime: new Date().toJSON(),\n time: new Date().toJSON(),\n }))\n\n return id\n}\n", "import { ChildProcess, ExecOptions, spawn, SpawnOptions } from 'child_process'\nimport { access, mkdir, stat } from 'fs/promises'\nimport { dirname } from 'path'\n\nexport { checksum } from '@xmcl/core'\n\nexport function missing(target: string) {\n return access(target).then(() => false, () => true)\n}\n\nexport async function ensureDir(target: string) {\n try {\n await mkdir(target)\n } catch (err) {\n const e: any = err\n if (await stat(target).then((s) => s.isDirectory()).catch(() => false)) { return }\n if (e.code === 'EEXIST') { return }\n if (e.code === 'ENOENT') {\n if (dirname(target) === target) {\n throw e\n }\n try {\n await ensureDir(dirname(target))\n await mkdir(target)\n } catch {\n if (await stat(target).then((s) => s.isDirectory()).catch((e) => false)) { return }\n throw e\n }\n return\n }\n throw e\n }\n}\n\nexport interface SpawnJavaOptions {\n /**\n * The java exectable path. It will use `java` by default.\n *\n * @defaults \"java\"\n */\n java?: string\n\n /**\n * The spawn process function. Used for spawn the java process at the end.\n *\n * By default, it will be the spawn function from \"child_process\" module. You can use this option to change the 3rd party spawn like [cross-spawn](https://www.npmjs.com/package/cross-spawn)\n */\n spawn?: (command: string, args?: ReadonlyArray<string>, options?: SpawnOptions) => ChildProcess\n}\n\nexport function ensureFile(target: string) {\n return ensureDir(dirname(target))\n}\nexport function normalizeArray<T>(arr: T | T[] = []): T[] {\n return arr instanceof Array ? arr : [arr]\n}\nexport function spawnProcess(spawnJavaOptions: SpawnJavaOptions, args: string[], options?: ExecOptions) {\n const process = (spawnJavaOptions?.spawn ?? spawn)(spawnJavaOptions.java ?? 'java', args, options)\n return waitProcess(process)\n}\n\nexport function waitProcess(process: ChildProcess) {\n return new Promise<void>((resolve, reject) => {\n const errorMsg: string[] = []\n process.on('error', (err) => {\n reject(err)\n })\n process.on('close', (code) => {\n if (code !== 0) { reject(errorMsg.join('')) } else { resolve() }\n })\n process.on('exit', (code) => {\n if (code !== 0) { reject(errorMsg.join('')) } else { resolve() }\n })\n process.stdout?.setEncoding('utf-8')\n process.stdout?.on('data', (buf) => { })\n process.stderr?.setEncoding('utf-8')\n process.stderr?.on('data', (buf) => { errorMsg.push(buf.toString()) })\n })\n}\n\n/**\n * Join two urls\n */\nexport function joinUrl(a: string, b: string) {\n if (a.endsWith('/') && b.startsWith('/')) {\n return a + b.substring(1)\n }\n if (!a.endsWith('/') && !b.startsWith('/')) {\n return a + '/' + b\n }\n return a + b\n}\n\nexport interface ParallelTaskOptions {\n throwErrorImmediately?: boolean\n}\n/**\n * Shared install options\n */\nexport interface InstallOptions {\n /**\n * When you want to install a version over another one.\n *\n * Like, you want to install liteloader over a forge version.\n * You should fill this with that forge version id.\n */\n inheritsFrom?: string\n\n /**\n * Override the newly installed version id.\n *\n * If this is absent, the installed version id will be either generated or provided by installer.\n */\n versionId?: string\n}\n\nexport function errorToString(e: any) {\n if (e instanceof Error) {\n return e.stack ? e.stack : e.message\n }\n return e.toString()\n}\n", "import { MinecraftFolder, MinecraftLocation } from '@xmcl/core'\nimport { Task, task } from '@xmcl/task'\nimport { readFile, writeFile } from 'fs/promises'\nimport { join } from 'path'\nimport { Dispatcher, request } from 'undici'\nimport { ensureDir, InstallOptions, missing } from './utils'\n\nexport const DEFAULT_VERSION_MANIFEST = 'http://dl.liteloader.com/versions/versions.json'\n/**\n * The liteloader version list. Containing the minecraft version -> liteloader version info mapping.\n */\nexport interface LiteloaderVersionList {\n meta: {\n description: string\n authors: string\n url: string\n updated: string\n updatedTime: number\n }\n versions: { [version: string]: { snapshot?: LiteloaderVersion; release?: LiteloaderVersion } }\n}\nfunction processLibraries(lib: { name: string; url?: string }) {\n if (Object.keys(lib).length === 1 && lib.name) {\n if (lib.name.startsWith('org.ow2.asm')) {\n lib.url = 'https://files.minecraftforge.net/maven/'\n }\n }\n return lib\n}\n// eslint-disable-next-line @typescript-eslint/no-namespace\nexport namespace LiteloaderVersionList {\n export function parse(content: string) {\n const result = JSON.parse(content)\n const metalist = { meta: result.meta, versions: {} }\n for (const mcversion in result.versions) {\n const versions: { release?: LiteloaderVersion; snapshot?: LiteloaderVersion } =\n (metalist.versions as any)[mcversion] = {}\n const snapshots = result.versions[mcversion].snapshots\n const artifacts = result.versions[mcversion].artefacts // that's right, artefact\n const url = result.versions[mcversion].repo.url\n if (snapshots) {\n const { stream, file, version, md5, timestamp, tweakClass, libraries } = snapshots['com.mumfrey:liteloader'].latest\n const type = (stream === 'RELEASE' ? 'RELEASE' : 'SNAPSHOT')\n versions.snapshot = {\n url,\n type,\n file,\n version,\n md5,\n timestamp,\n mcversion,\n tweakClass,\n libraries: libraries.map(processLibraries),\n }\n }\n if (artifacts) {\n const { stream, file, version, md5, timestamp, tweakClass, libraries } = artifacts['com.mumfrey:liteloader'].latest\n const type = (stream === 'RELEASE' ? 'RELEASE' : 'SNAPSHOT')\n versions.release = {\n url,\n type,\n file,\n version,\n md5,\n timestamp,\n mcversion,\n tweakClass,\n libraries: libraries.map(processLibraries),\n }\n }\n }\n return metalist\n }\n}\n\n/**\n * A liteloader remote version information\n */\nexport interface LiteloaderVersion {\n version: string\n url: string\n file: string\n mcversion: string\n type: 'RELEASE' | 'SNAPSHOT'\n md5: string\n timestamp: string\n libraries: Array<{ name: string; url?: string }>\n tweakClass: string\n}\n\nconst snapshotRoot = 'http://dl.liteloader.com/versions/'\nconst releaseRoot = 'http://repo.mumfrey.com/content/repositories/liteloader/'\n\n/**\n * This error is only thrown from liteloader install currently.\n */\nexport class MissingVersionJsonError extends Error {\n constructor(public version: string,\n /**\n * The path of version json\n */\n public path: string) {\n super()\n this.name = 'MissingVersionJson'\n }\n}\n/**\n * Get or update the LiteLoader version list.\n *\n * This will request liteloader offical json by default. You can replace the request by assigning the remote option.\n */\nexport async function getLiteloaderVersionList(options: {\n /**\n * The request dispatcher\n */\n dispatcher?: Dispatcher\n} = {}): Promise<LiteloaderVersionList> {\n const response = await request(DEFAULT_VERSION_MANIFEST, { dispatcher: options.dispatcher, throwOnError: true })\n const body = await response.body.text()\n return LiteloaderVersionList.parse(body)\n}\n\n/**\n * Install the liteloader to specific minecraft location.\n *\n * This will install the liteloader amount on the corresponded Minecraft version by default.\n * If you want to install over the forge. You should first install forge and pass the installed forge version id to the third param,\n * like `1.12-forge-xxxx`\n *\n * @param versionMeta The liteloader version metadata.\n * @param location The minecraft location you want to install\n * @param version The real existed version id (under the the provided minecraft location) you want to installed liteloader inherit\n * @throws {@link MissingVersionJsonError}\n */\nexport function installLiteloader(versionMeta: LiteloaderVersion, location: MinecraftLocation, options?: InstallOptions) {\n return installLiteloaderTask(versionMeta, location, options).startAndWait()\n}\n\nfunction buildVersionInfo(versionMeta: LiteloaderVersion, mountedJSON: any) {\n const id = `${mountedJSON.id}-Liteloader${versionMeta.mcversion}-${versionMeta.version}`\n const time = new Date(Number.parseInt(versionMeta.timestamp, 10) * 1000).toISOString()\n const releaseTime = time\n const type = versionMeta.type\n const libraries = [\n {\n name: `com.mumfrey:liteloader:${versionMeta.version}`,\n url: type === 'SNAPSHOT' ? snapshotRoot : releaseRoot,\n },\n ...versionMeta.libraries.map(processLibraries),\n ]\n const mainClass = 'net.minecraft.launchwrapper.Launch'\n const inheritsFrom = mountedJSON.id\n const jar = mountedJSON.jar || mountedJSON.id\n const info: any = {\n id, time, releaseTime, type, libraries, mainClass, inheritsFrom, jar,\n }\n if (mountedJSON.arguments) {\n // liteloader not supported for version > 1.12...\n // just write this for exception\n info.arguments = {\n game: ['--tweakClass', versionMeta.tweakClass],\n jvm: [],\n }\n } else {\n info.minecraftArguments = `--tweakClass ${versionMeta.tweakClass} ` + mountedJSON.minecraftArguments\n }\n return info\n}\n\n/**\n * Install the liteloader to specific minecraft location.\n *\n * This will install the liteloader amount on the corresponded Minecraft version by default.\n * If you want to install over the forge. You should first install forge and pass the installed forge version id to the third param,\n * like `1.12-forge-xxxx`\n *\n * @tasks installLiteloader, installLiteloader.resolveVersionJson installLiteloader.generateLiteloaderJson\n *\n * @param versionMeta The liteloader version metadata.\n * @param location The minecraft location you want to install\n * @param version The real existed version id (under the the provided minecraft location) you want to installed liteloader inherit\n */\nexport function installLiteloaderTask(versionMeta: LiteloaderVersion, location: MinecraftLocation, options: InstallOptions = {}): Task<string> {\n return task('installLiteloader', async function installLiteloader() {\n const mc: MinecraftFolder = MinecraftFolder.from(location)\n\n const mountVersion = options.inheritsFrom || versionMeta.mcversion\n\n const mountedJSON: any = await this.yield(task('resolveVersionJson', async function resolveVersionJson() {\n if (await missing(mc.getVersionJson(mountVersion))) {\n throw new MissingVersionJsonError(mountVersion, mc.getVersionJson(mountVersion))\n }\n return readFile(mc.getVersionJson(mountVersion)).then((b) => b.toString()).then(JSON.parse)\n }))\n\n const versionInf = await this.yield(task('generateLiteloaderJson', async function generateLiteloaderJson() {\n const inf = buildVersionInfo(versionMeta, mountedJSON)\n\n inf.id = options.versionId || inf.id\n inf.inheritsFrom = options.inheritsFrom || inf.inheritsFrom\n\n const versionPath = mc.getVersionRoot(inf.id)\n\n await ensureDir(versionPath)\n await writeFile(join(versionPath, inf.id + '.json'), JSON.stringify(inf, undefined, 4))\n\n return inf\n }))\n return versionInf.id as string\n })\n}\n", "import { LibraryInfo, MinecraftFolder, MinecraftLocation, Version as VersionJson } from '@xmcl/core'\nimport { parse as parseForge } from '@xmcl/forge-site-parser'\nimport { Task, task } from '@xmcl/task'\nimport { filterEntries, open, openEntryReadStream, readEntry } from '@xmcl/unzip'\nimport { createWriteStream } from 'fs'\nimport { writeFile } from 'fs/promises'\nimport { dirname, join } from 'path'\nimport { pipeline } from 'stream/promises'\nimport { Dispatcher, request } from 'undici'\nimport { Entry, ZipFile } from 'yauzl'\nimport { DownloadTask } from './downloadTask'\nimport { LibraryOptions, resolveLibraryDownloadUrls } from './minecraft'\nimport { installByProfileTask, InstallProfile, InstallProfileOption } from './profile'\nimport { ensureFile, InstallOptions as InstallOptionsBase, joinUrl, normalizeArray } from './utils'\nimport { ZipValidator } from './zipValdiator'\n\nexport interface ForgeVersionList {\n mcversion: string\n versions: ForgeVersion[]\n}\n/**\n * The forge version metadata to download a forge\n */\nexport interface ForgeVersion {\n /**\n * The installer info\n */\n installer: {\n md5: string\n sha1: string\n /**\n * The url path to concat with forge maven\n */\n path: string\n }\n universal: {\n md5: string\n sha1: string\n /**\n * The url path to concat with forge maven\n */\n path: string\n }\n /**\n * The minecraft version\n */\n mcversion: string\n /**\n * The forge version (without minecraft version)\n */\n version: string\n\n type: 'buggy' | 'recommended' | 'common' | 'latest'\n}\n\n/**\n * All the useful entries in forge installer jar\n */\nexport interface ForgeInstallerEntries {\n /**\n * maven/net/minecraftforge/forge/${forgeVersion}/forge-${forgeVersion}.jar\n */\n forgeJar?: Entry\n /**\n * maven/net/minecraftforge/forge/${forgeVersion}/forge-${forgeVersion}-universal.jar\n */\n forgeUniversalJar?: Entry\n /**\n * data/client.lzma\n */\n clientLzma?: Entry\n /**\n * data/server.lzma\n */\n serverLzma?: Entry\n /**\n * install_profile.json\n */\n installProfileJson?: Entry\n /**\n * version.json\n */\n versionJson?: Entry\n /**\n * forge-${forgeVersion}-universal.jar\n */\n legacyUniversalJar?: Entry\n /**\n * data/run.sh\n */\n runSh?: Entry\n /**\n * data/run.bat\n */\n runBat?: Entry\n /**\n * data/unix_args.txt\n */\n unixArgs?: Entry\n /**\n * data/user_jvm_args.txt\n */\n userJvmArgs?: Entry\n /**\n * data/win_args.txt\n */\n winArgs?: Entry\n}\n\nexport type ForgeInstallerEntriesPattern = ForgeInstallerEntries & Required<Pick<ForgeInstallerEntries, 'versionJson' | 'installProfileJson'>>\nexport type ForgeLegacyInstallerEntriesPattern = Required<Pick<ForgeInstallerEntries, 'installProfileJson' | 'legacyUniversalJar'>>\n\ntype RequiredVersion = {\n /**\n * The installer info.\n *\n * If this is not presented, it will genreate from mcversion and forge version.\n */\n installer?: {\n sha1?: string\n /**\n * The url path to concat with forge maven\n */\n path: string\n }\n /**\n * The minecraft version\n */\n mcversion: string\n /**\n * The forge version (without minecraft version)\n */\n version: string\n}\n\nexport const DEFAULT_FORGE_MAVEN = 'http://files.minecraftforge.net/maven'\n\n/**\n * The options to install forge.\n */\nexport interface InstallForgeOptions extends LibraryOptions, InstallOptionsBase, InstallProfileOption {\n}\n\nexport class DownloadForgeInstallerTask extends DownloadTask {\n readonly installJarPath: string\n\n constructor(forgeVersion: string, installer: RequiredVersion['installer'], minecraft: MinecraftFolder, options: InstallForgeOptions) {\n const path = installer ? installer.path : `net/minecraftforge/forge/${forgeVersion}/forge-${forgeVersion}-installer.jar`\n let url: string\n if (installer) {\n try {\n const parsedUrl = new URL(path)\n url = parsedUrl.toString()\n } catch (e) {\n const forgeMavenPath = path.replace('/maven', '').replace('maven', '')\n url = joinUrl(DEFAULT_FORGE_MAVEN, forgeMavenPath)\n }\n } else {\n const forgeMavenPath = path.replace('/maven', '').replace('maven', '')\n url = joinUrl(DEFAULT_FORGE_MAVEN, forgeMavenPath)\n }\n\n const library = VersionJson.resolveLibrary({\n name: `net.minecraftforge:forge:${forgeVersion}:installer`,\n downloads: {\n artifact: {\n url,\n path: `net/minecraftforge/forge/${forgeVersion}/forge-${forgeVersion}-installer.jar`,\n size: -1,\n sha1: installer?.sha1 || '',\n },\n },\n })!\n const mavenHost = options.mavenHost ? normalizeArray(options.mavenHost) : []\n\n if (mavenHost.indexOf(DEFAULT_FORGE_MAVEN) === -1) {\n mavenHost.push(DEFAULT_FORGE_MAVEN)\n }\n\n const urls = resolveLibraryDownloadUrls(library, { ...options, mavenHost })\n\n const installJarPath = minecraft.getLibraryByPath(library.path)\n\n super({\n url: urls,\n destination: installJarPath,\n validator: installer?.sha1\n ? options.checksumValidatorResolver?.({ algorithm: 'sha1', hash: installer?.sha1 }) || { algorithm: 'sha1', hash: installer?.sha1 }\n : new ZipValidator(),\n agent: options.agent,\n skipPrevalidate: options.skipPrevalidate,\n skipRevalidate: options.skipRevalidate,\n })\n\n this.installJarPath = installJarPath\n this.name = 'downloadInstaller'\n this.param = { version: forgeVersion }\n }\n}\n\nfunction getLibraryPathWithoutMaven(mc: MinecraftFolder, name: string) {\n // remove the maven/ prefix\n return mc.getLibraryByPath(name.substring(name.indexOf('/') + 1))\n}\nfunction extractEntryTo(zip: ZipFile, e: Entry, dest: string) {\n return openEntryReadStream(zip, e).then((stream) => pipeline(stream, createWriteStream(dest)))\n}\n\nasync function installLegacyForgeFromZip(zip: ZipFile, entries: ForgeLegacyInstallerEntriesPattern, profile: InstallProfile, mc: MinecraftFolder, jarFilePath: string, options: InstallForgeOptions) {\n const versionJson = profile.versionInfo\n if (!versionJson) {\n throw new Error(`Malform legacy installer json ${profile.version}`)\n }\n\n // apply override for inheritsFrom\n versionJson.id = options.versionId || versionJson.id\n versionJson.inheritsFrom = options.inheritsFrom || versionJson.inheritsFrom\n\n const rootPath = mc.getVersionRoot(versionJson.id)\n const versionJsonPath = join(rootPath, `${versionJson.id}.json`)\n await ensureFile(versionJsonPath)\n\n const forgeLib = versionJson.libraries.find((l) => l.name.startsWith('net.minecraftforge:forge') || l.name.startsWith('net.minecraftforge:minecraftforge'))\n if (!forgeLib) {\n throw new BadForgeInstallerJarError(jarFilePath)\n }\n const library = LibraryInfo.resolve(forgeLib)\n const jarPath = mc.getLibraryByPath(library.path)\n await ensureFile(jarPath)\n\n await Promise.all([\n writeFile(versionJsonPath, JSON.stringify(versionJson, undefined, 4)),\n extractEntryTo(zip, entries.legacyUniversalJar, jarPath),\n ])\n\n return versionJson.id\n}\n\n/**\n * Unpack forge installer jar file content to the version library artifact directory.\n * @param zip The forge jar file\n * @param entries The entries\n * @param forgeVersion The expected version of forge\n * @param profile The forge install profile\n * @param mc The minecraft location\n * @returns The installed version id\n */\nexport async function unpackForgeInstaller(zip: ZipFile, entries: ForgeInstallerEntriesPattern, forgeVersion: string, profile: InstallProfile, mc: MinecraftFolder, jarPath: string, options: InstallForgeOptions) {\n const versionJson: VersionJson = await readEntry(zip, entries.versionJson).then((b) => b.toString()).then(JSON.parse)\n\n // apply override for inheritsFrom\n versionJson.id = options.versionId || versionJson.id\n versionJson.inheritsFrom = options.inheritsFrom || versionJson.inheritsFrom\n\n // resolve all the required paths\n const rootPath = mc.getVersionRoot(versionJson.id)\n\n const versionJsonPath = join(rootPath, `${versionJson.id}.json`)\n const installJsonPath = join(rootPath, 'install_profile.json')\n\n const dataRoot = dirname(jarPath)\n\n const unpackData = (entry: Entry) => {\n promises.push(extractEntryTo(zip, entry, join(dataRoot, entry.fileName.substring('data/'.length))))\n }\n\n await ensureFile(versionJsonPath)\n\n const promises: Promise<void>[] = []\n if (entries.forgeUniversalJar) {\n promises.push(extractEntryTo(zip, entries.forgeUniversalJar, getLibraryPathWithoutMaven(mc, entries.forgeUniversalJar.fileName)))\n }\n\n if (!profile.data) {\n profile.data = {}\n }\n\n const installerMaven = `net.minecraftforge:forge:${forgeVersion}:installer`\n profile.data.INSTALLER = {\n client: `[${installerMaven}]`,\n server: `[${installerMaven}]`,\n }\n\n if (entries.serverLzma) {\n // forge version and mavens, compatible with twitch api\n const serverMaven = `net.minecraftforge:forge:${forgeVersion}:serverdata@lzma`\n // override forge bin patch location\n profile.data.BINPATCH.server = `[${serverMaven}]`\n\n const serverBinPath = mc.getLibraryByPath(LibraryInfo.resolve(serverMaven).path)\n await ensureFile(serverBinPath)\n promises.push(extractEntryTo(zip, entries.serverLzma, serverBinPath))\n }\n\n if (entries.clientLzma) {\n // forge version and mavens, compatible with twitch api\n const clientMaven = `net.minecraftforge:forge:${forgeVersion}:clientdata@lzma`\n // override forge bin patch location\n profile.data.BINPATCH.client = `[${clientMaven}]`\n\n const clientBinPath = mc.getLibraryByPath(LibraryInfo.resolve(clientMaven).path)\n await ensureFile(clientBinPath)\n promises.push(extractEntryTo(zip, entries.clientLzma, clientBinPath))\n }\n\n if (entries.forgeJar) {\n promises.push(extractEntryTo(zip, entries.forgeJar, getLibraryPathWithoutMaven(mc, entries.forgeJar.fileName)))\n }\n if (entries.runBat) { unpackData(entries.runBat) }\n if (entries.runSh) { unpackData(entries.runSh) }\n if (entries.winArgs) { unpackData(entries.winArgs) }\n if (entries.unixArgs) { unpackData(entries.unixArgs) }\n if (entries.userJvmArgs) { unpackData(entries.userJvmArgs) }\n\n promises.push(\n writeFile(installJsonPath, JSON.stringify(profile)),\n writeFile(versionJsonPath, JSON.stringify(versionJson)),\n )\n\n await Promise.all(promises)\n\n return versionJson.id\n}\n\nexport function isLegacyForgeInstallerEntries(entries: ForgeInstallerEntries): entries is ForgeLegacyInstallerEntriesPattern {\n return !!entries.legacyUniversalJar && !!entries.installProfileJson\n}\n\nexport function isForgeInstallerEntries(entries: ForgeInstallerEntries): entries is ForgeInstallerEntriesPattern {\n return !!entries.installProfileJson && !!entries.versionJson\n}\n\n/**\n * Walk the forge installer file to find key entries\n * @param zip THe forge instal\n * @param forgeVersion Forge version to install\n */\nexport async function walkForgeInstallerEntries(zip: ZipFile, forgeVersion: string): Promise<ForgeInstallerEntries> {\n const [forgeJar, forgeUniversalJar, clientLzma, serverLzma, installProfileJson, versionJson, legacyUniversalJar, runSh, runBat, unixArgs, userJvmArgs, winArgs] = await filterEntries(zip, [\n `maven/net/minecraftforge/forge/${forgeVersion}/forge-${forgeVersion}.jar`,\n `maven/net/minecraftforge/forge/${forgeVersion}/forge-${forgeVersion}-universal.jar`,\n 'data/client.lzma',\n 'data/server.lzma',\n 'install_profile.json',\n 'version.json',\n (e) => e.fileName === `forge-${forgeVersion}-universal.jar` || (e.fileName.startsWith('forge-') && e.fileName.endsWith('-universal.jar')) || (e.fileName.startsWith('minecraftforge-universal-')), // legacy installer format\n 'data/run.sh',\n 'data/run.bat',\n 'data/unix_args.txt',\n 'data/user_jvm_args.txt',\n 'data/win_args.txt',\n ])\n return {\n forgeJar,\n forgeUniversalJar,\n clientLzma,\n serverLzma,\n installProfileJson,\n versionJson,\n legacyUniversalJar,\n runSh,\n runBat,\n unixArgs,\n userJvmArgs,\n winArgs,\n }\n}\n\nexport class BadForgeInstallerJarError extends Error {\n name = 'BadForgeInstallerJarError'\n\n constructor(\n public jarPath: string,\n /**\n * What entry in jar is missing\n */\n public entry?: string) {\n super(entry ? `Missing entry ${entry} in forge installer jar: ${jarPath}` : `Bad forge installer: ${jarPath}`)\n }\n}\n\nexport function installByInstallerTask(version: RequiredVersion, minecraft: MinecraftLocation, options: InstallForgeOptions) {\n return task('installForge', async function () {\n function getForgeArtifactVersion() {\n const [_, minor] = version.mcversion.split('.')\n const minorVersion = Number.parseInt(minor)\n if (minorVersion >= 7 && minorVersion <= 8) {\n return `${version.mcversion}-${version.version}-${version.mcversion}`\n }\n if (version.version.startsWith(version.mcversion)) {\n return version.version\n }\n return `${version.mcversion}-${version.version}`\n }\n const forgeVersion = getForgeArtifactVersion()\n const mc = MinecraftFolder.from(minecraft)\n const jarPath = await this.yield(new DownloadForgeInstallerTask(forgeVersion, version.installer, mc, options)\n .map(function () { return this.installJarPath }))\n\n const zip = await open(jarPath, { lazyEntries: true, autoClose: false })\n const entries = await walkForgeInstallerEntries(zip, forgeVersion)\n\n if (!entries.installProfileJson) {\n throw new BadForgeInstallerJarError(jarPath, 'install_profile.json')\n }\n const profile: InstallProfile = await readEntry(zip, entries.installProfileJson).then((b) => b.toString()).then(JSON.parse)\n if (isForgeInstallerEntries(entries)) {\n // new forge\n const versionId = await unpackForgeInstaller(zip, entries, forgeVersion, profile, mc, jarPath, options)\n await this.concat(installByProfileTask(profile, minecraft, options))\n return versionId\n } else if (isLegacyForgeInstallerEntries(entries)) {\n // legacy forge\n return installLegacyForgeFromZip(zip, entries, profile, mc, jarPath, options)\n } else {\n // bad forge\n throw new BadForgeInstallerJarError(jarPath)\n }\n })\n}\n\n/**\n * Install forge to target location.\n * Installation task for forge with mcversion >= 1.13 requires java installed on your pc.\n * @param version The forge version meta\n * @returns The installed version name.\n * @throws {@link BadForgeInstallerJarError}\n */\nexport function installForge(version: RequiredVersion, minecraft: MinecraftLocation, options?: InstallForgeOptions) {\n return installForgeTask(version, minecraft, options).startAndWait()\n}\n\n/**\n * Install forge to target location.\n * Installation task for forge with mcversion >= 1.13 requires java installed on your pc.\n * @param version The forge version meta\n * @returns The task to install the forge\n * @throws {@link BadForgeInstallerJarError}\n */\nexport function installForgeTask(version: RequiredVersion, minecraft: MinecraftLocation, options: InstallForgeOptions = {}): Task<string> {\n return installByInstallerTask(version, minecraft, options)\n}\n\n/**\n * Query the webpage content from files.minecraftforge.net.\n *\n * You can put the last query result to the fallback option. It will check if your old result is up-to-date.\n * It will request a new page only when the fallback option is outdated.\n *\n * @param option The option can control querying minecraft version, and page caching.\n */\nexport async function getForgeVersionList(options: {\n /**\n * The minecraft version you are requesting\n */\n minecraft?: string\n dispatcher?: Dispatcher\n} = {}): Promise<ForgeVersionList> {\n const mcversion = options.minecraft || ''\n const url = mcversion === '' ? 'http://files.minecraftforge.net/maven/net/minecraftforge/forge/index.html' : `http://files.minecraftforge.net/maven/net/minecraftforge/forge/index_${mcversion}.html`\n const response = await request(url, {\n dispatcher: options.dispatcher,\n maxRedirections: 3,\n })\n const body = parseForge(await response.body.text())\n return body as any\n}\n", "import { AbortSignal, download, DownloadAbortError, DownloadOptions, ProgressController } from '@xmcl/file-transfer'\nimport { AbortableTask } from '@xmcl/task'\n\nexport class DownloadTask extends AbortableTask<void> implements ProgressController {\n protected abort: (isCancelled: boolean) => void = () => { }\n\n constructor(protected options: DownloadOptions) {\n super()\n this._from = options.url instanceof Array ? options.url[0] : options.url\n this._to = options.destination\n }\n\n onProgress(url: URL, chunkSize: number, progress: number, total: number): void {\n this._progress = progress\n this._total = total\n this._from = url.toString()\n this.update(chunkSize)\n }\n\n protected process(): Promise<void> {\n const listeners: Array<() => void> = []\n const aborted = () => this.isCancelled || this.isPaused\n const signal: AbortSignal = {\n get aborted() { return aborted() },\n addEventListener(event, listener) {\n if (event !== 'abort') {\n return this\n }\n listeners.push(listener)\n return this\n },\n removeEventListener(event, listener) {\n // noop as this will be auto gc\n return this\n },\n }\n this.abort = () => {\n listeners.forEach((l) => l())\n }\n return download({\n ...this.options,\n progressController: this,\n abortSignal: signal,\n })\n }\n\n protected isAbortedError(e: any): boolean {\n if (e instanceof DownloadAbortError) {\n return true\n }\n return false\n }\n}\n", "import { MinecraftFolder, MinecraftLocation, ResolvedLibrary, ResolvedVersion, Version, Version as VersionJson } from '@xmcl/core'\nimport { ChecksumNotMatchError, ChecksumValidatorOptions, DownloadBaseOptions, JsonValidator, Validator } from '@xmcl/file-transfer'\nimport { task, Task } from '@xmcl/task'\nimport { readFile, stat, writeFile } from 'fs/promises'\nimport { join } from 'path'\nimport { Dispatcher, request } from 'undici'\nimport { DownloadTask } from './downloadTask'\nimport { ensureDir, errorToString, joinUrl, normalizeArray, ParallelTaskOptions } from './utils'\nimport { ZipValidator } from './zipValdiator'\n\n/**\n * The function to swap library host.\n */\nexport type LibraryHost = (library: ResolvedLibrary) => string | string[] | undefined\n\nexport interface MinecraftVersionBaseInfo {\n /**\n * The version id, like 1.14.4\n */\n id: string\n /**\n * The version json download url\n */\n url: string\n}\n\n/**\n * The version metadata containing the version information, like download url\n */\nexport interface MinecraftVersion extends MinecraftVersionBaseInfo {\n /**\n * The version id, like 1.14.4\n */\n id: string\n type: string\n time: string\n releaseTime: string\n /**\n * The version json download url\n */\n url: string\n}\n\nexport interface AssetInfo {\n name: string\n hash: string\n size: number\n}\n\n/**\n * Minecraft version metadata list\n */\nexport interface MinecraftVersionList {\n latest: {\n /**\n * Snapshot version id of the Minecraft\n */\n snapshot: string\n /**\n * Release version id of the Minecraft, like 1.14.2\n */\n release: string\n }\n /**\n * All the vesrsion list\n */\n versions: MinecraftVersion[]\n}\n\n/**\n * Default minecraft version manifest url.\n */\nexport const DEFAULT_VERSION_MANIFEST_URL = 'https://launchermeta.mojang.com/mc/game/version_manifest.json'\n/**\n * Default resource/assets url root\n */\nexport const DEFAULT_RESOURCE_ROOT_URL = 'https://resources.download.minecraft.net'\n\n/**\n * Get and update the version list.\n * This try to send http GET request to offical Minecraft metadata endpoint by default.\n * You can swap the endpoint by passing url on `remote` in option.\n *\n * @returns The new list if there is\n */\nexport async function getVersionList(options: {\n /**\n * Request dispatcher\n */\n dispatcher?: Dispatcher\n} = {}): Promise<MinecraftVersionList> {\n const response = await request(DEFAULT_VERSION_MANIFEST_URL, { dispatcher: options.dispatcher, throwOnError: true })\n return await response.body.json() as any\n}\n\n/**\n * Change the library host url\n */\nexport interface LibraryOptions extends DownloadBaseOptions, ParallelTaskOptions {\n /**\n * A more flexiable way to control library download url.\n * @see mavenHost\n */\n libraryHost?: LibraryHost\n /**\n * The alterative maven host to download library. It will try to use these host from the `[0]` to the `[maven.length - 1]`\n */\n mavenHost?: string | string[]\n /**\n * Control how many libraries download task should run at the same time.\n * It will override the `maxConcurrencyOption` if this is presented.\n *\n * This will be ignored if you have your own downloader assigned.\n */\n librariesDownloadConcurrency?: number\n\n checksumValidatorResolver?: (checksum: ChecksumValidatorOptions) => Validator\n}\n/**\n * Change the host url of assets download\n */\nexport interface AssetsOptions extends DownloadBaseOptions, ParallelTaskOptions {\n /**\n * The alternative assets host to download asset. It will try to use these host from the `[0]` to the `[assetsHost.length - 1]`\n */\n assetsHost?: string | string[]\n /**\n * Control how many assets download task should run at the same time.\n * It will override the `maxConcurrencyOption` if this is presented.\n *\n * This will be ignored if you have your own downloader assigned.\n */\n assetsDownloadConcurrency?: number\n\n /**\n * The assets index download or url replacement\n */\n assetsIndexUrl?: string | string[] | ((version: ResolvedVersion) => string | string[])\n\n checksumValidatorResolver?: (checksum: ChecksumValidatorOptions) => Validator\n /**\n * Only precheck the size of the assets. Do not check the hash.\n */\n prevalidSizeOnly?: boolean\n}\n\nexport type InstallLibraryVersion = Pick<ResolvedVersion, 'libraries' | 'minecraftDirectory'>\n\nfunction resolveDownloadUrls<T>(original: string, version: T, option?: string | string[] | ((version: T) => string | string[])) {\n const result = [] as string[]\n if (typeof option === 'function') {\n result.unshift(...normalizeArray(option(version)))\n } else {\n result.unshift(...normalizeArray(option))\n }\n if (result.indexOf(original) === -1) {\n result.push(original)\n }\n return result\n}\n/**\n * Replace the minecraft client or server jar download\n */\nexport interface JarOption extends DownloadBaseOptions, ParallelTaskOptions, InstallSideOption {\n /**\n * The version json url replacement\n */\n json?: string | string[] | ((version: MinecraftVersionBaseInfo) => string | string[])\n /**\n * The client jar url replacement\n */\n client?: string | string[] | ((version: ResolvedVersion) => string | string[])\n /**\n * The server jar url replacement\n */\n server?: string | string[] | ((version: ResolvedVersion) => string | string[])\n\n checksumValidatorResolver?: (checksum: ChecksumValidatorOptions) => Validator\n}\n\nexport interface InstallSideOption {\n /**\n * The installation side\n */\n side?: 'client' | 'server'\n}\n\nexport type Options = DownloadBaseOptions & ParallelTaskOptions & AssetsOptions & JarOption & LibraryOptions & InstallSideOption\n\n/**\n * Install the Minecraft game to a location by version metadata.\n *\n * This will install version json, version jar, and all dependencies (assets, libraries)\n *\n * @param versionMeta The version metadata\n * @param minecraft The Minecraft location\n * @param option\n */\nexport async function install(versionMeta: MinecraftVersionBaseInfo, minecraft: MinecraftLocation, option: Options = {}): Promise<ResolvedVersion> {\n return installTask(versionMeta, minecraft, option).startAndWait()\n}\n\n/**\n * Only install the json/jar. Do not install dependencies.\n *\n * @param versionMeta the version metadata; get from updateVersionMeta\n * @param minecraft minecraft location\n */\nexport function installVersion(versionMeta: MinecraftVersionBaseInfo, minecraft: MinecraftLocation, options: JarOption = {}): Promise<ResolvedVersion> {\n return installVersionTask(versionMeta, minecraft, options).startAndWait()\n}\n\n/**\n * Install the completeness of the Minecraft game assets and libraries on a existed version.\n *\n * @param version The resolved version produced by Version.parse\n * @param minecraft The minecraft location\n */\nexport function installDependencies(version: ResolvedVersion, options?: Options): Promise<ResolvedVersion> {\n return installDependenciesTask(version, options).startAndWait()\n}\n\n/**\n * Install or check the assets to resolved version\n *\n * @param version The target version\n * @param options The option to replace assets host url\n */\nexport function installAssets(version: ResolvedVersion, options: AssetsOptions = {}): Promise<ResolvedVersion> {\n return installAssetsTask(version, options).startAndWait()\n}\n\n/**\n * Install all the libraries of providing version\n * @param version The target version\n * @param options The library host swap option\n */\nexport function installLibraries(version: ResolvedVersion, options: LibraryOptions = {}): Promise<void> {\n return installLibrariesTask(version, options).startAndWait()\n}\n\n/**\n * Only install several resolved libraries\n * @param libraries The resolved libraries\n * @param minecraft The minecraft location\n * @param option The install option\n */\nexport async function installResolvedLibraries(libraries: ResolvedLibrary[], minecraft: MinecraftLocation, option?: LibraryOptions): Promise<void> {\n await installLibrariesTask({ libraries, minecraftDirectory: typeof minecraft === 'string' ? minecraft : minecraft.root }, option).startAndWait()\n}\n\n/**\n * Install the Minecraft game to a location by version metadata.\n *\n * This will install version json, version jar, and all dependencies (assets, libraries)\n *\n * @param type The type of game, client or server\n * @param versionMeta The version metadata\n * @param minecraft The Minecraft location\n * @param options\n */\nexport function installTask(versionMeta: MinecraftVersionBaseInfo, minecraft: MinecraftLocation, options: Options = {}): Task<ResolvedVersion> {\n return task('install', async function () {\n const version = await this.yield(installVersionTask(versionMeta, minecraft, options))\n if (options.side !== 'server') {\n await this.yield(installDependenciesTask(version, options))\n }\n return version\n })\n}\n/**\n * Only install the json/jar. Do not install dependencies.\n *\n * @param type client or server\n * @param versionMeta the version metadata; get from updateVersionMeta\n * @param minecraft minecraft location\n */\nexport function installVersionTask(versionMeta: MinecraftVersionBaseInfo, minecraft: MinecraftLocation, options: JarOption = {}): Task<ResolvedVersion> {\n return task('version', async function () {\n await this.yield(new InstallJsonTask(versionMeta, minecraft, options))\n const version = await VersionJson.parse(minecraft, versionMeta.id)\n if (version.downloads[options.side ?? 'client']) {\n await this.yield(new InstallJarTask(version as any, minecraft, options))\n }\n return version\n }, versionMeta)\n}\n\n/**\n * Install the completeness of the Minecraft game assets and libraries on a existed version.\n *\n * @param version The resolved version produced by Version.parse\n * @param minecraft The minecraft location\n */\nexport function installDependenciesTask(version: ResolvedVersion, options: Options = {}): Task<ResolvedVersion> {\n return task('dependencies', async function () {\n await Promise.all([\n this.yield(installAssetsTask(version, options)),\n this.yield(installLibrariesTask(version, options)),\n ])\n return version\n })\n}\n\n/**\n * Install or check the assets to resolved version\n *\n * @param version The target version\n * @param options The option to replace assets host url\n */\nexport function installAssetsTask(version: ResolvedVersion, options: AssetsOptions = {}): Task<ResolvedVersion> {\n return task('assets', async function () {\n const folder = MinecraftFolder.from(version.minecraftDirectory)\n if (version.logging?.client?.file) {\n const file = version.logging.client.file\n\n await this.yield(new DownloadTask({\n url: file.url,\n validator: {\n algorithm: 'sha1',\n hash: file.sha1,\n },\n destination: folder.getLogConfig(file.id),\n agent: options.agent,\n headers: options.headers,\n }).setName('asset', { name: file.id, hash: file.sha1, size: file.size }))\n }\n const jsonPath = folder.getPath('assets', 'indexes', version.assets + '.json')\n\n if (version.assetIndex) {\n await this.yield(new InstallAssetIndexTask(version as any, options))\n }\n\n await ensureDir(folder.getPath('assets', 'objects'))\n interface AssetIndex {\n objects: {\n [key: string]: {\n hash: string\n size: number\n }\n }\n }\n\n const getAssetIndexFallback = async () => {\n const urls = resolveDownloadUrls(version.assetIndex!.url, version, options.assetsIndexUrl)\n for (const url of urls) {\n try {\n const response = await request(url, { dispatcher: options.agent?.dispatcher })\n const json = await response.body.json() as any\n await writeFile(jsonPath, JSON.stringify(json))\n return json\n } catch {\n // ignore\n }\n }\n }\n\n let objectArray: any[]\n try {\n const { objects } = JSON.parse(await readFile(jsonPath).then((b) => b.toString())) as AssetIndex\n objectArray = Object.keys(objects).map((k) => ({ name: k, ...objects[k] }))\n } catch (e) {\n if ((e instanceof SyntaxError)) {\n throw e\n }\n const { objects } = await getAssetIndexFallback()\n object