/// <summary> /// Adds a file to the add/replace list of files to install. This will replace an existing file in the mapping if the destination path is the same. /// </summary> /// <param name="destRelativePath">Relative in-game path (from game root) to install file to.</param> /// <param name="sourceRelativePath">Relative (to mod root) path of new file to install</param> /// <param name="ignoreLoadErrors">Ignore checking if new file exists on disk</param> /// <param name="mod">Mod to parse against</param> /// <returns>string of failure reason. null if OK.</returns> internal string AddFileToInstall(string destRelativePath, string sourceRelativePath, Mod mod) { //Security check if (!checkExtension(sourceRelativePath, out string failReason)) { return(failReason); } string checkingSourceFile; if (JobDirectory != null) { checkingSourceFile = FilesystemInterposer.PathCombine(mod.IsInArchive, mod.ModPath, JobDirectory, sourceRelativePath); } else { //root (legacy) checkingSourceFile = FilesystemInterposer.PathCombine(mod.IsInArchive, mod.ModPath, sourceRelativePath); } if (!FilesystemInterposer.FileExists(checkingSourceFile, mod.Archive)) { return(M3L.GetString(M3L.string_interp_validation_modjob_replacementFileSpecifiedByJobDoesntExist, checkingSourceFile)); } FilesToInstall[destRelativePath.Replace('/', '\\').TrimStart('\\')] = sourceRelativePath.Replace('/', '\\'); return(null); }
/// <summary> /// Adds a file to the add/replace list of files to install. This will replace an existing file in the mapping if the destination path is the same. /// </summary> /// <param name="destRelativePath">Relative in-game path (from game root) to install file to.</param> /// <param name="sourceRelativePath">Relative (to mod root) path of new file to install</param> /// <param name="ignoreLoadErrors">Ignore checking if new file exists on disk</param> /// <param name="mod">Mod to parse against</param> /// <returns>string of failure reason. null if OK.</returns> internal string AddFileToInstall(string destRelativePath, string sourceRelativePath, Mod mod) { //Security check if (!checkExtension(sourceRelativePath, out string failReason)) { return(failReason); } string checkingSourceFile; if (JobDirectory != null) { checkingSourceFile = FilesystemInterposer.PathCombine(mod.IsInArchive, mod.ModPath, JobDirectory, sourceRelativePath); } else { //root (legacy) checkingSourceFile = FilesystemInterposer.PathCombine(mod.IsInArchive, mod.ModPath, sourceRelativePath); } if (!FilesystemInterposer.FileExists(checkingSourceFile, mod.Archive)) { return($"Failed to add replacement file to mod job: {checkingSourceFile} does not exist but is specified by the job"); } FilesToInstall[destRelativePath.Replace('/', '\\').TrimStart('\\')] = sourceRelativePath.Replace('/', '\\'); return(null); }
/// <summary> /// Gets all files referenced by this mod. This does not include moddessc.ini. /// </summary> /// <param name="archive">Archive, if this mod is in an archive.</param> /// <returns></returns> public List <string> GetAllRelativeReferences(SevenZipExtractor archive = null) { var references = new List <string>(); //references.Add("moddesc.ini"); //Moddesc is implicitly referenced by the mod. //Replace or Add references foreach (var job in InstallationJobs) { foreach (var jobFile in job.FilesToInstall.Values) { if (job.JobDirectory == @".") { references.Add(jobFile); } else { references.Add(job.JobDirectory + @"\" + jobFile); } } foreach (var dlc in job.AlternateDLCs) { if (dlc.HasRelativeFiles()) { var files = FilesystemInterposer.DirectoryGetFiles(FilesystemInterposer.PathCombine(IsInArchive, ModPath, dlc.AlternateDLCFolder), "*", SearchOption.AllDirectories, archive).Select(x => IsInArchive ? x : x.Substring(ModPath.Length + 1)).ToList(); references.AddRange(files); } } foreach (var file in job.AlternateFiles) { if (file.HasRelativeFile()) { if (IsInArchive) { references.Add(FilesystemInterposer.PathCombine(true, ModPath, file.AltFile)); } else { references.Add(file.AltFile); } } } foreach (var customDLCmapping in job.CustomDLCFolderMapping) { references.AddRange(FilesystemInterposer.DirectoryGetFiles(FilesystemInterposer.PathCombine(IsInArchive, ModPath, customDLCmapping.Key), "*", SearchOption.AllDirectories, archive).Select(x => IsInArchive ? x : x.Substring(ModPath.Length + 1)).ToList()); } } references.AddRange(AdditionalDeploymentFiles); foreach (var additionalDeploymentDir in AdditionalDeploymentFolders) { references.AddRange(FilesystemInterposer.DirectoryGetFiles(FilesystemInterposer.PathCombine(IsInArchive, ModPath, additionalDeploymentDir), "*", SearchOption.AllDirectories, archive).Select(x => IsInArchive ? x : x.Substring(ModPath.Length + 1)).ToList()); } return(references); }
/// <summary> /// Adds a file to the add/replace list of files to install. This will not replace an existing file in the mapping if the destination path is the same, it will instead throw an error. /// </summary> /// <param name="destRelativePath">Relative in-game path (from game root) to install file to.</param> /// <param name="sourceRelativePath">Relative (to mod root) path of new file to install</param> /// <param name="mod">Mod to parse against</param> /// <returns>string of failure reason. null if OK.</returns> internal string AddAdditionalFileToInstall(string destRelativePath, string sourceRelativePath, Mod mod) { //Security check if (!checkExtension(sourceRelativePath, out string failReason)) { return(failReason); } var checkingSourceFile = FilesystemInterposer.PathCombine(mod.IsInArchive, mod.ModPath, JobDirectory, sourceRelativePath); if (!FilesystemInterposer.FileExists(checkingSourceFile, mod.Archive)) { return(M3L.GetString(M3L.string_interp_validation_modjob_additionalFileSpecifiedByJobDoesntExist, checkingSourceFile)); } if (FilesToInstall.ContainsKey(destRelativePath)) { return(M3L.GetString(M3L.string_interp_validation_modjob_additionalFileAlreadyMarkedForModification, destRelativePath)); } FilesToInstall[destRelativePath.Replace('/', '\\').TrimStart('\\')] = sourceRelativePath.Replace('/', '\\'); return(null); }
/// <summary> /// Adds a file to the add/replace list of files to install. This will not replace an existing file in the mapping if the destination path is the same, it will instead throw an error. /// </summary> /// <param name="destRelativePath">Relative in-game path (from game root) to install file to.</param> /// <param name="sourceRelativePath">Relative (to mod root) path of new file to install</param> /// <param name="mod">Mod to parse against</param> /// <returns>string of failure reason. null if OK.</returns> internal string AddAdditionalFileToInstall(string destRelativePath, string sourceRelativePath, Mod mod) { //Security check if (!checkExtension(sourceRelativePath, out string failReason)) { return(failReason); } var checkingSourceFile = FilesystemInterposer.PathCombine(mod.IsInArchive, mod.ModPath, JobDirectory, sourceRelativePath); if (!FilesystemInterposer.FileExists(checkingSourceFile, mod.Archive)) { return($"Failed to add additional file to mod job: {checkingSourceFile} does not exist but is specified by the job"); } if (FilesToInstall.ContainsKey(destRelativePath)) { return($"Failed to add additional file to mod job: {destRelativePath} already is marked for modification. Files that are in the addfiles descriptor cannot overlap each other or replacement files."); } FilesToInstall[destRelativePath.Replace('/', '\\').TrimStart('\\')] = sourceRelativePath.Replace('/', '\\'); return(null); }
/// <summary> /// Gets all files referenced by this mod. This does not include moddessc.ini by default /// </summary> /// <param name="includeModdesc">Include moddesc.ini in the results</param> /// <param name="archive">Archive, if this mod is in an archive.</param> /// <returns></returns> public List <string> GetAllRelativeReferences(bool includeModdesc = false, SevenZipExtractor archive = null) { var references = new List <string>(); //references.Add("moddesc.ini"); //Moddesc is implicitly referenced by the mod. //Replace or Add references foreach (var job in InstallationJobs) { foreach (var jobFile in job.FilesToInstall.Values) { if (job.JobDirectory == @"." || job.JobDirectory == null) { references.Add(jobFile); } else { references.Add(job.JobDirectory + @"\" + jobFile); } } foreach (var dlc in job.AlternateDLCs) { if (dlc.HasRelativeFiles()) { if (dlc.AlternateDLCFolder != null) { var files = FilesystemInterposer.DirectoryGetFiles(FilesystemInterposer.PathCombine(IsInArchive, ModPath, dlc.AlternateDLCFolder), "*", SearchOption.AllDirectories, archive).Select(x => (IsInArchive && ModPath.Length == 0) ? x : x.Substring(ModPath.Length + 1)).ToList(); references.AddRange(files); } else if (dlc.MultiListSourceFiles != null) { foreach (var mf in dlc.MultiListSourceFiles) { var relpath = Path.Combine(ModPath, dlc.MultiListRootPath, mf).Substring(ModPath.Length + 1); references.Add(relpath); } } } } foreach (var file in job.AlternateFiles) { if (file.HasRelativeFile()) { if (file.AltFile != null) { //Commented out: AltFile should be direct path to file from mod root, we should only put in relative path //if (IsInArchive) //{ // references.Add(FilesystemInterposer.PathCombine(true, ModPath, file.AltFile)); //} //else //{ references.Add(file.AltFile); //} } else if (file.MultiListSourceFiles != null) { foreach (var mf in file.MultiListSourceFiles) { var relPath = FilesystemInterposer.PathCombine(IsInArchive, ModPath, file.MultiListRootPath, mf); //Should this be different from above AltFile? if (IsInArchive) { references.Add(relPath); } else { references.Add(relPath.Substring(ModPath.Length + 1)); } } } } } foreach (var customDLCmapping in job.CustomDLCFolderMapping) { references.AddRange(FilesystemInterposer.DirectoryGetFiles(FilesystemInterposer.PathCombine(IsInArchive, ModPath, customDLCmapping.Key), "*", SearchOption.AllDirectories, archive).Select(x => (IsInArchive && ModPath.Length == 0) ? x : x.Substring(ModPath.Length + 1)).ToList()); } } references.AddRange(AdditionalDeploymentFiles); foreach (var additionalDeploymentDir in AdditionalDeploymentFolders) { references.AddRange(FilesystemInterposer.DirectoryGetFiles(FilesystemInterposer.PathCombine(IsInArchive, ModPath, additionalDeploymentDir), "*", SearchOption.AllDirectories, archive).Select(x => (IsInArchive && ModPath.Length == 0) ? x : x.Substring(ModPath.Length + 1)).ToList()); } if (includeModdesc && GetJob(ModJob.JobHeader.ME2_RCWMOD) == null) { references.Add(ModDescPath.Substring(ModPath.Length).TrimStart('/', '\\')); //references.Add(ModDescPath.TrimStart('/', '\\')); } return(references.Distinct(StringComparer.InvariantCultureIgnoreCase).ToList()); }
public (Dictionary <ModJob, (Dictionary <string, string> unpackedJobMapping, List <string> dlcFoldersBeingInstalled)>, List <(ModJob job, string sfarPath, Dictionary <string, string>)>) GetInstallationQueues(GameTarget gameTarget) { if (IsInArchive) { Archive = new SevenZipExtractor(ArchivePath); //load archive file for inspection } var gameDLCPath = MEDirectories.DLCPath(gameTarget); var customDLCMapping = InstallationJobs.FirstOrDefault(x => x.Header == ModJob.JobHeader.CUSTOMDLC)?.CustomDLCFolderMapping; if (customDLCMapping != null) { //Make clone so original value is not modified customDLCMapping = new Dictionary <string, string>(customDLCMapping); //prevent altering the source object } var unpackedJobInstallationMapping = new Dictionary <ModJob, (Dictionary <string, string> mapping, List <string> dlcFoldersBeingInstalled)>(); var sfarInstallationJobs = new List <(ModJob job, string sfarPath, Dictionary <string, string> installationMapping)>(); foreach (var job in InstallationJobs) { Log.Information($"Preprocessing installation job: {job.Header}"); var alternateFiles = job.AlternateFiles.Where(x => x.IsSelected).ToList(); var alternateDLC = job.AlternateDLCs.Where(x => x.IsSelected).ToList(); if (job.Header == ModJob.JobHeader.CUSTOMDLC) { #region Installation: CustomDLC var installationMapping = new Dictionary <string, string>(); unpackedJobInstallationMapping[job] = (installationMapping, new List <string>()); foreach (var altdlc in alternateDLC) { if (altdlc.Operation == AlternateDLC.AltDLCOperation.OP_ADD_CUSTOMDLC) { customDLCMapping[altdlc.AlternateDLCFolder] = altdlc.DestinationDLCFolder; } } foreach (var mapping in customDLCMapping) { //Mapping is done as DESTINATIONFILE = SOURCEFILE so you can override keys var source = FilesystemInterposer.PathCombine(IsInArchive, ModPath, mapping.Key); var target = Path.Combine(gameDLCPath, mapping.Value); //get list of all normal files we will install //var allSourceDirFiles = Directory.GetFiles(source, "*", SearchOption.AllDirectories).Select(x => x.Substring(ModPath.Length)).ToList(); var allSourceDirFiles = FilesystemInterposer.DirectoryGetFiles(source, "*", SearchOption.AllDirectories, Archive).Select(x => x.Substring(ModPath.Length).TrimStart('\\')).ToList(); unpackedJobInstallationMapping[job].dlcFoldersBeingInstalled.Add(target); //loop over every file foreach (var sourceFile in allSourceDirFiles) { //Check against alt files bool altApplied = false; foreach (var altFile in alternateFiles) { //todo: Support wildcards if OP_NOINSTALL if (altFile.ModFile.Equals(sourceFile, StringComparison.InvariantCultureIgnoreCase)) { //Alt applies to this file switch (altFile.Operation) { case AlternateFile.AltFileOperation.OP_NOINSTALL: CLog.Information($"Not installing {sourceFile} for Alternate File {altFile.FriendlyName} due to operation OP_NOINSTALL", Settings.LogModInstallation); altApplied = true; break; case AlternateFile.AltFileOperation.OP_SUBSTITUTE: CLog.Information($"Repointing {sourceFile} to {altFile.AltFile} for Alternate File {altFile.FriendlyName} due to operation OP_SUBSTITUTE", Settings.LogModInstallation); installationMapping[altFile.AltFile] = sourceFile; //use alternate file as key instead altApplied = true; break; } break; } } if (altApplied) { continue; //no further processing for file } var relativeDestStartIndex = sourceFile.IndexOf(mapping.Value); string destPath = sourceFile.Substring(relativeDestStartIndex); installationMapping[destPath] = sourceFile; //destination is mapped to source file that will replace it. } foreach (var altdlc in alternateDLC) { if (altdlc.Operation == AlternateDLC.AltDLCOperation.OP_ADD_FOLDERFILES_TO_CUSTOMDLC) { string alternatePathRoot = FilesystemInterposer.PathCombine(IsInArchive, ModPath, altdlc.AlternateDLCFolder); //Todo: Change to Filesystem Interposer to support installation from archives var filesToAdd = FilesystemInterposer.DirectoryGetFiles(alternatePathRoot, "*", SearchOption.AllDirectories, Archive).Select(x => x.Substring(ModPath.Length).TrimStart('\\')).ToList(); foreach (var fileToAdd in filesToAdd) { var destFile = Path.Combine(altdlc.DestinationDLCFolder, fileToAdd.Substring(altdlc.AlternateDLCFolder.Length).TrimStart('\\', '/')); CLog.Information($"Adding extra CustomDLC file ({fileToAdd} => {destFile}) due to Alternate DLC {altdlc.FriendlyName}'s {altdlc.Operation}", Settings.LogModInstallation); installationMapping[destFile] = fileToAdd; } } } //Log.Information($"Copying CustomDLC to target: {source} -> {target}"); //CopyDir.CopyFiles_ProgressBar(installatnionMapping, FileInstalledCallback); //Log.Information($"Installed CustomDLC {mapping.Value}"); } #endregion } else if (job.Header == ModJob.JobHeader.ME2_COALESCED) { } else if (job.Header == ModJob.JobHeader.BASEGAME || job.Header == ModJob.JobHeader.BALANCE_CHANGES || job.Header == ModJob.JobHeader.ME1_CONFIG) { #region Installation: BASEGAME, BALANCE CHANGES, ME1 CONFIG var installationMapping = new Dictionary <string, string>(); unpackedJobInstallationMapping[job] = (installationMapping, new List <string>()); buildInstallationQueue(job, installationMapping, false); #endregion } else if (Game == MEGame.ME3 && ModJob.ME3SupportedNonCustomDLCJobHeaders.Contains(job.Header)) //previous else if will catch BASEGAME { #region Installation: DLC Unpacked and SFAR (ME3 ONLY) if (MEDirectories.IsOfficialDLCInstalled(job.Header, gameTarget)) { Debug.WriteLine("Building installation queue for header: " + job.Header); string sfarPath = job.Header == ModJob.JobHeader.TESTPATCH ? Utilities.GetTestPatchPath(gameTarget) : Path.Combine(gameDLCPath, ModJob.GetHeadersToDLCNamesMap(MEGame.ME3)[job.Header], "CookedPCConsole", "Default.sfar"); if (File.Exists(sfarPath)) { var installationMapping = new Dictionary <string, string>(); if (new FileInfo(sfarPath).Length == 32) { //Unpacked unpackedJobInstallationMapping[job] = (installationMapping, new List <string>()); buildInstallationQueue(job, installationMapping, false); } else { //Packed //unpackedJobInstallationMapping[job] = installationMapping; buildInstallationQueue(job, installationMapping, true); sfarInstallationJobs.Add((job, sfarPath, installationMapping)); } } } else { Log.Warning($"DLC not installed, skipping: {job.Header}"); } #endregion } else if (Game == MEGame.ME2 || Game == MEGame.ME1) { #region Installation: DLC Unpacked (ME1/ME2 ONLY) //Unpacked if (MEDirectories.IsOfficialDLCInstalled(job.Header, gameTarget)) { var installationMapping = new Dictionary <string, string>(); unpackedJobInstallationMapping[job] = (installationMapping, new List <string>()); buildInstallationQueue(job, installationMapping, false); } else { Log.Warning($"DLC not installed, skipping: {job.Header}"); } #endregion } else { //?? Header throw new Exception("Unsupported installation job header! " + job.Header); } } return(unpackedJobInstallationMapping, sfarInstallationJobs); }
public void ExtractFromArchive(string archivePath, string outputFolderPath, bool compressPackages, Action <string> updateTextCallback = null, Action <DetailedProgressEventArgs> extractingCallback = null, Action <string, int, int> compressedPackageCallback = null, bool testRun = false) { if (!IsInArchive) { throw new Exception(@"Cannot extract a mod that is not part of an archive."); } compressPackages &= Game == MEGame.ME3; //ME3 ONLY FOR NOW var isExe = archivePath.EndsWith(@".exe", StringComparison.InvariantCultureIgnoreCase); var archiveFile = isExe ? new SevenZipExtractor(archivePath, InArchiveFormat.Nsis) : new SevenZipExtractor(archivePath); using (archiveFile) { var fileIndicesToExtract = new List <int>(); var referencedFiles = GetAllRelativeReferences(!IsVirtualized, archiveFile); if (isExe) { //remap to mod root. Not entirely sure if this needs to be done for sub mods? referencedFiles = referencedFiles.Select(x => FilesystemInterposer.PathCombine(IsInArchive, ModPath, x)).ToList(); //remap to in-archive paths so they match entry paths } foreach (var info in archiveFile.ArchiveFileData) { if (!info.IsDirectory && (ModPath == "" || info.FileName.Contains(ModPath))) { var relativedName = isExe ? info.FileName : info.FileName.Substring(ModPath.Length).TrimStart('\\'); if (referencedFiles.Contains(relativedName)) { Log.Information(@"Adding file to extraction list: " + info.FileName); fileIndicesToExtract.Add(info.Index); } } } #region old /* * bool fileAdded = false; * //moddesc.ini * if (info.FileName == ModDescPath) * { * //Debug.WriteLine("Add file to extraction list: " + info.FileName); * fileIndicesToExtract.Add(info.Index); * continue; * } * * //Check each job * foreach (ModJob job in InstallationJobs) * { * if (job.Header == ModJob.JobHeader.CUSTOMDLC) * { #region Extract Custom DLC * foreach (var localCustomDLCFolder in job.CustomDLCFolderMapping.Keys) * { * if (info.FileName.StartsWith(FilesystemInterposer.PathCombine(IsInArchive, ModPath, localCustomDLCFolder))) * { * //Debug.WriteLine("Add file to extraction list: " + info.FileName); * fileIndicesToExtract.Add(info.Index); * fileAdded = true; * break; * } * } * * if (fileAdded) break; * * //Alternate files * foreach (var alt in job.AlternateFiles) * { * if (alt.AltFile != null && info.FileName.Equals(FilesystemInterposer.PathCombine(IsInArchive, ModPath, alt.AltFile), StringComparison.InvariantCultureIgnoreCase)) * { * //Debug.WriteLine("Add alternate file to extraction list: " + info.FileName); * fileIndicesToExtract.Add(info.Index); * fileAdded = true; * break; * } * } * * if (fileAdded) break; * * //Alternate DLC * foreach (var alt in job.AlternateDLCs) * { * if (info.FileName.StartsWith(FilesystemInterposer.PathCombine(IsInArchive, ModPath, alt.AlternateDLCFolder), StringComparison.InvariantCultureIgnoreCase)) * { * //Debug.WriteLine("Add alternate dlc file to extraction list: " + info.FileName); * fileIndicesToExtract.Add(info.Index); * fileAdded = true; * break; * } * } * * if (fileAdded) break; * #endregion * } * else * { #region Official headers * * foreach (var inSubDirFile in job.FilesToInstall.Values) * { * var inArchivePath = FilesystemInterposer.PathCombine(IsInArchive, ModPath, job.JobDirectory, inSubDirFile); //keep relative if unpacked mod, otherwise use full in-archive path for extraction * if (info.FileName.Equals(inArchivePath, StringComparison.InvariantCultureIgnoreCase)) * { * //Debug.WriteLine("Add file to extraction list: " + info.FileName); * fileIndicesToExtract.Add(info.Index); * fileAdded = true; * break; * } * } * * if (fileAdded) break; * //Alternate files * foreach (var alt in job.AlternateFiles) * { * if (alt.AltFile != null && info.FileName.Equals(FilesystemInterposer.PathCombine(IsInArchive, ModPath, alt.AltFile), StringComparison.InvariantCultureIgnoreCase)) * { * //Debug.WriteLine("Add alternate file to extraction list: " + info.FileName); * fileIndicesToExtract.Add(info.Index); * fileAdded = true; * break; * } * } * * if (fileAdded) break; * #endregion * } * } * }*/ #endregion archiveFile.Progressing += (sender, args) => { extractingCallback?.Invoke(args); }; string outputFilePathMapping(ArchiveFileInfo entryInfo) { Log.Information(@"Mapping extraction target for " + entryInfo.FileName); string entryPath = entryInfo.FileName; if (ExeExtractionTransform != null && ExeExtractionTransform.PatchRedirects.Any(x => x.index == entryInfo.Index)) { Log.Information(@"Extracting vpatch file at index " + entryInfo.Index); return(Path.Combine(Utilities.GetVPatchRedirectsFolder(), ExeExtractionTransform.PatchRedirects.First(x => x.index == entryInfo.Index).outfile)); } if (ExeExtractionTransform != null && ExeExtractionTransform.NoExtractIndexes.Any(x => x == entryInfo.Index)) { Log.Information(@"Extracting file to trash (not used): " + entryPath); return(Path.Combine(Utilities.GetTempPath(), @"Trash", @"trashfile")); } if (ExeExtractionTransform != null && ExeExtractionTransform.AlternateRedirects.Any(x => x.index == entryInfo.Index)) { var outfile = ExeExtractionTransform.AlternateRedirects.First(x => x.index == entryInfo.Index).outfile; Log.Information($@"Extracting file with redirection: {entryPath} {outfile}"); return(Path.Combine(outputFolderPath, outfile)); } //Archive path might start with a \. Substring may return value that start with a \ var subModPath = entryPath /*.TrimStart('\\')*/.Substring(ModPath.Length).TrimStart('\\'); var path = Path.Combine(outputFolderPath, subModPath); //Debug.WriteLine("remapping output: " + entryPath + " -> " + path); return(path); } if (compressPackages) { compressionQueue = new BlockingCollection <string>(); } int numberOfPackagesToCompress = referencedFiles.Count(x => x.RepresentsPackageFilePath()); int compressedPackageCount = 0; NamedBackgroundWorker compressionThread; if (compressPackages) { compressionThread = new NamedBackgroundWorker(@"ImportingCompressionThread"); compressionThread.DoWork += (a, b) => { try { while (true) { var package = compressionQueue.Take(); //updateTextCallback?.Invoke(M3L.GetString(M3L.string_interp_compressingX, Path.GetFileName(package))); var p = MEPackageHandler.OpenMEPackage(package); //Check if any compressed textures. bool shouldNotCompress = false; foreach (var texture in p.Exports.Where(x => x.IsTexture())) { var storageType = Texture2D.GetTopMipStorageType(texture); shouldNotCompress |= storageType == ME3Explorer.Unreal.StorageTypes.pccLZO || storageType == ME3Explorer.Unreal.StorageTypes.pccZlib; if (!shouldNotCompress) { break; } } if (!shouldNotCompress) { compressedPackageCallback?.Invoke(M3L.GetString(M3L.string_interp_compressingX, Path.GetFileName(package)), compressedPackageCount, numberOfPackagesToCompress); Log.Information(@"Compressing package: " + package); p.save(true); } else { Log.Information(@"Not compressing package due to file containing compressed textures: " + package); } Interlocked.Increment(ref compressedPackageCount); compressedPackageCallback?.Invoke(M3L.GetString(M3L.string_interp_compressedX, Path.GetFileName(package)), compressedPackageCount, numberOfPackagesToCompress); } } catch (InvalidOperationException) { //Done. lock (compressionCompletedSignaler) { Monitor.Pulse(compressionCompletedSignaler); } } }; compressionThread.RunWorkerAsync(); } archiveFile.FileExtractionFinished += (sender, args) => { if (compressPackages) { var fToCompress = outputFilePathMapping(args.FileInfo); if (fToCompress.RepresentsPackageFilePath()) { //Debug.WriteLine("Adding to blocking queue"); compressionQueue.TryAdd(fToCompress); } } }; if (!testRun) { Log.Information(@"Extracting files..."); archiveFile.ExtractFiles(outputFolderPath, outputFilePathMapping, fileIndicesToExtract.ToArray()); } else { //test run mode if (fileIndicesToExtract.Count != referencedFiles.Count) { throw new Exception("The amount of referenced files does not match the amount of files that are going to be extracted!"); } } Log.Information(@"File extraction completed."); compressionQueue?.CompleteAdding(); if (compressPackages && numberOfPackagesToCompress > 0 && numberOfPackagesToCompress > compressedPackageCount) { Log.Information(@"Waiting for compression of packages to complete."); while (!compressionQueue.IsCompleted) { lock (compressionCompletedSignaler) { Monitor.Wait(compressionCompletedSignaler); } } Log.Information(@"Package compression has completed."); } ModPath = outputFolderPath; if (IsVirtualized) { var parser = new IniDataParser().Parse(VirtualizedIniText); parser[@"ModInfo"][@"modver"] = ModVersionString; //In event relay service resolved this if (!testRun) { File.WriteAllText(Path.Combine(ModPath, @"moddesc.ini"), parser.ToString()); } IsVirtualized = false; //no longer virtualized } if (ExeExtractionTransform != null) { if (ExeExtractionTransform.VPatches.Any()) { var vpat = Utilities.GetCachedExecutablePath(@"vpat.exe"); if (!testRun) { Utilities.ExtractInternalFile(@"MassEffectModManagerCore.modmanager.executables.vpat.exe", vpat, true); } //Handle VPatching foreach (var transform in ExeExtractionTransform.VPatches) { var patchfile = Path.Combine(Utilities.GetVPatchRedirectsFolder(), transform.patchfile); var inputfile = Path.Combine(ModPath, transform.inputfile); var outputfile = Path.Combine(ModPath, transform.outputfile); var args = $"\"{patchfile}\" \"{inputfile}\" \"{outputfile}\""; //do not localize if (!testRun) { Directory.CreateDirectory(Directory.GetParent(outputfile).FullName); //ensure output directory exists as vpatch will not make one. } Log.Information($@"VPatching file into alternate: {inputfile} to {outputfile}"); updateTextCallback?.Invoke(M3L.GetString(M3L.string_interp_vPatchingIntoAlternate, Path.GetFileName(inputfile))); if (!testRun) { Utilities.RunProcess(vpat, args, true, false, false, true); } } } //Handle copyfile foreach (var copyfile in ExeExtractionTransform.CopyFiles) { string srcfile = Path.Combine(ModPath, copyfile.inputfile); string destfile = Path.Combine(ModPath, copyfile.outputfile); Log.Information($@"Applying transform copyfile: {srcfile} -> {destfile}"); if (!testRun) { File.Copy(srcfile, destfile, true); } } if (ExeExtractionTransform.PostTransformModdesc != null) { //fetch online moddesc for this mod. Log.Information(@"Fetching post-transform third party moddesc."); var moddesc = OnlineContent.FetchThirdPartyModdesc(ExeExtractionTransform.PostTransformModdesc); if (!testRun) { File.WriteAllText(Path.Combine(ModPath, @"moddesc.ini"), moddesc); } } } //int packagesCompressed = 0; //if (compressPackages) //{ // var packages = Utilities.GetPackagesInDirectory(ModPath, true); // extractingCallback?.Invoke(new ProgressEventArgs((byte)(packagesCompressed * 100.0 / packages.Count), 0)); // foreach (var package in packages) // { // updateTextCallback?.Invoke(M3L.GetString(M3L.string_interp_compressingX, Path.GetFileName(package))); // Log.Information("Compressing package: " + package); // var p = MEPackageHandler.OpenMEPackage(package); // p.save(true); // packagesCompressed++; // extractingCallback?.Invoke(new ProgressEventArgs((byte)(packagesCompressed * 100.0 / packages.Count), 0)); // } //} } }
/// <summary> /// Builds the installation queues for the mod. Item 1 is the unpacked job mappings (per modjob) along with the list of custom dlc folders being installed. Item 2 is the list of modjobs, their sfar paths, and the list of source files to install for SFAR jobs. /// </summary> /// <param name="gameTarget"></param> /// <returns></returns> public (Dictionary <ModJob, (Dictionary <string, InstallSourceFile> unpackedJobMapping, List <string> dlcFoldersBeingInstalled)>, List <(ModJob job, string sfarPath, Dictionary <string, InstallSourceFile>)>) GetInstallationQueues(GameTarget gameTarget) { if (IsInArchive) { Archive = new SevenZipExtractor(ArchivePath); //load archive file for inspection } var gameDLCPath = MEDirectories.DLCPath(gameTarget); var customDLCMapping = InstallationJobs.FirstOrDefault(x => x.Header == ModJob.JobHeader.CUSTOMDLC)?.CustomDLCFolderMapping; if (customDLCMapping != null) { //Make clone so original value is not modified customDLCMapping = new Dictionary <string, string>(customDLCMapping); //prevent altering the source object } var unpackedJobInstallationMapping = new Dictionary <ModJob, (Dictionary <string, InstallSourceFile> mapping, List <string> dlcFoldersBeingInstalled)>(); var sfarInstallationJobs = new List <(ModJob job, string sfarPath, Dictionary <string, InstallSourceFile> installationMapping)>(); foreach (var job in InstallationJobs) { Log.Information($@"Preprocessing installation job: {job.Header}"); var alternateFiles = job.AlternateFiles.Where(x => x.IsSelected && x.Operation != AlternateFile.AltFileOperation.OP_NOTHING && x.Operation != AlternateFile.AltFileOperation.OP_NOINSTALL_MULTILISTFILES).ToList(); var alternateDLC = job.AlternateDLCs.Where(x => x.IsSelected).ToList(); if (job.Header == ModJob.JobHeader.CUSTOMDLC) { #region Installation: CustomDLC //Key = destination file, value = source file to install var installationMapping = new Dictionary <string, InstallSourceFile>(); unpackedJobInstallationMapping[job] = (installationMapping, new List <string>()); foreach (var altdlc in alternateDLC) { if (altdlc.Operation == AlternateDLC.AltDLCOperation.OP_ADD_CUSTOMDLC) { customDLCMapping[altdlc.AlternateDLCFolder] = altdlc.DestinationDLCFolder; } } foreach (var mapping in customDLCMapping) { //Mapping is done as DESTINATIONFILE = SOURCEFILE so you can override keys var source = FilesystemInterposer.PathCombine(IsInArchive, ModPath, mapping.Key); var target = Path.Combine(gameDLCPath, mapping.Value); //get list of all normal files we will install var allSourceDirFiles = FilesystemInterposer.DirectoryGetFiles(source, "*", SearchOption.AllDirectories, Archive).Select(x => x.Substring(ModPath.Length).TrimStart('\\')).ToList(); unpackedJobInstallationMapping[job].dlcFoldersBeingInstalled.Add(target); //loop over every file foreach (var sourceFile in allSourceDirFiles) { //Check against alt files bool altApplied = false; foreach (var altFile in alternateFiles) { if (altFile.ModFile.Equals(sourceFile, StringComparison.InvariantCultureIgnoreCase)) { //Alt applies to this file switch (altFile.Operation) { case AlternateFile.AltFileOperation.OP_NOINSTALL: CLog.Information($@"Not installing {sourceFile} for Alternate File {altFile.FriendlyName} due to operation OP_NOINSTALL", Settings.LogModInstallation); //we simply don't map as we just do a continue below. altApplied = true; break; case AlternateFile.AltFileOperation.OP_SUBSTITUTE: CLog.Information($@"Repointing {sourceFile} to {altFile.AltFile} for Alternate File {altFile.FriendlyName} due to operation OP_SUBSTITUTE", Settings.LogModInstallation); if (job.JobDirectory != null && altFile.AltFile.StartsWith(job.JobDirectory)) { installationMapping[sourceFile] = new InstallSourceFile(altFile.AltFile.Substring(job.JobDirectory.Length).TrimStart('/', '\\')) { AltApplied = true, IsFullRelativeFilePath = true }; //use alternate file as key instead } else { installationMapping[sourceFile] = new InstallSourceFile(altFile.AltFile) { AltApplied = true, IsFullRelativeFilePath = true }; //use alternate file as key instead } altApplied = true; break; case AlternateFile.AltFileOperation.OP_INSTALL: //same logic as substitute, just different logging. CLog.Information($@"Adding {sourceFile} to install (from {altFile.AltFile}) as part of Alternate File {altFile.FriendlyName} due to operation OP_INSTALL", Settings.LogModInstallation); if (job.JobDirectory != null && altFile.AltFile.StartsWith(job.JobDirectory)) { installationMapping[sourceFile] = new InstallSourceFile(altFile.AltFile.Substring(job.JobDirectory.Length).TrimStart('/', '\\')) { AltApplied = true, IsFullRelativeFilePath = true }; //use alternate file as key instead } else { installationMapping[sourceFile] = new InstallSourceFile(altFile.AltFile) { AltApplied = true, IsFullRelativeFilePath = true }; //use alternate file as key instead } altApplied = true; break; } break; } } if (altApplied) { continue; //no further processing for file } var relativeDestStartIndex = sourceFile.IndexOf(mapping.Value); string destPath = sourceFile.Substring(relativeDestStartIndex); installationMapping[destPath] = new InstallSourceFile(sourceFile); //destination is mapped to source file that will replace it. } foreach (var altdlc in alternateDLC) { if (altdlc.Operation == AlternateDLC.AltDLCOperation.OP_ADD_FOLDERFILES_TO_CUSTOMDLC) { string alternatePathRoot = FilesystemInterposer.PathCombine(IsInArchive, ModPath, altdlc.AlternateDLCFolder); var filesToAdd = FilesystemInterposer.DirectoryGetFiles(alternatePathRoot, "*", SearchOption.AllDirectories, Archive).Select(x => x.Substring(ModPath.Length).TrimStart('\\')).ToList(); foreach (var fileToAdd in filesToAdd) { var destFile = Path.Combine(altdlc.DestinationDLCFolder, fileToAdd.Substring(altdlc.AlternateDLCFolder.Length).TrimStart('\\', '/')); CLog.Information($@"Adding extra CustomDLC file ({fileToAdd} => {destFile}) due to Alternate DLC {altdlc.FriendlyName}'s {altdlc.Operation}", Settings.LogModInstallation); installationMapping[destFile] = new InstallSourceFile(fileToAdd) { AltApplied = true }; } } else if (altdlc.Operation == AlternateDLC.AltDLCOperation.OP_ADD_MULTILISTFILES_TO_CUSTOMDLC) { string alternatePathRoot = FilesystemInterposer.PathCombine(IsInArchive, ModPath, altdlc.MultiListRootPath); foreach (var fileToAdd in altdlc.MultiListSourceFiles) { var sourceFile = FilesystemInterposer.PathCombine(IsInArchive, alternatePathRoot, fileToAdd).Substring(ModPath.Length).TrimStart('\\'); var destFile = Path.Combine(altdlc.DestinationDLCFolder, fileToAdd.TrimStart('\\', '/')); CLog.Information($@"Adding extra CustomDLC file (MultiList) ({sourceFile} => {destFile}) due to Alternate DLC {altdlc.FriendlyName}'s {altdlc.Operation}", Settings.LogModInstallation); installationMapping[destFile] = new InstallSourceFile(sourceFile) { AltApplied = true }; } } } // Process altfile removal of multilist, since it should be done last var fileRemoveAltFiles = job.AlternateFiles.Where(x => x.IsSelected && x.Operation == AlternateFile.AltFileOperation.OP_NOINSTALL_MULTILISTFILES); foreach (var altFile in fileRemoveAltFiles) { foreach (var multifile in altFile.MultiListSourceFiles) { CLog.Information($@"Attempting to remove multilist file {multifile} from install (from {altFile.MultiListTargetPath}) as part of Alternate File {altFile.FriendlyName} due to operation OP_NOINSTALL_MULTILISTFILES", Settings.LogModInstallation); string relativeSourcePath = altFile.MultiListRootPath + '\\' + multifile; var targetPath = altFile.MultiListTargetPath + '\\' + multifile; if (installationMapping.Remove(targetPath)) { CLog.Information($@" > Removed multilist file {targetPath} from installation", Settings.LogModInstallation); } } } } #endregion } else if (job.Header == ModJob.JobHeader.LOCALIZATION) { #region Installation: LOCALIZATION var installationMapping = new CaseInsensitiveDictionary <InstallSourceFile>(); unpackedJobInstallationMapping[job] = (installationMapping, new List <string>()); buildInstallationQueue(job, installationMapping, false); #endregion } else if (job.Header == ModJob.JobHeader.BASEGAME || job.Header == ModJob.JobHeader.BALANCE_CHANGES || job.Header == ModJob.JobHeader.ME1_CONFIG) { #region Installation: BASEGAME, BALANCE CHANGES, ME1 CONFIG var installationMapping = new CaseInsensitiveDictionary <InstallSourceFile>(); unpackedJobInstallationMapping[job] = (installationMapping, new List <string>()); buildInstallationQueue(job, installationMapping, false); #endregion } else if (Game == MEGame.ME3 && ModJob.ME3SupportedNonCustomDLCJobHeaders.Contains(job.Header)) //previous else if will catch BASEGAME { #region Installation: DLC Unpacked and SFAR (ME3 ONLY) if (MEDirectories.IsOfficialDLCInstalled(job.Header, gameTarget)) { string sfarPath = job.Header == ModJob.JobHeader.TESTPATCH ? ME3Directory.GetTestPatchPath(gameTarget) : Path.Combine(gameDLCPath, ModJob.GetHeadersToDLCNamesMap(MEGame.ME3)[job.Header], @"CookedPCConsole", @"Default.sfar"); if (File.Exists(sfarPath)) { var installationMapping = new CaseInsensitiveDictionary <InstallSourceFile>(); if (new FileInfo(sfarPath).Length == 32) { //Unpacked unpackedJobInstallationMapping[job] = (installationMapping, new List <string>()); buildInstallationQueue(job, installationMapping, false); } else { //Packed //unpackedJobInstallationMapping[job] = installationMapping; buildInstallationQueue(job, installationMapping, true); sfarInstallationJobs.Add((job, sfarPath, installationMapping)); } } } else { Log.Warning($@"DLC not installed, skipping: {job.Header}"); } #endregion } else if (Game == MEGame.ME2 || Game == MEGame.ME1) { #region Installation: DLC Unpacked (ME1/ME2 ONLY) //Unpacked if (MEDirectories.IsOfficialDLCInstalled(job.Header, gameTarget)) { var installationMapping = new CaseInsensitiveDictionary <InstallSourceFile>(); unpackedJobInstallationMapping[job] = (installationMapping, new List <string>()); buildInstallationQueue(job, installationMapping, false); } else { Log.Warning($@"DLC not installed, skipping: {job.Header}"); } #endregion } else { //?? Header throw new Exception(@"Unsupported installation job header! " + job.Header); } } return(unpackedJobInstallationMapping, sfarInstallationJobs); }