using System.Collections; using System.Diagnostics.CodeAnalysis; using System.IO.Compression; using System.Xml.Serialization; using PluginLoader.Data; using PluginLoader.Network; using ProtoBuf; using ProtoBuf.Meta; namespace PluginLoader; public class PluginList : IEnumerable { private Dictionary plugins = new(); public PluginList(string mainDirectory, PluginConfig config) { var lbl = Main.Instance.Splash; lbl.SetText("Downloading plugin list..."); DownloadList(mainDirectory, config); if (plugins.Count == 0) { LogFile.Log.Warn("WARNING: No plugins in the plugin list. Plugin list will contain local plugins only."); HasError = true; } FindWorkshopPlugins(config); FindLocalPlugins(config, mainDirectory); LogFile.Log.Debug($"Found {plugins.Count} plugins"); FindPluginGroups(); FindModDependencies(); } public int Count => plugins.Count; public bool HasError { get; } public PluginData this[string key] { get => plugins[key]; set => plugins[key] = value; } public IEnumerator GetEnumerator() { return plugins.Values.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return plugins.Values.GetEnumerator(); } public bool Contains(string id) { return plugins.ContainsKey(id); } public bool TryGetPlugin(string id, out PluginData pluginData) { return plugins.TryGetValue(id, out pluginData); } /// /// Ensures the user is subscribed to the steam plugin. /// public void SubscribeToItem(string id) { if (plugins.TryGetValue(id, out var data) && data is ISteamItem steam) SteamAPI.SubscribeToItem(steam.WorkshopId); } public bool Remove(string id) { return plugins.Remove(id); } public void Add(PluginData data) { plugins[data.Id] = data; } private void FindPluginGroups() { var groups = 0; foreach (var group in plugins.Values.Where(x => !string.IsNullOrWhiteSpace(x.GroupId)).GroupBy(x => x.GroupId)) { groups++; foreach (var data in group) data.Group.AddRange(group.Where(x => x != data)); } if (groups > 0) LogFile.Log.Debug($"Found {groups} plugin groups"); } private void FindModDependencies() { foreach (var data in plugins.Values) if (data is ModPlugin mod) FindModDependencies(mod); } private void FindModDependencies(ModPlugin mod) { if (mod.DependencyIds == null) return; var dependencies = new Dictionary(); dependencies.Add(mod.WorkshopId, mod); var toProcess = new Stack(); toProcess.Push(mod); while (toProcess.Count > 0) { var temp = toProcess.Pop(); if (temp.DependencyIds == null) continue; foreach (var id in temp.DependencyIds) if (!dependencies.ContainsKey(id) && plugins.TryGetValue(id.ToString(), out var data) && data is ModPlugin dependency) { toProcess.Push(dependency); dependencies[id] = dependency; } } dependencies.Remove(mod.WorkshopId); mod.Dependencies = dependencies.Values.ToArray(); } private void DownloadList(string mainDirectory, PluginConfig config) { var whitelist = Path.Combine(mainDirectory, "whitelist.bin"); PluginData[] list; var currentHash = config.ListHash; string newHash; if (!TryDownloadWhitelistHash(out newHash)) { // No connection to plugin hub, read from cache if (!TryReadWhitelistFile(whitelist, out list)) return; } else if (currentHash == null || currentHash != newHash) { // Plugin list changed, try downloading new version first if (!TryDownloadWhitelistFile(whitelist, newHash, config, out list) && !TryReadWhitelistFile(whitelist, out list)) return; } else { // Plugin list did not change, try reading the current version first if (!TryReadWhitelistFile(whitelist, out list) && !TryDownloadWhitelistFile(whitelist, newHash, config, out list)) return; } if (list != null) plugins = list.ToDictionary(x => x.Id); } private bool TryReadWhitelistFile(string file, [NotNullWhen(true)] out PluginData[]? list) { list = null; if (File.Exists(file) && new FileInfo(file).Length > 0) { LogFile.Log.Debug("Reading whitelist from cache"); try { using (Stream binFile = File.OpenRead(file)) { list = Serializer.Deserialize(binFile); } LogFile.Log.Debug("Whitelist retrieved from disk"); return true; } catch (Exception e) { LogFile.Log.Warn(e, "Error while reading whitelist"); } } else { LogFile.Log.Debug("No whitelist cache exists"); } return false; } private bool TryDownloadWhitelistFile(string file, string hash, PluginConfig config, out PluginData[] list) { var newPlugins = new Dictionary(); try { using (var zipFileStream = GitHub.DownloadRepo(GitHub.listRepoName, GitHub.listRepoCommit, out _)) using (var zipFile = new ZipArchive(zipFileStream)) { var xml = new XmlSerializer(typeof(PluginData)); foreach (var entry in zipFile.Entries) { if (!entry.FullName.EndsWith("xml", StringComparison.OrdinalIgnoreCase)) continue; using var entryStream = entry.Open(); try { var data = (PluginData?)xml.Deserialize(entryStream) ?? throw new InvalidOperationException($"Deserialized data is null for {entry.FullName}"); newPlugins[data.Id] = data; } catch (InvalidOperationException e) { LogFile.Log.Error(e, "An error occurred while reading the plugin xml"); } } } list = newPlugins.Values.ToArray(); return TrySaveWhitelist(file, list, hash, config); } catch (Exception e) { LogFile.Log.Error(e, "Error while downloading whitelist"); throw; } } private bool TrySaveWhitelist(string file, PluginData[] list, string hash, PluginConfig config) { try { LogFile.Log.Debug("Saving whitelist to disk"); using (var binFile = File.Create(file)) { Serializer.Serialize(binFile, list); } config.ListHash = hash; config.Save(); LogFile.Log.Debug("Whitelist updated"); return true; } catch (Exception e) { LogFile.Log.Error(e, "Error while saving whitelist"); try { File.Delete(file); } catch { } return false; } } private bool TryDownloadWhitelistHash(out string hash) { hash = null; try { using (var hashStream = GitHub.DownloadFile(GitHub.listRepoName, GitHub.listRepoCommit, GitHub.listRepoHash)) using (var hashStreamReader = new StreamReader(hashStream)) { hash = hashStreamReader.ReadToEnd().Trim(); } return true; } catch (Exception e) { LogFile.Log.Debug("Error while downloading whitelist hash: " + e); return false; } } private void FindLocalPlugins(PluginConfig config, string mainDirectory) { foreach (var dll in Directory.EnumerateFiles(mainDirectory, "*.dll", SearchOption.AllDirectories)) if (!dll.Contains(Path.DirectorySeparatorChar + "GitHub" + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)) { var local = new LocalPlugin(dll); var name = local.FriendlyName; if (!name.StartsWith("0Harmony") && !name.StartsWith("Microsoft")) plugins[local.Id] = local; } foreach (var folderConfig in config.PluginFolders.Values) if (folderConfig.Valid) { var local = new LocalFolderPlugin(folderConfig); plugins[local.Id] = local; } } private void FindWorkshopPlugins(PluginConfig config) { var steamPlugins = new List(plugins.Values.Select(x => x as ISteamItem).Where(x => x != null)); Main.Instance.Splash?.SetText("Updating workshop items..."); SteamAPI.Update(steamPlugins.Where(x => config.IsEnabled(x.Id)).Select(x => x.WorkshopId)); var workshop = Path.GetFullPath(@"..\..\..\workshop\content\244850\"); foreach (var steam in steamPlugins) try { var path = Path.Combine(workshop, steam.Id); if (Directory.Exists(path)) { if (steam is SteamPlugin plugin && TryGetPlugin(path, out string dllFile)) plugin.Init(dllFile); } else if (config.IsEnabled(steam.Id)) { ((PluginData)steam).Status = PluginStatus.Error; LogFile.Log.Debug($"The plugin '{steam}' is missing and cannot be loaded."); } } catch (Exception e) { LogFile.Log.Debug($"An error occurred while searching for the workshop plugin {steam}: {e}"); } } private bool TryGetPlugin(string modRoot, out string pluginFile) { foreach (var file in Directory.EnumerateFiles(modRoot, "*.plugin")) { var name = Path.GetFileName(file); if (!name.StartsWith("0Harmony", StringComparison.OrdinalIgnoreCase)) { pluginFile = file; return true; } } var sepm = Path.Combine(modRoot, "Data", "sepm-plugin.zip"); if (File.Exists(sepm)) { pluginFile = sepm; return true; } pluginFile = null; return false; } }