Esempio n. 1
0
        public static void GenerateMergeDLC(GameTarget target, Guid guid)
        {
            // Generate M3 DLC Folder

            // Does not work for LE1/ME1!
            var sko = new StarterKitGeneratorWindow.StarterKitOptions()
            {
                ModGame                = target.Game,
                GenerateModdesc        = false,
                OutputFolderOverride   = M3Directories.GetDLCPath(target),
                ModDescription         = null,
                ModInternalName        = @"ME3Tweaks Mod Manager Merge DLC",
                ModInternalTLKID       = 1928304430,
                ModMountFlag           = target.Game.IsGame3() ? new MountFlag(EME3MountFileFlag.LoadsInSingleplayer) : new MountFlag(0, true),
                ModDeveloper           = @"ME3Tweaks Mod Manager",
                ModMountPriority       = 1900000000,
                ModDLCFolderNameSuffix = MERGE_DLC_FOLDERNAME.Substring(@"DLC_MOD_".Length)
            };

            StarterKitGeneratorWindow.CreateStarterKitMod(sko, null);
            MetaCMM mcmm = new MetaCMM()
            {
                ModName = @"ME3Tweaks Mod Manager Auto-Generated Merge DLC",
                Version = @"1.0"
            };

            mcmm.WriteMetaCMM(Path.Combine(M3Directories.GetDLCPath(target), MERGE_DLC_FOLDERNAME, @"_metacmm.txt"));
        }
Esempio n. 2
0
        public static void RemoveMergeDLC(GameTarget target)
        {
            var mergePath = Path.Combine(M3Directories.GetDLCPath(target), MERGE_DLC_FOLDERNAME);

            if (Directory.Exists(mergePath))
            {
                Utilities.DeleteFilesAndFoldersRecursively(mergePath);
            }
        }
