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); }
//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 } }); } } } }
//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 } }); } } } }