Ejemplo n.º 1
0
        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 _);
Ejemplo n.º 2
0
        /// <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);
        }
Ejemplo n.º 3
0
        private static Mod AttemptLoadVirtualMod(ArchiveFileInfo sfarEntry, SevenZipExtractor archive, Mod.MEGame game, string md5)
        {
            var sfarPath   = sfarEntry.FileName;
            var cookedPath = FilesystemInterposer.DirectoryGetParent(sfarPath, true);

            //Todo: Check if value is CookedPC/CookedPCConsole as further validation
            if (!string.IsNullOrEmpty(FilesystemInterposer.DirectoryGetParent(cookedPath, true)))
            {
                var dlcDir        = FilesystemInterposer.DirectoryGetParent(cookedPath, true);
                var dlcFolderName = Path.GetFileName(dlcDir);
                if (!string.IsNullOrEmpty(dlcFolderName))
                {
                    var thirdPartyInfo = ThirdPartyServices.GetThirdPartyModInfo(dlcFolderName, game);
                    if (thirdPartyInfo != null)
                    {
                        Log.Information($@"Third party mod found: {thirdPartyInfo.modname}, preparing virtual moddesc.ini");
                        //We will have to load a virtual moddesc. Since Mod constructor requires reading an ini, we will build and feed it a virtual one.
                        IniData virtualModDesc = new IniData();
                        virtualModDesc[@"ModManager"][@"cmmver"]     = App.HighestSupportedModDesc.ToString();
                        virtualModDesc[@"ModManager"][@"importedby"] = App.BuildNumber.ToString();
                        virtualModDesc[@"ModInfo"][@"game"]          = @"ME3";
                        virtualModDesc[@"ModInfo"][@"modname"]       = thirdPartyInfo.modname;
                        virtualModDesc[@"ModInfo"][@"moddev"]        = thirdPartyInfo.moddev;
                        virtualModDesc[@"ModInfo"][@"modsite"]       = thirdPartyInfo.modsite;
                        virtualModDesc[@"ModInfo"][@"moddesc"]       = thirdPartyInfo.moddesc;
                        virtualModDesc[@"ModInfo"][@"unofficial"]    = @"true";
                        if (int.TryParse(thirdPartyInfo.updatecode, out var updatecode) && updatecode > 0)
                        {
                            virtualModDesc[@"ModInfo"][@"updatecode"] = updatecode.ToString();
                            virtualModDesc[@"ModInfo"][@"modver"]     = 0.001.ToString(CultureInfo.InvariantCulture); //This will force mod to check for update after reload
                        }
                        else
                        {
                            virtualModDesc[@"ModInfo"][@"modver"] = 0.0.ToString(CultureInfo.InvariantCulture); //Will attempt to look up later after mods have parsed.
                        }

                        virtualModDesc[@"CUSTOMDLC"][@"sourcedirs"]        = dlcFolderName;
                        virtualModDesc[@"CUSTOMDLC"][@"destdirs"]          = dlcFolderName;
                        virtualModDesc[@"UPDATES"][@"originalarchivehash"] = md5;

                        var archiveSize    = new FileInfo(archive.FileName).Length;
                        var importingInfos = ThirdPartyServices.GetImportingInfosBySize(archiveSize);
                        if (importingInfos.Count == 1 && importingInfos[0].GetParsedRequiredDLC().Count > 0)
                        {
                            OnlineContent.QueryModRelay(importingInfos[0].md5, archiveSize); //Tell telemetry relay we are accessing the TPIS for an existing item so it can update latest for tracking
                            virtualModDesc[@"ModInfo"][@"requireddlc"] = importingInfos[0].requireddlc;
                        }

                        return(new Mod(virtualModDesc.ToString(), FilesystemInterposer.DirectoryGetParent(dlcDir, true), archive));
                    }
                }
                else
                {
                    Log.Information($@"No third party mod information for importing {dlcFolderName}. Should this be supported for import? Contact Mgamerz on the ME3Tweaks Discord if it should.");
                }
            }

            return(null);
        }
Ejemplo n.º 4
0
        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)
        }
Ejemplo n.º 5
0
        /// <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);
        }
Ejemplo n.º 6
0
        /// <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);
        }