Esempio n. 3
0
 private void ToggleDLC()
 {
     try
     {
         var    dlcFPath       = M3Directories.GetDLCPath(target);
         var    currentDLCPath = Path.Combine(dlcFPath, DLCFolderName);
         string destPath       = Path.Combine(dlcFPath, Enabled ? @"x" + UIDLCFolderName : UIDLCFolderName);
         Directory.Move(currentDLCPath, destPath);
         Enabled       = !Enabled;
         DLCFolderName = Enabled ? UIDLCFolderName : @"x" + UIDLCFolderName;
     }
     catch (Exception e)
     {
         Log.Error($@"Error toggling DLC {DLCFolderName}: {e.Message}");
         M3L.ShowDialog(Application.Current?.MainWindow, M3L.GetString(M3L.string_interp_errorTogglingDLC, e.Message), M3L.GetString(M3L.string_error), MessageBoxButton.OK, MessageBoxImage.Error); //this needs updated to be better
     }
 }
        /// <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)
            {
#if DEBUG
                if (Archive.IsDisposed())
                {
                    Debug.WriteLine(@">>> ARCHIVE IS DISPOSED");
                }
#endif
                if (File.Exists(ArchivePath) && (Archive == null || Archive.IsDisposed()))
                {
                    Archive = new SevenZipExtractor(ArchivePath); //load archive file for inspection
                }
                else if (Archive != null && Archive.GetBackingStream() is SevenZip.ArchiveEmulationStreamProxy aesp && aesp.Source is MemoryStream ms)
                {
                    var isExe = ArchivePath.EndsWith(@".exe", StringComparison.InvariantCultureIgnoreCase);
                    Archive = isExe ? new SevenZipExtractor(ms, InArchiveFormat.Nsis) : new SevenZipExtractor(ms);
                    MemoryAnalyzer.AddTrackedMemoryItem($@"Re-opened SVE archive for {ModName}", new WeakReference(Archive));
                }
            }
            var gameDLCPath      = M3Directories.GetDLCPath(gameTarget);
            var customDLCMapping = Enumerable.FirstOrDefault <ModJob>(InstallationJobs, 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 = Enumerable.Where <AlternateFile>(job.AlternateFiles, x => x.IsSelected && x.Operation != AlternateFile.AltFileOperation.OP_NOTHING &&
                                                                      x.Operation != AlternateFile.AltFileOperation.OP_NOINSTALL_MULTILISTFILES).ToList();
                var alternateDLC = Enumerable.Where <AlternateDLC>(job.AlternateDLCs, 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((string)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((string)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 = Enumerable.Where <AlternateFile>(job.AlternateFiles, 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 (M3Directories.IsOfficialDLCInstalled(job.Header, gameTarget))
                    {
                        string sfarPath = job.Header == ModJob.JobHeader.TESTPATCH ? M3Directories.GetTestPatchSFARPath(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 (M3Directories.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);
        }
        private void ImportSelectedFolder()
        {
            //Check destination path
            var destinationName = Utilities.SanitizePath(ModNameText);

            if (string.IsNullOrWhiteSpace(destinationName))
            {
                //cannot use this name
                Log.Error(@"Invalid mod name: " + ModNameText);
                M3L.ShowDialog(mainwindow, M3L.GetString(M3L.string_dialog_invalidModNameWillResolveToNothing), M3L.GetString(M3L.string_invalidModName), MessageBoxButton.OK, MessageBoxImage.Error);
                return;
            }

            //Check free space.
            var sourceDir = Path.Combine(M3Directories.GetDLCPath(SelectedTarget), SelectedDLCFolder.DLCFolderName);
            var library   = Utilities.GetModDirectoryForGame(SelectedTarget.Game);

            if (Utilities.DriveFreeBytes(library, out var freeBytes))
            {
                //Check enough space
                var sourceSize = Utilities.GetSizeOfDirectory(sourceDir);
                if (sourceSize > (long)freeBytes)
                {
                    //Not enough space
                    Log.Error($@"Not enough disk space to import mod. Required space: {FileSize.FormatSize(sourceSize)}, available space: {FileSize.FormatSize(freeBytes)}");
                    M3L.ShowDialog(mainwindow, M3L.GetString(M3L.string_interp_insufficientDiskSpaceToImport, Path.GetPathRoot(library), FileSize.FormatSize(sourceSize), FileSize.FormatSize(freeBytes)), M3L.GetString(M3L.string_insufficientFreeDiskSpace), MessageBoxButton.OK, MessageBoxImage.Error);
                    return;
                }
            }

            //Check directory doesn't exist already
            var outDir = Path.Combine(library, destinationName);

            if (Directory.Exists(outDir))
            {
                var okToDelete = M3L.ShowDialog(mainwindow, M3L.GetString(M3L.string_interp_dialog_importingWillDeleteExistingMod, outDir), M3L.GetString(M3L.string_sameNamedModInLibrary), MessageBoxButton.YesNo, MessageBoxImage.Warning);
                if (okToDelete == MessageBoxResult.No)
                {
                    return; //cancel
                }

                try
                {
                    Utilities.DeleteFilesAndFoldersRecursively(outDir);
                }
                catch (Exception e)
                {
                    M3L.ShowDialog(mainwindow, M3L.GetString(M3L.string_interp_couldNotDeleteExistingModDirectory, e.Message), M3L.GetString(M3L.string_errorDeletingModFolder), MessageBoxButton.OK, MessageBoxImage.Error);
                    return; //abort
                }
            }

            NamedBackgroundWorker nbw = new NamedBackgroundWorker(@"GameDLCModImporter");

            nbw.DoWork             += ImportDLCFolder_BackgroundThread;
            nbw.RunWorkerCompleted += (a, b) =>
            {
                if (b.Error != null)
                {
                    Log.Error($@"Exception occurred in {nbw.Name} thread: {b.Error.Message}");
                }
                else
                {
                    if (b.Error == null && b.Result != null)
                    {
                        Analytics.TrackEvent(@"Imported a mod from game installation", new Dictionary <string, string>()
                        {
                            { @"Game", SelectedTarget.Game.ToString() },
                            { @"Folder", SelectedDLCFolder.DLCFolderName }
                        });
                    }

                    OperationInProgress = false;
                    if (b.Error == null && b.Result != null)
                    {
                        OnClosing(new DataEventArgs(b.Result)); //avoid accessing b.Result if error occurred
                    }
                }
            };
            nbw.RunWorkerAsync();
        }
        private void ImportDLCFolder_BackgroundThread(object sender, DoWorkEventArgs e)
        {
            OperationInProgress = true;
            var sourceDir = Path.Combine(M3Directories.GetDLCPath(SelectedTarget), SelectedDLCFolder.DLCFolderName);

            // Check for MEMI, we will not allow importing files with MEMI
            foreach (var file in Directory.GetFiles(sourceDir, @"*.*", SearchOption.AllDirectories))
            {
                if (file.RepresentsPackageFilePath() && Utilities.HasALOTMarker(file))
                {
                    Log.Error($@"Found a file marked as texture modded: {file}. These files cannot be imported into mod manager");
                    Application.Current.Dispatcher.Invoke(delegate
                    {
                        M3L.ShowDialog(window, M3L.GetString(M3L.string_dialog_cannotImportModTextureMarkersFound), M3L.GetString(M3L.string_cannotImportMod), MessageBoxButton.OK, MessageBoxImage.Error);
                    });
                    return;
                }
            }



            var library         = Utilities.GetModDirectoryForGame(SelectedTarget.Game);
            var destinationName = Utilities.SanitizePath(ModNameText);
            var modFolder       = Path.Combine(library, destinationName);
            var copyDestination = Path.Combine(modFolder, SelectedDLCFolder.DLCFolderName);
            var outInfo         = Directory.CreateDirectory(copyDestination);

            Log.Information($@"Importing mod: {sourceDir} -> {copyDestination}");

            int numToDo = 0;
            int numDone = 0;

            void totalItemToCopyCallback(int total)
            {
                numToDo        = total;
                ProgressBarMax = total;
            }

            void fileCopiedCallback()
            {
                numDone++;
                ProgressBarValue = numDone;
            }

            CopyDir.CopyAll_ProgressBar(new DirectoryInfo(sourceDir), outInfo, totalItemToCopyCallback, fileCopiedCallback);

            //Write a moddesc
            IniData ini = new IniData();

            ini[@"ModManager"][@"cmmver"]    = App.HighestSupportedModDesc.ToString(CultureInfo.InvariantCulture); //prevent commas
            ini[@"ModInfo"][@"game"]         = SelectedTarget.Game.ToString();
            ini[@"ModInfo"][@"modname"]      = ModNameText;
            ini[@"ModInfo"][@"moddev"]       = M3L.GetString(M3L.string_importedFromGame);
            ini[@"ModInfo"][@"moddesc"]      = M3L.GetString(M3L.string_defaultDescriptionForImportedMod, Utilities.GetGameName(SelectedTarget.Game), DateTime.Now);
            ini[@"ModInfo"][@"modver"]       = M3L.GetString(M3L.string_unknown);
            ini[@"ModInfo"][@"unofficial"]   = @"true";
            ini[@"ModInfo"][@"importedby"]   = App.BuildNumber.ToString();
            ini[@"CUSTOMDLC"][@"sourcedirs"] = SelectedDLCFolder.DLCFolderName;
            ini[@"CUSTOMDLC"][@"destdirs"]   = SelectedDLCFolder.DLCFolderName;


            var moddescPath = Path.Combine(modFolder, @"moddesc.ini");

            File.WriteAllText(moddescPath, ini.ToString());

            //Generate and load mod
            var m = new Mod(moddescPath, MEGame.ME3);

            e.Result = m;
            Log.Information(@"Mod import complete.");
            Analytics.TrackEvent(@"Imported already installed mod", new Dictionary <string, string>()
            {
                { @"Mod name", m.ModName },
                { @"Game", SelectedTarget.Game.ToString() },
                { @"Folder name", SelectedDLCFolder.DLCFolderName }
            });

            if (!CurrentModInTPMI)
            {
                //Submit telemetry to ME3Tweaks
                try
                {
                    TPMITelemetrySubmissionForm.TelemetryPackage tp = TPMITelemetrySubmissionForm.GetTelemetryPackageForDLC(SelectedTarget.Game,
                                                                                                                            M3Directories.GetDLCPath(SelectedTarget),
                                                                                                                            SelectedDLCFolder.DLCFolderName,
                                                                                                                            SelectedDLCFolder.DLCFolderName, //same as foldername as this is already installed
                                                                                                                            ModNameText,
                                                                                                                            @"N/A",
                                                                                                                            ModSiteText,
                                                                                                                            null
                                                                                                                            );

                    tp.SubmitPackage();
                }
                catch (Exception ex)
                {
                    Log.Error(@"Cannot submit telemetry: " + ex.Message);
                }
            }
        }
Esempio n. 7
0
        public static void BuildBioPGlobal(GameTarget target)
        {
            M3MergeDLC.RemoveMergeDLC(target);
            var loadedFiles = MELoadedFiles.GetFilesLoadedInGame(target.Game, gameRootOverride: target.TargetPath);

            //var mergeFiles = loadedFiles.Where(x =>
            //    x.Key.StartsWith(@"BioH_") && x.Key.Contains(@"_DLC_MOD_") && x.Key.EndsWith(@".pcc") && !x.Key.Contains(@"_LOC_") && !x.Key.Contains(@"_Explore."));

            Log.Information($@"SQMMERGE: Building BioP_Global");
            var appearanceInfo = new CaseInsensitiveDictionary <List <SquadmateInfoSingle> >();

            int appearanceId       = 255; // starting
            int currentConditional = STARTING_OUTFIT_CONDITIONAL;

            // Scan squadmate merge files
            var sqmSupercedances = M3Directories.GetFileSupercedances(target, new[] { @".sqm" });

            if (sqmSupercedances.TryGetValue(SQUADMATE_MERGE_MANIFEST_FILE, out var infoList))
            {
                infoList.Reverse();
                foreach (var dlc in infoList)
                {
                    Log.Information($@"SQMMERGE: Processing {dlc}");

                    var jsonFile    = Path.Combine(M3Directories.GetDLCPath(target), dlc, target.Game.CookedDirName(), SQUADMATE_MERGE_MANIFEST_FILE);
                    var infoPackage = JsonConvert.DeserializeObject <SquadmateMergeInfo>(File.ReadAllText(jsonFile));
                    if (!infoPackage.Validate(dlc, target, loadedFiles))
                    {
                        continue; // skip this
                    }

                    // Enumerate all outfits listed for a single squadmate
                    foreach (var outfit in infoPackage.Outfits)
                    {
                        List <SquadmateInfoSingle> list;

                        // See if we already have an outfit list for this squadmate, maybe from another mod...
                        if (!appearanceInfo.TryGetValue(outfit.HenchName, out list))
                        {
                            list = new List <SquadmateInfoSingle>();
                            appearanceInfo[outfit.HenchName] = list;
                        }

                        outfit.ConditionalIndex = currentConditional++; // This is always incremented, so it might appear out of order in game files depending on how mod order is processed, that should be okay though.
                        outfit.AppearanceId     = appearanceId++;       // may need adjusted
                        outfit.DLCName          = dlc;
                        list.Add(outfit);
                        Log.Information($@"SQMMERGE: ConditionalIndex for {outfit.HenchName} appearanceid {outfit.AppearanceId}: {outfit.ConditionalIndex}");
                    }
                }
            }

            if (appearanceInfo.Any())
            {
                var biopGlobal      = MEPackageHandler.OpenMEPackage(loadedFiles[@"BioP_Global.pcc"]);
                var lsk             = biopGlobal.Exports.FirstOrDefault(x => x.ClassName == @"LevelStreamingKismet");
                var persistentLevel = biopGlobal.FindExport(@"TheWorld.PersistentLevel");

                // Clone LevelStreamingKismets
                foreach (var sqm in appearanceInfo.Values)
                {
                    foreach (var outfit in sqm)
                    {
                        var fName  = outfit.HenchPackage;
                        var newLSK = EntryCloner.CloneEntry(lsk);
                        newLSK.WriteProperty(new NameProperty(fName, @"PackageName"));

                        if (target.Game.IsGame3())
                        {
                            // Game 3 has _Explore files too
                            fName += @"_Explore";
                            newLSK = EntryCloner.CloneEntry(lsk);
                            newLSK.WriteProperty(new NameProperty(fName, @"PackageName"));
                        }
                    }
                }

                // Update BioWorldInfo
                // Doesn't have consistent number so we can't find it by instanced full path
                var bioWorldInfo = biopGlobal.Exports.FirstOrDefault(x => x.ClassName == @"BioWorldInfo");

                var props = bioWorldInfo.GetProperties();

                // Update Plot Streaming
                var plotStreaming = props.GetProp <ArrayProperty <StructProperty> >(@"PlotStreaming");
                foreach (var sqm in appearanceInfo.Values)
                {
                    foreach (var outfit in sqm)
                    {
                        // find item to add to
                        buildPlotElementObject(plotStreaming, outfit, target.Game, false);
                        if (target.Game.IsGame3())
                        {
                            buildPlotElementObject(plotStreaming, outfit, target.Game, true);
                        }
                    }
                }


                // Update StreamingLevels
                var streamingLevels = props.GetProp <ArrayProperty <ObjectProperty> >(@"StreamingLevels");
                streamingLevels.ReplaceAll(biopGlobal.Exports.Where(x => x.ClassName == @"LevelStreamingKismet").Select(x => new ObjectProperty(x)));

                bioWorldInfo.WriteProperties(props);


                M3MergeDLC.GenerateMergeDLC(target, Guid.NewGuid());

                // Save BioP_Global into DLC
                var cookedDir = Path.Combine(M3Directories.GetDLCPath(target), M3MergeDLC.MERGE_DLC_FOLDERNAME, target.Game.CookedDirName());
                var outP      = Path.Combine(cookedDir, @"BioP_Global.pcc");
                biopGlobal.Save(outP);

                // Generate conditionals file
                if (target.Game.IsGame3())
                {
                    CNDFile cnd = new CNDFile();
                    cnd.ConditionalEntries = new List <CNDFile.ConditionalEntry>();

                    foreach (var sqm in appearanceInfo.Values)
                    {
                        foreach (var outfit in sqm)
                        {
                            var scText   = $@"(plot.ints[{GetSquadmateOutfitInt(outfit.HenchName, target.Game)}] == i{outfit.MemberAppearanceValue})";
                            var compiled = ME3ConditionalsCompiler.Compile(scText);
                            cnd.ConditionalEntries.Add(new CNDFile.ConditionalEntry()
                            {
                                Data = compiled, ID = outfit.ConditionalIndex
                            });
                        }
                    }

                    cnd.ToFile(Path.Combine(cookedDir, $@"Conditionals{M3MergeDLC.MERGE_DLC_FOLDERNAME}.cnd"));
                }
                else if (target.Game.IsGame2())
                {
                    var startupF = Path.Combine(cookedDir, $@"Startup_{M3MergeDLC.MERGE_DLC_FOLDERNAME}.pcc");
                    var startup  = MEPackageHandler.OpenMEPackageFromStream(Utilities.GetResourceStream(
                                                                                $@"MassEffectModManagerCore.modmanager.mergedlc.{target.Game}.Startup_{M3MergeDLC.MERGE_DLC_FOLDERNAME}.pcc"));
                    var conditionalClass =
                        startup.FindExport($@"PlotManager{M3MergeDLC.MERGE_DLC_FOLDERNAME}.BioAutoConditionals");

                    // Add Conditional Functions
                    FileLib fl          = new FileLib(startup);
                    bool    initialized = fl.Initialize(new RelativePackageCache()
                    {
                        RootPath = M3Directories.GetBioGamePath(target)
                    });
                    if (!initialized)
                    {
                        throw new Exception(
                                  $@"FileLib for script update could not initialize, cannot install conditionals");
                    }


                    var funcToClone =
                        startup.FindExport($@"PlotManager{M3MergeDLC.MERGE_DLC_FOLDERNAME}.BioAutoConditionals.TemplateFunction");
                    foreach (var sqm in appearanceInfo.Values)
                    {
                        foreach (var outfit in sqm)
                        {
                            var func = EntryCloner.CloneEntry(funcToClone);
                            func.ObjectName = $@"F{outfit.ConditionalIndex}";
                            func.indexValue = 0;

                            var scText = new StreamReader(Utilities.GetResourceStream(
                                                              $@"MassEffectModManagerCore.modmanager.squadmates.{target.Game}.HasOutfitOnConditional.txt"))
                                         .ReadToEnd();

                            scText = scText.Replace(@"%CONDITIONALNUM%", outfit.ConditionalIndex.ToString());
                            scText = scText.Replace(@"%SQUADMATEOUTFITPLOTINT%", outfit.AppearanceId.ToString());
                            scText = scText.Replace(@"%OUTFITINDEX%", outfit.MemberAppearanceValue.ToString());

                            (_, MessageLog log) = UnrealScriptCompiler.CompileFunction(func, scText, fl);
                            if (log.AllErrors.Any())
                            {
                                Log.Error($@"Error compiling function {func.InstancedFullPath}:");
                                foreach (var l in log.AllErrors)
                                {
                                    Log.Error(l.Message);
                                }

                                throw new Exception(M3L.GetString(M3L.string_interp_errorCompilingConditionalFunction, func, string.Join('\n', log.AllErrors.Select(x => x.Message))));
                            }
                        }
                    }


                    // Relink the conditionals chain
                    UClass uc = ObjectBinary.From <UClass>(conditionalClass);
                    uc.UpdateLocalFunctions();
                    uc.UpdateChildrenChain();
                    conditionalClass.WriteBinary(uc);

                    startup.Save(startupF);
                }


                // Add startup package, member appearances
                if (target.Game.IsGame2())
                {
                    var bioEngine = Path.Combine(cookedDir, @"BIOEngine.ini");
                    var ini       = DuplicatingIni.LoadIni(bioEngine);

                    var startupSection = ini.GetOrAddSection(@"Engine.StartupPackages");

                    startupSection.Entries.Add(new DuplicatingIni.IniEntry(@"+DLCStartupPackage", $@"Startup_{M3MergeDLC.MERGE_DLC_FOLDERNAME}"));
                    startupSection.Entries.Add(new DuplicatingIni.IniEntry(@"+Package", $@"PlotManager{M3MergeDLC.MERGE_DLC_FOLDERNAME}"));

                    ini.WriteToFile(bioEngine);
                }
                else if (target.Game.IsGame3())
                {
                    var mergeCoalFile = Path.Combine(M3Directories.GetDLCPath(target), M3MergeDLC.MERGE_DLC_FOLDERNAME, target.Game.CookedDirName(), $@"Default_{M3MergeDLC.MERGE_DLC_FOLDERNAME}.bin");
                    var mergeCoal     = CoalescedConverter.DecompileGame3ToMemory(new MemoryStream(File.ReadAllBytes(mergeCoalFile)));

                    // Member appearances
                    var bioUiDoc = XDocument.Parse(mergeCoal[@"BIOUI.xml"]);

                    foreach (var sqm in appearanceInfo.Values)
                    {
                        foreach (var outfit in sqm)
                        {
                            var entry = new Game3CoalescedValueEntry()
                            {
                                Section = @"sfxgame.sfxguidata_teamselect",
                                Name    = @"selectappearances",
                                Type    = 3,
                                Value   = StringStructParser.BuildCommaSeparatedSplitValueList(outfit.ToPropertyDictionary(), @"AvailableImage", @"HighlightImage", @"DeadImage", @"SilhouetteImage")
                            };
                            Game3CoalescedHelper.AddArrayEntry(bioUiDoc, entry);
                        }
                    }

                    mergeCoal[@"BIOUI.xml"] = bioUiDoc.ToString();

                    // Dynamic load mapping
                    var bioEngineDoc = XDocument.Parse(mergeCoal[@"BIOEngine.xml"]);

                    foreach (var sqm in appearanceInfo.Values)
                    {
                        foreach (var outfit in sqm)
                        {
                            //
                            // * <Section name="sfxgame.sfxengine">
                            // <Property name="dynamicloadmapping">
                            // <Value type="3">(ObjectName="BIOG_GesturesConfigDLC.RuntimeData",SeekFreePackageName="GesturesConfigDLC")</Value>

                            var entry = new Game3CoalescedValueEntry()
                            {
                                Section = @"sfxgame.sfxengine",
                                Name    = @"dynamicloadmapping",
                                Type    = 3
                            };

                            entry.Values.Add($"(ObjectName=\"{outfit.AvailableImage}\",SeekFreePackageName=\"SFXHenchImages_{outfit.DLCName}\")"); // do not localize
                            entry.Values.Add($"(ObjectName=\"{outfit.HighlightImage}\",SeekFreePackageName=\"SFXHenchImages_{outfit.DLCName}\")"); // do not localize
                            Game3CoalescedHelper.AddArrayEntry(bioEngineDoc, entry);
                        }
                    }

                    mergeCoal[@"BIOEngine.xml"] = bioEngineDoc.ToString();


                    CoalescedConverter.CompileFromMemory(mergeCoal).WriteToFile(mergeCoalFile);
                }
            }
        }
            private void BeginBackup()
            {
                var targetToBackup = BackupSourceTarget;

                if (!targetToBackup.IsCustomOption)
                {
                    if (Utilities.IsGameRunning(targetToBackup.Game))
                    {
                        M3L.ShowDialog(window, M3L.GetString(M3L.string_interp_cannotBackupGameWhileRunning, Utilities.GetGameName(BackupSourceTarget.Game)), M3L.GetString(M3L.string_gameRunning), MessageBoxButton.OK, MessageBoxImage.Error);
                        return;
                    }
                }
                else
                {
                    // Point to existing game installation
                    Log.Information(@"BeginBackup() with IsCustomOption.");
                    var linkWarning = M3L.ShowDialog(window,
                                                     M3L.GetString(M3L.string_dialog_linkTargetWontBeModdable), M3L.GetString(M3L.string_linkWarning), MessageBoxButton.OKCancel, MessageBoxImage.Warning);
                    if (linkWarning == MessageBoxResult.Cancel)
                    {
                        Log.Information(@"User aborted linking due to dialog");
                        return;
                    }

                    Log.Information(@"Prompting user to select executable of link target");
                    var gameexe = Utilities.PromptForGameExecutable(new[] { Game });
                    if (gameexe == null)
                    {
                        return;
                    }
                    targetToBackup = new GameTarget(Game, Utilities.GetGamePathFromExe(Game, gameexe), false, true);
                    if (AvailableBackupSources.Any(x => x.TargetPath.Equals(targetToBackup.TargetPath, StringComparison.InvariantCultureIgnoreCase)))
                    {
                        // Can't point to an existing modding target
                        Log.Error(@"This target is not valid to point to as a backup: It is listed a modding target already, it must be removed as a target first");
                        M3L.ShowDialog(window, M3L.GetString(M3L.string_interp_dialog_linkFailedAlreadyATarget), M3L.GetString(M3L.string_cannotLinkGameCopy), MessageBoxButton.OK, MessageBoxImage.Error);
                        return;
                    }

                    var validationFailureReason = targetToBackup.ValidateTarget(ignoreCmmVanilla: true);
                    if (!targetToBackup.IsValid)
                    {
                        Log.Error(@"This installation is not valid to point to as a backup: " + validationFailureReason);
                        M3L.ShowDialog(window, M3L.GetString(M3L.string_interp_dialog_linkFailedInvalidTarget, validationFailureReason), M3L.GetString(M3L.string_invalidGameCopy), MessageBoxButton.OK, MessageBoxImage.Error);
                        return;
                    }
                }

                NamedBackgroundWorker nbw = new NamedBackgroundWorker(Game + @"Backup");

                nbw.WorkerReportsProgress = true;
                nbw.ProgressChanged      += (a, b) =>
                {
                    if (b.UserState is double d)
                    {
                        TaskbarHelper.SetProgress(d);
                    }
                    else if (b.UserState is TaskbarProgressBarState tbs)
                    {
                        TaskbarHelper.SetProgressState(tbs);
                    }
                };
                nbw.DoWork += (a, b) =>
                {
                    Log.Information(@"Starting the backup thread. Checking path: " + targetToBackup.TargetPath);
                    BackupInProgress = true;
                    bool end = false;

                    List <string> nonVanillaFiles = new List <string>();

                    void nonVanillaFileFoundCallback(string filepath)
                    {
                        Log.Error($@"Non-vanilla file found: {filepath}");
                        nonVanillaFiles.Add(filepath);
                    }

                    List <string> inconsistentDLC = new List <string>();

                    void inconsistentDLCFoundCallback(string filepath)
                    {
                        if (targetToBackup.Supported)
                        {
                            Log.Error($@"DLC is in an inconsistent state: {filepath}");
                            inconsistentDLC.Add(filepath);
                        }
                        else
                        {
                            Log.Error(@"Detected an inconsistent DLC, likely due to an unofficial copy of the game");
                        }
                    }

                    ProgressVisible       = true;
                    ProgressIndeterminate = true;
                    BackupStatus          = M3L.GetString(M3L.string_validatingBackupSource);
                    Log.Information(@"Checking target is vanilla");
                    bool isVanilla = VanillaDatabaseService.ValidateTargetAgainstVanilla(targetToBackup, nonVanillaFileFoundCallback);

                    Log.Information(@"Checking DLC consistency");
                    bool isDLCConsistent = VanillaDatabaseService.ValidateTargetDLCConsistency(targetToBackup, inconsistentDLCCallback: inconsistentDLCFoundCallback);

                    Log.Information(@"Checking only vanilla DLC is installed");
                    List <string> dlcModsInstalled = VanillaDatabaseService.GetInstalledDLCMods(targetToBackup).Select(x =>
                    {
                        var tpmi = ThirdPartyServices.GetThirdPartyModInfo(x, targetToBackup.Game);
                        if (tpmi != null)
                        {
                            return($@"{x} ({tpmi.modname})");
                        }
                        return(x);
                    }).ToList();
                    var installedDLC   = VanillaDatabaseService.GetInstalledOfficialDLC(targetToBackup);
                    var allOfficialDLC = MEDirectories.OfficialDLC(targetToBackup.Game);

                    if (installedDLC.Count() < allOfficialDLC.Count())
                    {
                        var dlcList = string.Join("\n - ", allOfficialDLC.Except(installedDLC).Select(x => $@"{MEDirectories.OfficialDLCNames(targetToBackup.Game)[x]} ({x})")); //do not localize
                        dlcList = @" - " + dlcList;
                        Log.Information(@"The following dlc will be missing in the backup if user continues: ");
                        Log.Information(dlcList);

                        Application.Current.Dispatcher.Invoke(delegate
                        {
                            var cancelDueToNotAllDLC = M3L.ShowDialog(window, M3L.GetString(M3L.string_dialog_notAllDLCInstalled, dlcList), M3L.GetString(M3L.string_someDlcNotInstalled), MessageBoxButton.YesNo, MessageBoxImage.Warning);
                            if (cancelDueToNotAllDLC == MessageBoxResult.No)
                            {
                                end = true;
                                EndBackup();
                                return;
                            }
                        });
                    }

                    Log.Information(@"Checking for TexturesMEM TFCs");
                    var memTextures = Directory.GetFiles(targetToBackup.TargetPath, @"TexturesMEM*.tfc", SearchOption.AllDirectories);

                    if (end)
                    {
                        return;
                    }
                    if (isVanilla && isDLCConsistent && !Enumerable.Any(dlcModsInstalled) && !Enumerable.Any(memTextures))
                    {
                        BackupStatus = M3L.GetString(M3L.string_waitingForUserInput);

                        string backupPath = null;
                        if (!targetToBackup.IsCustomOption)
                        {
                            // Creating a new backup
                            nbw.ReportProgress(0, TaskbarProgressBarState.Paused);

                            Application.Current.Dispatcher.Invoke(delegate
                            {
                                Log.Information(@"Prompting user to select backup destination");

                                CommonOpenFileDialog m = new CommonOpenFileDialog
                                {
                                    IsFolderPicker   = true,
                                    EnsurePathExists = true,
                                    Title            = M3L.GetString(M3L.string_selectBackupDestination)
                                };
                                if (m.ShowDialog() == CommonFileDialogResult.Ok)
                                {
                                    backupPath = m.FileName;
                                    Log.Information(@"Backup path chosen: " + backupPath);

                                    bool okToBackup = validateBackupPath(backupPath, targetToBackup);
                                    if (!okToBackup)
                                    {
                                        end = true;
                                        EndBackup();
                                        return;
                                    }
                                }
                                else
                                {
                                    end = true;
                                    EndBackup();
                                    return;
                                }
                            });
                            if (end)
                            {
                                return;
                            }
                            nbw.ReportProgress(0, TaskbarProgressBarState.Indeterminate);
                        }
                        else
                        {
                            Log.Information(@"Linking existing backup at " + targetToBackup.TargetPath);
                            backupPath = targetToBackup.TargetPath;
                            // Linking existing backup
                            Application.Current.Dispatcher.Invoke(delegate
                            {
                                bool okToBackup = validateBackupPath(targetToBackup.TargetPath, targetToBackup);
                                if (!okToBackup)
                                {
                                    end = true;
                                    EndBackup();
                                    return;
                                }
                            });
                        }

                        if (end)
                        {
                            return;
                        }

                        if (!targetToBackup.IsCustomOption)
                        {
                            #region callbacks and copy code

                            // Copy to new backup
                            void fileCopiedCallback()
                            {
                                ProgressValue++;
                                if (ProgressMax > 0)
                                {
                                    nbw.ReportProgress(0, ProgressValue * 1.0 / ProgressMax);
                                }
                            }

                            string dlcFolderpath   = M3Directories.GetDLCPath(targetToBackup) + '\\';
                            int    dlcSubStringLen = dlcFolderpath.Length;

                            bool aboutToCopyCallback(string file)
                            {
                                try
                                {
                                    if (file.Contains(@"\cmmbackup\"))
                                    {
                                        return(false);                               //do not copy cmmbackup files
                                    }
                                    if (file.StartsWith(dlcFolderpath, StringComparison.InvariantCultureIgnoreCase))
                                    {
                                        //It's a DLC!
                                        string dlcname             = file.Substring(dlcSubStringLen);
                                        var    dlcFolderNameEndPos = dlcname.IndexOf('\\');
                                        if (dlcFolderNameEndPos > 0)
                                        {
                                            dlcname = dlcname.Substring(0, dlcFolderNameEndPos);
                                            if (MEDirectories.OfficialDLCNames(targetToBackup.Game)
                                                .TryGetValue(dlcname, out var hrName))
                                            {
                                                BackupStatusLine2 = M3L.GetString(M3L.string_interp_backingUpX,
                                                                                  hrName);
                                            }
                                            else
                                            {
                                                BackupStatusLine2 = M3L.GetString(M3L.string_interp_backingUpX,
                                                                                  dlcname);
                                            }
                                        }
                                        else
                                        {
                                            // Loose files in the DLC folder
                                            BackupStatusLine2 = M3L.GetString(M3L.string_interp_backingUpX,
                                                                              M3L.GetString(M3L.string_basegame));
                                        }
                                    }
                                    else
                                    {
                                        //It's basegame
                                        if (file.EndsWith(@".bik"))
                                        {
                                            BackupStatusLine2 = M3L.GetString(M3L.string_interp_backingUpX,
                                                                              M3L.GetString(M3L.string_movies));
                                        }
                                        else if (new FileInfo(file).Length > 52428800)
                                        {
                                            BackupStatusLine2 = M3L.GetString(M3L.string_interp_backingUpX,
                                                                              Path.GetFileName(file));
                                        }
                                        else
                                        {
                                            BackupStatusLine2 = M3L.GetString(M3L.string_interp_backingUpX,
                                                                              M3L.GetString(M3L.string_basegame));
                                        }
                                    }
                                }
                                catch (Exception e)
                                {
                                    Crashes.TrackError(e, new Dictionary <string, string>()
                                    {
                                        { @"dlcFolderpath", dlcFolderpath },
                                        { @"dlcSubStringLen", dlcSubStringLen.ToString() },
                                        { @"file", file }
                                    });
                                }

                                return(true);
                            }

                            void totalFilesToCopyCallback(int total)
                            {
                                ProgressValue         = 0;
                                ProgressIndeterminate = false;
                                ProgressMax           = total;
                                nbw.ReportProgress(0, TaskbarProgressBarState.Normal);
                            }

                            BackupStatus = M3L.GetString(M3L.string_creatingBackup);
                            Log.Information($@"Backing up {targetToBackup.TargetPath} to {backupPath}");
                            nbw.ReportProgress(0, TaskbarProgressBarState.Normal);
                            CopyDir.CopyAll_ProgressBar(new DirectoryInfo(targetToBackup.TargetPath),
                                                        new DirectoryInfo(backupPath),
                                                        totalItemsToCopyCallback: totalFilesToCopyCallback,
                                                        aboutToCopyCallback: aboutToCopyCallback,
                                                        fileCopiedCallback: fileCopiedCallback,
                                                        ignoredExtensions: new[] { @"*.pdf", @"*.mp3" });
                            #endregion
                        }

                        // Write key
                        switch (Game)
                        {
                        case MEGame.ME1:
                        case MEGame.ME2:
                            Utilities.WriteRegistryKey(App.BACKUP_REGISTRY_KEY, Game + @"VanillaBackupLocation",
                                                       backupPath);
                            break;

                        case MEGame.ME3:
                            Utilities.WriteRegistryKey(App.REGISTRY_KEY_ME3CMM, @"VanillaCopyLocation",
                                                       backupPath);
                            break;
                        }

                        var cmmvanilla = Path.Combine(backupPath, @"cmm_vanilla");
                        if (!File.Exists(cmmvanilla))
                        {
                            Log.Information($@"Writing cmm_vanilla to " + cmmvanilla);
                            File.Create(cmmvanilla).Close();
                        }

                        Log.Information($@"Backup completed.");

                        Analytics.TrackEvent(@"Created a backup", new Dictionary <string, string>()
                        {
                            { @"Game", Game.ToString() },
                            { @"Result", @"Success" },
                            { @"Type", targetToBackup.IsCustomOption ? @"Linked" : @"Copy" }
                        });

                        EndBackup();
                        return;
                    }


                    if (!isVanilla)
                    {
                        //Show UI for non vanilla
                        Analytics.TrackEvent(@"Created a backup", new Dictionary <string, string>()
                        {
                            { @"Game", Game.ToString() },
                            { @"Result", @"Failure, Game modified" }
                        });
                        b.Result = (nonVanillaFiles, M3L.GetString(M3L.string_cannotBackupModifiedGame),
                                    M3L.GetString(M3L.string_followingFilesDoNotMatchTheVanillaDatabase));
                    }
                    else if (!isDLCConsistent)
                    {
                        Analytics.TrackEvent(@"Created a backup", new Dictionary <string, string>()
                        {
                            { @"Game", Game.ToString() },
                            { @"Result", @"Failure, DLC inconsistent" }
                        });
                        if (targetToBackup.Supported)
                        {
                            b.Result = (inconsistentDLC, M3L.GetString(M3L.string_inconsistentDLCDetected),
                                        M3L.GetString(M3L.string_dialogTheFollowingDLCAreInAnInconsistentState));
                        }
                        else
                        {
                            b.Result = (M3L.GetString(M3L.string_inconsistentDLCDetected),
                                        M3L.GetString(M3L.string_inconsistentDLCDetectedUnofficialGame));
                        }
                    }
                    else if (Enumerable.Any(dlcModsInstalled))
                    {
                        Analytics.TrackEvent(@"Created a backup", new Dictionary <string, string>()
                        {
                            { @"Game", Game.ToString() },
                            { @"Result", @"Failure, DLC mods found" }
                        });
                        b.Result = (dlcModsInstalled, M3L.GetString(M3L.string_dlcModsAreInstalled),
                                    M3L.GetString(M3L.string_dialogDLCModsWereDetectedCannotBackup));
                    }
                    else if (Enumerable.Any(memTextures))
                    {
                        Analytics.TrackEvent(@"Created a backup", new Dictionary <string, string>()
                        {
                            { @"Game", Game.ToString() },
                            { @"Result", @"Failure, TexturesMEM files found" }
                        });
                        b.Result = (M3L.GetString(M3L.string_leftoverTextureFilesFound),
                                    M3L.GetString(M3L.string_dialog_foundLeftoverTextureFiles));
                    }
                    EndBackup();
                };
                nbw.RunWorkerCompleted += (a, b) =>
                {
                    if (b.Error != null)
                    {
                        Log.Error($@"Exception occurred in {nbw.Name} thread: {b.Error.Message}");
                    }
                    TaskbarHelper.SetProgressState(TaskbarProgressBarState.NoProgress);
                    if (b.Result is (List <string> listItems, string title, string text))
                    {
                        ListDialog ld = new ListDialog(listItems, title, text, window);
                        ld.Show();
                    }
                    else if (b.Result is (string errortitle, string message))
                    {
                        M3L.ShowDialog(window, message, errortitle, MessageBoxButton.OK, MessageBoxImage.Error);
                    }
                    CommandManager.InvalidateRequerySuggested();
                };
                nbw.RunWorkerAsync();
            }
Esempio n. 9
0
        public void TestM3DirectoryResults()
        {
            GlobalTest.Init();
            List <GameTarget> targets = new List <GameTarget>();
            var root = GlobalTest.GetTestGameFoldersDirectory(MEGame.ME1);

            foreach (var d in Directory.GetDirectories(root))
            {
                GameTarget gt = new GameTarget(MEGame.ME1, d, false, false);
                gt.ValidateTarget();
                if (gt.IsValid)
                {
                    targets.Add(gt);
                }
            }
            root = GlobalTest.GetTestGameFoldersDirectory(MEGame.ME2);
            foreach (var d in Directory.GetDirectories(root))
            {
                GameTarget gt = new GameTarget(MEGame.ME2, d, false, false);
                gt.ValidateTarget();
                if (gt.IsValid)
                {
                    targets.Add(gt);
                }
            }
            root = GlobalTest.GetTestGameFoldersDirectory(MEGame.ME3);
            foreach (var d in Directory.GetDirectories(root))
            {
                GameTarget gt = new GameTarget(MEGame.ME3, d, false, false);
                gt.ValidateTarget();
                if (gt.IsValid)
                {
                    targets.Add(gt);
                }
            }

            foreach (var target in targets)
            {
                string expectedDLCPath;
                string expectedASIPath;
                string expectedBioGamePath;
                string expectedCookedPath;
                string expectedExecutableDir;
                if (target.Game == MEGame.ME1)
                {
                    expectedDLCPath       = Path.Combine(target.TargetPath, @"DLC");
                    expectedASIPath       = Path.Combine(target.TargetPath, @"Binaries", @"asi");
                    expectedBioGamePath   = Path.Combine(target.TargetPath, @"BioGame");
                    expectedCookedPath    = Path.Combine(target.TargetPath, @"BioGame", @"CookedPC");
                    expectedExecutableDir = Path.Combine(target.TargetPath, @"Binaries");
                }
                else if (target.Game == MEGame.ME2)
                {
                    expectedDLCPath       = Path.Combine(target.TargetPath, @"BioGame", @"DLC");
                    expectedASIPath       = Path.Combine(target.TargetPath, @"Binaries", @"asi");
                    expectedBioGamePath   = Path.Combine(target.TargetPath, @"BioGame");
                    expectedCookedPath    = Path.Combine(target.TargetPath, @"BioGame", @"CookedPC");
                    expectedExecutableDir = Path.Combine(target.TargetPath, @"Binaries");
                }
                else
                {
                    expectedDLCPath       = Path.Combine(target.TargetPath, @"BIOGame", @"DLC");
                    expectedASIPath       = Path.Combine(target.TargetPath, @"Binaries", @"Win32", @"asi");
                    expectedBioGamePath   = Path.Combine(target.TargetPath, @"BIOGame");
                    expectedCookedPath    = Path.Combine(target.TargetPath, @"BIOGame", @"CookedPCConsole");
                    expectedExecutableDir = Path.Combine(target.TargetPath, @"Binaries", @"Win32");
                }

                Assert.AreEqual(expectedDLCPath, M3Directories.GetDLCPath(target));
                Assert.AreEqual(expectedASIPath, M3Directories.GetASIPath(target));
                Assert.AreEqual(expectedBioGamePath, M3Directories.GetBioGamePath(target));
                Assert.AreEqual(expectedCookedPath, M3Directories.GetCookedPath(target));
                Assert.AreEqual(expectedExecutableDir, M3Directories.GetExecutableDirectory(target));
            }
        }
        private void CompileIntoGame()
        {
            NamedBackgroundWorker nbw = new NamedBackgroundWorker(@"MixinManager CompileIntoGameThread");
            List <string>         failedApplications = new List <string>();

            nbw.DoWork += (a, b) =>
            {
                BottomLeftMessage   = M3L.GetString(M3L.string_compilingMixins);
                OperationInProgress = true;
                //DEBUG STUFF
#if DEBUG
                int numCoresToApplyWith = 1;
#else
                var numCoresToApplyWith = Environment.ProcessorCount;
                if (numCoresToApplyWith > 4)
                {
                    numCoresToApplyWith = 4;                          //no more than 4 as this uses a lot of memory
                }
#endif

                var mixins = AvailableOfficialMixins.Where(x => x.UISelectedForUse).ToList();
                MixinHandler.LoadPatchDataForMixins(mixins); //before dynamic
                void failedApplicationCallback(string str)
                {
                    failedApplications.Add(str);
                }

                var compilingListsPerModule = MixinHandler.GetMixinApplicationList(mixins, failedApplicationCallback);
                if (Enumerable.Any(failedApplications))
                {
                    //Error building list
                    Log.Information(@"Aborting mixin install due to incompatible selection of mixins");
                    return;
                }

                ProgressBarMax   = mixins.Count();
                ProgressBarValue = 0;
                int numdone = 0;

                void completedSingleApplicationCallback()
                {
                    var val = Interlocked.Increment(ref numdone);

                    ProgressBarValue = val;
                }

                //Mixins are ready to be applied
                Parallel.ForEach(compilingListsPerModule,
                                 new ParallelOptions
                {
                    MaxDegreeOfParallelism = Environment.ProcessorCount > numCoresToApplyWith
                    ? numCoresToApplyWith
                    : Environment.ProcessorCount
                }, mapping =>
                {
                    var dlcFolderName = ModMakerCompiler.ModmakerChunkNameToDLCFoldername(mapping.Key.ToString());
                    //var outdir = Path.Combine(modpath, ModMakerCompiler.HeaderToDefaultFoldername(mapping.Key), @"CookedPCConsole");
                    //Directory.CreateDirectory(outdir);
                    if (mapping.Key == ModJob.JobHeader.BASEGAME)
                    {
                        //basegame
                        foreach (var file in mapping.Value)
                        {
                            try
                            {
                                using var vanillaPackageAsStream = VanillaDatabaseService.FetchBasegameFile(MEGame.ME3, Path.GetFileName(file.Key));
                                //packageAsStream.WriteToFile(@"C:\users\dev\desktop\compressed.pcc");
                                using var decompressedStream = MEPackage.GetDecompressedPackageStream(vanillaPackageAsStream, false, true);
                                decompressedStream.Position  = 0;
                                var vanillaPackage           = MEPackageHandler.OpenMEPackageFromStream(decompressedStream, $@"Vanilla - {Path.GetFileName(file.Key)}");
                                //decompressedStream.WriteToFile(@"C:\users\dev\desktop\decompressed.pcc");

                                using var mixinModifiedStream = MixinHandler.ApplyMixins(decompressedStream, file.Value, true,
                                                                                         completedSingleApplicationCallback, failedApplicationCallback);
                                mixinModifiedStream.Position = 0;
                                var modifiedPackage          = MEPackageHandler.OpenMEPackageFromStream(mixinModifiedStream, $@"Mixin Modified - {Path.GetFileName(file.Key)}");

                                // three way merge: get target stream
                                var targetFile    = Path.Combine(M3Directories.GetCookedPath(SelectedInstallTarget), Path.GetFileName(file.Key));
                                var targetPackage = MEPackageHandler.OpenMEPackage(targetFile);

                                var merged = ThreeWayPackageMerge.AttemptMerge(vanillaPackage, modifiedPackage, targetPackage);
                                if (merged)
                                {
                                    targetPackage.Save(compress: true);
                                    Log.Information(@"Three way merge succeeded for " + targetFile);
                                }
                                else
                                {
                                    Log.Error(@"Could not merge three way merge into " + targetFile);
                                }
                                //var outfile = Path.Combine(outdir, Path.GetFileName(file.Key));
                                //package.save(outfile, false); // don't compress
                                //finalStream.WriteToFile(outfile);
                                //File.WriteAllBytes(outfile, finalStream.ToArray());
                            }
                            catch (Exception e)
                            {
                                var mixinsStr = string.Join(@", ", file.Value.Select(x => x.PatchName));
                                Log.Error($@"Error in mixin application for file {file.Key}: {e.Message}");
                                failedApplicationCallback(M3L.GetString(M3L.string_interp_errorApplyingMixinsForFile, mixinsStr, file.Key, e.Message));
                            }
                        }
                    }
                    else
                    {
                        //dlc
                        var dlcPackage        = VanillaDatabaseService.FetchVanillaSFAR(dlcFolderName); //do not have to open file multiple times.
                        var targetCookedPCDir = Path.Combine(M3Directories.GetDLCPath(SelectedInstallTarget), dlcFolderName, @"CookedPCConsole");
                        var sfar      = mapping.Key == ModJob.JobHeader.TESTPATCH ? M3Directories.GetTestPatchSFARPath(SelectedInstallTarget) : Path.Combine(targetCookedPCDir, @"Default.sfar");
                        bool unpacked = new FileInfo(sfar).Length == 32;
                        DLCPackage targetDLCPackage = unpacked ? null : new DLCPackage(sfar); //cache SFAR target

                        foreach (var file in mapping.Value)
                        {
                            try
                            {
                                using var vanillaPackageAsStream = VanillaDatabaseService.FetchFileFromVanillaSFAR(dlcFolderName, file.Key, forcedDLC: dlcPackage);
                                using var decompressedStream     = MEPackage.GetDecompressedPackageStream(vanillaPackageAsStream);
                                decompressedStream.Position      = 0;
                                var vanillaPackage            = MEPackageHandler.OpenMEPackageFromStream(decompressedStream, $@"VanillaDLC - {Path.GetFileName(file.Key)}");
                                using var mixinModifiedStream = MixinHandler.ApplyMixins(decompressedStream, file.Value, true, completedSingleApplicationCallback, failedApplicationCallback);
                                mixinModifiedStream.Position  = 0;
                                var modifiedPackage           = MEPackageHandler.OpenMEPackageFromStream(mixinModifiedStream, $@"Mixin Modified - {Path.GetFileName(file.Key)}");

                                // three way merge: get target stream
                                // must see if DLC is unpacked first

                                MemoryStream targetFileStream = null;

                                //Packed
                                if (unpacked)
                                {
                                    targetFileStream = new MemoryStream(File.ReadAllBytes(Path.Combine(targetCookedPCDir, file.Key)));
                                }
                                else
                                {
                                    targetFileStream = VanillaDatabaseService.FetchFileFromVanillaSFAR(dlcFolderName, Path.GetFileName(file.Key), forcedDLC: targetDLCPackage);
                                }

                                var targetPackage = MEPackageHandler.OpenMEPackageFromStream(targetFileStream, $@"Target package {dlcFolderName} - {file.Key}, from SFAR: {unpacked}");

                                var merged = ThreeWayPackageMerge.AttemptMerge(vanillaPackage, modifiedPackage, targetPackage);
                                if (merged)
                                {
                                    if (unpacked)
                                    {
                                        targetPackage.Save(Path.Combine(targetCookedPCDir, file.Key));
                                        Log.Information(@"Three way merge succeeded for " + targetPackage.FilePath);
                                    }
                                    else
                                    {
                                        var finalSTream = targetPackage.SaveToStream(false); // No compress. Not sure if we should support doing that though.
                                        targetDLCPackage.ReplaceEntry(finalSTream.ToArray(), targetDLCPackage.FindFileEntry(Path.GetFileName(file.Key)));
                                        Log.Information(@"Three way merge succeeded for " + targetPackage.FilePath);
                                    }
                                }
                                else
                                {
                                    Log.Error(@"Could not three way merge into: " + targetFileStream);
                                }
                            }
                            catch (Exception e)
                            {
                                var mixinsStr = string.Join(@", ", file.Value.Select(x => x.PatchName));
                                Log.Error($@"Error in mixin application for file {file.Key}: {e.Message}");
                                failedApplicationCallback(M3L.GetString(M3L.string_interp_errorApplyingMixinsForFile, mixinsStr, file.Key, e.Message));
                            }

                            //finalStream.WriteToFile(outfile);
                        }
                    }
                });

                MixinHandler.FreeME3TweaksPatchData();
                var percent = 0; //this is used to save a localization
                BottomLeftMessage = M3L.GetString(M3L.string_interp_runningAutoTOCOnGamePercentX, percent);

                //Run autotoc
                void tocingUpdate(int percent)
                {
                    BottomLeftMessage = M3L.GetString(M3L.string_interp_runningAutoTOCOnGamePercentX, percent);
                }

                AutoTOC.RunTOCOnGameTarget(SelectedInstallTarget, tocingUpdate);

                //Generate moddesc
                //IniData ini = new IniData();
                //ini[@"ModManager"][@"cmmver"] = App.HighestSupportedModDesc.ToString(CultureInfo.InvariantCulture); //prevent commas
                //ini[@"ModInfo"][@"game"] = @"ME3";
                //ini[@"ModInfo"][@"modname"] = modname;
                //ini[@"ModInfo"][@"moddev"] = App.AppVersionHR;
                //ini[@"ModInfo"][@"moddesc"] = M3L.GetString(M3L.string_compiledFromTheFollowingMixins);
                //ini[@"ModInfo"][@"modver"] = @"1.0";

                //generateRepaceFilesMapping(ini, modpath);
                //File.WriteAllText(Path.Combine(modpath, @"moddesc.ini"), ini.ToString());
            };
            nbw.RunWorkerCompleted += (a, b) =>
            {
                if (b.Error != null)
                {
                    Log.Error($@"Exception occurred in {nbw.Name} thread: {b.Error.Message}");
                }
                OperationInProgress = false;
                ClearMixinHandler();
                if (failedApplications.Count > 0)
                {
                    var ld = new ListDialog(failedApplications, M3L.GetString(M3L.string_failedToApplyAllMixins), M3L.GetString(M3L.string_theFollowingMixinsFailedToApply), mainwindow);
                    ld.ShowDialog();
                }

                /*if (modpath != null)
                 * {
                 *  OnClosing(new DataEventArgs(modpath));
                 * }
                 * else
                 * {*/
                BottomLeftMessage = M3L.GetString(M3L.string_mixinsInstalledMaybe);
                //}
            };
            CompilePanelButton.IsOpen = false;
            nbw.RunWorkerAsync();
        }
Esempio n. 11
0
        public static void RunGame2EmailMerge(GameTarget target)
        {
            M3MergeDLC.RemoveMergeDLC(target);
            var loadedFiles = MELoadedFiles.GetFilesLoadedInGame(target.Game, gameRootOverride: target.TargetPath);

            // File to base modifications on
            using IMEPackage pcc = MEPackageHandler.OpenMEPackage(loadedFiles[@"BioD_Nor103_Messages.pcc"]);

            // Path to Message templates file - different files for ME2/LE2
            string ResourcesFilePath = $@"MassEffectModManagerCore.modmanager.emailmerge.{target.Game}.103Message_Template_{target.Game}";

            using IMEPackage resources = MEPackageHandler.OpenMEPackageFromStream(Utilities.GetResourceStream(ResourcesFilePath));

            // Startup file to place conditionals and transitions into
            using IMEPackage startup = MEPackageHandler.OpenMEPackageFromStream(Utilities.GetResourceStream(
                                                                                    $@"MassEffectModManagerCore.modmanager.mergedlc.{target.Game}.Startup_{M3MergeDLC.MERGE_DLC_FOLDERNAME}.pcc"));


            var emailInfos        = new List <ME2EmailMergeFile>();
            var jsonSupercedances = M3Directories.GetFileSupercedances(target, new[] { @".json" });

            if (jsonSupercedances.TryGetValue(EMAIL_MERGE_MANIFEST_FILE, out var jsonList))
            {
                jsonList.Reverse();
                foreach (var dlc in jsonList)
                {
                    var jsonFile = Path.Combine(M3Directories.GetDLCPath(target), dlc, target.Game.CookedDirName(),
                                                EMAIL_MERGE_MANIFEST_FILE);
                    emailInfos.Add(JsonConvert.DeserializeObject <ME2EmailMergeFile>(File.ReadAllText(jsonFile)));
                }
            }

            // Sanity checks
            if (!emailInfos.Any() || !emailInfos.SelectMany(e => e.Emails).Any())
            {
                return;
            }

            if (emailInfos.Any(e => e.Game != target.Game))
            {
                throw new Exception("ME2 email merge manifest targets incorrect game");
            }

            // Startup File
            // Could replace this with full instanced path in M3 implementation
            ExportEntry stateEventMapExport = startup.Exports
                                              .First(e => e.ClassName == "BioStateEventMap" && e.ObjectName == "StateTransitionMap");
            BioStateEventMap StateEventMap    = stateEventMapExport.GetBinaryData <BioStateEventMap>();
            ExportEntry      ConditionalClass =
                startup.FindExport($@"PlotManager{M3MergeDLC.MERGE_DLC_FOLDERNAME}.BioAutoConditionals");

            #region Sequence Exports
            // Send message - All email conditionals are checked and emails transitions are triggered
            ExportEntry SendMessageContainer = pcc.FindExport(@"TheWorld.PersistentLevel.Main_Sequence.Send_Messages");
            ExportEntry LastSendMessage      = KismetHelper.GetSequenceObjects(SendMessageContainer).OfType <ExportEntry>()
                                               .FirstOrDefault(e =>
            {
                var outbound = KismetHelper.GetOutboundLinksOfNode(e);
                return(outbound.Count == 1 && outbound[0].Count == 0);
            });
            ExportEntry TemplateSendMessage          = resources.FindExport(@"TheWorld.PersistentLevel.Main_Sequence.Send_MessageTemplate");
            ExportEntry TemplateSendMessageBoolCheck = resources.FindExport(@"TheWorld.PersistentLevel.Main_Sequence.Send_MessageTemplate_BoolCheck");


            // Mark Read - email ints are set to read
            // This is the only section that does not gracefully handle different DLC installations - DLC_CER is required atm
            ExportEntry MarkReadContainer = pcc.FindExport(@"TheWorld.PersistentLevel.Main_Sequence.Mark_Read");
            ExportEntry LastMarkRead      = pcc.FindExport(@"TheWorld.PersistentLevel.Main_Sequence.Mark_Read.DLC_CER");
            ExportEntry MarkReadOutLink   = KismetHelper.GetOutboundLinksOfNode(LastMarkRead)[0][0].LinkedOp as ExportEntry;
            KismetHelper.RemoveOutputLinks(LastMarkRead);

            ExportEntry TemplateMarkRead           = resources.FindExport(@"TheWorld.PersistentLevel.Main_Sequence.Mark_ReadTemplate");
            ExportEntry TemplateMarkReadTransition = resources.FindExport(@"TheWorld.PersistentLevel.Main_Sequence.Mark_Read_Transition");


            // Display Messages - Str refs are passed through to GUI
            ExportEntry DisplayMessageContainer =
                pcc.FindExport(@"TheWorld.PersistentLevel.Main_Sequence.Display_Messages");
            ExportEntry DisplayMessageOutLink =
                pcc.FindExport(@"TheWorld.PersistentLevel.Main_Sequence.Display_Messages.SeqCond_CompareBool_0");

            ExportEntry LastDisplayMessage = SeqTools.FindOutboundConnectionsToNode(DisplayMessageOutLink, KismetHelper.GetSequenceObjects(DisplayMessageContainer).OfType <ExportEntry>())[0];
            KismetHelper.RemoveOutputLinks(LastDisplayMessage);
            var         DisplayMessageVariableLinks = LastDisplayMessage.GetProperty <ArrayProperty <StructProperty> >("VariableLinks");
            ExportEntry TemplateDisplayMessage      =
                resources.FindExport(@"TheWorld.PersistentLevel.Main_Sequence.Display_MessageTemplate");

            // Archive Messages - Message ints are set to 3
            ExportEntry ArchiveContainer = pcc.FindExport(@"TheWorld.PersistentLevel.Main_Sequence.Archive_Message");
            ExportEntry ArchiveSwitch    = pcc.FindExport(@"TheWorld.PersistentLevel.Main_Sequence.Archive_Message.SeqAct_Switch_0");
            ExportEntry ArchiveOutLink   =
                pcc.FindExport(
                    @"TheWorld.PersistentLevel.Main_Sequence.Archive_Message.BioSeqAct_PMCheckConditional_1");
            ExportEntry ExampleSetInt  = KismetHelper.GetOutboundLinksOfNode(ArchiveSwitch)[0][0].LinkedOp as ExportEntry;
            ExportEntry ExamplePlotInt = SeqTools.GetVariableLinksOfNode(ExampleSetInt)[0].LinkedNodes[0] as ExportEntry;
            #endregion

            int messageID      = KismetHelper.GetOutboundLinksOfNode(ArchiveSwitch).Count + 1;
            int currentSwCount = ArchiveSwitch.GetProperty <IntProperty>("LinkCount").Value;

            foreach (var emailMod in emailInfos)
            {
                string modName = "DLC_MOD_" + emailMod.ModName;

                foreach (var email in emailMod.Emails)
                {
                    string emailName = modName + "_" + email.EmailName;

                    // Create send transition
                    int transitionId  = WriteTransition(StateEventMap, email.StatusPlotInt);
                    int conditionalId = WriteConditional(email.TriggerConditional);

                    #region SendMessage
                    //////////////
                    // SendMessage
                    //////////////

                    // Create seq object
                    var SMTemp = emailMod.InMemoryBool.HasValue ? TemplateSendMessageBoolCheck : TemplateSendMessage;
                    EntryImporter.ImportAndRelinkEntries(EntryImporter.PortingOption.CloneTreeAsChild,
                                                         SMTemp, pcc, SendMessageContainer, true, new RelinkerOptionsPackage(), out var outSendEntry);

                    var newSend = outSendEntry as ExportEntry;

                    // Set name, comment, add to sequence
                    newSend.ObjectName = new NameReference(emailName);
                    KismetHelper.AddObjectToSequence(newSend, SendMessageContainer);
                    KismetHelper.SetComment(newSend, emailName);
                    if (target.Game == MEGame.ME2)
                    {
                        newSend.WriteProperty(new StrProperty(emailName, "ObjName"));
                    }

                    // Set Trigger Conditional
                    var pmCheckConditionalSM = newSend.GetChildren()
                                               .FirstOrDefault(e => e.ClassName == "BioSeqAct_PMCheckConditional" && e is ExportEntry);
                    if (pmCheckConditionalSM is ExportEntry conditional)
                    {
                        conditional.WriteProperty(new IntProperty(conditionalId, "m_nIndex"));
                        KismetHelper.SetComment(conditional, "Time for " + email.EmailName + "?");
                    }

                    // Set Send Transition
                    var pmExecuteTransitionSM = newSend.GetChildren()
                                                .FirstOrDefault(e => e.ClassName == "BioSeqAct_PMExecuteTransition" && e is ExportEntry);
                    if (pmExecuteTransitionSM is ExportEntry transition)
                    {
                        transition.WriteProperty(new IntProperty(transitionId, "m_nIndex"));
                        KismetHelper.SetComment(transition, "Send " + email.EmailName + " message.");
                    }

                    // Set Send Transition
                    if (emailMod.InMemoryBool.HasValue)
                    {
                        var pmCheckStateSM = newSend.GetChildren()
                                             .FirstOrDefault(e => e.ClassName == "BioSeqAct_PMCheckState" && e is ExportEntry);
                        if (pmCheckStateSM is ExportEntry checkState)
                        {
                            checkState.WriteProperty(new IntProperty(emailMod.InMemoryBool.Value, "m_nIndex"));
                            KismetHelper.SetComment(checkState, "Is " + emailMod.ModName + " installed?");
                        }
                    }

                    // Hook up output links
                    KismetHelper.CreateOutputLink(LastSendMessage, "Out", newSend);
                    LastSendMessage = newSend;
                    #endregion

                    #region MarkRead
                    ///////////
                    // MarkRead
                    ///////////

                    // Create seq object
                    var MRTemp = email.ReadTransition.HasValue ? TemplateMarkReadTransition : TemplateMarkRead;
                    EntryImporter.ImportAndRelinkEntries(EntryImporter.PortingOption.CloneTreeAsChild,
                                                         MRTemp, pcc, MarkReadContainer, true, new RelinkerOptionsPackage(), out var outMarkReadEntry);
                    var newMarkRead = outMarkReadEntry as ExportEntry;

                    // Set name, comment, add to sequence
                    newMarkRead.ObjectName = new NameReference(emailName);
                    KismetHelper.AddObjectToSequence(newMarkRead, MarkReadContainer);
                    KismetHelper.SetComment(newMarkRead, emailName);
                    if (target.Game == MEGame.ME2)
                    {
                        newMarkRead.WriteProperty(new StrProperty(emailName, "ObjName"));
                    }

                    // Set Plot Int
                    var storyManagerIntMR = newMarkRead.GetChildren()
                                            .FirstOrDefault(e => e.ClassName == "BioSeqVar_StoryManagerInt" && e is ExportEntry);
                    if (storyManagerIntMR is ExportEntry plotIntMR)
                    {
                        plotIntMR.WriteProperty(new IntProperty(email.StatusPlotInt, "m_nIndex"));
                        KismetHelper.SetComment(plotIntMR, email.EmailName);
                    }

                    if (email.ReadTransition.HasValue)
                    {
                        var pmExecuteTransitionMR = newMarkRead.GetChildren()
                                                    .FirstOrDefault(e => e.ClassName == "BioSeqAct_PMExecuteTransition" && e is ExportEntry);
                        if (pmExecuteTransitionMR is ExportEntry transitionMR)
                        {
                            transitionMR.WriteProperty(new IntProperty(email.ReadTransition.Value, "m_nIndex"));
                            KismetHelper.SetComment(transitionMR, "Trigger " + email.EmailName + " read transition");
                        }
                    }

                    // Hook up output links
                    KismetHelper.CreateOutputLink(LastMarkRead, "Out", newMarkRead);
                    LastMarkRead = newMarkRead;
                    #endregion

                    #region DisplayEmail
                    ////////////////
                    // Display Email
                    ////////////////

                    // Create seq object
                    EntryImporter.ImportAndRelinkEntries(EntryImporter.PortingOption.CloneTreeAsChild,
                                                         TemplateDisplayMessage, pcc, DisplayMessageContainer, true, new RelinkerOptionsPackage(), out var outDisplayMessage);
                    var newDisplayMessage = outDisplayMessage as ExportEntry;

                    // Set name, comment, variable links, add to sequence
                    newDisplayMessage.ObjectName = new NameReference(emailName);
                    KismetHelper.AddObjectToSequence(newDisplayMessage, DisplayMessageContainer);
                    newDisplayMessage.WriteProperty(DisplayMessageVariableLinks);
                    KismetHelper.SetComment(newDisplayMessage, emailName);
                    if (target.Game == MEGame.ME2)
                    {
                        newDisplayMessage.WriteProperty(new StrProperty(emailName, "ObjName"));
                    }

                    var displayChildren = newDisplayMessage.GetChildren();

                    // Set Plot Int
                    var storyManagerIntDE = displayChildren.FirstOrDefault(e =>
                                                                           e.ClassName == "BioSeqVar_StoryManagerInt" && e is ExportEntry);
                    if (storyManagerIntDE is ExportEntry plotIntDE)
                    {
                        plotIntDE.WriteProperty(new IntProperty(email.StatusPlotInt, "m_nIndex"));
                    }

                    // Set Email ID
                    var emailIdDE = displayChildren.FirstOrDefault(e =>
                                                                   e.ClassName == "SeqVar_Int" && e is ExportEntry);
                    if (emailIdDE is ExportEntry EmailIDDE)
                    {
                        EmailIDDE.WriteProperty(new IntProperty(messageID, "IntValue"));
                    }

                    // Set Title StrRef
                    var titleStrRef = displayChildren.FirstOrDefault(e =>
                                                                     e.ClassName == "BioSeqVar_StrRef" && e is ExportEntry ee && ee.GetProperty <NameProperty>("VarName").Value == "Title StrRef");
                    if (titleStrRef is ExportEntry Title)
                    {
                        Title.WriteProperty(new StringRefProperty(email.TitleStrRef, "m_srValue"));
                    }

                    // Set Description StrRef
                    var descStrRef = displayChildren.FirstOrDefault(e =>
                                                                    e.ClassName == "BioSeqVar_StrRef" && e is ExportEntry ee && ee.GetProperty <NameProperty>("VarName").Value == "Desc StrRef");
                    if (descStrRef is ExportEntry Desc)
                    {
                        Desc.WriteProperty(new StringRefProperty(email.DescStrRef, "m_srValue"));
                    }

                    // Hook up output links
                    KismetHelper.CreateOutputLink(LastDisplayMessage, "Out", newDisplayMessage);
                    LastDisplayMessage = newDisplayMessage;
                    #endregion

                    #region ArchiveEmail
                    ////////////////
                    // Archive Email
                    ////////////////

                    var NewSetInt = EntryCloner.CloneEntry(ExampleSetInt);
                    KismetHelper.AddObjectToSequence(NewSetInt, ArchiveContainer);
                    KismetHelper.CreateOutputLink(NewSetInt, "Out", ArchiveOutLink);

                    KismetHelper.CreateNewOutputLink(ArchiveSwitch, "Link " + (messageID - 1), NewSetInt);

                    var NewPlotInt = EntryCloner.CloneEntry(ExamplePlotInt);
                    KismetHelper.AddObjectToSequence(NewPlotInt, ArchiveContainer);
                    NewPlotInt.WriteProperty(new IntProperty(email.StatusPlotInt, "m_nIndex"));
                    NewPlotInt.WriteProperty(new StrProperty(emailName, "m_sRefName"));

                    var linkedVars = SeqTools.GetVariableLinksOfNode(NewSetInt);
                    linkedVars[0].LinkedNodes = new List <IEntry>()
                    {
                        NewPlotInt
                    };
                    SeqTools.WriteVariableLinksToNode(NewSetInt, linkedVars);

                    messageID++;
                    currentSwCount++;
                    #endregion
                }
            }
            KismetHelper.CreateOutputLink(LastMarkRead, "Out", MarkReadOutLink);
            KismetHelper.CreateOutputLink(LastDisplayMessage, "Out", DisplayMessageOutLink);
            ArchiveSwitch.WriteProperty(new IntProperty(currentSwCount, "LinkCount"));

            stateEventMapExport.WriteBinary(StateEventMap);

            // Save Messages file into DLC
            var cookedDir   = Path.Combine(M3Directories.GetDLCPath(target), M3MergeDLC.MERGE_DLC_FOLDERNAME, target.Game.CookedDirName());
            var outMessages = Path.Combine(cookedDir, @"BioD_Nor103_Messages.pcc");
            pcc.Save(outMessages);

            // Save Startup file into DLC
            var startupF = Path.Combine(cookedDir, $@"Startup_{M3MergeDLC.MERGE_DLC_FOLDERNAME}.pcc");
            startup.Save(startupF);
        }
        public static bool RunPlotManagerUpdate(GameTarget target)
        {
            Log.Information($@"Updating PlotManager for game: {target.TargetPath}");
            var supercedances = M3Directories.GetFileSupercedances(target, new[] { @".pmu" });
            Dictionary <string, string> funcMap = new();
            List <string> combinedNames         = new List <string>();

            if (supercedances.TryGetValue(@"PlotManagerUpdate.pmu", out var supercedanes))
            {
                supercedanes.Reverse(); // list goes from highest to lowest. We want to build in lowest to highest
                StringBuilder sb             = null;
                string        currentFuncNum = null;
                var           metaMaps       = M3Directories.GetMetaMappedInstalledDLC(target, false);
                foreach (var pmuDLCName in supercedanes)
                {
                    var uiName = metaMaps[pmuDLCName]?.ModName ?? ThirdPartyServices.GetThirdPartyModInfo(pmuDLCName, target.Game)?.modname ?? pmuDLCName;
                    combinedNames.Add(uiName);
                    var text = File.ReadAllLines(Path.Combine(M3Directories.GetDLCPath(target), pmuDLCName, target.Game.CookedDirName(), @"PlotManagerUpdate.pmu"));
                    foreach (var line in text)
                    {
                        if (line.StartsWith(@"public function bool F"))
                        {
                            if (sb != null)
                            {
                                funcMap[currentFuncNum] = sb.ToString();
                                Log.Information($@"PlotSync: Adding function {currentFuncNum} from {pmuDLCName}");
                                currentFuncNum = null;
                            }

                            sb = new StringBuilder();
                            sb.AppendLine(line);

                            // Method name
                            currentFuncNum = line.Substring(22);
                            currentFuncNum = currentFuncNum.Substring(0, currentFuncNum.IndexOf('('));
                            if (int.TryParse(currentFuncNum, out var num))
                            {
                                if (num <= 0)
                                {
                                    Log.Error($@"Skipping plot manager update: Conditional {num} is not a valid number for use. Values must be greater than 0 and less than 2 billion.");
                                    Analytics.TrackEvent(@"Bad plot manager function", new Dictionary <string, string>()
                                    {
                                        { @"FunctionName", $@"F{currentFuncNum}" },
                                        { @"DLCName", pmuDLCName }
                                    });
                                    sb = null;
                                    return(false);
                                }
                                else if (num.ToString().Length != currentFuncNum.Length)
                                {
                                    Log.Error($@"Skipping plot manager update: Conditional {currentFuncNum} is not a valid number for use. Values must not contain leading zeros");
                                    Analytics.TrackEvent(@"Bad plot manager function", new Dictionary <string, string>()
                                    {
                                        { @"FunctionName", $@"F{currentFuncNum}" },
                                        { @"DLCName", pmuDLCName }
                                    });
                                    sb = null;
                                    return(false);
                                }
                            }
                            else
                            {
                                Log.Error($@"Skipping plot manager update: Conditional {currentFuncNum} is not a valid number for use. Values must be greater than 0 and less than 2 billion.");
                                Analytics.TrackEvent(@"Bad plot manager function", new Dictionary <string, string>()
                                {
                                    { @"FunctionName", $@"F{currentFuncNum}" },
                                    { @"DLCName", pmuDLCName }
                                });
                                sb = null;
                                return(false);
                            }
                        }
                        else
                        {
                            sb?.AppendLine(line);
                        }
                    }

                    // Add final, if any was found
                    if (sb != null)
                    {
                        funcMap[currentFuncNum] = sb.ToString();
                        Log.Information($@"PlotSync: Adding function {currentFuncNum} from {pmuDLCName}");
                    }
                }
            }

            var pmPath = GetPlotManagerPath(target);
            var vpm    = Utilities.ExtractInternalFileToStream($@"MassEffectModManagerCore.modmanager.plotmanager.{target.Game}.PlotManager.{(target.Game == MEGame.ME1 ? @"u" : @"pcc")}"); // do not localize

            if (funcMap.Any())
            {
                var plotManager      = MEPackageHandler.OpenMEPackageFromStream(vpm, $@"PlotManager.{(target.Game == MEGame.ME1 ? @"u" : @"pcc")}"); // do not localize
                var clonableFunction = plotManager.Exports.FirstOrDefault(x => x.ClassName == @"Function");

                // STEP 1: ADD ALL NEW FUNCTIONS BEFORE WE INITIALIZE THE FILELIB.
                foreach (var v in funcMap)
                {
                    var pmKey = $@"BioAutoConditionals.F{v.Key}";
                    var exp   = plotManager.FindExport(pmKey);
                    if (exp == null)
                    {
                        // Adding a new conditional
                        exp            = EntryCloner.CloneEntry(clonableFunction);
                        exp.ObjectName = new NameReference($@"F{v.Key}", 0);
                        exp.FileRef.InvalidateLookupTable(); // We changed the name.

                        // Reduces trash
                        UFunction uf = ObjectBinary.From <UFunction>(exp);
                        uf.Children    = 0;
                        uf.ScriptBytes = Array.Empty <byte>(); // No script data
                        exp.WriteBinary(uf);
                        Log.Information($@"Generated new blank conditional function export: {exp.UIndex} {exp.InstancedFullPath}", Settings.LogModInstallation);
                    }
                }

                // Relink child chain
                UClass uc = ObjectBinary.From <UClass>(plotManager.FindExport(@"BioAutoConditionals"));
                uc.UpdateChildrenChain();
                uc.UpdateLocalFunctions();
                uc.Export.WriteBinary(uc);


                // STEP 2: UPDATE FUNCTIONS
                Stopwatch sw          = Stopwatch.StartNew();
                var       fl          = new FileLib(plotManager);
                bool      initialized = fl.Initialize(new RelativePackageCache()
                {
                    RootPath = M3Directories.GetBioGamePath(target)
                }, target.TargetPath, canUseBinaryCache: false);
                if (!initialized)
                {
                    Log.Error(@"Error initializing FileLib for plot manager sync:");
                    foreach (var v in fl.InitializationLog.AllErrors)
                    {
                        Log.Error(v.Message);
                    }
                    throw new Exception(M3L.GetString(M3L.string_interp_fileLibInitFailedPlotManager, string.Join(Environment.NewLine, fl.InitializationLog.AllErrors.Select(x => x.Message)))); //force localize
                }
                sw.Stop();
                Debug.WriteLine($@"Took {sw.ElapsedMilliseconds}ms to load filelib");

                bool relinkChain = false;
                foreach (var v in funcMap)
                {
                    var pmKey = $@"BioAutoConditionals.F{v.Key}";
                    Log.Information($@"Updating conditional entry: {pmKey}", Settings.LogModInstallation);
                    var exp = plotManager.FindExport(pmKey);
                    (_, MessageLog log) = UnrealScriptCompiler.CompileFunction(exp, v.Value, fl);
                    if (log.AllErrors.Any())
                    {
                        Log.Error($@"Error compiling function {exp.InstancedFullPath}:");
                        foreach (var l in log.AllErrors)
                        {
                            Log.Error(l.Message);
                        }

                        throw new Exception(M3L.GetString(M3L.string_interp_errorCompilingFunctionReason, exp, string.Join('\n', log.AllErrors.Select(x => x.Message))));
                    }
                }

                if (plotManager.IsModified)
                {
                    plotManager.Save(pmPath, true);
                    // Update local file DB
                    var bgfe = new BasegameFileIdentificationService.BasegameCloudDBFile(pmPath.Substring(target.TargetPath.Length + 1), (int)new FileInfo(pmPath).Length, target.Game, M3L.GetString(M3L.string_interp_plotManagerSyncForX, string.Join(@", ", combinedNames)), Utilities.CalculateMD5(pmPath));
                    BasegameFileIdentificationService.AddLocalBasegameIdentificationEntries(new List <BasegameFileIdentificationService.BasegameCloudDBFile>(new[] { bgfe }));
                }
            }
            else
            {
                // Just write out vanilla.
                vpm.WriteToFile(pmPath);
            }

            return(true);
        }
        public static bool RunTOCOnGameTarget(GameTarget target, Action <int> percentDoneCallback = null)
        {
            Log.Information(@"Autotocing game: " + target.TargetPath);

            //get toc target folders, ensuring we clean up the inputs a bit.
            string baseDir    = Path.GetFullPath(Path.Combine(target.TargetPath, @"BIOGame"));
            string dlcDirRoot = M3Directories.GetDLCPath(target);

            if (!Directory.Exists(dlcDirRoot))
            {
                Log.Error(@"Specified game directory does not appear to be a Mass Effect 3 root game directory (DLC folder missing).");
                return(false);
            }

            var tocTargets = (new DirectoryInfo(dlcDirRoot)).GetDirectories().Select(x => x.FullName).Where(x => Path.GetFileName(x).StartsWith(@"DLC_", StringComparison.OrdinalIgnoreCase)).ToList();

            tocTargets.Add(baseDir);
            tocTargets.Add(Path.Combine(target.TargetPath, @"BIOGame\Patches\PCConsole\Patch_001.sfar"));

            //Debug.WriteLine("Found TOC Targets:");
            //tocTargets.ForEach(x => Debug.WriteLine(x));
            //Debug.WriteLine("=====Generating TOC Files=====");
            int done = 0;

            foreach (var tocTarget in tocTargets)
            {
                string sfar = Path.Combine(tocTarget, SFAR_SUBPATH);
                if (tocTarget.EndsWith(@".sfar"))
                {
                    //TestPatch
                    var        watch     = Stopwatch.StartNew();
                    DLCPackage dlc       = new DLCPackage(tocTarget);
                    var        tocResult = dlc.UpdateTOCbin();
                    watch.Stop();
                    if (tocResult == DLCPackage.DLCTOCUpdateResult.RESULT_UPDATE_NOT_NECESSARY)
                    {
                        Log.Information($@"TOC is already up to date in {tocTarget}");
                    }
                    else if (tocResult == DLCPackage.DLCTOCUpdateResult.RESULT_UPDATED)
                    {
                        var elapsedMs = watch.ElapsedMilliseconds;
                        Log.Information($@"{tocTarget} - Ran SFAR TOC, took {elapsedMs}ms");
                    }
                }
                else if (ME3Directory.OfficialDLCNames.ContainsKey(Path.GetFileName(tocTarget)))
                {
                    //Official DLC
                    if (File.Exists(sfar))
                    {
                        if (new FileInfo(sfar).Length == 32) //DLC is unpacked for sure
                        {
                            CreateUnpackedTOC(tocTarget);
                        }
                        else
                        {
                            //AutoTOC it - SFAR is not unpacked
                            var watch = System.Diagnostics.Stopwatch.StartNew();

                            DLCPackage dlc       = new DLCPackage(sfar);
                            var        tocResult = dlc.UpdateTOCbin();
                            watch.Stop();
                            if (tocResult == DLCPackage.DLCTOCUpdateResult.RESULT_ERROR_NO_ENTRIES)
                            {
                                Log.Information($@"No DLC entries in SFAR... Suspicious. Creating empty TOC for {tocTarget}");
                                CreateUnpackedTOC(tocTarget);
                            }
                            else if (tocResult == DLCPackage.DLCTOCUpdateResult.RESULT_UPDATE_NOT_NECESSARY)
                            {
                                Log.Information($@"TOC is already up to date in {tocTarget}");
                            }
                            else if (tocResult == DLCPackage.DLCTOCUpdateResult.RESULT_UPDATED)
                            {
                                var elapsedMs = watch.ElapsedMilliseconds;
                                Log.Information($@"{Path.GetFileName(tocTarget)} - Ran SFAR TOC, took {elapsedMs}ms");
                            }
                        }
                    }
                }
                else
                {
                    //TOC it unpacked style
                    // Console.WriteLine(foldername + ", - UNPACKED TOC");
                    CreateUnpackedTOC(tocTarget);
                }

                done++;
                percentDoneCallback?.Invoke((int)Math.Floor(done * 100.0 / tocTargets.Count));
            }
            return(true);
        }