import unzip from "../archive/Unzipper" ; import { createWriteStream , unlinkSync , removeSync , readFileSync , writeFileSync } from "fs-extra" ; import { mkdirsSync } from "fs-extra" ; import { join as joinPath , sep as pathSep } from "path" ; import { inferMapper } from "./ModNormaliser" ; import ArchiveConverter from "../archive/ArchiveConverter" ; import { app } from "electron" ; import { randomBytes } from "crypto" ; import Logger from "../utils/Logger" ; import { copyFile } from "fs" ; import SpecialCaseManager from "./SpecialCaseManager" ; export default class ModInstaller { param modPath param installPath param specialCaseManager public static async installMod ( modPath , installPath , specialCaseManager ) { const specialCases = await specialCaseManager . get ( modPath ) ; specialCases . filter ( sc => sc . time === "preinstall" ) . forEach ( sc => { ModInstaller . handleSpecialCaseAction ( sc . action , sc . parameter , installPath ) ; } ) ; if ( modPath . endsWith ( ".zip" ) ) { await ModInstaller . installZip ( modPath , installPath ) ; } else if ( modPath . endsWith ( ".rpa" ) ) { await ModInstaller . installRPA ( modPath , installPath ) ; } else if ( ModInstaller . isArchive ( modPath ) ) { await new Promise ( ( ff , rj ) => { const tempZipPath = joinPath ( app . getPath ( "temp" ) , "ddmm" + randomBytes ( 8 ) . toString ( "hex" ) + ".zip" ) ; ArchiveConverter . convertToZip ( modPath , tempZipPath ) . then ( ( ) => { Logger . debug ( "Mod Installer" , tempZipPath ) ; ModInstaller . installZip ( tempZipPath , installPath ) . then ( ( ) => { removeSync ( tempZipPath ) ; ff ( ) ; } ) . catch ( e => { rj ( e ) ; } ) ; } ) . catch ( e => { rj ( e ) ; } ) ; } ) ; } else { return new Promise ( ( ff , rj ) => { rj ( new Error ( "File was not an archive." ) ) ; } ) ; } specialCases . filter ( sc => sc . time === "postinstall" ) . forEach ( sc => { ModInstaller . handleSpecialCaseAction ( sc . action , sc . parameter , installPath ) ; } ) ; } private static installRPA ( modPath , installPath ) { return new Promise ( ( ff , rj ) => { Logger . info ( "Mod Installer" , "Preparing to install RPA from " + modPath ) ; copyFile ( modPath , joinPath ( installPath , "game" , "scripts.rpa" ) , err => { if ( err ) { rj ( err ) ; } else { Logger . info ( "Mod Installer" , "RPA installaton complete" ) ; ff ( ) ; } } ) ; } ) ; } private static installZip ( modPath , installPath ) { return new Promise ( ( ff , rj ) => { let hasReadMetadata = false ; let hasReadBG = false ; Logger . info ( "Mod Installer" , "Preparing to install mod from " + modPath ) ; inferMapper ( modPath ) . then ( ( mapper ) => { for ( const file of mapper . getFilesToDelete ( ) ) { Logger . info ( "Mod Installer" , "Deleting file " + file ) ; unlinkSync ( joinPath ( installPath , "game" , file ) ) ; } const zip = unzip ( modPath ) ; Logger . info ( "Mod Installer" , "Using mapper " + mapper . getFriendlyName ( ) ) ; zip . on ( "file" , ( file ) => { if ( file . path . endsWith ( "ddmm-mod.json" ) ) { if ( hasReadMetadata ) { Logger . warn ( "Mod Installer" , "More than one ddmm-mod.json was found. Skipping this one." ) ; return ; } Logger . info ( "Mod Installer" , "Writing metadata to install.json" ) ; file . openStream ( ( err , stream ) => { if ( err ) { rj ( err ) ; } let fileContents = "" ; stream . on ( "data" , chunk => { fileContents += chunk . toString ( ) ; } ) ; stream . on ( "end" , ( ) => { hasReadMetadata = true ; try { const modDataPath = joinPath ( installPath , "../install.json" ) ; const oldInstallContents = JSON . parse ( readFileSync ( modDataPath ) . toString ( ) ) ; oldInstallContents . mod = JSON . parse ( fileContents ) ; writeFileSync ( modDataPath , JSON . stringify ( oldInstallContents ) ) ; } catch ( err ) { Logger . warn ( "Mod Installer" , "Failed to update install.json" ) ; console . warn ( err ) ; rj ( err ) ; } } ) ; } ) ; return ; } else if ( file . path . endsWith ( "ddmm-bg.png" ) ) { if ( hasReadBG ) { Logger . warn ( "Mod Installer" , "More than one ddmm-bg.png was found. Skipping this one." ) ; return ; } Logger . info ( "Mod Installer" , "Copying ddmm-bg.png to install directory" ) ; const bgPath = joinPath ( installPath , "../ddmm-bg.png" ) ; file . openStream ( ( err , stream ) => { hasReadBG = true ; if ( err ) { rj ( err ) ; } stream . pipe ( createWriteStream ( bgPath ) ) ; } ) ; return ; } const newPath = mapper . mapFile ( file . path ) ; if ( ! newPath ) { return ; } let newPathFull ; if ( process . platform === "darwin" ) { newPathFull = joinPath ( installPath , "Resources" , "autorun" , newPath ) ; } else { newPathFull = joinPath ( installPath , newPath ) ; } Logger . debug ( "Mod Installer" , file . path + " > " + newPathFull ) ; const newPathParts = newPathFull . split ( pathSep ) ; newPathParts . pop ( ) ; mkdirsSync ( newPathParts . join ( pathSep ) ) ; file . openStream ( ( err , stream ) => { if ( err ) { rj ( err ) ; } stream . pipe ( createWriteStream ( newPathFull ) ) ; } ) ; } ) ; zip . on ( "close" , ( ) => { try { const modDataPath = joinPath ( installPath , "../install.json" ) ; const oldInstallContents = JSON . parse ( readFileSync ( modDataPath ) . toString ( ) ) ; oldInstallContents . modded = true ; writeFileSync ( modDataPath , JSON . stringify ( oldInstallContents ) ) ; } catch ( err ) { Logger . warn ( "Mod Installer" , "Failed to update install.json" ) ; console . warn ( err ) ; rj ( err ) ; } ff ( ) ; Logger . info ( "Mod Installer" , "Installation complete" ) ; } ) ; zip . on ( "error" , ( e ) => { Logger . warn ( "Mod Installer" , "Encountered an error while extracting archive" ) ; console . warn ( e ) ; rj ( e ) ; } ) ; } ) . catch ( err => { Logger . error ( "Mod Installer" , "Failed to pick a mapper. Something is seriously wrong." ) ; console . warn ( err ) ; rj ( err ) ; } ) ; } ) ; } private static handleSpecialCaseAction ( action , parameter , installPath ) { Logger . debug ( "Special Case Action" , `Action: ${ action } Parameter: ${ parameter } Install: ${ installPath }` ) ; switch ( action ) { case "delete" : Logger . debug ( "Special Case Action" , "Deleting " + parameter ) ; const toDelete = joinPath ( installPath , parameter ) ; removeSync ( toDelete ) ; break ; } } private static isArchive ( filename ) { return [ "zip" , "rar" , "7z" ] . filter ( ext => filename . endsWith ( "." + ext ) ) . length > 0 ; } }