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