private static EverestModuleMetadata[] loadZip(string archive) { try { using (ZipFile zip = new ZipFile(archive)) { foreach (ZipEntry entry in zip.Entries) { if (entry.FileName == "metadata.yaml") { using (MemoryStream stream = entry.ExtractStream()) using (StreamReader reader = new StreamReader(stream)) { EverestModuleMetadata meta = YamlHelper.Deserializer.Deserialize <EverestModuleMetadata>(reader); meta.PathArchive = archive; meta.PostParse(); return(new EverestModuleMetadata[] { meta }); } } if (entry.FileName == "multimetadata.yaml" || entry.FileName == "everest.yaml" || entry.FileName == "everest.yml") { using (MemoryStream stream = entry.ExtractStream()) using (StreamReader reader = new StreamReader(stream)) { if (!reader.EndOfStream) { EverestModuleMetadata[] multimetas = YamlHelper.Deserializer.Deserialize <EverestModuleMetadata[]>(reader); foreach (EverestModuleMetadata multimeta in multimetas) { multimeta.PathArchive = archive; multimeta.PostParse(); } return(multimetas); } } } } } } catch (Exception e) { Logger.Log(LogLevel.Warn, "loader", $"Failed loading everest.yaml in archive {archive}: {e}"); } return(null); }
private static bool tryUnblacklist(EverestModuleMetadata dependency, Dictionary <EverestModuleMetadata, string> allModsInformation, HashSet <string> modsToUnblacklist) { KeyValuePair <EverestModuleMetadata, string> match = default; // let's find the most recent installed mod that has the required name. foreach (KeyValuePair <EverestModuleMetadata, string> candidate in allModsInformation) { if (dependency.Name == candidate.Key.Name && (match.Key == null || match.Key.Version < candidate.Key.Version)) { match = candidate; } } if (match.Key == null || !Everest.Loader.Blacklist.Contains(match.Value)) { // no result for this dependency (it isn't actually installed) or the mod isn't blacklisted (so it failed loading for some other reason). return(false); } if (modsToUnblacklist.Contains(match.Value)) { // STOP RIGHT HERE! we already are planning to unblacklist this dependency. No need to go further! return(true); } // this dependency will have to be unblacklisted. modsToUnblacklist.Add(match.Value); // unblacklist all dependencies for this dependency. if one of them isn't unblacklistable, that doesn't matter: it will fail to load // after restarting the game and it will be handled then (this case should be extremely rare anyway). foreach (EverestModuleMetadata dependencyDependency in match.Key.Dependencies) { if (!Everest.Loader.DependencyLoaded(dependencyDependency)) { tryUnblacklist(dependencyDependency, allModsInformation, modsToUnblacklist); } } // and we are done! return(true); }
/// <summary> /// Installs a mod update in the Mods directory once it has been downloaded. /// This method will replace the installed mod zip with the one that was just downloaded. /// </summary> /// <param name="update">The update info coming from the update server</param> /// <param name="mod">The mod metadata from Everest for the installed mod</param> /// <param name="zipPath">The path to the zip the update has been downloaded to</param> public static void InstallModUpdate(ModUpdateInfo update, EverestModuleMetadata mod, string zipPath) { // let's close the zip, as we will replace it now. foreach (ModContent content in Everest.Content.Mods) { if (content.GetType() == typeof(ZipModContent) && (content as ZipModContent).Path == mod.PathArchive) { ZipModContent modZip = content as ZipModContent; Logger.Log("ModUpdaterHelper", $"Closing mod .zip: {modZip.Path}"); modZip.Dispose(); } } // delete the old zip, and move the new one. Logger.Log("ModUpdaterHelper", $"Deleting mod .zip: {mod.PathArchive}"); File.Delete(mod.PathArchive); Logger.Log("ModUpdaterHelper", $"Moving {zipPath} to {mod.PathArchive}"); File.Move(zipPath, mod.PathArchive); }
/// <summary> /// List all mods needing an update, by comparing the installed mods' hashes with the ones in the update checker database. /// </summary> /// <param name="updateCatalog">The update checker database (must not be null!)</param> /// <returns>A map listing all the updates: info from the update checker database => info from the installed mod</returns> public static SortedDictionary <ModUpdateInfo, EverestModuleMetadata> ListAvailableUpdates(Dictionary <string, ModUpdateInfo> updateCatalog) { SortedDictionary <ModUpdateInfo, EverestModuleMetadata> availableUpdatesCatalog = new SortedDictionary <ModUpdateInfo, EverestModuleMetadata>(new MostRecentUpdatedFirst()); Logger.Log("ModUpdaterHelper", "Checking for updates"); foreach (EverestModule module in Everest.Modules) { EverestModuleMetadata metadata = module.Metadata; if (metadata.PathArchive != null && updateCatalog.ContainsKey(metadata.Name)) { string xxHashStringInstalled = BitConverter.ToString(metadata.Hash).Replace("-", "").ToLowerInvariant(); Logger.Log("ModUpdaterHelper", $"Mod {metadata.Name}: installed hash {xxHashStringInstalled}, latest hash(es) {string.Join(", ", updateCatalog[metadata.Name].xxHash)}"); if (!updateCatalog[metadata.Name].xxHash.Contains(xxHashStringInstalled)) { availableUpdatesCatalog.Add(updateCatalog[metadata.Name], metadata); } } } Logger.Log("ModUpdaterHelper", $"{availableUpdatesCatalog.Count} update(s) available"); return(availableUpdatesCatalog); }
private static void Load() { EverestModuleMetadata meta = CelesteTasModule.Instance.Metadata; try { watcher = new FileSystemWatcher { Path = Path.GetDirectoryName(meta.DLL), NotifyFilter = NotifyFilters.LastWrite, }; watcher.Changed += (s, e) => { if (e.FullPath == meta.DLL && Manager.Running) { Manager.DisableRun(); } }; watcher.EnableRaisingEvents = true; } catch (Exception e) { e.LogException($"Failed watching folder: {Path.GetDirectoryName(meta.DLL)}"); Unload(); } }
public override void Update() { if (menu != null && task != null && task.IsCompleted) { // there is no download or install task in progress if (fetchingButton != null) { // This means fetching the updates just finished. We have to remove the "Checking for updates" button // and put the actual update list instead. Logger.Log("OuiModUpdateList", "Rendering updates"); menu.Remove(fetchingButton); fetchingButton = null; if (updateCatalog == null) { // display an error message TextMenu.Button button = new TextMenu.Button(Dialog.Clean("MODUPDATECHECKER_ERROR")); button.Disabled = true; menu.Add(button); } else if (availableUpdatesCatalog.Count == 0) { // display a dummy "no update available" button TextMenu.Button button = new TextMenu.Button(Dialog.Clean("MODUPDATECHECKER_NOUPDATE")); button.Disabled = true; menu.Add(button); } else { // display one button per update foreach (ModUpdateInfo update in availableUpdatesCatalog.Keys) { EverestModuleMetadata metadata = availableUpdatesCatalog[update]; string versionUpdate = metadata.VersionString; if (metadata.VersionString != update.Version) { versionUpdate = $"{metadata.VersionString} > {update.Version}"; } TextMenu.Button button = new TextMenu.Button($"{metadata.Name.SpacedPascalCase()} | v. {versionUpdate} ({new DateTime(1970, 1, 1, 0, 0, 0, 0).AddSeconds(update.LastUpdate):yyyy-MM-dd})"); button.Pressed(() => { // make the menu non-interactive menu.Focused = false; button.Disabled = true; // trigger the update download downloadModUpdate(update, metadata, button); }); // if there is more than one hash, it means there is multiple downloads for this mod. Thus, we can't update it manually. if (update.xxHash.Count > 1) { button.Disabled = true; } menu.Add(button); } } } if (menu.Focused && Selected && Input.MenuCancel.Pressed) { if (shouldRestart) { Everest.QuickFullRestart(); } else { // go back to mod options instead Audio.Play(SFX.ui_main_button_back); Overworld.Goto <OuiModOptions>(); } } } base.Update(); }
private void ProcessGetModInfo() { if (Engine.Scene is Level level) { string MetaToString(EverestModuleMetadata metadata, int Indentation = 0, bool comment = true) { return((comment ? "# " : string.Empty) + string.Empty.PadLeft(Indentation) + $"{metadata.Name} {metadata.VersionString}\n"); } List <EverestModuleMetadata> metas = Everest.Modules .Where(module => module.Metadata.Name != "UpdateChecker" && module.Metadata.Name != "DialogCutscene") .Select(module => module.Metadata).ToList(); metas.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase)); AreaData areaData = AreaData.Get(level); string moduleName = string.Empty; EverestModuleMetadata mapMeta = null; if (Everest.Content.TryGet <AssetTypeMap>("Maps/" + areaData.SID, out ModAsset mapModAsset) && mapModAsset.Source != null) { moduleName = mapModAsset.Source.Name; mapMeta = metas.FirstOrDefault(meta => meta.Name == moduleName); } string command = ""; EverestModuleMetadata celesteMeta = metas.First(metadata => metadata.Name == "Celeste"); EverestModuleMetadata everestMeta = metas.First(metadata => metadata.Name == "Everest"); EverestModuleMetadata tasMeta = metas.First(metadata => metadata.Name == "CelesteTAS"); command += MetaToString(celesteMeta); command += MetaToString(everestMeta); command += MetaToString(tasMeta); metas.Remove(celesteMeta); metas.Remove(everestMeta); metas.Remove(tasMeta); EverestModuleMetadata speedrunToolMeta = metas.FirstOrDefault(metadata => metadata.Name == "SpeedrunTool"); if (speedrunToolMeta != null) { command += MetaToString(speedrunToolMeta); } command += "\n# Map:\n"; if (mapMeta != null) { command += MetaToString(mapMeta, 2); } string mode = level.Session.Area.Mode == AreaMode.Normal ? "ASide" : level.Session.Area.Mode.ToString(); command += $"# {areaData.SID} {mode}\n"; if (!string.IsNullOrEmpty(moduleName) && mapMeta != null) { List <EverestModuleMetadata> dependencies = mapMeta.Dependencies.Where(metadata => metadata.Name != "Celeste" && metadata.Name != "Everest" && metadata.Name != "UpdateChecker" && metadata.Name != "DialogCutscene" && metadata.Name != "CelesteTAS").ToList(); dependencies.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase)); if (dependencies.Count > 0) { command += "\n# Dependencies:\n"; command += string.Join(string.Empty, dependencies.Select(meta => metas.First(metadata => metadata.Name == meta.Name)).Select(meta => MetaToString(meta, 2))); } command += "\n# Other Installed Mods:\n"; command += string.Join(string.Empty, metas.Where(meta => meta.Name != moduleName && dependencies.All(metadata => metadata.Name != meta.Name)) .Select(meta => MetaToString(meta, 2))); } else { command += "\n# Other Installed Mods:\n"; command += string.Join(string.Empty, metas.Select(meta => MetaToString(meta, 2))); } byte[] commandBytes = Encoding.Default.GetBytes(command); WriteMessageGuaranteed(new Message(MessageIDs.ReturnModInfo, commandBytes)); } }
public override IEnumerator Enter(Oui from) { menu = new TextMenu(); // display the title and a dummy "Fetching" button menu.Add(new TextMenu.Header(Dialog.Clean("UPDATECHECKER_MENU_TITLE"))); menu.Add(subHeader = new TextMenuExt.SubHeaderExt(Dialog.Clean("UPDATECHECKER_MENU_HEADER"))); fetchingButton = new TextMenu.Button(Dialog.Clean("UPDATECHECKER_FETCHING")); fetchingButton.Disabled = true; menu.Add(fetchingButton); Scene.Add(menu); menu.Visible = Visible = true; menu.Focused = false; for (float p = 0f; p < 1f; p += Engine.DeltaTime * 4f) { menu.X = offScreenX + -1920f * Ease.CubeOut(p); alpha = Ease.CubeOut(p); yield return(null); } menu.Focused = true; task = new Task(() => { // 1. Download the updates list Logger.Log("UpdateChecker", "Downloading last versions list"); try { using (WebClient wc = new WebClient()) { string yamlData = wc.DownloadString("https://max480-random-stuff.appspot.com/celeste/everest_update.yaml"); updateCatalog = new Deserializer().Deserialize <Dictionary <string, ModUpdateInfo> >(yamlData); foreach (string name in updateCatalog.Keys) { updateCatalog[name].Name = name; } Logger.Log("UpdateChecker", $"Downloaded {updateCatalog.Count} item(s)"); } } catch (Exception e) { Logger.Log("UpdateChecker", $"Download failed! {e.ToString()}"); } // 2. Find out what actually has been updated availableUpdatesCatalog.Clear(); if (updateCatalog != null) { Logger.Log("UpdateChecker", "Checking for updates"); foreach (EverestModule module in Everest.Modules) { EverestModuleMetadata metadata = module.Metadata; if (metadata.PathArchive != null && updateCatalog.ContainsKey(metadata.Name)) { string xxHashStringInstalled = BitConverter.ToString(metadata.Hash).Replace("-", "").ToLowerInvariant(); Logger.Log("UpdateChecker", $"Mod {metadata.Name}: installed hash {xxHashStringInstalled}, latest hash(es) {string.Join(", ", updateCatalog[metadata.Name].xxHash)}"); if (!updateCatalog[metadata.Name].xxHash.Contains(xxHashStringInstalled)) { availableUpdatesCatalog.Add(updateCatalog[metadata.Name], metadata); } } } Logger.Log("UpdateChecker", $"{availableUpdatesCatalog.Count} update(s) available"); } }); task.Start(); }
private void downloadModUpdate(ModUpdateInfo update, EverestModuleMetadata mod, TextMenu.Button button) { task = new Task(() => { // we will download the mod to Celeste_Directory/mod-update.zip at first. string zipPath = Path.Combine(Everest.PathGame, "mod-update.zip"); try { // download it... button.Label = $"{update.Name} ({Dialog.Clean("UPDATECHECKER_DOWNLOADING")})"; downloadMod(update, button, zipPath); // verify its checksum string actualHash = BitConverter.ToString(Everest.GetChecksum("mod-update.zip")).Replace("-", "").ToLowerInvariant(); string expectedHash = update.xxHash[0]; Logger.Log("UpdateChecker", $"Verifying checksum: actual hash is {actualHash}, expected hash is {expectedHash}"); if (expectedHash != actualHash) { throw new IOException($"Checksum error: expected {expectedHash}, got {actualHash}"); } // mark restarting as required, as we will do weird stuff like closing zips afterwards. if (!shouldRestart) { shouldRestart = true; subHeader.TextColor = Color.OrangeRed; subHeader.Title = $"{Dialog.Clean("UPDATECHECKER_MENU_HEADER")} ({Dialog.Clean("UPDATECHECKER_WILLRESTART")})"; } // install it button.Label = $"{update.Name} ({Dialog.Clean("UPDATECHECKER_INSTALLING")})"; installMod(update, mod, zipPath); // done! button.Label = $"{update.Name} ({Dialog.Clean("UPDATECHECKER_UPDATED")})"; // select another enabled option: the next one, or the last one if there is no next one. if (menu.Selection + 1 > menu.LastPossibleSelection) { menu.Selection = menu.LastPossibleSelection; } else { menu.Selection++; } } catch (Exception e) { // update failed button.Label = $"{update.Name} ({Dialog.Clean("UPDATECHECKER_FAILED")})"; Logger.Log("UpdateChecker", $"Updating {update.Name} failed"); Logger.LogDetailed(e); button.Disabled = false; // try to delete mod-update.zip if it still exists. if (File.Exists(zipPath)) { try { Logger.Log("UpdateChecker", $"Deleting temp file {zipPath}"); File.Delete(zipPath); } catch (Exception) { Logger.Log("UpdateChecker", $"Removing {zipPath} failed"); } } } // give the menu control back to the player menu.Focused = true; }); task.Start(); }
private void downloadAllDependencies() { // 1. Compute the list of dependencies we must download. LogLine(Dialog.Clean("DEPENDENCYDOWNLOADER_DOWNLOADING_DATABASE")); Dictionary <string, ModUpdateInfo> availableDownloads = ModUpdaterHelper.DownloadModUpdateList(); if (availableDownloads == null) { shouldAutoRestart = false; shouldRestart = false; LogLine(Dialog.Clean("DEPENDENCYDOWNLOADER_DOWNLOAD_DATABASE_FAILED")); } else { Logger.Log("OuiDependencyDownloader", "Computing dependencies to download..."); // these mods are not installed currently, we will install them Dictionary <string, ModUpdateInfo> modsToInstall = new Dictionary <string, ModUpdateInfo>(); // these mods are already installed, but need an update to satisfy the dependency Dictionary <string, ModUpdateInfo> modsToUpdate = new Dictionary <string, ModUpdateInfo>(); Dictionary <string, EverestModuleMetadata> modsToUpdateCurrentVersions = new Dictionary <string, EverestModuleMetadata>(); // Everest should be updated to satisfy a dependency on Everest bool shouldUpdateEverest = false; // these mods are absent from the database HashSet <string> modsNotFound = new HashSet <string>(); // these mods have multiple downloads, and as such, should be installed manually HashSet <string> modsNotInstallableAutomatically = new HashSet <string>(); // these mods are blacklisted, and should be removed from the blacklist instead of being re-installed HashSet <string> modsBlacklisted = new HashSet <string>(); // these mods are in the database, but the version found in there won't satisfy the dependency Dictionary <string, HashSet <Version> > modsWithIncompatibleVersionInDatabase = new Dictionary <string, HashSet <Version> >(); Dictionary <string, string> modsDatabaseVersions = new Dictionary <string, string>(); foreach (EverestModuleMetadata dependency in MissingDependencies) { if (Everest.Loader.Delayed.Any(delayedMod => dependency.Name == delayedMod.Item1.Name)) { Logger.Log("OuiDependencyDownloader", $"{dependency.Name} is installed but failed to load, skipping"); } else if (dependency.Name == "Everest") { Logger.Log("OuiDependencyDownloader", $"Everest should be updated"); shouldUpdateEverest = true; shouldAutoRestart = false; // TODO: maybe check more precisely for blacklisted mods? We're only basing ourselves on the name here. } else if (Everest.Loader.Blacklist.Contains($"{dependency.Name}.zip")) { Logger.Log("OuiDependencyDownloader", $"{dependency.Name} is blacklisted, and should be unblacklisted instead"); modsBlacklisted.Add(dependency.Name); shouldAutoRestart = false; } else if (!availableDownloads.ContainsKey(dependency.Name)) { Logger.Log("OuiDependencyDownloader", $"{dependency.Name} was not found in the database"); modsNotFound.Add(dependency.Name); shouldAutoRestart = false; } else if (availableDownloads[dependency.Name].xxHash.Count > 1) { Logger.Log("OuiDependencyDownloader", $"{dependency.Name} has multiple versions and cannot be installed automatically"); modsNotInstallableAutomatically.Add(dependency.Name); shouldAutoRestart = false; } else if (!isVersionCompatible(dependency.Version, availableDownloads[dependency.Name].Version)) { Logger.Log("OuiDependencyDownloader", $"{dependency.Name} has a version in database ({availableDownloads[dependency.Name].Version}) that would not satisfy dependency ({dependency.Version})"); // add the required version to the list of versions for this mod HashSet <Version> requiredVersions = modsWithIncompatibleVersionInDatabase.TryGetValue(dependency.Name, out HashSet <Version> result) ? result : new HashSet <Version>(); requiredVersions.Add(dependency.Version); modsWithIncompatibleVersionInDatabase[dependency.Name] = requiredVersions; modsDatabaseVersions[dependency.Name] = availableDownloads[dependency.Name].Version; shouldAutoRestart = false; } else { EverestModuleMetadata installedVersion = null; foreach (EverestModule module in Everest.Modules) { // note: if the mod is installed, but not as a zip, this will be treated as a fresh install rather than an update. // this is fine since zips take the priority over directories, and we cannot update directory mods anyway. if (module.Metadata.PathArchive != null && module.Metadata.Name == dependency.Name) { installedVersion = module.Metadata; break; } } if (installedVersion != null) { Logger.Log("OuiDependencyDownloader", $"{dependency.Name} is already installed and will be updated"); modsToUpdate[dependency.Name] = availableDownloads[dependency.Name]; modsToUpdateCurrentVersions[dependency.Name] = installedVersion; } else { Logger.Log("OuiDependencyDownloader", $"{dependency.Name} will be installed"); modsToInstall[dependency.Name] = availableDownloads[dependency.Name]; } } } // actually install the mods now foreach (ModUpdateInfo modToInstall in modsToInstall.Values) { downloadDependency(modToInstall, null); } foreach (ModUpdateInfo modToUpdate in modsToUpdate.Values) { downloadDependency(modToUpdate, modsToUpdateCurrentVersions[modToUpdate.Name]); } // display all mods that couldn't be accounted for if (shouldUpdateEverest) { LogLine(Dialog.Clean("DEPENDENCYDOWNLOADER_MUST_UPDATE_EVEREST")); } foreach (string mod in modsNotFound) { LogLine(string.Format(Dialog.Get("DEPENDENCYDOWNLOADER_MOD_NOT_FOUND"), mod)); } foreach (string mod in modsNotInstallableAutomatically) { LogLine(string.Format(Dialog.Get("DEPENDENCYDOWNLOADER_MOD_NOT_AUTO_INSTALLABLE"), mod)); } foreach (string mod in modsBlacklisted) { LogLine(string.Format(Dialog.Get("DEPENDENCYDOWNLOADER_MOD_BLACKLISTED"), mod)); } foreach (string mod in modsWithIncompatibleVersionInDatabase.Keys) { LogLine(string.Format(Dialog.Get("DEPENDENCYDOWNLOADER_MOD_WRONG_VERSION"), mod, string.Join(", ", modsWithIncompatibleVersionInDatabase[mod]), modsDatabaseVersions[mod])); } } Progress = 1; ProgressMax = 1; if (shouldAutoRestart) { // there are no errors to display: restart automatically LogLine(Dialog.Clean("DEPENDENCYDOWNLOADER_RESTARTING")); for (int i = 3; i > 0; --i) { Lines[Lines.Count - 1] = string.Format(Dialog.Get("DEPENDENCYDOWNLOADER_RESTARTING_IN"), i); Thread.Sleep(1000); } Lines[Lines.Count - 1] = Dialog.Clean("DEPENDENCYDOWNLOADER_RESTARTING"); Everest.QuickFullRestart(); } else if (shouldRestart) { LogLine("\n" + Dialog.Clean("DEPENDENCYDOWNLOADER_PRESS_BACK_TO_RESTART")); } else { LogLine("\n" + Dialog.Clean("DEPENDENCYDOWNLOADER_PRESS_BACK_TO_GO_BACK")); } }
private void downloadDependency(ModUpdateInfo mod, EverestModuleMetadata installedVersion) { string downloadDestination = Path.Combine(Everest.PathGame, $"dependency-download.zip"); try { // 1. Download LogLine(string.Format(Dialog.Get("DEPENDENCYDOWNLOADER_DOWNLOADING"), mod.Name, mod.URL)); LogLine("", false); Everest.Updater.DownloadFileWithProgress(mod.URL, downloadDestination, (position, length, speed) => { if (length > 0) { Lines[Lines.Count - 1] = $"{((int) Math.Floor(100D * (position / (double) length)))}% @ {speed} KiB/s"; Progress = position; ProgressMax = (int)length; } else { Lines[Lines.Count - 1] = $"{((int) Math.Floor(position / 1000D))}KiB @ {speed} KiB/s"; ProgressMax = 0; } }); ProgressMax = 0; Lines[Lines.Count - 1] = Dialog.Clean("DEPENDENCYDOWNLOADER_DOWNLOAD_FINISHED"); // 2. Verify checksum LogLine(Dialog.Clean("DEPENDENCYDOWNLOADER_VERIFYING_CHECKSUM")); ModUpdaterHelper.VerifyChecksum(mod, downloadDestination); // 3. Install mod shouldRestart = true; if (installedVersion != null) { LogLine(string.Format(Dialog.Get("DEPENDENCYDOWNLOADER_UPDATING"), mod.Name, installedVersion.Version, mod.Version, installedVersion.PathArchive)); ModUpdaterHelper.InstallModUpdate(mod, installedVersion, downloadDestination); } else { string installDestination = Path.Combine(Everest.Loader.PathMods, $"{mod.Name}.zip"); LogLine(string.Format(Dialog.Get("DEPENDENCYDOWNLOADER_INSTALLING"), mod.Name, mod.Version, installDestination)); File.Move(downloadDestination, installDestination); } } catch (Exception e) { // install failed LogLine(string.Format(Dialog.Get("DEPENDENCYDOWNLOADER_INSTALL_FAILED"), mod.Name)); Logger.LogDetailed(e); shouldAutoRestart = false; // try to delete the file if it still exists. if (File.Exists(downloadDestination)) { try { Logger.Log("OuiDependencyDownloader", $"Deleting temp file {downloadDestination}"); File.Delete(downloadDestination); } catch (Exception) { Logger.Log("OuiDependencyDownloader", $"Removing {downloadDestination} failed"); } } } }
public override void Update() { // check if the "press Back to restart" message has to be toggled if (menu != null && subHeader != null && (shouldRestart && menu.Focused) != willRestartMessageShown) { willRestartMessageShown = !willRestartMessageShown; if (willRestartMessageShown) { subHeader.TextColor = Color.OrangeRed; subHeader.Title = $"{Dialog.Clean("MODUPDATECHECKER_MENU_HEADER")} ({Dialog.Clean("MODUPDATECHECKER_WILLRESTART")})"; } else { subHeader.TextColor = Color.Gray; subHeader.Title = Dialog.Clean("MODUPDATECHECKER_MENU_HEADER"); } } if (menu != null && task != null && task.IsCompleted) { // there is no download or install task in progress if (fetchingButton != null) { // This means fetching the updates just finished. We have to remove the "Checking for updates" button // and put the actual update list instead. Logger.Log("OuiModUpdateList", "Rendering updates"); menu.Remove(fetchingButton); fetchingButton = null; if (updateCatalog == null) { // display an error message TextMenu.Button button = new TextMenu.Button(Dialog.Clean("MODUPDATECHECKER_ERROR")); button.Disabled = true; menu.Add(button); } else if (availableUpdatesCatalog.Count == 0) { // display a dummy "no update available" button TextMenu.Button button = new TextMenu.Button(Dialog.Clean("MODUPDATECHECKER_NOUPDATE")); button.Disabled = true; menu.Add(button); } else { // if there are multiple updates... if (availableUpdatesCatalog.Count > 1) { // display an "update all" button at the top of the list updateAllButton = new TextMenu.Button(Dialog.Clean("MODUPDATECHECKER_UPDATE_ALL")); updateAllButton.Pressed(() => downloadAllMods()); menu.Add(updateAllButton); } // then, display one button per update foreach (ModUpdateInfo update in availableUpdatesCatalog.Keys) { EverestModuleMetadata metadata = availableUpdatesCatalog[update]; string versionUpdate = metadata.VersionString; if (metadata.VersionString != update.Version) { versionUpdate = $"{metadata.VersionString} > {update.Version}"; } TextMenu.Button button = new TextMenu.Button($"{metadata.Name.SpacedPascalCase()} | v. {versionUpdate} ({new DateTime(1970, 1, 1, 0, 0, 0, 0).AddSeconds(update.LastUpdate):yyyy-MM-dd})"); button.Pressed(() => { // make the menu non-interactive menu.Focused = false; button.Disabled = true; // trigger the update download downloadModUpdate(update, metadata, button); }); // if there is more than one hash, it means there is multiple downloads for this mod. Thus, we can't update it manually. // if there isnt, add it to the list of mods that can be updated via "update all" if (update.xxHash.Count > 1) { button.Disabled = true; } else { updatableMods.Add(new ModUpdateHolder() { update = update, metadata = metadata, button = button }); } menu.Add(button); } } } if (menu.Focused && Selected && Input.MenuCancel.Pressed) { if (shouldRestart) { Everest.QuickFullRestart(); } else { // go back to mod options instead Audio.Play(SFX.ui_main_button_back); Overworld.Goto <OuiModOptions>(); } } } base.Update(); }
private void downloadAllDependencies() { // 1. Compute the list of dependencies we must download. LogLine(Dialog.Clean("DEPENDENCYDOWNLOADER_DOWNLOADING_DATABASE")); Everest.Updater.Entry everestVersionToInstall = null; Dictionary <string, ModUpdateInfo> availableDownloads = ModUpdaterHelper.DownloadModUpdateList(); if (availableDownloads == null) { shouldAutoExit = false; shouldRestart = false; LogLine(Dialog.Clean("DEPENDENCYDOWNLOADER_DOWNLOAD_DATABASE_FAILED")); } else { // load information on all installed mods, so that we can spot blacklisted ones easily. LogLine(Dialog.Clean("DEPENDENCYDOWNLOADER_LOADING_INSTALLED_MODS")); Progress = 0; ProgressMax = 100; Dictionary <string, EverestModuleMetadata[]> allModsInformationFlipped = OuiModToggler.LoadAllModYamls(progress => { Lines[Lines.Count - 1] = $"{Dialog.Clean("DEPENDENCYDOWNLOADER_LOADING_INSTALLED_MODS")} ({(int) (progress * 100)}%)"; Progress = (int)(progress * 100); }); ProgressMax = 0; // but flip around the mapping for convenience. Dictionary <EverestModuleMetadata, string> allModsInformation = new Dictionary <EverestModuleMetadata, string>(); foreach (KeyValuePair <string, EverestModuleMetadata[]> mods in allModsInformationFlipped) { foreach (EverestModuleMetadata mod in mods.Value) { allModsInformation[mod] = mods.Key; } } Lines[Lines.Count - 1] = $"{Dialog.Clean("DEPENDENCYDOWNLOADER_LOADING_INSTALLED_MODS")} {Dialog.Clean("DEPENDENCYDOWNLOADER_DONE")}"; Logger.Log("OuiDependencyDownloader", "Computing dependencies to download..."); // these mods are not installed currently, we will install them Dictionary <string, ModUpdateInfo> modsToInstall = new Dictionary <string, ModUpdateInfo>(); // these mods are already installed, but need an update to satisfy the dependency Dictionary <string, ModUpdateInfo> modsToUpdate = new Dictionary <string, ModUpdateInfo>(); Dictionary <string, EverestModuleMetadata> modsToUpdateCurrentVersions = new Dictionary <string, EverestModuleMetadata>(); // Everest should be updated to satisfy a dependency on Everest bool shouldUpdateEverestManually = false; // these mods are absent from the database HashSet <string> modsNotFound = new HashSet <string>(); // these mods have multiple downloads, and as such, should be installed manually HashSet <string> modsNotInstallableAutomatically = new HashSet <string>(); // these mods should be unblacklisted. HashSet <string> modFilenamesToUnblacklist = new HashSet <string>(); // these mods are in the database, but the version found in there won't satisfy the dependency Dictionary <string, HashSet <Version> > modsWithIncompatibleVersionInDatabase = new Dictionary <string, HashSet <Version> >(); Dictionary <string, string> modsDatabaseVersions = new Dictionary <string, string>(); foreach (EverestModuleMetadata dependency in MissingDependencies) { if (Everest.Loader.Delayed.Any(delayedMod => dependency.Name == delayedMod.Item1.Name)) { Logger.Log("OuiDependencyDownloader", $"{dependency.Name} is installed but failed to load, skipping"); } else if (dependency.Name == "Everest") { Logger.Log("OuiDependencyDownloader", $"Everest should be updated"); shouldAutoExit = false; if (dependency.Version.Major != 1 || dependency.Version.Build > 0 || dependency.Version.Revision > 0) { // the Everest version is not 1.XXX.0.0: Everest should be updated manually because this shouldn't happen. shouldUpdateEverestManually = true; } else if (!shouldUpdateEverestManually && (everestVersionToInstall == null || everestVersionToInstall.Build < dependency.Version.Minor)) { everestVersionToInstall = findEverestVersionToInstall(dependency.Version.Minor); if (everestVersionToInstall == null) { // a suitable version was not found! so, it should be installed manually. shouldUpdateEverestManually = true; } } } else if (tryUnblacklist(dependency, allModsInformation, modFilenamesToUnblacklist)) { Logger.Log("OuiDependencyDownloader", $"{dependency.Name} is blacklisted, and should be unblacklisted instead"); } else if (!availableDownloads.ContainsKey(dependency.Name)) { Logger.Log("OuiDependencyDownloader", $"{dependency.Name} was not found in the database"); modsNotFound.Add(dependency.Name); shouldAutoExit = false; } else if (availableDownloads[dependency.Name].xxHash.Count > 1) { Logger.Log("OuiDependencyDownloader", $"{dependency.Name} has multiple versions and cannot be installed automatically"); modsNotInstallableAutomatically.Add(dependency.Name); shouldAutoExit = false; } else if (!isVersionCompatible(dependency.Version, availableDownloads[dependency.Name].Version)) { Logger.Log("OuiDependencyDownloader", $"{dependency.Name} has a version in database ({availableDownloads[dependency.Name].Version}) that would not satisfy dependency ({dependency.Version})"); // add the required version to the list of versions for this mod HashSet <Version> requiredVersions = modsWithIncompatibleVersionInDatabase.TryGetValue(dependency.Name, out HashSet <Version> result) ? result : new HashSet <Version>(); requiredVersions.Add(dependency.Version); modsWithIncompatibleVersionInDatabase[dependency.Name] = requiredVersions; modsDatabaseVersions[dependency.Name] = availableDownloads[dependency.Name].Version; shouldAutoExit = false; } else { EverestModuleMetadata installedVersion = null; foreach (EverestModule module in Everest.Modules) { // note: if the mod is installed, but not as a zip, this will be treated as a fresh install rather than an update. // this is fine since zips take the priority over directories, and we cannot update directory mods anyway. if (module.Metadata.PathArchive != null && module.Metadata.Name == dependency.Name) { installedVersion = module.Metadata; break; } } if (installedVersion != null) { Logger.Log("OuiDependencyDownloader", $"{dependency.Name} is already installed and will be updated"); modsToUpdate[dependency.Name] = availableDownloads[dependency.Name]; modsToUpdateCurrentVersions[dependency.Name] = installedVersion; } else { Logger.Log("OuiDependencyDownloader", $"{dependency.Name} will be installed"); modsToInstall[dependency.Name] = availableDownloads[dependency.Name]; } } } // actually install the mods now foreach (ModUpdateInfo modToInstall in modsToInstall.Values) { downloadDependency(modToInstall, null); } foreach (ModUpdateInfo modToUpdate in modsToUpdate.Values) { downloadDependency(modToUpdate, modsToUpdateCurrentVersions[modToUpdate.Name]); } // unblacklist mods if this is needed if (modFilenamesToUnblacklist.Count > 0) { // remove the mods from blacklist.txt if (!unblacklistMods(modFilenamesToUnblacklist)) { // something bad happened shouldAutoExit = false; shouldRestart = true; } foreach (string modFilename in modFilenamesToUnblacklist) { try { LogLine(string.Format(Dialog.Get("DEPENDENCYDOWNLOADER_MOD_UNBLACKLIST"), modFilename)); // remove the mod from the loaded blacklist while (Everest.Loader._Blacklist.Contains(modFilename)) { Everest.Loader._Blacklist.Remove(modFilename); } // hot load the mod if (modFilename.EndsWith(".zip")) { Everest.Loader.LoadZip(Path.Combine(Everest.Loader.PathMods, modFilename)); } else { Everest.Loader.LoadDir(Path.Combine(Everest.Loader.PathMods, modFilename)); } } catch (Exception e) { // something bad happened during the mod hot loading, log it and prompt to restart the game to load the mod. LogLine(Dialog.Clean("DEPENDENCYDOWNLOADER_UNBLACKLIST_FAILED")); Logger.LogDetailed(e); shouldAutoExit = false; shouldRestart = true; break; } } } // display all mods that couldn't be accounted for if (shouldUpdateEverestManually) { LogLine(Dialog.Clean("DEPENDENCYDOWNLOADER_MUST_UPDATE_EVEREST")); } foreach (string mod in modsNotFound) { LogLine(string.Format(Dialog.Get("DEPENDENCYDOWNLOADER_MOD_NOT_FOUND"), mod)); } foreach (string mod in modsNotInstallableAutomatically) { LogLine(string.Format(Dialog.Get("DEPENDENCYDOWNLOADER_MOD_NOT_AUTO_INSTALLABLE"), mod)); } foreach (string mod in modsWithIncompatibleVersionInDatabase.Keys) { LogLine(string.Format(Dialog.Get("DEPENDENCYDOWNLOADER_MOD_WRONG_VERSION"), mod, string.Join(", ", modsWithIncompatibleVersionInDatabase[mod]), modsDatabaseVersions[mod])); } } Progress = 1; ProgressMax = 1; if (shouldAutoExit) { // there are no errors to display: restart automatically if (shouldRestart) { LogLine(Dialog.Clean("DEPENDENCYDOWNLOADER_RESTARTING")); for (int i = 3; i > 0; --i) { Lines[Lines.Count - 1] = string.Format(Dialog.Get("DEPENDENCYDOWNLOADER_RESTARTING_IN"), i); Thread.Sleep(1000); } Lines[Lines.Count - 1] = Dialog.Clean("DEPENDENCYDOWNLOADER_RESTARTING"); Everest.QuickFullRestart(); } else { Exit(); } } else if (everestVersionToInstall != null) { LogLine("\n" + string.Format(Dialog.Get("DEPENDENCYDOWNLOADER_EVEREST_UPDATE"), everestVersionToInstall.Build)); this.everestVersionToInstall = everestVersionToInstall; } else if (shouldRestart) { LogLine("\n" + Dialog.Clean("DEPENDENCYDOWNLOADER_PRESS_BACK_TO_RESTART")); } else { LogLine("\n" + Dialog.Clean("DEPENDENCYDOWNLOADER_PRESS_BACK_TO_GO_BACK")); } }