private static Mod AttemptLoadVirtualMod(ArchiveFileInfo sfarEntry, SevenZipExtractor archive, Mod.MEGame game, string md5) { var sfarPath = sfarEntry.FileName; var cookedPath = FilesystemInterposer.DirectoryGetParent(sfarPath, true); //Todo: Check if value is CookedPC/CookedPCConsole as further validation if (!string.IsNullOrEmpty(FilesystemInterposer.DirectoryGetParent(cookedPath, true))) { var dlcDir = FilesystemInterposer.DirectoryGetParent(cookedPath, true); var dlcFolderName = Path.GetFileName(dlcDir); if (!string.IsNullOrEmpty(dlcFolderName)) { var thirdPartyInfo = ThirdPartyServices.GetThirdPartyModInfo(dlcFolderName, game); if (thirdPartyInfo != null) { Log.Information($@"Third party mod found: {thirdPartyInfo.modname}, preparing virtual moddesc.ini"); //We will have to load a virtual moddesc. Since Mod constructor requires reading an ini, we will build and feed it a virtual one. IniData virtualModDesc = new IniData(); virtualModDesc[@"ModManager"][@"cmmver"] = App.HighestSupportedModDesc.ToString(); virtualModDesc[@"ModManager"][@"importedby"] = App.BuildNumber.ToString(); virtualModDesc[@"ModInfo"][@"game"] = @"ME3"; virtualModDesc[@"ModInfo"][@"modname"] = thirdPartyInfo.modname; virtualModDesc[@"ModInfo"][@"moddev"] = thirdPartyInfo.moddev; virtualModDesc[@"ModInfo"][@"modsite"] = thirdPartyInfo.modsite; virtualModDesc[@"ModInfo"][@"moddesc"] = thirdPartyInfo.moddesc; virtualModDesc[@"ModInfo"][@"unofficial"] = @"true"; if (int.TryParse(thirdPartyInfo.updatecode, out var updatecode) && updatecode > 0) { virtualModDesc[@"ModInfo"][@"updatecode"] = updatecode.ToString(); virtualModDesc[@"ModInfo"][@"modver"] = 0.001.ToString(CultureInfo.InvariantCulture); //This will force mod to check for update after reload } else { virtualModDesc[@"ModInfo"][@"modver"] = 0.0.ToString(CultureInfo.InvariantCulture); //Will attempt to look up later after mods have parsed. } virtualModDesc[@"CUSTOMDLC"][@"sourcedirs"] = dlcFolderName; virtualModDesc[@"CUSTOMDLC"][@"destdirs"] = dlcFolderName; virtualModDesc[@"UPDATES"][@"originalarchivehash"] = md5; var archiveSize = new FileInfo(archive.FileName).Length; var importingInfos = ThirdPartyServices.GetImportingInfosBySize(archiveSize); if (importingInfos.Count == 1 && importingInfos[0].GetParsedRequiredDLC().Count > 0) { OnlineContent.QueryModRelay(importingInfos[0].md5, archiveSize); //Tell telemetry relay we are accessing the TPIS for an existing item so it can update latest for tracking virtualModDesc[@"ModInfo"][@"requireddlc"] = importingInfos[0].requireddlc; } return(new Mod(virtualModDesc.ToString(), FilesystemInterposer.DirectoryGetParent(dlcDir, true), archive)); } } else { Log.Information($@"No third party mod information for importing {dlcFolderName}. Should this be supported for import? Contact Mgamerz on the ME3Tweaks Discord if it should."); } } return(null); }
public void OnSelectedTargetChanged() { InstalledDLCs.ClearEx(); if (SelectedTarget != null) { // maps DLC folder name -> mount number var installedDlc = VanillaDatabaseService.GetInstalledOfficialDLC(SelectedTarget, true); foreach (var dlc in installedDlc) { Debug.WriteLine(dlc); var foldername = dlc.TrimStart('x'); InstalledDLCs.Add(new InstalledDLC() { target = SelectedTarget, DLCFolderName = dlc, UIDLCFolderName = foldername, Enabled = !dlc.StartsWith('x'), HumanName = ThirdPartyServices.GetThirdPartyModInfo(foldername, SelectedTarget.Game).modname }); } } }
private Mod GenerateCompatibilityPackForFiles(List <string> dlcModList, List <string> filesToBePatched, SevenZipExtractor uiArchive) { dlcModList = dlcModList.Select(x => { var tpmi = ThirdPartyServices.GetThirdPartyModInfo(x, MEGame.ME3); if (tpmi == null) { return(x); } return(tpmi.modname); }).ToList(); string dlcs = string.Join(@"\n - ", dlcModList); StarterKitOptions sko = new StarterKitOptions { ModName = M3L.GetString(M3L.string_guiCompatibilityPack), ModDescription = M3L.GetString(M3L.string_interp_compatPackModDescription, dlcs, DateTime.Now.ToString()), ModDeveloper = App.AppVersionHR, ModDLCFolderName = UI_MOD_NAME, ModGame = MEGame.ME3, ModInternalName = @"UI Mod Compatibility Pack", ModInternalTLKID = 1420400890, ModMountFlag = EMountFileFlag.ME3_SPOnly_NoSaveFileDependency, ModMountPriority = 31050, ModURL = null, ModModuleNumber = 0 }; Mod generatedMod = null; void modGenerated(Mod mod) { generatedMod = mod; lock (modGeneratedSignaler) { Monitor.Pulse(modGeneratedSignaler); } } void skUicallback(string text) { ActionSubstring = text; } StarterKitGeneratorWindow.CreateStarterKitMod(sko, skUicallback, modGenerated); lock (modGeneratedSignaler) { Monitor.Wait(modGeneratedSignaler); } //Mod has been generated. string outputPath = Path.Combine(generatedMod.ModPath, @"DLC_MOD_" + UI_MOD_NAME, @"CookedPCConsole"); ActionString = M3L.GetString(M3L.string_preparingUiLibrary); ActionSubstring = M3L.GetString(M3L.string_decompressingData); int done = 0; CaseInsensitiveDictionary <byte[]> uiLibraryData = new CaseInsensitiveDictionary <byte[]>(); var filesToDecompress = uiArchive.ArchiveFileData.Where(x => x.FileName.EndsWith(@".swf")).ToList(); foreach (var f in filesToDecompress) { MemoryStream decompressedStream = new MemoryStream(); uiArchive.ExtractFile(f.Index, decompressedStream); string fname = f.FileName.Substring(f.FileName.IndexOf('\\') + 1); fname = fname.Substring(0, fname.IndexOf(@".swf", StringComparison.InvariantCultureIgnoreCase)); uiLibraryData[fname] = decompressedStream.ToArray(); done++; Percent = getPercent(done, filesToDecompress.Count); } ActionString = M3L.GetString(M3L.string_patchingFiles); Percent = 0; done = 0; string singlesuffix = M3L.GetString(M3L.string_singularFile); string pluralsuffix = M3L.GetString(M3L.string_pluralFiles); ActionSubstring = M3L.GetString(M3L.string_interp_patchedXY, done.ToString(), done == 1 ? singlesuffix : pluralsuffix); foreach (var file in filesToBePatched) { var package = MEPackageHandler.OpenMEPackage(file); var guiExports = package.Exports.Where(x => !x.IsDefaultObject && x.ClassName == @"GFxMovieInfo").ToList(); if (guiExports.Count > 0) { //potential item needing replacement //Check GUI library to see if we have anything. foreach (var export in guiExports) { if (uiLibraryData.TryGetValue(export.GetFullPath, out var newData)) { //Patching this export. var exportProperties = export.GetProperties(); var rawData = exportProperties.GetProp <ArrayProperty <ByteProperty> >(@"RawData"); rawData.Clear(); rawData.AddRange(newData.Select(x => new ByteProperty(x))); //This will be terribly slow. Need to port over new ME3Exp binary data handler export.WriteProperties(exportProperties); } else { Debug.WriteLine(@"Not patching gui export, file not in library: " + export.GetFullPath); } } var outpath = Path.Combine(outputPath, Path.GetFileName(package.FilePath)); if (package.IsModified) { Log.Information(@"Saving patched package to " + outpath); package.save(outpath); done++; ActionSubstring = M3L.GetString(M3L.string_interp_patchedXY, done.ToString(), done == 1 ? singlesuffix : pluralsuffix); } else { done++; Log.Information(@"File was patched but data did not change! " + outpath); } Percent = getPercent(done, filesToBePatched.Count); } else { throw new Exception($@"Error: {Path.GetFileName(file)} doesn't contain GUI exports! This shouldn't have been possible."); } } return(generatedMod); }
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) { window.TaskBarItemInfoHandler.ProgressValue = d; } else if (b.UserState is TaskbarItemProgressState tbs) { window.TaskBarItemInfoHandler.ProgressState = 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(); List <string> installedDLC = VanillaDatabaseService.GetInstalledOfficialDLC(targetToBackup); List <string> 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: " + 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; } }); } if (end) { return; } if (isVanilla && isDLCConsistent && dlcModsInstalled.Count == 0) { BackupStatus = M3L.GetString(M3L.string_waitingForUserInput); string backupPath = null; if (!targetToBackup.IsCustomOption) { // Creating a new backup nbw.ReportProgress(0, TaskDialogProgressBarState.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, TaskbarItemProgressState.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 = MEDirectories.DLCPath(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, TaskbarItemProgressState.Normal); } BackupStatus = M3L.GetString(M3L.string_creatingBackup); Log.Information($@"Backing up {targetToBackup.TargetPath} to {backupPath}"); nbw.ReportProgress(0, TaskbarItemProgressState.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 Mod.MEGame.ME1: case Mod.MEGame.ME2: Utilities.WriteRegistryKey(App.BACKUP_REGISTRY_KEY, Game + @"VanillaBackupLocation", backupPath); break; case Mod.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 (dlcModsInstalled.Count > 0) { 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)); } EndBackup(); }; nbw.RunWorkerCompleted += (a, b) => { if (b.Error != null) { Log.Error($@"Exception occured in {nbw.Name} thread: {b.Error.Message}"); } window.TaskBarItemInfoHandler.ProgressState = TaskbarItemProgressState.None; 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(); }
//this should be private but no way to test it private for now... /// <summary> /// Inspects and loads compressed mods from an archive. /// </summary> /// <param name="filepath">Path of the archive</param> /// <param name="addCompressedModCallback">Callback indicating that the mod should be added to the collection of found mods</param> /// <param name="currentOperationTextCallback">Callback to tell caller what's going on'</param> /// <param name="forcedOverrideData">Override data about archive. Used for testing only</param> public static void InspectArchive(string filepath, Action <Mod> addCompressedModCallback = null, Action <Mod> failedToLoadModeCallback = null, Action <string> currentOperationTextCallback = null, string forcedMD5 = null, int forcedSize = -1) { string relayVersionResponse = @"-1"; List <Mod> internalModList = new List <Mod>(); //internal mod list is for this function only so we don't need a callback to get our list since results are returned immediately var isExe = filepath.EndsWith(@".exe"); var archiveFile = isExe ? new SevenZipExtractor(filepath, InArchiveFormat.Nsis) : new SevenZipExtractor(filepath); using (archiveFile) { #if DEBUG foreach (var v in archiveFile.ArchiveFileData) { Debug.WriteLine($@"{v.FileName} | Index {v.Index} | Size {v.Size}"); } #endif var moddesciniEntries = new List <ArchiveFileInfo>(); var sfarEntries = new List <ArchiveFileInfo>(); //ME3 DLC var bioengineEntries = new List <ArchiveFileInfo>(); //ME2 DLC var me2mods = new List <ArchiveFileInfo>(); //ME2 RCW Mods foreach (var entry in archiveFile.ArchiveFileData) { string fname = Path.GetFileName(entry.FileName); if (!entry.IsDirectory && fname.Equals(@"moddesc.ini", StringComparison.InvariantCultureIgnoreCase)) { moddesciniEntries.Add(entry); } else if (!entry.IsDirectory && fname.Equals(@"Default.sfar", StringComparison.InvariantCultureIgnoreCase)) { //for unofficial lookups sfarEntries.Add(entry); } else if (!entry.IsDirectory && fname.Equals(@"BIOEngine.ini", StringComparison.InvariantCultureIgnoreCase)) { //for unofficial lookups bioengineEntries.Add(entry); } else if (!entry.IsDirectory && Path.GetExtension(fname) == @".me2mod") { //for unofficial lookups me2mods.Add(entry); } } if (moddesciniEntries.Count > 0) { foreach (var entry in moddesciniEntries) { currentOperationTextCallback?.Invoke(M3L.GetString(M3L.string_interp_readingX, entry.FileName)); Mod m = new Mod(entry, archiveFile); if (m.ValidMod) { addCompressedModCallback?.Invoke(m); internalModList.Add(m); } else { failedToLoadModeCallback?.Invoke(m); } } } else if (me2mods.Count > 0) { //found some .me2mod files. foreach (var entry in me2mods) { currentOperationTextCallback?.Invoke(M3L.GetString(M3L.string_interp_readingX, entry.FileName)); MemoryStream ms = new MemoryStream(); archiveFile.ExtractFile(entry.Index, ms); ms.Position = 0; StreamReader reader = new StreamReader(ms); string text = reader.ReadToEnd(); var rcwModsForFile = RCWMod.ParseRCWMods(Path.GetFileNameWithoutExtension(entry.FileName), text); foreach (var rcw in rcwModsForFile) { Mod m = new Mod(rcw); addCompressedModCallback?.Invoke(m); internalModList.Add(m); } } } else { Log.Information(@"Querying third party importing service for information about this file"); currentOperationTextCallback?.Invoke(M3L.GetString(M3L.string_queryingThirdPartyImportingService)); var md5 = forcedMD5 ?? Utilities.CalculateMD5(filepath); long size = forcedSize > 0 ? forcedSize : new FileInfo(filepath).Length; var potentialImportinInfos = ThirdPartyServices.GetImportingInfosBySize(size); var importingInfo = potentialImportinInfos.FirstOrDefault(x => x.md5 == md5); if (importingInfo == null && isExe) { Log.Error(@"EXE-based mods must be validated by ME3Tweaks before they can be imported into M3. This is to prevent breaking third party mods."); return; } if (importingInfo?.servermoddescname != null) { //Partially supported unofficial third party mod //Mod has a custom written moddesc.ini stored on ME3Tweaks Log.Information(@"Fetching premade moddesc.ini from ME3Tweaks for this mod archive"); string custommoddesc = OnlineContent.FetchThirdPartyModdesc(importingInfo.servermoddescname); Mod virutalCustomMod = new Mod(custommoddesc, "", archiveFile); //Load virutal mod if (virutalCustomMod.ValidMod) { addCompressedModCallback?.Invoke(virutalCustomMod); internalModList.Add(virutalCustomMod); return; //Don't do further parsing as this is custom written } else { Log.Error(@"Server moddesc was not valid for this mod. This shouldn't occur. Please report to Mgamerz."); return; } } ExeTransform transform = null; if (importingInfo?.exetransform != null) { Log.Information(@"TPIS lists exe transform for this mod: " + importingInfo.exetransform); transform = new ExeTransform(OnlineContent.FetchExeTransform(importingInfo.exetransform)); } //Fully unofficial third party mod. //ME3 foreach (var sfarEntry in sfarEntries) { var vMod = AttemptLoadVirtualMod(sfarEntry, archiveFile, Mod.MEGame.ME3, md5); if (vMod.ValidMod) { addCompressedModCallback?.Invoke(vMod); internalModList.Add(vMod); vMod.ExeExtractionTransform = transform; } } //TODO: ME2 //foreach (var entry in bioengineEntries) //{ // var vMod = AttemptLoadVirtualMod(entry, archiveFile, Mod.MEGame.ME2, md5); // if (vMod.ValidMod) // { // addCompressedModCallback?.Invoke(vMod); // internalModList.Add(vMod); // } //} //TODO: ME1 if (importingInfo?.version != null) { foreach (Mod compressedMod in internalModList) { compressedMod.ModVersionString = importingInfo.version; Version.TryParse(importingInfo.version, out var parsedValue); compressedMod.ParsedModVersion = parsedValue; } } else if (relayVersionResponse == @"-1") { //If no version information, check ME3Tweaks to see if it's been added recently //see if server has information on version number currentOperationTextCallback?.Invoke(M3L.GetString(M3L.string_gettingAdditionalInformationAboutFileFromME3Tweaks)); Log.Information(@"Querying ME3Tweaks for additional information"); var modInfo = OnlineContent.QueryModRelay(md5, size); //todo: make this work offline. if (modInfo != null && modInfo.TryGetValue(@"version", out string value)) { Log.Information(@"ME3Tweaks reports version number for this file is: " + value); foreach (Mod compressedMod in internalModList) { compressedMod.ModVersionString = value; Version.TryParse(value, out var parsedValue); compressedMod.ParsedModVersion = parsedValue; } relayVersionResponse = value; } else { Log.Information(@"ME3Tweaks does not have additional version information for this file"); Analytics.TrackEvent("Non Mod Manager Mod Dropped", new Dictionary <string, string>() { { "Filename", Path.GetFileName(filepath) }, { "MD5", md5 } }); } } else { //Try straight up TPMI import? Log.Warning($@"No importing information is available for file with hash {md5}. No mods could be found."); Analytics.TrackEvent("Non Mod Manager Mod Dropped", new Dictionary <string, string>() { { "Filename", Path.GetFileName(filepath) }, { "MD5", md5 } }); } } } }
private void InspectArchiveBackgroundThread(object sender, DoWorkEventArgs e) { TaskRunning = true; ActionText = M3L.GetString(M3L.string_interp_openingX, ScanningFile); var archive = e.Argument as string; void AddCompressedModCallback(Mod m) { Application.Current.Dispatcher.Invoke(delegate { CompressedMods.Add(m); if (CompressedMods.Count > 1 && !openedMultipanel) { Storyboard sb = FindResource(@"OpenWebsitePanel") as Storyboard; if (sb.IsSealed) { sb = sb.Clone(); } Storyboard.SetTarget(sb, MultipleModsPopupPanel); sb.Begin(); openedMultipanel = true; } CompressedMods.Sort(x => x.ModName); }); } void CompressedModFailedCallback(Mod m) { Application.Current.Dispatcher.Invoke(delegate { NoModSelectedText += M3L.GetString(M3L.string_interp_XfailedToLoadY, m.ModName, m.LoadFailedReason); }); } if (Path.GetExtension(archive) == @".me2mod") { //RCW var RCWMods = RCWMod.ParseRCWMods(Path.GetFileNameWithoutExtension(archive), File.ReadAllText(archive)); foreach (var rcw in RCWMods) { AddCompressedModCallback(new Mod(rcw)); } return; } //Embedded executables. var archiveSize = new FileInfo(archive).Length; var knownModsOfThisSize = ThirdPartyServices.GetImportingInfosBySize(archiveSize); string pathOverride = null; if (knownModsOfThisSize.Count > 0 && knownModsOfThisSize.Any(x => x.zippedexepath != null)) { //might have embedded exe if (archive.RepresentsFileArchive()) { SevenZipExtractor sve = new SevenZipExtractor(archive); string embeddedExePath = null; Log.Information(@"This file may contain a known exe-based mod."); foreach (var importingInfo in knownModsOfThisSize) { if (importingInfo.zippedexepath == null) { continue; } if (sve.ArchiveFileNames.Contains(importingInfo.zippedexepath)) { embeddedExePath = importingInfo.zippedexepath; //Ensure embedded exe is supported at least by decompressed size var exedata = sve.ArchiveFileData.FirstOrDefault(x => x.FileName == embeddedExePath); if (exedata.FileName != null) { var importingInfo2 = ThirdPartyServices.GetImportingInfosBySize((long)exedata.Size); if (importingInfo2.Count == 0) { Log.Warning(@"zip wrapper for this file has importing information but the embedded exe does not!"); break; //no importing info } Log.Information(@"Reading embedded executable file in archive: " + embeddedExePath); ActionText = M3L.GetString(M3L.string_readingZippedExecutable); pathOverride = Path.Combine(Utilities.GetTempPath(), Path.GetFileName(embeddedExePath)); using var outstream = new FileStream(pathOverride, FileMode.Create); sve.Extracting += (o, pea) => { ActionText = $@"{M3L.GetString(M3L.string_readingZippedExecutable)} {pea.PercentDone}%"; }; sve.ExtractFile(embeddedExePath, outstream); ArchiveFilePath = pathOverride; //set new path so further extraction calls use correct archive path. break; } } } } } void ActionTextUpdateCallback(string newText) { ActionText = newText; } InspectArchive(pathOverride ?? archive, AddCompressedModCallback, CompressedModFailedCallback, ActionTextUpdateCallback); }
public void OnTargetModChanged() { var tpmi = ThirdPartyServices.GetThirdPartyModInfo(TargetMod, EditingMod.Game); TPMIModName = tpmi?.modname; }
//this should be private but no way to test it private for now... /// <summary> /// Inspects and loads compressed mods from an archive. /// </summary> /// <param name="filepath">Path of the archive</param> /// <param name="addCompressedModCallback">Callback indicating that the mod should be added to the collection of found mods</param> /// <param name="currentOperationTextCallback">Callback to tell caller what's going on'</param> /// <param name="forcedOverrideData">Override data about archive. Used for testing only</param> public static void InspectArchive(string filepath, Action <Mod> addCompressedModCallback = null, Action <Mod> failedToLoadModeCallback = null, Action <string> currentOperationTextCallback = null, Action showALOTLauncher = null, string forcedMD5 = null, int forcedSize = -1) { string relayVersionResponse = @"-1"; List <Mod> internalModList = new List <Mod>(); //internal mod list is for this function only so we don't need a callback to get our list since results are returned immediately var isExe = filepath.EndsWith(@".exe"); var archiveFile = isExe ? new SevenZipExtractor(filepath, InArchiveFormat.Nsis) : new SevenZipExtractor(filepath); using (archiveFile) { #if DEBUG foreach (var v in archiveFile.ArchiveFileData) { Debug.WriteLine($@"{v.FileName} | Index {v.Index} | Size {v.Size} | Last Modified {v.LastWriteTime}"); } #endif var moddesciniEntries = new List <ArchiveFileInfo>(); var sfarEntries = new List <ArchiveFileInfo>(); //ME3 DLC var bioengineEntries = new List <ArchiveFileInfo>(); //ME2 DLC var me2mods = new List <ArchiveFileInfo>(); //ME2 RCW Mods var textureModEntries = new List <ArchiveFileInfo>(); //TPF MEM MOD files bool isAlotFile = false; try { foreach (var entry in archiveFile.ArchiveFileData) { if (!entry.IsDirectory) { string fname = Path.GetFileName(entry.FileName); if (fname.Equals(@"ALOTInstaller.exe", StringComparison.InvariantCultureIgnoreCase)) { isAlotFile = true; } else if (fname.Equals(@"moddesc.ini", StringComparison.InvariantCultureIgnoreCase)) { moddesciniEntries.Add(entry); } else if (fname.Equals(@"Default.sfar", StringComparison.InvariantCultureIgnoreCase)) { //for unofficial lookups sfarEntries.Add(entry); } else if (fname.Equals(@"BIOEngine.ini", StringComparison.InvariantCultureIgnoreCase)) { //for unofficial lookups bioengineEntries.Add(entry); } else if (Path.GetExtension(fname) == @".me2mod") { me2mods.Add(entry); } else if (Path.GetExtension(fname) == @".mem" || Path.GetExtension(fname) == @".tpf" || Path.GetExtension(fname) == @".mod") { //for forwarding to ALOT Installer textureModEntries.Add(entry); } } } } catch (SevenZipArchiveException svae) { //error reading archive! Mod failed = new Mod(false); failed.ModName = M3L.GetString(M3L.string_archiveError); failed.LoadFailedReason = M3L.GetString(M3L.string_couldNotInspectArchive7zException); Log.Error($@"Unable to inspect archive {filepath}: SevenZipException occurred! It may be corrupt. The specific error was: {svae.Message}"); failedToLoadModeCallback?.Invoke(failed); addCompressedModCallback?.Invoke(failed); return; } // Used for TPIS information lookup long archiveSize = forcedSize > 0 ? forcedSize : new FileInfo(filepath).Length; if (moddesciniEntries.Count > 0) { foreach (var entry in moddesciniEntries) { currentOperationTextCallback?.Invoke(M3L.GetString(M3L.string_interp_readingX, entry.FileName)); Mod m = new Mod(entry, archiveFile); if (!m.ValidMod) { failedToLoadModeCallback?.Invoke(m); m.SelectedForImport = false; } addCompressedModCallback?.Invoke(m); internalModList.Add(m); } } else if (me2mods.Count > 0) { //found some .me2mod files. foreach (var entry in me2mods) { currentOperationTextCallback?.Invoke(M3L.GetString(M3L.string_interp_readingX, entry.FileName)); MemoryStream ms = new MemoryStream(); archiveFile.ExtractFile(entry.Index, ms); ms.Position = 0; StreamReader reader = new StreamReader(ms); string text = reader.ReadToEnd(); var rcwModsForFile = RCWMod.ParseRCWMods(Path.GetFileNameWithoutExtension(entry.FileName), text); foreach (var rcw in rcwModsForFile) { Mod m = new Mod(rcw); addCompressedModCallback?.Invoke(m); internalModList.Add(m); } } } else if (textureModEntries.Any() && isAlotFile) { if (isAlotFile) { //is alot installer Log.Information(@"This file contains texture files and ALOTInstaller.exe - this is an ALOT main file"); var textureLibraryPath = Utilities.GetALOTInstallerTextureLibraryDirectory(); if (textureLibraryPath != null) { //we have destination var destPath = Path.Combine(textureLibraryPath, Path.GetFileName(filepath)); if (!File.Exists(destPath)) { Log.Information(M3L.GetString(M3L.string_thisFileIsNotInTheTextureLibraryMovingItToTheTextureLibrary)); currentOperationTextCallback?.Invoke(M3L.GetString(M3L.string_movingALOTFileToTextureLibraryPleaseWait)); archiveFile.Dispose(); File.Move(filepath, destPath, true); showALOTLauncher?.Invoke(); } } } //todo: Parse //else //{ // //found some texture-mod only files // foreach (var entry in textureModEntries) // { // currentOperationTextCallback?.Invoke(M3L.GetString(M3L.string_interp_readingX, entry.FileName)); // MemoryStream ms = new MemoryStream(); // archiveFile.ExtractFile(entry.Index, ms); // ms.Position = 0; // StreamReader reader = new StreamReader(ms); // string text = reader.ReadToEnd(); // var rcwModsForFile = RCWMod.ParseRCWMods(Path.GetFileNameWithoutExtension(entry.FileName), text); // foreach (var rcw in rcwModsForFile) // { // Mod m = new Mod(rcw); // addCompressedModCallback?.Invoke(m); // internalModList.Add(m); // } // } //} } else { Log.Information(@"Querying third party importing service for information about this file: " + filepath); currentOperationTextCallback?.Invoke(M3L.GetString(M3L.string_queryingThirdPartyImportingService)); var md5 = forcedMD5 ?? Utilities.CalculateMD5(filepath); var potentialImportinInfos = ThirdPartyServices.GetImportingInfosBySize(archiveSize); var importingInfo = potentialImportinInfos.FirstOrDefault(x => x.md5 == md5); if (importingInfo == null && isExe) { Log.Error(@"EXE-based mods must be validated by ME3Tweaks before they can be imported into M3. This is to prevent breaking third party mods."); return; } if (importingInfo?.servermoddescname != null) { //Partially supported unofficial third party mod //Mod has a custom written moddesc.ini stored on ME3Tweaks Log.Information(@"Fetching premade moddesc.ini from ME3Tweaks for this mod archive"); string custommoddesc = null; string loadFailedReason = null; try { custommoddesc = OnlineContent.FetchThirdPartyModdesc(importingInfo.servermoddescname); } catch (Exception e) { loadFailedReason = e.Message; Log.Error(@"Error fetching moddesc from server: " + e.Message); } Mod virutalCustomMod = new Mod(custommoddesc, "", archiveFile); //Load virutal mod if (virutalCustomMod.ValidMod) { Log.Information(@"Mod loaded from server moddesc."); addCompressedModCallback?.Invoke(virutalCustomMod); internalModList.Add(virutalCustomMod); return; //Don't do further parsing as this is custom written } else { if (loadFailedReason != null) { virutalCustomMod.LoadFailedReason = M3L.GetString(M3L.string_interp_failedToFetchModdesciniFileFromServerReasonLoadFailedReason, loadFailedReason); } else { Log.Error(@"Server moddesc was not valid for this mod. This shouldn't occur. Please report to Mgamerz."); } return; } } ExeTransform transform = null; if (importingInfo?.exetransform != null) { Log.Information(@"TPIS lists exe transform for this mod: " + importingInfo.exetransform); transform = new ExeTransform(OnlineContent.FetchExeTransform(importingInfo.exetransform)); } //Fully unofficial third party mod. //ME3 foreach (var sfarEntry in sfarEntries) { var vMod = AttemptLoadVirtualMod(sfarEntry, archiveFile, Mod.MEGame.ME3, md5); if (vMod != null) { addCompressedModCallback?.Invoke(vMod); internalModList.Add(vMod); vMod.ExeExtractionTransform = transform; } } //TODO: ME2 ? //foreach (var entry in bioengineEntries) //{ // var vMod = AttemptLoadVirtualMod(entry, archiveFile, Mod.MEGame.ME2, md5); // if (vMod.ValidMod) // { // addCompressedModCallback?.Invoke(vMod); // internalModList.Add(vMod); // } //} //TODO: ME1 ? if (importingInfo?.version != null) { foreach (Mod compressedMod in internalModList) { compressedMod.ModVersionString = importingInfo.version; Version.TryParse(importingInfo.version, out var parsedValue); compressedMod.ParsedModVersion = parsedValue; } } else if (relayVersionResponse == @"-1") { //If no version information, check ME3Tweaks to see if it's been added recently //see if server has information on version number currentOperationTextCallback?.Invoke(M3L.GetString(M3L.string_gettingAdditionalInformationAboutFileFromME3Tweaks)); Log.Information(@"Querying ME3Tweaks for additional information for this file..."); var modInfo = OnlineContent.QueryModRelay(md5, archiveSize); //todo: make this work offline. if (modInfo != null && modInfo.TryGetValue(@"version", out string value)) { Log.Information(@"ME3Tweaks reports version number for this file is: " + value); foreach (Mod compressedMod in internalModList) { compressedMod.ModVersionString = value; Version.TryParse(value, out var parsedValue); compressedMod.ParsedModVersion = parsedValue; } relayVersionResponse = value; } else { Log.Information(@"ME3Tweaks does not have additional version information for this file."); Analytics.TrackEvent(@"Non Mod Manager Mod Dropped", new Dictionary <string, string>() { { @"Filename", Path.GetFileName(filepath) }, { @"MD5", md5 } }); foreach (Mod compressedMod in internalModList) { compressedMod.ModVersionString = M3L.GetString(M3L.string_unknown); } } } else { //Try straight up TPMI import? Log.Warning($@"No importing information is available for file with hash {md5}. No mods could be found."); Analytics.TrackEvent(@"Non Mod Manager Mod Dropped", new Dictionary <string, string>() { { @"Filename", Path.GetFileName(filepath) }, { @"MD5", md5 } }); } } } }
public InstalledOfficialDLC(string foldername, bool installed, MEGame game) { FolderName = foldername; Installed = installed; HumanName = ThirdPartyServices.GetThirdPartyModInfo(FolderName, game)?.modname ?? foldername; }
private void PerformSearch() { Results.ClearEx(); var searchGames = new List <string>(); if (SearchME1) { searchGames.Add(@"masseffect"); } if (SearchME2) { searchGames.Add(@"masseffect2"); } if (SearchME3) { searchGames.Add(@"masseffect3"); } QueryInProgress = true; try { foreach (var domain in searchGames) { if (!LoadedDatabases.TryGetValue(domain, out var db)) { db = GameDatabase.LoadDatabase(domain); if (db != null) { LoadedDatabases[domain] = db; } } // Check if the name exists in filenames. If it doesn't, it will never find it #if DEBUG var ignoredItems = new List <string>() { @"DLC_MOD_FMRM_Patches", @"DLC_MOD_FJRM_Patches", @"DLC_ASH_MiniSkirt_Mods", @"DLC_Explorer", @"DLC_LIA_RA4_MeshOnly", @"DLC_ASH_Shorts_Mod", @"DLC_ASH_Alt_Mods", @"DLC_ASH_Socks_Mod", @"DLC_ASH_Topless_Mod", @"DLC_GAR_FRM_Altered_Face_Legs_Mod", @"DLC_GAR_GFC_Altered_Face_Legs_Mod", @"DLC_LIA_NKDSlippers_Mod", @"DLC_LIA_NKDSnickers_Mod", @"DLC_GAR_GFC_New_Version", @"DLC_GAR_GFC_Old_Version", @"DLC_GAR_FRM_Textures", @"DLC_MIR_Shorts_Mod", @"DLC_MOD_IT_RUS", }; var dlcNames = db.NameTable.Values.Where(x => !ignoredItems.Contains(x) && x.StartsWith(@"DLC_") && Path.GetExtension(x) == string.Empty && !x.Contains(" ") && ThirdPartyServices.GetThirdPartyModInfo(x, MEGame.ME3) == null).Select(x => x.Trim()).Distinct().ToList(); var xx = new List <string>(); foreach (var i in db.FileInstances.Values) { foreach (var f in i) { if (f.ParentPathID > 0) { var path = db.Paths[f.ParentPathID].GetFullPath(db); if (path.ContainsAny(dlcNames, StringComparison.Ordinal)) { var finfo = $@"https://nexusmods.com/masseffect/mods/{f.ModID}"; xx.Add(db.NameTable[db.ModFileInfos[f.FileID].NameID] + " " + finfo); } } } } File.WriteAllLines(@"D:\dlcNames.txt", dlcNames); File.WriteAllLines(@"D:\mods.txt", xx); #endif var match = db.NameTable.FirstOrDefault(x => x.Value.Equals(SearchTerm, StringComparison.InvariantCultureIgnoreCase)); if (match.Key != 0) { // Found var instances = db.FileInstances[match.Key]; Results.AddRange(instances.Select(x => new SearchedItemResult() { Instance = x, Domain = domain, Filename = db.NameTable[x.FilenameId], AssociatedDB = db })); } } StatusText = M3L.GetString(M3L.string_interp_resultsCount, Results.Count); QueryInProgress = false; } catch (Exception e) { Log.Error($@"Could not perform search: {e.Message}"); QueryInProgress = false; } }
private void InspectArchiveBackgroundThread(object sender, DoWorkEventArgs e) { TaskRunning = true; ActionText = $"Opening {ScanningFile}"; var archive = e.Argument as string; //Embedded executables. var archiveSize = new FileInfo(archive).Length; var knownModsOfThisSize = ThirdPartyServices.GetImportingInfosBySize(archiveSize); string pathOverride = null; if (knownModsOfThisSize.Count > 0 && knownModsOfThisSize.Any(x => x.zippedexepath != null)) { //might have embedded exe if (archive.RepresentsFileArchive()) { SevenZipExtractor sve = new SevenZipExtractor(archive); string embeddedExePath = null; Log.Information("This file may contain a known exe-based mod."); foreach (var importingInfo in knownModsOfThisSize) { if (importingInfo.zippedexepath == null) { continue; } if (sve.ArchiveFileNames.Contains(importingInfo.zippedexepath)) { embeddedExePath = importingInfo.zippedexepath; //Ensure embedded exe is supported at least by decompressed size var exedata = sve.ArchiveFileData.FirstOrDefault(x => x.FileName == embeddedExePath); if (exedata.FileName != null) { var importingInfo2 = ThirdPartyServices.GetImportingInfosBySize((long)exedata.Size); if (importingInfo2.Count == 0) { Log.Warning("zip wrapper for this file has importing information but the embedded exe does not!"); break; //no importing info } Log.Information("Reading embedded executable file in archive: " + embeddedExePath); ActionText = "Reading zipped executable"; pathOverride = Path.Combine(Utilities.GetTempPath(), Path.GetFileName(embeddedExePath)); using var outstream = new FileStream(pathOverride, FileMode.Create); sve.Extracting += (o, pea) => { ActionText = $"Reading zipped executable {pea.PercentDone}%"; }; sve.ExtractFile(embeddedExePath, outstream); ArchiveFilePath = pathOverride; //set new path so further extraction calls use correct archive path. break; } } } } } void AddCompressedModCallback(Mod m) { Application.Current.Dispatcher.Invoke(delegate { CompressedMods.Add(m); CompressedMods.Sort(x => x.ModName); }); } void CompressedModFailedCallback(Mod m) { Application.Current.Dispatcher.Invoke(delegate { NoModSelectedText += $"\n\n{m.ModName} failed to load: {m.LoadFailedReason}"; }); } void ActionTextUpdateCallback(string newText) { ActionText = newText; } InspectArchive(pathOverride ?? archive, AddCompressedModCallback, CompressedModFailedCallback, ActionTextUpdateCallback); }
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); }