private void InstallModBackgroundThread(object sender, DoWorkEventArgs e) { Log.Information(@"Mod Installer Background thread starting"); var installationJobs = ModBeingInstalled.InstallationJobs; var gameDLCPath = MEDirectories.DLCPath(gameTarget); Directory.CreateDirectory(gameDLCPath); //me1/me2 missing dlc might not have this folder //Check we can install var missingRequiredDLC = ModBeingInstalled.ValidateRequiredModulesAreInstalled(gameTarget); if (missingRequiredDLC.Count > 0) { e.Result = (ModInstallCompletedStatus.INSTALL_FAILED_REQUIRED_DLC_MISSING, missingRequiredDLC); return; } //Check/warn on official headers if (!PrecheckHeaders(installationJobs)) { e.Result = ModInstallCompletedStatus.INSTALL_FAILED_USER_CANCELED_MISSING_MODULES; return; } //todo: If statment on this Utilities.InstallBinkBypass(gameTarget); //Always install binkw32, don't bother checking if it is already ASI version. if (ModBeingInstalled.Game == Mod.MEGame.ME2 && ModBeingInstalled.GetJob(ModJob.JobHeader.ME2_RCWMOD) != null && installationJobs.Count == 1) { e.Result = InstallAttachedRCWMod(); return; } //Prepare queues (Dictionary <ModJob, (Dictionary <string, string> fileMapping, List <string> dlcFoldersBeingInstalled)> unpackedJobMappings, List <(ModJob job, string sfarPath, Dictionary <string, string> sfarInstallationMapping)> sfarJobs)installationQueues = ModBeingInstalled.GetInstallationQueues(gameTarget); var readOnlyTargets = ModBeingInstalled.GetAllRelativeReadonlyTargets(me1ConfigReadOnlyOption.IsSelected); if (gameTarget.ALOTInstalled) { //Check if any packages are being installed. If there are, we will block this installation. bool installsPackageFile = false; foreach (var jobMappings in installationQueues.unpackedJobMappings) { installsPackageFile |= jobMappings.Value.fileMapping.Keys.Any(x => x.EndsWith(@".pcc", StringComparison.InvariantCultureIgnoreCase)); installsPackageFile |= jobMappings.Value.fileMapping.Keys.Any(x => x.EndsWith(@".u", StringComparison.InvariantCultureIgnoreCase)); installsPackageFile |= jobMappings.Value.fileMapping.Keys.Any(x => x.EndsWith(@".upk", StringComparison.InvariantCultureIgnoreCase)); installsPackageFile |= jobMappings.Value.fileMapping.Keys.Any(x => x.EndsWith(@".sfm", StringComparison.InvariantCultureIgnoreCase)); } foreach (var jobMappings in installationQueues.sfarJobs) { installsPackageFile |= jobMappings.sfarInstallationMapping.Keys.Any(x => x.EndsWith(@".pcc", StringComparison.InvariantCultureIgnoreCase)); installsPackageFile |= jobMappings.sfarInstallationMapping.Keys.Any(x => x.EndsWith(@".u", StringComparison.InvariantCultureIgnoreCase)); installsPackageFile |= jobMappings.sfarInstallationMapping.Keys.Any(x => x.EndsWith(@".upk", StringComparison.InvariantCultureIgnoreCase)); installsPackageFile |= jobMappings.sfarInstallationMapping.Keys.Any(x => x.EndsWith(@".sfm", StringComparison.InvariantCultureIgnoreCase)); } if (installsPackageFile) { if (Settings.DeveloperMode) { Log.Warning(@"ALOT is installed and user is attempting to install a mod (in developer mode). Prompting user to cancel installation"); bool cancel = false; Application.Current.Dispatcher.Invoke(delegate { var res = M3L.ShowDialog(Window.GetWindow(this), M3L.GetString(M3L.string_interp_devModeAlotInstalledWarning, ModBeingInstalled.ModName), M3L.GetString(M3L.string_brokenTexturesWarning), MessageBoxButton.YesNo, MessageBoxImage.Error, MessageBoxResult.No); cancel = res == MessageBoxResult.No; }); if (cancel) { e.Result = ModInstallCompletedStatus.USER_CANCELED_INSTALLATION; return; } Log.Warning(@"User installing mod anyways even with ALOT installed"); } else { Log.Error(@"ALOT is installed. Installing mods that install package files after installing ALOT is not permitted."); //ALOT Installed, this is attempting to install a package file e.Result = ModInstallCompletedStatus.INSTALL_FAILED_ALOT_BLOCKING; return; } } } Action = M3L.GetString(M3L.string_installing); PercentVisibility = Visibility.Visible; Percent = 0; int numdone = 0; //Calculate number of installation tasks beforehand int numFilesToInstall = installationQueues.unpackedJobMappings.Select(x => x.Value.fileMapping.Count).Sum(); numFilesToInstall += installationQueues.sfarJobs.Select(x => x.sfarInstallationMapping.Count).Sum() * (ModBeingInstalled.IsInArchive ? 2 : 1); //*2 as we have to extract and install Debug.WriteLine(@"Number of expected installation tasks: " + numFilesToInstall); void FileInstalledCallback(string target) { numdone++; Debug.WriteLine(@"Installed: " + target); Action = M3L.GetString(M3L.string_installing); var now = DateTime.Now; if (numdone > numFilesToInstall) { Debug.WriteLine($@"Percentage calculated is wrong. Done: {numdone} NumToDoTotal: {numFilesToInstall}"); } if ((now - lastPercentUpdateTime).Milliseconds > PERCENT_REFRESH_COOLDOWN) { //Don't update UI too often. Once per second is enough. Percent = (int)(numdone * 100.0 / numFilesToInstall); lastPercentUpdateTime = now; } } //Stage: Unpacked files build map Dictionary <string, string> fullPathMappingDisk = new Dictionary <string, string>(); Dictionary <int, string> fullPathMappingArchive = new Dictionary <int, string>(); SortedSet <string> customDLCsBeingInstalled = new SortedSet <string>(); List <string> mappedReadOnlyTargets = new List <string>(); foreach (var unpackedQueue in installationQueues.unpackedJobMappings) { foreach (var originalMapping in unpackedQueue.Value.fileMapping) { //always unpacked //if (unpackedQueue.Key == ModJob.JobHeader.CUSTOMDLC || unpackedQueue.Key == ModJob.JobHeader.BALANCE_CHANGES || unpackedQueue.Key == ModJob.JobHeader.BASEGAME) //{ //Resolve source file path string sourceFile; if (unpackedQueue.Key.JobDirectory == null) { sourceFile = FilesystemInterposer.PathCombine(ModBeingInstalled.IsInArchive, ModBeingInstalled.ModPath, originalMapping.Value); } else { sourceFile = FilesystemInterposer.PathCombine(ModBeingInstalled.IsInArchive, ModBeingInstalled.ModPath, unpackedQueue.Key.JobDirectory, originalMapping.Value); } if (unpackedQueue.Key.Header == ModJob.JobHeader.ME1_CONFIG) { var destFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), @"BioWare", @"Mass Effect", @"Config", originalMapping.Key); if (ModBeingInstalled.IsInArchive) { int archiveIndex = ModBeingInstalled.Archive.ArchiveFileNames.IndexOf(sourceFile, StringComparer.InvariantCultureIgnoreCase); fullPathMappingArchive[archiveIndex] = destFile; //used for extraction indexing if (archiveIndex == -1) { Log.Error($@"Archive Index is -1 for file {sourceFile}. This will probably throw an exception!"); Debugger.Break(); } fullPathMappingDisk[sourceFile] = destFile; //used for redirection } else { fullPathMappingDisk[sourceFile] = destFile; } } else { var destFile = Path.Combine(unpackedQueue.Key.Header == ModJob.JobHeader.CUSTOMDLC ? MEDirectories.DLCPath(gameTarget) : gameTarget.TargetPath, originalMapping.Key); //official //Extract Custom DLC name if (unpackedQueue.Key.Header == ModJob.JobHeader.CUSTOMDLC) { var custDLC = destFile.Substring(gameDLCPath.Length, destFile.Length - gameDLCPath.Length).TrimStart('\\', '/'); var nextSlashIndex = custDLC.IndexOf('\\'); if (nextSlashIndex == -1) { nextSlashIndex = custDLC.IndexOf('/'); } if (nextSlashIndex != -1) { custDLC = custDLC.Substring(0, nextSlashIndex); customDLCsBeingInstalled.Add(custDLC); } } if (ModBeingInstalled.IsInArchive) { int archiveIndex = ModBeingInstalled.Archive.ArchiveFileNames.IndexOf(sourceFile, StringComparer.InvariantCultureIgnoreCase); fullPathMappingArchive[archiveIndex] = destFile; //used for extraction indexing if (archiveIndex == -1) { Log.Error($@"Archive Index is -1 for file {sourceFile}. This will probably throw an exception!"); Debugger.Break(); } } fullPathMappingDisk[sourceFile] = destFile; //archive also uses this for redirection } if (readOnlyTargets.Contains(originalMapping.Key)) { CLog.Information(@"Adding resolved read only target: " + originalMapping.Key + @" -> " + fullPathMappingDisk[sourceFile], Settings.LogModInstallation); mappedReadOnlyTargets.Add(fullPathMappingDisk[sourceFile]); } //} } } //Substage: Add SFAR staging targets string sfarStagingDirectory = (ModBeingInstalled.IsInArchive && installationQueues.sfarJobs.Count > 0) ? Directory.CreateDirectory(Path.Combine(Utilities.GetTempPath(), @"SFARJobStaging")).FullName : null; //don't make directory if we don't need one if (sfarStagingDirectory != null) { foreach (var sfarJob in installationQueues.sfarJobs) { foreach (var fileToInstall in sfarJob.sfarInstallationMapping) { string sourceFile = FilesystemInterposer.PathCombine(ModBeingInstalled.IsInArchive, ModBeingInstalled.ModPath, sfarJob.job.JobDirectory, fileToInstall.Value); int archiveIndex = ModBeingInstalled.Archive.ArchiveFileNames.IndexOf(sourceFile, StringComparer.InvariantCultureIgnoreCase); if (archiveIndex == -1) { Log.Error($@"Archive Index is -1 for file {sourceFile}. This will probably throw an exception!"); Debugger.Break(); } string destFile = Path.Combine(sfarStagingDirectory, sfarJob.job.JobDirectory, fileToInstall.Value); fullPathMappingArchive[archiveIndex] = destFile; //used for extraction indexing fullPathMappingDisk[sourceFile] = destFile; //used for redirection Debug.WriteLine($@"SFAR Disk Staging: {fileToInstall.Key} => {destFile}"); } } } //Check we have enough disk space long requiredSpaceToInstall = 0L; if (ModBeingInstalled.IsInArchive) { foreach (var f in ModBeingInstalled.Archive.ArchiveFileData) { if (fullPathMappingArchive.ContainsKey(f.Index)) { //we are installing this file requiredSpaceToInstall += (long)f.Size; } } } else { foreach (var file in fullPathMappingDisk) { requiredSpaceToInstall += new FileInfo(file.Key).Length; } } Utilities.DriveFreeBytes(gameTarget.TargetPath, out var freeSpaceOnTargetDisk); requiredSpaceToInstall = (long)(requiredSpaceToInstall * 1.05); //+5% for some overhead if (requiredSpaceToInstall > (long)freeSpaceOnTargetDisk && freeSpaceOnTargetDisk != 0) { string driveletter = Path.GetPathRoot(gameTarget.TargetPath); Log.Error($@"Insufficient disk space to install mod. Required: {ByteSize.FromBytes(requiredSpaceToInstall)}, available on {driveletter}: {ByteSize.FromBytes(freeSpaceOnTargetDisk)}"); Application.Current.Dispatcher.Invoke(() => { string message = M3L.GetString(M3L.string_interp_dialogNotEnoughSpaceToInstall, driveletter, ModBeingInstalled.ModName, ByteSize.FromBytes(requiredSpaceToInstall).ToString(), ByteSize.FromBytes(freeSpaceOnTargetDisk).ToString()); Xceed.Wpf.Toolkit.MessageBox.Show(window, message, M3L.GetString(M3L.string_insufficientDiskSpace), MessageBoxButton.OK, MessageBoxImage.Error); }); e.Result = ModInstallCompletedStatus.INSTALL_ABORTED_NOT_ENOUGH_SPACE; return; } //Delete existing custom DLC mods with same name foreach (var cdbi in customDLCsBeingInstalled) { var path = Path.Combine(gameDLCPath, cdbi); if (Directory.Exists(path)) { Log.Information($@"Deleting existing DLC directory: {path}"); Utilities.DeleteFilesAndFoldersRecursively(path); } } //Stage: Unpacked files installation if (!ModBeingInstalled.IsInArchive) { //Direct copy Log.Information($@"Installing {fullPathMappingDisk.Count} unpacked files into game directory"); CopyDir.CopyFiles_ProgressBar(fullPathMappingDisk, FileInstalledCallback); } else { Action = M3L.GetString(M3L.string_loadingModArchive); //Extraction to destination string installationRedirectCallback(ArchiveFileInfo info) { var inArchivePath = info.FileName; var redirectedPath = fullPathMappingDisk[inArchivePath]; Debug.WriteLine($@"Redirecting {inArchivePath} to {redirectedPath}"); return(redirectedPath); } ModBeingInstalled.Archive.FileExtractionStarted += (sender, args) => { //CLog.Information("Extracting mod file for installation: " + args.FileInfo.FileName, Settings.LogModInstallation); }; List <string> filesInstalled = new List <string>(); List <string> filesToInstall = installationQueues.unpackedJobMappings.SelectMany(x => x.Value.fileMapping.Keys).ToList(); ModBeingInstalled.Archive.FileExtractionFinished += (sender, args) => { if (args.FileInfo.IsDirectory) { return; //ignore } if (!fullPathMappingArchive.ContainsKey(args.FileInfo.Index)) { return; //archive extracted this file (in memory) but did not do anything with this file (7z) } FileInstalledCallback(args.FileInfo.FileName); filesInstalled.Add(args.FileInfo.FileName); //Debug.WriteLine($"{args.FileInfo.FileName} as file { numdone}"); //Debug.WriteLine(numdone); }; ModBeingInstalled.Archive.ExtractFiles(gameTarget.TargetPath, installationRedirectCallback, fullPathMappingArchive.Keys.ToArray()); //directory parameter shouldn't be used here as we will be redirecting everything } //Write MetaCMM List <string> addedDLCFolders = new List <string>(); foreach (var v in installationQueues.unpackedJobMappings) { addedDLCFolders.AddRange(v.Value.dlcFoldersBeingInstalled); } foreach (var addedDLCFolder in addedDLCFolders) { var metacmm = Path.Combine(addedDLCFolder, @"_metacmm.txt"); ModBeingInstalled.HumanReadableCustomDLCNames.TryGetValue(Path.GetFileName(addedDLCFolder), out var assignedDLCName); string contents = $"{assignedDLCName ?? ModBeingInstalled.ModName}\n{ModBeingInstalled.ModVersionString}\n{App.BuildNumber}\n{Guid.NewGuid().ToString()}"; //Do not localize File.WriteAllText(metacmm, contents); } //Stage: SFAR Installation foreach (var sfarJob in installationQueues.sfarJobs) { InstallIntoSFAR(sfarJob, ModBeingInstalled, FileInstalledCallback, ModBeingInstalled.IsInArchive ? sfarStagingDirectory : null); } //Main installation step has completed CLog.Information(@"Main stage of mod installation has completed", Settings.LogModInstallation); Percent = (int)(numdone * 100.0 / numFilesToInstall); //Mark items read only foreach (var readonlytarget in mappedReadOnlyTargets) { CLog.Information(@"Setting file to read-only: " + readonlytarget, Settings.LogModInstallation); File.SetAttributes(readonlytarget, File.GetAttributes(readonlytarget) | FileAttributes.ReadOnly); } //Remove outdated custom DLC foreach (var outdatedDLCFolder in ModBeingInstalled.OutdatedCustomDLC) { var outdatedDLCInGame = Path.Combine(gameDLCPath, outdatedDLCFolder); if (Directory.Exists(outdatedDLCInGame)) { Log.Information(@"Deleting outdated custom DLC folder: " + outdatedDLCInGame); Utilities.DeleteFilesAndFoldersRecursively(outdatedDLCInGame); } } //Install supporting ASI files if necessary //Todo: Upgrade to version detection code from ME3EXP to prevent conflicts Action = M3L.GetString(M3L.string_installingSupportFiles); PercentVisibility = Visibility.Collapsed; CLog.Information(@"Installing supporting ASI files", Settings.LogModInstallation); if (ModBeingInstalled.Game == Mod.MEGame.ME1) { //Todo: Convert to ASI Manager installer Utilities.InstallASIByGroupID(gameTarget, @"DLC Mod Enabler", 16); //16 = DLC M -od Enabler //Utilities.InstallEmbeddedASI(@"ME1-DLC-ModEnabler-v1.0", 1.0, gameTarget); //Todo: Switch to ASI Manager } else if (ModBeingInstalled.Game == Mod.MEGame.ME2) { //None right now } else { if (ModBeingInstalled.GetJob(ModJob.JobHeader.BALANCE_CHANGES) != null) { Utilities.InstallASIByGroupID(gameTarget, @"Balance Changes Replacer", 5); //Utilities.InstallASIByGroupID(gameTarget, @"ME3Logger-Truncating", 5); //Utilities.InstallEmbeddedASI(@"BalanceChangesReplacer-v2.0", 2.0, gameTarget); //todo: Switch to ASI Manager } } if (sfarStagingDirectory != null) { Utilities.DeleteFilesAndFoldersRecursively(Utilities.GetTempPath()); } if (numFilesToInstall == numdone) { e.Result = ModInstallCompletedStatus.INSTALL_SUCCESSFUL; Action = M3L.GetString(M3L.string_installed); } else { Log.Warning($@"Number of completed items does not equal the amount of items to install! Number installed {numdone} Number expected: {numFilesToInstall}"); e.Result = ModInstallCompletedStatus.INSTALL_WRONG_NUMBER_OF_COMPLETED_ITEMS; } }
private void InstallModBackgroundThread(object sender, DoWorkEventArgs e) { Log.Information($"Mod Installer Background thread starting"); var installationJobs = ModBeingInstalled.InstallationJobs; var gamePath = gameTarget.TargetPath; var gameDLCPath = MEDirectories.DLCPath(gameTarget); Directory.CreateDirectory(gameDLCPath); //me1/me2 missing dlc might not have this folder //Check we can install var missingRequiredDLC = ModBeingInstalled.ValidateRequiredModulesAreInstalled(gameTarget); if (missingRequiredDLC.Count > 0) { e.Result = (ModInstallCompletedStatus.INSTALL_FAILED_REQUIRED_DLC_MISSING, missingRequiredDLC); return; } //Check/warn on official headers if (!PrecheckHeaders(gameDLCPath, installationJobs)) { e.Result = ModInstallCompletedStatus.INSTALL_FAILED_USER_CANCELED_MISSING_MODULES; return; } //todo: If statment on this Utilities.InstallBinkBypass(gameTarget); //Always install binkw32, don't bother checking if it is already ASI version. //Prepare queues (Dictionary <ModJob, (Dictionary <string, string> fileMapping, List <string> dlcFoldersBeingInstalled)> unpackedJobMappings, List <(ModJob job, string sfarPath, Dictionary <string, string> sfarInstallationMapping)> sfarJobs)installationQueues = ModBeingInstalled.GetInstallationQueues(gameTarget); if (gameTarget.ALOTInstalled) { //Check if any packages are being installed. If there are, we will block this installation. bool installsPackageFile = false; foreach (var jobMappings in installationQueues.unpackedJobMappings) { installsPackageFile |= jobMappings.Value.fileMapping.Keys.Any(x => x.EndsWith(".pcc", StringComparison.InvariantCultureIgnoreCase)); installsPackageFile |= jobMappings.Value.fileMapping.Keys.Any(x => x.EndsWith(".u", StringComparison.InvariantCultureIgnoreCase)); installsPackageFile |= jobMappings.Value.fileMapping.Keys.Any(x => x.EndsWith(".upk", StringComparison.InvariantCultureIgnoreCase)); installsPackageFile |= jobMappings.Value.fileMapping.Keys.Any(x => x.EndsWith(".sfm", StringComparison.InvariantCultureIgnoreCase)); } foreach (var jobMappings in installationQueues.sfarJobs) { installsPackageFile |= jobMappings.sfarInstallationMapping.Keys.Any(x => x.EndsWith(".pcc", StringComparison.InvariantCultureIgnoreCase)); installsPackageFile |= jobMappings.sfarInstallationMapping.Keys.Any(x => x.EndsWith(".u", StringComparison.InvariantCultureIgnoreCase)); installsPackageFile |= jobMappings.sfarInstallationMapping.Keys.Any(x => x.EndsWith(".upk", StringComparison.InvariantCultureIgnoreCase)); installsPackageFile |= jobMappings.sfarInstallationMapping.Keys.Any(x => x.EndsWith(".sfm", StringComparison.InvariantCultureIgnoreCase)); } if (installsPackageFile) { if (Settings.DeveloperMode) { Log.Warning("ALOT is installed and user is attemping to install a mod (in developer mode). Prompting user to cancel installation"); bool cancel = false; Application.Current.Dispatcher.Invoke(delegate { var res = Xceed.Wpf.Toolkit.MessageBox.Show(Window.GetWindow(this), $"ALOT is installed and this mod installs package files. Continuing to install this mod will likely cause broken textures to occur or game crashes due to invalid texture pointers and possibly empty mips. It will also put your ALOT installation into an unsupported configuration.\n\nContinue to install {ModBeingInstalled.ModName}? You have been warned.", $"Broken textures warning", MessageBoxButton.YesNo, MessageBoxImage.Error, MessageBoxResult.No); cancel = res == MessageBoxResult.No; }); if (cancel) { e.Result = ModInstallCompletedStatus.USER_CANCELED_INSTALLATION; return; } Log.Warning("User installing mod anyways even with ALOT installed"); } else { Log.Error("ALOT is installed. Installing mods that install package files after installing ALOT is not permitted."); //ALOT Installed, this is attempting to install a package file e.Result = ModInstallCompletedStatus.INSTALL_FAILED_ALOT_BLOCKING; return; } } } Action = $"Installing"; PercentVisibility = Visibility.Visible; Percent = 0; int numdone = 0; //Calculate number of installation tasks beforehand int numFilesToInstall = installationQueues.unpackedJobMappings.Select(x => x.Value.fileMapping.Count).Sum(); numFilesToInstall += installationQueues.sfarJobs.Select(x => x.sfarInstallationMapping.Count).Sum() * (ModBeingInstalled.IsInArchive ? 2 : 1); //*2 as we have to extract and install Debug.WriteLine("Number of expected installation tasks: " + numFilesToInstall); void FileInstalledCallback(string target) { numdone++; Debug.WriteLine("Installed: " + target); Action = "Installing"; var now = DateTime.Now; if (numdone > numFilesToInstall) { Debug.WriteLine($"Percentage calculated is wrong. Done: {numdone} NumToDoTotal: {numFilesToInstall}"); } if ((now - lastPercentUpdateTime).Milliseconds > PERCENT_REFRESH_COOLDOWN) { //Don't update UI too often. Once per second is enough. Percent = (int)(numdone * 100.0 / numFilesToInstall); lastPercentUpdateTime = now; } } //Stage: Unpacked files build map Dictionary <string, string> fullPathMappingDisk = new Dictionary <string, string>(); Dictionary <int, string> fullPathMappingArchive = new Dictionary <int, string>(); SortedSet <string> customDLCsBeingInstalled = new SortedSet <string>(); foreach (var unpackedQueue in installationQueues.unpackedJobMappings) { foreach (var originalMapping in unpackedQueue.Value.fileMapping) { //always unpacked //if (unpackedQueue.Key == ModJob.JobHeader.CUSTOMDLC || unpackedQueue.Key == ModJob.JobHeader.BALANCE_CHANGES || unpackedQueue.Key == ModJob.JobHeader.BASEGAME) //{ string sourceFile; if (unpackedQueue.Key.JobDirectory == null) { sourceFile = FilesystemInterposer.PathCombine(ModBeingInstalled.IsInArchive, ModBeingInstalled.ModPath, originalMapping.Value); } else { sourceFile = FilesystemInterposer.PathCombine(ModBeingInstalled.IsInArchive, ModBeingInstalled.ModPath, unpackedQueue.Key.JobDirectory, originalMapping.Value); } if (unpackedQueue.Key.Header == ModJob.JobHeader.ME1_CONFIG) { var destFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "BioWare", "Mass Effect", "Config", originalMapping.Key); if (ModBeingInstalled.IsInArchive) { int archiveIndex = ModBeingInstalled.Archive.ArchiveFileNames.IndexOf(sourceFile, StringComparer.InvariantCultureIgnoreCase); fullPathMappingArchive[archiveIndex] = destFile; //used for extraction indexing if (archiveIndex == -1) { Log.Error("Archive Index is -1 for file " + sourceFile + ". This will probably throw an exception!"); Debugger.Break(); } fullPathMappingDisk[sourceFile] = destFile; //used for redirection } else { fullPathMappingDisk[sourceFile] = destFile; } } else { var destFile = Path.Combine(unpackedQueue.Key.Header == ModJob.JobHeader.CUSTOMDLC ? MEDirectories.DLCPath(gameTarget) : gameTarget.TargetPath, originalMapping.Key); //official //Extract Custom DLC name if (unpackedQueue.Key.Header == ModJob.JobHeader.CUSTOMDLC) { var custDLC = destFile.Substring(gameDLCPath.Length, destFile.Length - gameDLCPath.Length).TrimStart('\\', '/'); var nextSlashIndex = custDLC.IndexOf('\\'); if (nextSlashIndex == -1) { nextSlashIndex = custDLC.IndexOf('/'); } if (nextSlashIndex != -1) { custDLC = custDLC.Substring(0, nextSlashIndex); customDLCsBeingInstalled.Add(custDLC); } } if (ModBeingInstalled.IsInArchive) { int archiveIndex = ModBeingInstalled.Archive.ArchiveFileNames.IndexOf(sourceFile, StringComparer.InvariantCultureIgnoreCase); fullPathMappingArchive[archiveIndex] = destFile; //used for extraction indexing if (archiveIndex == -1) { Log.Error("Archive Index is -1 for file " + sourceFile + ". This will probably throw an exception!"); Debugger.Break(); } fullPathMappingDisk[sourceFile] = destFile; //used for redirection } else { fullPathMappingDisk[sourceFile] = destFile; } } //} } } //Substage: Add SFAR staging targets string sfarStagingDirectory = (ModBeingInstalled.IsInArchive && installationQueues.sfarJobs.Count > 0) ? Directory.CreateDirectory(Path.Combine(Utilities.GetTempPath(), "SFARJobStaging")).FullName : null; //don't make directory if we don't need one if (sfarStagingDirectory != null) { foreach (var sfarJob in installationQueues.sfarJobs) { foreach (var fileToInstall in sfarJob.sfarInstallationMapping) { string sourceFile = FilesystemInterposer.PathCombine(ModBeingInstalled.IsInArchive, ModBeingInstalled.ModPath, sfarJob.job.JobDirectory, fileToInstall.Value); int archiveIndex = ModBeingInstalled.Archive.ArchiveFileNames.IndexOf(sourceFile, StringComparer.InvariantCultureIgnoreCase); if (archiveIndex == -1) { Log.Error("Archive Index is -1 for file " + sourceFile + ". This will probably throw an exception!"); Debugger.Break(); } string destFile = Path.Combine(sfarStagingDirectory, sfarJob.job.JobDirectory, fileToInstall.Value); fullPathMappingArchive[archiveIndex] = destFile; //used for extraction indexing fullPathMappingDisk[sourceFile] = destFile; //used for redirection Debug.WriteLine($"SFAR Disk Staging: {fileToInstall.Key} => {destFile}"); } } } foreach (var cdbi in customDLCsBeingInstalled) { var path = Path.Combine(gameDLCPath, cdbi); if (Directory.Exists(path)) { Log.Information("Deleting existing DLC directory: " + path); Utilities.DeleteFilesAndFoldersRecursively(path); } } //Stage: Unpacked files installation if (!ModBeingInstalled.IsInArchive) { //Direct copy Log.Information($"Installing {fullPathMappingDisk.Count} unpacked files into game directory"); CopyDir.CopyFiles_ProgressBar(fullPathMappingDisk, FileInstalledCallback); } else { Action = "Loading mod archive"; //Extraction to destination string installationRedirectCallback(ArchiveFileInfo info) { var inArchivePath = info.FileName; var redirectedPath = fullPathMappingDisk[inArchivePath]; Debug.WriteLine($"Redirecting {inArchivePath} to {redirectedPath}"); return(redirectedPath); } ModBeingInstalled.Archive.FileExtractionStarted += (sender, args) => { //CLog.Information("Extracting mod file for installation: " + args.FileInfo.FileName, Settings.LogModInstallation); }; List <string> filesInstalled = new List <string>(); List <string> filesToInstall = installationQueues.unpackedJobMappings.SelectMany(x => x.Value.fileMapping.Keys).ToList(); ModBeingInstalled.Archive.FileExtractionFinished += (sender, args) => { if (args.FileInfo.IsDirectory) { return; //ignore } if (!fullPathMappingArchive.ContainsKey(args.FileInfo.Index)) { return; //archive extracted this file (in memory) but did not do anything with this file (7z) } FileInstalledCallback(args.FileInfo.FileName); filesInstalled.Add(args.FileInfo.FileName); //Debug.WriteLine($"{args.FileInfo.FileName} as file { numdone}"); //Debug.WriteLine(numdone); }; ModBeingInstalled.Archive.ExtractFiles(gameTarget.TargetPath, installationRedirectCallback, fullPathMappingArchive.Keys.ToArray()); //directory parameter shouldn't be used here as we will be redirecting everything //filesInstalled.Sort(); //filesToInstall.Sort(); //Debug.WriteLine("Files installed:"); //foreach (var f in filesInstalled) //{ // Debug.WriteLine(f); //} //Debug.WriteLine("Files expected:"); //foreach (var f in filesToInstall) //{ // Debug.WriteLine(f); //} } //Write MetaCMM List <string> addedDLCFolders = new List <string>(); foreach (var v in installationQueues.unpackedJobMappings) { addedDLCFolders.AddRange(v.Value.dlcFoldersBeingInstalled); } foreach (var addedDLCFolder in addedDLCFolders) { var metacmm = Path.Combine(addedDLCFolder, "_metacmm.txt"); ModBeingInstalled.HumanReadableCustomDLCNames.TryGetValue(Path.GetFileName(addedDLCFolder), out var assignedDLCName); string contents = $"{assignedDLCName ?? ModBeingInstalled.ModName}\n{ModBeingInstalled.ModVersionString}\n{App.BuildNumber}\n{Guid.NewGuid().ToString()}"; File.WriteAllText(metacmm, contents); } //Stage: SFAR Installation foreach (var sfarJob in installationQueues.sfarJobs) { InstallIntoSFAR(sfarJob, ModBeingInstalled, FileInstalledCallback, ModBeingInstalled.IsInArchive ? sfarStagingDirectory : null); } //Main installation step has completed CLog.Information("Main stage of mod installation has completed", Settings.LogModInstallation); Percent = (int)(numdone * 100.0 / numFilesToInstall); //Remove outdated custom DLC foreach (var outdatedDLCFolder in ModBeingInstalled.OutdatedCustomDLC) { var outdatedDLCInGame = Path.Combine(gameDLCPath, outdatedDLCFolder); if (Directory.Exists(outdatedDLCInGame)) { Log.Information("Deleting outdated custom DLC folder: " + outdatedDLCInGame); Utilities.DeleteFilesAndFoldersRecursively(outdatedDLCInGame); } } //Install supporting ASI files if necessary //Todo: Upgrade to version detection code from ME3EXP to prevent conflicts Action = "Installing support files"; CLog.Information("Installing supporting ASI files", Settings.LogModInstallation); if (ModBeingInstalled.Game == Mod.MEGame.ME1) { Utilities.InstallEmbeddedASI("ME1-DLC-ModEnabler-v1.0", 1.0, gameTarget); } else if (ModBeingInstalled.Game == Mod.MEGame.ME2) { //None right now } else { //Todo: Port detection code from ME3Exp //Utilities.InstallEmbeddedASI("ME3Logger_truncating-v1.0", 1.0, gameTarget); if (ModBeingInstalled.GetJob(ModJob.JobHeader.BALANCE_CHANGES) != null) { Utilities.InstallEmbeddedASI("BalanceChangesReplacer-v2.0", 2.0, gameTarget); } } if (sfarStagingDirectory != null) { Utilities.DeleteFilesAndFoldersRecursively(Utilities.GetTempPath()); } if (numFilesToInstall == numdone) { e.Result = ModInstallCompletedStatus.INSTALL_SUCCESSFUL; Action = "Installed"; } else { Log.Warning($"Number of completed items does not equal the amount of items to install! Number installed {numdone} Number expected: {numFilesToInstall}"); e.Result = ModInstallCompletedStatus.INSTALL_WRONG_NUMBER_OF_COMPLETED_ITEMS; } }