Merge pull request #121 from TorchAPI/plugins

Plugin loader improvements
This commit is contained in:
John Gross
2017-09-21 22:58:02 -07:00
committed by GitHub
17 changed files with 495 additions and 302 deletions

View File

@@ -14,12 +14,12 @@ namespace Torch.API.Managers
/// <summary>
/// Fired when plugins are loaded.
/// </summary>
event Action<IList<ITorchPlugin>> PluginsLoaded;
event Action<IReadOnlyCollection<ITorchPlugin>> PluginsLoaded;
/// <summary>
/// Collection of loaded plugins.
/// </summary>
IList<ITorchPlugin> Plugins { get; }
IReadOnlyDictionary<Guid, ITorchPlugin> Plugins { get; }
/// <summary>
/// Updates all loaded plugins.

View File

@@ -17,7 +17,7 @@ namespace Torch.API.Plugins
/// <summary>
/// The version of the plugin.
/// </summary>
Version Version { get; }
string Version { get; }
/// <summary>
/// The name of the plugin.

View File

@@ -10,6 +10,7 @@ namespace Torch.API.Plugins
/// <summary>
/// Indicates that the given type should be loaded by the plugin manager as a plugin.
/// </summary>
[Obsolete("All plugin meta-information is now defined in the manifest.xml.")]
[AttributeUsage(AttributeTargets.Class)]
public class PluginAttribute : Attribute
{

View File

@@ -29,7 +29,7 @@ namespace Torch.Server.ViewModels
pluginManager.PluginsLoaded += PluginManager_PluginsLoaded;
}
private void PluginManager_PluginsLoaded(IList<ITorchPlugin> obj)
private void PluginManager_PluginsLoaded(IReadOnlyCollection<ITorchPlugin> obj)
{
Plugins.Clear();
foreach (var plugin in obj)

View File

@@ -12,7 +12,7 @@
</UserControl.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="200"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
@@ -27,7 +27,7 @@
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Button Grid.Row="1" Content="Open Folder" Margin="3" DockPanel.Dock="Bottom" IsEnabled="false"/>
<Button Grid.Row="1" Content="Open Folder" Margin="3" DockPanel.Dock="Bottom" Click="OpenFolder_OnClick"/>
</Grid>
<Frame Grid.Column="1" NavigationUIVisibility="Hidden" Content="{Binding SelectedPlugin.Control}"/>
</Grid>

View File

@@ -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
/// </summary>
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<PluginManager>();
var pluginManager = new PluginManagerViewModel(_plugins);
DataContext = pluginManager;
}
private void OpenFolder_OnClick(object sender, RoutedEventArgs e)
{
Process.Start("explorer.exe", _plugins.PluginDir);
}
}
}

View File

@@ -0,0 +1,54 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace Torch
{
public static class ICollectionExtensions
{
/// <summary>
/// Returns a read-only wrapped <see cref="ICollection{T}"/>
/// </summary>
public static IReadOnlyCollection<T> AsReadOnly<T>(this ICollection<T> source)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
return source as IReadOnlyCollection<T> ?? new ReadOnlyCollectionAdapter<T>(source);
}
/// <summary>
/// Returns a read-only wrapped <see cref="IList{T}"/>
/// </summary>
public static IReadOnlyList<T> AsReadOnly<T>(this IList<T> source)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
return source as IReadOnlyList<T> ?? new ReadOnlyCollection<T>(source);
}
/// <summary>
/// Returns a read-only wrapped <see cref="IDictionary{TKey, TValue}"/>
/// </summary>
public static IReadOnlyDictionary<TKey, TValue> AsReadOnly<TKey, TValue>(this IDictionary<TKey, TValue> source)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
return source as IReadOnlyDictionary<TKey, TValue> ?? new ReadOnlyDictionary<TKey, TValue>(source);
}
sealed class ReadOnlyCollectionAdapter<T> : IReadOnlyCollection<T>
{
private readonly ICollection<T> _source;
public ReadOnlyCollectionAdapter(ICollection<T> source)
{
_source = source;
}
public int Count => _source.Count;
public IEnumerator<T> GetEnumerator() => _source.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
}

View File

@@ -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)
/// <summary>
/// Try to extract a 3 component version from the string. Format: #.#.#
/// </summary>
public static bool TryExtractVersion(this string version, out Version result)
{
return s.Length <= maxLength ? s : s.Substring(0, maxLength);
}
public static IEnumerable<string> 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);
}
}
}

View File

