embed plugin loader directly into the launcher
This commit is contained in:
369
PluginLoader/PluginList.cs
Normal file
369
PluginLoader/PluginList.cs
Normal 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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user