/// <summary> /// Adds a file to the add/replace list of files to install. This will replace an existing file in the mapping if the destination path is the same. /// </summary> /// <param name="destRelativePath">Relative in-game path (from game root) to install file to.</param> /// <param name="sourceRelativePath">Relative (to mod root) path of new file to install</param> /// <param name="ignoreLoadErrors">Ignore checking if new file exists on disk</param> /// <param name="mod">Mod to parse against</param> /// <returns>string of failure reason. null if OK.</returns> internal string AddFileToInstall(string destRelativePath, string sourceRelativePath, Mod mod) { //Security check if (!checkExtension(sourceRelativePath, out string failReason)) { return(failReason); } string checkingSourceFile; if (JobDirectory != null) { checkingSourceFile = FilesystemInterposer.PathCombine(mod.IsInArchive, mod.ModPath, JobDirectory, sourceRelativePath); } else { //root (legacy) checkingSourceFile = FilesystemInterposer.PathCombine(mod.IsInArchive, mod.ModPath, sourceRelativePath); } if (!FilesystemInterposer.FileExists(checkingSourceFile, mod.Archive)) { return(M3L.GetString(M3L.string_interp_validation_modjob_replacementFileSpecifiedByJobDoesntExist, checkingSourceFile)); } FilesToInstall[destRelativePath.Replace('/', '\\').TrimStart('\\')] = sourceRelativePath.Replace('/', '\\'); return(null); }
public bool ApplyUpdate(IMEPackage package, ExportEntry targetExport, Mod installingMod) { Stream binaryStream; if (OwningMM.Assets[AssetName].AssetBinary != null) { binaryStream = new MemoryStream(OwningMM.Assets[AssetName].AssetBinary); } else { var sourcePath = FilesystemInterposer.PathCombine(installingMod.IsInArchive, installingMod.ModPath, Mod.MergeModFolderName, OwningMM.MergeModFilename); using var fileS = File.OpenRead(sourcePath); fileS.Seek(OwningMM.Assets[AssetName].FileOffset, SeekOrigin.Begin); binaryStream = fileS. ReadToMemoryStream(OwningMM.Assets[AssetName].FileSize); } using var sourcePackage = MEPackageHandler.OpenMEPackageFromStream(binaryStream); var sourceEntry = sourcePackage.FindExport(EntryName); if (sourceEntry == null) { throw new Exception(M3L.GetString(M3L.string_interp_mergefile_cannotFindAssetEntryInAssetPackage, AssetName, EntryName)); } var resultst = EntryImporter.ImportAndRelinkEntries(EntryImporter.PortingOption.ReplaceSingular, sourceEntry, targetExport.FileRef, targetExport, true, new RelinkerOptionsPackage() { ErrorOccurredCallback = x => throw new Exception(M3L.GetString(M3L.string_interp_mergefile_errorMergingAssetsX, x)) }, out _);
public bool ReadImageAssetOptions(Mod modForValidating, Dictionary <string, string> properties) { if (modForValidating.ModDescTargetVersion >= 6.2) { if (properties.TryGetValue(@"ImageAssetName", out string imageAssetName) && !string.IsNullOrWhiteSpace(imageAssetName)) { // We need to validate the file exists var iap = FilesystemInterposer.PathCombine(modForValidating.Archive != null, modForValidating.ModImageAssetsPath, imageAssetName); if (!FilesystemInterposer.FileExists(iap, modForValidating.Archive)) { Log.Error($@"Alternate file {FriendlyName} lists image asset {imageAssetName}, but the asset does not exist in the mods {Mod.ModImageAssetFolderName} directory."); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_validation_alt_imageAssetNotFound, FriendlyName, ImageAssetName, Mod.ModImageAssetFolderName); return(false); } if (modForValidating.Archive != null) { // We need to load this asset cause it's not going to have an open archive until we begin install, if user tries to do install ImageBitmap = LoadImageAsset(modForValidating, imageAssetName); if (ImageBitmap == null) { return(false); // Loading failed. } } ImageAssetName = imageAssetName; } if (!string.IsNullOrWhiteSpace(ImageAssetName)) { // We need to ensure height is also set if (properties.TryGetValue(@"ImageHeight", out string imageHeightStr) && int.TryParse(imageHeightStr, out var imageHeight)) { if (imageHeight < 0 || imageHeight > 1040) { Log.Error($@"Alternate {FriendlyName} lists image asset height {imageHeight}, but it is not within the valid values range. ImageHeight must be between 1 and 1039 inclusive."); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_validation_alt_imageAssetOutOfRangeHeight, FriendlyName, imageHeight); return(false); } ImageHeight = imageHeight; } else { Log.Error($@"Alternate {FriendlyName} specifies an image asset but does not set (or have a valid value for) ImageHeight. ImageHeight is required to be set on alternates that specify an image asset."); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_validation_alt_imageAssetMissingHeight, FriendlyName); return(false); } } } return(true); //Succeeded (or older moddesc that does not support this) }
/// <summary> /// Loads the requested image /// </summary> /// <param name="assetName"></param> /// <returns></returns> public BitmapSource LoadModImageAsset(string assetName) { if (assetName == null) { return(null); } if (assetName.StartsWith(@"/") || assetName.StartsWith(@"\\") || assetName.Contains(@"..")) { Log.Error($@"Image assets cannot contain .. or start with / or \. The asset will not be loaded: {assetName}"); return(null); } if (LoadedImageAssets.TryGetValue(assetName, out var loaded)) { return(loaded); } var imagePathFull = FilesystemInterposer.PathCombine(Archive != null, ModImageAssetsPath, assetName); if (FilesystemInterposer.FileExists(imagePathFull, Archive)) { // Load the image Stream loadStream; if (Archive == null) { // read from disk loadStream = File.OpenRead(imagePathFull); } else { // read from Archive loadStream = new MemoryStream(); Archive.ExtractFile(imagePathFull, loadStream); loadStream.Position = 0; } var bitmap = new BitmapImage(); bitmap.BeginInit(); bitmap.CacheOption = BitmapCacheOption.OnLoad; bitmap.StreamSource = loadStream; bitmap.EndInit(); bitmap.Freeze(); if (loadStream is FileStream fs) { fs.Close(); fs.Dispose(); } LoadedImageAssets[assetName] = bitmap; return(bitmap); // This is so xaml doesn't trigger possibly before this code block has executed } return(null); }
/// <summary> /// Adds a file to the add/replace list of files to install. This will not replace an existing file in the mapping if the destination path is the same, it will instead throw an error. /// </summary> /// <param name="destRelativePath">Relative in-game path (from game root) to install file to.</param> /// <param name="sourceRelativePath">Relative (to mod root) path of new file to install</param> /// <param name="mod">Mod to parse against</param> /// <returns>string of failure reason. null if OK.</returns> internal string AddAdditionalFileToInstall(string destRelativePath, string sourceRelativePath, Mod mod) { //Security check if (!checkExtension(sourceRelativePath, out string failReason)) { return(failReason); } var checkingSourceFile = FilesystemInterposer.PathCombine(mod.IsInArchive, mod.ModPath, JobDirectory, sourceRelativePath); if (!FilesystemInterposer.FileExists(checkingSourceFile, mod.Archive)) { return(M3L.GetString(M3L.string_interp_validation_modjob_additionalFileSpecifiedByJobDoesntExist, checkingSourceFile)); } if (FilesToInstall.ContainsKey(destRelativePath)) { return(M3L.GetString(M3L.string_interp_validation_modjob_additionalFileAlreadyMarkedForModification, destRelativePath)); } FilesToInstall[destRelativePath.Replace('/', '\\').TrimStart('\\')] = sourceRelativePath.Replace('/', '\\'); return(null); }
/// <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(); } }
public AlternateDLC(string alternateDLCText, Mod modForValidating, ModJob job) { var properties = StringStructParser.GetCommaSplitValues(alternateDLCText); //todo: if statements to check these. if (properties.TryGetValue(@"FriendlyName", out string friendlyName)) { FriendlyName = friendlyName; } if (modForValidating.ModDescTargetVersion >= 6 && string.IsNullOrWhiteSpace(FriendlyName)) { //Cannot be null. Log.Error(@"Alternate DLC does not specify FriendlyName. Mods targeting moddesc >= 6.0 require FriendlyName"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_validation_altdlc_oneAltDlcMissingFriendlyNameCmm6); return; } if (!Enum.TryParse(properties[@"Condition"], out Condition)) { Log.Error($@"Alternate DLC specifies unknown/unsupported condition: {properties[@"Condition"]}"); //do not localize ValidAlternate = false; var condition = properties[@"Condition"]; LoadFailedReason = $@"{M3L.GetString(M3L.string_validation_altdlc_unknownCondition)} {condition}"; return; } if (!Enum.TryParse(properties[@"ModOperation"], out Operation)) { Log.Error($@"Alternate DLC specifies unknown/unsupported operation: {properties[@"ModOperation"]}"); //do not localize ValidAlternate = false; var operation = properties[@"ModOperation"]; LoadFailedReason = $@"{M3L.GetString(M3L.string_validation_altdlc_unknownOperation)} {operation}"; return; } if (properties.TryGetValue(@"Description", out string description)) { Description = description; } if (modForValidating.ModDescTargetVersion >= 6 && string.IsNullOrWhiteSpace(Description)) { //Cannot be null. Log.Error($@"Alternate DLC {FriendlyName} cannot have empty Description or missing description as it targets cmmver >= 6"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_cmmver6RequiresDescription, FriendlyName); return; } //OP_NOTHING can have conditions if (properties.TryGetValue(@"ConditionalDLC", out string conditionalDlc)) { var conditionalList = StringStructParser.GetSemicolonSplitList(conditionalDlc); foreach (var dlc in conditionalList) { //if (modForValidating.Game == Mod.MEGame.ME3) //{ //} if (Condition == AltDLCCondition.COND_SPECIFIC_DLC_SETUP) { //check +/- if (!dlc.StartsWith(@"-") && !dlc.StartsWith(@"+")) { Log.Error($@"An item in Alternate DLC's ({FriendlyName}) ConditionalDLC doesn't start with + or -. When using the condition {Condition}, you must precede DLC names with + or -. Bad value: {dlc}"); LoadFailedReason = $"An item in Alternate DLC's ({FriendlyName}) ConditionalDLC doesn't start with + or -. When using the condition {Condition}, you must precede DLC names with + or -. Bad value: {dlc}"; return; } var prefix = dlc.Substring(0, 1); var realname = dlc.Substring(1); //official headers if (Enum.TryParse(realname, out ModJob.JobHeader header) && ModJob.GetHeadersToDLCNamesMap(modForValidating.Game).TryGetValue(header, out var foldername)) { ConditionalDLC.Add(prefix + foldername); continue; } //dlc mods if (!realname.StartsWith(@"DLC_")) { Log.Error($@"An item in Alternate DLC's ({FriendlyName}) ConditionalDLC doesn't start with DLC_ or is not official header (after the +/- required by {Condition}). Bad value: {dlc}"); LoadFailedReason = $"An item in Alternate DLC's ({FriendlyName}) ConditionalDLC doesn't start with DLC_ or is not official header (after the +/- required by {Condition}). Bad value: {dlc}"; return; } else { ConditionalDLC.Add(prefix + realname); } } else { if (Enum.TryParse(dlc, out ModJob.JobHeader header) && ModJob.GetHeadersToDLCNamesMap(modForValidating.Game).TryGetValue(header, out var foldername)) { ConditionalDLC.Add(foldername); continue; } if (!dlc.StartsWith(@"DLC_")) { Log.Error($@"An item in Alternate DLC's ({FriendlyName}) ConditionalDLC doesn't start with DLC_ or is not official header"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_conditionalDLCInvalidValue, FriendlyName); return; } else { ConditionalDLC.Add(dlc); } } } } if (Operation != AltDLCOperation.OP_NOTHING) { int multilistid = -1; if (Operation == AltDLCOperation.OP_ADD_MULTILISTFILES_TO_CUSTOMDLC) { if (properties.TryGetValue(@"MultiListRootPath", out var rootpath)) { MultiListRootPath = rootpath.TrimStart('\\', '/').Replace('/', '\\'); } else { Log.Error($@"Alternate DLC ({FriendlyName}) specifies operation OP_ADD_MULTILISTFILES_TO_CUSTOMDLC but does not specify the required item MultiListRootPath."); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_altdlc_multilistMissingMultiListRootPath, FriendlyName); return; } if (properties.TryGetValue(@"MultiListId", out string multilistidstr) && int.TryParse(multilistidstr, out multilistid)) { if (job.MultiLists.TryGetValue(multilistid, out var ml)) { MultiListSourceFiles = ml; } else { Log.Error($@"Alternate DLC ({FriendlyName}) Multilist ID does not exist as part of the task: multilist" + multilistid); ValidAlternate = false; var id = @"multilist" + multilistid; LoadFailedReason = M3L.GetString(M3L.string_interp_altdlc_multilistMissingMultiListX, FriendlyName, id); return; } } else { Log.Error($@"Alternate DLC ({FriendlyName}) specifies operation OP_ADD_MULTILISTFILES_TO_CUSTOMDLC but does not specify the MultiListId attribute, or it could not be parsed to an integer."); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_altdlc_multilistIdNotIntegerOrMissing, FriendlyName); return; } } else { if (properties.TryGetValue(@"ModAltDLC", out string altDLCFolder)) { AlternateDLCFolder = altDLCFolder.Replace('/', '\\'); } else { Log.Error(@"Alternate DLC does not specify ModAltDLC but is required"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_missingModAltDLC, FriendlyName); return; } } if (properties.TryGetValue(@"ModDestDLC", out string destDLCFolder)) { DestinationDLCFolder = destDLCFolder.Replace('/', '\\'); } else { Log.Error(@"Alternate DLC does not specify ModDestDLC but is required"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_missingModDestDLC, FriendlyName); return; } //todo: Validate target in mod folder //Validation if (string.IsNullOrWhiteSpace(AlternateDLCFolder) && MultiListRootPath == null) { Log.Error($@"Alternate DLC directory (ModAltDLC) not specified for { FriendlyName}"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_sourceDirectoryNotSpecifiedForModAltDLC, FriendlyName); return; } if (string.IsNullOrWhiteSpace(DestinationDLCFolder)) { Log.Error($@"Destination DLC directory (ModDestDLC) not specified for {FriendlyName}"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_destinationDirectoryNotSpecifiedForModDestDLC, FriendlyName); return; } if (AlternateDLCFolder != null) { AlternateDLCFolder = AlternateDLCFolder.TrimStart('\\', '/').Replace('/', '\\'); //Check ModAltDLC directory exists var localAltDlcDir = FilesystemInterposer.PathCombine(modForValidating.IsInArchive, modForValidating.ModPath, AlternateDLCFolder); if (!FilesystemInterposer.DirectoryExists(localAltDlcDir, modForValidating.Archive)) { Log.Error($@"Alternate DLC directory (ModAltDLC) does not exist: {AlternateDLCFolder}"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_sourceDirectoryDoesntExist, FriendlyName, AlternateDLCFolder); return; } } else if (MultiListRootPath != null) { foreach (var multif in MultiListSourceFiles) { var path = FilesystemInterposer.PathCombine(modForValidating.IsInArchive, modForValidating.ModPath, MultiListRootPath, multif); if (!FilesystemInterposer.FileExists(path, modForValidating.Archive)) { Log.Error($@"Alternate DLC ({FriendlyName}) specifies a multilist (index {multilistid}) that contains file that does not exist: {multif}"); LoadFailedReason = M3L.GetString(M3L.string_interp_altdlc_multilistMissingFileInMultilist, FriendlyName, multilistid, multif); return; } } } } DLCRequirementsForManual = properties.TryGetValue(@"DLCRequirements", out string dlcReqs) ? dlcReqs.Split(';') : null; if (Condition == AltDLCCondition.COND_SPECIFIC_SIZED_FILES) { var requiredFilePaths = properties.TryGetValue(@"RequiredFileRelativePaths", out string _requiredFilePaths) ? _requiredFilePaths.Split(';').ToList() : new List <string>(); var requiredFileSizes = properties.TryGetValue(@"RequiredFileSizes", out string _requiredFileSizes) ? _requiredFileSizes.Split(';').ToList() : new List <string>(); if (requiredFilePaths.Count() != requiredFileSizes.Count()) { Log.Error($@"Alternate DLC {FriendlyName} uses COND_SPECIFIC_SIZED_FILES but the amount of items in the RequiredFileRelativePaths and RequiredFileSizes lists are not equal"); ValidAlternate = false; LoadFailedReason = $"Alternate DLC {FriendlyName} uses COND_SPECIFIC_SIZED_FILES but the amount of items in the RequiredFileRelativePaths and RequiredFileSizes lists are not equal"; return; } for (int i = 0; i < requiredFilePaths.Count(); i++) { var reqFile = requiredFilePaths[i]; var reqSizeStr = requiredFileSizes[i]; if (reqFile.Contains(@"..")) { Log.Error($@"Alternate DLC {FriendlyName} RequiredFileRelativePaths item {reqFile} is invalid: Values cannot contain '..' for security reasons"); ValidAlternate = false; LoadFailedReason = $"Alternate DLC {FriendlyName} RequiredFileRelativePaths item {reqFile} is invalid: Values cannot contain '..' for security reasons"; return; } if (long.TryParse(reqSizeStr, out var reqSize) && reqSize >= 0) { RequiredSpecificFiles[reqFile] = reqSize; } else { Log.Error($@"Alternate DLC {FriendlyName} RequiredFileSizes item {reqFile} is invalid: {reqSizeStr}. Values must be greater than or equal to zero."); ValidAlternate = false; LoadFailedReason = $"Alternate DLC {FriendlyName} RequiredFileSizes item {reqFile} is invalid: {reqSizeStr}. Values must be greater than or equal to zero."; return; } } if (!RequiredSpecificFiles.Any()) { Log.Error($@"Alternate DLC {FriendlyName} is invalid: COND_SPECIFIC_SIZED_FILES is specified as the condition but there are no values in RequiredFileRelativePaths/RequiredFileSizes"); ValidAlternate = false; LoadFailedReason = $"Alternate DLC {FriendlyName} is invalid: COND_SPECIFIC_SIZED_FILES is specified as the condition but there are no values in RequiredFileRelativePaths/RequiredFileSizes"; return; } } ApplicableAutoText = properties.TryGetValue(@"ApplicableAutoText", out string applicableText) ? applicableText : M3L.GetString(M3L.string_autoApplied); NotApplicableAutoText = properties.TryGetValue(@"NotApplicableAutoText", out string notApplicableText) ? notApplicableText : M3L.GetString(M3L.string_notApplicable); if (modForValidating.ModDescTargetVersion >= 6.0) { GroupName = properties.TryGetValue(@"OptionGroup", out string groupName) ? groupName : null; //TODO: FORCE OPTIONGROUP TO HAVE ONE ITEM CHECKEDBYDFEAULT. HAVE TO CHECK AT HIGHER LEVEL IN PARSER } if (Condition == AltDLCCondition.COND_MANUAL && properties.TryGetValue(@"CheckedByDefault", out string checkedByDefault) && bool.TryParse(checkedByDefault, out bool cbd)) { CheckedByDefault = cbd; } if (Condition != AltDLCCondition.COND_MANUAL && Condition != AltDLCCondition.INVALID_CONDITION) { //ensure conditional dlc list has at least one item. if (ConditionalDLC.Count == 0) { Log.Error($@"Alternate DLC {FriendlyName} cannot have empty or missing Conditional DLC list, as it does not use COND_MANUAL."); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_altdlc_emptyConditionalDLCList, FriendlyName); return; } } CLog.Information($@"AlternateDLC loaded and validated: {FriendlyName}", Settings.LogModStartup); ValidAlternate = true; }
public AlternateDLC(string alternateDLCText, mod.Mod modForValidating, ModJob job) { var properties = StringStructParser.GetCommaSplitValues(alternateDLCText); //todo: if statements to check these. if (properties.TryGetValue(@"FriendlyName", out string friendlyName)) { FriendlyName = friendlyName; } if (modForValidating.ModDescTargetVersion >= 6 && string.IsNullOrWhiteSpace(FriendlyName)) { //Cannot be null. Log.Error(@"Alternate DLC does not specify FriendlyName. Mods targeting moddesc >= 6.0 require FriendlyName"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_validation_altdlc_oneAltDlcMissingFriendlyNameCmm6); return; } if (!Enum.TryParse(properties[@"Condition"], out Condition)) { Log.Error($@"Alternate DLC specifies unknown/unsupported condition: {properties[@"Condition"]}"); //do not localize ValidAlternate = false; var condition = properties[@"Condition"]; LoadFailedReason = $@"{M3L.GetString(M3L.string_validation_altdlc_unknownCondition)} {condition}"; return; } if (!Enum.TryParse(properties[@"ModOperation"], out Operation)) { Log.Error($@"Alternate DLC specifies unknown/unsupported operation: {properties[@"ModOperation"]}"); //do not localize ValidAlternate = false; var operation = properties[@"ModOperation"]; LoadFailedReason = $@"{M3L.GetString(M3L.string_validation_altdlc_unknownOperation)} {operation}"; return; } if (properties.TryGetValue(@"Description", out string description)) { Description = description; } if (modForValidating.ModDescTargetVersion >= 6 && string.IsNullOrWhiteSpace(Description)) { //Cannot be null. Log.Error($@"Alternate DLC {FriendlyName} cannot have empty Description or missing Description descriptor as it targets cmmver >= 6"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_cmmver6RequiresDescription, FriendlyName); return; } //OP_NOTHING can have conditions if (properties.TryGetValue(@"ConditionalDLC", out string conditionalDlc)) { var conditionalList = StringStructParser.GetSemicolonSplitList(conditionalDlc); foreach (var dlc in conditionalList) { if (Condition == AltDLCCondition.COND_MANUAL) { if (modForValidating.ModDescTargetVersion >= 6.3) { // On 6.3 trigger failure on this mod to help ensure users design mod properly Log.Error($@"{modForValidating.ModName} has Alternate DLC {friendlyName} that has a value for ConditionalDLC on Condition COND_MANUAL. COND_MANUAL does not use ConditionalDLC, use DLCRequirements instead."); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_condManualWithConditionalDLC, friendlyName); return; } else { Log.Warning($@"{modForValidating.ModName} has AlternateDLC {friendlyName} that has a value for ConditionalDLC on Condition COND_MANUAL. COND_MANUAL does not use ConditionalDLC, use DLCRequirements instead. On mods targetting moddesc 6.3 and above, this will trigger a load failure for a mod."); } break; } else if (Condition == AltDLCCondition.COND_SPECIFIC_DLC_SETUP) { //check +/- if (!dlc.StartsWith(@"-") && !dlc.StartsWith(@"+")) { Log.Error($@"An item in Alternate DLC's ({FriendlyName}) ConditionalDLC doesn't start with + or -. When using the condition {Condition}, you must precede DLC names with + or -. Bad value: {dlc}"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_specificDlcSetupMissingPlusMinus, FriendlyName, Condition, dlc); return; } var prefix = dlc.Substring(0, 1); var realname = dlc.Substring(1); //official headers if (Enum.TryParse(realname, out ModJob.JobHeader header) && ModJob.GetHeadersToDLCNamesMap(modForValidating.Game).TryGetValue(header, out var foldername)) { ConditionalDLC.Add(prefix + foldername); continue; } //dlc mods if (!realname.StartsWith(@"DLC_")) { Log.Error($@"An item in Alternate DLC's ({FriendlyName}) ConditionalDLC doesn't start with DLC_ or is not official header (after the +/- required by {Condition}). Bad value: {dlc}"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_specificDlcSetupInvalidDlcName, FriendlyName, Condition, dlc); return; } else { ConditionalDLC.Add(prefix + realname); } } else { if (Enum.TryParse(dlc, out ModJob.JobHeader header) && ModJob.GetHeadersToDLCNamesMap(modForValidating.Game).TryGetValue(header, out var foldername)) { ConditionalDLC.Add(foldername); continue; } if (!dlc.StartsWith(@"DLC_")) { Log.Error($@"An item in Alternate DLC's ({FriendlyName}) ConditionalDLC doesn't start with DLC_ or is not official header"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_conditionalDLCInvalidValue, FriendlyName); return; } else { ConditionalDLC.Add(dlc); } } } } if (Operation != AltDLCOperation.OP_NOTHING) { int multilistid = -1; if (Operation == AltDLCOperation.OP_ADD_MULTILISTFILES_TO_CUSTOMDLC) { if (properties.TryGetValue(@"MultiListRootPath", out var rootpath)) { MultiListRootPath = rootpath.TrimStart('\\', '/').Replace('/', '\\'); } else { Log.Error($@"Alternate DLC ({FriendlyName}) specifies operation OP_ADD_MULTILISTFILES_TO_CUSTOMDLC but does not specify the required item MultiListRootPath."); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_altdlc_multilistMissingMultiListRootPath, FriendlyName); return; } if (properties.TryGetValue(@"MultiListId", out string multilistidstr) && int.TryParse(multilistidstr, out multilistid)) { if (job.MultiLists.TryGetValue(multilistid, out var ml)) { MultiListId = multilistid; MultiListSourceFiles = ml.Select(x => x.TrimStart('\\', '/')).ToArray(); } else { Log.Error($@"Alternate DLC ({FriendlyName}) Multilist ID does not exist as part of the {job.Header} task: multilist" + multilistid); ValidAlternate = false; var id = @"multilist" + multilistid; LoadFailedReason = M3L.GetString(M3L.string_interp_altdlc_multilistMissingMultiListX, FriendlyName, job.Header, id); return; } } else { Log.Error($@"Alternate DLC ({FriendlyName}) specifies operation OP_ADD_MULTILISTFILES_TO_CUSTOMDLC but does not specify the MultiListId attribute, or it could not be parsed to an integer."); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_altdlc_multilistIdNotIntegerOrMissing, FriendlyName); return; } } else { if (properties.TryGetValue(@"ModAltDLC", out string altDLCFolder)) { AlternateDLCFolder = altDLCFolder.Replace('/', '\\'); } else { Log.Error(@"Alternate DLC does not specify ModAltDLC but is required"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_missingModAltDLC, FriendlyName); return; } } if (properties.TryGetValue(@"ModDestDLC", out string destDLCFolder)) { DestinationDLCFolder = destDLCFolder.Replace('/', '\\'); } else { Log.Error(@"Alternate DLC does not specify ModDestDLC but is required"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_missingModDestDLC, FriendlyName); return; } //todo: Validate target in mod folder //Validation if (string.IsNullOrWhiteSpace(AlternateDLCFolder) && MultiListRootPath == null) { Log.Error($@"Alternate DLC directory (ModAltDLC) not specified for {FriendlyName}"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_sourceDirectoryNotSpecifiedForModAltDLC, FriendlyName); return; } if (string.IsNullOrWhiteSpace(DestinationDLCFolder)) { Log.Error($@"Destination DLC directory (ModDestDLC) not specified for {FriendlyName}"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_destinationDirectoryNotSpecifiedForModDestDLC, FriendlyName); return; } if (AlternateDLCFolder != null) { AlternateDLCFolder = AlternateDLCFolder.TrimStart('\\', '/').Replace('/', '\\'); //Check ModAltDLC directory exists var localAltDlcDir = FilesystemInterposer.PathCombine(modForValidating.IsInArchive, modForValidating.ModPath, AlternateDLCFolder); if (!FilesystemInterposer.DirectoryExists(localAltDlcDir, modForValidating.Archive)) { Log.Error($@"Alternate DLC directory (ModAltDLC) does not exist: {AlternateDLCFolder}"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_sourceDirectoryDoesntExist, FriendlyName, AlternateDLCFolder); return; } } else if (MultiListRootPath != null) { foreach (var multif in MultiListSourceFiles) { var path = FilesystemInterposer.PathCombine(modForValidating.IsInArchive, modForValidating.ModPath, MultiListRootPath, multif); if (!FilesystemInterposer.FileExists(path, modForValidating.Archive)) { Log.Error($@"Alternate DLC ({FriendlyName}) specifies a multilist (index {multilistid}) that contains file that does not exist: {multif}"); LoadFailedReason = M3L.GetString(M3L.string_interp_altdlc_multilistMissingFileInMultilist, FriendlyName, multilistid, multif); return; } } } // Validate multilist dlc } var dlcReqs = properties.TryGetValue(@"DLCRequirements", out string _dlcReqs) ? _dlcReqs.Split(';') : null; if (dlcReqs != null) { var reqList = new List <string>(); foreach (var originalReq in dlcReqs) { var testreq = originalReq; string prefix = ""; if (modForValidating.ModDescTargetVersion >= 6.3) { if (testreq.StartsWith("-") || testreq.StartsWith("+")) { prefix = testreq[0].ToString(); } testreq = testreq.TrimStart('-', '+'); } //official headers if (Enum.TryParse(testreq, out ModJob.JobHeader header) && ModJob.GetHeadersToDLCNamesMap(modForValidating.Game).TryGetValue(header, out var foldername)) { reqList.Add(prefix + foldername); continue; } //dlc mods if (!testreq.StartsWith(@"DLC_")) { Log.Error($@"An item in Alternate DLC's ({FriendlyName}) DLCRequirements doesn't start with DLC_ or is not official header. Bad value: {originalReq}"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_dlcRequirementInvalid, FriendlyName, originalReq); return; } else { reqList.Add(originalReq); } } DLCRequirementsForManual = reqList.ToArray(); } if (Condition == AltDLCCondition.COND_SPECIFIC_SIZED_FILES) { var requiredFilePaths = properties.TryGetValue(@"RequiredFileRelativePaths", out string _requiredFilePaths) ? _requiredFilePaths.Split(';').ToList() : new List <string>(); var requiredFileSizes = properties.TryGetValue(@"RequiredFileSizes", out string _requiredFileSizes) ? _requiredFileSizes.Split(';').ToList() : new List <string>(); if (requiredFilePaths.Count() != requiredFileSizes.Count()) { Log.Error($@"Alternate DLC {FriendlyName} uses COND_SPECIFIC_SIZED_FILES but the amount of items in the RequiredFileRelativePaths and RequiredFileSizes lists are not equal"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_specificSizedFilesMismatchedParams, FriendlyName); return; } for (int i = 0; i < requiredFilePaths.Count(); i++) { var reqFile = requiredFilePaths[i]; var reqSizeStr = requiredFileSizes[i]; if (reqFile.Contains(@"..")) { Log.Error($@"Alternate DLC {FriendlyName} RequiredFileRelativePaths item {reqFile} is invalid: Values cannot contain '..' for security reasons"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_specificSizedFilesContainsIllegalPattern, FriendlyName, reqFile); return; } reqFile = reqFile.Replace('/', '\\').TrimStart('\\'); //standardize if (long.TryParse(reqSizeStr, out var reqSize) && reqSize >= 0) { RequiredSpecificFiles[reqFile] = reqSize; } else { Log.Error($@"Alternate DLC {FriendlyName} RequiredFileSizes item {reqFile} is invalid: {reqSizeStr}. Values must be greater than or equal to zero."); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_specificSizedFileMustBeLargerThanZero, FriendlyName, reqFile, reqSizeStr); return; } } if (!RequiredSpecificFiles.Any()) { Log.Error($@"Alternate DLC {FriendlyName} is invalid: COND_SPECIFIC_SIZED_FILES is specified as the condition but there are no values in RequiredFileRelativePaths/RequiredFileSizes"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_specificSizedFilesMissingRequiredParams, FriendlyName); return; } } if (!ReadImageAssetOptions(modForValidating, properties)) { return; // Failed in super call } ReadAutoApplicableText(properties); if (modForValidating.ModDescTargetVersion >= 6.0) { GroupName = properties.TryGetValue(@"OptionGroup", out string groupName) ? groupName : null; } if (Condition == AltDLCCondition.COND_MANUAL && properties.TryGetValue(@"CheckedByDefault", out string checkedByDefault) && bool.TryParse(checkedByDefault, out bool cbd)) { CheckedByDefault = cbd; } if (Condition != AltDLCCondition.COND_MANUAL && Condition != AltDLCCondition.COND_SPECIFIC_SIZED_FILES && Condition != AltDLCCondition.INVALID_CONDITION) { //ensure conditional dlc list has at least one item. if (ConditionalDLC.Count == 0) { Log.Error($@"Alternate DLC {FriendlyName} cannot have empty or missing Conditional DLC list, as it does not use COND_MANUAL or COND_SPECIFIC_SIZED_FILES."); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_altdlc_emptyConditionalDLCList, FriendlyName); return; } } CLog.Information($@"AlternateDLC loaded and validated: {FriendlyName}", Settings.LogModStartup); ValidAlternate = true; }
/// <summary> /// Loads the requested image /// </summary> /// <param name="assetName"></param> /// <returns></returns> public BitmapSource LoadModImageAsset(string assetName) { if (assetName == null) { return(null); } if (assetName.StartsWith(@"/") || assetName.StartsWith(@"\\") || assetName.Contains(@"..")) { Log.Error($@"Image assets cannot contain .. or start with / or \. The asset will not be loaded: {assetName}"); return(null); } if (LoadedImageAssets.TryGetValue(assetName, out var loaded)) { return(loaded); } Stream loadStream = null; try { var imagePathFull = FilesystemInterposer.PathCombine(Archive != null, ModImageAssetsPath, assetName); if (FilesystemInterposer.FileExists(imagePathFull, Archive)) { // Load the image if (Archive == null) { // read from disk loadStream = File.OpenRead(imagePathFull); } else { // read from Archive loadStream = new MemoryStream(); Archive.ExtractFile(imagePathFull, loadStream); loadStream.Position = 0; } var bitmap = new BitmapImage(); bitmap.BeginInit(); bitmap.CacheOption = BitmapCacheOption.OnLoad; bitmap.CreateOptions = BitmapCreateOptions.IgnoreColorProfile; // Fixes crashes on things like ICC, maybe? bitmap.StreamSource = loadStream; bitmap.EndInit(); bitmap.Freeze(); LoadedImageAssets[assetName] = bitmap; return(bitmap); // This is so xaml doesn't trigger possibly before this code block has executed } } catch (Exception e) { Log.Error($@"Error loading image asset {assetName}: {e.Message}. The asset will not be loaded"); } finally { // Ensure file is closed if (loadStream is FileStream fs) { fs.Close(); fs.Dispose(); } } return(null); }
public AlternateFile(string alternateFileText, Mod modForValidating) { var properties = StringStructParser.GetCommaSplitValues(alternateFileText); if (properties.TryGetValue("FriendlyName", out string friendlyName)) { FriendlyName = friendlyName; } if (modForValidating.ModDescTargetVersion >= 6 && string.IsNullOrWhiteSpace(FriendlyName)) { //Cannot be null. Log.Error($"Alternate File does not specify FriendlyName. Mods targeting moddesc >= 6.0 cannot have empty FriendlyName"); ValidAlternate = false; LoadFailedReason = $"At least one specified Alternate File does not specify a FriendlyName, which is required for mods targeting cmmver >= 6.0."; return; } if (!Enum.TryParse(properties["Condition"], out Condition)) { Log.Error("Alternate File specifies unknown/unsupported condition: " + properties["Condition"]); ValidAlternate = false; LoadFailedReason = "Alternate File specifies unknown/unsupported condition: " + properties["Condition"]; return; } if (properties.TryGetValue("ConditionalDLC", out string conditionalDlc)) { var conditionalList = StringStructParser.GetSemicolonSplitList(conditionalDlc); foreach (var dlc in conditionalList) { //if (modForValidating.Game == Mod.MEGame.ME3) //{ if (Enum.TryParse(dlc, out ModJob.JobHeader header) && ModJob.GetHeadersToDLCNamesMap(modForValidating.Game).TryGetValue(header, out var foldername)) { ConditionalDLC.Add(foldername); continue; } //} if (!dlc.StartsWith("DLC_")) { Log.Error("An item in Alternate Files's ConditionalDLC doesn't start with DLC_"); LoadFailedReason = $"Alternate File ({FriendlyName}) specifies conditional DLC but no values match the allowed headers or start with DLC_."; return; } else { ConditionalDLC.Add(dlc); } } } if (!Enum.TryParse(properties["ModOperation"], out Operation)) { Log.Error("Alternate File specifies unknown/unsupported operation: " + properties["ModOperation"]); ValidAlternate = false; LoadFailedReason = "Alternate File specifies unknown/unsupported operation: " + properties["ModOperation"]; return; } if (properties.TryGetValue("Description", out string description)) { Description = description; } if (modForValidating.ModDescTargetVersion >= 6 && string.IsNullOrWhiteSpace(Description)) { //Cannot be null. Log.Error($"Alternate File {FriendlyName} with mod targeting moddesc >= 6.0 cannot have empty Description or missing description"); ValidAlternate = false; LoadFailedReason = $"Alternate File {FriendlyName} does not specify a Description, which is required for mods targeting cmmver >= 6.0."; return; } if (properties.TryGetValue("ModFile", out string modfile)) { ModFile = modfile.TrimStart('\\', '/'); } else { Log.Error("Alternate file in-mod target (ModFile) required but not specified. This value is required for all Alternate files"); ValidAlternate = false; LoadFailedReason = $"Alternate file {FriendlyName} does not declare ModFile but it is required for all Alternate Files."; return; } if (properties.TryGetValue("MultiMappingFile", out string multifilemapping)) { MultiMappingFile = multifilemapping.TrimStart('\\', '/'); } if (properties.TryGetValue("AltFile", out string altfile)) { AltFile = altfile; } else if (AltFile == null && properties.TryGetValue("ModAltFile", out string maltfile)) { AltFile = maltfile; } properties.TryGetValue("SubstituteFile", out SubstituteFile); //Only used in 4.5. In 5.0 and above this became AltFile. //workaround for 4.5 if (modForValidating.ModDescTargetVersion == 4.5 && Operation == AltFileOperation.OP_SUBSTITUTE && SubstituteFile != null) { AltFile = SubstituteFile; } if (!string.IsNullOrEmpty(AltFile)) { AltFile = AltFile.Replace('/', '\\'); //Standardize paths } //This needs reworked from java's hack implementation //Need to identify mods using substitution features if (Operation == AltFileOperation.OP_INSTALL || Operation == AltFileOperation.OP_SUBSTITUTE) { if (MultiMappingFile == null) { //Validate file var altPath = FilesystemInterposer.PathCombine(modForValidating.IsInArchive, modForValidating.ModPath, AltFile); var altFileSourceExists = FilesystemInterposer.FileExists(altPath, modForValidating.Archive); if (!altFileSourceExists) { Log.Error("Alternate file source (AltFile) does not exist: " + AltFile); ValidAlternate = false; LoadFailedReason = $"Alternate file is specified with operation {Operation}, but required file doesn't exist: {AltFile}"; return; } //Ensure it is not part of DLC directory itself. var modFile = FilesystemInterposer.PathCombine(modForValidating.IsInArchive, modForValidating.ModPath, ModFile); //Todo } else { //Multimapping, Todo } } if (properties.TryGetValue("ApplicableAutoText", out string applicableText)) { ApplicableAutoText = applicableText; } else { ApplicableAutoText = "Auto Applied"; } if (properties.TryGetValue("NotApplicableAutoText", out string notApplicableText)) { NotApplicableAutoText = notApplicableText; } else { NotApplicableAutoText = "Not applicable"; } if (Condition == AltFileCondition.COND_MANUAL && properties.TryGetValue("CheckedByDefault", out string checkedByDefault) && bool.TryParse(checkedByDefault, out bool cbd)) { CheckedByDefault = cbd; } CLog.Information($"Alternate file loaded and validated: {FriendlyName}", Settings.LogModStartup); ValidAlternate = true; }
/// <summary> /// Installs a TLK merge. Returns null if OK, otherwise returns an error string. /// </summary> /// <param name="tlkXmlName"></param> /// <param name="gameFileMapping"></param> /// <returns></returns> public string InstallTLKMerge(string tlkXmlName, Dictionary <string, string> gameFileMapping, bool savePackage, PackageCache cache, GameTarget target, Mod modBeingInstalled, Action <BasegameFileIdentificationService.BasegameCloudDBFile> addCloudDBEntry) { // Need to load file into memory string xmlContents; var sourcePath = FilesystemInterposer.PathCombine(IsInArchive, ModPath, Mod.Game1EmbeddedTlkFolderName, tlkXmlName); if (Archive != null) { var ms = new MemoryStream(); Archive.ExtractFile(sourcePath, ms); ms.Position = 0; xmlContents = new StreamReader(ms).ReadToEnd(); } else { xmlContents = File.ReadAllText(sourcePath); } var tlkDoc = XDocument.Parse(xmlContents); var stringNodes = tlkDoc.Root.Descendants(@"string").ToList(); if (stringNodes.Any()) { // Open package var packageName = tlkXmlName.Substring(0, tlkXmlName.IndexOf('.')); var exportPath = Path.GetFileNameWithoutExtension(tlkXmlName.Substring(packageName.Length + 1)); string packagePath = null;; if (Game is MEGame.LE1) { gameFileMapping.TryGetValue(packageName + @".pcc", out packagePath); } else if (Game is MEGame.ME1) { gameFileMapping.TryGetValue(packageName + @".sfm", out packagePath); if (packagePath == null) { gameFileMapping.TryGetValue(packageName + @".u", out packagePath); } if (packagePath == null) { gameFileMapping.TryGetValue(packageName + @".upk", out packagePath); } } if (packagePath != null) { var package = cache.GetCachedPackage(packagePath); var exp = package.FindExport(exportPath); if (exp == null) { // WRONGLY NAMED EXPORT! Log.Error($@"Could not find export in package {packagePath} for TLK merge: {exportPath}"); return(M3L.GetString(M3L.string_interp_tlkmerge_couldNotFindExportInPackage, packagePath, exportPath)); } var talkFile = package.LocalTalkFiles.FirstOrDefault(x => x.UIndex == exp.UIndex); var strRefs = talkFile.StringRefs.ToList(); int numDone = 0; foreach (var node in stringNodes) { var tlkId = int.Parse(node.Element(@"id").Value); var flags = int.Parse(node.Element(@"flags").Value); var data = node.Element(@"data").Value; var strRef = talkFile.StringRefs.FirstOrDefault(x => x.StringID == tlkId); if (strRef == null) { CLog.Information($@"Adding new TLK id {tlkId}", Settings.LogModInstallation); strRefs.Add(new TLKStringRef(tlkId, data, flags)); } else { //if (numDone <= 25) //{ // //CLog.Information($@"Updating TLK id {tlkId}", Settings.LogModInstallation); // if (numDone == 25) // { // //CLog.Information($@"Remaining updates will not be logged for this TLK to trim log size...", Settings.LogModInstallation); // } //} strRef.Data = data; // Flags update was removed here } numDone++; } HuffmanCompression huff = new HuffmanCompression(); huff.LoadInputData(strRefs); huff.SerializeTalkfileToExport(exp); if (savePackage && package.IsModified) { Log.Information($@"Saving TLKMerged package {packagePath}"); package.Save(); addCloudDBEntry?.Invoke(new BasegameFileIdentificationService.BasegameCloudDBFile(package.FilePath, (int)new FileInfo(package.FilePath).Length, target, modBeingInstalled)); cache.DropPackageFromCache(packagePath); // we are not doing more operations on this file so drop it out } } } // Logic subject to change in future! return(null); }
private void InstallModBackgroundThread(object sender, DoWorkEventArgs e) { Log.Information(@"Mod Installer Background thread starting"); var installationJobs = ModBeingInstalled.InstallationJobs; var gameDLCPath = MEDirectories.DLCPath(gameTarget); Directory.CreateDirectory(gameDLCPath); //me1/me2 missing dlc might not have this folder //Check we can install var missingRequiredDLC = ModBeingInstalled.ValidateRequiredModulesAreInstalled(gameTarget); if (missingRequiredDLC.Count > 0) { e.Result = (ModInstallCompletedStatus.INSTALL_FAILED_REQUIRED_DLC_MISSING, missingRequiredDLC); return; } //Check/warn on official headers if (!PrecheckHeaders(installationJobs)) { e.Result = ModInstallCompletedStatus.INSTALL_FAILED_USER_CANCELED_MISSING_MODULES; return; } //todo: If statment on this Utilities.InstallBinkBypass(gameTarget); //Always install binkw32, don't bother checking if it is already ASI version. if (ModBeingInstalled.Game == Mod.MEGame.ME2 && ModBeingInstalled.GetJob(ModJob.JobHeader.ME2_RCWMOD) != null && installationJobs.Count == 1) { e.Result = InstallAttachedRCWMod(); return; } //Prepare queues (Dictionary <ModJob, (Dictionary <string, string> fileMapping, List <string> dlcFoldersBeingInstalled)> unpackedJobMappings, List <(ModJob job, string sfarPath, Dictionary <string, string> sfarInstallationMapping)> sfarJobs)installationQueues = ModBeingInstalled.GetInstallationQueues(gameTarget); var readOnlyTargets = ModBeingInstalled.GetAllRelativeReadonlyTargets(me1ConfigReadOnlyOption.IsSelected); if (gameTarget.ALOTInstalled) { //Check if any packages are being installed. If there are, we will block this installation. bool installsPackageFile = false; foreach (var jobMappings in installationQueues.unpackedJobMappings) { installsPackageFile |= jobMappings.Value.fileMapping.Keys.Any(x => x.EndsWith(@".pcc", StringComparison.InvariantCultureIgnoreCase)); installsPackageFile |= jobMappings.Value.fileMapping.Keys.Any(x => x.EndsWith(@".u", StringComparison.InvariantCultureIgnoreCase)); installsPackageFile |= jobMappings.Value.fileMapping.Keys.Any(x => x.EndsWith(@".upk", StringComparison.InvariantCultureIgnoreCase)); installsPackageFile |= jobMappings.Value.fileMapping.Keys.Any(x => x.EndsWith(@".sfm", StringComparison.InvariantCultureIgnoreCase)); } foreach (var jobMappings in installationQueues.sfarJobs) { installsPackageFile |= jobMappings.sfarInstallationMapping.Keys.Any(x => x.EndsWith(@".pcc", StringComparison.InvariantCultureIgnoreCase)); installsPackageFile |= jobMappings.sfarInstallationMapping.Keys.Any(x => x.EndsWith(@".u", StringComparison.InvariantCultureIgnoreCase)); installsPackageFile |= jobMappings.sfarInstallationMapping.Keys.Any(x => x.EndsWith(@".upk", StringComparison.InvariantCultureIgnoreCase)); installsPackageFile |= jobMappings.sfarInstallationMapping.Keys.Any(x => x.EndsWith(@".sfm", StringComparison.InvariantCultureIgnoreCase)); } if (installsPackageFile) { if (Settings.DeveloperMode) { Log.Warning(@"ALOT is installed and user is attempting to install a mod (in developer mode). Prompting user to cancel installation"); bool cancel = false; Application.Current.Dispatcher.Invoke(delegate { var res = M3L.ShowDialog(Window.GetWindow(this), M3L.GetString(M3L.string_interp_devModeAlotInstalledWarning, ModBeingInstalled.ModName), M3L.GetString(M3L.string_brokenTexturesWarning), MessageBoxButton.YesNo, MessageBoxImage.Error, MessageBoxResult.No); cancel = res == MessageBoxResult.No; }); if (cancel) { e.Result = ModInstallCompletedStatus.USER_CANCELED_INSTALLATION; return; } Log.Warning(@"User installing mod anyways even with ALOT installed"); } else { Log.Error(@"ALOT is installed. Installing mods that install package files after installing ALOT is not permitted."); //ALOT Installed, this is attempting to install a package file e.Result = ModInstallCompletedStatus.INSTALL_FAILED_ALOT_BLOCKING; return; } } } Action = M3L.GetString(M3L.string_installing); PercentVisibility = Visibility.Visible; Percent = 0; int numdone = 0; //Calculate number of installation tasks beforehand int numFilesToInstall = installationQueues.unpackedJobMappings.Select(x => x.Value.fileMapping.Count).Sum(); numFilesToInstall += installationQueues.sfarJobs.Select(x => x.sfarInstallationMapping.Count).Sum() * (ModBeingInstalled.IsInArchive ? 2 : 1); //*2 as we have to extract and install Debug.WriteLine(@"Number of expected installation tasks: " + numFilesToInstall); void FileInstalledCallback(string target) { numdone++; Debug.WriteLine(@"Installed: " + target); Action = M3L.GetString(M3L.string_installing); var now = DateTime.Now; if (numdone > numFilesToInstall) { Debug.WriteLine($@"Percentage calculated is wrong. Done: {numdone} NumToDoTotal: {numFilesToInstall}"); } if ((now - lastPercentUpdateTime).Milliseconds > PERCENT_REFRESH_COOLDOWN) { //Don't update UI too often. Once per second is enough. Percent = (int)(numdone * 100.0 / numFilesToInstall); lastPercentUpdateTime = now; } } //Stage: Unpacked files build map Dictionary <string, string> fullPathMappingDisk = new Dictionary <string, string>(); Dictionary <int, string> fullPathMappingArchive = new Dictionary <int, string>(); SortedSet <string> customDLCsBeingInstalled = new SortedSet <string>(); List <string> mappedReadOnlyTargets = new List <string>(); foreach (var unpackedQueue in installationQueues.unpackedJobMappings) { foreach (var originalMapping in unpackedQueue.Value.fileMapping) { //always unpacked //if (unpackedQueue.Key == ModJob.JobHeader.CUSTOMDLC || unpackedQueue.Key == ModJob.JobHeader.BALANCE_CHANGES || unpackedQueue.Key == ModJob.JobHeader.BASEGAME) //{ //Resolve source file path string sourceFile; if (unpackedQueue.Key.JobDirectory == null) { sourceFile = FilesystemInterposer.PathCombine(ModBeingInstalled.IsInArchive, ModBeingInstalled.ModPath, originalMapping.Value); } else { sourceFile = FilesystemInterposer.PathCombine(ModBeingInstalled.IsInArchive, ModBeingInstalled.ModPath, unpackedQueue.Key.JobDirectory, originalMapping.Value); } if (unpackedQueue.Key.Header == ModJob.JobHeader.ME1_CONFIG) { var destFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), @"BioWare", @"Mass Effect", @"Config", originalMapping.Key); if (ModBeingInstalled.IsInArchive) { int archiveIndex = ModBeingInstalled.Archive.ArchiveFileNames.IndexOf(sourceFile, StringComparer.InvariantCultureIgnoreCase); fullPathMappingArchive[archiveIndex] = destFile; //used for extraction indexing if (archiveIndex == -1) { Log.Error($@"Archive Index is -1 for file {sourceFile}. This will probably throw an exception!"); Debugger.Break(); } fullPathMappingDisk[sourceFile] = destFile; //used for redirection } else { fullPathMappingDisk[sourceFile] = destFile; } } else { var destFile = Path.Combine(unpackedQueue.Key.Header == ModJob.JobHeader.CUSTOMDLC ? MEDirectories.DLCPath(gameTarget) : gameTarget.TargetPath, originalMapping.Key); //official //Extract Custom DLC name if (unpackedQueue.Key.Header == ModJob.JobHeader.CUSTOMDLC) { var custDLC = destFile.Substring(gameDLCPath.Length, destFile.Length - gameDLCPath.Length).TrimStart('\\', '/'); var nextSlashIndex = custDLC.IndexOf('\\'); if (nextSlashIndex == -1) { nextSlashIndex = custDLC.IndexOf('/'); } if (nextSlashIndex != -1) { custDLC = custDLC.Substring(0, nextSlashIndex); customDLCsBeingInstalled.Add(custDLC); } } if (ModBeingInstalled.IsInArchive) { int archiveIndex = ModBeingInstalled.Archive.ArchiveFileNames.IndexOf(sourceFile, StringComparer.InvariantCultureIgnoreCase); fullPathMappingArchive[archiveIndex] = destFile; //used for extraction indexing if (archiveIndex == -1) { Log.Error($@"Archive Index is -1 for file {sourceFile}. This will probably throw an exception!"); Debugger.Break(); } } fullPathMappingDisk[sourceFile] = destFile; //archive also uses this for redirection } if (readOnlyTargets.Contains(originalMapping.Key)) { CLog.Information(@"Adding resolved read only target: " + originalMapping.Key + @" -> " + fullPathMappingDisk[sourceFile], Settings.LogModInstallation); mappedReadOnlyTargets.Add(fullPathMappingDisk[sourceFile]); } //} } } //Substage: Add SFAR staging targets string sfarStagingDirectory = (ModBeingInstalled.IsInArchive && installationQueues.sfarJobs.Count > 0) ? Directory.CreateDirectory(Path.Combine(Utilities.GetTempPath(), @"SFARJobStaging")).FullName : null; //don't make directory if we don't need one if (sfarStagingDirectory != null) { foreach (var sfarJob in installationQueues.sfarJobs) { foreach (var fileToInstall in sfarJob.sfarInstallationMapping) { string sourceFile = FilesystemInterposer.PathCombine(ModBeingInstalled.IsInArchive, ModBeingInstalled.ModPath, sfarJob.job.JobDirectory, fileToInstall.Value); int archiveIndex = ModBeingInstalled.Archive.ArchiveFileNames.IndexOf(sourceFile, StringComparer.InvariantCultureIgnoreCase); if (archiveIndex == -1) { Log.Error($@"Archive Index is -1 for file {sourceFile}. This will probably throw an exception!"); Debugger.Break(); } string destFile = Path.Combine(sfarStagingDirectory, sfarJob.job.JobDirectory, fileToInstall.Value); fullPathMappingArchive[archiveIndex] = destFile; //used for extraction indexing fullPathMappingDisk[sourceFile] = destFile; //used for redirection Debug.WriteLine($@"SFAR Disk Staging: {fileToInstall.Key} => {destFile}"); } } } //Check we have enough disk space long requiredSpaceToInstall = 0L; if (ModBeingInstalled.IsInArchive) { foreach (var f in ModBeingInstalled.Archive.ArchiveFileData) { if (fullPathMappingArchive.ContainsKey(f.Index)) { //we are installing this file requiredSpaceToInstall += (long)f.Size; } } } else { foreach (var file in fullPathMappingDisk) { requiredSpaceToInstall += new FileInfo(file.Key).Length; } } Utilities.DriveFreeBytes(gameTarget.TargetPath, out var freeSpaceOnTargetDisk); requiredSpaceToInstall = (long)(requiredSpaceToInstall * 1.05); //+5% for some overhead if (requiredSpaceToInstall > (long)freeSpaceOnTargetDisk && freeSpaceOnTargetDisk != 0) { string driveletter = Path.GetPathRoot(gameTarget.TargetPath); Log.Error($@"Insufficient disk space to install mod. Required: {ByteSize.FromBytes(requiredSpaceToInstall)}, available on {driveletter}: {ByteSize.FromBytes(freeSpaceOnTargetDisk)}"); Application.Current.Dispatcher.Invoke(() => { string message = M3L.GetString(M3L.string_interp_dialogNotEnoughSpaceToInstall, driveletter, ModBeingInstalled.ModName, ByteSize.FromBytes(requiredSpaceToInstall).ToString(), ByteSize.FromBytes(freeSpaceOnTargetDisk).ToString()); Xceed.Wpf.Toolkit.MessageBox.Show(window, message, M3L.GetString(M3L.string_insufficientDiskSpace), MessageBoxButton.OK, MessageBoxImage.Error); }); e.Result = ModInstallCompletedStatus.INSTALL_ABORTED_NOT_ENOUGH_SPACE; return; } //Delete existing custom DLC mods with same name foreach (var cdbi in customDLCsBeingInstalled) { var path = Path.Combine(gameDLCPath, cdbi); if (Directory.Exists(path)) { Log.Information($@"Deleting existing DLC directory: {path}"); Utilities.DeleteFilesAndFoldersRecursively(path); } } //Stage: Unpacked files installation if (!ModBeingInstalled.IsInArchive) { //Direct copy Log.Information($@"Installing {fullPathMappingDisk.Count} unpacked files into game directory"); CopyDir.CopyFiles_ProgressBar(fullPathMappingDisk, FileInstalledCallback); } else { Action = M3L.GetString(M3L.string_loadingModArchive); //Extraction to destination string installationRedirectCallback(ArchiveFileInfo info) { var inArchivePath = info.FileName; var redirectedPath = fullPathMappingDisk[inArchivePath]; Debug.WriteLine($@"Redirecting {inArchivePath} to {redirectedPath}"); return(redirectedPath); } ModBeingInstalled.Archive.FileExtractionStarted += (sender, args) => { //CLog.Information("Extracting mod file for installation: " + args.FileInfo.FileName, Settings.LogModInstallation); }; List <string> filesInstalled = new List <string>(); List <string> filesToInstall = installationQueues.unpackedJobMappings.SelectMany(x => x.Value.fileMapping.Keys).ToList(); ModBeingInstalled.Archive.FileExtractionFinished += (sender, args) => { if (args.FileInfo.IsDirectory) { return; //ignore } if (!fullPathMappingArchive.ContainsKey(args.FileInfo.Index)) { return; //archive extracted this file (in memory) but did not do anything with this file (7z) } FileInstalledCallback(args.FileInfo.FileName); filesInstalled.Add(args.FileInfo.FileName); //Debug.WriteLine($"{args.FileInfo.FileName} as file { numdone}"); //Debug.WriteLine(numdone); }; ModBeingInstalled.Archive.ExtractFiles(gameTarget.TargetPath, installationRedirectCallback, fullPathMappingArchive.Keys.ToArray()); //directory parameter shouldn't be used here as we will be redirecting everything } //Write MetaCMM List <string> addedDLCFolders = new List <string>(); foreach (var v in installationQueues.unpackedJobMappings) { addedDLCFolders.AddRange(v.Value.dlcFoldersBeingInstalled); } foreach (var addedDLCFolder in addedDLCFolders) { var metacmm = Path.Combine(addedDLCFolder, @"_metacmm.txt"); ModBeingInstalled.HumanReadableCustomDLCNames.TryGetValue(Path.GetFileName(addedDLCFolder), out var assignedDLCName); string contents = $"{assignedDLCName ?? ModBeingInstalled.ModName}\n{ModBeingInstalled.ModVersionString}\n{App.BuildNumber}\n{Guid.NewGuid().ToString()}"; //Do not localize File.WriteAllText(metacmm, contents); } //Stage: SFAR Installation foreach (var sfarJob in installationQueues.sfarJobs) { InstallIntoSFAR(sfarJob, ModBeingInstalled, FileInstalledCallback, ModBeingInstalled.IsInArchive ? sfarStagingDirectory : null); } //Main installation step has completed CLog.Information(@"Main stage of mod installation has completed", Settings.LogModInstallation); Percent = (int)(numdone * 100.0 / numFilesToInstall); //Mark items read only foreach (var readonlytarget in mappedReadOnlyTargets) { CLog.Information(@"Setting file to read-only: " + readonlytarget, Settings.LogModInstallation); File.SetAttributes(readonlytarget, File.GetAttributes(readonlytarget) | FileAttributes.ReadOnly); } //Remove outdated custom DLC foreach (var outdatedDLCFolder in ModBeingInstalled.OutdatedCustomDLC) { var outdatedDLCInGame = Path.Combine(gameDLCPath, outdatedDLCFolder); if (Directory.Exists(outdatedDLCInGame)) { Log.Information(@"Deleting outdated custom DLC folder: " + outdatedDLCInGame); Utilities.DeleteFilesAndFoldersRecursively(outdatedDLCInGame); } } //Install supporting ASI files if necessary //Todo: Upgrade to version detection code from ME3EXP to prevent conflicts Action = M3L.GetString(M3L.string_installingSupportFiles); PercentVisibility = Visibility.Collapsed; CLog.Information(@"Installing supporting ASI files", Settings.LogModInstallation); if (ModBeingInstalled.Game == Mod.MEGame.ME1) { //Todo: Convert to ASI Manager installer Utilities.InstallASIByGroupID(gameTarget, @"DLC Mod Enabler", 16); //16 = DLC M -od Enabler //Utilities.InstallEmbeddedASI(@"ME1-DLC-ModEnabler-v1.0", 1.0, gameTarget); //Todo: Switch to ASI Manager } else if (ModBeingInstalled.Game == Mod.MEGame.ME2) { //None right now } else { if (ModBeingInstalled.GetJob(ModJob.JobHeader.BALANCE_CHANGES) != null) { Utilities.InstallASIByGroupID(gameTarget, @"Balance Changes Replacer", 5); //Utilities.InstallASIByGroupID(gameTarget, @"ME3Logger-Truncating", 5); //Utilities.InstallEmbeddedASI(@"BalanceChangesReplacer-v2.0", 2.0, gameTarget); //todo: Switch to ASI Manager } } if (sfarStagingDirectory != null) { Utilities.DeleteFilesAndFoldersRecursively(Utilities.GetTempPath()); } if (numFilesToInstall == numdone) { e.Result = ModInstallCompletedStatus.INSTALL_SUCCESSFUL; Action = M3L.GetString(M3L.string_installed); } else { Log.Warning($@"Number of completed items does not equal the amount of items to install! Number installed {numdone} Number expected: {numFilesToInstall}"); e.Result = ModInstallCompletedStatus.INSTALL_WRONG_NUMBER_OF_COMPLETED_ITEMS; } }
private void InstallModBackgroundThread(object sender, DoWorkEventArgs e) { Log.Information($"Mod Installer Background thread starting"); var installationJobs = ModBeingInstalled.InstallationJobs; var gamePath = gameTarget.TargetPath; var gameDLCPath = MEDirectories.DLCPath(gameTarget); Directory.CreateDirectory(gameDLCPath); //me1/me2 missing dlc might not have this folder //Check we can install var missingRequiredDLC = ModBeingInstalled.ValidateRequiredModulesAreInstalled(gameTarget); if (missingRequiredDLC.Count > 0) { e.Result = (ModInstallCompletedStatus.INSTALL_FAILED_REQUIRED_DLC_MISSING, missingRequiredDLC); return; } //Check/warn on official headers if (!PrecheckHeaders(gameDLCPath, installationJobs)) { e.Result = ModInstallCompletedStatus.INSTALL_FAILED_USER_CANCELED_MISSING_MODULES; return; } //todo: If statment on this Utilities.InstallBinkBypass(gameTarget); //Always install binkw32, don't bother checking if it is already ASI version. //Prepare queues (Dictionary <ModJob, (Dictionary <string, string> fileMapping, List <string> dlcFoldersBeingInstalled)> unpackedJobMappings, List <(ModJob job, string sfarPath, Dictionary <string, string> sfarInstallationMapping)> sfarJobs)installationQueues = ModBeingInstalled.GetInstallationQueues(gameTarget); if (gameTarget.ALOTInstalled) { //Check if any packages are being installed. If there are, we will block this installation. bool installsPackageFile = false; foreach (var jobMappings in installationQueues.unpackedJobMappings) { installsPackageFile |= jobMappings.Value.fileMapping.Keys.Any(x => x.EndsWith(".pcc", StringComparison.InvariantCultureIgnoreCase)); installsPackageFile |= jobMappings.Value.fileMapping.Keys.Any(x => x.EndsWith(".u", StringComparison.InvariantCultureIgnoreCase)); installsPackageFile |= jobMappings.Value.fileMapping.Keys.Any(x => x.EndsWith(".upk", StringComparison.InvariantCultureIgnoreCase)); installsPackageFile |= jobMappings.Value.fileMapping.Keys.Any(x => x.EndsWith(".sfm", StringComparison.InvariantCultureIgnoreCase)); } foreach (var jobMappings in installationQueues.sfarJobs) { installsPackageFile |= jobMappings.sfarInstallationMapping.Keys.Any(x => x.EndsWith(".pcc", StringComparison.InvariantCultureIgnoreCase)); installsPackageFile |= jobMappings.sfarInstallationMapping.Keys.Any(x => x.EndsWith(".u", StringComparison.InvariantCultureIgnoreCase)); installsPackageFile |= jobMappings.sfarInstallationMapping.Keys.Any(x => x.EndsWith(".upk", StringComparison.InvariantCultureIgnoreCase)); installsPackageFile |= jobMappings.sfarInstallationMapping.Keys.Any(x => x.EndsWith(".sfm", StringComparison.InvariantCultureIgnoreCase)); } if (installsPackageFile) { if (Settings.DeveloperMode) { Log.Warning("ALOT is installed and user is attemping to install a mod (in developer mode). Prompting user to cancel installation"); bool cancel = false; Application.Current.Dispatcher.Invoke(delegate { var res = Xceed.Wpf.Toolkit.MessageBox.Show(Window.GetWindow(this), $"ALOT is installed and this mod installs package files. Continuing to install this mod will likely cause broken textures to occur or game crashes due to invalid texture pointers and possibly empty mips. It will also put your ALOT installation into an unsupported configuration.\n\nContinue to install {ModBeingInstalled.ModName}? You have been warned.", $"Broken textures warning", MessageBoxButton.YesNo, MessageBoxImage.Error, MessageBoxResult.No); cancel = res == MessageBoxResult.No; }); if (cancel) { e.Result = ModInstallCompletedStatus.USER_CANCELED_INSTALLATION; return; } Log.Warning("User installing mod anyways even with ALOT installed"); } else { Log.Error("ALOT is installed. Installing mods that install package files after installing ALOT is not permitted."); //ALOT Installed, this is attempting to install a package file e.Result = ModInstallCompletedStatus.INSTALL_FAILED_ALOT_BLOCKING; return; } } } Action = $"Installing"; PercentVisibility = Visibility.Visible; Percent = 0; int numdone = 0; //Calculate number of installation tasks beforehand int numFilesToInstall = installationQueues.unpackedJobMappings.Select(x => x.Value.fileMapping.Count).Sum(); numFilesToInstall += installationQueues.sfarJobs.Select(x => x.sfarInstallationMapping.Count).Sum() * (ModBeingInstalled.IsInArchive ? 2 : 1); //*2 as we have to extract and install Debug.WriteLine("Number of expected installation tasks: " + numFilesToInstall); void FileInstalledCallback(string target) { numdone++; Debug.WriteLine("Installed: " + target); Action = "Installing"; var now = DateTime.Now; if (numdone > numFilesToInstall) { Debug.WriteLine($"Percentage calculated is wrong. Done: {numdone} NumToDoTotal: {numFilesToInstall}"); } if ((now - lastPercentUpdateTime).Milliseconds > PERCENT_REFRESH_COOLDOWN) { //Don't update UI too often. Once per second is enough. Percent = (int)(numdone * 100.0 / numFilesToInstall); lastPercentUpdateTime = now; } } //Stage: Unpacked files build map Dictionary <string, string> fullPathMappingDisk = new Dictionary <string, string>(); Dictionary <int, string> fullPathMappingArchive = new Dictionary <int, string>(); SortedSet <string> customDLCsBeingInstalled = new SortedSet <string>(); foreach (var unpackedQueue in installationQueues.unpackedJobMappings) { foreach (var originalMapping in unpackedQueue.Value.fileMapping) { //always unpacked //if (unpackedQueue.Key == ModJob.JobHeader.CUSTOMDLC || unpackedQueue.Key == ModJob.JobHeader.BALANCE_CHANGES || unpackedQueue.Key == ModJob.JobHeader.BASEGAME) //{ string sourceFile; if (unpackedQueue.Key.JobDirectory == null) { sourceFile = FilesystemInterposer.PathCombine(ModBeingInstalled.IsInArchive, ModBeingInstalled.ModPath, originalMapping.Value); } else { sourceFile = FilesystemInterposer.PathCombine(ModBeingInstalled.IsInArchive, ModBeingInstalled.ModPath, unpackedQueue.Key.JobDirectory, originalMapping.Value); } if (unpackedQueue.Key.Header == ModJob.JobHeader.ME1_CONFIG) { var destFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "BioWare", "Mass Effect", "Config", originalMapping.Key); if (ModBeingInstalled.IsInArchive) { int archiveIndex = ModBeingInstalled.Archive.ArchiveFileNames.IndexOf(sourceFile, StringComparer.InvariantCultureIgnoreCase); fullPathMappingArchive[archiveIndex] = destFile; //used for extraction indexing if (archiveIndex == -1) { Log.Error("Archive Index is -1 for file " + sourceFile + ". This will probably throw an exception!"); Debugger.Break(); } fullPathMappingDisk[sourceFile] = destFile; //used for redirection } else { fullPathMappingDisk[sourceFile] = destFile; } } else { var destFile = Path.Combine(unpackedQueue.Key.Header == ModJob.JobHeader.CUSTOMDLC ? MEDirectories.DLCPath(gameTarget) : gameTarget.TargetPath, originalMapping.Key); //official //Extract Custom DLC name if (unpackedQueue.Key.Header == ModJob.JobHeader.CUSTOMDLC) { var custDLC = destFile.Substring(gameDLCPath.Length, destFile.Length - gameDLCPath.Length).TrimStart('\\', '/'); var nextSlashIndex = custDLC.IndexOf('\\'); if (nextSlashIndex == -1) { nextSlashIndex = custDLC.IndexOf('/'); } if (nextSlashIndex != -1) { custDLC = custDLC.Substring(0, nextSlashIndex); customDLCsBeingInstalled.Add(custDLC); } } if (ModBeingInstalled.IsInArchive) { int archiveIndex = ModBeingInstalled.Archive.ArchiveFileNames.IndexOf(sourceFile, StringComparer.InvariantCultureIgnoreCase); fullPathMappingArchive[archiveIndex] = destFile; //used for extraction indexing if (archiveIndex == -1) { Log.Error("Archive Index is -1 for file " + sourceFile + ". This will probably throw an exception!"); Debugger.Break(); } fullPathMappingDisk[sourceFile] = destFile; //used for redirection } else { fullPathMappingDisk[sourceFile] = destFile; } } //} } } //Substage: Add SFAR staging targets string sfarStagingDirectory = (ModBeingInstalled.IsInArchive && installationQueues.sfarJobs.Count > 0) ? Directory.CreateDirectory(Path.Combine(Utilities.GetTempPath(), "SFARJobStaging")).FullName : null; //don't make directory if we don't need one if (sfarStagingDirectory != null) { foreach (var sfarJob in installationQueues.sfarJobs) { foreach (var fileToInstall in sfarJob.sfarInstallationMapping) { string sourceFile = FilesystemInterposer.PathCombine(ModBeingInstalled.IsInArchive, ModBeingInstalled.ModPath, sfarJob.job.JobDirectory, fileToInstall.Value); int archiveIndex = ModBeingInstalled.Archive.ArchiveFileNames.IndexOf(sourceFile, StringComparer.InvariantCultureIgnoreCase); if (archiveIndex == -1) { Log.Error("Archive Index is -1 for file " + sourceFile + ". This will probably throw an exception!"); Debugger.Break(); } string destFile = Path.Combine(sfarStagingDirectory, sfarJob.job.JobDirectory, fileToInstall.Value); fullPathMappingArchive[archiveIndex] = destFile; //used for extraction indexing fullPathMappingDisk[sourceFile] = destFile; //used for redirection Debug.WriteLine($"SFAR Disk Staging: {fileToInstall.Key} => {destFile}"); } } } foreach (var cdbi in customDLCsBeingInstalled) { var path = Path.Combine(gameDLCPath, cdbi); if (Directory.Exists(path)) { Log.Information("Deleting existing DLC directory: " + path); Utilities.DeleteFilesAndFoldersRecursively(path); } } //Stage: Unpacked files installation if (!ModBeingInstalled.IsInArchive) { //Direct copy Log.Information($"Installing {fullPathMappingDisk.Count} unpacked files into game directory"); CopyDir.CopyFiles_ProgressBar(fullPathMappingDisk, FileInstalledCallback); } else { Action = "Loading mod archive"; //Extraction to destination string installationRedirectCallback(ArchiveFileInfo info) { var inArchivePath = info.FileName; var redirectedPath = fullPathMappingDisk[inArchivePath]; Debug.WriteLine($"Redirecting {inArchivePath} to {redirectedPath}"); return(redirectedPath); } ModBeingInstalled.Archive.FileExtractionStarted += (sender, args) => { //CLog.Information("Extracting mod file for installation: " + args.FileInfo.FileName, Settings.LogModInstallation); }; List <string> filesInstalled = new List <string>(); List <string> filesToInstall = installationQueues.unpackedJobMappings.SelectMany(x => x.Value.fileMapping.Keys).ToList(); ModBeingInstalled.Archive.FileExtractionFinished += (sender, args) => { if (args.FileInfo.IsDirectory) { return; //ignore } if (!fullPathMappingArchive.ContainsKey(args.FileInfo.Index)) { return; //archive extracted this file (in memory) but did not do anything with this file (7z) } FileInstalledCallback(args.FileInfo.FileName); filesInstalled.Add(args.FileInfo.FileName); //Debug.WriteLine($"{args.FileInfo.FileName} as file { numdone}"); //Debug.WriteLine(numdone); }; ModBeingInstalled.Archive.ExtractFiles(gameTarget.TargetPath, installationRedirectCallback, fullPathMappingArchive.Keys.ToArray()); //directory parameter shouldn't be used here as we will be redirecting everything //filesInstalled.Sort(); //filesToInstall.Sort(); //Debug.WriteLine("Files installed:"); //foreach (var f in filesInstalled) //{ // Debug.WriteLine(f); //} //Debug.WriteLine("Files expected:"); //foreach (var f in filesToInstall) //{ // Debug.WriteLine(f); //} } //Write MetaCMM List <string> addedDLCFolders = new List <string>(); foreach (var v in installationQueues.unpackedJobMappings) { addedDLCFolders.AddRange(v.Value.dlcFoldersBeingInstalled); } foreach (var addedDLCFolder in addedDLCFolders) { var metacmm = Path.Combine(addedDLCFolder, "_metacmm.txt"); ModBeingInstalled.HumanReadableCustomDLCNames.TryGetValue(Path.GetFileName(addedDLCFolder), out var assignedDLCName); string contents = $"{assignedDLCName ?? ModBeingInstalled.ModName}\n{ModBeingInstalled.ModVersionString}\n{App.BuildNumber}\n{Guid.NewGuid().ToString()}"; File.WriteAllText(metacmm, contents); } //Stage: SFAR Installation foreach (var sfarJob in installationQueues.sfarJobs) { InstallIntoSFAR(sfarJob, ModBeingInstalled, FileInstalledCallback, ModBeingInstalled.IsInArchive ? sfarStagingDirectory : null); } //Main installation step has completed CLog.Information("Main stage of mod installation has completed", Settings.LogModInstallation); Percent = (int)(numdone * 100.0 / numFilesToInstall); //Remove outdated custom DLC foreach (var outdatedDLCFolder in ModBeingInstalled.OutdatedCustomDLC) { var outdatedDLCInGame = Path.Combine(gameDLCPath, outdatedDLCFolder); if (Directory.Exists(outdatedDLCInGame)) { Log.Information("Deleting outdated custom DLC folder: " + outdatedDLCInGame); Utilities.DeleteFilesAndFoldersRecursively(outdatedDLCInGame); } } //Install supporting ASI files if necessary //Todo: Upgrade to version detection code from ME3EXP to prevent conflicts Action = "Installing support files"; CLog.Information("Installing supporting ASI files", Settings.LogModInstallation); if (ModBeingInstalled.Game == Mod.MEGame.ME1) { Utilities.InstallEmbeddedASI("ME1-DLC-ModEnabler-v1.0", 1.0, gameTarget); } else if (ModBeingInstalled.Game == Mod.MEGame.ME2) { //None right now } else { //Todo: Port detection code from ME3Exp //Utilities.InstallEmbeddedASI("ME3Logger_truncating-v1.0", 1.0, gameTarget); if (ModBeingInstalled.GetJob(ModJob.JobHeader.BALANCE_CHANGES) != null) { Utilities.InstallEmbeddedASI("BalanceChangesReplacer-v2.0", 2.0, gameTarget); } } if (sfarStagingDirectory != null) { Utilities.DeleteFilesAndFoldersRecursively(Utilities.GetTempPath()); } if (numFilesToInstall == numdone) { e.Result = ModInstallCompletedStatus.INSTALL_SUCCESSFUL; Action = "Installed"; } else { Log.Warning($"Number of completed items does not equal the amount of items to install! Number installed {numdone} Number expected: {numFilesToInstall}"); e.Result = ModInstallCompletedStatus.INSTALL_WRONG_NUMBER_OF_COMPLETED_ITEMS; } }
public AlternateFile(string alternateFileText, ModJob associatedJob, Mod modForValidating) { var properties = StringStructParser.GetCommaSplitValues(alternateFileText); if (properties.TryGetValue(@"FriendlyName", out string friendlyName)) { FriendlyName = friendlyName; } if (modForValidating.ModDescTargetVersion >= 6 && string.IsNullOrWhiteSpace(FriendlyName)) { //Cannot be null. Log.Error(@"Alternate File does not specify FriendlyName. Mods targeting moddesc >= 6.0 cannot have empty FriendlyName"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_validation_altfile_oneAltDlcMissingFriendlyNameCmm6); return; } if (!Enum.TryParse(properties[@"Condition"], out Condition)) { Log.Error($@"Alternate File specifies unknown/unsupported condition: {properties[@"Condition"]}"); //do not localize ValidAlternate = false; LoadFailedReason = $@"{M3L.GetString(M3L.string_validation_altfile_unknownCondition)} {properties[@"Condition"]}"; return; } if (properties.TryGetValue(@"ConditionalDLC", out string conditionalDlc)) { var conditionalList = StringStructParser.GetSemicolonSplitList(conditionalDlc); foreach (var dlc in conditionalList) { //if (modForValidating.Game == Mod.MEGame.ME3) //{ if (Enum.TryParse(dlc, out ModJob.JobHeader header) && ModJob.GetHeadersToDLCNamesMap(modForValidating.Game).TryGetValue(header, out var foldername)) { ConditionalDLC.Add(foldername); continue; } //} if (!dlc.StartsWith(@"DLC_")) { Log.Error(@"An item in Alternate Files's ConditionalDLC doesn't start with DLC_"); LoadFailedReason = M3L.GetString(M3L.string_validation_altfile_conditionalDLCInvalidValue, FriendlyName); return; } else { ConditionalDLC.Add(dlc); } } } if (!Enum.TryParse(properties[@"ModOperation"], out Operation)) { Log.Error(@"Alternate File specifies unknown/unsupported operation: " + properties[@"ModOperation"]); ValidAlternate = false; LoadFailedReason = $@"{M3L.GetString(M3L.string_validation_altfile_unknownOperation)} { properties[@"ModOperation"]}"; return; } if (properties.TryGetValue(@"Description", out string description)) { Description = description; } if (modForValidating.ModDescTargetVersion >= 6 && string.IsNullOrWhiteSpace(Description)) { //Cannot be null. Log.Error($@"Alternate File {FriendlyName} with mod targeting moddesc >= 6.0 cannot have empty Description or missing description"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altfile_cmmver6RequiresDescription, FriendlyName); return; } if (Operation != AltFileOperation.OP_NOTHING) { if (properties.TryGetValue(@"ModFile", out string modfile)) { ModFile = modfile.TrimStart('\\', '/').Replace('/', '\\'); } else { Log.Error($@"Alternate file in-mod target (ModFile) required but not specified. This value is required for all Alternate files. Friendlyname: {FriendlyName}"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altfile_noModFileDeclared, FriendlyName); return; } if (associatedJob.Header == ModJob.JobHeader.CUSTOMDLC) { var modFilePath = FilesystemInterposer.PathCombine(modForValidating.IsInArchive, modForValidating.ModPath, ModFile); var pathSplit = ModFile.Split('\\'); if (pathSplit.Length > 0) { var dlcName = pathSplit[0]; var jobKey = associatedJob.CustomDLCFolderMapping.FirstOrDefault(x => x.Value.Equals(dlcName, StringComparison.InvariantCultureIgnoreCase)); if (jobKey.Key != null) { //if (associatedJob.CustomDLCFolderMapping.TryGetValue(ModFile, out var sourceFile)) //{ //} } else { Log.Error($@"Alternate file {FriendlyName} in-mod target (ModFile) does not appear to target a DLC target this mod will (always) install: {ModFile}"); ValidAlternate = false; LoadFailedReason = "Dummy placeholder"; //LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altfile_couldNotFindModFile, FriendlyName, ModFile); return; } } } else { if (!associatedJob.FilesToInstall.TryGetValue(ModFile, out var sourceFile)) { Log.Error($@"Alternate file {FriendlyName} in-mod target (ModFile) specified but does not exist in job: {ModFile}"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altfile_couldNotFindModFile, FriendlyName, ModFile); return; } } //todo: implement multimap if (properties.TryGetValue(@"MultiMappingFile", out string multifilemapping)) { MultiMappingFile = multifilemapping.TrimStart('\\', '/'); } if (properties.TryGetValue(@"AltFile", out string altfile)) { AltFile = altfile; } else if (AltFile == null && properties.TryGetValue(@"ModAltFile", out string maltfile)) { AltFile = maltfile; } properties.TryGetValue(@"SubstituteFile", out SubstituteFile); //Only used in 4.5. In 5.0 and above this became AltFile. //workaround for 4.5 if (modForValidating.ModDescTargetVersion == 4.5 && Operation == AltFileOperation.OP_SUBSTITUTE && SubstituteFile != null) { AltFile = SubstituteFile; } if (!string.IsNullOrEmpty(AltFile)) { AltFile = AltFile.Replace('/', '\\'); //Standardize paths } //This needs reworked from java's hack implementation //Need to identify mods using substitution features if (Operation == AltFileOperation.OP_INSTALL || Operation == AltFileOperation.OP_SUBSTITUTE) { if (MultiMappingFile == null) { //Validate file var altPath = FilesystemInterposer.PathCombine(modForValidating.IsInArchive, modForValidating.ModPath, AltFile); var altFileSourceExists = FilesystemInterposer.FileExists(altPath, modForValidating.Archive); if (!altFileSourceExists) { Log.Error(@"Alternate file source (AltFile) does not exist: " + AltFile); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altfile_specifiedAltFileDoesntExist, Operation.ToString(), AltFile); return; } //Ensure it is not part of DLC directory itself. var modFile = FilesystemInterposer.PathCombine(modForValidating.IsInArchive, modForValidating.ModPath, ModFile); //Todo } else { //Multimapping, Todo } } } ApplicableAutoText = properties.TryGetValue(@"ApplicableAutoText", out string applicableText) ? applicableText : M3L.GetString(M3L.string_autoApplied); NotApplicableAutoText = properties.TryGetValue(@"NotApplicableAutoText", out string notApplicableText) ? notApplicableText : M3L.GetString(M3L.string_notApplicable); if (modForValidating.ModDescTargetVersion >= 6.0) { GroupName = properties.TryGetValue(@"OptionGroup", out string groupName) ? groupName : null; } if (Condition == AltFileCondition.COND_MANUAL && properties.TryGetValue(@"CheckedByDefault", out string checkedByDefault) && bool.TryParse(checkedByDefault, out bool cbd)) { CheckedByDefault = cbd; } CLog.Information($@"Alternate file loaded and validated: {FriendlyName}", Settings.LogModStartup); ValidAlternate = true; }
/// <summary> /// Gets all files referenced by this mod. This does not include moddessc.ini by default /// </summary> /// <param name="includeModdesc">Include moddesc.ini in the results</param> /// <param name="archive">Archive, if this mod is in an archive.</param> /// <returns></returns> public List<string> GetAllRelativeReferences(bool includeModdesc = false, SevenZipExtractor archive = null) { var references = new List<string>(); //references.Add("moddesc.ini"); //Moddesc is implicitly referenced by the mod. //Replace or Add references foreach (var job in InstallationJobs) { foreach (var jobFile in job.FilesToInstall.Values) { if (job.JobDirectory == @"." || job.JobDirectory == null) { references.Add(jobFile); } else { references.Add(job.JobDirectory + @"\" + jobFile); } } foreach (var dlc in job.AlternateDLCs) { if (dlc.HasRelativeFiles()) { if (dlc.AlternateDLCFolder != null) { var files = FilesystemInterposer.DirectoryGetFiles(FilesystemInterposer.PathCombine(IsInArchive, ModPath, dlc.AlternateDLCFolder), "*", SearchOption.AllDirectories, archive).Select(x => (IsInArchive && ModPath.Length == 0) ? x : x.Substring(ModPath.Length + 1)).ToList(); references.AddRange(files); } else if (dlc.MultiListSourceFiles != null) { foreach (var mf in dlc.MultiListSourceFiles) { var relpath = Path.Combine(ModPath, dlc.MultiListRootPath, mf).Substring(ModPath.Length > 0 ? ModPath.Length + 1 : 0); references.Add(relpath); } } } // Add the referenced image asset if (dlc.ImageAssetName != null) { references.Add(FilesystemInterposer.PathCombine(IsInArchive, ModImageAssetsPath, dlc.ImageAssetName).Substring(ModPath.Length + (ModPath.Length > 1 ? 1 : 0))); } } foreach (var file in job.AlternateFiles) { if (file.HasRelativeFile()) { if (file.AltFile != null) { //Commented out: AltFile should be direct path to file from mod root, we should only put in relative path //if (IsInArchive) //{ // references.Add(FilesystemInterposer.PathCombine(true, ModPath, file.AltFile)); //} //else //{ references.Add(file.AltFile); //} } else if (file.MultiListSourceFiles != null) { foreach (var mf in file.MultiListSourceFiles) { var relPath = FilesystemInterposer.PathCombine(IsInArchive, ModPath, file.MultiListRootPath, mf); //Should this be different from above AltFile? if (IsInArchive) { references.Add(relPath.Substring(ModPath.Length + (ModPath.Length > 1 ? 1 : 0))); //substring so its relative to the path of the mod in the archive } else { references.Add(relPath.Substring(ModPath.Length + 1)); //chop off the root path of the moddesc.ini } } } } // Add the referenced image asset if (file.ImageAssetName != null) { references.Add(FilesystemInterposer.PathCombine(IsInArchive, ModImageAssetsPath, file.ImageAssetName).Substring(ModPath.Length + (ModPath.Length > 1 ? 1 : 0))); } } foreach (var customDLCmapping in job.CustomDLCFolderMapping) { references.AddRange(FilesystemInterposer.DirectoryGetFiles(FilesystemInterposer.PathCombine(IsInArchive, ModPath, customDLCmapping.Key), "*", SearchOption.AllDirectories, archive).Select(x => (IsInArchive && ModPath.Length == 0) ? x : x.Substring(ModPath.Length + 1)).ToList()); } } references.AddRange(AdditionalDeploymentFiles); foreach (var additionalDeploymentDir in AdditionalDeploymentFolders) { references.AddRange(FilesystemInterposer.DirectoryGetFiles(FilesystemInterposer.PathCombine(IsInArchive, ModPath, additionalDeploymentDir), "*", SearchOption.AllDirectories, archive).Select(x => (IsInArchive && ModPath.Length == 0) ? x : x.Substring(ModPath.Length + 1)).ToList()); } // Banner Image if (!string.IsNullOrWhiteSpace(BannerImageName)) { references.Add(FilesystemInterposer.PathCombine(IsInArchive, Mod.ModImageAssetFolderName, BannerImageName)); } if (includeModdesc && GetJob(ModJob.JobHeader.ME2_RCWMOD) == null) { references.Add(ModDescPath.Substring(ModPath.Length).TrimStart('/', '\\')); //references.Add(ModDescPath.TrimStart('/', '\\')); } return references.Distinct(StringComparer.InvariantCultureIgnoreCase).ToList(); }
/// <summary> /// Builds the installation queues for the mod. Item 1 is the unpacked job mappings (per modjob) along with the list of custom dlc folders being installed. Item 2 is the list of modjobs, their sfar paths, and the list of source files to install for SFAR jobs. /// </summary> /// <param name="gameTarget"></param> /// <returns></returns> public (Dictionary <ModJob, (Dictionary <string, InstallSourceFile> unpackedJobMapping, List <string> dlcFoldersBeingInstalled)>, List <(ModJob job, string sfarPath, Dictionary <string, InstallSourceFile>)>) GetInstallationQueues(GameTarget gameTarget) { if (IsInArchive) { #if DEBUG if (Archive.IsDisposed()) { Debug.WriteLine(@">>> ARCHIVE IS DISPOSED"); } #endif if (File.Exists(ArchivePath) && (Archive == null || Archive.IsDisposed())) { Archive = new SevenZipExtractor(ArchivePath); //load archive file for inspection } else if (Archive != null && Archive.GetBackingStream() is SevenZip.ArchiveEmulationStreamProxy aesp && aesp.Source is MemoryStream ms) { var isExe = ArchivePath.EndsWith(@".exe", StringComparison.InvariantCultureIgnoreCase); Archive = isExe ? new SevenZipExtractor(ms, InArchiveFormat.Nsis) : new SevenZipExtractor(ms); MemoryAnalyzer.AddTrackedMemoryItem($@"Re-opened SVE archive for {ModName}", new WeakReference(Archive)); } } var gameDLCPath = M3Directories.GetDLCPath(gameTarget); var customDLCMapping = Enumerable.FirstOrDefault <ModJob>(InstallationJobs, x => x.Header == ModJob.JobHeader.CUSTOMDLC)?.CustomDLCFolderMapping; if (customDLCMapping != null) { //Make clone so original value is not modified customDLCMapping = new Dictionary <string, string>(customDLCMapping); //prevent altering the source object } var unpackedJobInstallationMapping = new Dictionary <ModJob, (Dictionary <string, InstallSourceFile> mapping, List <string> dlcFoldersBeingInstalled)>(); var sfarInstallationJobs = new List <(ModJob job, string sfarPath, Dictionary <string, InstallSourceFile> installationMapping)>(); foreach (var job in InstallationJobs) { Log.Information($@"Preprocessing installation job: {job.Header}"); var alternateFiles = Enumerable.Where <AlternateFile>(job.AlternateFiles, x => x.IsSelected && x.Operation != AlternateFile.AltFileOperation.OP_NOTHING && x.Operation != AlternateFile.AltFileOperation.OP_NOINSTALL_MULTILISTFILES).ToList(); var alternateDLC = Enumerable.Where <AlternateDLC>(job.AlternateDLCs, x => x.IsSelected).ToList(); if (job.Header == ModJob.JobHeader.CUSTOMDLC) { #region Installation: CustomDLC //Key = destination file, value = source file to install var installationMapping = new Dictionary <string, InstallSourceFile>(); unpackedJobInstallationMapping[job] = (installationMapping, new List <string>()); foreach (var altdlc in alternateDLC) { if (altdlc.Operation == AlternateDLC.AltDLCOperation.OP_ADD_CUSTOMDLC) { customDLCMapping[altdlc.AlternateDLCFolder] = altdlc.DestinationDLCFolder; } } foreach (var mapping in customDLCMapping) { //Mapping is done as DESTINATIONFILE = SOURCEFILE so you can override keys var source = FilesystemInterposer.PathCombine(IsInArchive, ModPath, mapping.Key); var target = Path.Combine(gameDLCPath, mapping.Value); //get list of all normal files we will install var allSourceDirFiles = FilesystemInterposer.DirectoryGetFiles(source, "*", SearchOption.AllDirectories, Archive).Select(x => x.Substring(ModPath.Length).TrimStart('\\')).ToList(); unpackedJobInstallationMapping[job].dlcFoldersBeingInstalled.Add(target); //loop over every file foreach (var sourceFile in allSourceDirFiles) { //Check against alt files bool altApplied = false; foreach (var altFile in alternateFiles) { if (altFile.ModFile.Equals(sourceFile, StringComparison.InvariantCultureIgnoreCase)) { //Alt applies to this file switch (altFile.Operation) { case AlternateFile.AltFileOperation.OP_NOINSTALL: CLog.Information($@"Not installing {sourceFile} for Alternate File {altFile.FriendlyName} due to operation OP_NOINSTALL", Settings.LogModInstallation); //we simply don't map as we just do a continue below. altApplied = true; break; case AlternateFile.AltFileOperation.OP_SUBSTITUTE: CLog.Information($@"Repointing {sourceFile} to {altFile.AltFile} for Alternate File {altFile.FriendlyName} due to operation OP_SUBSTITUTE", Settings.LogModInstallation); if (job.JobDirectory != null && altFile.AltFile.StartsWith((string)job.JobDirectory)) { installationMapping[sourceFile] = new InstallSourceFile(altFile.AltFile.Substring(job.JobDirectory.Length).TrimStart('/', '\\')) { AltApplied = true, IsFullRelativeFilePath = true }; //use alternate file as key instead } else { installationMapping[sourceFile] = new InstallSourceFile(altFile.AltFile) { AltApplied = true, IsFullRelativeFilePath = true }; //use alternate file as key instead } altApplied = true; break; case AlternateFile.AltFileOperation.OP_INSTALL: //same logic as substitute, just different logging. CLog.Information($@"Adding {sourceFile} to install (from {altFile.AltFile}) as part of Alternate File {altFile.FriendlyName} due to operation OP_INSTALL", Settings.LogModInstallation); if (job.JobDirectory != null && altFile.AltFile.StartsWith((string)job.JobDirectory)) { installationMapping[sourceFile] = new InstallSourceFile(altFile.AltFile.Substring(job.JobDirectory.Length).TrimStart('/', '\\')) { AltApplied = true, IsFullRelativeFilePath = true }; //use alternate file as key instead } else { installationMapping[sourceFile] = new InstallSourceFile(altFile.AltFile) { AltApplied = true, IsFullRelativeFilePath = true }; //use alternate file as key instead } altApplied = true; break; } break; } } if (altApplied) { continue; //no further processing for file } var relativeDestStartIndex = sourceFile.IndexOf(mapping.Value); string destPath = sourceFile.Substring(relativeDestStartIndex); installationMapping[destPath] = new InstallSourceFile(sourceFile); //destination is mapped to source file that will replace it. } foreach (var altdlc in alternateDLC) { if (altdlc.Operation == AlternateDLC.AltDLCOperation.OP_ADD_FOLDERFILES_TO_CUSTOMDLC) { string alternatePathRoot = FilesystemInterposer.PathCombine(IsInArchive, ModPath, altdlc.AlternateDLCFolder); var filesToAdd = FilesystemInterposer.DirectoryGetFiles(alternatePathRoot, "*", SearchOption.AllDirectories, Archive).Select(x => x.Substring(ModPath.Length).TrimStart('\\')).ToList(); foreach (var fileToAdd in filesToAdd) { var destFile = Path.Combine(altdlc.DestinationDLCFolder, fileToAdd.Substring(altdlc.AlternateDLCFolder.Length).TrimStart('\\', '/')); CLog.Information($@"Adding extra CustomDLC file ({fileToAdd} => {destFile}) due to Alternate DLC {altdlc.FriendlyName}'s {altdlc.Operation}", Settings.LogModInstallation); installationMapping[destFile] = new InstallSourceFile(fileToAdd) { AltApplied = true }; } } else if (altdlc.Operation == AlternateDLC.AltDLCOperation.OP_ADD_MULTILISTFILES_TO_CUSTOMDLC) { string alternatePathRoot = FilesystemInterposer.PathCombine(IsInArchive, ModPath, altdlc.MultiListRootPath); foreach (var fileToAdd in altdlc.MultiListSourceFiles) { var sourceFile = FilesystemInterposer.PathCombine(IsInArchive, alternatePathRoot, fileToAdd).Substring(ModPath.Length).TrimStart('\\'); var destFile = Path.Combine(altdlc.DestinationDLCFolder, fileToAdd.TrimStart('\\', '/')); CLog.Information($@"Adding extra CustomDLC file (MultiList) ({sourceFile} => {destFile}) due to Alternate DLC {altdlc.FriendlyName}'s {altdlc.Operation}", Settings.LogModInstallation); installationMapping[destFile] = new InstallSourceFile(sourceFile) { AltApplied = true }; } } } // Process altfile removal of multilist, since it should be done last var fileRemoveAltFiles = Enumerable.Where <AlternateFile>(job.AlternateFiles, x => x.IsSelected && x.Operation == AlternateFile.AltFileOperation.OP_NOINSTALL_MULTILISTFILES); foreach (var altFile in fileRemoveAltFiles) { foreach (var multifile in altFile.MultiListSourceFiles) { CLog.Information($@"Attempting to remove multilist file {multifile} from install (from {altFile.MultiListTargetPath}) as part of Alternate File {altFile.FriendlyName} due to operation OP_NOINSTALL_MULTILISTFILES", Settings.LogModInstallation); string relativeSourcePath = altFile.MultiListRootPath + '\\' + multifile; var targetPath = altFile.MultiListTargetPath + '\\' + multifile; if (installationMapping.Remove(targetPath)) { CLog.Information($@" > Removed multilist file {targetPath} from installation", Settings.LogModInstallation); } } } } #endregion } else if (job.Header == ModJob.JobHeader.LOCALIZATION) { #region Installation: LOCALIZATION var installationMapping = new CaseInsensitiveDictionary <InstallSourceFile>(); unpackedJobInstallationMapping[job] = (installationMapping, new List <string>()); buildInstallationQueue(job, installationMapping, false); #endregion } else if (job.Header == ModJob.JobHeader.BASEGAME || job.Header == ModJob.JobHeader.BALANCE_CHANGES || job.Header == ModJob.JobHeader.ME1_CONFIG) { #region Installation: BASEGAME, BALANCE CHANGES, ME1 CONFIG var installationMapping = new CaseInsensitiveDictionary <InstallSourceFile>(); unpackedJobInstallationMapping[job] = (installationMapping, new List <string>()); buildInstallationQueue(job, installationMapping, false); #endregion } else if (Game == MEGame.ME3 && ModJob.ME3SupportedNonCustomDLCJobHeaders.Contains(job.Header)) //previous else if will catch BASEGAME { #region Installation: DLC Unpacked and SFAR (ME3 ONLY) if (M3Directories.IsOfficialDLCInstalled(job.Header, gameTarget)) { string sfarPath = job.Header == ModJob.JobHeader.TESTPATCH ? M3Directories.GetTestPatchSFARPath(gameTarget) : Path.Combine(gameDLCPath, ModJob.GetHeadersToDLCNamesMap(MEGame.ME3)[job.Header], @"CookedPCConsole", @"Default.sfar"); if (File.Exists(sfarPath)) { var installationMapping = new CaseInsensitiveDictionary <InstallSourceFile>(); if (new FileInfo(sfarPath).Length == 32) { //Unpacked unpackedJobInstallationMapping[job] = (installationMapping, new List <string>()); buildInstallationQueue(job, installationMapping, false); } else { //Packed //unpackedJobInstallationMapping[job] = installationMapping; buildInstallationQueue(job, installationMapping, true); sfarInstallationJobs.Add((job, sfarPath, installationMapping)); } } } else { Log.Warning($@"DLC not installed, skipping: {job.Header}"); } #endregion } else if (Game == MEGame.ME2 || Game == MEGame.ME1) { #region Installation: DLC Unpacked (ME1/ME2 ONLY) //Unpacked if (M3Directories.IsOfficialDLCInstalled(job.Header, gameTarget)) { var installationMapping = new CaseInsensitiveDictionary <InstallSourceFile>(); unpackedJobInstallationMapping[job] = (installationMapping, new List <string>()); buildInstallationQueue(job, installationMapping, false); } else { Log.Warning($@"DLC not installed, skipping: {job.Header}"); } #endregion } else { //?? Header throw new Exception(@"Unsupported installation job header! " + job.Header); } } return(unpackedJobInstallationMapping, sfarInstallationJobs); }
public AlternateDLC(string alternateDLCText, Mod modForValidating) { var properties = StringStructParser.GetCommaSplitValues(alternateDLCText); //todo: if statements to check these. if (properties.TryGetValue(@"FriendlyName", out string friendlyName)) { FriendlyName = friendlyName; } if (modForValidating.ModDescTargetVersion >= 6 && string.IsNullOrWhiteSpace(FriendlyName)) { //Cannot be null. Log.Error(@"Alternate DLC does not specify FriendlyName. Mods targeting moddesc >= 6.0 require FriendlyName"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_validation_altdlc_oneAltDlcMissingFriendlyNameCmm6); return; } if (!Enum.TryParse(properties[@"Condition"], out Condition)) { Log.Error($@"Alternate DLC specifies unknown/unsupported condition: {properties[@"Condition"]}"); //do not localize ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_validation_altdlc_unknownCondition) + properties[@"Condition"]; return; } if (!Enum.TryParse(properties[@"ModOperation"], out Operation)) { Log.Error($@"Alternate DLC specifies unknown/unsupported operation: {properties[@"ModOperation"]}"); //do not localize ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_validation_altdlc_unknownOperation) + properties[@"ModOperation"]; return; } if (properties.TryGetValue(@"Description", out string description)) { Description = description; } if (modForValidating.ModDescTargetVersion >= 6 && string.IsNullOrWhiteSpace(Description)) { //Cannot be null. Log.Error($@"Alternate DLC {FriendlyName} cannot have empty Description or missing description as it targets cmmver >= 6"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_cmmver6RequiresDescription, FriendlyName); return; } if (Operation != AltDLCOperation.OP_NOTHING) { if (properties.TryGetValue(@"ModAltDLC", out string altDLCFolder)) { AlternateDLCFolder = altDLCFolder.Replace('/', '\\'); } else { Log.Error(@"Alternate DLC does not specify ModAltDLC but is required"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_missingModAltDLC, FriendlyName); return; } if (properties.TryGetValue(@"ModDestDLC", out string destDLCFolder)) { DestinationDLCFolder = destDLCFolder.Replace('/', '\\'); } else { Log.Error(@"Alternate DLC does not specify ModDestDLC but is required"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_missingModDestDLC, FriendlyName); return; } //todo: Validate target in mod folder if (properties.TryGetValue(@"ConditionalDLC", out string conditionalDlc)) { var conditionalList = StringStructParser.GetSemicolonSplitList(conditionalDlc); foreach (var dlc in conditionalList) { //if (modForValidating.Game == Mod.MEGame.ME3) //{ if (Enum.TryParse(dlc, out ModJob.JobHeader header) && ModJob.GetHeadersToDLCNamesMap(modForValidating.Game).TryGetValue(header, out var foldername)) { ConditionalDLC.Add(foldername); continue; } //} if (!dlc.StartsWith(@"DLC_")) { Log.Error($@"An item in Alternate DLC's ({FriendlyName}) ConditionalDLC doesn't start with DLC_ or is not official header"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_conditionalDLCInvalidValue, FriendlyName); return; } else { ConditionalDLC.Add(dlc); } } } //Validation if (string.IsNullOrWhiteSpace(AlternateDLCFolder)) { Log.Error($@"Alternate DLC directory (ModAltDLC) not specified for { FriendlyName}"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_sourceDirectoryNotSpecifiedForModAltDLC, FriendlyName); return; } if (string.IsNullOrWhiteSpace(DestinationDLCFolder)) { Log.Error($@"Destination DLC directory (ModDestDLC) not specified for {FriendlyName}"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_destinationDirectoryNotSpecifiedForModDestDLC, FriendlyName); return; } AlternateDLCFolder = AlternateDLCFolder.TrimStart('\\', '/').Replace('/', '\\'); //Check ModAltDLC directory exists var localAltDlcDir = FilesystemInterposer.PathCombine(modForValidating.IsInArchive, modForValidating.ModPath, AlternateDLCFolder); if (!FilesystemInterposer.DirectoryExists(localAltDlcDir, modForValidating.Archive)) { Log.Error($@"Alternate DLC directory (ModAltDLC) does not exist: {AlternateDLCFolder}"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_sourceDirectoryDoesntExist, FriendlyName, AlternateDLCFolder); return; } } ApplicableAutoText = properties.TryGetValue(@"ApplicableAutoText", out string applicableText) ? applicableText : M3L.GetString(M3L.string_autoApplied); NotApplicableAutoText = properties.TryGetValue(@"NotApplicableAutoText", out string notApplicableText) ? notApplicableText : M3L.GetString(M3L.string_notApplicable); if (modForValidating.ModDescTargetVersion >= 6.0) { GroupName = properties.TryGetValue(@"OptionGroup", out string groupName) ? groupName : null; } if (Condition == AltDLCCondition.COND_MANUAL && properties.TryGetValue(@"CheckedByDefault", out string checkedByDefault) && bool.TryParse(checkedByDefault, out bool cbd)) { CheckedByDefault = cbd; } CLog.Information($@"AlternateDLC loaded and validated: {FriendlyName}", Settings.LogModStartup); ValidAlternate = true; }
public void TestBuildingExtractionQueues() { GlobalTest.Init(); Console.WriteLine("Fetching third party services"); App.ThirdPartyImportingService = OnlineContent.FetchThirdPartyImportingService(); App.ThirdPartyIdentificationService = OnlineContent.FetchThirdPartyIdentificationManifest(); var compressedModsDirectory = Path.Combine(GlobalTest.GetTestDataDirectory(), "compressedmods"); List <Mod> modsFoundInArchive = new List <Mod>(); void addModCallback(Mod m) { Console.WriteLine($"Found mod in archive: {m.ModName}"); modsFoundInArchive.Add(m); } void failedModCallback(Mod m) { //Console.WriteLine($"A mod failed to load. This may be expected: {m.ModName}"); } void logMessageCallback(string m) { Console.WriteLine(m); } #region Get Targets List <GameTarget> targets = new List <GameTarget>(); var root = GlobalTest.GetTestGameFoldersDirectory(Mod.MEGame.ME1); foreach (var d in Directory.GetDirectories(root)) { GameTarget gt = new GameTarget(Mod.MEGame.ME1, d, false, false); gt.ValidateTarget(); if (gt.IsValid) { targets.Add(gt); } } root = GlobalTest.GetTestGameFoldersDirectory(Mod.MEGame.ME2); foreach (var d in Directory.GetDirectories(root)) { GameTarget gt = new GameTarget(Mod.MEGame.ME2, d, false, false); gt.ValidateTarget(); if (gt.IsValid) { targets.Add(gt); } } root = GlobalTest.GetTestGameFoldersDirectory(Mod.MEGame.ME3); foreach (var d in Directory.GetDirectories(root)) { GameTarget gt = new GameTarget(Mod.MEGame.ME3, d, false, false); gt.ValidateTarget(); if (gt.IsValid) { targets.Add(gt); } } #endregion //Compressed Mods foreach (var archive in Directory.GetFiles(compressedModsDirectory)) { modsFoundInArchive.Clear(); var realArchiveInfo = GlobalTest.ParseRealArchiveAttributes(archive); Console.WriteLine($@"Inspecting archive: { archive}"); ModArchiveImporter.InspectArchive(archive, addModCallback, failedModCallback, logMessageCallback, forcedMD5: realArchiveInfo.md5, forcedSize: realArchiveInfo.size); var archiveZ = new SevenZipExtractor(archive); foreach (var mod in modsFoundInArchive) { bool altsOn = false; while (true) { if (altsOn) { foreach (var job in mod.InstallationJobs) { List <string> selectedGroups = new List <string>(); foreach (var altfile in job.AlternateFiles) { if (altfile.GroupName != null) { if (selectedGroups.Contains(altfile.GroupName)) { continue; //we already did first time of this. I know that's a weak test case... } selectedGroups.Add(altfile.GroupName); } altfile.IsSelected = true; } } } var refs = mod.GetAllRelativeReferences(!mod.IsVirtualized, archiveZ); //test //validate references are actually in this archive foreach (var fileREf in refs) { var expectedPath = FilesystemInterposer.PathCombine(mod.IsInArchive, mod.ModPath, fileREf); //var expectedPath = fileREf; var inArchiveFile = archiveZ.ArchiveFileData.FirstOrDefault(x => x.FileName == expectedPath); Assert.IsNotNull(inArchiveFile.FileName, "Relative referenced file was not found in archive: " + fileREf); } //size test Assert.AreNotEqual(0, mod.SizeRequiredtoExtract, "Archive extraction size is zero! For file " + archive); var targetsForMod = targets.Where(x => x.Game == mod.Game).ToList(); foreach (var target in targetsForMod) { var queue = mod.GetInstallationQueues(target); foreach (var jobMapping in queue.Item1) { foreach (var unpackedItem in jobMapping.Value.unpackedJobMapping) { string sourceFile; if (jobMapping.Key.JobDirectory == null || unpackedItem.Value.IsFullRelativeFilePath) { sourceFile = FilesystemInterposer.PathCombine(mod.IsInArchive, mod.ModPath, unpackedItem.Value.FilePath); } else { sourceFile = FilesystemInterposer.PathCombine(mod.IsInArchive, mod.ModPath, jobMapping.Key.JobDirectory, unpackedItem.Value.FilePath); } Assert.IsTrue(archiveZ.ArchiveFileNames.Contains(sourceFile), "Archive should contain a file specified by mod (mod is valid) but does not appear to. File: " + sourceFile); } } } if (!altsOn) { altsOn = true; } else { break; } } } } //EXE mods var exeModsDirectory = Path.Combine(GlobalTest.GetTestDataDirectory(), "exemods"); if (Directory.Exists(exeModsDirectory)) { foreach (var exe in Directory.GetFiles(exeModsDirectory)) { modsFoundInArchive.Clear(); //var realArchiveInfo = GlobalTest.ParseRealArchiveAttributes(exe); Console.WriteLine($@"Inspecting exe: { exe}"); ModArchiveImporter.InspectArchive(exe, addModCallback, failedModCallback, logMessageCallback); var archiveZ = new SevenZipExtractor(exe, InArchiveFormat.Nsis); foreach (var mod in modsFoundInArchive) { foreach (var job in mod.InstallationJobs) { List <string> selectedGroups = new List <string>(); foreach (var altfile in job.AlternateFiles) { if (altfile.GroupName != null) { if (selectedGroups.Contains(altfile.GroupName)) { continue; //we already did first time of this. I know that's a weak test case... } selectedGroups.Add(altfile.GroupName); } altfile.IsSelected = true; } } var refs = mod.GetAllRelativeReferences(false, archiveZ); //test and get refs. exe mods will always be virtualized as they won't have a moddesc.ini file. //exe mods remap to subconetns //same code as Mod-Extraction.cs foreach (var fileREf in refs) { var expectedPath = FilesystemInterposer.PathCombine(mod.IsInArchive, mod.ModPath, fileREf); //var expectedPath = fileREf; var inArchiveFile = archiveZ.ArchiveFileData.FirstOrDefault(x => x.FileName == expectedPath); Assert.IsNotNull(inArchiveFile.FileName, "Relative referenced file was not found in archive: " + fileREf); } mod.ExtractFromArchive(exe, "", false, testRun: true); } } } else { Console.WriteLine("No exemods directory found. This section of testing will be skipped"); } }
public AlternateDLC(string alternateDLCText, Mod modForValidating) { var properties = StringStructParser.GetCommaSplitValues(alternateDLCText); //todo: if statements to check these. if (properties.TryGetValue("FriendlyName", out string friendlyName)) { FriendlyName = friendlyName; } if (modForValidating.ModDescTargetVersion >= 6 && string.IsNullOrWhiteSpace(FriendlyName)) { //Cannot be null. Log.Error($"Alternate DLC does not specify FriendlyName. Mods targeting moddesc >= 6.0 require FriendlyName"); ValidAlternate = false; LoadFailedReason = $"At least one specified Alternate DLC does not specify a FriendlyName, which is required for mods targeting cmmver >= 6.0."; return; } if (!Enum.TryParse(properties["Condition"], out Condition)) { Log.Error("Alternate DLC specifies unknown/unsupported condition: " + properties["Condition"]); ValidAlternate = false; LoadFailedReason = "Alternate DLC specifies unknown/unsupported condition: " + properties["Condition"]; return; } if (!Enum.TryParse(properties["ModOperation"], out Operation)) { Log.Error("Alternate DLC specifies unknown/unsupported operation: " + properties["ModOperation"]); ValidAlternate = false; LoadFailedReason = "Alternate DLC specifies unknown/unsupported operation: " + properties["ModOperation"]; return; } if (properties.TryGetValue("Description", out string description)) { Description = description; } if (modForValidating.ModDescTargetVersion >= 6 && string.IsNullOrWhiteSpace(Description)) { //Cannot be null. Log.Error($"Alternate DLC {FriendlyName} cannot have empty Description or missing description as it targets cmmver >= 6"); ValidAlternate = false; LoadFailedReason = $"Alternate DLC {FriendlyName} does not specify a Description, which is required for mods targeting cmmver >= 6.0."; return; } if (properties.TryGetValue("ModAltDLC", out string altDLCFolder)) { AlternateDLCFolder = altDLCFolder.Replace('/', '\\'); } else { Log.Error("Alternate DLC does not specify ModAltDLC but is required"); ValidAlternate = false; LoadFailedReason = $"Alternate DLC {FriendlyName} does not declare ModAltDLC but it is required for all Alternate DLC."; return; } if (properties.TryGetValue("ModDestDLC", out string destDLCFolder)) { DestinationDLCFolder = destDLCFolder.Replace('/', '\\'); } else { Log.Error("Alternate DLC does not specify ModDestDLC but is required"); ValidAlternate = false; LoadFailedReason = $"Alternate DLC {FriendlyName} does not declare ModDestDLC but it is required for all Alternate DLC."; return; } //todo: Validate target in mod folder if (properties.TryGetValue("ConditionalDLC", out string conditionalDlc)) { var conditionalList = StringStructParser.GetSemicolonSplitList(conditionalDlc); foreach (var dlc in conditionalList) { //if (modForValidating.Game == Mod.MEGame.ME3) //{ if (Enum.TryParse(dlc, out ModJob.JobHeader header) && ModJob.GetHeadersToDLCNamesMap(modForValidating.Game).TryGetValue(header, out var foldername)) { ConditionalDLC.Add(foldername); continue; } //} if (!dlc.StartsWith("DLC_")) { Log.Error("An item in Alternate DLC's ConditionalDLC doesn't start with DLC_"); LoadFailedReason = $"Alternate DLC ({FriendlyName}) specifies conditional DLC but no values match the allowed headers or start with DLC_."; return; } else { ConditionalDLC.Add(dlc); } } } if (properties.TryGetValue("ApplicableAutoText", out string applicableText)) { ApplicableAutoText = applicableText; } else { ApplicableAutoText = "Auto Applied"; } if (properties.TryGetValue("NotApplicableAutoText", out string notApplicableText)) { NotApplicableAutoText = notApplicableText; } else { NotApplicableAutoText = "Not applicable"; } if (Condition == AltDLCCondition.COND_MANUAL && properties.TryGetValue("CheckedByDefault", out string checkedByDefault) && bool.TryParse(checkedByDefault, out bool cbd)) { CheckedByDefault = cbd; } //Validation if (string.IsNullOrWhiteSpace(AlternateDLCFolder)) { Log.Error("Alternate DLC directory (ModAltDLC) not specified"); LoadFailedReason = $"Alternate DLC for AltDLC ({FriendlyName}) is specified, but source directory (ModAltDLC) was not specified."; return; } if (string.IsNullOrWhiteSpace(DestinationDLCFolder)) { Log.Error("Destination DLC directory (ModDestDLC) not specified"); LoadFailedReason = $"Destination DLC for AltDLC ({FriendlyName}) is specified, but source directory (ModDestDLC) was not specified."; return; } AlternateDLCFolder = AlternateDLCFolder.TrimStart('\\', '/').Replace('/', '\\'); //Check ModAltDLC directory exists var localAltDlcDir = FilesystemInterposer.PathCombine(modForValidating.IsInArchive, modForValidating.ModPath, AlternateDLCFolder); if (!FilesystemInterposer.DirectoryExists(localAltDlcDir, modForValidating.Archive)) { Log.Error("Alternate DLC directory (ModAltDLC) does not exist: " + AlternateDLCFolder); LoadFailedReason = $"Alternate DLC ({FriendlyName}) is specified, but source for alternate DLC directory does not exist: {AlternateDLCFolder}"; return; } CLog.Information($"AlternateDLC loaded and validated: {FriendlyName}", Settings.LogModStartup); ValidAlternate = true; }