369 lines
11 KiB
C#
369 lines
11 KiB
C#
using System.Collections;
|
|
using System.IO.Compression;
|
|
using System.Xml.Serialization;
|
|
using PluginLoader.Data;
|
|
using PluginLoader.Network;
|
|
using ProtoBuf;
|
|
|
|
namespace PluginLoader;
|
|
|
|
public class PluginList : IEnumerable<PluginData>
|
|
{
|
|
private Dictionary<string, PluginData> 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.WriteLine("WARNING: No plugins in the plugin list. Plugin list will contain local plugins only.");
|
|
HasError = true;
|
|
}
|
|
|
|
FindWorkshopPlugins(config);
|
|
FindLocalPlugins(config, mainDirectory);
|
|
LogFile.WriteLine($"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<PluginData> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensures the user is subscribed to the steam plugin.
|
|
/// </summary>
|
|
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.WriteLine($"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<ulong, ModPlugin>();
|
|
dependencies.Add(mod.WorkshopId, mod);
|
|
var toProcess = new Stack<ModPlugin>();
|
|
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, out PluginData[] list)
|
|
{
|
|
list = null;
|
|
|
|
if (File.Exists(file) && new FileInfo(file).Length > 0)
|
|
{
|
|
LogFile.WriteLine("Reading whitelist from cache");
|
|
try
|
|
{
|
|
using (Stream binFile = File.OpenRead(file))
|
|
{
|
|
list = Serializer.Deserialize<PluginData[]>(binFile);
|
|
}
|
|
|
|
LogFile.WriteLine("Whitelist retrieved from disk");
|
|
return true;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
LogFile.WriteLine("Error while reading whitelist: " + e);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
LogFile.WriteLine("No whitelist cache exists");
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool TryDownloadWhitelistFile(string file, string hash, PluginConfig config, out PluginData[] list)
|
|
{
|
|
list = null;
|
|
var newPlugins = new Dictionary<string, PluginData>();
|
|
|
|
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())
|
|
using (var entryReader = new StreamReader(entryStream))
|
|
{
|
|
try
|
|
{
|
|
var data = (PluginData)xml.Deserialize(entryReader);
|
|
newPlugins[data.Id] = data;
|
|
}
|
|
catch (InvalidOperationException e)
|
|
{
|
|
LogFile.WriteLine("An error occurred while reading the plugin xml: " +
|
|
(e.InnerException ?? e));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
list = newPlugins.Values.ToArray();
|
|
return TrySaveWhitelist(file, list, hash, config);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
LogFile.WriteLine("Error while downloading whitelist: " + e);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool TrySaveWhitelist(string file, PluginData[] list, string hash, PluginConfig config)
|
|
{
|
|
try
|
|
{
|
|
LogFile.WriteLine("Saving whitelist to disk");
|
|
using (var mem = new MemoryStream())
|
|
{
|
|
Serializer.Serialize(mem, list);
|
|
using (Stream binFile = File.Create(file))
|
|
{
|
|
mem.WriteTo(binFile);
|
|
}
|
|
}
|
|
|
|
config.ListHash = hash;
|
|
config.Save();
|
|
|
|
LogFile.WriteLine("Whitelist updated");
|
|
return true;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
LogFile.WriteLine("Error while saving whitelist: " + e);
|
|
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.WriteLine("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<ISteamItem>(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.WriteLine($"The plugin '{steam}' is missing and cannot be loaded.");
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
LogFile.WriteLine($"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;
|
|
}
|
|
} |