@@ -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
{
/// <inheritdoc />
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;
/// <inheritdoc />
public IList<ITorchPlugin> Plugins { get; } = new ObservableList<ITorchPlugin>();
public event Action<IList<ITorchPlugin>> PluginsLoaded;
public PluginManager(ITorchBase torchInstance) : base(torchInstance)
{
if (!Directory.Exists(PluginDir))
Directory.CreateDirectory(PluginDir);
}
/// <summary>
/// Updates loaded plugins in parallel.
/// </summary>
public void UpdatePlugins()
{
foreach (var plugin in Plugins)
plugin.Update();
}
/// <summary>
/// Unloads all plugins.
/// </summary>
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<Task>();
//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());
}
/// <inheritdoc />
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<PluginAttribute>() == 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<ITorchPlugin> GetEnumerator()
{
return Plugins.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

View File

@@ -44,7 +44,7 @@ namespace Torch.Managers
CheckAndUpdateTorch();
}
private async Task<Tuple<Version, string>> GetLatestRelease(string owner, string name)
private async Task<Tuple<Version, string>> TryGetLatestArchiveUrl(string owner, string name)
{
try
{
@@ -53,56 +53,17 @@ namespace Torch.Managers
return new Tuple<Version, string>(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<Version, string>(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, string>(version, zip?.BrowserDownloadUrl);
}
catch (Exception e)
{
_log.Error($"An error occurred getting release information for '{owner}/{name}'");
_log.Error(e);
return new Tuple<Version, string>(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<Version, string>);
}
}
@@ -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}");

View File

@@ -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);
}
}
}
}

View File

@@ -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
{
/// <inheritdoc />
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<Guid, ITorchPlugin> _plugins = new ObservableDictionary<Guid, ITorchPlugin>();
[Dependency]
private CommandManager _commandManager;
/// <inheritdoc />
public IReadOnlyDictionary<Guid, ITorchPlugin> Plugins => _plugins.AsReadOnly();
public event Action<IReadOnlyCollection<ITorchPlugin>> PluginsLoaded;
public PluginManager(ITorchBase torchInstance) : base(torchInstance)
{
if (!Directory.Exists(PluginDir))
Directory.CreateDirectory(PluginDir);
}
/// <summary>
/// Updates loaded plugins in parallel.
/// </summary>
public void UpdatePlugins()
{
foreach (var plugin in _plugins.Values)
plugin.Update();
}
/// <summary>
/// Unloads all plugins.
/// </summary>
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<Tuple<Version, string>> 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<Version, string>(latestVersion, zipAsset?.BrowserDownloadUrl);
}
catch (Exception e)
{
_log.Error($"Unable to get the latest release of '{repository}.'");
_log.Error(e);
return default(Tuple<Version, string>);
}
}
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<Assembly>();
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<Assembly>();
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<Assembly> 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<PluginAttribute>();
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);
}
/// <inheritdoc cref="IEnumerable.GetEnumerator"/>
public IEnumerator<ITorchPlugin> GetEnumerator()
{
return _plugins.Values.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

View File

@@ -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
{
/// <summary>
/// The display name of the plugin.
/// </summary>
public string Name { get; set; }
/// <summary>
/// A unique identifier for the plugin.
/// </summary>
public Guid Guid { get; set; }
/// <summary>
/// A GitHub repository in the format of Author/Repository to retrieve plugin updates.
/// </summary>
public string Repository { get; set; }
/// <summary>
/// The plugin version. This must include a string in the format of #[.#[.#]] for update checking purposes.
/// </summary>
public string Version { get; set; }
/// <summary>
/// A list of dependent plugin repositories. This may be updated to include GUIDs in the future.
/// </summary>
public List<string> Dependencies { get; } = new List<string>();
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);
}
}
}

View File

@@ -156,7 +156,8 @@
</Compile>
<Compile Include="ChatMessage.cs" />
<Compile Include="Collections\ObservableList.cs" />
<Compile Include="DispatcherExtensions.cs" />
<Compile Include="Extensions\DispatcherExtensions.cs" />
<Compile Include="Extensions\ICollectionExtensions.cs" />
<Compile Include="Managers\DependencyManager.cs" />
<Compile Include="Managers\PatchManager\AssemblyMemory.cs" />
<Compile Include="Managers\PatchManager\DecoratedMethod.cs" />
@@ -200,7 +201,7 @@
<Compile Include="Managers\MultiplayerManager.cs" />
<Compile Include="Managers\UpdateManager.cs" />
<Compile Include="Persistent.cs" />
<Compile Include="PluginManifest.cs" />
<Compile Include="Plugins\PluginManifest.cs" />
<Compile Include="Utils\Reflection.cs" />
<Compile Include="Managers\ScriptingManager.cs" />
<Compile Include="Utils\TorchAssemblyResolver.cs" />
@@ -217,7 +218,7 @@
<Compile Include="Extensions\StringExtensions.cs" />
<Compile Include="ViewModels\PlayerViewModel.cs" />
<Compile Include="ViewModels\ViewModel.cs" />
<Compile Include="Managers\PluginManager.cs" />
<Compile Include="Plugins\PluginManager.cs" />
<Compile Include="ViewModels\PluginViewModel.cs" />
<Compile Include="Views\CollectionEditor.xaml.cs">
<DependentUpon>CollectionEditor.xaml</DependentUpon>

View File

@@ -257,7 +257,7 @@ namespace Torch
try { Console.Title = $"{Config.InstanceName} - Torch {TorchVersion}, SE {GameVersion}"; }
catch
{
///Running as service
//Running as service
}
#if DEBUG

View File

@@ -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<PluginAttribute>();
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)
{