public static MemoryStream ApplyMixins(MemoryStream decompressedStream, List <Mixin> mixins, bool logMixinApplication = true, Action notifyApplicationDone = null, Action <string> failedApplicationCallback = null) { foreach (var mixin in mixins) { CLog.Information($@"Applying mixin: {mixin.PatchName} on {mixin.TargetFile}", logMixinApplication); if (decompressedStream.Length == mixin.TargetSize) { decompressedStream.Position = 0; var outStream = MixinMemoryStreamManager.GetStream(); JPatch.ApplyJPatch(decompressedStream, mixin.PatchData, outStream); if (!mixin.IsFinalizer && outStream.Length != decompressedStream.Length) { Log.Error($@"Applied mixin {mixin.PatchName} is not a finalizer but the filesize has changed! The output of this mixin patch will be discarded."); Crashes.TrackError(new Exception($@"Applied mixin {mixin.PatchName} is not a finalizer but the filesize has changed! The output of this mixin patch will be discarded.")); } else { CLog.Information(@"Applied mixin: " + mixin.PatchName, logMixinApplication); decompressedStream.Dispose(); decompressedStream = outStream; //pass through } } else { Crashes.TrackError(new Exception($@"Mixin {mixin.PatchName} cannot be applied, length of data is wrong. Expected size {mixin.TargetSize} but received source data size of {decompressedStream.Length}")); Log.Error($@"Mixin {mixin.PatchName} cannot be applied to this data, length of data is wrong. Expected size {mixin.TargetSize} but received source data size of {decompressedStream.Length}"); failedApplicationCallback?.Invoke(M3L.GetString(M3L.string_interp_cannotApplyMixinWrongSize, mixin.PatchName, mixin.TargetFile, mixin.TargetSize, decompressedStream.Length)); } notifyApplicationDone?.Invoke(); } return(decompressedStream); }
/// <summary> /// Builds the installation queues for the mod. Item 1 is the unpacked job mappings (per modjob) along with the list of custom dlc folders being installed. Item 2 is the list of modjobs, their sfar paths, and the list of source files to install for SFAR jobs. /// </summary> /// <param name="gameTarget"></param> /// <returns></returns> public (Dictionary <ModJob, (Dictionary <string, InstallSourceFile> unpackedJobMapping, List <string> dlcFoldersBeingInstalled)>, List <(ModJob job, string sfarPath, Dictionary <string, InstallSourceFile>)>) GetInstallationQueues(GameTarget gameTarget) { if (IsInArchive) { #if DEBUG if (Archive.IsDisposed()) { Debug.WriteLine(@">>> ARCHIVE IS DISPOSED"); } #endif if (File.Exists(ArchivePath) && (Archive == null || Archive.IsDisposed())) { Archive = new SevenZipExtractor(ArchivePath); //load archive file for inspection } else if (Archive != null && Archive.GetBackingStream() is SevenZip.ArchiveEmulationStreamProxy aesp && aesp.Source is MemoryStream ms) { var isExe = ArchivePath.EndsWith(@".exe", StringComparison.InvariantCultureIgnoreCase); Archive = isExe ? new SevenZipExtractor(ms, InArchiveFormat.Nsis) : new SevenZipExtractor(ms); MemoryAnalyzer.AddTrackedMemoryItem($@"Re-opened SVE archive for {ModName}", new WeakReference(Archive)); } } var gameDLCPath = M3Directories.GetDLCPath(gameTarget); var customDLCMapping = Enumerable.FirstOrDefault <ModJob>(InstallationJobs, x => x.Header == ModJob.JobHeader.CUSTOMDLC)?.CustomDLCFolderMapping; if (customDLCMapping != null) { //Make clone so original value is not modified customDLCMapping = new Dictionary <string, string>(customDLCMapping); //prevent altering the source object } var unpackedJobInstallationMapping = new Dictionary <ModJob, (Dictionary <string, InstallSourceFile> mapping, List <string> dlcFoldersBeingInstalled)>(); var sfarInstallationJobs = new List <(ModJob job, string sfarPath, Dictionary <string, InstallSourceFile> installationMapping)>(); foreach (var job in InstallationJobs) { Log.Information($@"Preprocessing installation job: {job.Header}"); var alternateFiles = Enumerable.Where <AlternateFile>(job.AlternateFiles, x => x.IsSelected && x.Operation != AlternateFile.AltFileOperation.OP_NOTHING && x.Operation != AlternateFile.AltFileOperation.OP_NOINSTALL_MULTILISTFILES).ToList(); var alternateDLC = Enumerable.Where <AlternateDLC>(job.AlternateDLCs, x => x.IsSelected).ToList(); if (job.Header == ModJob.JobHeader.CUSTOMDLC) { #region Installation: CustomDLC //Key = destination file, value = source file to install var installationMapping = new Dictionary <string, InstallSourceFile>(); unpackedJobInstallationMapping[job] = (installationMapping, new List <string>()); foreach (var altdlc in alternateDLC) { if (altdlc.Operation == AlternateDLC.AltDLCOperation.OP_ADD_CUSTOMDLC) { customDLCMapping[altdlc.AlternateDLCFolder] = altdlc.DestinationDLCFolder; } } foreach (var mapping in customDLCMapping) { //Mapping is done as DESTINATIONFILE = SOURCEFILE so you can override keys var source = FilesystemInterposer.PathCombine(IsInArchive, ModPath, mapping.Key); var target = Path.Combine(gameDLCPath, mapping.Value); //get list of all normal files we will install var allSourceDirFiles = FilesystemInterposer.DirectoryGetFiles(source, "*", SearchOption.AllDirectories, Archive).Select(x => x.Substring(ModPath.Length).TrimStart('\\')).ToList(); unpackedJobInstallationMapping[job].dlcFoldersBeingInstalled.Add(target); //loop over every file foreach (var sourceFile in allSourceDirFiles) { //Check against alt files bool altApplied = false; foreach (var altFile in alternateFiles) { if (altFile.ModFile.Equals(sourceFile, StringComparison.InvariantCultureIgnoreCase)) { //Alt applies to this file switch (altFile.Operation) { case AlternateFile.AltFileOperation.OP_NOINSTALL: CLog.Information($@"Not installing {sourceFile} for Alternate File {altFile.FriendlyName} due to operation OP_NOINSTALL", Settings.LogModInstallation); //we simply don't map as we just do a continue below. altApplied = true; break; case AlternateFile.AltFileOperation.OP_SUBSTITUTE: CLog.Information($@"Repointing {sourceFile} to {altFile.AltFile} for Alternate File {altFile.FriendlyName} due to operation OP_SUBSTITUTE", Settings.LogModInstallation); if (job.JobDirectory != null && altFile.AltFile.StartsWith((string)job.JobDirectory)) { installationMapping[sourceFile] = new InstallSourceFile(altFile.AltFile.Substring(job.JobDirectory.Length).TrimStart('/', '\\')) { AltApplied = true, IsFullRelativeFilePath = true }; //use alternate file as key instead } else { installationMapping[sourceFile] = new InstallSourceFile(altFile.AltFile) { AltApplied = true, IsFullRelativeFilePath = true }; //use alternate file as key instead } altApplied = true; break; case AlternateFile.AltFileOperation.OP_INSTALL: //same logic as substitute, just different logging. CLog.Information($@"Adding {sourceFile} to install (from {altFile.AltFile}) as part of Alternate File {altFile.FriendlyName} due to operation OP_INSTALL", Settings.LogModInstallation); if (job.JobDirectory != null && altFile.AltFile.StartsWith((string)job.JobDirectory)) { installationMapping[sourceFile] = new InstallSourceFile(altFile.AltFile.Substring(job.JobDirectory.Length).TrimStart('/', '\\')) { AltApplied = true, IsFullRelativeFilePath = true }; //use alternate file as key instead } else { installationMapping[sourceFile] = new InstallSourceFile(altFile.AltFile) { AltApplied = true, IsFullRelativeFilePath = true }; //use alternate file as key instead } altApplied = true; break; } break; } } if (altApplied) { continue; //no further processing for file } var relativeDestStartIndex = sourceFile.IndexOf(mapping.Value); string destPath = sourceFile.Substring(relativeDestStartIndex); installationMapping[destPath] = new InstallSourceFile(sourceFile); //destination is mapped to source file that will replace it. } foreach (var altdlc in alternateDLC) { if (altdlc.Operation == AlternateDLC.AltDLCOperation.OP_ADD_FOLDERFILES_TO_CUSTOMDLC) { string alternatePathRoot = FilesystemInterposer.PathCombine(IsInArchive, ModPath, altdlc.AlternateDLCFolder); var filesToAdd = FilesystemInterposer.DirectoryGetFiles(alternatePathRoot, "*", SearchOption.AllDirectories, Archive).Select(x => x.Substring(ModPath.Length).TrimStart('\\')).ToList(); foreach (var fileToAdd in filesToAdd) { var destFile = Path.Combine(altdlc.DestinationDLCFolder, fileToAdd.Substring(altdlc.AlternateDLCFolder.Length).TrimStart('\\', '/')); CLog.Information($@"Adding extra CustomDLC file ({fileToAdd} => {destFile}) due to Alternate DLC {altdlc.FriendlyName}'s {altdlc.Operation}", Settings.LogModInstallation); installationMapping[destFile] = new InstallSourceFile(fileToAdd) { AltApplied = true }; } } else if (altdlc.Operation == AlternateDLC.AltDLCOperation.OP_ADD_MULTILISTFILES_TO_CUSTOMDLC) { string alternatePathRoot = FilesystemInterposer.PathCombine(IsInArchive, ModPath, altdlc.MultiListRootPath); foreach (var fileToAdd in altdlc.MultiListSourceFiles) { var sourceFile = FilesystemInterposer.PathCombine(IsInArchive, alternatePathRoot, fileToAdd).Substring(ModPath.Length).TrimStart('\\'); var destFile = Path.Combine(altdlc.DestinationDLCFolder, fileToAdd.TrimStart('\\', '/')); CLog.Information($@"Adding extra CustomDLC file (MultiList) ({sourceFile} => {destFile}) due to Alternate DLC {altdlc.FriendlyName}'s {altdlc.Operation}", Settings.LogModInstallation); installationMapping[destFile] = new InstallSourceFile(sourceFile) { AltApplied = true }; } } } // Process altfile removal of multilist, since it should be done last var fileRemoveAltFiles = Enumerable.Where <AlternateFile>(job.AlternateFiles, x => x.IsSelected && x.Operation == AlternateFile.AltFileOperation.OP_NOINSTALL_MULTILISTFILES); foreach (var altFile in fileRemoveAltFiles) { foreach (var multifile in altFile.MultiListSourceFiles) { CLog.Information($@"Attempting to remove multilist file {multifile} from install (from {altFile.MultiListTargetPath}) as part of Alternate File {altFile.FriendlyName} due to operation OP_NOINSTALL_MULTILISTFILES", Settings.LogModInstallation); string relativeSourcePath = altFile.MultiListRootPath + '\\' + multifile; var targetPath = altFile.MultiListTargetPath + '\\' + multifile; if (installationMapping.Remove(targetPath)) { CLog.Information($@" > Removed multilist file {targetPath} from installation", Settings.LogModInstallation); } } } } #endregion } else if (job.Header == ModJob.JobHeader.LOCALIZATION) { #region Installation: LOCALIZATION var installationMapping = new CaseInsensitiveDictionary <InstallSourceFile>(); unpackedJobInstallationMapping[job] = (installationMapping, new List <string>()); buildInstallationQueue(job, installationMapping, false); #endregion } else if (job.Header == ModJob.JobHeader.BASEGAME || job.Header == ModJob.JobHeader.BALANCE_CHANGES || job.Header == ModJob.JobHeader.ME1_CONFIG) { #region Installation: BASEGAME, BALANCE CHANGES, ME1 CONFIG var installationMapping = new CaseInsensitiveDictionary <InstallSourceFile>(); unpackedJobInstallationMapping[job] = (installationMapping, new List <string>()); buildInstallationQueue(job, installationMapping, false); #endregion } else if (Game == MEGame.ME3 && ModJob.ME3SupportedNonCustomDLCJobHeaders.Contains(job.Header)) //previous else if will catch BASEGAME { #region Installation: DLC Unpacked and SFAR (ME3 ONLY) if (M3Directories.IsOfficialDLCInstalled(job.Header, gameTarget)) { string sfarPath = job.Header == ModJob.JobHeader.TESTPATCH ? M3Directories.GetTestPatchSFARPath(gameTarget) : Path.Combine(gameDLCPath, ModJob.GetHeadersToDLCNamesMap(MEGame.ME3)[job.Header], @"CookedPCConsole", @"Default.sfar"); if (File.Exists(sfarPath)) { var installationMapping = new CaseInsensitiveDictionary <InstallSourceFile>(); if (new FileInfo(sfarPath).Length == 32) { //Unpacked unpackedJobInstallationMapping[job] = (installationMapping, new List <string>()); buildInstallationQueue(job, installationMapping, false); } else { //Packed //unpackedJobInstallationMapping[job] = installationMapping; buildInstallationQueue(job, installationMapping, true); sfarInstallationJobs.Add((job, sfarPath, installationMapping)); } } } else { Log.Warning($@"DLC not installed, skipping: {job.Header}"); } #endregion } else if (Game == MEGame.ME2 || Game == MEGame.ME1) { #region Installation: DLC Unpacked (ME1/ME2 ONLY) //Unpacked if (M3Directories.IsOfficialDLCInstalled(job.Header, gameTarget)) { var installationMapping = new CaseInsensitiveDictionary <InstallSourceFile>(); unpackedJobInstallationMapping[job] = (installationMapping, new List <string>()); buildInstallationQueue(job, installationMapping, false); } else { Log.Warning($@"DLC not installed, skipping: {job.Header}"); } #endregion } else { //?? Header throw new Exception(@"Unsupported installation job header! " + job.Header); } } return(unpackedJobInstallationMapping, sfarInstallationJobs); }
public 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; }
private void CompileAsNewMod() { NamedBackgroundWorker nbw = new NamedBackgroundWorker(@"MixinManager CompileAsNewModThread"); List <string> failedApplications = new List <string>(); var modname = NewModName; var modpath = Path.Combine(Utilities.GetME3ModsDirectory(), Utilities.SanitizePath(modname)); var result = M3L.ShowDialog(mainwindow, M3L.GetString(M3L.string_interp_dialogCreatingNewModWithExistingName, NewModName, modpath), M3L.GetString(M3L.string_modAlreadyExists), MessageBoxButton.YesNo, MessageBoxImage.Warning, MessageBoxResult.No); if (result == MessageBoxResult.No) { Log.Information(@"User has aborted mixin compilation due to same-named mod existing"); return; //abort. } nbw.DoWork += (a, b) => { BottomLeftMessage = M3L.GetString(M3L.string_compilingMixins); OperationInProgress = true; //DEBUG STUFF #if DEBUG int numCoresToApplyWith = 1; #else var numCoresToApplyWith = Environment.ProcessorCount; if (numCoresToApplyWith > 4) { numCoresToApplyWith = 4; //no more than 4 as this uses a lot of memory } #endif var mixins = AvailableOfficialMixins.Where(x => x.UISelectedForUse).ToList(); MixinHandler.LoadPatchDataForMixins(mixins); //before dynamic void failedApplicationCallback(string str) { failedApplications.Add(str); } var compilingListsPerModule = MixinHandler.GetMixinApplicationList(mixins, failedApplicationCallback); if (failedApplications.Any()) { //Error building list modpath = null; Log.Information(@"Aborting mixin compiling due to incompatible selection of mixins"); return; } if (Directory.Exists(modpath)) { Utilities.DeleteFilesAndFoldersRecursively(modpath); } ProgressBarMax = mixins.Count(); ProgressBarValue = 0; int numdone = 0; void completedSingleApplicationCallback() { var val = Interlocked.Increment(ref numdone); ProgressBarValue = val; } //Mixins are ready to be applied Parallel.ForEach(compilingListsPerModule, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount > numCoresToApplyWith ? numCoresToApplyWith : Environment.ProcessorCount }, mapping => { var dlcFolderName = ModMakerCompiler.ModmakerChunkNameToDLCFoldername(mapping.Key.ToString()); var outdir = Path.Combine(modpath, ModMakerCompiler.HeaderToDefaultFoldername(mapping.Key), @"CookedPCConsole"); Directory.CreateDirectory(outdir); if (mapping.Key == ModJob.JobHeader.BASEGAME) { //basegame foreach (var file in mapping.Value) { try { using var packageAsStream = VanillaDatabaseService.FetchBasegameFile(Mod.MEGame.ME3, Path.GetFileName(file.Key)); //packageAsStream.WriteToFile(@"C:\users\dev\desktop\compressed.pcc"); using var decompressedStream = MEPackage.GetDecompressedPackageStream(packageAsStream, true); //decompressedStream.WriteToFile(@"C:\users\dev\desktop\decompressed.pcc"); using var finalStream = MixinHandler.ApplyMixins(decompressedStream, file.Value, completedSingleApplicationCallback, failedApplicationCallback); CLog.Information(@"Compressing package to mod directory: " + file.Key, Settings.LogModMakerCompiler); finalStream.Position = 0; var package = MEPackageHandler.OpenMEPackage(finalStream); var outfile = Path.Combine(outdir, Path.GetFileName(file.Key)); package.save(outfile, false); // don't compress //finalStream.WriteToFile(outfile); //File.WriteAllBytes(outfile, finalStream.ToArray()); } catch (Exception e) { var mixinsStr = string.Join(@", ", file.Value.Select(x => x.PatchName)); Log.Error($@"Error in mixin application for file {file.Key}: {e.Message}"); failedApplicationCallback(M3L.GetString(M3L.string_interp_errorApplyingMixinsForFile, mixinsStr, file.Key, e.Message)); } } } else { //dlc var dlcPackage = VanillaDatabaseService.FetchVanillaSFAR(dlcFolderName); //do not have to open file multiple times. foreach (var file in mapping.Value) { try { using var packageAsStream = VanillaDatabaseService.FetchFileFromVanillaSFAR(dlcFolderName, file.Key, forcedDLC: dlcPackage); using var decompressedStream = MEPackage.GetDecompressedPackageStream(packageAsStream); using var finalStream = MixinHandler.ApplyMixins(decompressedStream, file.Value, completedSingleApplicationCallback, failedApplicationCallback); CLog.Information(@"Compressing package to mod directory: " + file.Key, Settings.LogModMakerCompiler); finalStream.Position = 0; var package = MEPackageHandler.OpenMEPackage(finalStream); var outfile = Path.Combine(outdir, Path.GetFileName(file.Key)); package.save(outfile, true); } catch (Exception e) { var mixinsStr = string.Join(@", ", file.Value.Select(x => x.PatchName)); Log.Error($@"Error in mixin application for file {file.Key}: {e.Message}"); failedApplicationCallback(M3L.GetString(M3L.string_interp_errorApplyingMixinsForFile, mixinsStr, file.Key, e.Message)); } //finalStream.WriteToFile(outfile); } } }); MixinHandler.FreeME3TweaksPatchData(); //Generate moddesc IniData ini = new IniData(); ini[@"ModManager"][@"cmmver"] = App.HighestSupportedModDesc.ToString(CultureInfo.InvariantCulture); //prevent commas ini[@"ModInfo"][@"game"] = @"ME3"; ini[@"ModInfo"][@"modname"] = modname; ini[@"ModInfo"][@"moddev"] = App.AppVersionHR; ini[@"ModInfo"][@"moddesc"] = M3L.GetString(M3L.string_compiledFromTheFollowingMixins); ini[@"ModInfo"][@"modver"] = @"1.0"; generateRepaceFilesMapping(ini, modpath); File.WriteAllText(Path.Combine(modpath, @"moddesc.ini"), ini.ToString()); }; nbw.RunWorkerCompleted += (a, b) => { OperationInProgress = false; ClearMixinHandler(); if (failedApplications.Count > 0) { var ld = new ListDialog(failedApplications, M3L.GetString(M3L.string_failedToApplyAllMixins), M3L.GetString(M3L.string_theFollowingMixinsFailedToApply), mainwindow); ld.ShowDialog(); } if (modpath != null) { OnClosing(new DataEventArgs(modpath)); } else { BottomLeftMessage = M3L.GetString(M3L.string_selectMixinsToCompile); } }; CompilePanelButton.IsOpen = false; nbw.RunWorkerAsync(); }
public AlternateDLC(string alternateDLCText, Mod modForValidating) { var properties = StringStructParser.GetCommaSplitValues(alternateDLCText); //todo: if statements to check these. if (properties.TryGetValue(@"FriendlyName", out string friendlyName)) { FriendlyName = friendlyName; } if (modForValidating.ModDescTargetVersion >= 6 && string.IsNullOrWhiteSpace(FriendlyName)) { //Cannot be null. Log.Error(@"Alternate DLC does not specify FriendlyName. Mods targeting moddesc >= 6.0 require FriendlyName"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_validation_altdlc_oneAltDlcMissingFriendlyNameCmm6); return; } if (!Enum.TryParse(properties[@"Condition"], out Condition)) { Log.Error($@"Alternate DLC specifies unknown/unsupported condition: {properties[@"Condition"]}"); //do not localize ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_validation_altdlc_unknownCondition) + properties[@"Condition"]; return; } if (!Enum.TryParse(properties[@"ModOperation"], out Operation)) { Log.Error($@"Alternate DLC specifies unknown/unsupported operation: {properties[@"ModOperation"]}"); //do not localize ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_validation_altdlc_unknownOperation) + properties[@"ModOperation"]; return; } if (properties.TryGetValue(@"Description", out string description)) { Description = description; } if (modForValidating.ModDescTargetVersion >= 6 && string.IsNullOrWhiteSpace(Description)) { //Cannot be null. Log.Error($@"Alternate DLC {FriendlyName} cannot have empty Description or missing description as it targets cmmver >= 6"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_cmmver6RequiresDescription, FriendlyName); return; } if (Operation != AltDLCOperation.OP_NOTHING) { if (properties.TryGetValue(@"ModAltDLC", out string altDLCFolder)) { AlternateDLCFolder = altDLCFolder.Replace('/', '\\'); } else { Log.Error(@"Alternate DLC does not specify ModAltDLC but is required"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_missingModAltDLC, FriendlyName); return; } if (properties.TryGetValue(@"ModDestDLC", out string destDLCFolder)) { DestinationDLCFolder = destDLCFolder.Replace('/', '\\'); } else { Log.Error(@"Alternate DLC does not specify ModDestDLC but is required"); ValidAlternate = false; LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_missingModDestDLC, FriendlyName); return; } //todo: Validate target in mod folder if (properties.TryGetValue(@"ConditionalDLC", out string conditionalDlc)) { var conditionalList = StringStructParser.GetSemicolonSplitList(conditionalDlc); foreach (var dlc in conditionalList) { //if (modForValidating.Game == Mod.MEGame.ME3) //{ if (Enum.TryParse(dlc, out ModJob.JobHeader header) && ModJob.GetHeadersToDLCNamesMap(modForValidating.Game).TryGetValue(header, out var foldername)) { ConditionalDLC.Add(foldername); continue; } //} if (!dlc.StartsWith(@"DLC_")) { Log.Error($@"An item in Alternate DLC's ({FriendlyName}) ConditionalDLC doesn't start with DLC_ or is not official header"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_conditionalDLCInvalidValue, FriendlyName); return; } else { ConditionalDLC.Add(dlc); } } } //Validation if (string.IsNullOrWhiteSpace(AlternateDLCFolder)) { Log.Error($@"Alternate DLC directory (ModAltDLC) not specified for { FriendlyName}"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_sourceDirectoryNotSpecifiedForModAltDLC, FriendlyName); return; } if (string.IsNullOrWhiteSpace(DestinationDLCFolder)) { Log.Error($@"Destination DLC directory (ModDestDLC) not specified for {FriendlyName}"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_destinationDirectoryNotSpecifiedForModDestDLC, FriendlyName); return; } AlternateDLCFolder = AlternateDLCFolder.TrimStart('\\', '/').Replace('/', '\\'); //Check ModAltDLC directory exists var localAltDlcDir = FilesystemInterposer.PathCombine(modForValidating.IsInArchive, modForValidating.ModPath, AlternateDLCFolder); if (!FilesystemInterposer.DirectoryExists(localAltDlcDir, modForValidating.Archive)) { Log.Error($@"Alternate DLC directory (ModAltDLC) does not exist: {AlternateDLCFolder}"); LoadFailedReason = M3L.GetString(M3L.string_interp_validation_altdlc_sourceDirectoryDoesntExist, FriendlyName, AlternateDLCFolder); return; } } ApplicableAutoText = properties.TryGetValue(@"ApplicableAutoText", out string applicableText) ? applicableText : M3L.GetString(M3L.string_autoApplied); NotApplicableAutoText = properties.TryGetValue(@"NotApplicableAutoText", out string notApplicableText) ? notApplicableText : M3L.GetString(M3L.string_notApplicable); if (modForValidating.ModDescTargetVersion >= 6.0) { GroupName = properties.TryGetValue(@"OptionGroup", out string groupName) ? groupName : null; } if (Condition == AltDLCCondition.COND_MANUAL && properties.TryGetValue(@"CheckedByDefault", out string checkedByDefault) && bool.TryParse(checkedByDefault, out bool cbd)) { CheckedByDefault = cbd; } CLog.Information($@"AlternateDLC loaded and validated: {FriendlyName}", Settings.LogModStartup); ValidAlternate = true; }
public 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 void SetupInitialSelection(GameTarget target, Mod mod) { UIIsSelectable = false; //Reset IsSelected = false; //Reset if (Condition == AltDLCCondition.COND_MANUAL) { IsSelected = CheckedByDefault; if (DLCRequirementsForManual != null) { var dlc = M3Directories.GetInstalledDLC(target); if (mod.ModDescTargetVersion >= 6.3) { var requiredDLC = DLCRequirementsForManual.Where(x => !x.StartsWith(@"-") || x.StartsWith(@"+")).Select(x => x.TrimStart('+')); // none or + means 'must exist' var notPresentDLCRequired = DLCRequirementsForManual.Where(x => x.StartsWith(@"-")).Select(x => x.TrimStart('-')); UIIsSelectable = dlc.ContainsAll(requiredDLC, StringComparer.InvariantCultureIgnoreCase) && dlc.ContainsNone(notPresentDLCRequired, StringComparer.InvariantCultureIgnoreCase); } else { // Previous logic. Left here to ensure nothing changes. UIIsSelectable = dlc.ContainsAll(DLCRequirementsForManual, StringComparer.InvariantCultureIgnoreCase); } if (!UIIsSelectable && mod.ModDescTargetVersion >= 6.2) { // Mod Manager 6.2: If requirements are not met this option is forcibly not checked. // Mods targeting Moddesc 6 or 6.1 will possibly be bugged if they used this feature IsSelected = false; } CLog.Information($@" > AlternateDLC SetupInitialSelection() {FriendlyName}: UISelectable: {UIIsSelectable}, conducted DLCRequirements check.", Settings.LogModInstallation); } else { UIIsSelectable = true; } return; } var installedDLC = M3Directories.GetInstalledDLC(target); switch (Condition) { case AltDLCCondition.COND_DLC_NOT_PRESENT: case AltDLCCondition.COND_ANY_DLC_NOT_PRESENT: IsSelected = !ConditionalDLC.All(i => installedDLC.Contains(i, StringComparer.CurrentCultureIgnoreCase)); break; case AltDLCCondition.COND_DLC_PRESENT: case AltDLCCondition.COND_ANY_DLC_PRESENT: IsSelected = ConditionalDLC.Any(i => installedDLC.Contains(i, StringComparer.CurrentCultureIgnoreCase)); break; case AltDLCCondition.COND_ALL_DLC_NOT_PRESENT: IsSelected = !ConditionalDLC.Any(i => installedDLC.Contains(i, StringComparer.CurrentCultureIgnoreCase)); break; case AltDLCCondition.COND_ALL_DLC_PRESENT: IsSelected = ConditionalDLC.All(i => installedDLC.Contains(i, StringComparer.CurrentCultureIgnoreCase)); break; case AltDLCCondition.COND_SPECIFIC_SIZED_FILES: { var selected = true; foreach (var reqPair in RequiredSpecificFiles) { if (selected) { var targetFile = Path.Combine(target.TargetPath, reqPair.Key); selected &= File.Exists(targetFile) && new FileInfo(targetFile).Length == reqPair.Value; } } IsSelected = selected; } break; case AltDLCCondition.COND_SPECIFIC_DLC_SETUP: { var selected = true; foreach (var condDlc in ConditionalDLC) { if (selected) { bool existenceRule = condDlc.Substring(0, 1) == @"+"; var dlcfoldername = condDlc.Substring(1); if (existenceRule) { selected &= installedDLC.Contains(dlcfoldername, StringComparer.CurrentCultureIgnoreCase); } else { selected &= !installedDLC.Contains(dlcfoldername, StringComparer.CurrentCultureIgnoreCase); } } } IsSelected = selected; } break; } UIIsSelectable = false; //autos //IsSelected; //autos }
/// <summary> /// Gets a dictionary of module=> [filename, list of mixins] to apply. /// </summary> /// <param name="allmixins"></param> /// <returns></returns> public static Dictionary <ModJob.JobHeader, Dictionary <string, List <Mixin> > > GetMixinApplicationList(List <Mixin> allmixins, Action <string> errorCallback = null) { var compilingListsPerModule = new Dictionary <ModJob.JobHeader, Dictionary <string, List <Mixin> > >(); var modules = allmixins.Select(x => x.TargetModule).Distinct().ToList(); foreach (var module in modules) { var moduleMixinMapping = new Dictionary <string, List <Mixin> >(); var mixinsForModule = allmixins.Where(x => x.TargetModule == module).ToList(); foreach (var mixin in mixinsForModule) { List <Mixin> mixinListForFile; if (!moduleMixinMapping.TryGetValue(mixin.TargetFile, out mixinListForFile)) { mixinListForFile = new List <Mixin>(); moduleMixinMapping[mixin.TargetFile] = mixinListForFile; } //make sure finalizer is last if (mixin.IsFinalizer) { CLog.Information( $@"Adding finalizer mixin to mixin list for file {Path.GetFileName(mixin.TargetFile)}: {mixin.PatchName}", Settings.LogModMakerCompiler); mixinListForFile.Add(mixin); } else { CLog.Information( $@"Adding mixin to mixin list for file {Path.GetFileName(mixin.TargetFile)}: {mixin.PatchName}", Settings.LogModMakerCompiler); mixinListForFile.Insert(0, mixin); } } //verify only one finalizer foreach (var list in moduleMixinMapping) { if (list.Value.Count(x => x.IsFinalizer) > 1) { Log.Error(@"ERROR: MORE THAN ONE FINALIZER IS PRESENT FOR FILE: " + list.Key); string error = M3L.GetString(M3L.string_interp_cannotApplyMultipleFinalizers, list.Key); foreach (var fin in list.Value.Where(x => x.IsFinalizer)) { error += "\n"; //do not localize error += fin.PatchName; } errorCallback?.Invoke(error); list.Value.Clear(); //remove items //do something here to abort } } var uniuqe = moduleMixinMapping.Where(x => x.Value.Any()); moduleMixinMapping = uniuqe.ToDictionary(x => x.Key, x => x.Value); compilingListsPerModule[module] = moduleMixinMapping; } return(compilingListsPerModule); }
public void SetupInitialSelection(GameTarget target) { UIIsSelectable = false; //Reset IsSelected = false; //Reset if (Condition == AltDLCCondition.COND_MANUAL) { IsSelected = CheckedByDefault; if (DLCRequirementsForManual != null) { var dlc = MEDirectories.GetInstalledDLC(target); UIIsSelectable = dlc.ContainsAll(DLCRequirementsForManual, StringComparer.InvariantCultureIgnoreCase); CLog.Information($@" > AlternateDLC SetupInitialSelection() {FriendlyName}: UISelectable: {UIIsSelectable}, conducted DLCRequirements check.", Settings.LogModInstallation); } else //TODO: FILE LEVEL CHECKS FOR ALOV { UIIsSelectable = true; } return; } var installedDLC = MEDirectories.GetInstalledDLC(target); switch (Condition) { case AltDLCCondition.COND_DLC_NOT_PRESENT: case AltDLCCondition.COND_ANY_DLC_NOT_PRESENT: IsSelected = !ConditionalDLC.All(i => installedDLC.Contains(i, StringComparer.CurrentCultureIgnoreCase)); break; case AltDLCCondition.COND_DLC_PRESENT: case AltDLCCondition.COND_ANY_DLC_PRESENT: IsSelected = ConditionalDLC.Any(i => installedDLC.Contains(i, StringComparer.CurrentCultureIgnoreCase)); break; case AltDLCCondition.COND_ALL_DLC_NOT_PRESENT: IsSelected = !ConditionalDLC.Any(i => installedDLC.Contains(i, StringComparer.CurrentCultureIgnoreCase)); break; case AltDLCCondition.COND_ALL_DLC_PRESENT: IsSelected = ConditionalDLC.All(i => installedDLC.Contains(i, StringComparer.CurrentCultureIgnoreCase)); break; case AltDLCCondition.COND_SPECIFIC_SIZED_FILES: { var selected = true; foreach (var reqPair in RequiredSpecificFiles) { if (selected) { var targetFile = Path.Combine(target.TargetPath, reqPair.Key); selected &= File.Exists(targetFile) && new FileInfo(targetFile).Length == reqPair.Value; } } IsSelected = selected; } break; case AltDLCCondition.COND_SPECIFIC_DLC_SETUP: { var selected = true; foreach (var condDlc in ConditionalDLC) { if (selected) { bool existenceRule = condDlc.Substring(0, 1) == @"+"; var dlcfoldername = condDlc.Substring(1); if (existenceRule) { selected &= installedDLC.Contains(dlcfoldername, StringComparer.CurrentCultureIgnoreCase); } else { selected &= !installedDLC.Contains(dlcfoldername, StringComparer.CurrentCultureIgnoreCase); } } } IsSelected = selected; } break; } UIIsSelectable = false; //autos //IsSelected; //autos }
/// <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); }
public static void ApplyMixinsToModule(KeyValuePair <ModJob.JobHeader, Dictionary <string, List <Mixin> > > mapping, string modpath, Action completedSingleApplicationCallback, Action <string> failedApplicationCallback) { var dlcFolderName = ModMakerCompiler.ModmakerChunkNameToDLCFoldername(mapping.Key.ToString()); var outdir = Path.Combine(modpath, ModMakerCompiler.HeaderToDefaultFoldername(mapping.Key), @"CookedPCConsole"); Directory.CreateDirectory(outdir); if (mapping.Key == ModJob.JobHeader.BASEGAME) { //basegame foreach (var file in mapping.Value) { try { using var packageAsStream = VanillaDatabaseService.FetchBasegameFile(MEGame.ME3, Path.GetFileName(file.Key)); //packageAsStream.WriteToFile(@"C:\users\dev\desktop\compressed.pcc"); using var decompressedStream = MEPackage.GetDecompressedPackageStream(packageAsStream, false, true); using var finalStream = MixinHandler.ApplyMixins(decompressedStream, file.Value, true, completedSingleApplicationCallback, failedApplicationCallback); CLog.Information(@"Compressing package to mod directory: " + file.Key, Settings.LogModMakerCompiler); finalStream.Position = 0; var package = MEPackageHandler.OpenMEPackageFromStream(finalStream); var outfile = Path.Combine(outdir, Path.GetFileName(file.Key)); package.Save(outfile, false, includeAdditionalPackagesToCook: false, includeDependencyTable: true); // don't compress, use mixin saving rules for basegame files } catch (Exception e) { var mixinsStr = string.Join(@", ", file.Value.Select(x => x.PatchName)); Log.Error($@"Error in mixin application for file {file.Key}: {e.Message}"); failedApplicationCallback(M3L.GetString(M3L.string_interp_errorApplyingMixinsForFile, mixinsStr, file.Key, e.Message)); } } } else { //dlc var dlcPackage = VanillaDatabaseService.FetchVanillaSFAR(dlcFolderName); //do not have to open file multiple times. foreach (var file in mapping.Value) { try { using var packageAsStream = VanillaDatabaseService.FetchFileFromVanillaSFAR(dlcFolderName, file.Key, forcedDLC: dlcPackage); //as file comes from backup, we don't need to decompress it, it will always be decompressed in sfar using var finalStream = MixinHandler.ApplyMixins(packageAsStream, file.Value, true, completedSingleApplicationCallback, failedApplicationCallback); var outfile = Path.Combine(outdir, Path.GetFileName(file.Key)); if (mapping.Key != ModJob.JobHeader.TESTPATCH) { // TestPatch is never unpacked. So there is not really point to // compressing it's rather small files. The other DLC jobs likely will be packed still, but this will save some disk space. CLog.Information($@"Compressing package to mod directory: {outfile}", Settings.LogModMakerCompiler); finalStream.Position = 0; var package = MEPackageHandler.OpenMEPackageFromStream(finalStream); package.Save(outfile, true); } else { Log.Information($@"Writing patched file to disk: {outfile}"); finalStream.WriteToFile(outfile); } } catch (Exception e) { var mixinsStr = string.Join(@", ", file.Value.Select(x => x.PatchName)); Log.Error($@"Error in mixin application for file {file.Key}: {e.Message}"); failedApplicationCallback(M3L.GetString(M3L.string_interp_errorApplyingMixinsForFile, mixinsStr, file.Key, e.Message)); } //finalStream.WriteToFile(outfile); } } }
private ModInstallCompletedStatus InstallAttachedRCWMod() { CLog.Information(@"Installing attached RCW mod. Checking Coalesced.ini first to make sure this mod can be safely applied", Settings.LogModInstallation); ME2Coalesced me2c = new ME2Coalesced(ME2Directory.CoalescedPath(gameTarget)); RCWMod rcw = ModBeingInstalled.GetJob(ModJob.JobHeader.ME2_RCWMOD).RCW; foreach (var rcwF in rcw.Files) { var me2cF = me2c.Inis.FirstOrDefault(x => x.Key == rcwF.FileName); if (me2cF.Key == null) { Log.Error(@"RCW mod specifies a file in coalesced that does not exist in the local one: " + rcwF); return(ModInstallCompletedStatus.INSTALL_FAILED_MALFORMED_RCW_FILE); } foreach (var rcwS in rcwF.Sections) { var section = me2cF.Value.Sections.FirstOrDefault(x => x.Header == rcwS.SectionName); if (section == null) { Log.Error($@"RCW mod specifies a section in {rcwF.FileName} that does not exist in the local coalesced: {rcwS.SectionName}"); return(ModInstallCompletedStatus.INSTALL_FAILED_MALFORMED_RCW_FILE); } } //Apply mod foreach (var rcwS in rcwF.Sections) { var section = me2cF.Value.Sections.FirstOrDefault(x => x.Header == rcwS.SectionName); Dictionary <string, int> keyCount = new Dictionary <string, int>(); foreach (var key in section.Entries) { if (keyCount.TryGetValue(key.Key, out var existingCount)) { keyCount[key.Key] = existingCount + 1; } else { keyCount[key.Key] = 1; } } Dictionary <string, bool> keysSupportingMulti = keyCount.ToDictionary(x => x.Key, x => x.Value > 1); //Remove items foreach (var itemToDelete in rcwS.KeysToDelete) { for (int i = section.Entries.Count - 1; i > 0; i--) { var entry = section.Entries[i]; if (entry.Key == itemToDelete.Key && entry.Value == itemToDelete.Value) { CLog.Information($@"Removing ini entry {entry.RawText} in section {section.Header} of file {me2cF.Key}", Settings.LogModInstallation); section.Entries.RemoveAt(i); } } } foreach (var itemToAdd in rcwS.KeysToAdd) { var existingEntries = section.Entries.Where(x => x.Key == itemToAdd.Key).ToList(); if (existingEntries.Count <= 0) { //just add it CLog.Information($@"Adding ini entry {itemToAdd.RawText} in section {section.Header} of file {me2cF.Key}", Settings.LogModInstallation); section.Entries.Add(itemToAdd); } else if (existingEntries.Count > 1) { //Supports multi. Add this key - but making sure the data doesn't already exist! if (existingEntries.Any(x => x.Value == itemToAdd.Value)) { //Duplicate. CLog.Information($@"Not adding duplicate ini entry {itemToAdd.RawText} in section {section.Header} of file {me2cF.Key}", Settings.LogModInstallation); } else { //Not duplicate - installing key CLog.Information($@"Adding ini entry {itemToAdd.RawText} in section {section.Header} of file {me2cF.Key}", Settings.LogModInstallation); section.Entries.Add(itemToAdd); } } else { //Only one key exists currently. We need to check multi lookup to choose how to install if (keysSupportingMulti[itemToAdd.Key]) { //Supports multi. Add this key - but making sure the data doesn't already exist! if (existingEntries.Any(x => x.Value == itemToAdd.Value)) { //Duplicate. CLog.Information($@"Not adding duplicate ini entry {itemToAdd.RawText} in section {section.Header} of file {me2cF.Key}", Settings.LogModInstallation); } else { //Not duplicate - installing key CLog.Information($@"Adding ini entry {itemToAdd.RawText} in section {section.Header} of file {me2cF.Key}", Settings.LogModInstallation); section.Entries.Add(itemToAdd); } } else { //Replace existing key existingEntries[0].Value = itemToAdd.Value; } } } } } me2c.Serialize(); return(ModInstallCompletedStatus.INSTALL_SUCCESSFUL); }
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; } }
private void buildInstallationQueue(ModJob job, CaseInsensitiveDictionary <InstallSourceFile> installationMapping, bool isSFAR) { CLog.Information(@"Building installation queue for " + job.Header, Settings.LogModInstallation); foreach (var entry in job.FilesToInstall) { //Key is destination, value is source file var destFile = entry.Key; var sourceFile = entry.Value; bool altApplied = false; foreach (var altFile in job.AlternateFiles.Where(x => x.IsSelected)) { Debug.WriteLine(@"Checking alt conditions for application: " + altFile.FriendlyName); if (altFile.Operation == AlternateFile.AltFileOperation.OP_NOTHING) { continue; //skip nothing } if (altFile.Operation == AlternateFile.AltFileOperation.OP_APPLY_MULTILISTFILES) { continue; //do not apply in the main loop. } if (altFile.Operation == AlternateFile.AltFileOperation.OP_NOINSTALL_MULTILISTFILES) { continue; //do not apply in the main loop. } if (altFile.ModFile.Equals(destFile, StringComparison.InvariantCultureIgnoreCase)) { //Alt applies to this file switch (altFile.Operation) { case AlternateFile.AltFileOperation.OP_NOINSTALL: CLog.Information($@"Not installing {destFile} 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 {destFile} to {altFile.AltFile} for Alternate File {altFile.FriendlyName} due to operation OP_SUBSTITUTE", Settings.LogModInstallation); if (job.JobDirectory != null && (altFile.AltFile.StartsWith(job.JobDirectory) && job.Header == ModJob.JobHeader.CUSTOMDLC)) { installationMapping[destFile] = new InstallSourceFile(altFile.AltFile.Substring(job.JobDirectory.Length).TrimStart('/', '\\')) { AltApplied = true, IsFullRelativeFilePath = true }; //use alternate file as key instead } else { installationMapping[destFile] = 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(job.JobDirectory) && job.Header == ModJob.JobHeader.CUSTOMDLC)) { installationMapping[destFile] = new InstallSourceFile(altFile.AltFile.Substring(job.JobDirectory.Length).TrimStart('/', '\\')) { AltApplied = true, IsFullRelativeFilePath = true }; //use alternate file as key instead } else { installationMapping[destFile] = 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 } //installationMapping[sourceFile] = sourceFile; //Nothing different, just add to installation list installationMapping[destFile] = new InstallSourceFile(sourceFile); CLog.Information($@"Adding {job.Header} file to installation {(isSFAR ? @"SFAR" : @"unpacked")} queue: {entry.Value} -> {destFile}", Settings.LogModInstallation); //do not localize } //Apply autolist alternate files foreach (var altFile in job.AlternateFiles.Where(x => x.IsSelected && x.Operation == AlternateFile.AltFileOperation.OP_APPLY_MULTILISTFILES)) { foreach (var multifile in altFile.MultiListSourceFiles) { CLog.Information( $@"Adding multilist file {multifile} to install (from {altFile.MultiListRootPath}) as part of Alternate File {altFile.FriendlyName} due to operation OP_APPLY_MULTILISTFILES", Settings.LogModInstallation); string relativeSourcePath = altFile.MultiListRootPath + '\\' + multifile; var targetPath = altFile.MultiListTargetPath + '\\' + multifile; installationMapping[targetPath] = new InstallSourceFile(relativeSourcePath) { AltApplied = true, IsFullRelativeFilePath = true }; //use alternate file as key instead //} //not sure if there should be an else case here. //else //{ // installationMapping[destFile] = new InstallSourceFile(multifile) // { // AltApplied = true, // IsFullRelativeFilePath = true // }; //use alternate file as key instead //} } } // Remove multilist noinstall files foreach (var altFile in job.AlternateFiles.Where(x => x.IsSelected && x.Operation == AlternateFile.AltFileOperation.OP_NOINSTALL_MULTILISTFILES)) { foreach (var multifile in altFile.MultiListSourceFiles) { CLog.Information( $@"Attempting to remove multilist file {multifile} from install (from {altFile.MultiListRootPath}) 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); } else { Log.Warning($@"Failed to remove multilist file from installation queue as specified by altfile: {targetPath}, path not present in installation files"); } } } }
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; }
public void ReloadGameTarget(bool lodUpdateAndLogging = true, bool forceLodUpdate = false) { if (Game != Mod.MEGame.Unknown && !IsCustomOption) { if (Directory.Exists(TargetPath)) { var oldTMOption = TextureModded; var alotInfo = GetInstalledALOTInfo(); if (alotInfo != null) { TextureModded = true; ALOTVersion = alotInfo.ToString(); if (alotInfo.MEUITMVER > 0) { MEUITMInstalled = true; MEUITMVersion = alotInfo.MEUITMVER; } } else { TextureModded = false; ALOTVersion = null; MEUITMInstalled = false; MEUITMVersion = 0; } CLog.Information(@"Getting game source for target " + TargetPath, lodUpdateAndLogging); var hashCheckResult = VanillaDatabaseService.GetGameSource(this); GameSource = hashCheckResult.result; ExecutableHash = hashCheckResult.hash; if (GameSource == null) { CLog.Error(@"Unknown source or illegitimate installation: " + hashCheckResult.hash, lodUpdateAndLogging); } else { if (GameSource.Contains(@"Origin") && Game == Mod.MEGame.ME3) { // Check for steam if (Directory.Exists(Path.Combine(TargetPath, @"__overlay"))) { GameSource += @" (Steam version)"; } } CLog.Information(@"Source: " + GameSource, lodUpdateAndLogging); } IsPolishME1 = Game == Mod.MEGame.ME1 && File.Exists(Path.Combine(TargetPath, @"BioGame", @"CookedPC", @"Movies", @"niebieska_pl.bik")); if (IsPolishME1) { CLog.Information(@"ME1 Polish Edition detected", lodUpdateAndLogging); } if (RegistryActive && (Settings.AutoUpdateLODs2K || Settings.AutoUpdateLODs4K) && oldTMOption != TextureModded && (lodUpdateAndLogging || forceLodUpdate)) { UpdateLODs(Settings.AutoUpdateLODs2K); } } else { Log.Error($@"Target is invalid: {TargetPath} does not exist (or is not accessible)"); IsValid = false; //set to false if target becomes invalid } } }
/// <summary> /// Checks mods for updates. ForceUpdateCheck will force the mod to validate against the server (essentially repair mode). It is not used for rate limiting! /// </summary> /// <param name="modsToCheck">Mods to have server send information about</param> /// <param name="forceUpdateCheck">Force update check regardless of version</param> /// <returns></returns> public static List <ModUpdateInfo> CheckForModUpdates(List <Mod> modsToCheck, bool forceUpdateCheck) { string updateFinalRequest = UpdaterServiceManifestEndpoint; bool first = true; foreach (var mod in modsToCheck) { if (mod.ModModMakerID > 0) { //Modmaker style if (first) { updateFinalRequest += "?"; first = false; } else { updateFinalRequest += "&"; } updateFinalRequest += "modmakerupdatecode[]=" + mod.ModModMakerID; } else if (mod.ModClassicUpdateCode > 0) { //Classic style if (first) { updateFinalRequest += "?"; first = false; } else { updateFinalRequest += "&"; } updateFinalRequest += "classicupdatecode[]=" + mod.ModClassicUpdateCode; } else if (mod.NexusModID > 0) { //Nexus style if (first) { updateFinalRequest += "?"; first = false; } else { updateFinalRequest += "&"; } updateFinalRequest += "nexusupdatecode[]=" + mod.Game.ToString().Substring(2) + "-" + mod.NexusModID; } //else if (mod.NexusModID > 0) //{ // //Classic style // if (first) // { // updateFinalRequest += "?"; // first = false; // } // else // { // updateFinalRequest += "&"; // } // updateFinalRequest += "nexusupdatecode[]=" + mod.ModClassicUpdateCode; //} } using var wc = new System.Net.WebClient(); try { Debug.WriteLine(updateFinalRequest); string updatexml = wc.DownloadStringAwareOfEncoding(updateFinalRequest); XElement rootElement = XElement.Parse(updatexml); #region classic mods var modUpdateInfos = new List <ModUpdateInfo>(); var classicUpdateInfos = (from e in rootElement.Elements("mod") select new ModUpdateInfo { changelog = (string)e.Attribute("changelog"), versionstr = (string)e.Attribute("version"), updatecode = (int)e.Attribute("updatecode"), serverfolder = (string)e.Attribute("folder"), sourceFiles = (from f in e.Elements("sourcefile") select new SourceFile { lzmahash = (string)f.Attribute("lzmahash"), hash = (string)f.Attribute("hash"), size = (int)f.Attribute("size"), lzmasize = (int)f.Attribute("lzmasize"), relativefilepath = f.Value, timestamp = (Int64?)f.Attribute("timestamp") ?? (Int64)0 }).ToList(), blacklistedFiles = e.Elements("blacklistedfile").Select(x => x.Value).ToList() }).ToList(); foreach (var modUpdateInfo in classicUpdateInfos) { modUpdateInfo.ResolveVersionVar(); //Calculate update information var matchingMod = modsToCheck.FirstOrDefault(x => x.ModClassicUpdateCode == modUpdateInfo.updatecode); if (matchingMod != null && (forceUpdateCheck || matchingMod.ParsedModVersion < modUpdateInfo.version)) { modUpdateInfo.mod = matchingMod; modUpdateInfo.SetLocalizedInfo(); string modBasepath = matchingMod.ModPath; foreach (var serverFile in modUpdateInfo.sourceFiles) { var localFile = Path.Combine(modBasepath, serverFile.relativefilepath); if (File.Exists(localFile)) { var info = new FileInfo(localFile); if (info.Length != serverFile.size) { modUpdateInfo.applicableUpdates.Add(serverFile); } else { //Check hash CLog.Information("Hashing file for update check: " + localFile, Settings.LogModUpdater); var md5 = Utilities.CalculateMD5(localFile); if (md5 != serverFile.hash) { modUpdateInfo.applicableUpdates.Add(serverFile); } } } else { modUpdateInfo.applicableUpdates.Add(serverFile); } } foreach (var blacklistedFile in modUpdateInfo.blacklistedFiles) { var localFile = Path.Combine(modBasepath, blacklistedFile); if (File.Exists(localFile)) { Log.Information(@"Blacklisted file marked for deletion: " + localFile); modUpdateInfo.filesToDelete.Add(localFile); } } //Files to remove calculation var modFiles = Directory.GetFiles(modBasepath, "*", SearchOption.AllDirectories).Select(x => x.Substring(modBasepath.Length + 1)).ToList(); modUpdateInfo.filesToDelete.AddRange(modFiles.Except(modUpdateInfo.sourceFiles.Select(x => x.relativefilepath), StringComparer.InvariantCultureIgnoreCase).Distinct().ToList()); //Todo: Add security check here to prevent malicious values modUpdateInfo.TotalBytesToDownload = modUpdateInfo.applicableUpdates.Sum(x => x.lzmasize); } } modUpdateInfos.AddRange(classicUpdateInfos); #endregion #region modmaker mods var modmakerModUpdateInfos = (from e in rootElement.Elements("modmakermod") select new ModMakerModUpdateInfo { ModMakerId = (int)e.Attribute("id"), versionstr = (string)e.Attribute("version"), PublishDate = DateTime.ParseExact((string)e.Attribute("publishdate"), "yyyy-MM-dd", CultureInfo.InvariantCulture), changelog = (string)e.Attribute("changelog") }).ToList(); modUpdateInfos.AddRange(modmakerModUpdateInfos); #endregion #region Nexus Mod Third Party var nexusModsUpdateInfo = (from e in rootElement.Elements("nexusmod") select new NexusModUpdateInfo { NexusModsId = (int)e.Attribute("id"), GameId = (int)e.Attribute("game"), versionstr = (string)e.Attribute("version"), UpdatedTime = DateTimeOffset.FromUnixTimeSeconds((long)e.Attribute("updated_timestamp")).DateTime }).ToList(); modUpdateInfos.AddRange(nexusModsUpdateInfo); #endregion return(modUpdateInfos); } catch (Exception e) { Log.Error("Error checking for mod updates: " + App.FlattenException(e)); Crashes.TrackError(e, new Dictionary <string, string>() { { "Update check URL", updateFinalRequest } }); } return(null); }
/// <summary> /// Checks mods for updates. ForceUpdateCheck will force the mod to validate against the server (essentially repair mode). It is not used for rate limiting! /// </summary> /// <param name="modsToCheck">Mods to have server send information about</param> /// <param name="forceUpdateCheck">Force update check regardless of version</param> /// <returns></returns> public static List <ModUpdateInfo> CheckForModUpdates(List <Mod> modsToCheck, bool forceUpdateCheck, Action <string> updateStatusCallback = null) { string updateFinalRequest = UpdaterServiceManifestEndpoint; bool first = true; foreach (var mod in modsToCheck) { if (mod.ModModMakerID > 0) { //Modmaker style if (first) { updateFinalRequest += "?"; first = false; } else { updateFinalRequest += "&"; } updateFinalRequest += "modmakerupdatecode[]=" + mod.ModModMakerID; } else if (mod.ModClassicUpdateCode > 0) { //Classic style if (first) { updateFinalRequest += "?"; first = false; } else { updateFinalRequest += "&"; } updateFinalRequest += "classicupdatecode[]=" + mod.ModClassicUpdateCode; } else if (mod.NexusModID > 0 && mod.NexusUpdateCheck) { //Nexus style if (first) { updateFinalRequest += "?"; first = false; } else { updateFinalRequest += "&"; } updateFinalRequest += "nexusupdatecode[]=" + mod.Game.ToString().Substring(2) + "-" + mod.NexusModID; } //else if (mod.NexusModID > 0) //{ // //Classic style // if (first) // { // updateFinalRequest += "?"; // first = false; // } // else // { // updateFinalRequest += "&"; // } // updateFinalRequest += "nexusupdatecode[]=" + mod.ModClassicUpdateCode; //} } using var wc = new System.Net.WebClient(); try { Debug.WriteLine(updateFinalRequest); string updatexml = wc.DownloadStringAwareOfEncoding(updateFinalRequest); XElement rootElement = XElement.Parse(updatexml); #region classic mods var modUpdateInfos = new List <ModUpdateInfo>(); var classicUpdateInfos = (from e in rootElement.Elements("mod") select new ModUpdateInfo { changelog = (string)e.Attribute("changelog"), versionstr = (string)e.Attribute("version"), updatecode = (int)e.Attribute("updatecode"), serverfolder = (string)e.Attribute("folder"), sourceFiles = (from f in e.Elements("sourcefile") select new SourceFile { lzmahash = (string)f.Attribute("lzmahash"), hash = (string)f.Attribute("hash"), size = (int)f.Attribute("size"), lzmasize = (int)f.Attribute("lzmasize"), relativefilepath = f.Value, timestamp = (Int64?)f.Attribute("timestamp") ?? (Int64)0 }).ToList(), blacklistedFiles = e.Elements("blacklistedfile").Select(x => x.Value).ToList() }).ToList(); // CALCULATE UPDATE DELTA CaseInsensitiveDictionary <USFileInfo> hashMap = new CaseInsensitiveDictionary <USFileInfo>(); //Used to rename files foreach (var modUpdateInfo in classicUpdateInfos) { modUpdateInfo.ResolveVersionVar(); //Calculate update information var matchingMod = modsToCheck.FirstOrDefault(x => x.ModClassicUpdateCode == modUpdateInfo.updatecode); if (matchingMod != null && (forceUpdateCheck || matchingMod.ParsedModVersion < modUpdateInfo.version)) { // The following line is left so we know that it was at one point considered implemented. // This prevents updating copies of the same mod in the library. Cause it's just kind of a bandwidth waste. //modsToCheck.Remove(matchingMod); //This makes it so we don't feed in multiple same-mods. For example, nexus check on 3 Project Variety downloads modUpdateInfo.mod = matchingMod; modUpdateInfo.SetLocalizedInfo(); string modBasepath = matchingMod.ModPath; double i = 0; List <string> references = null; try { references = matchingMod.GetAllRelativeReferences(true); } catch { // There's an error. Underlying disk state may have changed since we originally loaded the mod } if (references == null || !matchingMod.ValidMod) { // The mod failed to load. We should just index everything the // references will not be dfully parsed. var localFiles = Directory.GetFiles(matchingMod.ModPath, "*", SearchOption.AllDirectories); references = localFiles.Select(x => x.Substring(matchingMod.ModPath.Length + 1)).ToList(); } int total = references.Count; // Index existing files foreach (var v in references) { updateStatusCallback?.Invoke( $"Indexing {modUpdateInfo.mod.ModName} for updates {(int)(i * 100 / total)}%"); i++; var fpath = Path.Combine(matchingMod.ModPath, v); if (fpath.RepresentsPackageFilePath()) { // We need to make sure it's decompressed var qPackage = MEPackageHandler.QuickOpenMEPackage(fpath); if (qPackage.IsCompressed) { CLog.Information( $" >> Decompressing compressed package for update comparison check: {fpath}", Settings.LogModUpdater); qPackage = MEPackageHandler.OpenMEPackage(fpath); MemoryStream tStream = new MemoryStream(); tStream = qPackage.SaveToStream(false); hashMap[v] = new USFileInfo() { MD5 = Utilities.CalculateMD5(tStream), CompressedMD5 = Utilities.CalculateMD5(fpath), Filesize = tStream.Length, RelativeFilepath = v }; continue; } } hashMap[v] = new USFileInfo() { MD5 = Utilities.CalculateMD5(fpath), Filesize = new FileInfo(fpath).Length, RelativeFilepath = v }; } i = 0; total = modUpdateInfo.sourceFiles.Count; foreach (var serverFile in modUpdateInfo.sourceFiles) { Log.Information($@"Checking {serverFile.relativefilepath} for update applicability"); updateStatusCallback?.Invoke( $"Calculating update delta for {modUpdateInfo.mod.ModName} {(int)(i * 100 / total)}%"); i++; bool calculatedOp = false; if (hashMap.TryGetValue(serverFile.relativefilepath, out var indexInfo)) { if (indexInfo.MD5 == serverFile.hash) { CLog.Information(@" >> File is up to date", Settings.LogModUpdater); calculatedOp = true; } else if (indexInfo.CompressedMD5 != null && indexInfo.CompressedMD5 == serverFile.hash) { CLog.Information(@" >> Compressed package file is up to date", Settings.LogModUpdater); calculatedOp = true; } } if (!calculatedOp) { // File is missing or hash was wrong. We should try to map it to another existing file // to save bandwidth var existingFilesThatMatchServerHash = hashMap.Where(x => x.Value.MD5 == serverFile.hash || (x.Value.CompressedMD5 != null && x.Value.CompressedMD5 == serverFile.hash)) .ToList(); if (existingFilesThatMatchServerHash.Any()) { CLog.Information( $" >> Server file can be cloned from local file {existingFilesThatMatchServerHash[0].Value.RelativeFilepath} as it has same hash", Settings.LogModUpdater); modUpdateInfo.cloneOperations[serverFile] = existingFilesThatMatchServerHash[0] .Value; // Server file can be sourced from the value } else if (indexInfo == null) { // we don't have file hashed (new file) CLog.Information( $" >> Applicable for updates, File does not exist locally", Settings.LogModUpdater); modUpdateInfo.applicableUpdates.Add(serverFile); } else { // Existing file has wrong hash CLog.Information($" >> Applicable for updates, hash has changed", Settings.LogModUpdater); modUpdateInfo.applicableUpdates.Add(serverFile); } } } foreach (var blacklistedFile in modUpdateInfo.blacklistedFiles) { var blLocalFile = Path.Combine(modBasepath, blacklistedFile); if (File.Exists(blLocalFile)) { Log.Information(@"Blacklisted file marked for deletion: " + blLocalFile); modUpdateInfo.filesToDelete.Add(blLocalFile); } } // alphabetize files modUpdateInfo.applicableUpdates.Sort(x => x.relativefilepath); //Files to remove calculation var modFiles = Directory.GetFiles(modBasepath, "*", SearchOption.AllDirectories) .Select(x => x.Substring(modBasepath.Length + 1)).ToList(); var additionalFilesToDelete = modFiles.Except( modUpdateInfo.sourceFiles.Select(x => x.relativefilepath), StringComparer.InvariantCultureIgnoreCase).Distinct().ToList(); modUpdateInfo.filesToDelete.AddRange( additionalFilesToDelete); //Todo: Add security check here to prevent malicious modUpdateInfo.TotalBytesToDownload = modUpdateInfo.applicableUpdates.Sum(x => x.lzmasize); } } modUpdateInfos.AddRange(classicUpdateInfos); #endregion #region modmaker mods var modmakerModUpdateInfos = (from e in rootElement.Elements("modmakermod") select new ModMakerModUpdateInfo { ModMakerId = (int)e.Attribute("id"), versionstr = (string)e.Attribute("version"), PublishDate = DateTime.ParseExact((string)e.Attribute("publishdate"), "yyyy-MM-dd", CultureInfo.InvariantCulture), changelog = (string)e.Attribute("changelog") }).ToList(); modUpdateInfos.AddRange(modmakerModUpdateInfos); #endregion #region Nexus Mod Third Party var nexusModsUpdateInfo = (from e in rootElement.Elements("nexusmod") select new NexusModUpdateInfo { NexusModsId = (int)e.Attribute("id"), GameId = (int)e.Attribute("game"), versionstr = (string)e.Attribute("version"), UpdatedTime = DateTimeOffset.FromUnixTimeSeconds((long)e.Attribute("updated_timestamp")) .DateTime }).ToList(); modUpdateInfos.AddRange(nexusModsUpdateInfo); #endregion return(modUpdateInfos); } catch (Exception e) { Log.Error("Error checking for mod updates: " + App.FlattenException(e)); Crashes.TrackError(e, new Dictionary <string, string>() { { "Update check URL", updateFinalRequest } }); } return(null); }
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; }
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; }