embed plugin loader directly into the launcher

This commit is contained in:
zznty
2022-10-29 01:50:14 +07:00
parent 7204815c0c
commit 66d3dc2ead
53 changed files with 5689 additions and 10 deletions

369
PluginLoader/PluginList.cs Normal file
View File

@@ -0,0 +1,369 @@
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;
}
}