Ejemplo n.º 7
0
        /// <summary>
        /// Extracts the mod from the archive. The caller should handle exception that may be thrown.
        /// </summary>
        /// <param name="archivePath"></param>
        /// <param name="outputFolderPath"></param>
        /// <param name="compressPackages"></param>
        /// <param name="updateTextCallback"></param>
        /// <param name="extractingCallback"></param>
        /// <param name="compressedPackageCallback"></param>
        /// <param name="testRun"></param>
        public void ExtractFromArchive(string archivePath, string outputFolderPath, bool compressPackages,
                                       Action <string> updateTextCallback = null, Action <DetailedProgressEventArgs> extractingCallback = null, Action <string, int, int> compressedPackageCallback = null,
                                       bool testRun = false, Stream archiveStream = null)
        {
            if (!IsInArchive)
            {
                throw new Exception(@"Cannot extract a mod that is not part of an archive.");
            }
            if (archiveStream == null && !File.Exists(archivePath))
            {
                throw new Exception(M3L.GetString(M3L.string_interp_theArchiveFileArchivePathIsNoLongerAvailable, archivePath));
            }

            compressPackages &= Game >= MEGame.ME2;

            SevenZipExtractor archive;
            var isExe = archivePath.EndsWith(@".exe", StringComparison.InvariantCultureIgnoreCase);

            bool closeStreamOnFinish = true;

            if (archiveStream != null)
            {
                archive             = isExe ? new SevenZipExtractor(archiveStream, InArchiveFormat.Nsis) : new SevenZipExtractor(archiveStream);
                closeStreamOnFinish = false;
            }
            else
            {
                archive = isExe ? new SevenZipExtractor(archivePath, InArchiveFormat.Nsis) : new SevenZipExtractor(archivePath);
            }

            var fileIndicesToExtract       = new List <int>();
            var filePathsToExtractTESTONLY = new List <string>();
            var referencedFiles            = GetAllRelativeReferences(!IsVirtualized, archive);

            if (isExe)
            {
                //remap to mod root. Not entirely sure if this needs to be done for sub mods?
                referencedFiles = referencedFiles.Select(x => FilesystemInterposer.PathCombine(IsInArchive, ModPath, x)).ToList(); //remap to in-archive paths so they match entry paths
            }
            foreach (var info in archive.ArchiveFileData)
            {
                if (!info.IsDirectory && (ModPath == "" || info.FileName.Contains((string)ModPath)))
                {
                    var relativedName = isExe ? info.FileName : info.FileName.Substring(ModPath.Length).TrimStart('\\');
                    if (referencedFiles.Contains(relativedName))
                    {
                        Log.Information(@"Adding file to extraction list: " + info.FileName);
                        fileIndicesToExtract.Add(info.Index);
                        filePathsToExtractTESTONLY.Add(relativedName);
                    }
                }
            }

            void archiveExtractionProgress(object?sender, DetailedProgressEventArgs args)
            {
                extractingCallback?.Invoke(args);
            }

            archive.Progressing += archiveExtractionProgress;
            string outputFilePathMapping(ArchiveFileInfo entryInfo)
            {
                Log.Information(@"Mapping extraction target for " + entryInfo.FileName);

                string entryPath = entryInfo.FileName;

                if (ExeExtractionTransform != null && ExeExtractionTransform.PatchRedirects.Any(x => x.index == entryInfo.Index))
                {
                    Log.Information(@"Extracting vpatch file at index " + entryInfo.Index);
                    return(Path.Combine(Utilities.GetVPatchRedirectsFolder(), ExeExtractionTransform.PatchRedirects.First(x => x.index == entryInfo.Index).outfile));
                }

                if (ExeExtractionTransform != null && ExeExtractionTransform.NoExtractIndexes.Any(x => x == entryInfo.Index))
                {
                    Log.Information(@"Extracting file to trash (not used): " + entryPath);
                    return(Path.Combine(Utilities.GetTempPath(), @"Trash", @"trashfile"));
                }

                if (ExeExtractionTransform != null && ExeExtractionTransform.AlternateRedirects.Any(x => x.index == entryInfo.Index))
                {
                    var outfile = ExeExtractionTransform.AlternateRedirects.First(x => x.index == entryInfo.Index).outfile;
                    Log.Information($@"Extracting file with redirection: {entryPath} -> {outfile}");
                    return(Path.Combine(outputFolderPath, outfile));
                }

                //Archive path might start with a \. Substring may return value that start with a \
                var subModPath = entryPath /*.TrimStart('\\')*/.Substring(ModPath.Length).TrimStart('\\');
                var path       = Path.Combine(outputFolderPath, subModPath);


                //Debug.WriteLine("remapping output: " + entryPath + " -> " + path);
                return(path);
            }

            if (compressPackages)
            {
                compressionQueue = new BlockingCollection <string>();
            }

            int numberOfPackagesToCompress = referencedFiles.Count(x => x.RepresentsPackageFilePath());
            int compressedPackageCount     = 0;
            NamedBackgroundWorker compressionThread;

            if (compressPackages)
            {
                compressionThread         = new NamedBackgroundWorker(@"ImportingCompressionThread");
                compressionThread.DoWork += (a, b) =>
                {
                    try
                    {
                        while (true)
                        {
                            var  package           = compressionQueue.Take();
                            var  p                 = MEPackageHandler.OpenMEPackage(package);
                            bool shouldNotCompress = Game == MEGame.ME1;
                            if (!shouldNotCompress)
                            {
                                //updateTextCallback?.Invoke(M3L.GetString(M3L.string_interp_compressingX, Path.GetFileName(package)));
                                FileInfo fileInfo     = new FileInfo(package);
                                var      created      = fileInfo.CreationTime;  //File Creation
                                var      lastmodified = fileInfo.LastWriteTime; //File Modification

                                compressedPackageCallback?.Invoke(M3L.GetString(M3L.string_interp_compressingX, Path.GetFileName(package)), compressedPackageCount, numberOfPackagesToCompress);
                                Log.Information(@"Compressing package: " + package);
                                p.Save(compress: true);
                                File.SetCreationTime(package, created);
                                File.SetLastWriteTime(package, lastmodified);
                            }
                            else
                            {
                                Log.Information(@"Skipping compression for ME1 package file: " + package);
                            }


                            Interlocked.Increment(ref compressedPackageCount);
                            compressedPackageCallback?.Invoke(M3L.GetString(M3L.string_interp_compressedX, Path.GetFileName(package)), compressedPackageCount, numberOfPackagesToCompress);
                        }
                    }
                    catch (InvalidOperationException)
                    {
                        //Done.
                        lock (compressionCompletedSignaler)
                        {
                            Monitor.Pulse(compressionCompletedSignaler);
                        }
                    }
                };
                compressionThread.RunWorkerAsync();
            }

            void compressPackage(object?sender, FileInfoEventArgs args)
            {
                if (compressPackages)
                {
                    var fToCompress = outputFilePathMapping(args.FileInfo);
                    if (fToCompress.RepresentsPackageFilePath())
                    {
                        //Debug.WriteLine("Adding to blocking queue");
                        compressionQueue.TryAdd(fToCompress);
                    }
                }
            }

            archive.FileExtractionFinished += compressPackage;

            if (!testRun)
            {
                Log.Information(@"Extracting files...");
                archive.ExtractFiles(outputFolderPath, outputFilePathMapping, fileIndicesToExtract.ToArray());
            }
            else
            {
                // test run mode
                // exes can have duplicate filenames but different indexes so we must check for those here.
                if (fileIndicesToExtract.Count != referencedFiles.Count && filePathsToExtractTESTONLY.Distinct().ToList().Count != referencedFiles.Count)
                {
                    throw new Exception(@"The amount of referenced files does not match the amount of files that are going to be extracted!");
                }
            }
            Log.Information(@"File extraction completed.");
            archive.Progressing -= archiveExtractionProgress;

            compressionQueue?.CompleteAdding();
            if (compressPackages && numberOfPackagesToCompress > 0 && numberOfPackagesToCompress > compressedPackageCount)
            {
                Log.Information(@"Waiting for compression of packages to complete.");
                while (!compressionQueue.IsCompleted)
                {
                    lock (compressionCompletedSignaler)
                    {
                        Monitor.Wait(compressionCompletedSignaler);
                    }
                }

                Log.Information(@"Package compression has completed.");
            }

            archive.FileExtractionFinished -= compressPackage;

            ModPath = outputFolderPath;
            if (IsVirtualized)
            {
                var parser = new IniDataParser().Parse(VirtualizedIniText);
                parser[@"ModInfo"][@"modver"] = ModVersionString; //In event relay service resolved this
                if (!testRun)
                {
                    File.WriteAllText(Path.Combine(ModPath, @"moddesc.ini"), parser.ToString());
                }
                IsVirtualized = false; //no longer virtualized
            }

            if (ExeExtractionTransform != null)
            {
                if (ExeExtractionTransform.VPatches.Any())
                {
                    // MEHEM uses Vpatching for its alternates.
                    var vpat = Utilities.GetCachedExecutablePath(@"vpat.exe");
                    if (!testRun)
                    {
                        Utilities.ExtractInternalFile(@"MassEffectModManagerCore.modmanager.executables.vpat.exe", vpat, true);
                    }
                    //Handle VPatching
                    foreach (var transform in ExeExtractionTransform.VPatches)
                    {
                        var patchfile  = Path.Combine(Utilities.GetVPatchRedirectsFolder(), transform.patchfile);
                        var inputfile  = Path.Combine(ModPath, transform.inputfile);
                        var outputfile = Path.Combine(ModPath, transform.outputfile);

                        var args = $"\"{patchfile}\" \"{inputfile}\" \"{outputfile}\""; //do not localize
                        if (!testRun)
                        {
                            Directory.CreateDirectory(Directory.GetParent(outputfile).FullName); //ensure output directory exists as vpatch will not make one.
                        }
                        Log.Information($@"VPatching file into alternate: {inputfile} to {outputfile}");
                        updateTextCallback?.Invoke(M3L.GetString(M3L.string_interp_vPatchingIntoAlternate, Path.GetFileName(inputfile)));
                        if (!testRun)
                        {
                            Utilities.RunProcess(vpat, args, true, false, false, true);
                        }
                    }
                }

                //Handle copyfile
                foreach (var copyfile in ExeExtractionTransform.CopyFiles)
                {
                    string srcfile  = Path.Combine(ModPath, copyfile.inputfile);
                    string destfile = Path.Combine(ModPath, copyfile.outputfile);
                    Log.Information($@"Applying transform copyfile: {srcfile} -> {destfile}");
                    if (!testRun)
                    {
                        File.Copy(srcfile, destfile, true);
                    }
                }

                if (ExeExtractionTransform.PostTransformModdesc != null)
                {
                    //fetch online moddesc for this mod.
                    Log.Information(@"Fetching post-transform third party moddesc.");
                    var moddesc = OnlineContent.FetchThirdPartyModdesc(ExeExtractionTransform.PostTransformModdesc);
                    if (!testRun)
                    {
                        File.WriteAllText(Path.Combine(ModPath, @"moddesc.ini"), moddesc);
                    }
                }
            }

            //int packagesCompressed = 0;
            //if (compressPackages)
            //{
            //    var packages = Utilities.GetPackagesInDirectory(ModPath, true);
            //    extractingCallback?.Invoke(new ProgressEventArgs((byte)(packagesCompressed * 100.0 / packages.Count), 0));
            //    foreach (var package in packages)
            //    {
            //        updateTextCallback?.Invoke(M3L.GetString(M3L.string_interp_compressingX, Path.GetFileName(package)));
            //        Log.Information("Compressing package: " + package);
            //        var p = MEPackageHandler.OpenMEPackage(package);
            //        p.save(true);

            //        packagesCompressed++;
            //        extractingCallback?.Invoke(new ProgressEventArgs((byte)(packagesCompressed * 100.0 / packages.Count), 0));
            //    }
            //}
            if (closeStreamOnFinish)
            {
                archive?.Dispose();
            }
            else
            {
                archive?.DisposeObjectOnly();
            }
        }
Ejemplo n.º 8
0
        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;
        }
Ejemplo n.º 10
0
        /// <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);
        }
Ejemplo n.º 11
0
        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;
        }
Ejemplo n.º 12
0
        /// <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;
            }
        }
Ejemplo n.º 15
0
        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);
        }
Ejemplo n.º 18
0
        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;
        }
Ejemplo n.º 19
0
        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");
            }
        }
Ejemplo n.º 20
0
        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;
        }