//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 } }); } } } }
public void ExtractFromArchive(string archivePath, string outputFolderPath, bool compressPackages, Action <string> updateTextCallback = null, Action <DetailedProgressEventArgs> extractingCallback = null, Action <string, int, int> compressedPackageCallback = null) { if (!IsInArchive) { throw new Exception(@"Cannot extract a mod that is not part of an archive."); } compressPackages &= Game == MEGame.ME3; //ME3 ONLY FOR NOW var archiveFile = archivePath.EndsWith(@".exe") ? new SevenZipExtractor(archivePath, InArchiveFormat.Nsis) : new SevenZipExtractor(archivePath); using (archiveFile) { var fileIndicesToExtract = new List <int>(); var referencedFiles = GetAllRelativeReferences(archiveFile); if (!IsVirtualized) { referencedFiles.Add(@"moddesc.ini"); } //unsure if this is required?? doesn't work for MEHEM EXE //referencedFiles = referencedFiles.Select(x => FilesystemInterposer.PathCombine(IsInArchive, ModPath, x)).ToList(); //remap to in-archive paths so they match entry paths foreach (var info in archiveFile.ArchiveFileData) { if (referencedFiles.Contains(info.FileName)) { Log.Information(@"Adding file to extraction list: " + info.FileName); fileIndicesToExtract.Add(info.Index); } } #region old /* * bool fileAdded = false; * //moddesc.ini * if (info.FileName == ModDescPath) * { * //Debug.WriteLine("Add file to extraction list: " + info.FileName); * fileIndicesToExtract.Add(info.Index); * continue; * } * * //Check each job * foreach (ModJob job in InstallationJobs) * { * if (job.Header == ModJob.JobHeader.CUSTOMDLC) * { #region Extract Custom DLC * foreach (var localCustomDLCFolder in job.CustomDLCFolderMapping.Keys) * { * if (info.FileName.StartsWith(FilesystemInterposer.PathCombine(IsInArchive, ModPath, localCustomDLCFolder))) * { * //Debug.WriteLine("Add file to extraction list: " + info.FileName); * fileIndicesToExtract.Add(info.Index); * fileAdded = true; * break; * } * } * * if (fileAdded) break; * * //Alternate files * foreach (var alt in job.AlternateFiles) * { * if (alt.AltFile != null && info.FileName.Equals(FilesystemInterposer.PathCombine(IsInArchive, ModPath, alt.AltFile), StringComparison.InvariantCultureIgnoreCase)) * { * //Debug.WriteLine("Add alternate file to extraction list: " + info.FileName); * fileIndicesToExtract.Add(info.Index); * fileAdded = true; * break; * } * } * * if (fileAdded) break; * * //Alternate DLC * foreach (var alt in job.AlternateDLCs) * { * if (info.FileName.StartsWith(FilesystemInterposer.PathCombine(IsInArchive, ModPath, alt.AlternateDLCFolder), StringComparison.InvariantCultureIgnoreCase)) * { * //Debug.WriteLine("Add alternate dlc file to extraction list: " + info.FileName); * fileIndicesToExtract.Add(info.Index); * fileAdded = true; * break; * } * } * * if (fileAdded) break; * #endregion * } * else * { #region Official headers * * foreach (var inSubDirFile in job.FilesToInstall.Values) * { * var inArchivePath = FilesystemInterposer.PathCombine(IsInArchive, ModPath, job.JobDirectory, inSubDirFile); //keep relative if unpacked mod, otherwise use full in-archive path for extraction * if (info.FileName.Equals(inArchivePath, StringComparison.InvariantCultureIgnoreCase)) * { * //Debug.WriteLine("Add file to extraction list: " + info.FileName); * fileIndicesToExtract.Add(info.Index); * fileAdded = true; * break; * } * } * * if (fileAdded) break; * //Alternate files * foreach (var alt in job.AlternateFiles) * { * if (alt.AltFile != null && info.FileName.Equals(FilesystemInterposer.PathCombine(IsInArchive, ModPath, alt.AltFile), StringComparison.InvariantCultureIgnoreCase)) * { * //Debug.WriteLine("Add alternate file to extraction list: " + info.FileName); * fileIndicesToExtract.Add(info.Index); * fileAdded = true; * break; * } * } * * if (fileAdded) break; * #endregion * } * } * }*/ #endregion archiveFile.Progressing += (sender, args) => { extractingCallback?.Invoke(args); }; string outputFilePathMapping(ArchiveFileInfo entryInfo) { string entryPath = entryInfo.FileName; if (ExeExtractionTransform != null && ExeExtractionTransform.PatchRedirects.Any(x => x.index == entryInfo.Index)) { Log.Information(@"Extracting vpatch file at index " + entryInfo.Index); return(Path.Combine(Utilities.GetVPatchRedirectsFolder(), ExeExtractionTransform.PatchRedirects.First(x => x.index == entryInfo.Index).outfile)); } if (ExeExtractionTransform != null && ExeExtractionTransform.NoExtractIndexes.Any(x => x == entryInfo.Index)) { Log.Information(@"Extracting file to trash (not used): " + entryPath); return(Path.Combine(Utilities.GetTempPath(), @"Trash", @"trashfile")); } if (ExeExtractionTransform != null && ExeExtractionTransform.AlternateRedirects.Any(x => x.index == entryInfo.Index)) { var outfile = ExeExtractionTransform.AlternateRedirects.First(x => x.index == entryInfo.Index).outfile; Log.Information($@"Extracting file with redirection: {entryPath} {outfile}"); return(Path.Combine(outputFolderPath, outfile)); } //Archive path might start with a \. Substring may return value that start with a \ var subModPath = entryPath /*.TrimStart('\\')*/.Substring(ModPath.Length).TrimStart('\\'); var path = Path.Combine(outputFolderPath, subModPath); //Debug.WriteLine("remapping output: " + entryPath + " -> " + path); return(path); } if (compressPackages) { compressionQueue = new BlockingCollection <string>(); } int numberOfPackagesToCompress = referencedFiles.Count(x => x.RepresentsPackageFilePath()); int compressedPackageCount = 0; NamedBackgroundWorker compressionThread; if (compressPackages) { compressionThread = new NamedBackgroundWorker(@"ImportingCompressionThread"); compressionThread.DoWork += (a, b) => { try { while (true) { var package = compressionQueue.Take(); //updateTextCallback?.Invoke(M3L.GetString(M3L.string_interp_compressingX, Path.GetFileName(package))); var p = MEPackageHandler.OpenMEPackage(package); //Check if any compressed textures. bool shouldNotCompress = false; foreach (var texture in p.Exports.Where(x => x.IsTexture())) { var storageType = Texture2D.GetTopMipStorageType(texture); shouldNotCompress |= storageType == ME3Explorer.Unreal.StorageTypes.pccLZO || storageType == ME3Explorer.Unreal.StorageTypes.pccZlib; if (!shouldNotCompress) { break; } } if (!shouldNotCompress) { compressedPackageCallback?.Invoke(M3L.GetString(M3L.string_interp_compressingX, Path.GetFileName(package)), compressedPackageCount, numberOfPackagesToCompress); Log.Information(@"Compressing package: " + package); p.save(true); } else { Log.Information(@"Not compressing package due to file containing compressed textures: " + package); } Interlocked.Increment(ref compressedPackageCount); compressedPackageCallback?.Invoke(M3L.GetString(M3L.string_interp_compressedX, Path.GetFileName(package)), compressedPackageCount, numberOfPackagesToCompress); } } catch (InvalidOperationException) { //Done. lock (compressionCompletedSignaler) { Monitor.Pulse(compressionCompletedSignaler); } } }; compressionThread.RunWorkerAsync(); } archiveFile.FileExtractionFinished += (sender, args) => { if (compressPackages) { var fToCompress = outputFilePathMapping(args.FileInfo); if (fToCompress.RepresentsPackageFilePath()) { //Debug.WriteLine("Adding to blocking queue"); compressionQueue.TryAdd(fToCompress); } } }; archiveFile.ExtractFiles(outputFolderPath, outputFilePathMapping, fileIndicesToExtract.ToArray()); Log.Information(@"File extraction completed."); compressionQueue?.CompleteAdding(); if (compressPackages && numberOfPackagesToCompress > 0 && numberOfPackagesToCompress > compressedPackageCount) { Log.Information(@"Waiting for compression of packages to complete."); while (!compressionQueue.IsCompleted) { lock (compressionCompletedSignaler) { Monitor.Wait(compressionCompletedSignaler); } } Log.Information(@"Package compression has completed."); } ModPath = outputFolderPath; if (IsVirtualized) { var parser = new IniDataParser().Parse(VirtualizedIniText); parser[@"ModInfo"][@"modver"] = ModVersionString; //In event relay service resolved this File.WriteAllText(Path.Combine(ModPath, @"moddesc.ini"), parser.ToString()); IsVirtualized = false; //no longer virtualized } if (ExeExtractionTransform != null) { var vpat = Utilities.GetCachedExecutablePath(@"vpat.exe"); Utilities.ExtractInternalFile(@"MassEffectModManagerCore.modmanager.executables.vpat.exe", vpat, true); //Handle VPatching foreach (var transform in ExeExtractionTransform.VPatches) { var patchfile = Path.Combine(Utilities.GetVPatchRedirectsFolder(), transform.patchfile); var inputfile = Path.Combine(ModPath, transform.inputfile); var outputfile = Path.Combine(ModPath, transform.outputfile); var args = $"\"{patchfile}\" \"{inputfile}\" \"{outputfile}\""; //do not localize Directory.CreateDirectory(Directory.GetParent(outputfile).FullName); //ensure output directory exists as vpatch will not make one. Log.Information($@"VPatching file into alternate: {inputfile} to {outputfile}"); updateTextCallback?.Invoke(M3L.GetString(M3L.string_interp_vPatchingIntoAlternate, Path.GetFileName(inputfile))); Utilities.RunProcess(vpat, args, true, false, false, true); } //Handle copyfile foreach (var copyfile in ExeExtractionTransform.CopyFiles) { string srcfile = Path.Combine(ModPath, copyfile.inputfile); string destfile = Path.Combine(ModPath, copyfile.outputfile); Log.Information($@"Applying transform copyfile: {srcfile} -> {destfile}"); File.Copy(srcfile, destfile, true); } if (ExeExtractionTransform.PostTransformModdesc != null) { //fetch online moddesc for this mod. Log.Information(@"Fetching post-transform third party moddesc."); var moddesc = OnlineContent.FetchThirdPartyModdesc(ExeExtractionTransform.PostTransformModdesc); File.WriteAllText(Path.Combine(ModPath, @"moddesc.ini"), moddesc); } } //int packagesCompressed = 0; //if (compressPackages) //{ // var packages = Utilities.GetPackagesInDirectory(ModPath, true); // extractingCallback?.Invoke(new ProgressEventArgs((byte)(packagesCompressed * 100.0 / packages.Count), 0)); // foreach (var package in packages) // { // updateTextCallback?.Invoke(M3L.GetString(M3L.string_interp_compressingX, Path.GetFileName(package))); // Log.Information("Compressing package: " + package); // var p = MEPackageHandler.OpenMEPackage(package); // p.save(true); // packagesCompressed++; // extractingCallback?.Invoke(new ProgressEventArgs((byte)(packagesCompressed * 100.0 / packages.Count), 0)); // } //} } }
/// <summary> /// Extracts the mod from the archive. The caller should handle exception that may be thrown. /// </summary> /// <param name="archivePath"></param> /// <param name="outputFolderPath"></param> /// <param name="compressPackages"></param> /// <param name="updateTextCallback"></param> /// <param name="extractingCallback"></param> /// <param name="compressedPackageCallback"></param> /// <param name="testRun"></param> public void ExtractFromArchive(string archivePath, string outputFolderPath, bool compressPackages, Action <string> updateTextCallback = null, Action <DetailedProgressEventArgs> extractingCallback = null, Action <string, int, int> compressedPackageCallback = null, bool testRun = false, Stream archiveStream = null) { if (!IsInArchive) { throw new Exception(@"Cannot extract a mod that is not part of an archive."); } if (archiveStream == null && !File.Exists(archivePath)) { throw new Exception(M3L.GetString(M3L.string_interp_theArchiveFileArchivePathIsNoLongerAvailable, archivePath)); } compressPackages &= Game >= MEGame.ME2; SevenZipExtractor archive; var isExe = archivePath.EndsWith(@".exe", StringComparison.InvariantCultureIgnoreCase); bool closeStreamOnFinish = true; if (archiveStream != null) { archive = isExe ? new SevenZipExtractor(archiveStream, InArchiveFormat.Nsis) : new SevenZipExtractor(archiveStream); closeStreamOnFinish = false; } else { archive = isExe ? new SevenZipExtractor(archivePath, InArchiveFormat.Nsis) : new SevenZipExtractor(archivePath); } var fileIndicesToExtract = new List <int>(); var filePathsToExtractTESTONLY = new List <string>(); var referencedFiles = GetAllRelativeReferences(!IsVirtualized, archive); if (isExe) { //remap to mod root. Not entirely sure if this needs to be done for sub mods? referencedFiles = referencedFiles.Select(x => FilesystemInterposer.PathCombine(IsInArchive, ModPath, x)).ToList(); //remap to in-archive paths so they match entry paths } foreach (var info in archive.ArchiveFileData) { if (!info.IsDirectory && (ModPath == "" || info.FileName.Contains((string)ModPath))) { var relativedName = isExe ? info.FileName : info.FileName.Substring(ModPath.Length).TrimStart('\\'); if (referencedFiles.Contains(relativedName)) { Log.Information(@"Adding file to extraction list: " + info.FileName); fileIndicesToExtract.Add(info.Index); filePathsToExtractTESTONLY.Add(relativedName); } } } void archiveExtractionProgress(object?sender, DetailedProgressEventArgs args) { extractingCallback?.Invoke(args); } archive.Progressing += archiveExtractionProgress; string outputFilePathMapping(ArchiveFileInfo entryInfo) { Log.Information(@"Mapping extraction target for " + entryInfo.FileName); string entryPath = entryInfo.FileName; if (ExeExtractionTransform != null && ExeExtractionTransform.PatchRedirects.Any(x => x.index == entryInfo.Index)) { Log.Information(@"Extracting vpatch file at index " + entryInfo.Index); return(Path.Combine(Utilities.GetVPatchRedirectsFolder(), ExeExtractionTransform.PatchRedirects.First(x => x.index == entryInfo.Index).outfile)); } if (ExeExtractionTransform != null && ExeExtractionTransform.NoExtractIndexes.Any(x => x == entryInfo.Index)) { Log.Information(@"Extracting file to trash (not used): " + entryPath); return(Path.Combine(Utilities.GetTempPath(), @"Trash", @"trashfile")); } if (ExeExtractionTransform != null && ExeExtractionTransform.AlternateRedirects.Any(x => x.index == entryInfo.Index)) { var outfile = ExeExtractionTransform.AlternateRedirects.First(x => x.index == entryInfo.Index).outfile; Log.Information($@"Extracting file with redirection: {entryPath} -> {outfile}"); return(Path.Combine(outputFolderPath, outfile)); } //Archive path might start with a \. Substring may return value that start with a \ var subModPath = entryPath /*.TrimStart('\\')*/.Substring(ModPath.Length).TrimStart('\\'); var path = Path.Combine(outputFolderPath, subModPath); //Debug.WriteLine("remapping output: " + entryPath + " -> " + path); return(path); } if (compressPackages) { compressionQueue = new BlockingCollection <string>(); } int numberOfPackagesToCompress = referencedFiles.Count(x => x.RepresentsPackageFilePath()); int compressedPackageCount = 0; NamedBackgroundWorker compressionThread; if (compressPackages) { compressionThread = new NamedBackgroundWorker(@"ImportingCompressionThread"); compressionThread.DoWork += (a, b) => { try { while (true) { var package = compressionQueue.Take(); var p = MEPackageHandler.OpenMEPackage(package); bool shouldNotCompress = Game == MEGame.ME1; if (!shouldNotCompress) { //updateTextCallback?.Invoke(M3L.GetString(M3L.string_interp_compressingX, Path.GetFileName(package))); FileInfo fileInfo = new FileInfo(package); var created = fileInfo.CreationTime; //File Creation var lastmodified = fileInfo.LastWriteTime; //File Modification compressedPackageCallback?.Invoke(M3L.GetString(M3L.string_interp_compressingX, Path.GetFileName(package)), compressedPackageCount, numberOfPackagesToCompress); Log.Information(@"Compressing package: " + package); p.Save(compress: true); File.SetCreationTime(package, created); File.SetLastWriteTime(package, lastmodified); } else { Log.Information(@"Skipping compression for ME1 package file: " + package); } Interlocked.Increment(ref compressedPackageCount); compressedPackageCallback?.Invoke(M3L.GetString(M3L.string_interp_compressedX, Path.GetFileName(package)), compressedPackageCount, numberOfPackagesToCompress); } } catch (InvalidOperationException) { //Done. lock (compressionCompletedSignaler) { Monitor.Pulse(compressionCompletedSignaler); } } }; compressionThread.RunWorkerAsync(); } void compressPackage(object?sender, FileInfoEventArgs args) { if (compressPackages) { var fToCompress = outputFilePathMapping(args.FileInfo); if (fToCompress.RepresentsPackageFilePath()) { //Debug.WriteLine("Adding to blocking queue"); compressionQueue.TryAdd(fToCompress); } } } archive.FileExtractionFinished += compressPackage; if (!testRun) { Log.Information(@"Extracting files..."); archive.ExtractFiles(outputFolderPath, outputFilePathMapping, fileIndicesToExtract.ToArray()); } else { // test run mode // exes can have duplicate filenames but different indexes so we must check for those here. if (fileIndicesToExtract.Count != referencedFiles.Count && filePathsToExtractTESTONLY.Distinct().ToList().Count != referencedFiles.Count) { throw new Exception(@"The amount of referenced files does not match the amount of files that are going to be extracted!"); } } Log.Information(@"File extraction completed."); archive.Progressing -= archiveExtractionProgress; compressionQueue?.CompleteAdding(); if (compressPackages && numberOfPackagesToCompress > 0 && numberOfPackagesToCompress > compressedPackageCount) { Log.Information(@"Waiting for compression of packages to complete."); while (!compressionQueue.IsCompleted) { lock (compressionCompletedSignaler) { Monitor.Wait(compressionCompletedSignaler); } } Log.Information(@"Package compression has completed."); } archive.FileExtractionFinished -= compressPackage; ModPath = outputFolderPath; if (IsVirtualized) { var parser = new IniDataParser().Parse(VirtualizedIniText); parser[@"ModInfo"][@"modver"] = ModVersionString; //In event relay service resolved this if (!testRun) { File.WriteAllText(Path.Combine(ModPath, @"moddesc.ini"), parser.ToString()); } IsVirtualized = false; //no longer virtualized } if (ExeExtractionTransform != null) { if (ExeExtractionTransform.VPatches.Any()) { // MEHEM uses Vpatching for its alternates. var vpat = Utilities.GetCachedExecutablePath(@"vpat.exe"); if (!testRun) { Utilities.ExtractInternalFile(@"MassEffectModManagerCore.modmanager.executables.vpat.exe", vpat, true); } //Handle VPatching foreach (var transform in ExeExtractionTransform.VPatches) { var patchfile = Path.Combine(Utilities.GetVPatchRedirectsFolder(), transform.patchfile); var inputfile = Path.Combine(ModPath, transform.inputfile); var outputfile = Path.Combine(ModPath, transform.outputfile); var args = $"\"{patchfile}\" \"{inputfile}\" \"{outputfile}\""; //do not localize if (!testRun) { Directory.CreateDirectory(Directory.GetParent(outputfile).FullName); //ensure output directory exists as vpatch will not make one. } Log.Information($@"VPatching file into alternate: {inputfile} to {outputfile}"); updateTextCallback?.Invoke(M3L.GetString(M3L.string_interp_vPatchingIntoAlternate, Path.GetFileName(inputfile))); if (!testRun) { Utilities.RunProcess(vpat, args, true, false, false, true); } } } //Handle copyfile foreach (var copyfile in ExeExtractionTransform.CopyFiles) { string srcfile = Path.Combine(ModPath, copyfile.inputfile); string destfile = Path.Combine(ModPath, copyfile.outputfile); Log.Information($@"Applying transform copyfile: {srcfile} -> {destfile}"); if (!testRun) { File.Copy(srcfile, destfile, true); } } if (ExeExtractionTransform.PostTransformModdesc != null) { //fetch online moddesc for this mod. Log.Information(@"Fetching post-transform third party moddesc."); var moddesc = OnlineContent.FetchThirdPartyModdesc(ExeExtractionTransform.PostTransformModdesc); if (!testRun) { File.WriteAllText(Path.Combine(ModPath, @"moddesc.ini"), moddesc); } } } //int packagesCompressed = 0; //if (compressPackages) //{ // var packages = Utilities.GetPackagesInDirectory(ModPath, true); // extractingCallback?.Invoke(new ProgressEventArgs((byte)(packagesCompressed * 100.0 / packages.Count), 0)); // foreach (var package in packages) // { // updateTextCallback?.Invoke(M3L.GetString(M3L.string_interp_compressingX, Path.GetFileName(package))); // Log.Information("Compressing package: " + package); // var p = MEPackageHandler.OpenMEPackage(package); // p.save(true); // packagesCompressed++; // extractingCallback?.Invoke(new ProgressEventArgs((byte)(packagesCompressed * 100.0 / packages.Count), 0)); // } //} if (closeStreamOnFinish) { archive?.Dispose(); } else { archive?.DisposeObjectOnly(); } }