diff --git a/Torch.API/Managers/IPluginManager.cs b/Torch.API/Managers/IPluginManager.cs
index 28caf87..2a2ee08 100644
--- a/Torch.API/Managers/IPluginManager.cs
+++ b/Torch.API/Managers/IPluginManager.cs
@@ -14,12 +14,12 @@ namespace Torch.API.Managers
///
/// Fired when plugins are loaded.
///
- event Action> PluginsLoaded;
+ event Action> PluginsLoaded;
///
/// Collection of loaded plugins.
///
- IList Plugins { get; }
+ IReadOnlyDictionary Plugins { get; }
///
/// Updates all loaded plugins.
diff --git a/Torch.API/Plugins/ITorchPlugin.cs b/Torch.API/Plugins/ITorchPlugin.cs
index 3e8f9aa..35a34df 100644
--- a/Torch.API/Plugins/ITorchPlugin.cs
+++ b/Torch.API/Plugins/ITorchPlugin.cs
@@ -17,7 +17,7 @@ namespace Torch.API.Plugins
///
/// The version of the plugin.
///
- Version Version { get; }
+ string Version { get; }
///
/// The name of the plugin.
diff --git a/Torch.API/Plugins/PluginAttribute.cs b/Torch.API/Plugins/PluginAttribute.cs
index 7b60e32..d8c1297 100644
--- a/Torch.API/Plugins/PluginAttribute.cs
+++ b/Torch.API/Plugins/PluginAttribute.cs
@@ -10,6 +10,7 @@ namespace Torch.API.Plugins
///
/// Indicates that the given type should be loaded by the plugin manager as a plugin.
///
+ [Obsolete("All plugin meta-information is now defined in the manifest.xml.")]
[AttributeUsage(AttributeTargets.Class)]
public class PluginAttribute : Attribute
{
diff --git a/Torch.Server/ViewModels/PluginManagerViewModel.cs b/Torch.Server/ViewModels/PluginManagerViewModel.cs
index ed43547..d910cf8 100644
--- a/Torch.Server/ViewModels/PluginManagerViewModel.cs
+++ b/Torch.Server/ViewModels/PluginManagerViewModel.cs
@@ -29,7 +29,7 @@ namespace Torch.Server.ViewModels
pluginManager.PluginsLoaded += PluginManager_PluginsLoaded;
}
- private void PluginManager_PluginsLoaded(IList obj)
+ private void PluginManager_PluginsLoaded(IReadOnlyCollection obj)
{
Plugins.Clear();
foreach (var plugin in obj)
diff --git a/Torch.Server/Views/PluginsControl.xaml b/Torch.Server/Views/PluginsControl.xaml
index a9f343e..f0bc7a4 100644
--- a/Torch.Server/Views/PluginsControl.xaml
+++ b/Torch.Server/Views/PluginsControl.xaml
@@ -12,7 +12,7 @@
-
+
@@ -27,7 +27,7 @@
-
+
diff --git a/Torch.Server/Views/PluginsControl.xaml.cs b/Torch.Server/Views/PluginsControl.xaml.cs
index 2f53d43..6b54f26 100644
--- a/Torch.Server/Views/PluginsControl.xaml.cs
+++ b/Torch.Server/Views/PluginsControl.xaml.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@@ -15,6 +16,8 @@ using System.Windows.Navigation;
using System.Windows.Shapes;
using NLog;
using Torch.API;
+using Torch.API.Managers;
+using Torch.Managers;
using Torch.Server.ViewModels;
namespace Torch.Server.Views
@@ -24,6 +27,9 @@ namespace Torch.Server.Views
///
public partial class PluginsControl : UserControl
{
+ private ITorchServer _server;
+ private PluginManager _plugins;
+
public PluginsControl()
{
InitializeComponent();
@@ -31,8 +37,15 @@ namespace Torch.Server.Views
public void BindServer(ITorchServer server)
{
- var pluginManager = new PluginManagerViewModel(server.Plugins);
+ _server = server;
+ _plugins = _server.Managers.GetManager();
+ var pluginManager = new PluginManagerViewModel(_plugins);
DataContext = pluginManager;
}
+
+ private void OpenFolder_OnClick(object sender, RoutedEventArgs e)
+ {
+ Process.Start("explorer.exe", _plugins.PluginDir);
+ }
}
}
diff --git a/Torch/DispatcherExtensions.cs b/Torch/Extensions/DispatcherExtensions.cs
similarity index 100%
rename from Torch/DispatcherExtensions.cs
rename to Torch/Extensions/DispatcherExtensions.cs
diff --git a/Torch/Extensions/ICollectionExtensions.cs b/Torch/Extensions/ICollectionExtensions.cs
new file mode 100644
index 0000000..acc952c
--- /dev/null
+++ b/Torch/Extensions/ICollectionExtensions.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+namespace Torch
+{
+ public static class ICollectionExtensions
+ {
+ ///
+ /// Returns a read-only wrapped
+ ///
+ public static IReadOnlyCollection AsReadOnly(this ICollection source)
+ {
+ if (source == null)
+ throw new ArgumentNullException(nameof(source));
+ return source as IReadOnlyCollection ?? new ReadOnlyCollectionAdapter(source);
+ }
+
+ ///
+ /// Returns a read-only wrapped
+ ///
+ public static IReadOnlyList AsReadOnly(this IList source)
+ {
+ if (source == null)
+ throw new ArgumentNullException(nameof(source));
+ return source as IReadOnlyList ?? new ReadOnlyCollection(source);
+ }
+
+ ///
+ /// Returns a read-only wrapped
+ ///
+ public static IReadOnlyDictionary AsReadOnly(this IDictionary source)
+ {
+ if (source == null)
+ throw new ArgumentNullException(nameof(source));
+ return source as IReadOnlyDictionary ?? new ReadOnlyDictionary(source);
+ }
+
+ sealed class ReadOnlyCollectionAdapter : IReadOnlyCollection
+ {
+ private readonly ICollection _source;
+
+ public ReadOnlyCollectionAdapter(ICollection source)
+ {
+ _source = source;
+ }
+
+ public int Count => _source.Count;
+ public IEnumerator GetEnumerator() => _source.GetEnumerator();
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Torch/Extensions/StringExtensions.cs b/Torch/Extensions/StringExtensions.cs
index 29c8a60..10e86d2 100644
--- a/Torch/Extensions/StringExtensions.cs
+++ b/Torch/Extensions/StringExtensions.cs
@@ -2,51 +2,21 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
+using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Torch
{
public static class StringExtensions
{
- public static string Truncate(this string s, int maxLength)
+ ///
+ /// Try to extract a 3 component version from the string. Format: #.#.#
+ ///
+ public static bool TryExtractVersion(this string version, out Version result)
{
- return s.Length <= maxLength ? s : s.Substring(0, maxLength);
- }
-
- public static IEnumerable ReadLines(this string s, int max, bool skipEmpty = false, char delim = '\n')
- {
- var lines = s.Split(delim);
-
- for (var i = 0; i < lines.Length && i < max; i++)
- {
- var l = lines[i];
- if (skipEmpty && string.IsNullOrWhiteSpace(l))
- continue;
-
- yield return l;
- }
- }
-
- public static string Wrap(this string s, int lineLength)
- {
- if (s.Length <= lineLength)
- return s;
-
- var result = new StringBuilder();
- for (var i = 0; i < s.Length;)
- {
- var next = i + lineLength;
- if (s.Length - 1 < next)
- {
- result.AppendLine(s.Substring(i));
- break;
- }
-
- result.AppendLine(s.Substring(i, next));
- i = next;
- }
-
- return result.ToString();
+ result = null;
+ var match = Regex.Match(version, @"(\d+\.)?(\d+\.)?(\d+)");
+ return match.Success && Version.TryParse(match.Value, out result);
}
}
}
diff --git a/Torch/Managers/PluginManager.cs b/Torch/Managers/PluginManager.cs
deleted file mode 100644
index 3f0108d..0000000
--- a/Torch/Managers/PluginManager.cs
+++ /dev/null
@@ -1,148 +0,0 @@
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Reflection;
-using System.Threading.Tasks;
-using NLog;
-using Torch.API;
-using Torch.API.Managers;
-using Torch.API.Plugins;
-using Torch.Commands;
-using VRage.Collections;
-
-namespace Torch.Managers
-{
- ///
- public class PluginManager : Manager, IPluginManager
- {
- private static Logger _log = LogManager.GetLogger(nameof(PluginManager));
- public readonly string PluginDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins");
- [Dependency]
- private UpdateManager _updateManager;
- [Dependency]
- private CommandManager _commandManager;
-
- ///
- public IList Plugins { get; } = new ObservableList();
-
- public event Action> PluginsLoaded;
-
- public PluginManager(ITorchBase torchInstance) : base(torchInstance)
- {
- if (!Directory.Exists(PluginDir))
- Directory.CreateDirectory(PluginDir);
- }
-
- ///
- /// Updates loaded plugins in parallel.
- ///
- public void UpdatePlugins()
- {
- foreach (var plugin in Plugins)
- plugin.Update();
- }
-
- ///
- /// Unloads all plugins.
- ///
- public override void Detach()
- {
- foreach (var plugin in Plugins)
- plugin.Dispose();
-
- Plugins.Clear();
- }
-
- private void DownloadPlugins()
- {
- var folders = Directory.GetDirectories(PluginDir);
- var taskList = new List();
-
- //Copy list because we don't want to modify the config.
- var toDownload = Torch.Config.Plugins.ToList();
-
- foreach (var folder in folders)
- {
- var manifestPath = Path.Combine(folder, "manifest.xml");
- if (!File.Exists(manifestPath))
- {
- _log.Debug($"No manifest in {folder}, skipping");
- continue;
- }
-
- var manifest = PluginManifest.Load(manifestPath);
- toDownload.RemoveAll(x => string.Compare(manifest.Repository, x, StringComparison.InvariantCultureIgnoreCase) == 0);
- taskList.Add(_updateManager.CheckAndUpdatePlugin(manifest));
- }
-
- foreach (var repository in toDownload)
- {
- var manifest = new PluginManifest {Repository = repository, Version = "0.0"};
- taskList.Add(_updateManager.CheckAndUpdatePlugin(manifest));
- }
-
- Task.WaitAll(taskList.ToArray());
- }
-
- ///
- public void LoadPlugins()
- {
- if (Torch.Config.ShouldUpdatePlugins)
- DownloadPlugins();
- else
- _log.Warn("Automatic plugin updates are disabled.");
-
- _log.Info("Loading plugins");
- var dlls = Directory.GetFiles(PluginDir, "*.dll", SearchOption.AllDirectories);
- foreach (var dllPath in dlls)
- {
- _log.Debug($"Loading plugin {dllPath}");
- var asm = Assembly.UnsafeLoadFrom(dllPath);
-
- foreach (var type in asm.GetExportedTypes())
- {
- if (type.GetInterfaces().Contains(typeof(ITorchPlugin)))
- {
- if (type.GetCustomAttribute() == null)
- continue;
-
- try
- {
- var plugin = (TorchPluginBase)Activator.CreateInstance(type);
- if (plugin.Id == default(Guid))
- throw new TypeLoadException($"Plugin '{type.FullName}' is missing a {nameof(PluginAttribute)}");
-
- _log.Info($"Loading plugin {plugin.Name} ({plugin.Version})");
- plugin.StoragePath = Torch.Config.InstancePath;
- Plugins.Add(plugin);
-
- _commandManager.RegisterPluginCommands(plugin);
- }
- catch (Exception e)
- {
- _log.Error($"Error loading plugin '{type.FullName}'");
- _log.Error(e);
- throw;
- }
- }
- }
- }
-
- Plugins.ForEach(p => p.Init(Torch));
- PluginsLoaded?.Invoke(Plugins.ToList());
- }
-
- public IEnumerator GetEnumerator()
- {
- return Plugins.GetEnumerator();
- }
-
- IEnumerator IEnumerable.GetEnumerator()
- {
- return GetEnumerator();
- }
- }
-}
diff --git a/Torch/Managers/UpdateManager.cs b/Torch/Managers/UpdateManager.cs
index 02d0a2a..62b8e25 100644
--- a/Torch/Managers/UpdateManager.cs
+++ b/Torch/Managers/UpdateManager.cs
@@ -44,7 +44,7 @@ namespace Torch.Managers
CheckAndUpdateTorch();
}
- private async Task> GetLatestRelease(string owner, string name)
+ private async Task> TryGetLatestArchiveUrl(string owner, string name)
{
try
{
@@ -53,56 +53,17 @@ namespace Torch.Managers
return new Tuple(new Version(), null);
var zip = latest.Assets.FirstOrDefault(x => x.Name.Contains(".zip"));
- var versionName = Regex.Match(latest.TagName, "(\\d+\\.)+\\d+").ToString();
- if (string.IsNullOrWhiteSpace(versionName))
- {
- _log.Warn("Unable to parse tag {0} for {1}/{2}", latest.TagName, owner, name);
- versionName = "0.0";
- }
- return new Tuple(new Version(versionName), zip?.BrowserDownloadUrl);
+ if (zip == null)
+ _log.Error($"Latest release of {owner}/{name} does not contain a zip archive.");
+ if (!latest.TagName.TryExtractVersion(out Version version))
+ _log.Error($"Unable to parse version tag for {owner}/{name}");
+ return new Tuple(version, zip?.BrowserDownloadUrl);
}
catch (Exception e)
{
_log.Error($"An error occurred getting release information for '{owner}/{name}'");
_log.Error(e);
- return new Tuple(new Version(), null);
- }
- }
-
- public async Task CheckAndUpdatePlugin(PluginManifest manifest)
- {
- if (!Torch.Config.GetPluginUpdates)
- return;
-
- try
- {
- var name = manifest.Repository.Split('/');
- if (name.Length != 2)
- {
- _log.Error($"'{manifest.Repository}' is not a valid GitHub repository.");
- return;
- }
-
- var currentVersion = new Version(manifest.Version);
- var releaseInfo = await GetLatestRelease(name[0], name[1]).ConfigureAwait(false);
- if (releaseInfo.Item1 > currentVersion)
- {
- _log.Warn($"Updating {manifest.Repository} from version {currentVersion} to version {releaseInfo.Item1}");
- var updateName = Path.Combine(_fsManager.TempDirectory, $"{name[0]}_{name[1]}.zip");
- var updatePath = Path.Combine(_torchDir, "Plugins");
- await new WebClient().DownloadFileTaskAsync(new Uri(releaseInfo.Item2), updateName).ConfigureAwait(false);
- UpdateFromZip(updateName, updatePath);
- File.Delete(updateName);
- }
- else
- {
- _log.Info($"{manifest.Repository} is up to date. ({currentVersion})");
- }
- }
- catch (Exception e)
- {
- _log.Error($"An error occured downloading the plugin update for {manifest.Repository}.");
- _log.Error(e);
+ return default(Tuple);
}
}
@@ -116,7 +77,7 @@ namespace Torch.Managers
try
{
- var releaseInfo = await GetLatestRelease("TorchAPI", "Torch").ConfigureAwait(false);
+ var releaseInfo = await TryGetLatestArchiveUrl("TorchAPI", "Torch").ConfigureAwait(false);
if (releaseInfo.Item1 > Torch.TorchVersion)
{
_log.Warn($"Updating Torch from version {Torch.TorchVersion} to version {releaseInfo.Item1}");
diff --git a/Torch/PluginManifest.cs b/Torch/PluginManifest.cs
deleted file mode 100644
index 6f13cb4..0000000
--- a/Torch/PluginManifest.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using System.Xml.Serialization;
-
-namespace Torch
-{
- public class PluginManifest
- {
- public string Repository { get; set; } = "Jimmacle/notarealrepo";
- public string Version { get; set; } = "1.0";
-
- public void Save(string path)
- {
- using (var f = File.OpenWrite(path))
- {
- var ser = new XmlSerializer(typeof(PluginManifest));
- ser.Serialize(f, this);
- }
- }
-
- public static PluginManifest Load(string path)
- {
- using (var f = File.OpenRead(path))
- {
- var ser = new XmlSerializer(typeof(PluginManifest));
- return (PluginManifest)ser.Deserialize(f);
- }
- }
- }
-}
diff --git a/Torch/Plugins/PluginManager.cs b/Torch/Plugins/PluginManager.cs
new file mode 100644
index 0000000..25eb10c
--- /dev/null
+++ b/Torch/Plugins/PluginManager.cs
@@ -0,0 +1,327 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Net;
+using System.Reflection;
+using System.Threading.Tasks;
+using System.Xml.Serialization;
+using NLog;
+using Octokit;
+using Torch.API;
+using Torch.API.Managers;
+using Torch.API.Plugins;
+using Torch.Collections;
+using Torch.Commands;
+
+namespace Torch.Managers
+{
+ ///
+ public class PluginManager : Manager, IPluginManager
+ {
+ private GitHubClient _gitClient = new GitHubClient(new ProductHeaderValue("Torch"));
+ private static Logger _log = LogManager.GetLogger(nameof(PluginManager));
+ private const string MANIFEST_NAME = "manifest.xml";
+ public readonly string PluginDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins");
+ private readonly ObservableDictionary _plugins = new ObservableDictionary();
+ [Dependency]
+ private CommandManager _commandManager;
+
+ ///
+ public IReadOnlyDictionary Plugins => _plugins.AsReadOnly();
+
+ public event Action> PluginsLoaded;
+
+ public PluginManager(ITorchBase torchInstance) : base(torchInstance)
+ {
+ if (!Directory.Exists(PluginDir))
+ Directory.CreateDirectory(PluginDir);
+ }
+
+ ///
+ /// Updates loaded plugins in parallel.
+ ///
+ public void UpdatePlugins()
+ {
+ foreach (var plugin in _plugins.Values)
+ plugin.Update();
+ }
+
+ ///
+ /// Unloads all plugins.
+ ///
+ public override void Detach()
+ {
+ foreach (var plugin in _plugins.Values)
+ plugin.Dispose();
+
+ _plugins.Clear();
+ }
+
+ public void LoadPlugins()
+ {
+ DownloadPluginUpdates();
+ _log.Info("Loading plugins...");
+ var pluginItems = Directory.EnumerateFiles(PluginDir, "*.zip").Union(Directory.EnumerateDirectories(PluginDir));
+ foreach (var item in pluginItems)
+ {
+ var path = Path.Combine(PluginDir, item);
+ var isZip = item.EndsWith(".zip", StringComparison.CurrentCultureIgnoreCase);
+ var manifest = isZip ? GetManifestFromZip(path) : GetManifestFromDirectory(path);
+ if (manifest == null)
+ {
+ _log.Warn($"Item '{item}' is missing a manifest, skipping.");
+ continue;
+ }
+
+ if (_plugins.ContainsKey(manifest.Guid))
+ {
+ _log.Error($"The GUID provided by {manifest.Name} ({item}) is already in use by {_plugins[manifest.Guid].Name}");
+ continue;
+ }
+
+ if (isZip)
+ LoadPluginFromZip(path);
+ else
+ LoadPluginFromFolder(path);
+ }
+
+ _plugins.ForEach(x => x.Value.Init(Torch));
+ _log.Info($"Loaded {_plugins.Count} plugins.");
+ PluginsLoaded?.Invoke(_plugins.Values.AsReadOnly());
+ }
+
+ private void DownloadPluginUpdates()
+ {
+ _log.Info("Checking for plugin updates...");
+ var count = 0;
+ var pluginItems = Directory.EnumerateFiles(PluginDir, "*.zip").Union(Directory.EnumerateDirectories(PluginDir));
+ Parallel.ForEach(pluginItems, async item =>
+ {
+ PluginManifest manifest = null;
+ try
+ {
+ var path = Path.Combine(PluginDir, item);
+ var isZip = item.EndsWith(".zip", StringComparison.CurrentCultureIgnoreCase);
+ manifest = isZip ? GetManifestFromZip(path) : GetManifestFromDirectory(path);
+ if (manifest == null)
+ {
+ _log.Warn($"Item '{item}' is missing a manifest, skipping update check.");
+ return;
+ }
+
+ manifest.Version.TryExtractVersion(out Version currentVersion);
+ var latest = await GetLatestArchiveAsync(manifest.Repository).ConfigureAwait(false);
+
+ if (currentVersion == null || latest.Item1 == null)
+ {
+ _log.Error($"Error parsing version from manifest or GitHub for plugin '{manifest.Name}.'");
+ return;
+ }
+
+ if (latest.Item1 <= currentVersion)
+ {
+ _log.Debug($"{manifest.Name} {manifest.Version} is up to date.");
+ return;
+ }
+
+ _log.Info($"Updating plugin '{manifest.Name}' from {currentVersion} to {latest.Item1}.");
+ await UpdatePluginAsync(path, latest.Item2).ConfigureAwait(false);
+ count++;
+ }
+ catch (Exception e)
+ {
+ _log.Error($"An error occurred updating the plugin {manifest.Name}.");
+ _log.Error(e);
+ }
+ });
+
+ _log.Info($"Updated {count} plugins.");
+ }
+
+ private async Task> GetLatestArchiveAsync(string repository)
+ {
+ try
+ {
+ var split = repository.Split('/');
+ var latest = await _gitClient.Repository.Release.GetLatest(split[0], split[1]).ConfigureAwait(false);
+ if (!latest.TagName.TryExtractVersion(out Version latestVersion))
+ {
+ _log.Error($"Unable to parse version tag for the latest release of '{repository}.'");
+ }
+
+ var zipAsset = latest.Assets.FirstOrDefault(x => x.Name.Contains(".zip", StringComparison.CurrentCultureIgnoreCase));
+ if (zipAsset == null)
+ {
+ _log.Error($"Unable to find archive for the latest release of '{repository}.'");
+ }
+
+ return new Tuple(latestVersion, zipAsset?.BrowserDownloadUrl);
+ }
+ catch (Exception e)
+ {
+ _log.Error($"Unable to get the latest release of '{repository}.'");
+ _log.Error(e);
+ return default(Tuple);
+ }
+ }
+
+ private Task UpdatePluginAsync(string localPath, string downloadUrl)
+ {
+ if (File.Exists(localPath))
+ File.Delete(localPath);
+
+ if (Directory.Exists(localPath))
+ Directory.Delete(localPath, true);
+
+ var fileName = downloadUrl.Split('/').Last();
+ var filePath = Path.Combine(PluginDir, fileName);
+
+ return new WebClient().DownloadFileTaskAsync(downloadUrl, filePath);
+ }
+
+ private void LoadPluginFromFolder(string directory)
+ {
+ var assemblies = new List();
+ var files = Directory.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories).ToList();
+
+ var manifest = GetManifestFromDirectory(directory);
+ if (manifest == null)
+ {
+ _log.Warn($"Directory {directory} is missing a manifest, skipping load.");
+ return;
+ }
+
+ foreach (var file in files)
+ {
+ if (!file.Contains(".dll", StringComparison.CurrentCultureIgnoreCase))
+ continue;
+
+ using (var stream = File.OpenRead(file))
+ {
+ var data = new byte[stream.Length];
+ stream.Read(data, 0, data.Length);
+ assemblies.Add(Assembly.Load(data));
+ }
+ }
+
+ InstantiatePlugin(manifest, assemblies);
+ }
+
+ private void LoadPluginFromZip(string path)
+ {
+ PluginManifest manifest;
+ var assemblies = new List();
+ using (var zipFile = ZipFile.OpenRead(path))
+ {
+ manifest = GetManifestFromZip(path);
+ if (manifest == null)
+ {
+ _log.Warn($"Zip file {path} is missing a manifest, skipping.");
+ return;
+ }
+
+ foreach (var entry in zipFile.Entries)
+ {
+ if (!entry.Name.Contains(".dll", StringComparison.CurrentCultureIgnoreCase))
+ continue;
+
+ using (var stream = entry.Open())
+ {
+ var data = new byte[entry.Length];
+ stream.Read(data, 0, data.Length);
+ assemblies.Add(Assembly.Load(data));
+ }
+ }
+ }
+
+ InstantiatePlugin(manifest, assemblies);
+ }
+
+ private PluginManifest GetManifestFromZip(string path)
+ {
+ using (var zipFile = ZipFile.OpenRead(path))
+ {
+ foreach (var entry in zipFile.Entries)
+ {
+ if (!entry.Name.Equals(MANIFEST_NAME, StringComparison.CurrentCultureIgnoreCase))
+ continue;
+
+ using (var stream = new StreamReader(entry.Open()))
+ {
+ return PluginManifest.Load(stream);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private PluginManifest GetManifestFromDirectory(string directory)
+ {
+ var path = Path.Combine(directory, MANIFEST_NAME);
+ return !File.Exists(path) ? null : PluginManifest.Load(path);
+ }
+
+ private void InstantiatePlugin(PluginManifest manifest, IEnumerable assemblies)
+ {
+ Type pluginType = null;
+ foreach (var asm in assemblies)
+ {
+ foreach (var type in asm.GetExportedTypes())
+ {
+ if (!type.GetInterfaces().Contains(typeof(ITorchPlugin)))
+ continue;
+
+ if (pluginType != null)
+ {
+ _log.Error($"The plugin '{manifest.Name}' has multiple implementations of {nameof(ITorchPlugin)}, not loading.");
+ return;
+ }
+
+ pluginType = type;
+ }
+ }
+
+ if (pluginType == null)
+ {
+ _log.Error($"The plugin '{manifest.Name}' does not have an implementation of {nameof(ITorchPlugin)}, not loading.");
+ return;
+ }
+
+ // Backwards compatibility for PluginAttribute.
+ var pluginAttr = pluginType.GetCustomAttribute();
+ if (pluginAttr != null)
+ {
+ _log.Warn($"Plugin '{manifest.Name}' is using the obsolete {nameof(PluginAttribute)}, using info from attribute if necessary.");
+ manifest.Version = manifest.Version ?? pluginAttr.Version.ToString();
+ manifest.Name = manifest.Name ?? pluginAttr.Name;
+ if (manifest.Guid == default(Guid))
+ manifest.Guid = pluginAttr.Guid;
+ }
+
+ _log.Info($"Loading plugin '{manifest.Name}' ({manifest.Version})");
+ var plugin = (TorchPluginBase)Activator.CreateInstance(pluginType);
+
+ plugin.Manifest = manifest;
+ plugin.StoragePath = Torch.Config.InstancePath;
+ plugin.Torch = Torch;
+ _plugins.Add(manifest.Guid, plugin);
+ _commandManager.RegisterPluginCommands(plugin);
+ }
+
+ ///
+ public IEnumerator GetEnumerator()
+ {
+ return _plugins.Values.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+ }
+}
diff --git a/Torch/Plugins/PluginManifest.cs b/Torch/Plugins/PluginManifest.cs
new file mode 100644
index 0000000..13618a9
--- /dev/null
+++ b/Torch/Plugins/PluginManifest.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Xml.Serialization;
+
+namespace Torch
+{
+ public class PluginManifest
+ {
+ ///
+ /// The display name of the plugin.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// A unique identifier for the plugin.
+ ///
+ public Guid Guid { get; set; }
+
+ ///
+ /// A GitHub repository in the format of Author/Repository to retrieve plugin updates.
+ ///
+ public string Repository { get; set; }
+
+ ///
+ /// The plugin version. This must include a string in the format of #[.#[.#]] for update checking purposes.
+ ///
+ public string Version { get; set; }
+
+ ///
+ /// A list of dependent plugin repositories. This may be updated to include GUIDs in the future.
+ ///
+ public List Dependencies { get; } = new List();
+
+ public void Save(string path)
+ {
+ using (var f = File.OpenWrite(path))
+ {
+ var ser = new XmlSerializer(typeof(PluginManifest));
+ ser.Serialize(f, this);
+ }
+ }
+
+ public static PluginManifest Load(string path)
+ {
+ using (var f = File.OpenRead(path))
+ {
+ return Load(f);
+ }
+ }
+
+ public static PluginManifest Load(Stream stream)
+ {
+ var ser = new XmlSerializer(typeof(PluginManifest));
+ return (PluginManifest)ser.Deserialize(stream);
+ }
+
+ public static PluginManifest Load(TextReader reader)
+ {
+ var ser = new XmlSerializer(typeof(PluginManifest));
+ return (PluginManifest)ser.Deserialize(reader);
+ }
+ }
+}
diff --git a/Torch/Torch.csproj b/Torch/Torch.csproj
index 32e91d1..bab46f8 100644
--- a/Torch/Torch.csproj
+++ b/Torch/Torch.csproj
@@ -156,7 +156,8 @@
-
+
+
@@ -200,7 +201,7 @@
-
+
@@ -217,7 +218,7 @@
-
+
CollectionEditor.xaml
diff --git a/Torch/TorchBase.cs b/Torch/TorchBase.cs
index 3a40a23..58d9f8f 100644
--- a/Torch/TorchBase.cs
+++ b/Torch/TorchBase.cs
@@ -257,7 +257,7 @@ namespace Torch
try { Console.Title = $"{Config.InstanceName} - Torch {TorchVersion}, SE {GameVersion}"; }
catch
{
- ///Running as service
+ //Running as service
}
#if DEBUG
diff --git a/Torch/TorchPluginBase.cs b/Torch/TorchPluginBase.cs
index 0bead82..64859d9 100644
--- a/Torch/TorchPluginBase.cs
+++ b/Torch/TorchPluginBase.cs
@@ -16,30 +16,11 @@ namespace Torch
public abstract class TorchPluginBase : ITorchPlugin
{
public string StoragePath { get; internal set; }
- public Guid Id { get; }
- public Version Version { get; }
- public string Name { get; }
- public ITorchBase Torch { get; private set; }
- private static readonly Logger _log = LogManager.GetLogger(nameof(TorchPluginBase));
-
- protected TorchPluginBase()
- {
- var type = GetType();
- var pluginInfo = type.GetCustomAttribute();
- if (pluginInfo == null)
- {
- _log.Warn($"Plugin {type.FullName} has no PluginAttribute");
- Name = type.FullName;
- Version = new Version(0, 0, 0, 0);
- Id = default(Guid);
- }
- else
- {
- Name = pluginInfo.Name;
- Version = pluginInfo.Version;
- Id = pluginInfo.Guid;
- }
- }
+ public PluginManifest Manifest { get; internal set; }
+ public Guid Id => Manifest.Guid;
+ public string Version => Manifest.Version;
+ public string Name => Manifest.Name;
+ public ITorchBase Torch { get; internal set; }
public virtual void Init(ITorchBase torch)
{