From fd95379aa3b0429669a4ee8b10101c221a12a10e Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 20 Feb 2020 18:07:00 +0900 Subject: [PATCH] feat: add plugin installer --- Dalamud.Injector/Program.cs | 37 ++--- Dalamud/Dalamud.cs | 50 +++--- Dalamud/Game/ChatHandlers.cs | 2 +- Dalamud/Plugin/Features/IHasConfigUi.cs | 16 ++ Dalamud/Plugin/Features/IHasUi.cs | 21 +++ Dalamud/Plugin/PluginDefinition.cs | 17 ++ Dalamud/Plugin/PluginInstallerWindow.cs | 207 ++++++++++++++++++++++++ Dalamud/Plugin/PluginManager.cs | 114 +++++++------ 8 files changed, 377 insertions(+), 87 deletions(-) create mode 100644 Dalamud/Plugin/Features/IHasConfigUi.cs create mode 100644 Dalamud/Plugin/Features/IHasUi.cs create mode 100644 Dalamud/Plugin/PluginDefinition.cs create mode 100644 Dalamud/Plugin/PluginInstallerWindow.cs diff --git a/Dalamud.Injector/Program.cs b/Dalamud.Injector/Program.cs index addc1227a..6f0e1fa1d 100644 --- a/Dalamud.Injector/Program.cs +++ b/Dalamud.Injector/Program.cs @@ -44,15 +44,15 @@ namespace Dalamud.Injector { break; } + DalamudStartInfo startInfo; if (args.Length == 1) { - var defaultStartInfo = GetDefaultStartInfo(); + startInfo = GetDefaultStartInfo(); Console.WriteLine("\nA Dalamud start info was not found in the program arguments. One has been generated for you."); Console.WriteLine("\nCopy the following contents into the program arguments:"); - Console.WriteLine(defaultStartInfo); - return; + } else { + startInfo = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(Convert.FromBase64String(args[1]))); } - var startInfo = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(Convert.FromBase64String(args[1]))); startInfo.WorkingDirectory = Directory.GetCurrentDirectory(); // Seems to help with the STATUS_INTERNAL_ERROR condition @@ -77,22 +77,17 @@ namespace Dalamud.Injector { Console.WriteLine("Injected"); } - private static string GetDefaultStartInfo() { - DalamudStartInfo startInfo = new DalamudStartInfo(); - - startInfo.WorkingDirectory = null; - - startInfo.ConfigurationPath = - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + - @"\XIVLauncher\dalamudConfig.json"; - - startInfo.PluginDirectory = - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\XIVLauncher\plugins"; - - startInfo.DefaultPluginDirectory = - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\XIVLauncher\defaultplugins"; - - startInfo.Language = ClientLanguage.English; + private static DalamudStartInfo GetDefaultStartInfo() { + var startInfo = new DalamudStartInfo { + WorkingDirectory = null, + ConfigurationPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + + @"\XIVLauncher\dalamudConfig.json", + PluginDirectory = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + + @"\XIVLauncher\plugins", + DefaultPluginDirectory = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + + @"\XIVLauncher\defaultplugins", + Language = ClientLanguage.English + }; Console.WriteLine("Creating a StartInfo with:\n" + $"ConfigurationPath: {startInfo.ConfigurationPath}\n" + @@ -100,7 +95,7 @@ namespace Dalamud.Injector { $"DefaultPluginDirectory: {startInfo.DefaultPluginDirectory}\n" + $"Language: {startInfo.Language}"); - return Convert.ToBase64String(Encoding.ASCII.GetBytes(JsonConvert.SerializeObject(startInfo))); + return startInfo; } } } diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 44c7d00a7..20a4fe595 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -33,11 +33,13 @@ namespace Dalamud { public readonly SigScanner SigScanner; - public Framework Framework { get; } + public readonly Framework Framework; - public CommandManager CommandManager { get; } - public ChatHandlers ChatHandlers { get; } - public NetworkHandlers NetworkHandlers { get; } + public readonly CommandManager CommandManager; + + public readonly ChatHandlers ChatHandlers; + + public readonly NetworkHandlers NetworkHandlers; public readonly DiscordBotManager BotManager; @@ -67,27 +69,26 @@ namespace Dalamud { // Initialize the process information. this.targetModule = Process.GetCurrentProcess().MainModule; - SigScanner = new SigScanner(this.targetModule, true); + this.SigScanner = new SigScanner(this.targetModule, true); // Initialize game subsystem - Framework = new Framework(this.SigScanner, this); + this.Framework = new Framework(this.SigScanner, this); // Initialize managers. Basically handlers for the logic - CommandManager = new CommandManager(this, info.Language); + this.CommandManager = new CommandManager(this, info.Language); SetupCommands(); - ChatHandlers = new ChatHandlers(this); - NetworkHandlers = new NetworkHandlers(this, this.Configuration.OptOutMbCollection); + this.ChatHandlers = new ChatHandlers(this); + this.NetworkHandlers = new NetworkHandlers(this, this.Configuration.OptOutMbCollection); this.Data = new DataManager(this.StartInfo.Language); - //Task.Run(() => ); this.Data.Initialize(); this.ClientState = new ClientState(this, info, this.SigScanner, this.targetModule); this.BotManager = new DiscordBotManager(this, this.Configuration.DiscordFeatureConfig); - this.PluginManager = new PluginManager(this, info.PluginDirectory, info.DefaultPluginDirectory); + this.PluginManager = new PluginManager(this, info.PluginDirectory); this.WinSock2 = new WinSockHandlers(); @@ -105,18 +106,15 @@ namespace Dalamud { } catch (Exception e) { Log.Information("Could not enable interface."); } - - Framework.Enable(); + + this.Framework.Enable(); this.BotManager.Start(); - try - { + try { this.PluginManager.LoadPlugins(); - } - catch (Exception ex) - { - Framework.Gui.Chat.PrintError( + } catch (Exception ex) { + this.Framework.Gui.Chat.PrintError( "[XIVLAUNCHER] There was an error loading additional plugins. Please check the log for more details."); Log.Error(ex, "Plugin load failed."); } @@ -167,9 +165,11 @@ namespace Dalamud { private bool isImguiDrawLogWindow = false; private bool isImguiDrawDataWindow = false; + private bool isImguiDrawPluginWindow = false; private DalamudLogWindow logWindow; private DalamudDataWindow dataWindow; + private PluginInstallerWindow pluginWindow; private void BuildDalamudUi() { @@ -209,10 +209,15 @@ namespace Dalamud { if (ImGui.BeginMenu("Plugins")) { + if (ImGui.MenuItem("Open Plugin installer")) + { + this.pluginWindow = new PluginInstallerWindow(this.PluginManager, this.StartInfo.PluginDirectory); + this.isImguiDrawPluginWindow = true; + } if (ImGui.MenuItem("Print plugin info")) { foreach (var plugin in this.PluginManager.Plugins) { // TODO: some more here, state maybe? - Log.Information($"{plugin.Name}"); + Log.Information($"{plugin.Plugin.Name}"); } } if (ImGui.MenuItem("Reload plugins")) @@ -241,6 +246,11 @@ namespace Dalamud { this.isImguiDrawDataWindow = this.dataWindow != null && this.dataWindow.Draw(); } + if (this.isImguiDrawPluginWindow) + { + this.isImguiDrawPluginWindow = this.pluginWindow != null && this.pluginWindow.Draw(); + } + if (this.isImguiDrawDemoWindow) ImGui.ShowDemoWindow(); } diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index 81e05c801..6a78f44d1 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -105,7 +105,7 @@ namespace Dalamud.Game { this.dalamud.Framework.Gui.Chat.Print($"XIVLauncher in-game addon v{assemblyVersion} loaded."); foreach (var plugin in this.dalamud.PluginManager.Plugins) { - this.dalamud.Framework.Gui.Chat.Print($" -> {plugin.Name} v{plugin.GetType().Assembly.GetName().Version} loaded."); + this.dalamud.Framework.Gui.Chat.Print($" -> {plugin.Plugin.Name} v{plugin.GetType().Assembly.GetName().Version} loaded."); } this.hasSeenLoadingMsg = true; diff --git a/Dalamud/Plugin/Features/IHasConfigUi.cs b/Dalamud/Plugin/Features/IHasConfigUi.cs new file mode 100644 index 000000000..53e64ea98 --- /dev/null +++ b/Dalamud/Plugin/Features/IHasConfigUi.cs @@ -0,0 +1,16 @@ +using System; +using Dalamud.Interface; + +namespace Dalamud.Plugin.Features +{ + /// + /// This interface represents a Dalamud plugin that has a configuration UI which can be triggered. + /// + public interface IHasConfigUi : IHasUi + { + /// + /// An event handler that is fired when the plugin should show its configuration interface. + /// + EventHandler OpenConfigUi { get; } + } +} diff --git a/Dalamud/Plugin/Features/IHasUi.cs b/Dalamud/Plugin/Features/IHasUi.cs new file mode 100644 index 000000000..afdb74df8 --- /dev/null +++ b/Dalamud/Plugin/Features/IHasUi.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Dalamud.Interface; + +namespace Dalamud.Plugin.Features +{ + /// + /// This interface represents a Dalamud plugin that has user interface which can be drawn. + /// + public interface IHasUi : IDalamudPlugin + { + /// + /// A function that gets called when Dalamud is ready to draw your UI. + /// + /// An object you can use to e.g. load images. + void Draw(UiBuilder uiBuilder); + } +} diff --git a/Dalamud/Plugin/PluginDefinition.cs b/Dalamud/Plugin/PluginDefinition.cs new file mode 100644 index 000000000..29b373b75 --- /dev/null +++ b/Dalamud/Plugin/PluginDefinition.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Dalamud.Plugin +{ + public class PluginDefinition + { + public string Author { get; set; } + public string Name { get; set; } + public string InternalName { get; set; } + public string AssemblyVersion { get; set; } + public string Description { get; set; } + } +} diff --git a/Dalamud/Plugin/PluginInstallerWindow.cs b/Dalamud/Plugin/PluginInstallerWindow.cs new file mode 100644 index 000000000..2f5d9af86 --- /dev/null +++ b/Dalamud/Plugin/PluginInstallerWindow.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; +using Dalamud.Plugin.Features; +using ImGuiNET; +using Newtonsoft.Json; +using Serilog; + +namespace Dalamud.Plugin +{ + class PluginInstallerWindow { + private const string PluginRepoBaseUrl = "https://goaaats.github.io/DalamudPlugins/"; + + private PluginManager manager; + private string pluginDirectory; + private ReadOnlyCollection pluginMaster; + private bool errorModalDrawing = true; + + private enum PluginInstallStatus { + None, + InProgress, + Success, + Fail + } + + private PluginInstallStatus installStatus = PluginInstallStatus.None; + + private bool masterDownloadFailed = false; + + public PluginInstallerWindow(PluginManager manager, string pluginDirectory) { + this.manager = manager; + this.pluginDirectory = pluginDirectory; + Task.Run(CachePluginMaster).ContinueWith(t => { this.masterDownloadFailed = t.IsFaulted; }); + } + + private void CachePluginMaster() { + try { + using var client = new WebClient(); + + var data = client.DownloadString(PluginRepoBaseUrl + "pluginmaster.json"); + + this.pluginMaster = JsonConvert.DeserializeObject>(data); + } catch { + this.masterDownloadFailed = true; + } + } + + private void InstallPlugin(PluginDefinition definition) { + try { + var path = Path.GetTempFileName(); + + Log.Information("Downloading plugin to {0}", path); + + using var client = new WebClient(); + + client.DownloadFile(PluginRepoBaseUrl + $"/plugins/{definition.InternalName}/latest.zip", path); + var outputDir = Path.Combine(this.pluginDirectory, definition.InternalName); + + if (File.Exists(Path.Combine(outputDir, ".disabled"))) { + Log.Information("Plugin was disabled, re-enabling."); + File.Delete(Path.Combine(outputDir, ".disabled")); + } + + Log.Information("Extracting to {0}", outputDir); + + Directory.CreateDirectory(outputDir); + + ZipFile.ExtractToDirectory(path, outputDir); + + this.installStatus = PluginInstallStatus.Success; + this.manager.LoadPluginFromAssembly(new FileInfo(Path.Combine(outputDir, $"{definition.InternalName}.dll"))); + } catch (Exception e) { + Log.Error(e, "Plugin download failed hard."); + this.installStatus = PluginInstallStatus.Fail; + } + } + + public bool Draw() { + var windowOpen = true; + + ImGui.SetNextWindowSize(new Vector2(750, 520)); + + ImGui.Begin("Plugin Installer", ref windowOpen, + ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoScrollbar); + + ImGui.Text("This window allows you install and remove in-game plugins."); + ImGui.Text("They are made by third-party developers."); + ImGui.Separator(); + + ImGui.BeginChild("scrolling", new Vector2(0, 400), true, ImGuiWindowFlags.HorizontalScrollbar); + + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1, 3)); + + if (this.pluginMaster == null) { + ImGui.Text("Loading plugins..."); + } else if (this.masterDownloadFailed) { + ImGui.Text("Download failed."); + } + else + { + foreach (var pluginDefinition in this.pluginMaster) + { + if (ImGui.CollapsingHeader(pluginDefinition.Name)) + { + ImGui.Indent(); + + ImGui.Text(pluginDefinition.Name); + ImGui.SameLine(); + ImGui.TextColored(new Vector4(0.5f, 0.5f, 0.5f, 1.0f), $" by {pluginDefinition.Author}"); + + ImGui.Text(pluginDefinition.Description); + + var isInstalled = this.manager.Plugins.Where(x=> x.Definition != null).Any( + x => x.Definition.InternalName == pluginDefinition.InternalName); + + if (!isInstalled) { + if (this.installStatus == PluginInstallStatus.InProgress) { + ImGui.Button($"Install in progress..."); + } else { + if (ImGui.Button($"Install v{pluginDefinition.AssemblyVersion}")) + { + this.installStatus = PluginInstallStatus.InProgress; + + Task.Run(() => InstallPlugin(pluginDefinition)).ContinueWith(t => { this.installStatus = t.IsFaulted ? PluginInstallStatus.Fail : this.installStatus; }); + } + } + + } else { + var installedPlugin = this.manager.Plugins.Where(x => x.Definition != null).First( + x => x.Definition.InternalName == pluginDefinition.InternalName); + + if (ImGui.Button("Disable")) + { + this.manager.DisablePlugin(installedPlugin.Definition); + } + + if (installedPlugin.Plugin is IHasConfigUi v2Plugin && v2Plugin.OpenConfigUi != null) { + ImGui.SameLine(); + + if (ImGui.Button("Open Configuration")) + { + v2Plugin.OpenConfigUi?.Invoke(null, null); + } + } + } + + ImGui.Unindent(); + } + } + } + + ImGui.PopStyleVar(); + + ImGui.EndChild(); + + ImGui.Separator(); + + if (ImGui.Button("Remove All")) + { + + } + + ImGui.SameLine(); + + if (ImGui.Button("Open Plugin folder")) + { + + } + + ImGui.SameLine(); + + if (ImGui.Button("Close")) + { + windowOpen = false; + } + + if (ImGui.Button("test modal")) { + this.installStatus = PluginInstallStatus.Fail; + } + + ImGui.Spacing(); + + if (this.installStatus == PluginInstallStatus.Fail || this.masterDownloadFailed) { + if (ImGui.BeginPopupModal("Installer failed", ref this.errorModalDrawing, ImGuiWindowFlags.AlwaysAutoResize)) + { + ImGui.TextWrapped("The installer ran into an issue. Please restart the game and report this error on our discord."); + + if (ImGui.Button("OK", new Vector2(120, 40))) { ImGui.CloseCurrentPopup(); } + + ImGui.EndPopup(); + } + } + + ImGui.End(); + + return windowOpen; + } + } +} diff --git a/Dalamud/Plugin/PluginManager.cs b/Dalamud/Plugin/PluginManager.cs index 4a8a1082d..2576475da 100644 --- a/Dalamud/Plugin/PluginManager.cs +++ b/Dalamud/Plugin/PluginManager.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; +using Newtonsoft.Json; using Serilog; namespace Dalamud.Plugin @@ -12,14 +13,14 @@ namespace Dalamud.Plugin public class PluginManager { private readonly Dalamud dalamud; private readonly string pluginDirectory; - private readonly string defaultPluginDirectory; - public List Plugins; + private readonly Type interfaceType = typeof(IDalamudPlugin); - public PluginManager(Dalamud dalamud, string pluginDirectory, string defaultPluginDirectory) { + public readonly List<(IDalamudPlugin Plugin, PluginDefinition Definition)> Plugins = new List<(IDalamudPlugin plugin, PluginDefinition def)>(); + + public PluginManager(Dalamud dalamud, string pluginDirectory) { this.dalamud = dalamud; this.pluginDirectory = pluginDirectory; - this.defaultPluginDirectory = defaultPluginDirectory; } public void UnloadPlugins() { @@ -27,60 +28,83 @@ namespace Dalamud.Plugin return; for (var i = 0; i < this.Plugins.Count; i++) { - this.Plugins[i].Dispose(); - this.Plugins[i] = null; + this.Plugins[i].Plugin.Dispose(); } + + this.Plugins.Clear(); } public void LoadPlugins() { - LoadPluginsAt(new DirectoryInfo(this.defaultPluginDirectory)); LoadPluginsAt(new DirectoryInfo(this.pluginDirectory)); } + public void DisablePlugin(PluginDefinition definition) { + var thisPlugin = this.Plugins.Where(x => x.Definition != null) + .First(x => x.Definition.InternalName == definition.InternalName); + + var outputDir = Path.Combine(this.pluginDirectory, definition.InternalName); + File.Create(Path.Combine(outputDir, ".disabled")); + + thisPlugin.Plugin.Dispose(); + + this.Plugins.Remove(thisPlugin); + } + + public void LoadPluginFromAssembly(FileInfo dllFile) { + Log.Information("Loading assembly at {0}", dllFile); + var assemblyName = AssemblyName.GetAssemblyName(dllFile.FullName); + var pluginAssembly = Assembly.Load(assemblyName); + + if (pluginAssembly != null) + { + Log.Information("Loading types for {0}", pluginAssembly.FullName); + var types = pluginAssembly.GetTypes(); + foreach (var type in types) + { + if (type.IsInterface || type.IsAbstract) + { + continue; + } + + if (type.GetInterface(interfaceType.FullName) != null) + { + var plugin = (IDalamudPlugin)Activator.CreateInstance(type); + + var dalamudInterface = new DalamudPluginInterface(this.dalamud, type.Assembly.GetName().Name); + + var defJsonFile = new FileInfo(Path.Combine(dllFile.Directory.FullName, $"{Path.GetFileNameWithoutExtension(dllFile.Name)}.json")); + + PluginDefinition pluginDef = null; + if (defJsonFile.Exists) + { + Log.Information("Loading definition for plugin DLL {0}", dllFile.FullName); + + pluginDef = + JsonConvert.DeserializeObject( + File.ReadAllText(defJsonFile.FullName)); + } + else + { + Log.Information("Plugin DLL {0} has no definition.", dllFile.FullName); + } + + plugin.Initialize(dalamudInterface); + Log.Information("Loaded plugin: {0}", plugin.Name); + this.Plugins.Add((plugin, pluginDef)); + } + } + } + } + private void LoadPluginsAt(DirectoryInfo folder) { if (folder.Exists) { - Log.Debug("Loading plugins at {0}", folder); + Log.Information("Loading plugins at {0}", folder); var pluginDlls = folder.GetFiles("*.dll", SearchOption.AllDirectories); - var assemblies = new List(pluginDlls.Length); - foreach (var dllFile in pluginDlls) - { - Log.Debug("Loading assembly at {0}", dllFile); - var assemblyName = AssemblyName.GetAssemblyName(dllFile.FullName); - var pluginAssembly = Assembly.Load(assemblyName); - assemblies.Add(pluginAssembly); - } - - var interfaceType = typeof(IDalamudPlugin); - var foundImplementations = new List(); - foreach (var assembly in assemblies) { - if (assembly != null) { - Log.Debug("Loading types for {0}", assembly.FullName); - var types = assembly.GetTypes(); - foreach (var type in types) { - if (type.IsInterface || type.IsAbstract) { - continue; - } - - if (type.GetInterface(interfaceType.FullName) != null) { - foundImplementations.Add(type); - } - } - } - } - - this.Plugins = new List(foundImplementations.Count); - foreach (var pluginType in foundImplementations) - { - var plugin = (IDalamudPlugin)Activator.CreateInstance(pluginType); - - var dalamudInterface = new DalamudPluginInterface(this.dalamud, pluginType.Assembly.GetName().Name); - - plugin.Initialize(dalamudInterface); - Log.Information("Loaded plugin: {0}", plugin.Name); - this.Plugins.Add(plugin); + foreach (var dllFile in pluginDlls) { + LoadPluginFromAssembly(dllFile); } } }