using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.IO; using System.Linq; using System.Net; using System.Reflection; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace TBMM { partial class MainForm { public HashSet GetInstalledMods() { bool disabled = false; if (!Directory.Exists(GamePath("\\Plugins"))) if (Directory.Exists(GamePath("\\Plugins_Disabled"))) disabled = true; else return new HashSet(); var installed = new HashSet(); foreach (var modPath in Directory.GetFiles(GamePath(disabled ? @"\Plugins_Disabled" : @"\Plugins"), "*.dll")) { try { var an = AssemblyName.GetAssemblyName(modPath); if (an.Name == "0Harmony") continue; //Use filename to avoid differences between repository & assembly name casing var mod = new ModInfo { Name = Path.GetFileNameWithoutExtension(modPath), Version = an.Version }; AddUpdateModInList(mod); installed.Add(mod.Name); } catch (BadImageFormatException) { //Not a .NET assembly } } try { string ipath = GamePath("\\IPA.exe"); //Heh if (File.Exists(ipath)) { var an = AssemblyName.GetAssemblyName(ipath); gcipa.Version = an.Version; } } catch (BadImageFormatException) { //Not a .NET assembly } tbmm.Version = Assembly.GetExecutingAssembly().GetName().Version; return installed; } public async Task GetAvailableMods() { bool preview = GetExe()?.Contains("Preview") ?? false; //byte anyModOutdated = 0; using (var client = GetClient()) { string str = await client.DownloadStringTaskAsync("https://exmods.org/mods/modlist.tsv"); foreach (string line in str.Trim().Split('\n')) { var sp = line.Split('\t'); if (sp.Length < 2) continue; DateTime updated = default; bool broken = false; if (sp.Length > 2) { if (DateTime.TryParse(sp[2].Trim(), out var updatedAt)) updated = updatedAt; else if (sp[2].Trim().ToLower() == "broken") broken = true; } var mod = new ModInfo { Author = sp[0].Trim(), Name = sp[1].Trim(), LastUpdated = updated, Broken = broken }; if (await FetchModInfo(mod, preview, true)) //If it's actually a mod AddUpdateModInList(mod); } } if (tbmm.LatestVersion == null) //Only check once { await FetchModInfo(gcipa, preview, false); await FetchModInfo(tbmm, preview, false); if (tbmm.Updatable) if (MessageBox.Show("There is a TBMM update available! Do you want to download it now? If yes, extract it over this installation.\n\n" + tbmm.UpdateDetails, "Mod Manager update", MessageBoxButtons.YesNo) == DialogResult.Yes) Process.Start(tbmm.DownloadURL); } UpdatePlayButtonColor(); } public async Task FetchModInfo(ModInfo mod, bool preview, bool desc) { string repoURL = "/api/v1/repos/" + mod.Author + "/" + mod.Name + "/releases"; using (var client = GetClient()) { string str; try { str = await client.DownloadStringTaskAsync(repoURL); } catch (WebException) { return false; } var arr = JArray.Parse(str); var release = arr.FirstOrDefault(rel => { if ((bool) rel["prerelease"] || (bool) rel["draft"]) return false; var vs = rel["tag_name"].ToString(); int ind = vs.IndexOf('-'); if (ind != -1) { if (vs.Substring(ind + 1).Equals("preview", StringComparison.InvariantCultureIgnoreCase) && !preview) return false; } return true; }); if (release == null) return false; var verstr = release["tag_name"].ToString().Replace("v", ""); int index = verstr.IndexOf('-'); if (index != -1) verstr = verstr.Substring(0, index); JToken asset; if (release["assets"].Count() == 1) asset = release["assets"].First; else asset = release["assets"].FirstOrDefault(token => { string name = token["name"].ToString(); return name == mod.Name + ".dll" || name == mod.Name + ".zip"; }); mod.DownloadURL = asset?["browser_download_url"]?.ToString(); var lastUpdated = (DateTime)release["published_at"]; if (mod.LastUpdated < lastUpdated) mod.LastUpdated = lastUpdated; //If there's a newer release than the last known working date var ver = verstr.Split('.').Select(str => int.Parse(str)).ToArray(); int getver(byte i) => ver.Length > i ? ver[i] : 0; //By default it sets values not present to -1, but we need them to be 0 mod.LatestVersion = new Version(getver(0), getver(1), getver(2), getver(3)); mod.UpdateDetails = release["name"] + "\n\n" + release["body"]; if (desc) { try { var obj = JObject.Parse(await client.DownloadStringTaskAsync("/api/v1/repos/" + mod.Author + "/" + mod.Name + "/contents/README.md")); mod.Description = Encoding.UTF8.GetString(Convert.FromBase64String(obj["content"]?.ToString() ?? string.Empty)); } catch (WebException) { //It returns a HTTP 500 if it doesn't exist... } } return true; } } public void AddUpdateModInList(ModInfo mod) { if (mods.ContainsKey(mod.Name) ^ modlist.Items.ContainsKey(mod.Name)) //The ListView's keys aren't case sensitive throw new InvalidOperationException("The mod isn't present in one of the two places: " + mod.Name); ListViewItem item; if (modlist.Items.ContainsKey(mod.Name)) { var omod = mods[mod.Name]; item = modlist.Items[mod.Name]; var items = item.SubItems; omod.Author = mod.Author ?? omod.Author; omod.Version = mod.Version ?? omod.Version; //If the object comes from the dictionary then it's directly modified (uninstall) omod.LatestVersion = mod.LatestVersion ?? omod.LatestVersion; omod.LastUpdated = mod.LastUpdated == default ? omod.LastUpdated : mod.LastUpdated; omod.Description = mod.Description ?? omod.Description; omod.DownloadURL = mod.DownloadURL ?? omod.DownloadURL; omod.UpdateDetails = mod.UpdateDetails ?? omod.UpdateDetails; omod.Broken = mod.Broken ?? omod.Broken; items[1].Text = omod.Author ?? ""; items[2].Text = (omod.Version ?? omod.LatestVersion)?.ToString(); items[3].Text = omod.LatestVersion != null ? omod.LastUpdated.ToString() : ""; item.Group = omod.Installed ? modlist.Groups["installed"] : modlist.Groups["available"]; modlist.Sort(); mod = omod; } else { mods.Add(mod.Name, mod); item = new ListViewItem(new[] { mod.Name, mod.Author ?? "", (mod.Version ?? mod.LatestVersion)?.ToString() ?? "", mod.LatestVersion != null ? mod.LastUpdated.ToString() : "" }, modlist.Groups[mod.Installed ? "installed" : "available"]); item.Name = mod.Name; modlist.Items.Add(item); } if (mod.LatestVersion != null && mod.Version != null && mod.Version < mod.LatestVersion) item.ForeColor = Color.Blue; else if(mod.Broken is true) item.ForeColor = Color.Red; else if (mod.Outdated(lastGameUpdateTime)) item.ForeColor = Color.DarkOrange; else item.ForeColor = modlist.ForeColor; } public void CheckUninstalledMods(HashSet installed) { List delete = new List(); foreach (string name in mods.Keys.Except(installed)) { var mod = mods[name]; mod.Version = null; if (mod.Author != null) AddUpdateModInList(mod); else delete.Add(name); } delete.ForEach(name => { mods.Remove(name); modlist.Items[name].Remove(); }); UpdatePlayButtonColor(); } } }