import { MinecraftFolder , MinecraftLocation } from "@xmcl/core" ; import { task , Task , TaskGroup } from "@xmcl/task" ; import { open , readAllEntries , readEntry } from "@xmcl/unzip" ; import { Agent as HttpsAgent } from "https" ; import { basename , join } from "path" ; import { Entry , ZipFile } from "yauzl" ; import { DownloadTask } from "./downloadTask" ; import { withAgents } from "./http/agents" ; import { DownloadBaseOptions } from "./http/download" ; import { fetchText } from "./http/fetch" ; import { UnzipTask } from "./unzip" ; import { errorToString , ParallelTaskOptions } from "./utils" ; export interface CurseforgeOptions extends DownloadBaseOptions , ParallelTaskOptions { /**
     * The function to query a curseforge project downloadable url.
     */ queryFileUrl ? : CurseforgeURLQuery ; /**
     * Should it replace the override files if the file is existed.
     */ replaceExisted ? : boolean ; /**
     * Overload the manifest for this installation.
     * It will use this manifest instead of the read manifest from modpack zip to install.
     */ manifest ? : Manifest ; /**
     * The function to resolve the file path from url and other.
     *
     * By default this will install all the file
     */ filePathResolver ? : FilePathResolver ; } export interface InstallFileOptions extends DownloadBaseOptions { /**
     * The function to query a curseforge project downloadable url.
     */ queryFileUrl ? : CurseforgeURLQuery ; } type InputType = string | Buffer | { zip : ZipFile ; entries : Entry [ ] ; } ; export interface Manifest { manifestType : string ; manifestVersion : number ; minecraft : { /**
         * Minecraft version
         */ version : string ; libraries ? : string ; /**
         * Can be forge
         */ modLoaders : { id : string ; primary : boolean ; } [ ] ; } ; name : string ; version : string ; author : string ; files : { projectID : number ; fileID : number ; required : boolean ; } [ ] ; overrides : string ; } export interface File { projectID : number ; fileID : number ; } export class BadCurseforgeModpackError extends Error { error = "BadCurseforgeModpack" ; constructor ( public modpack , /**
     * What required entry is missing in modpack.
     */ public entry ) { super ( `Missing entry ${ entry } in curseforge modpack: ${ modpack }` ) ; } } throws BadCurseforgeModpackError export function readManifestTask ( input ) { return task ( "unpack" , async ( ) => { const zip = await normalizeInput ( input ) ; const mainfiestEntry = zip . entries . find ( ( e ) => e . fileName === "manifest.json" ) ; if ( ! mainfiestEntry ) { throw new BadCurseforgeModpackError ( input , "manifest.json" ) ; } const buffer = await readEntry ( zip . zip , mainfiestEntry ) ; const content = JSON . parse ( buffer . toString ( ) ) ; return content ; } ) ; } throws BadCurseforgeModpackError export function readManifest ( zip ) { return readManifestTask ( zip ) . startAndWait ( ) ; } export type FilePathResolver = ( projectId : number , fileId : number , minecraft : MinecraftFolder , url : string ) => string | Promise < string > ; export type CurseforgeURLQuery = ( projectId : number , fileId : number ) => Promise < string > ; export type CurseforgeFileTypeQuery = ( projectId : number ) => Promise < "mods" | "resourcepacks" > ; export function createDefaultCurseforgeQuery ( ) { let agent = new HttpsAgent ( ) ; return ( projectId , fileId ) => fetchText ( `https://addons-ecs.forgesvc.net/api/v2/addon/${ projectId }/file/${ fileId }/download-url` , { https : agent } ) ; } param zip param minecraft param options export function installCurseforgeModpack ( zip , minecraft , options ? ) { return installCurseforgeModpackTask ( zip , minecraft , options ) . startAndWait ( ) ; } async function normalizeInput ( input ) { if ( typeof input === "string" || input instanceof Buffer ) { const zip = await open ( input , { lazyEntries : true , autoClose : false } ) ; return { zip , entries : await readAllEntries ( zip ) } ; } else { return input ; } } param input param minecraft param options throws BadCurseforgeModpackError export function installCurseforgeModpackTask ( input , minecraft , options = { } ) { return task ( "installCurseforgeModpack" , async function ( ) { const folder = MinecraftFolder . from ( minecraft ) ; const zip = await normalizeInput ( input ) ; const manifest = options ?. manifest ?? ( await this . yield ( readManifestTask ( zip ) ) ) ; const requestor = options ?. queryFileUrl || createDefaultCurseforgeQuery ( ) ; const resolver = options ?. filePathResolver || ( ( p , f , m , u ) => m . getMod ( basename ( u ) ) ) ; await withAgents ( options , async ( options ) => { const tasks = await Promise . all ( manifest . files . map ( async ( f ) => { const from = await requestor ( f . projectID , f . fileID ) ; const to = await resolver ( f . projectID , f . fileID , folder , from ) ; return new DownloadTask ( { url : from , destination : to , agents : options . agents , segmentPolicy : options . segmentPolicy , retryHandler : options . retryHandler , } ) . setName ( "download" ) ; } ) ) ; await this . all ( tasks , { throwErrorImmediately : options . throwErrorImmediately ?? false , getErrorMessage : ( errs ) => `Fail to install curseforge modpack to ${ folder . root }: ${ errs . map ( errorToString ) . join ( "\n" ) }` } ) ; } ) ; await this . yield ( new UnzipTask ( zip . zip , zip . entries . filter ( ( e ) => ! e . fileName . endsWith ( "/" ) && e . fileName . startsWith ( manifest . overrides ) ) , folder . root , ( e ) => e . fileName . substring ( manifest . overrides . length ) ) . setName ( "unpack" ) ) ; return manifest ; } ) ; } /**
 * Install a cureseforge xml file to a specific locations
 */ export function installCurseforgeFile ( file , destination , options ? ) { return installCurseforgeFileTask ( file , destination , options ) . startAndWait ( ) ; } /**
 * Install a cureseforge xml file to a specific locations
 */ export function installCurseforgeFileTask ( file , destination , options = { } ) { return task ( "installCurseforgeFile" , async function ( ) { const requestor = options . queryFileUrl || createDefaultCurseforgeQuery ( ) ; const url = await requestor ( file . projectID , file . fileID ) ; await new DownloadTask ( { url , destination : join ( destination , basename ( url ) ) , agents : options . agents , segmentPolicy : options . segmentPolicy , retryHandler : options . retryHandler , } ) . startAndWait ( this . context , this . parent ) ; } ) ; }