From c901ab424842b9e8bb346f5605ee4402cd9aaf57 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Mon, 25 Apr 2022 23:39:59 +0200 Subject: [PATCH 01/19] chore: replace IsDebugging check on PluginInterface with Debugger.IsAttached Makes a bit more sense api-wise --- Dalamud/Plugin/DalamudPluginInterface.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 2be81eaf5..4e2ecc6c6 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -138,11 +139,7 @@ namespace Dalamud.Plugin /// /// Gets a value indicating whether Dalamud is running in Debug mode or the /xldev menu is open. This can occur on release builds. /// -#if DEBUG - public bool IsDebugging => true; -#else - public bool IsDebugging => Service.GetNullable() is {IsDevMenuOpen: true}; // Can be null during boot -#endif + public bool IsDebugging => Debugger.IsAttached; /// /// Gets the current UI language in two-letter iso format. From 83676a3ac2afadf6fa4dc379b667b975a8ba4d61 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Tue, 26 Apr 2022 11:26:08 +0200 Subject: [PATCH 02/19] chore: only warn before manual injections in release builds --- Dalamud.Injector/EntryPoint.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index 8896dc293..a6fe9acdf 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -59,7 +59,11 @@ namespace Dalamud.Injector // No command defaults to inject args.Add("inject"); args.Add("--all"); + +#if !DEBUG args.Add("--warn"); +#endif + } else if (int.TryParse(args[1], out var _)) { From 3ba7029eeb60b8ae84260e1a4abe22e4c6375be5 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Tue, 26 Apr 2022 11:42:32 +0200 Subject: [PATCH 03/19] deps: update FFXIVClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 7d95dce09..a2972adbd 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 7d95dce097dce7aa6712e4ef499a9d4ad8fafed7 +Subproject commit a2972adbd333d0ad9c127fff1cfc288d1cecf6b4 From be848737c07e1fd1e8afc63e9c3b481d4c9c3e76 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Tue, 26 Apr 2022 16:50:30 +0200 Subject: [PATCH 04/19] build: 6.4.0.7 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index bb7b92f2b..6a977d981 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 6.4.0.6 + 6.4.0.7 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From ae54988f91b30553b22bc36174d7dd4092a76bee Mon Sep 17 00:00:00 2001 From: Ottermandias <70807659+Ottermandias@users.noreply.github.com> Date: Tue, 26 Apr 2022 17:09:44 +0200 Subject: [PATCH 05/19] Fix FilePicker broken callback handling and make function signatures unique (#822) --- .../ImGuiFileDialog/FileDialogManager.cs | 52 ++++++++----------- 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs index e5c5854db..f9cd06290 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs @@ -20,8 +20,7 @@ namespace Dalamud.Interface.ImGuiFileDialog /// The action to execute when the dialog is finished. public void OpenFolderDialog(string title, Action callback) { - this.SetCallback(callback); - this.SetDialog("OpenFolderDialog", title, string.Empty, this.savedPath, ".", string.Empty, 1, false, ImGuiFileDialogFlags.SelectOnly); + this.SetDialog("OpenFolderDialog", title, string.Empty, this.savedPath, ".", string.Empty, 1, false, ImGuiFileDialogFlags.SelectOnly, callback); } /// @@ -33,8 +32,7 @@ namespace Dalamud.Interface.ImGuiFileDialog /// Whether the dialog should be a modal popup. public void OpenFolderDialog(string title, Action callback, string? startPath, bool isModal = false) { - this.SetCallback(callback); - this.SetDialog("OpenFolderDialog", title, string.Empty, startPath ?? this.savedPath, ".", string.Empty, 1, isModal, ImGuiFileDialogFlags.SelectOnly); + this.SetDialog("OpenFolderDialog", title, string.Empty, startPath ?? this.savedPath, ".", string.Empty, 1, isModal, ImGuiFileDialogFlags.SelectOnly, callback); } /// @@ -45,8 +43,7 @@ namespace Dalamud.Interface.ImGuiFileDialog /// The action to execute when the dialog is finished. public void SaveFolderDialog(string title, string defaultFolderName, Action callback) { - this.SetCallback(callback); - this.SetDialog("SaveFolderDialog", title, string.Empty, this.savedPath, defaultFolderName, string.Empty, 1, false, ImGuiFileDialogFlags.None); + this.SetDialog("SaveFolderDialog", title, string.Empty, this.savedPath, defaultFolderName, string.Empty, 1, false, ImGuiFileDialogFlags.None, callback); } /// @@ -59,8 +56,7 @@ namespace Dalamud.Interface.ImGuiFileDialog /// Whether the dialog should be a modal popup. public void SaveFolderDialog(string title, string defaultFolderName, Action callback, string? startPath, bool isModal = false) { - this.SetCallback(callback); - this.SetDialog("SaveFolderDialog", title, string.Empty, startPath ?? this.savedPath, defaultFolderName, string.Empty, 1, isModal, ImGuiFileDialogFlags.None); + this.SetDialog("SaveFolderDialog", title, string.Empty, startPath ?? this.savedPath, defaultFolderName, string.Empty, 1, isModal, ImGuiFileDialogFlags.None, callback); } /// @@ -71,8 +67,7 @@ namespace Dalamud.Interface.ImGuiFileDialog /// The action to execute when the dialog is finished. public void OpenFileDialog(string title, string filters, Action callback) { - this.SetCallback(callback); - this.SetDialog("OpenFileDialog", title, filters, this.savedPath, ".", string.Empty, 1, false, ImGuiFileDialogFlags.SelectOnly); + this.SetDialog("OpenFileDialog", title, filters, this.savedPath, ".", string.Empty, 1, false, ImGuiFileDialogFlags.SelectOnly, callback); } /// @@ -81,19 +76,18 @@ namespace Dalamud.Interface.ImGuiFileDialog /// The header title of the dialog. /// Which files to show in the dialog. /// The action to execute when the dialog is finished. - /// The directory which the dialog should start inside of. The last path this manager was in is used if this is null. /// The maximum amount of files or directories which can be selected. Set to 0 for an infinite number. + /// The directory which the dialog should start inside of. The last path this manager was in is used if this is null. /// Whether the dialog should be a modal popup. public void OpenFileDialog( string title, string filters, Action> callback, + int selectionCountMax, string? startPath = null, - int selectionCountMax = 1, bool isModal = false) { - this.SetCallback(callback); - this.SetDialog("OpenFileDialog", title, filters, startPath ?? this.savedPath, ".", string.Empty, selectionCountMax, isModal, ImGuiFileDialogFlags.SelectOnly); + this.SetDialog("OpenFileDialog", title, filters, startPath ?? this.savedPath, ".", string.Empty, selectionCountMax, isModal, ImGuiFileDialogFlags.SelectOnly, callback); } /// @@ -111,8 +105,7 @@ namespace Dalamud.Interface.ImGuiFileDialog string defaultExtension, Action callback) { - this.SetCallback(callback); - this.SetDialog("SaveFileDialog", title, filters, this.savedPath, defaultFileName, defaultExtension, 1, false, ImGuiFileDialogFlags.None); + this.SetDialog("SaveFileDialog", title, filters, this.savedPath, defaultFileName, defaultExtension, 1, false, ImGuiFileDialogFlags.None, callback); } /// @@ -134,8 +127,7 @@ namespace Dalamud.Interface.ImGuiFileDialog string? startPath, bool isModal = false) { - this.SetCallback(callback); - this.SetDialog("SaveFileDialog", title, filters, startPath ?? this.savedPath, defaultFileName, defaultExtension, 1, isModal, ImGuiFileDialogFlags.None); + this.SetDialog("SaveFileDialog", title, filters, startPath ?? this.savedPath, defaultFileName, defaultExtension, 1, isModal, ImGuiFileDialogFlags.None, callback); } /// @@ -166,18 +158,6 @@ namespace Dalamud.Interface.ImGuiFileDialog this.multiCallback = null; } - private void SetCallback(Action action) - { - this.callback = action; - this.multiCallback = null; - } - - private void SetCallback(Action> action) - { - this.multiCallback = action; - this.callback = null; - } - private void SetDialog( string id, string title, @@ -187,9 +167,19 @@ namespace Dalamud.Interface.ImGuiFileDialog string defaultExtension, int selectionCountMax, bool isModal, - ImGuiFileDialogFlags flags) + ImGuiFileDialogFlags flags, + Delegate callback) { this.Reset(); + if (callback is Action> multi) + { + this.multiCallback = multi; + } + else + { + this.callback = callback as Action; + } + this.dialog = new FileDialog(id, title, filters, path, defaultFileName, defaultExtension, selectionCountMax, isModal, flags); this.dialog.Show(); } From c0522755899751e0fad9cc30e627e68a740851bc Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Tue, 26 Apr 2022 17:10:21 +0200 Subject: [PATCH 06/19] build: 6.4.0.8 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 6a977d981..ea6f5dddf 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 6.4.0.7 + 6.4.0.8 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From fde3f63dc727fdaae47943375a0126e0e26063f2 Mon Sep 17 00:00:00 2001 From: kizer Date: Sat, 30 Apr 2022 03:12:22 +0900 Subject: [PATCH 07/19] Fix dalamud launch language and add --dalamud-client-language (#826) * Fix dalamud launch language and add --dalamud-client-language * Fix help --- Dalamud.Injector/EntryPoint.cs | 128 +++++++++++++++++++-------------- 1 file changed, 74 insertions(+), 54 deletions(-) diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index a6fe9acdf..ccb093c1d 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -37,9 +37,9 @@ namespace Dalamud.Injector /// byte** string arguments. public static void Main(int argc, IntPtr argvPtr) { - Init(); - List args = new(argc); + Init(args); + unsafe { var argv = (IntPtr*)argvPtr; @@ -96,15 +96,13 @@ namespace Dalamud.Injector } else { - Console.WriteLine("Invalid command: {0}", mainCommand); - ProcessHelpCommand(args); - Environment.Exit(-1); + throw new CommandLineException($"\"{mainCommand}\" is not a valid command."); } } - private static void Init() + private static void Init(List args) { - InitUnhandledException(); + InitUnhandledException(args); InitLogging(); var cwd = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory; @@ -115,25 +113,31 @@ namespace Dalamud.Injector } } - private static void InitUnhandledException() + private static void InitUnhandledException(List args) { AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) => { - if (Log.Logger == null) + var exObj = eventArgs.ExceptionObject; + + if (exObj is CommandLineException clex) + { + Console.WriteLine(); + Console.WriteLine("Command line error: {0}", clex.Message); + Console.WriteLine(); + ProcessHelpCommand(args); + Environment.Exit(-1); + } + else if (Log.Logger == null) { Console.WriteLine($"A fatal error has occurred: {eventArgs.ExceptionObject}"); } + else if (exObj is Exception ex) + { + Log.Error(ex, "A fatal error has occurred."); + } else { - var exObj = eventArgs.ExceptionObject; - if (exObj is Exception ex) - { - Log.Error(ex, "A fatal error has occurred."); - } - else - { - Log.Error($"A fatal error has occurred: {eventArgs.ExceptionObject}"); - } + Log.Error($"A fatal error has occurred: {eventArgs.ExceptionObject}"); } #if DEBUG @@ -150,7 +154,7 @@ namespace Dalamud.Injector #endif _ = MessageBoxW(IntPtr.Zero, message, caption, MessageBoxType.IconError | MessageBoxType.Ok); - Environment.Exit(0); + Environment.Exit(-1); }; } @@ -229,6 +233,9 @@ namespace Dalamud.Injector private static DalamudStartInfo ExtractAndInitializeStartInfoFromArguments(DalamudStartInfo? startInfo, List args) { + int len; + string key; + if (startInfo == null) startInfo = new(); @@ -238,10 +245,10 @@ namespace Dalamud.Injector var defaultPluginDirectory = startInfo.DefaultPluginDirectory; var assetDirectory = startInfo.AssetDirectory; var delayInitializeMs = startInfo.DelayInitializeMs; + var languageStr = startInfo.Language.ToString().ToLowerInvariant(); for (var i = 2; i < args.Count; i++) { - string key; if (args[i].StartsWith(key = "--dalamud-working-directory=")) workingDirectory = args[i][key.Length..]; else if (args[i].StartsWith(key = "--dalamud-configuration-path=")) @@ -254,6 +261,8 @@ namespace Dalamud.Injector assetDirectory = args[i][key.Length..]; else if (args[i].StartsWith(key = "--dalamud-delay-initialize=")) delayInitializeMs = int.Parse(args[i][key.Length..]); + else if (args[i].StartsWith(key = "--dalamud-client-language=")) + languageStr = args[i][key.Length..].ToLowerInvariant(); else continue; @@ -270,6 +279,26 @@ namespace Dalamud.Injector defaultPluginDirectory ??= Path.Combine(xivlauncherDir, "devPlugins"); assetDirectory ??= Path.Combine(xivlauncherDir, "dalamudAssets", "dev"); + ClientLanguage clientLanguage; + if (languageStr[0..(len = Math.Min(languageStr.Length, (key = "english").Length))] == key[0..len]) + clientLanguage = ClientLanguage.English; + else if (languageStr[0..(len = Math.Min(languageStr.Length, (key = "japanese").Length))] == key[0..len]) + clientLanguage = ClientLanguage.Japanese; + else if (languageStr[0..(len = Math.Min(languageStr.Length, (key = "日本語").Length))] == key[0..len]) + clientLanguage = ClientLanguage.Japanese; + else if (languageStr[0..(len = Math.Min(languageStr.Length, (key = "german").Length))] == key[0..len]) + clientLanguage = ClientLanguage.German; + else if (languageStr[0..(len = Math.Min(languageStr.Length, (key = "deutsche").Length))] == key[0..len]) + clientLanguage = ClientLanguage.German; + else if (languageStr[0..(len = Math.Min(languageStr.Length, (key = "french").Length))] == key[0..len]) + clientLanguage = ClientLanguage.French; + else if (languageStr[0..(len = Math.Min(languageStr.Length, (key = "français").Length))] == key[0..len]) + clientLanguage = ClientLanguage.French; + else if (int.TryParse(languageStr, out var languageInt) && Enum.IsDefined((ClientLanguage)languageInt)) + clientLanguage = (ClientLanguage)languageInt; + else + throw new CommandLineException($"\"{languageStr}\" is not a valid supported language."); + return new() { WorkingDirectory = workingDirectory, @@ -277,7 +306,7 @@ namespace Dalamud.Injector PluginDirectory = pluginDirectory, DefaultPluginDirectory = defaultPluginDirectory, AssetDirectory = assetDirectory, - Language = ClientLanguage.English, + Language = clientLanguage, GameVersion = null, DelayInitializeMs = delayInitializeMs, }; @@ -303,12 +332,14 @@ namespace Dalamud.Injector Console.WriteLine("{0} [-g path/to/ffxiv_dx11.exe] [--game=path/to/ffxiv_dx11.exe]", exeSpaces); Console.WriteLine("{0} [-m entrypoint|inject] [--mode=entrypoint|inject]", exeSpaces); Console.WriteLine("{0} [--handle-owner=inherited-handle-value]", exeSpaces); + Console.WriteLine("{0} [--without-dalamud]", exeSpaces); Console.WriteLine("{0} [-- game_arg1=value1 game_arg2=value2 ...]", exeSpaces); } - Console.WriteLine("Specifying dalamud start info: [--dalamud-working-directory path] [--dalamud-configuration-path path]"); - Console.WriteLine(" [--dalamud-plugin-directory path] [--dalamud-dev-plugin-directory path]"); - Console.WriteLine(" [--dalamud-asset-directory path] [--dalamud-delay-initialize 0(ms)]"); + Console.WriteLine("Specifying dalamud start info: [--dalamud-working-directory=path] [--dalamud-configuration-path=path]"); + Console.WriteLine(" [--dalamud-plugin-directory=path] [--dalamud-dev-plugin-directory=path]"); + Console.WriteLine(" [--dalamud-asset-directory=path] [--dalamud-delay-initialize=0(ms)]"); + Console.WriteLine(" [--dalamud-client-language=0-3|j(apanese)|e(nglish)|d|g(erman)|f(rench)]"); return 0; } @@ -353,8 +384,7 @@ namespace Dalamud.Injector } else { - Log.Error("\"{0}\" is not a valid argument.", args[i]); - return -1; + throw new CommandLineException($"\"{args[i]}\" is not a command line argument."); } } @@ -366,8 +396,7 @@ namespace Dalamud.Injector if (!targetProcessSpecified) { - Log.Error("No target process has been specified."); - return -1; + throw new CommandLineException("No target process has been specified. Use -a(--all) option to inject to all ffxiv_dx11.exe processes."); } else if (!processes.Any()) { @@ -401,6 +430,7 @@ namespace Dalamud.Injector var useFakeArguments = false; var showHelp = args.Count <= 2; var handleOwner = IntPtr.Zero; + var withoutDalamud = false; var parsingGameArgument = false; for (var i = 2; i < args.Count; i++) @@ -412,42 +442,25 @@ namespace Dalamud.Injector } if (args[i] == "-h" || args[i] == "--help") - { showHelp = true; - } else if (args[i] == "-f" || args[i] == "--fake-arguments") - { useFakeArguments = true; - } + else if (args[i] == "--without-dalamud") + withoutDalamud = true; else if (args[i] == "-g") - { gamePath = args[++i]; - } else if (args[i].StartsWith("--game=")) - { gamePath = args[i].Split('=', 2)[1]; - } else if (args[i] == "-m") - { mode = args[++i]; - } else if (args[i].StartsWith("--mode=")) - { gamePath = args[i].Split('=', 2)[1]; - } else if (args[i].StartsWith("--handle-owner=")) - { handleOwner = IntPtr.Parse(args[i].Split('=', 2)[1]); - } else if (args[i] == "--") - { parsingGameArgument = true; - } else - { - Log.Error("No such command found: {0}", args[i]); - return -1; - } + throw new CommandLineException($"\"{args[i]}\" is not a command line argument."); } if (showHelp) @@ -467,8 +480,7 @@ namespace Dalamud.Injector } else { - Log.Error("Invalid mode: {0}", mode); - return -1; + throw new CommandLineException($"\"{mode}\" is not a valid Dalamud load mode."); } if (gamePath == null) @@ -526,7 +538,7 @@ namespace Dalamud.Injector "DEV.LobbyHost09=127.0.0.9", "DEV.LobbyPort09=54994", "SYS.Region=0", - "language=1", + $"language={(int)dalamudStartInfo.Language}", $"ver={gameVersion}", $"DEV.MaxEntitledExpansionID={maxEntitledExpansionId}", "DEV.GMServerHost=127.0.0.100", @@ -537,7 +549,7 @@ namespace Dalamud.Injector var gameArgumentString = string.Join(" ", gameArguments.Select(x => EncodeParameterArgument(x))); var process = NativeAclFix.LaunchGame(Path.GetDirectoryName(gamePath), gamePath, gameArgumentString, (Process p) => { - if (mode == "entrypoint") + if (!withoutDalamud && mode == "entrypoint") { var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath); Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo)); @@ -549,7 +561,7 @@ namespace Dalamud.Injector } }); - if (mode == "inject") + if (!withoutDalamud && mode == "inject") { var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath); Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo)); @@ -629,7 +641,7 @@ namespace Dalamud.Injector PluginDirectory = startInfo.PluginDirectory, DefaultPluginDirectory = startInfo.DefaultPluginDirectory, AssetDirectory = startInfo.AssetDirectory, - Language = ClientLanguage.English, + Language = startInfo.Language, GameVersion = gameVer, DelayInitializeMs = startInfo.DelayInitializeMs, }; @@ -738,5 +750,13 @@ namespace Dalamud.Injector return quoted.ToString(); } + + private class CommandLineException : Exception + { + public CommandLineException(string cause) + : base(cause) + { + } + } } } From 6c62bb1cff1ec8a3b9668723bdb8efcabccabfe6 Mon Sep 17 00:00:00 2001 From: kizer Date: Sat, 30 Apr 2022 03:13:40 +0900 Subject: [PATCH 08/19] build: 6.4.0.9 (#827) --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index ea6f5dddf..9c95f7f77 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 6.4.0.8 + 6.4.0.9 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 772ab40ada0d726fae35bd4fc5359ecadc289ef0 Mon Sep 17 00:00:00 2001 From: PunishedPineapple <50609717+PunishedPineapple@users.noreply.github.com> Date: Thu, 12 May 2022 03:32:14 -0500 Subject: [PATCH 09/19] Added digit separators to download count. (#837) --- .../Internal/Windows/PluginInstaller/PluginInstallerWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 838f03345..6c347b11d 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2253,7 +2253,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller public static string PluginBody_AuthorWithoutDownloadCount(string author) => Loc.Localize("InstallerAuthorWithoutDownloadCount", " by {0}").Format(author); - public static string PluginBody_AuthorWithDownloadCount(string author, long count) => Loc.Localize("InstallerAuthorWithDownloadCount", " by {0}, {1} downloads").Format(author, count); + public static string PluginBody_AuthorWithDownloadCount(string author, long count) => Loc.Localize("InstallerAuthorWithDownloadCount", " by {0} ({1} downloads)").Format(author, count.ToString("N0")); public static string PluginBody_AuthorWithDownloadCountUnavailable(string author) => Loc.Localize("InstallerAuthorWithDownloadCountUnavailable", " by {0}, download count unavailable").Format(author); From c2b43ad4ceed5d9beaae830ab9efe11866533e96 Mon Sep 17 00:00:00 2001 From: kizer Date: Thu, 12 May 2022 17:32:30 +0900 Subject: [PATCH 10/19] Settings: Show pt value instead of scale value for font (#834) --- .../Internal/Windows/SettingsWindow.cs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs index 8678b26bb..1a34ccd23 100644 --- a/Dalamud/Interface/Internal/Windows/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs @@ -24,9 +24,6 @@ namespace Dalamud.Interface.Internal.Windows /// internal class SettingsWindow : Window { - private const float MinScale = 0.3f; - private const float MaxScale = 3.0f; - private readonly string[] languages; private readonly string[] locLanguages; @@ -298,7 +295,7 @@ namespace Dalamud.Interface.Internal.Windows ImGui.Text(Loc.Localize("DalamudSettingsGlobalUiScale", "Global Font Scale")); ImGui.SameLine(); ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 3); - if (ImGui.Button(Loc.Localize("DalamudSettingsUiScalePreset6", "9.6pt") + "##DalamudSettingsGlobalUiScaleReset96")) + if (ImGui.Button("9.6pt##DalamudSettingsGlobalUiScaleReset96")) { this.globalUiScale = 9.6f / 12.0f; ImGui.GetIO().FontGlobalScale = this.globalUiScale; @@ -306,7 +303,7 @@ namespace Dalamud.Interface.Internal.Windows } ImGui.SameLine(); - if (ImGui.Button(Loc.Localize("DalamudSettingsUiScalePreset12", "Reset (12pt)") + "##DalamudSettingsGlobalUiScaleReset12")) + if (ImGui.Button("12pt##DalamudSettingsGlobalUiScaleReset12")) { this.globalUiScale = 1.0f; ImGui.GetIO().FontGlobalScale = this.globalUiScale; @@ -314,7 +311,7 @@ namespace Dalamud.Interface.Internal.Windows } ImGui.SameLine(); - if (ImGui.Button(Loc.Localize("DalamudSettingsUiScalePreset14", "14pt") + "##DalamudSettingsGlobalUiScaleReset14")) + if (ImGui.Button("14pt##DalamudSettingsGlobalUiScaleReset14")) { this.globalUiScale = 14.0f / 12.0f; ImGui.GetIO().FontGlobalScale = this.globalUiScale; @@ -322,7 +319,7 @@ namespace Dalamud.Interface.Internal.Windows } ImGui.SameLine(); - if (ImGui.Button(Loc.Localize("DalamudSettingsUiScalePreset18", "18pt") + "##DalamudSettingsGlobalUiScaleReset18")) + if (ImGui.Button("18pt##DalamudSettingsGlobalUiScaleReset18")) { this.globalUiScale = 18.0f / 12.0f; ImGui.GetIO().FontGlobalScale = this.globalUiScale; @@ -330,15 +327,17 @@ namespace Dalamud.Interface.Internal.Windows } ImGui.SameLine(); - if (ImGui.Button(Loc.Localize("DalamudSettingsUiScalePreset36", "36pt") + "##DalamudSettingsGlobalUiScaleReset36")) + if (ImGui.Button("36pt##DalamudSettingsGlobalUiScaleReset36")) { this.globalUiScale = 36.0f / 12.0f; ImGui.GetIO().FontGlobalScale = this.globalUiScale; interfaceManager.RebuildFonts(); } - if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref this.globalUiScale, 0.005f, MinScale, MaxScale, "%.2f", ImGuiSliderFlags.AlwaysClamp)) + var globalUiScaleInPt = 12f * this.globalUiScale; + if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref globalUiScaleInPt, 0.1f, 9.6f, 36f, "%.1fpt", ImGuiSliderFlags.AlwaysClamp)) { + this.globalUiScale = globalUiScaleInPt / 12f; ImGui.GetIO().FontGlobalScale = this.globalUiScale; interfaceManager.RebuildFonts(); } @@ -436,7 +435,7 @@ namespace Dalamud.Interface.Internal.Windows interfaceManager.RebuildFonts(); } - if (ImGui.DragFloat("##DalamudSettingsFontGammaDrag", ref this.fontGamma, 0.005f, MinScale, MaxScale, "%.2f", ImGuiSliderFlags.AlwaysClamp)) + if (ImGui.DragFloat("##DalamudSettingsFontGammaDrag", ref this.fontGamma, 0.005f, 0.3f, 3f, "%.2f", ImGuiSliderFlags.AlwaysClamp)) { interfaceManager.FontGammaOverride = this.fontGamma; interfaceManager.RebuildFonts(); From 8b7f9b58bf1bef946792e384d3559b3f196c09a7 Mon Sep 17 00:00:00 2001 From: kalilistic <35899782+kalilistic@users.noreply.github.com> Date: Thu, 12 May 2022 04:34:45 -0400 Subject: [PATCH 11/19] refactor: fix plugin internal style errors (#830) --- .editorconfig | 2 + Dalamud.sln.DotSettings | 4 + .../PluginInstaller/PluginChangelogEntry.cs | 1 + Dalamud/Plugin/Internal/LocalDevPlugin.cs | 164 -- Dalamud/Plugin/Internal/LocalPlugin.cs | 481 ---- Dalamud/Plugin/Internal/PluginManager.cs | 1985 ++++++++--------- Dalamud/Plugin/Internal/PluginRepository.cs | 120 - .../Internal/Types/AvailablePluginUpdate.cs | 59 +- .../Plugin/Internal/Types/LocalDevPlugin.cs | 162 ++ Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 501 +++++ .../Internal/Types/LocalPluginManifest.cs | 144 +- .../Plugin/Internal/Types/PluginManifest.cs | 274 ++- .../Plugin/Internal/Types/PluginRepository.cs | 118 + .../Internal/Types/PluginRepositoryState.cs | 41 +- Dalamud/Plugin/Internal/Types/PluginState.cs | 49 +- .../Internal/Types/PluginUpdateStatus.cs | 41 +- .../Internal/Types/RemotePluginManifest.cs | 25 +- 17 files changed, 2076 insertions(+), 2095 deletions(-) delete mode 100644 Dalamud/Plugin/Internal/LocalDevPlugin.cs delete mode 100644 Dalamud/Plugin/Internal/LocalPlugin.cs delete mode 100644 Dalamud/Plugin/Internal/PluginRepository.cs create mode 100644 Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs create mode 100644 Dalamud/Plugin/Internal/Types/LocalPlugin.cs create mode 100644 Dalamud/Plugin/Internal/Types/PluginRepository.cs diff --git a/.editorconfig b/.editorconfig index 109c9f406..1e4724862 100644 --- a/.editorconfig +++ b/.editorconfig @@ -123,10 +123,12 @@ resharper_foreach_can_be_partly_converted_to_query_using_another_get_enumerator_ resharper_invert_if_highlighting = none resharper_loop_can_be_converted_to_query_highlighting = none resharper_method_has_async_overload_highlighting = none +resharper_private_field_can_be_converted_to_local_variable_highlighting = none resharper_redundant_base_qualifier_highlighting = none resharper_suggest_var_or_type_built_in_types_highlighting = hint resharper_suggest_var_or_type_elsewhere_highlighting = hint resharper_suggest_var_or_type_simple_types_highlighting = hint +resharper_unused_auto_property_accessor_global_highlighting = none csharp_style_deconstructed_variable_declaration=true:silent [*.{appxmanifest,asax,ascx,aspx,axaml,axml,build,c,c++,cc,cginc,compute,config,cp,cpp,cs,cshtml,csproj,css,cu,cuh,cxx,dbml,discomap,dtd,h,hh,hlsl,hlsli,hlslinc,hpp,htm,html,hxx,inc,inl,ino,ipp,js,json,jsproj,jsx,lsproj,master,mpp,mq4,mq5,mqh,njsproj,nuspec,paml,proj,props,proto,razor,resjson,resw,resx,skin,StyleCop,targets,tasks,tpp,ts,tsx,usf,ush,vb,vbproj,xaml,xamlx,xml,xoml,xsd}] diff --git a/Dalamud.sln.DotSettings b/Dalamud.sln.DotSettings index 690f1a7ac..188e70c2b 100644 --- a/Dalamud.sln.DotSettings +++ b/Dalamud.sln.DotSettings @@ -50,11 +50,15 @@ True True True + True True True + True True True True + True + True True True True diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginChangelogEntry.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginChangelogEntry.cs index ce2353ec8..3e955e389 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginChangelogEntry.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginChangelogEntry.cs @@ -1,6 +1,7 @@ using System; using Dalamud.Plugin.Internal; +using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; namespace Dalamud.Interface.Internal.Windows.PluginInstaller diff --git a/Dalamud/Plugin/Internal/LocalDevPlugin.cs b/Dalamud/Plugin/Internal/LocalDevPlugin.cs deleted file mode 100644 index 27989531c..000000000 --- a/Dalamud/Plugin/Internal/LocalDevPlugin.cs +++ /dev/null @@ -1,164 +0,0 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -using Dalamud.Configuration.Internal; -using Dalamud.Interface.Internal.Notifications; -using Dalamud.Logging.Internal; -using Dalamud.Plugin.Internal.Types; - -namespace Dalamud.Plugin.Internal -{ - /// - /// This class represents a dev plugin and all facets of its lifecycle. - /// The DLL on disk, dependencies, loaded assembly, etc. - /// - internal class LocalDevPlugin : LocalPlugin, IDisposable - { - private static readonly ModuleLog Log = new("PLUGIN"); - - // Ref to Dalamud.Configuration.DevPluginSettings - private readonly DevPluginSettings devSettings; - - private FileSystemWatcher? fileWatcher; - private CancellationTokenSource fileWatcherTokenSource = new(); - private int reloadCounter; - - /// - /// Initializes a new instance of the class. - /// - /// Path to the DLL file. - /// The plugin manifest. - public LocalDevPlugin(FileInfo dllFile, LocalPluginManifest? manifest) - : base(dllFile, manifest) - { - var configuration = Service.Get(); - - if (!configuration.DevPluginSettings.TryGetValue(dllFile.FullName, out this.devSettings)) - { - configuration.DevPluginSettings[dllFile.FullName] = this.devSettings = new DevPluginSettings(); - configuration.Save(); - } - - if (this.AutomaticReload) - { - this.EnableReloading(); - } - } - - /// - /// Gets or sets a value indicating whether this dev plugin should start on boot. - /// - public bool StartOnBoot - { - get => this.devSettings.StartOnBoot; - set => this.devSettings.StartOnBoot = value; - } - - /// - /// Gets or sets a value indicating whether this dev plugin should reload on change. - /// - public bool AutomaticReload - { - get => this.devSettings.AutomaticReloading; - set - { - this.devSettings.AutomaticReloading = value; - - if (this.devSettings.AutomaticReloading) - { - this.EnableReloading(); - } - else - { - this.DisableReloading(); - } - } - } - - /// - public new void Dispose() - { - if (this.fileWatcher != null) - { - this.fileWatcher.Changed -= this.OnFileChanged; - this.fileWatcherTokenSource.Cancel(); - this.fileWatcher.Dispose(); - } - - base.Dispose(); - } - - /// - /// Configure this plugin for automatic reloading and enable it. - /// - public void EnableReloading() - { - if (this.fileWatcher == null) - { - this.fileWatcherTokenSource = new(); - this.fileWatcher = new FileSystemWatcher(this.DllFile.DirectoryName); - this.fileWatcher.Changed += this.OnFileChanged; - this.fileWatcher.Filter = this.DllFile.Name; - this.fileWatcher.NotifyFilter = NotifyFilters.LastWrite; - this.fileWatcher.EnableRaisingEvents = true; - } - } - - /// - /// Disable automatic reloading for this plugin. - /// - public void DisableReloading() - { - if (this.fileWatcher != null) - { - this.fileWatcherTokenSource.Cancel(); - this.fileWatcher.Changed -= this.OnFileChanged; - this.fileWatcher.Dispose(); - this.fileWatcher = null; - } - } - - private void OnFileChanged(object sender, FileSystemEventArgs args) - { - var current = Interlocked.Increment(ref this.reloadCounter); - - Task.Delay(500).ContinueWith( - _ => - { - if (this.fileWatcherTokenSource.IsCancellationRequested) - { - Log.Debug($"Skipping reload of {this.Name}, file watcher was cancelled."); - return; - } - - if (current != this.reloadCounter) - { - Log.Debug($"Skipping reload of {this.Name}, file has changed again."); - return; - } - - if (this.State != PluginState.Loaded) - { - Log.Debug($"Skipping reload of {this.Name}, state ({this.State}) is not {PluginState.Loaded}."); - return; - } - - var notificationManager = Service.Get(); - - try - { - this.Reload(); - notificationManager.AddNotification($"The DevPlugin '{this.Name} was reloaded successfully.", "Plugin reloaded!", NotificationType.Success); - } - catch (Exception ex) - { - Log.Error(ex, "DevPlugin reload failed."); - notificationManager.AddNotification($"The DevPlugin '{this.Name} could not be reloaded.", "Plugin reload failed!", NotificationType.Error); - } - }, - this.fileWatcherTokenSource.Token); - } - } -} diff --git a/Dalamud/Plugin/Internal/LocalPlugin.cs b/Dalamud/Plugin/Internal/LocalPlugin.cs deleted file mode 100644 index ba6297103..000000000 --- a/Dalamud/Plugin/Internal/LocalPlugin.cs +++ /dev/null @@ -1,481 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Reflection; - -using Dalamud.Configuration.Internal; -using Dalamud.Game; -using Dalamud.Game.Gui.Dtr; -using Dalamud.IoC.Internal; -using Dalamud.Logging.Internal; -using Dalamud.Plugin.Internal.Exceptions; -using Dalamud.Plugin.Internal.Loader; -using Dalamud.Plugin.Internal.Types; -using Dalamud.Utility; -using Dalamud.Utility.Signatures; - -namespace Dalamud.Plugin.Internal -{ - /// - /// This class represents a plugin and all facets of its lifecycle. - /// The DLL on disk, dependencies, loaded assembly, etc. - /// - internal class LocalPlugin : IDisposable - { - private static readonly ModuleLog Log = new("LOCALPLUGIN"); - - private readonly FileInfo manifestFile; - private readonly FileInfo disabledFile; - private readonly FileInfo testingFile; - - private PluginLoader? loader; - private Assembly? pluginAssembly; - private Type? pluginType; - private IDalamudPlugin? instance; - - /// - /// Initializes a new instance of the class. - /// - /// Path to the DLL file. - /// The plugin manifest. - public LocalPlugin(FileInfo dllFile, LocalPluginManifest? manifest) - { - if (dllFile.Name == "FFXIVClientStructs.Generators.dll") - { - // Could this be done another way? Sure. It is an extremely common source - // of errors in the log through, and should never be loaded as a plugin. - Log.Error($"Not a plugin: {dllFile.FullName}"); - throw new InvalidPluginException(dllFile); - } - - this.DllFile = dllFile; - this.State = PluginState.Unloaded; - - this.loader = PluginLoader.CreateFromAssemblyFile(this.DllFile.FullName, this.SetupLoaderConfig); - - try - { - this.pluginAssembly = this.loader.LoadDefaultAssembly(); - } - catch (Exception ex) - { - this.pluginAssembly = null; - this.pluginType = null; - this.loader.Dispose(); - - Log.Error(ex, $"Not a plugin: {this.DllFile.FullName}"); - throw new InvalidPluginException(this.DllFile); - } - - try - { - this.pluginType = this.pluginAssembly.GetTypes().FirstOrDefault(type => type.IsAssignableTo(typeof(IDalamudPlugin))); - } - catch (ReflectionTypeLoadException ex) - { - Log.Error(ex, $"Could not load one or more types when searching for IDalamudPlugin: {this.DllFile.FullName}"); - // Something blew up when parsing types, but we still want to look for IDalamudPlugin. Let Load() handle the error. - this.pluginType = ex.Types.FirstOrDefault(type => type.IsAssignableTo(typeof(IDalamudPlugin))); - } - - if (this.pluginType == default) - { - this.pluginAssembly = null; - this.pluginType = null; - this.loader.Dispose(); - - Log.Error($"Nothing inherits from IDalamudPlugin: {this.DllFile.FullName}"); - throw new InvalidPluginException(this.DllFile); - } - - var assemblyVersion = this.pluginAssembly.GetName().Version; - - // Although it is conditionally used here, we need to set the initial value regardless. - this.manifestFile = LocalPluginManifest.GetManifestFile(this.DllFile); - - // If the parameter manifest was null - if (manifest == null) - { - this.Manifest = new LocalPluginManifest() - { - Author = "developer", - Name = Path.GetFileNameWithoutExtension(this.DllFile.Name), - InternalName = Path.GetFileNameWithoutExtension(this.DllFile.Name), - AssemblyVersion = assemblyVersion, - Description = string.Empty, - ApplicableVersion = GameVersion.Any, - DalamudApiLevel = PluginManager.DalamudApiLevel, - IsHide = false, - }; - - // Save the manifest to disk so there won't be any problems later. - // We'll update the name property after it can be retrieved from the instance. - this.Manifest.Save(this.manifestFile); - } - else - { - this.Manifest = manifest; - } - - // This converts from the ".disabled" file feature to the manifest instead. - this.disabledFile = LocalPluginManifest.GetDisabledFile(this.DllFile); - if (this.disabledFile.Exists) - { - this.Manifest.Disabled = true; - this.disabledFile.Delete(); - } - - // This converts from the ".testing" file feature to the manifest instead. - this.testingFile = LocalPluginManifest.GetTestingFile(this.DllFile); - if (this.testingFile.Exists) - { - this.Manifest.Testing = true; - this.testingFile.Delete(); - } - - var pluginManager = Service.Get(); - this.IsBanned = pluginManager.IsManifestBanned(this.Manifest); - this.BanReason = pluginManager.GetBanReason(this.Manifest); - - this.SaveManifest(); - } - - /// - /// Gets the associated with this plugin. - /// - public DalamudPluginInterface? DalamudInterface { get; private set; } - - /// - /// Gets the path to the plugin DLL. - /// - public FileInfo DllFile { get; } - - /// - /// Gets the plugin manifest, if one exists. - /// - public LocalPluginManifest Manifest { get; private set; } - - /// - /// Gets or sets the current state of the plugin. - /// - public PluginState State { get; protected set; } - - /// - /// Gets the AssemblyName plugin, populated during . - /// - /// Plugin type. - public AssemblyName? AssemblyName { get; private set; } - - /// - /// Gets the plugin name, directly from the plugin or if it is not loaded from the manifest. - /// - public string Name => this.instance?.Name ?? this.Manifest.Name; - - /// - /// Gets an optional reason, if the plugin is banned. - /// - public string BanReason { get; } - - /// - /// Gets a value indicating whether the plugin is loaded and running. - /// - public bool IsLoaded => this.State == PluginState.Loaded; - - /// - /// Gets a value indicating whether the plugin is disabled. - /// - public bool IsDisabled => this.Manifest.Disabled; - - /// - /// Gets a value indicating whether this plugin's API level is out of date. - /// - public bool IsOutdated => this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel; - - /// - /// Gets a value indicating whether the plugin is for testing use only. - /// - public bool IsTesting => this.Manifest.IsTestingExclusive || this.Manifest.Testing; - - /// - /// Gets a value indicating whether this plugin has been banned. - /// - public bool IsBanned { get; } - - /// - /// Gets a value indicating whether this plugin is dev plugin. - /// - public bool IsDev => this is LocalDevPlugin; - - /// - public void Dispose() - { - this.instance?.Dispose(); - this.instance = null; - - this.DalamudInterface?.ExplicitDispose(); - this.DalamudInterface = null; - - this.pluginType = null; - this.pluginAssembly = null; - - this.loader?.Dispose(); - } - - /// - /// Load this plugin. - /// - /// The reason why this plugin is being loaded. - /// Load while reloading. - public void Load(PluginLoadReason reason, bool reloading = false) - { - var startInfo = Service.Get(); - var configuration = Service.Get(); - var pluginManager = Service.Get(); - - // Allowed: Unloaded - switch (this.State) - { - case PluginState.InProgress: - throw new InvalidPluginOperationException($"Unable to load {this.Name}, already working"); - case PluginState.Loaded: - throw new InvalidPluginOperationException($"Unable to load {this.Name}, already loaded"); - case PluginState.LoadError: - throw new InvalidPluginOperationException($"Unable to load {this.Name}, load previously faulted, unload first"); - case PluginState.UnloadError: - throw new InvalidPluginOperationException($"Unable to load {this.Name}, unload previously faulted, restart Dalamud"); - } - - if (pluginManager.IsManifestBanned(this.Manifest)) - throw new BannedPluginException($"Unable to load {this.Name}, banned"); - - if (this.Manifest.ApplicableVersion < startInfo.GameVersion) - throw new InvalidPluginOperationException($"Unable to load {this.Name}, no applicable version"); - - if (this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel && !configuration.LoadAllApiLevels) - throw new InvalidPluginOperationException($"Unable to load {this.Name}, incompatible API level"); - - if (this.Manifest.Disabled) - throw new InvalidPluginOperationException($"Unable to load {this.Name}, disabled"); - - this.State = PluginState.InProgress; - Log.Information($"Loading {this.DllFile.Name}"); - - if (this.DllFile.DirectoryName != null && File.Exists(Path.Combine(this.DllFile.DirectoryName, "Dalamud.dll"))) - { - Log.Error("==== IMPORTANT MESSAGE TO {0}, THE DEVELOPER OF {1} ====", this.Manifest.Author!, this.Manifest.InternalName); - Log.Error("YOU ARE INCLUDING DALAMUD DEPENDENCIES IN YOUR BUILDS!!!"); - Log.Error("You may not be able to load your plugin. \"False\" needs to be set in your csproj."); - Log.Error("If you are using ILMerge, do not merge anything other than your direct dependencies."); - Log.Error("Do not merge FFXIVClientStructs.Generators.dll."); - Log.Error("Please refer to https://github.com/goatcorp/Dalamud/discussions/603 for more information."); - } - - try - { - this.loader ??= PluginLoader.CreateFromAssemblyFile(this.DllFile.FullName, this.SetupLoaderConfig); - - if (reloading || this.IsDev) - { - if (this.IsDev) - { - // If a dev plugin is set to not autoload on boot, but we want to reload it at the arbitrary load - // time, we need to essentially "Unload" the plugin, but we can't call plugin.Unload because of the - // load state checks. Null any references to the assembly and types, then proceed with regular reload - // operations. - this.pluginAssembly = null; - this.pluginType = null; - } - - this.loader.Reload(); - - if (this.IsDev) - { - // Reload the manifest in-case there were changes here too. - var manifestFile = LocalPluginManifest.GetManifestFile(this.DllFile); - if (manifestFile.Exists) - { - this.Manifest = LocalPluginManifest.Load(manifestFile); - } - } - } - - // Load the assembly - this.pluginAssembly ??= this.loader.LoadDefaultAssembly(); - - this.AssemblyName = this.pluginAssembly.GetName(); - - // Find the plugin interface implementation. It is guaranteed to exist after checking in the ctor. - this.pluginType ??= this.pluginAssembly.GetTypes().First(type => type.IsAssignableTo(typeof(IDalamudPlugin))); - - // Check for any loaded plugins with the same assembly name - var assemblyName = this.pluginAssembly.GetName().Name; - foreach (var otherPlugin in pluginManager.InstalledPlugins) - { - // During hot-reloading, this plugin will be in the plugin list, and the instance will have been disposed - if (otherPlugin == this || otherPlugin.instance == null) - continue; - - var otherPluginAssemblyName = otherPlugin.instance.GetType().Assembly.GetName().Name; - if (otherPluginAssemblyName == assemblyName) - { - this.State = PluginState.Unloaded; - Log.Debug($"Duplicate assembly: {this.Name}"); - - throw new DuplicatePluginException(assemblyName); - } - } - - // Update the location for the Location and CodeBase patches - PluginManager.PluginLocations[this.pluginType.Assembly.FullName] = new(this.DllFile); - - this.DalamudInterface = new DalamudPluginInterface(this.pluginAssembly.GetName().Name!, this.DllFile, reason, this.IsDev); - - var ioc = Service.Get(); - this.instance = ioc.Create(this.pluginType, this.DalamudInterface) as IDalamudPlugin; - if (this.instance == null) - { - this.State = PluginState.LoadError; - this.DalamudInterface.ExplicitDispose(); - Log.Error($"Error while loading {this.Name}, failed to bind and call the plugin constructor"); - return; - } - - SignatureHelper.Initialise(this.instance); - - // In-case the manifest name was a placeholder. Can occur when no manifest was included. - if (this.instance.Name != this.Manifest.Name) - { - this.Manifest.Name = this.instance.Name; - this.Manifest.Save(this.manifestFile); - } - - this.State = PluginState.Loaded; - Log.Information($"Finished loading {this.DllFile.Name}"); - } - catch (Exception ex) - { - this.State = PluginState.LoadError; - Log.Error(ex, $"Error while loading {this.Name}"); - - throw; - } - } - - /// - /// Unload this plugin. This is the same as dispose, but without the "disposed" connotations. This object should stay - /// in the plugin list until it has been actually disposed. - /// - /// Unload while reloading. - public void Unload(bool reloading = false) - { - // Allowed: Loaded, LoadError(we are cleaning this up while we're at it) - switch (this.State) - { - case PluginState.InProgress: - throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already working"); - case PluginState.Unloaded: - throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already unloaded"); - case PluginState.UnloadError: - throw new InvalidPluginOperationException($"Unable to unload {this.Name}, unload previously faulted, restart Dalamud"); - } - - try - { - this.State = PluginState.InProgress; - Log.Information($"Unloading {this.DllFile.Name}"); - - this.instance?.Dispose(); - this.instance = null; - - this.DalamudInterface?.ExplicitDispose(); - this.DalamudInterface = null; - - this.pluginType = null; - this.pluginAssembly = null; - - if (!reloading) - { - this.loader?.Dispose(); - this.loader = null; - } - - this.State = PluginState.Unloaded; - Log.Information($"Finished unloading {this.DllFile.Name}"); - } - catch (Exception ex) - { - this.State = PluginState.UnloadError; - Log.Error(ex, $"Error while unloading {this.Name}"); - - throw; - } - } - - /// - /// Reload this plugin. - /// - public void Reload() - { - this.Unload(true); - - // We need to handle removed DTR nodes here, as otherwise, plugins will not be able to re-add their bar entries after updates. - var dtr = Service.Get(); - dtr.HandleRemovedNodes(); - - this.Load(PluginLoadReason.Reload, true); - } - - /// - /// Revert a disable. Must be unloaded first, does not load. - /// - public void Enable() - { - // Allowed: Unloaded, UnloadError - switch (this.State) - { - case PluginState.InProgress: - case PluginState.Loaded: - case PluginState.LoadError: - throw new InvalidPluginOperationException($"Unable to enable {this.Name}, still loaded"); - } - - if (!this.Manifest.Disabled) - throw new InvalidPluginOperationException($"Unable to enable {this.Name}, not disabled"); - - this.Manifest.Disabled = false; - this.SaveManifest(); - } - - /// - /// Disable this plugin, must be unloaded first. - /// - public void Disable() - { - // Allowed: Unloaded, UnloadError - switch (this.State) - { - case PluginState.InProgress: - case PluginState.Loaded: - case PluginState.LoadError: - throw new InvalidPluginOperationException($"Unable to disable {this.Name}, still loaded"); - } - - if (this.Manifest.Disabled) - throw new InvalidPluginOperationException($"Unable to disable {this.Name}, already disabled"); - - this.Manifest.Disabled = true; - this.SaveManifest(); - } - - private void SetupLoaderConfig(LoaderConfig config) - { - config.IsUnloadable = true; - config.LoadInMemory = true; - config.PreferSharedTypes = false; - config.SharedAssemblies.Add(typeof(Lumina.GameData).Assembly.GetName()); - config.SharedAssemblies.Add(typeof(Lumina.Excel.ExcelSheetImpl).Assembly.GetName()); - } - - private void SaveManifest() => this.Manifest.Save(this.manifestFile); - } -} diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index f98e122f5..25ec9a11c 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -21,441 +21,303 @@ using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; using Newtonsoft.Json; -namespace Dalamud.Plugin.Internal +namespace Dalamud.Plugin.Internal; + +/// +/// Class responsible for loading and unloading plugins. +/// +internal partial class PluginManager : IDisposable { /// - /// Class responsible for loading and unloading plugins. + /// The current Dalamud API level, used to handle breaking changes. Only plugins with this level will be loaded. /// - internal partial class PluginManager : IDisposable + public const int DalamudApiLevel = 6; + + private static readonly ModuleLog Log = new("PLUGINM"); + + private readonly DirectoryInfo pluginDirectory; + private readonly DirectoryInfo devPluginDirectory; + private readonly BannedPlugin[] bannedPlugins; + + /// + /// Initializes a new instance of the class. + /// + public PluginManager() { - /// - /// The current Dalamud API level, used to handle breaking changes. Only plugins with this level will be loaded. - /// - public const int DalamudApiLevel = 6; + var startInfo = Service.Get(); + var configuration = Service.Get(); - private static readonly ModuleLog Log = new("PLUGINM"); + this.pluginDirectory = new DirectoryInfo(startInfo.PluginDirectory); + this.devPluginDirectory = new DirectoryInfo(startInfo.DefaultPluginDirectory); - private readonly DirectoryInfo pluginDirectory; - private readonly DirectoryInfo devPluginDirectory; - private readonly BannedPlugin[] bannedPlugins; + if (!this.pluginDirectory.Exists) + this.pluginDirectory.Create(); - /// - /// Initializes a new instance of the class. - /// - public PluginManager() + if (!this.devPluginDirectory.Exists) + this.devPluginDirectory.Create(); + + this.SafeMode = EnvironmentConfiguration.DalamudNoPlugins || configuration.PluginSafeMode; + if (this.SafeMode) { - var startInfo = Service.Get(); - var configuration = Service.Get(); - - this.pluginDirectory = new DirectoryInfo(startInfo.PluginDirectory); - this.devPluginDirectory = new DirectoryInfo(startInfo.DefaultPluginDirectory); - - if (!this.pluginDirectory.Exists) - this.pluginDirectory.Create(); - - if (!this.devPluginDirectory.Exists) - this.devPluginDirectory.Create(); - - this.SafeMode = EnvironmentConfiguration.DalamudNoPlugins || configuration.PluginSafeMode; - if (this.SafeMode) - { - configuration.PluginSafeMode = false; - configuration.Save(); - } - - this.PluginConfigs = new PluginConfigurations(Path.Combine(Path.GetDirectoryName(startInfo.ConfigurationPath) ?? string.Empty, "pluginConfigs")); - - var bannedPluginsJson = File.ReadAllText(Path.Combine(startInfo.AssetDirectory, "UIRes", "bannedplugin.json")); - this.bannedPlugins = JsonConvert.DeserializeObject(bannedPluginsJson) ?? Array.Empty(); - - this.ApplyPatches(); + configuration.PluginSafeMode = false; + configuration.Save(); } - /// - /// An event that fires when the installed plugins have changed. - /// - public event Action? OnInstalledPluginsChanged; + this.PluginConfigs = new PluginConfigurations(Path.Combine(Path.GetDirectoryName(startInfo.ConfigurationPath) ?? string.Empty, "pluginConfigs")); - /// - /// An event that fires when the available plugins have changed. - /// - public event Action? OnAvailablePluginsChanged; + var bannedPluginsJson = File.ReadAllText(Path.Combine(startInfo.AssetDirectory, "UIRes", "bannedplugin.json")); + this.bannedPlugins = JsonConvert.DeserializeObject(bannedPluginsJson) ?? Array.Empty(); - /// - /// Gets a list of all loaded plugins. - /// - public ImmutableList InstalledPlugins { get; private set; } = ImmutableList.Create(); + this.ApplyPatches(); + } - /// - /// Gets a list of all available plugins. - /// - public ImmutableList AvailablePlugins { get; private set; } = ImmutableList.Create(); + /// + /// An event that fires when the installed plugins have changed. + /// + public event Action? OnInstalledPluginsChanged; - /// - /// Gets a list of all plugins with an available update. - /// - public ImmutableList UpdatablePlugins { get; private set; } = ImmutableList.Create(); + /// + /// An event that fires when the available plugins have changed. + /// + public event Action? OnAvailablePluginsChanged; - /// - /// Gets a list of all plugin repositories. The main repo should always be first. - /// - public List Repos { get; private set; } = new(); + /// + /// Gets a list of all loaded plugins. + /// + public ImmutableList InstalledPlugins { get; private set; } = ImmutableList.Create(); - /// - /// Gets a value indicating whether plugins are not still loading from boot. - /// - public bool PluginsReady { get; private set; } + /// + /// Gets a list of all available plugins. + /// + public ImmutableList AvailablePlugins { get; private set; } = ImmutableList.Create(); - /// - /// Gets a value indicating whether all added repos are not in progress. - /// - public bool ReposReady => this.Repos.All(repo => repo.State != PluginRepositoryState.InProgress); + /// + /// Gets a list of all plugins with an available update. + /// + public ImmutableList UpdatablePlugins { get; private set; } = ImmutableList.Create(); - /// - /// Gets a value indicating whether the plugin manager started in safe mode. - /// - public bool SafeMode { get; init; } + /// + /// Gets a list of all plugin repositories. The main repo should always be first. + /// + public List Repos { get; private set; } = new(); - /// - /// Gets the object used when initializing plugins. - /// - public PluginConfigurations PluginConfigs { get; } + /// + /// Gets a value indicating whether plugins are not still loading from boot. + /// + public bool PluginsReady { get; private set; } - /// - /// Print to chat any plugin updates and whether they were successful. - /// - /// The list of updated plugin metadata. - /// The header text to send to chat prior to any update info. - public static void PrintUpdatedPlugins(List? updateMetadata, string header) + /// + /// Gets a value indicating whether all added repos are not in progress. + /// + public bool ReposReady => this.Repos.All(repo => repo.State != PluginRepositoryState.InProgress); + + /// + /// Gets a value indicating whether the plugin manager started in safe mode. + /// + public bool SafeMode { get; init; } + + /// + /// Gets the object used when initializing plugins. + /// + public PluginConfigurations PluginConfigs { get; } + + /// + /// Print to chat any plugin updates and whether they were successful. + /// + /// The list of updated plugin metadata. + /// The header text to send to chat prior to any update info. + public static void PrintUpdatedPlugins(List? updateMetadata, string header) + { + var chatGui = Service.Get(); + + if (updateMetadata is { Count: > 0 }) { - var chatGui = Service.Get(); + chatGui.Print(header); - if (updateMetadata is { Count: > 0 }) + foreach (var metadata in updateMetadata) { - chatGui.Print(header); - - foreach (var metadata in updateMetadata) + if (metadata.WasUpdated) { - if (metadata.WasUpdated) + chatGui.Print(Locs.DalamudPluginUpdateSuccessful(metadata.Name, metadata.Version)); + } + else + { + chatGui.PrintChat(new XivChatEntry { - chatGui.Print(Locs.DalamudPluginUpdateSuccessful(metadata.Name, metadata.Version)); - } - else - { - chatGui.PrintChat(new XivChatEntry - { - Message = Locs.DalamudPluginUpdateFailed(metadata.Name, metadata.Version), - Type = XivChatType.Urgent, - }); - } + Message = Locs.DalamudPluginUpdateFailed(metadata.Name, metadata.Version), + Type = XivChatType.Urgent, + }); } } } + } - /// - /// For a given manifest, determine if the testing version should be used over the normal version. - /// The higher of the two versions is calculated after checking other settings. - /// - /// Manifest to check. - /// A value indicating whether testing should be used. - public static bool UseTesting(PluginManifest manifest) - { - var configuration = Service.Get(); - - if (!configuration.DoPluginTest) - return false; - - if (manifest.IsTestingExclusive) - return true; - - var av = manifest.AssemblyVersion; - var tv = manifest.TestingAssemblyVersion; - var hasTv = tv != null; - - if (hasTv) - { - return tv > av; - } + /// + /// For a given manifest, determine if the testing version should be used over the normal version. + /// The higher of the two versions is calculated after checking other settings. + /// + /// Manifest to check. + /// A value indicating whether testing should be used. + public static bool UseTesting(PluginManifest manifest) + { + var configuration = Service.Get(); + if (!configuration.DoPluginTest) return false; + + if (manifest.IsTestingExclusive) + return true; + + var av = manifest.AssemblyVersion; + var tv = manifest.TestingAssemblyVersion; + var hasTv = tv != null; + + if (hasTv) + { + return tv > av; } - /// - /// Gets a value indicating whether the given repo manifest should be visible to the user. - /// - /// Repo manifest. - /// If the manifest is visible. - public static bool IsManifestVisible(RemotePluginManifest manifest) + return false; + } + + /// + /// Gets a value indicating whether the given repo manifest should be visible to the user. + /// + /// Repo manifest. + /// If the manifest is visible. + public static bool IsManifestVisible(RemotePluginManifest manifest) + { + var configuration = Service.Get(); + + // Hidden by user + if (configuration.HiddenPluginInternalName.Contains(manifest.InternalName)) + return false; + + // Hidden by manifest + return !manifest.IsHide; + } + + /// + public void Dispose() + { + foreach (var plugin in this.InstalledPlugins) { - var configuration = Service.Get(); - - // Hidden by user - if (configuration.HiddenPluginInternalName.Contains(manifest.InternalName)) - return false; - - // Hidden by manifest - return !manifest.IsHide; + try + { + plugin.Dispose(); + } + catch (Exception ex) + { + Log.Error(ex, $"Error disposing {plugin.Name}"); + } } - /// - public void Dispose() + this.assemblyLocationMonoHook?.Dispose(); + this.assemblyCodeBaseMonoHook?.Dispose(); + } + + /// + /// Set the list of repositories to use and downloads their contents. + /// Should be called when the Settings window has been updated or at instantiation. + /// + /// Whether the available plugins changed event should be sent after. + /// A representing the asynchronous operation. + public async Task SetPluginReposFromConfigAsync(bool notify) + { + var configuration = Service.Get(); + + var repos = new List() { PluginRepository.MainRepo }; + repos.AddRange(configuration.ThirdRepoList + .Where(repo => repo.IsEnabled) + .Select(repo => new PluginRepository(repo.Url, repo.IsEnabled))); + + this.Repos = repos; + await this.ReloadPluginMastersAsync(notify); + } + + /// + /// Load all plugins, sorted by priority. Any plugins with no explicit definition file or a negative priority + /// are loaded asynchronously. + /// + /// + /// This should only be called during Dalamud startup. + /// + public void LoadAllPlugins() + { + if (this.SafeMode) { - foreach (var plugin in this.InstalledPlugins) + Log.Information("PluginSafeMode was enabled, not loading any plugins."); + return; + } + + var configuration = Service.Get(); + + var pluginDefs = new List(); + var devPluginDefs = new List(); + + if (!this.pluginDirectory.Exists) + this.pluginDirectory.Create(); + + if (!this.devPluginDirectory.Exists) + this.devPluginDirectory.Create(); + + // Add installed plugins. These are expected to be in a specific format so we can look for exactly that. + foreach (var pluginDir in this.pluginDirectory.GetDirectories()) + { + foreach (var versionDir in pluginDir.GetDirectories()) + { + var dllFile = new FileInfo(Path.Combine(versionDir.FullName, $"{pluginDir.Name}.dll")); + var manifestFile = LocalPluginManifest.GetManifestFile(dllFile); + + if (!manifestFile.Exists) + continue; + + var manifest = LocalPluginManifest.Load(manifestFile); + + pluginDefs.Add(new PluginDef(dllFile, manifest, false)); + } + } + + // devPlugins are more freeform. Look for any dll and hope to get lucky. + var devDllFiles = this.devPluginDirectory.GetFiles("*.dll", SearchOption.AllDirectories).ToList(); + + foreach (var setting in configuration.DevPluginLoadLocations) + { + if (!setting.IsEnabled) + continue; + + if (Directory.Exists(setting.Path)) + { + devDllFiles.AddRange(new DirectoryInfo(setting.Path).GetFiles("*.dll", SearchOption.AllDirectories)); + } + else if (File.Exists(setting.Path)) + { + devDllFiles.Add(new FileInfo(setting.Path)); + } + } + + foreach (var dllFile in devDllFiles) + { + // Manifests are not required for devPlugins. the Plugin type will handle any null manifests. + var manifestFile = LocalPluginManifest.GetManifestFile(dllFile); + var manifest = manifestFile.Exists ? LocalPluginManifest.Load(manifestFile) : null; + devPluginDefs.Add(new PluginDef(dllFile, manifest, true)); + } + + // Sort for load order - unloaded definitions have default priority of 0 + pluginDefs.Sort(PluginDef.Sorter); + devPluginDefs.Sort(PluginDef.Sorter); + + // Dev plugins should load first. + pluginDefs.InsertRange(0, devPluginDefs); + + void LoadPlugins(IEnumerable pluginDefsList) + { + foreach (var pluginDef in pluginDefsList) { try { - plugin.Dispose(); - } - catch (Exception ex) - { - Log.Error(ex, $"Error disposing {plugin.Name}"); - } - } - - this.assemblyLocationMonoHook?.Dispose(); - this.assemblyCodeBaseMonoHook?.Dispose(); - } - - /// - /// Set the list of repositories to use and downloads their contents. - /// Should be called when the Settings window has been updated or at instantiation. - /// - /// Whether the available plugins changed event should be sent after. - /// A representing the asynchronous operation. - public async Task SetPluginReposFromConfigAsync(bool notify) - { - var configuration = Service.Get(); - - var repos = new List() { PluginRepository.MainRepo }; - repos.AddRange(configuration.ThirdRepoList - .Where(repo => repo.IsEnabled) - .Select(repo => new PluginRepository(repo.Url, repo.IsEnabled))); - - this.Repos = repos; - await this.ReloadPluginMastersAsync(notify); - } - - /// - /// Load all plugins, sorted by priority. Any plugins with no explicit definition file or a negative priority - /// are loaded asynchronously. - /// - /// - /// This should only be called during Dalamud startup. - /// - public void LoadAllPlugins() - { - if (this.SafeMode) - { - Log.Information("PluginSafeMode was enabled, not loading any plugins."); - return; - } - - var configuration = Service.Get(); - - var pluginDefs = new List(); - var devPluginDefs = new List(); - - if (!this.pluginDirectory.Exists) - this.pluginDirectory.Create(); - - if (!this.devPluginDirectory.Exists) - this.devPluginDirectory.Create(); - - // Add installed plugins. These are expected to be in a specific format so we can look for exactly that. - foreach (var pluginDir in this.pluginDirectory.GetDirectories()) - { - foreach (var versionDir in pluginDir.GetDirectories()) - { - var dllFile = new FileInfo(Path.Combine(versionDir.FullName, $"{pluginDir.Name}.dll")); - var manifestFile = LocalPluginManifest.GetManifestFile(dllFile); - - if (!manifestFile.Exists) - continue; - - var manifest = LocalPluginManifest.Load(manifestFile); - - pluginDefs.Add(new PluginDef(dllFile, manifest, false)); - } - } - - // devPlugins are more freeform. Look for any dll and hope to get lucky. - var devDllFiles = this.devPluginDirectory.GetFiles("*.dll", SearchOption.AllDirectories).ToList(); - - foreach (var setting in configuration.DevPluginLoadLocations) - { - if (!setting.IsEnabled) - continue; - - if (Directory.Exists(setting.Path)) - { - devDllFiles.AddRange(new DirectoryInfo(setting.Path).GetFiles("*.dll", SearchOption.AllDirectories)); - } - else if (File.Exists(setting.Path)) - { - devDllFiles.Add(new FileInfo(setting.Path)); - } - } - - foreach (var dllFile in devDllFiles) - { - // Manifests are not required for devPlugins. the Plugin type will handle any null manifests. - var manifestFile = LocalPluginManifest.GetManifestFile(dllFile); - var manifest = manifestFile.Exists ? LocalPluginManifest.Load(manifestFile) : null; - devPluginDefs.Add(new PluginDef(dllFile, manifest, true)); - } - - // Sort for load order - unloaded definitions have default priority of 0 - pluginDefs.Sort(PluginDef.Sorter); - devPluginDefs.Sort(PluginDef.Sorter); - - // Dev plugins should load first. - pluginDefs.InsertRange(0, devPluginDefs); - - void LoadPlugins(IEnumerable pluginDefsList) - { - foreach (var pluginDef in pluginDefsList) - { - try - { - this.LoadPlugin(pluginDef.DllFile, pluginDef.Manifest, PluginLoadReason.Boot, pluginDef.IsDev, isBoot: true); - } - catch (InvalidPluginException) - { - // Not a plugin - } - catch (Exception ex) - { - Log.Error(ex, "During boot plugin load, an unexpected error occurred"); - } - } - } - - // Load sync plugins - var syncPlugins = pluginDefs.Where(def => def.Manifest?.LoadPriority > 0); - LoadPlugins(syncPlugins); - - var asyncPlugins = pluginDefs.Where(def => def.Manifest == null || def.Manifest.LoadPriority <= 0); - Task.Run(() => LoadPlugins(asyncPlugins)) - .ContinueWith(_ => - { - this.PluginsReady = true; - this.NotifyInstalledPluginsChanged(); - }); - } - - /// - /// Reload all loaded plugins. - /// - public void ReloadAllPlugins() - { - var aggregate = new List(); - - foreach (var plugin in this.InstalledPlugins) - { - if (plugin.IsLoaded) - { - try - { - plugin.Reload(); - } - catch (Exception ex) - { - Log.Error(ex, "Error during reload all"); - - aggregate.Add(ex); - } - } - } - - if (aggregate.Any()) - { - throw new AggregateException(aggregate); - } - } - - /// - /// Reload the PluginMaster for each repo, filter, and event that the list has updated. - /// - /// Whether to notify that available plugins have changed afterwards. - /// A representing the asynchronous operation. - public async Task ReloadPluginMastersAsync(bool notify = true) - { - await Task.WhenAll(this.Repos.Select(repo => repo.ReloadPluginMasterAsync())); - - this.RefilterPluginMasters(notify); - } - - /// - /// Apply visibility and eligibility filters to the available plugins, then event that the list has updated. - /// - /// Whether to notify that available plugins have changed afterwards. - public void RefilterPluginMasters(bool notify = true) - { - this.AvailablePlugins = this.Repos - .SelectMany(repo => repo.PluginMaster) - .Where(this.IsManifestEligible) - .Where(IsManifestVisible) - .ToImmutableList(); - - if (notify) - { - this.NotifyAvailablePluginsChanged(); - } - } - - /// - /// Scan the devPlugins folder for new DLL files that are not already loaded into the manager. They are not loaded, - /// only shown as disabled in the installed plugins window. This is a modified version of LoadAllPlugins that works - /// a little differently. - /// - public void ScanDevPlugins() - { - if (this.SafeMode) - { - Log.Information("PluginSafeMode was enabled, not scanning any dev plugins."); - return; - } - - var configuration = Service.Get(); - - if (!this.devPluginDirectory.Exists) - this.devPluginDirectory.Create(); - - // devPlugins are more freeform. Look for any dll and hope to get lucky. - var devDllFiles = this.devPluginDirectory.GetFiles("*.dll", SearchOption.AllDirectories).ToList(); - - foreach (var setting in configuration.DevPluginLoadLocations) - { - if (!setting.IsEnabled) - continue; - - if (Directory.Exists(setting.Path)) - { - devDllFiles.AddRange(new DirectoryInfo(setting.Path).GetFiles("*.dll", SearchOption.AllDirectories)); - } - else if (File.Exists(setting.Path)) - { - devDllFiles.Add(new FileInfo(setting.Path)); - } - } - - var listChanged = false; - - foreach (var dllFile in devDllFiles) - { - // This file is already known to us - if (this.InstalledPlugins.Any(lp => lp.DllFile.FullName == dllFile.FullName)) - continue; - - // Manifests are not required for devPlugins. the Plugin type will handle any null manifests. - var manifestFile = LocalPluginManifest.GetManifestFile(dllFile); - var manifest = manifestFile.Exists ? LocalPluginManifest.Load(manifestFile) : null; - - try - { - // Add them to the list and let the user decide, nothing is auto-loaded. - this.LoadPlugin(dllFile, manifest, PluginLoadReason.Installer, isDev: true, doNotLoad: true); - listChanged = true; + this.LoadPlugin(pluginDef.DllFile, pluginDef.Manifest, PluginLoadReason.Boot, pluginDef.IsDev, isBoot: true); } catch (InvalidPluginException) { @@ -463,675 +325,812 @@ namespace Dalamud.Plugin.Internal } catch (Exception ex) { - Log.Error(ex, $"During devPlugin scan, an unexpected error occurred"); + Log.Error(ex, "During boot plugin load, an unexpected error occurred"); } } + } - if (listChanged) + // Load sync plugins + var syncPlugins = pluginDefs.Where(def => def.Manifest?.LoadPriority > 0); + LoadPlugins(syncPlugins); + + var asyncPlugins = pluginDefs.Where(def => def.Manifest == null || def.Manifest.LoadPriority <= 0); + Task.Run(() => LoadPlugins(asyncPlugins)) + .ContinueWith(_ => + { + this.PluginsReady = true; this.NotifyInstalledPluginsChanged(); - } + }); + } - /// - /// Install a plugin from a repository and load it. - /// - /// The plugin definition. - /// If the testing version should be used. - /// The reason this plugin was loaded. - /// A representing the asynchronous operation. - public async Task InstallPluginAsync(RemotePluginManifest repoManifest, bool useTesting, PluginLoadReason reason) + /// + /// Reload all loaded plugins. + /// + public void ReloadAllPlugins() + { + var aggregate = new List(); + + foreach (var plugin in this.InstalledPlugins) { - Log.Debug($"Installing plugin {repoManifest.Name} (testing={useTesting})"); - - var downloadUrl = useTesting ? repoManifest.DownloadLinkTesting : repoManifest.DownloadLinkInstall; - var version = useTesting ? repoManifest.TestingAssemblyVersion : repoManifest.AssemblyVersion; - - var response = await Util.HttpClient.GetAsync(downloadUrl); - response.EnsureSuccessStatusCode(); - - var outputDir = new DirectoryInfo(Path.Combine(this.pluginDirectory.FullName, repoManifest.InternalName, version?.ToString() ?? string.Empty)); - - try - { - if (outputDir.Exists) - outputDir.Delete(true); - - outputDir.Create(); - } - catch - { - // ignored, since the plugin may be loaded already - } - - Log.Debug($"Extracting to {outputDir}"); - // This throws an error, even with overwrite=false - // ZipFile.ExtractToDirectory(tempZip.FullName, outputDir.FullName, false); - using (var archive = new ZipArchive(await response.Content.ReadAsStreamAsync())) - { - foreach (var zipFile in archive.Entries) - { - var outputFile = new FileInfo(Path.GetFullPath(Path.Combine(outputDir.FullName, zipFile.FullName))); - - if (!outputFile.FullName.StartsWith(outputDir.FullName, StringComparison.OrdinalIgnoreCase)) - { - throw new IOException("Trying to extract file outside of destination directory. See this link for more info: https://snyk.io/research/zip-slip-vulnerability"); - } - - if (outputFile.Directory == null) - { - throw new IOException("Output directory invalid."); - } - - if (zipFile.Name.IsNullOrEmpty()) - { - // Assuming Empty for Directory - Log.Verbose($"ZipFile name is null or empty, treating as a directory: {outputFile.Directory.FullName}"); - Directory.CreateDirectory(outputFile.Directory.FullName); - continue; - } - - // Ensure directory is created - Directory.CreateDirectory(outputFile.Directory.FullName); - - try - { - zipFile.ExtractToFile(outputFile.FullName, true); - } - catch (Exception ex) - { - if (outputFile.Extension.EndsWith("dll")) - { - throw new IOException($"Could not overwrite {zipFile.Name}: {ex.Message}"); - } - - Log.Error($"Could not overwrite {zipFile.Name}: {ex.Message}"); - } - } - } - - var dllFile = LocalPluginManifest.GetPluginFile(outputDir, repoManifest); - var manifestFile = LocalPluginManifest.GetManifestFile(dllFile); - - // We need to save the repoManifest due to how the repo fills in some fields that authors are not expected to use. - File.WriteAllText(manifestFile.FullName, JsonConvert.SerializeObject(repoManifest, Formatting.Indented)); - - // Reload as a local manifest, add some attributes, and save again. - var manifest = LocalPluginManifest.Load(manifestFile); - - if (useTesting) - { - manifest.Testing = true; - } - - if (repoManifest.SourceRepo.IsThirdParty) - { - // Only document the url if it came from a third party repo. - manifest.InstalledFromUrl = repoManifest.SourceRepo.PluginMasterUrl; - } - - manifest.Save(manifestFile); - - Log.Information($"Installed plugin {manifest.Name} (testing={useTesting})"); - - var plugin = this.LoadPlugin(dllFile, manifest, reason); - - this.NotifyInstalledPluginsChanged(); - return plugin; - } - - /// - /// Load a plugin. - /// - /// The associated with the main assembly of this plugin. - /// The already loaded definition, if available. - /// The reason this plugin was loaded. - /// If this plugin should support development features. - /// If this plugin is being loaded at boot. - /// Don't load the plugin, just don't do it. - /// The loaded plugin. - public LocalPlugin LoadPlugin(FileInfo dllFile, LocalPluginManifest? manifest, PluginLoadReason reason, bool isDev = false, bool isBoot = false, bool doNotLoad = false) - { - var name = manifest?.Name ?? dllFile.Name; - var loadPlugin = !doNotLoad; - - LocalPlugin plugin; - - if (isDev) - { - Log.Information($"Loading dev plugin {name}"); - var devPlugin = new LocalDevPlugin(dllFile, manifest); - loadPlugin &= !isBoot || devPlugin.StartOnBoot; - - // If we're not loading it, make sure it's disabled - if (!loadPlugin && !devPlugin.IsDisabled) - devPlugin.Disable(); - - plugin = devPlugin; - } - else - { - Log.Information($"Loading plugin {name}"); - plugin = new LocalPlugin(dllFile, manifest); - } - - if (loadPlugin) + if (plugin.IsLoaded) { try { - if (plugin.IsDisabled) - plugin.Enable(); - - plugin.Load(reason); + plugin.Reload(); } - catch (InvalidPluginException) + catch (Exception ex) + { + Log.Error(ex, "Error during reload all"); + + aggregate.Add(ex); + } + } + } + + if (aggregate.Any()) + { + throw new AggregateException(aggregate); + } + } + + /// + /// Reload the PluginMaster for each repo, filter, and event that the list has updated. + /// + /// Whether to notify that available plugins have changed afterwards. + /// A representing the asynchronous operation. + public async Task ReloadPluginMastersAsync(bool notify = true) + { + await Task.WhenAll(this.Repos.Select(repo => repo.ReloadPluginMasterAsync())); + + this.RefilterPluginMasters(notify); + } + + /// + /// Apply visibility and eligibility filters to the available plugins, then event that the list has updated. + /// + /// Whether to notify that available plugins have changed afterwards. + public void RefilterPluginMasters(bool notify = true) + { + this.AvailablePlugins = this.Repos + .SelectMany(repo => repo.PluginMaster) + .Where(this.IsManifestEligible) + .Where(IsManifestVisible) + .ToImmutableList(); + + if (notify) + { + this.NotifyAvailablePluginsChanged(); + } + } + + /// + /// Scan the devPlugins folder for new DLL files that are not already loaded into the manager. They are not loaded, + /// only shown as disabled in the installed plugins window. This is a modified version of LoadAllPlugins that works + /// a little differently. + /// + public void ScanDevPlugins() + { + if (this.SafeMode) + { + Log.Information("PluginSafeMode was enabled, not scanning any dev plugins."); + return; + } + + var configuration = Service.Get(); + + if (!this.devPluginDirectory.Exists) + this.devPluginDirectory.Create(); + + // devPlugins are more freeform. Look for any dll and hope to get lucky. + var devDllFiles = this.devPluginDirectory.GetFiles("*.dll", SearchOption.AllDirectories).ToList(); + + foreach (var setting in configuration.DevPluginLoadLocations) + { + if (!setting.IsEnabled) + continue; + + if (Directory.Exists(setting.Path)) + { + devDllFiles.AddRange(new DirectoryInfo(setting.Path).GetFiles("*.dll", SearchOption.AllDirectories)); + } + else if (File.Exists(setting.Path)) + { + devDllFiles.Add(new FileInfo(setting.Path)); + } + } + + var listChanged = false; + + foreach (var dllFile in devDllFiles) + { + // This file is already known to us + if (this.InstalledPlugins.Any(lp => lp.DllFile.FullName == dllFile.FullName)) + continue; + + // Manifests are not required for devPlugins. the Plugin type will handle any null manifests. + var manifestFile = LocalPluginManifest.GetManifestFile(dllFile); + var manifest = manifestFile.Exists ? LocalPluginManifest.Load(manifestFile) : null; + + try + { + // Add them to the list and let the user decide, nothing is auto-loaded. + this.LoadPlugin(dllFile, manifest, PluginLoadReason.Installer, isDev: true, doNotLoad: true); + listChanged = true; + } + catch (InvalidPluginException) + { + // Not a plugin + } + catch (Exception ex) + { + Log.Error(ex, $"During devPlugin scan, an unexpected error occurred"); + } + } + + if (listChanged) + this.NotifyInstalledPluginsChanged(); + } + + /// + /// Install a plugin from a repository and load it. + /// + /// The plugin definition. + /// If the testing version should be used. + /// The reason this plugin was loaded. + /// A representing the asynchronous operation. + public async Task InstallPluginAsync(RemotePluginManifest repoManifest, bool useTesting, PluginLoadReason reason) + { + Log.Debug($"Installing plugin {repoManifest.Name} (testing={useTesting})"); + + var downloadUrl = useTesting ? repoManifest.DownloadLinkTesting : repoManifest.DownloadLinkInstall; + var version = useTesting ? repoManifest.TestingAssemblyVersion : repoManifest.AssemblyVersion; + + var response = await Util.HttpClient.GetAsync(downloadUrl); + response.EnsureSuccessStatusCode(); + + var outputDir = new DirectoryInfo(Path.Combine(this.pluginDirectory.FullName, repoManifest.InternalName, version?.ToString() ?? string.Empty)); + + try + { + if (outputDir.Exists) + outputDir.Delete(true); + + outputDir.Create(); + } + catch + { + // ignored, since the plugin may be loaded already + } + + Log.Debug($"Extracting to {outputDir}"); + // This throws an error, even with overwrite=false + // ZipFile.ExtractToDirectory(tempZip.FullName, outputDir.FullName, false); + using (var archive = new ZipArchive(await response.Content.ReadAsStreamAsync())) + { + foreach (var zipFile in archive.Entries) + { + var outputFile = new FileInfo(Path.GetFullPath(Path.Combine(outputDir.FullName, zipFile.FullName))); + + if (!outputFile.FullName.StartsWith(outputDir.FullName, StringComparison.OrdinalIgnoreCase)) + { + throw new IOException("Trying to extract file outside of destination directory. See this link for more info: https://snyk.io/research/zip-slip-vulnerability"); + } + + if (outputFile.Directory == null) + { + throw new IOException("Output directory invalid."); + } + + if (zipFile.Name.IsNullOrEmpty()) + { + // Assuming Empty for Directory + Log.Verbose($"ZipFile name is null or empty, treating as a directory: {outputFile.Directory.FullName}"); + Directory.CreateDirectory(outputFile.Directory.FullName); + continue; + } + + // Ensure directory is created + Directory.CreateDirectory(outputFile.Directory.FullName); + + try + { + zipFile.ExtractToFile(outputFile.FullName, true); + } + catch (Exception ex) + { + if (outputFile.Extension.EndsWith("dll")) + { + throw new IOException($"Could not overwrite {zipFile.Name}: {ex.Message}"); + } + + Log.Error($"Could not overwrite {zipFile.Name}: {ex.Message}"); + } + } + } + + var dllFile = LocalPluginManifest.GetPluginFile(outputDir, repoManifest); + var manifestFile = LocalPluginManifest.GetManifestFile(dllFile); + + // We need to save the repoManifest due to how the repo fills in some fields that authors are not expected to use. + File.WriteAllText(manifestFile.FullName, JsonConvert.SerializeObject(repoManifest, Formatting.Indented)); + + // Reload as a local manifest, add some attributes, and save again. + var manifest = LocalPluginManifest.Load(manifestFile); + + if (useTesting) + { + manifest.Testing = true; + } + + if (repoManifest.SourceRepo.IsThirdParty) + { + // Only document the url if it came from a third party repo. + manifest.InstalledFromUrl = repoManifest.SourceRepo.PluginMasterUrl; + } + + manifest.Save(manifestFile); + + Log.Information($"Installed plugin {manifest.Name} (testing={useTesting})"); + + var plugin = this.LoadPlugin(dllFile, manifest, reason); + + this.NotifyInstalledPluginsChanged(); + return plugin; + } + + /// + /// Load a plugin. + /// + /// The associated with the main assembly of this plugin. + /// The already loaded definition, if available. + /// The reason this plugin was loaded. + /// If this plugin should support development features. + /// If this plugin is being loaded at boot. + /// Don't load the plugin, just don't do it. + /// The loaded plugin. + public LocalPlugin LoadPlugin(FileInfo dllFile, LocalPluginManifest? manifest, PluginLoadReason reason, bool isDev = false, bool isBoot = false, bool doNotLoad = false) + { + var name = manifest?.Name ?? dllFile.Name; + var loadPlugin = !doNotLoad; + + LocalPlugin plugin; + + if (isDev) + { + Log.Information($"Loading dev plugin {name}"); + var devPlugin = new LocalDevPlugin(dllFile, manifest); + loadPlugin &= !isBoot || devPlugin.StartOnBoot; + + // If we're not loading it, make sure it's disabled + if (!loadPlugin && !devPlugin.IsDisabled) + devPlugin.Disable(); + + plugin = devPlugin; + } + else + { + Log.Information($"Loading plugin {name}"); + plugin = new LocalPlugin(dllFile, manifest); + } + + if (loadPlugin) + { + try + { + if (plugin.IsDisabled) + plugin.Enable(); + + plugin.Load(reason); + } + catch (InvalidPluginException) + { + PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty); + throw; + } + catch (BannedPluginException) + { + // Out of date plugins get added so they can be updated. + Log.Information($"Plugin was banned, adding anyways: {dllFile.Name}"); + } + catch (Exception ex) + { + if (plugin.IsDev) + { + // Dev plugins always get added to the list so they can be fiddled with in the UI + Log.Information(ex, $"Dev plugin failed to load, adding anyways: {dllFile.Name}"); + plugin.Disable(); // Disable here, otherwise you can't enable+load later + } + else if (plugin.IsOutdated) + { + // Out of date plugins get added so they can be updated. + Log.Information(ex, $"Plugin was outdated, adding anyways: {dllFile.Name}"); + } + else { PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty); throw; } - catch (BannedPluginException) - { - // Out of date plugins get added so they can be updated. - Log.Information($"Plugin was banned, adding anyways: {dllFile.Name}"); - } - catch (Exception ex) - { - if (plugin.IsDev) - { - // Dev plugins always get added to the list so they can be fiddled with in the UI - Log.Information(ex, $"Dev plugin failed to load, adding anyways: {dllFile.Name}"); - plugin.Disable(); // Disable here, otherwise you can't enable+load later - } - else if (plugin.IsOutdated) - { - // Out of date plugins get added so they can be updated. - Log.Information(ex, $"Plugin was outdated, adding anyways: {dllFile.Name}"); - } - else - { - PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty); - throw; - } - } - } - - this.InstalledPlugins = this.InstalledPlugins.Add(plugin); - return plugin; - } - - /// - /// Remove a plugin. - /// - /// Plugin to remove. - public void RemovePlugin(LocalPlugin plugin) - { - if (plugin.State != PluginState.Unloaded) - throw new InvalidPluginOperationException($"Unable to remove {plugin.Name}, not unloaded"); - - this.InstalledPlugins = this.InstalledPlugins.Remove(plugin); - PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty); - - this.NotifyInstalledPluginsChanged(); - this.NotifyAvailablePluginsChanged(); - } - - /// - /// Cleanup disabled plugins. Does not target devPlugins. - /// - public void CleanupPlugins() - { - var configuration = Service.Get(); - var startInfo = Service.Get(); - - foreach (var pluginDir in this.pluginDirectory.GetDirectories()) - { - try - { - var versionDirs = pluginDir.GetDirectories(); - - versionDirs = versionDirs - .OrderByDescending(dir => - { - var isVersion = Version.TryParse(dir.Name, out var version); - - if (!isVersion) - { - Log.Debug($"Not a version, cleaning up {dir.FullName}"); - dir.Delete(true); - } - - return version; - }) - .ToArray(); - - if (versionDirs.Length == 0) - { - Log.Information($"No versions: cleaning up {pluginDir.FullName}"); - pluginDir.Delete(true); - } - else - { - foreach (var versionDir in versionDirs) - { - try - { - var dllFile = new FileInfo(Path.Combine(versionDir.FullName, $"{pluginDir.Name}.dll")); - if (!dllFile.Exists) - { - Log.Information($"Missing dll: cleaning up {versionDir.FullName}"); - versionDir.Delete(true); - continue; - } - - var manifestFile = LocalPluginManifest.GetManifestFile(dllFile); - if (!manifestFile.Exists) - { - Log.Information($"Missing manifest: cleaning up {versionDir.FullName}"); - versionDir.Delete(true); - continue; - } - - var manifest = LocalPluginManifest.Load(manifestFile); - if (manifest.Disabled) - { - Log.Information($"Disabled: cleaning up {versionDir.FullName}"); - versionDir.Delete(true); - continue; - } - - if (manifest.DalamudApiLevel < DalamudApiLevel - 1 && !configuration.LoadAllApiLevels) - { - Log.Information($"Lower API: cleaning up {versionDir.FullName}"); - versionDir.Delete(true); - continue; - } - - if (manifest.ApplicableVersion < startInfo.GameVersion) - { - Log.Information($"Inapplicable version: cleaning up {versionDir.FullName}"); - versionDir.Delete(true); - } - } - catch (Exception ex) - { - Log.Error(ex, $"Could not clean up {versionDir.FullName}"); - } - } - } - } - catch (Exception ex) - { - Log.Error(ex, $"Could not clean up {pluginDir.FullName}"); - } } } - /// - /// Update all non-dev plugins. - /// - /// Perform a dry run, don't install anything. - /// Success or failure and a list of updated plugin metadata. - public async Task> UpdatePluginsAsync(bool dryRun = false) + this.InstalledPlugins = this.InstalledPlugins.Add(plugin); + return plugin; + } + + /// + /// Remove a plugin. + /// + /// Plugin to remove. + public void RemovePlugin(LocalPlugin plugin) + { + if (plugin.State != PluginState.Unloaded) + throw new InvalidPluginOperationException($"Unable to remove {plugin.Name}, not unloaded"); + + this.InstalledPlugins = this.InstalledPlugins.Remove(plugin); + PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty); + + this.NotifyInstalledPluginsChanged(); + this.NotifyAvailablePluginsChanged(); + } + + /// + /// Cleanup disabled plugins. Does not target devPlugins. + /// + public void CleanupPlugins() + { + var configuration = Service.Get(); + var startInfo = Service.Get(); + + foreach (var pluginDir in this.pluginDirectory.GetDirectories()) { - Log.Information("Starting plugin update"); - - var updatedList = new List(); - - // Prevent collection was modified errors - foreach (var plugin in this.UpdatablePlugins) + try { - // Can't update that! - if (plugin.InstalledPlugin.IsDev) - continue; + var versionDirs = pluginDir.GetDirectories(); - var result = await this.UpdateSinglePluginAsync(plugin, false, dryRun); - if (result != null) - updatedList.Add(result); - } + versionDirs = versionDirs + .OrderByDescending(dir => + { + var isVersion = Version.TryParse(dir.Name, out var version); - this.NotifyInstalledPluginsChanged(); + if (!isVersion) + { + Log.Debug($"Not a version, cleaning up {dir.FullName}"); + dir.Delete(true); + } - Log.Information("Plugin update OK."); + return version; + }) + .ToArray(); - return updatedList; - } - - /// - /// Update a single plugin, provided a valid . - /// - /// The available plugin update. - /// Whether to notify that installed plugins have changed afterwards. - /// Whether or not to actually perform the update, or just indicate success. - /// The status of the update. - public async Task UpdateSinglePluginAsync(AvailablePluginUpdate metadata, bool notify, bool dryRun) - { - var plugin = metadata.InstalledPlugin; - - var updateStatus = new PluginUpdateStatus - { - InternalName = plugin.Manifest.InternalName, - Name = plugin.Manifest.Name, - Version = (metadata.UseTesting - ? metadata.UpdateManifest.TestingAssemblyVersion - : metadata.UpdateManifest.AssemblyVersion)!, - WasUpdated = true, - }; - - if (!dryRun) - { - // Unload if loaded - if (plugin.State == PluginState.Loaded || plugin.State == PluginState.LoadError) + if (versionDirs.Length == 0) { - try - { - plugin.Unload(); - } - catch (Exception ex) - { - Log.Error(ex, "Error during unload (update)"); - updateStatus.WasUpdated = false; - return updateStatus; - } - } - - if (plugin.IsDev) - { - try - { - plugin.DllFile.Delete(); - this.InstalledPlugins = this.InstalledPlugins.Remove(plugin); - } - catch (Exception ex) - { - Log.Error(ex, "Error during delete (update)"); - updateStatus.WasUpdated = false; - return updateStatus; - } + Log.Information($"No versions: cleaning up {pluginDir.FullName}"); + pluginDir.Delete(true); } else { - try + foreach (var versionDir in versionDirs) { - plugin.Disable(); - this.InstalledPlugins = this.InstalledPlugins.Remove(plugin); - } - catch (Exception ex) - { - Log.Error(ex, "Error during disable (update)"); - updateStatus.WasUpdated = false; - return updateStatus; + try + { + var dllFile = new FileInfo(Path.Combine(versionDir.FullName, $"{pluginDir.Name}.dll")); + if (!dllFile.Exists) + { + Log.Information($"Missing dll: cleaning up {versionDir.FullName}"); + versionDir.Delete(true); + continue; + } + + var manifestFile = LocalPluginManifest.GetManifestFile(dllFile); + if (!manifestFile.Exists) + { + Log.Information($"Missing manifest: cleaning up {versionDir.FullName}"); + versionDir.Delete(true); + continue; + } + + var manifest = LocalPluginManifest.Load(manifestFile); + if (manifest.Disabled) + { + Log.Information($"Disabled: cleaning up {versionDir.FullName}"); + versionDir.Delete(true); + continue; + } + + if (manifest.DalamudApiLevel < DalamudApiLevel - 1 && !configuration.LoadAllApiLevels) + { + Log.Information($"Lower API: cleaning up {versionDir.FullName}"); + versionDir.Delete(true); + continue; + } + + if (manifest.ApplicableVersion < startInfo.GameVersion) + { + Log.Information($"Inapplicable version: cleaning up {versionDir.FullName}"); + versionDir.Delete(true); + } + } + catch (Exception ex) + { + Log.Error(ex, $"Could not clean up {versionDir.FullName}"); + } } } + } + catch (Exception ex) + { + Log.Error(ex, $"Could not clean up {pluginDir.FullName}"); + } + } + } - // We need to handle removed DTR nodes here, as otherwise, plugins will not be able to re-add their bar entries after updates. - var dtr = Service.Get(); - dtr.HandleRemovedNodes(); + /// + /// Update all non-dev plugins. + /// + /// Perform a dry run, don't install anything. + /// Success or failure and a list of updated plugin metadata. + public async Task> UpdatePluginsAsync(bool dryRun = false) + { + Log.Information("Starting plugin update"); + var updatedList = new List(); + + // Prevent collection was modified errors + foreach (var plugin in this.UpdatablePlugins) + { + // Can't update that! + if (plugin.InstalledPlugin.IsDev) + continue; + + var result = await this.UpdateSinglePluginAsync(plugin, false, dryRun); + if (result != null) + updatedList.Add(result); + } + + this.NotifyInstalledPluginsChanged(); + + Log.Information("Plugin update OK."); + + return updatedList; + } + + /// + /// Update a single plugin, provided a valid . + /// + /// The available plugin update. + /// Whether to notify that installed plugins have changed afterwards. + /// Whether or not to actually perform the update, or just indicate success. + /// The status of the update. + public async Task UpdateSinglePluginAsync(AvailablePluginUpdate metadata, bool notify, bool dryRun) + { + var plugin = metadata.InstalledPlugin; + + var updateStatus = new PluginUpdateStatus + { + InternalName = plugin.Manifest.InternalName, + Name = plugin.Manifest.Name, + Version = (metadata.UseTesting + ? metadata.UpdateManifest.TestingAssemblyVersion + : metadata.UpdateManifest.AssemblyVersion)!, + WasUpdated = true, + }; + + if (!dryRun) + { + // Unload if loaded + if (plugin.State == PluginState.Loaded || plugin.State == PluginState.LoadError) + { try { - await this.InstallPluginAsync(metadata.UpdateManifest, metadata.UseTesting, PluginLoadReason.Update); + plugin.Unload(); } catch (Exception ex) { - Log.Error(ex, "Error during install (update)"); + Log.Error(ex, "Error during unload (update)"); updateStatus.WasUpdated = false; return updateStatus; } } - if (notify && updateStatus.WasUpdated) - this.NotifyInstalledPluginsChanged(); - - return updateStatus; - } - - /// - /// Unload the plugin, delete its configuration, and reload it. - /// - /// The plugin. - /// Throws if the plugin is still loading/unloading. - public void DeleteConfiguration(LocalPlugin plugin) - { - if (plugin.State == PluginState.InProgress) - throw new Exception("Cannot delete configuration for a loading/unloading plugin"); - - if (plugin.IsLoaded) - plugin.Unload(); - - // Let's wait so any handles on files in plugin configurations can be closed - Thread.Sleep(500); - - this.PluginConfigs.Delete(plugin.Name); - - Thread.Sleep(500); - - // Let's indicate "installer" here since this is supposed to be a fresh install - plugin.Load(PluginLoadReason.Installer); - } - - /// - /// Gets a value indicating whether the given manifest is eligible for ANYTHING. These are hard - /// checks that should not allow installation or loading. - /// - /// Plugin manifest. - /// If the manifest is eligible. - public bool IsManifestEligible(PluginManifest manifest) - { - var configuration = Service.Get(); - var startInfo = Service.Get(); - - // Testing exclusive - if (manifest.IsTestingExclusive && !configuration.DoPluginTest) - return false; - - // Applicable version - if (manifest.ApplicableVersion < startInfo.GameVersion) - return false; - - // API level - if (manifest.DalamudApiLevel < DalamudApiLevel && !configuration.LoadAllApiLevels) - return false; - - // Banned - return !this.IsManifestBanned(manifest); - } - - /// - /// Determine if a plugin has been banned by inspecting the manifest. - /// - /// Manifest to inspect. - /// A value indicating whether the plugin/manifest has been banned. - public bool IsManifestBanned(PluginManifest manifest) - { - var configuration = Service.Get(); - return !configuration.LoadBannedPlugins && this.bannedPlugins.Any(ban => (ban.Name == manifest.InternalName || ban.Name == Hash.GetStringSha256Hash(manifest.InternalName)) - && ban.AssemblyVersion >= manifest.AssemblyVersion); - } - - /// - /// Get the reason of a banned plugin by inspecting the manifest. - /// - /// Manifest to inspect. - /// The reason of the ban, if any. - public string GetBanReason(PluginManifest manifest) - { - return this.bannedPlugins.LastOrDefault(ban => ban.Name == manifest.InternalName).Reason; - } - - private void DetectAvailablePluginUpdates() - { - var updatablePlugins = new List(); - - foreach (var plugin in this.InstalledPlugins) + if (plugin.IsDev) { - var installedVersion = plugin.IsTesting - ? plugin.Manifest.TestingAssemblyVersion - : plugin.Manifest.AssemblyVersion; - - var updates = this.AvailablePlugins - .Where(remoteManifest => plugin.Manifest.InternalName == remoteManifest.InternalName) - .Select(remoteManifest => - { - var useTesting = UseTesting(remoteManifest); - var candidateVersion = useTesting - ? remoteManifest.TestingAssemblyVersion - : remoteManifest.AssemblyVersion; - var isUpdate = candidateVersion > installedVersion; - - return (isUpdate, useTesting, candidateVersion, remoteManifest); - }) - .Where(tpl => tpl.isUpdate) - .ToList(); - - if (updates.Count > 0) + try { - var update = updates.Aggregate((t1, t2) => t1.candidateVersion > t2.candidateVersion ? t1 : t2); - updatablePlugins.Add(new AvailablePluginUpdate(plugin, update.remoteManifest, update.useTesting)); + plugin.DllFile.Delete(); + this.InstalledPlugins = this.InstalledPlugins.Remove(plugin); + } + catch (Exception ex) + { + Log.Error(ex, "Error during delete (update)"); + updateStatus.WasUpdated = false; + return updateStatus; + } + } + else + { + try + { + plugin.Disable(); + this.InstalledPlugins = this.InstalledPlugins.Remove(plugin); + } + catch (Exception ex) + { + Log.Error(ex, "Error during disable (update)"); + updateStatus.WasUpdated = false; + return updateStatus; } } - this.UpdatablePlugins = updatablePlugins.ToImmutableList(); - } - - private void NotifyAvailablePluginsChanged() - { - this.DetectAvailablePluginUpdates(); + // We need to handle removed DTR nodes here, as otherwise, plugins will not be able to re-add their bar entries after updates. + var dtr = Service.Get(); + dtr.HandleRemovedNodes(); try { - this.OnAvailablePluginsChanged?.Invoke(); + await this.InstallPluginAsync(metadata.UpdateManifest, metadata.UseTesting, PluginLoadReason.Update); } catch (Exception ex) { - Log.Error(ex, $"Error notifying {nameof(this.OnAvailablePluginsChanged)}"); + Log.Error(ex, "Error during install (update)"); + updateStatus.WasUpdated = false; + return updateStatus; } } - private void NotifyInstalledPluginsChanged() - { - this.DetectAvailablePluginUpdates(); + if (notify && updateStatus.WasUpdated) + this.NotifyInstalledPluginsChanged(); - try - { - this.OnInstalledPluginsChanged?.Invoke(); - } - catch (Exception ex) - { - Log.Error(ex, $"Error notifying {nameof(this.OnInstalledPluginsChanged)}"); - } - } - - private static class Locs - { - public static string DalamudPluginUpdateSuccessful(string name, Version version) => Loc.Localize("DalamudPluginUpdateSuccessful", " 》 {0} updated to v{1}.").Format(name, version); - - public static string DalamudPluginUpdateFailed(string name, Version version) => Loc.Localize("DalamudPluginUpdateFailed", " 》 {0} update to v{1} failed.").Format(name, version); - } + return updateStatus; } /// - /// Class responsible for loading and unloading plugins. - /// This contains the assembly patching functionality to resolve assembly locations. + /// Unload the plugin, delete its configuration, and reload it. /// - internal partial class PluginManager + /// The plugin. + /// Throws if the plugin is still loading/unloading. + public void DeleteConfiguration(LocalPlugin plugin) { - /// - /// A mapping of plugin assembly name to patch data. Used to fill in missing data due to loading - /// plugins via byte[]. - /// - internal static readonly Dictionary PluginLocations = new(); + if (plugin.State == PluginState.InProgress) + throw new Exception("Cannot delete configuration for a loading/unloading plugin"); - private MonoMod.RuntimeDetour.Hook? assemblyLocationMonoHook; - private MonoMod.RuntimeDetour.Hook? assemblyCodeBaseMonoHook; + if (plugin.IsLoaded) + plugin.Unload(); - /// - /// Patch method for internal class RuntimeAssembly.Location, also known as Assembly.Location. - /// This patch facilitates resolving the assembly location for plugins that are loaded via byte[]. - /// It should never be called manually. - /// - /// A delegate that acts as the original method. - /// The equivalent of `this`. - /// The plugin location, or the result from the original method. - private static string AssemblyLocationPatch(Func orig, Assembly self) + // Let's wait so any handles on files in plugin configurations can be closed + Thread.Sleep(500); + + this.PluginConfigs.Delete(plugin.Name); + + Thread.Sleep(500); + + // Let's indicate "installer" here since this is supposed to be a fresh install + plugin.Load(PluginLoadReason.Installer); + } + + /// + /// Gets a value indicating whether the given manifest is eligible for ANYTHING. These are hard + /// checks that should not allow installation or loading. + /// + /// Plugin manifest. + /// If the manifest is eligible. + public bool IsManifestEligible(PluginManifest manifest) + { + var configuration = Service.Get(); + var startInfo = Service.Get(); + + // Testing exclusive + if (manifest.IsTestingExclusive && !configuration.DoPluginTest) + return false; + + // Applicable version + if (manifest.ApplicableVersion < startInfo.GameVersion) + return false; + + // API level + if (manifest.DalamudApiLevel < DalamudApiLevel && !configuration.LoadAllApiLevels) + return false; + + // Banned + return !this.IsManifestBanned(manifest); + } + + /// + /// Determine if a plugin has been banned by inspecting the manifest. + /// + /// Manifest to inspect. + /// A value indicating whether the plugin/manifest has been banned. + public bool IsManifestBanned(PluginManifest manifest) + { + var configuration = Service.Get(); + return !configuration.LoadBannedPlugins && this.bannedPlugins.Any(ban => (ban.Name == manifest.InternalName || ban.Name == Hash.GetStringSha256Hash(manifest.InternalName)) + && ban.AssemblyVersion >= manifest.AssemblyVersion); + } + + /// + /// Get the reason of a banned plugin by inspecting the manifest. + /// + /// Manifest to inspect. + /// The reason of the ban, if any. + public string GetBanReason(PluginManifest manifest) + { + return this.bannedPlugins.LastOrDefault(ban => ban.Name == manifest.InternalName).Reason; + } + + private void DetectAvailablePluginUpdates() + { + var updatablePlugins = new List(); + + foreach (var plugin in this.InstalledPlugins) { - var result = orig(self); + var installedVersion = plugin.IsTesting + ? plugin.Manifest.TestingAssemblyVersion + : plugin.Manifest.AssemblyVersion; - if (string.IsNullOrEmpty(result)) + var updates = this.AvailablePlugins + .Where(remoteManifest => plugin.Manifest.InternalName == remoteManifest.InternalName) + .Select(remoteManifest => + { + var useTesting = UseTesting(remoteManifest); + var candidateVersion = useTesting + ? remoteManifest.TestingAssemblyVersion + : remoteManifest.AssemblyVersion; + var isUpdate = candidateVersion > installedVersion; + + return (isUpdate, useTesting, candidateVersion, remoteManifest); + }) + .Where(tpl => tpl.isUpdate) + .ToList(); + + if (updates.Count > 0) { - foreach (var assemblyName in GetStackFrameAssemblyNames()) - { - if (PluginLocations.TryGetValue(assemblyName, out var data)) - { - result = data.Location; - break; - } - } - } - - result ??= string.Empty; - - Log.Verbose($"Assembly.Location // {self.FullName} // {result}"); - return result; - } - - /// - /// Patch method for internal class RuntimeAssembly.CodeBase, also known as Assembly.CodeBase. - /// This patch facilitates resolving the assembly location for plugins that are loaded via byte[]. - /// It should never be called manually. - /// - /// A delegate that acts as the original method. - /// The equivalent of `this`. - /// The plugin code base, or the result from the original method. - private static string AssemblyCodeBasePatch(Func orig, Assembly self) - { - var result = orig(self); - - if (string.IsNullOrEmpty(result)) - { - foreach (var assemblyName in GetStackFrameAssemblyNames()) - { - if (PluginLocations.TryGetValue(assemblyName, out var data)) - { - result = data.CodeBase; - break; - } - } - } - - result ??= string.Empty; - - Log.Verbose($"Assembly.CodeBase // {self.FullName} // {result}"); - return result; - } - - private static IEnumerable GetStackFrameAssemblyNames() - { - var stackTrace = new StackTrace(); - var stackFrames = stackTrace.GetFrames(); - - foreach (var stackFrame in stackFrames) - { - var methodBase = stackFrame.GetMethod(); - if (methodBase == null) - continue; - - yield return methodBase.Module.Assembly.FullName!; + var update = updates.Aggregate((t1, t2) => t1.candidateVersion > t2.candidateVersion ? t1 : t2); + updatablePlugins.Add(new AvailablePluginUpdate(plugin, update.remoteManifest, update.useTesting)); } } - private void ApplyPatches() + this.UpdatablePlugins = updatablePlugins.ToImmutableList(); + } + + private void NotifyAvailablePluginsChanged() + { + this.DetectAvailablePluginUpdates(); + + try { - var targetType = typeof(PluginManager).Assembly.GetType(); - - var locationTarget = targetType.GetProperty(nameof(Assembly.Location))!.GetGetMethod(); - var locationPatch = typeof(PluginManager).GetMethod(nameof(AssemblyLocationPatch), BindingFlags.NonPublic | BindingFlags.Static); - this.assemblyLocationMonoHook = new MonoMod.RuntimeDetour.Hook(locationTarget, locationPatch); - - #pragma warning disable CS0618 - #pragma warning disable SYSLIB0012 - var codebaseTarget = targetType.GetProperty(nameof(Assembly.CodeBase))?.GetGetMethod(); - #pragma warning restore SYSLIB0012 - #pragma warning restore CS0618 - var codebasePatch = typeof(PluginManager).GetMethod(nameof(AssemblyCodeBasePatch), BindingFlags.NonPublic | BindingFlags.Static); - this.assemblyCodeBaseMonoHook = new MonoMod.RuntimeDetour.Hook(codebaseTarget, codebasePatch); + this.OnAvailablePluginsChanged?.Invoke(); + } + catch (Exception ex) + { + Log.Error(ex, $"Error notifying {nameof(this.OnAvailablePluginsChanged)}"); } } + + private void NotifyInstalledPluginsChanged() + { + this.DetectAvailablePluginUpdates(); + + try + { + this.OnInstalledPluginsChanged?.Invoke(); + } + catch (Exception ex) + { + Log.Error(ex, $"Error notifying {nameof(this.OnInstalledPluginsChanged)}"); + } + } + + private static class Locs + { + public static string DalamudPluginUpdateSuccessful(string name, Version version) => Loc.Localize("DalamudPluginUpdateSuccessful", " 》 {0} updated to v{1}.").Format(name, version); + + public static string DalamudPluginUpdateFailed(string name, Version version) => Loc.Localize("DalamudPluginUpdateFailed", " 》 {0} update to v{1} failed.").Format(name, version); + } +} + +/// +/// Class responsible for loading and unloading plugins. +/// This contains the assembly patching functionality to resolve assembly locations. +/// +internal partial class PluginManager +{ + /// + /// A mapping of plugin assembly name to patch data. Used to fill in missing data due to loading + /// plugins via byte[]. + /// + internal static readonly Dictionary PluginLocations = new(); + + private MonoMod.RuntimeDetour.Hook? assemblyLocationMonoHook; + private MonoMod.RuntimeDetour.Hook? assemblyCodeBaseMonoHook; + + /// + /// Patch method for internal class RuntimeAssembly.Location, also known as Assembly.Location. + /// This patch facilitates resolving the assembly location for plugins that are loaded via byte[]. + /// It should never be called manually. + /// + /// A delegate that acts as the original method. + /// The equivalent of `this`. + /// The plugin location, or the result from the original method. + private static string AssemblyLocationPatch(Func orig, Assembly self) + { + var result = orig(self); + + if (string.IsNullOrEmpty(result)) + { + foreach (var assemblyName in GetStackFrameAssemblyNames()) + { + if (PluginLocations.TryGetValue(assemblyName, out var data)) + { + result = data.Location; + break; + } + } + } + + result ??= string.Empty; + + Log.Verbose($"Assembly.Location // {self.FullName} // {result}"); + return result; + } + + /// + /// Patch method for internal class RuntimeAssembly.CodeBase, also known as Assembly.CodeBase. + /// This patch facilitates resolving the assembly location for plugins that are loaded via byte[]. + /// It should never be called manually. + /// + /// A delegate that acts as the original method. + /// The equivalent of `this`. + /// The plugin code base, or the result from the original method. + private static string AssemblyCodeBasePatch(Func orig, Assembly self) + { + var result = orig(self); + + if (string.IsNullOrEmpty(result)) + { + foreach (var assemblyName in GetStackFrameAssemblyNames()) + { + if (PluginLocations.TryGetValue(assemblyName, out var data)) + { + result = data.CodeBase; + break; + } + } + } + + result ??= string.Empty; + + Log.Verbose($"Assembly.CodeBase // {self.FullName} // {result}"); + return result; + } + + private static IEnumerable GetStackFrameAssemblyNames() + { + var stackTrace = new StackTrace(); + var stackFrames = stackTrace.GetFrames(); + + foreach (var stackFrame in stackFrames) + { + var methodBase = stackFrame.GetMethod(); + if (methodBase == null) + continue; + + yield return methodBase.Module.Assembly.FullName!; + } + } + + private void ApplyPatches() + { + var targetType = typeof(PluginManager).Assembly.GetType(); + + var locationTarget = targetType.GetProperty(nameof(Assembly.Location))!.GetGetMethod(); + var locationPatch = typeof(PluginManager).GetMethod(nameof(AssemblyLocationPatch), BindingFlags.NonPublic | BindingFlags.Static); + this.assemblyLocationMonoHook = new MonoMod.RuntimeDetour.Hook(locationTarget, locationPatch); + +#pragma warning disable CS0618 +#pragma warning disable SYSLIB0012 + var codebaseTarget = targetType.GetProperty(nameof(Assembly.CodeBase))?.GetGetMethod(); +#pragma warning restore SYSLIB0012 +#pragma warning restore CS0618 + var codebasePatch = typeof(PluginManager).GetMethod(nameof(AssemblyCodeBasePatch), BindingFlags.NonPublic | BindingFlags.Static); + this.assemblyCodeBaseMonoHook = new MonoMod.RuntimeDetour.Hook(codebaseTarget, codebasePatch); + } } diff --git a/Dalamud/Plugin/Internal/PluginRepository.cs b/Dalamud/Plugin/Internal/PluginRepository.cs deleted file mode 100644 index c00755ec2..000000000 --- a/Dalamud/Plugin/Internal/PluginRepository.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading.Tasks; - -using Dalamud.Logging.Internal; -using Dalamud.Plugin.Internal.Types; -using Newtonsoft.Json; - -namespace Dalamud.Plugin.Internal -{ - /// - /// This class represents a single plugin repository. - /// - internal class PluginRepository - { - private const string DalamudPluginsMasterUrl = "https://kamori.goats.dev/Plugin/PluginMaster"; - - private static readonly ModuleLog Log = new("PLUGINR"); - - private static readonly HttpClient HttpClient = new() - { - DefaultRequestHeaders = - { - CacheControl = new CacheControlHeaderValue - { - NoCache = true, - }, - }, - }; - - /// - /// Initializes a new instance of the class. - /// - /// The plugin master URL. - /// Whether the plugin repo is enabled. - public PluginRepository(string pluginMasterUrl, bool isEnabled) - { - this.PluginMasterUrl = pluginMasterUrl; - this.IsThirdParty = pluginMasterUrl != DalamudPluginsMasterUrl; - this.IsEnabled = isEnabled; - } - - /// - /// Gets a new instance of the class for the main repo. - /// - public static PluginRepository MainRepo => new(DalamudPluginsMasterUrl, true); - - /// - /// Gets the pluginmaster.json URL. - /// - public string PluginMasterUrl { get; } - - /// - /// Gets a value indicating whether this plugin repository is from a third party. - /// - public bool IsThirdParty { get; } - - /// - /// Gets a value indicating whether this repo is enabled. - /// - public bool IsEnabled { get; } - - /// - /// Gets the plugin master list of available plugins. - /// - public ReadOnlyCollection? PluginMaster { get; private set; } - - /// - /// Gets the initialization state of the plugin repository. - /// - public PluginRepositoryState State { get; private set; } - - /// - /// Reload the plugin master asynchronously in a task. - /// - /// The new state. - public async Task ReloadPluginMasterAsync() - { - this.State = PluginRepositoryState.InProgress; - this.PluginMaster = new List().AsReadOnly(); - - try - { - Log.Information($"Fetching repo: {this.PluginMasterUrl}"); - - using var response = await HttpClient.GetAsync(this.PluginMasterUrl); - response.EnsureSuccessStatusCode(); - - var data = await response.Content.ReadAsStringAsync(); - var pluginMaster = JsonConvert.DeserializeObject>(data); - - if (pluginMaster == null) - { - throw new Exception("Deserialized PluginMaster was null."); - } - - pluginMaster.Sort((pm1, pm2) => pm1.Name.CompareTo(pm2.Name)); - - // Set the source for each remote manifest. Allows for checking if is 3rd party. - foreach (var manifest in pluginMaster) - { - manifest.SourceRepo = this; - } - - this.PluginMaster = pluginMaster.AsReadOnly(); - - Log.Debug($"Successfully fetched repo: {this.PluginMasterUrl}"); - this.State = PluginRepositoryState.Success; - } - catch (Exception ex) - { - Log.Error(ex, $"PluginMaster failed: {this.PluginMasterUrl}"); - this.State = PluginRepositoryState.Fail; - } - } - } -} diff --git a/Dalamud/Plugin/Internal/Types/AvailablePluginUpdate.cs b/Dalamud/Plugin/Internal/Types/AvailablePluginUpdate.cs index 32dde337c..13523a379 100644 --- a/Dalamud/Plugin/Internal/Types/AvailablePluginUpdate.cs +++ b/Dalamud/Plugin/Internal/Types/AvailablePluginUpdate.cs @@ -1,36 +1,35 @@ -namespace Dalamud.Plugin.Internal.Types +namespace Dalamud.Plugin.Internal.Types; + +/// +/// Information about an available plugin update. +/// +internal record AvailablePluginUpdate { /// - /// Information about an available plugin update. + /// Initializes a new instance of the class. /// - internal record AvailablePluginUpdate + /// The installed plugin to update. + /// The manifest to use for the update. + /// If the testing version should be used for the update. + public AvailablePluginUpdate(LocalPlugin installedPlugin, RemotePluginManifest updateManifest, bool useTesting) { - /// - /// Initializes a new instance of the class. - /// - /// The installed plugin to update. - /// The manifest to use for the update. - /// If the testing version should be used for the update. - public AvailablePluginUpdate(LocalPlugin installedPlugin, RemotePluginManifest updateManifest, bool useTesting) - { - this.InstalledPlugin = installedPlugin; - this.UpdateManifest = updateManifest; - this.UseTesting = useTesting; - } - - /// - /// Gets the currently installed plugin. - /// - public LocalPlugin InstalledPlugin { get; init; } - - /// - /// Gets the available update manifest. - /// - public RemotePluginManifest UpdateManifest { get; init; } - - /// - /// Gets a value indicating whether the update should use the testing URL. - /// - public bool UseTesting { get; init; } + this.InstalledPlugin = installedPlugin; + this.UpdateManifest = updateManifest; + this.UseTesting = useTesting; } + + /// + /// Gets the currently installed plugin. + /// + public LocalPlugin InstalledPlugin { get; init; } + + /// + /// Gets the available update manifest. + /// + public RemotePluginManifest UpdateManifest { get; init; } + + /// + /// Gets a value indicating whether the update should use the testing URL. + /// + public bool UseTesting { get; init; } } diff --git a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs new file mode 100644 index 000000000..eb0877227 --- /dev/null +++ b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs @@ -0,0 +1,162 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Configuration.Internal; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Logging.Internal; + +namespace Dalamud.Plugin.Internal.Types; + +/// +/// This class represents a dev plugin and all facets of its lifecycle. +/// The DLL on disk, dependencies, loaded assembly, etc. +/// +internal class LocalDevPlugin : LocalPlugin, IDisposable +{ + private static readonly ModuleLog Log = new("PLUGIN"); + + // Ref to Dalamud.Configuration.DevPluginSettings + private readonly DevPluginSettings devSettings; + + private FileSystemWatcher? fileWatcher; + private CancellationTokenSource fileWatcherTokenSource = new(); + private int reloadCounter; + + /// + /// Initializes a new instance of the class. + /// + /// Path to the DLL file. + /// The plugin manifest. + public LocalDevPlugin(FileInfo dllFile, LocalPluginManifest? manifest) + : base(dllFile, manifest) + { + var configuration = Service.Get(); + + if (!configuration.DevPluginSettings.TryGetValue(dllFile.FullName, out this.devSettings)) + { + configuration.DevPluginSettings[dllFile.FullName] = this.devSettings = new DevPluginSettings(); + configuration.Save(); + } + + if (this.AutomaticReload) + { + this.EnableReloading(); + } + } + + /// + /// Gets or sets a value indicating whether this dev plugin should start on boot. + /// + public bool StartOnBoot + { + get => this.devSettings.StartOnBoot; + set => this.devSettings.StartOnBoot = value; + } + + /// + /// Gets or sets a value indicating whether this dev plugin should reload on change. + /// + public bool AutomaticReload + { + get => this.devSettings.AutomaticReloading; + set + { + this.devSettings.AutomaticReloading = value; + + if (this.devSettings.AutomaticReloading) + { + this.EnableReloading(); + } + else + { + this.DisableReloading(); + } + } + } + + /// + public new void Dispose() + { + if (this.fileWatcher != null) + { + this.fileWatcher.Changed -= this.OnFileChanged; + this.fileWatcherTokenSource.Cancel(); + this.fileWatcher.Dispose(); + } + + base.Dispose(); + } + + /// + /// Configure this plugin for automatic reloading and enable it. + /// + public void EnableReloading() + { + if (this.fileWatcher == null && this.DllFile.DirectoryName != null) + { + this.fileWatcherTokenSource = new CancellationTokenSource(); + this.fileWatcher = new FileSystemWatcher(this.DllFile.DirectoryName); + this.fileWatcher.Changed += this.OnFileChanged; + this.fileWatcher.Filter = this.DllFile.Name; + this.fileWatcher.NotifyFilter = NotifyFilters.LastWrite; + this.fileWatcher.EnableRaisingEvents = true; + } + } + + /// + /// Disable automatic reloading for this plugin. + /// + public void DisableReloading() + { + if (this.fileWatcher != null) + { + this.fileWatcherTokenSource.Cancel(); + this.fileWatcher.Changed -= this.OnFileChanged; + this.fileWatcher.Dispose(); + this.fileWatcher = null; + } + } + + private void OnFileChanged(object sender, FileSystemEventArgs args) + { + var current = Interlocked.Increment(ref this.reloadCounter); + + Task.Delay(500).ContinueWith( + _ => + { + if (this.fileWatcherTokenSource.IsCancellationRequested) + { + Log.Debug($"Skipping reload of {this.Name}, file watcher was cancelled."); + return; + } + + if (current != this.reloadCounter) + { + Log.Debug($"Skipping reload of {this.Name}, file has changed again."); + return; + } + + if (this.State != PluginState.Loaded) + { + Log.Debug($"Skipping reload of {this.Name}, state ({this.State}) is not {PluginState.Loaded}."); + return; + } + + var notificationManager = Service.Get(); + + try + { + this.Reload(); + notificationManager.AddNotification($"The DevPlugin '{this.Name} was reloaded successfully.", "Plugin reloaded!", NotificationType.Success); + } + catch (Exception ex) + { + Log.Error(ex, "DevPlugin reload failed."); + notificationManager.AddNotification($"The DevPlugin '{this.Name} could not be reloaded.", "Plugin reload failed!", NotificationType.Error); + } + }, + this.fileWatcherTokenSource.Token); + } +} diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs new file mode 100644 index 000000000..1ac413ca1 --- /dev/null +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -0,0 +1,501 @@ +using System; +using System.IO; +using System.Linq; +using System.Reflection; + +using Dalamud.Configuration.Internal; +using Dalamud.Game; +using Dalamud.Game.Gui.Dtr; +using Dalamud.IoC.Internal; +using Dalamud.Logging.Internal; +using Dalamud.Plugin.Internal.Exceptions; +using Dalamud.Plugin.Internal.Loader; +using Dalamud.Utility; +using Dalamud.Utility.Signatures; + +namespace Dalamud.Plugin.Internal.Types; + +/// +/// This class represents a plugin and all facets of its lifecycle. +/// The DLL on disk, dependencies, loaded assembly, etc. +/// +internal class LocalPlugin : IDisposable +{ + private static readonly ModuleLog Log = new("LOCALPLUGIN"); + + private readonly FileInfo manifestFile; + private readonly FileInfo disabledFile; + private readonly FileInfo testingFile; + + private PluginLoader? loader; + private Assembly? pluginAssembly; + private Type? pluginType; + private IDalamudPlugin? instance; + + /// + /// Initializes a new instance of the class. + /// + /// Path to the DLL file. + /// The plugin manifest. + public LocalPlugin(FileInfo dllFile, LocalPluginManifest? manifest) + { + if (dllFile.Name == "FFXIVClientStructs.Generators.dll") + { + // Could this be done another way? Sure. It is an extremely common source + // of errors in the log through, and should never be loaded as a plugin. + Log.Error($"Not a plugin: {dllFile.FullName}"); + throw new InvalidPluginException(dllFile); + } + + this.DllFile = dllFile; + this.State = PluginState.Unloaded; + + this.loader = PluginLoader.CreateFromAssemblyFile(this.DllFile.FullName, SetupLoaderConfig); + + try + { + this.pluginAssembly = this.loader.LoadDefaultAssembly(); + } + catch (Exception ex) + { + this.pluginAssembly = null; + this.pluginType = null; + this.loader.Dispose(); + + Log.Error(ex, $"Not a plugin: {this.DllFile.FullName}"); + throw new InvalidPluginException(this.DllFile); + } + + try + { + this.pluginType = this.pluginAssembly.GetTypes().FirstOrDefault(type => type.IsAssignableTo(typeof(IDalamudPlugin))); + } + catch (ReflectionTypeLoadException ex) + { + Log.Error(ex, $"Could not load one or more types when searching for IDalamudPlugin: {this.DllFile.FullName}"); + // Something blew up when parsing types, but we still want to look for IDalamudPlugin. Let Load() handle the error. + this.pluginType = ex.Types.FirstOrDefault(type => type != null && type.IsAssignableTo(typeof(IDalamudPlugin))); + } + + if (this.pluginType == default) + { + this.pluginAssembly = null; + this.pluginType = null; + this.loader.Dispose(); + + Log.Error($"Nothing inherits from IDalamudPlugin: {this.DllFile.FullName}"); + throw new InvalidPluginException(this.DllFile); + } + + var assemblyVersion = this.pluginAssembly.GetName().Version; + + // Although it is conditionally used here, we need to set the initial value regardless. + this.manifestFile = LocalPluginManifest.GetManifestFile(this.DllFile); + + // If the parameter manifest was null + if (manifest == null) + { + this.Manifest = new LocalPluginManifest() + { + Author = "developer", + Name = Path.GetFileNameWithoutExtension(this.DllFile.Name), + InternalName = Path.GetFileNameWithoutExtension(this.DllFile.Name), + AssemblyVersion = assemblyVersion ?? new Version("1.0.0.0"), + Description = string.Empty, + ApplicableVersion = GameVersion.Any, + DalamudApiLevel = PluginManager.DalamudApiLevel, + IsHide = false, + }; + + // Save the manifest to disk so there won't be any problems later. + // We'll update the name property after it can be retrieved from the instance. + this.Manifest.Save(this.manifestFile); + } + else + { + this.Manifest = manifest; + } + + // This converts from the ".disabled" file feature to the manifest instead. + this.disabledFile = LocalPluginManifest.GetDisabledFile(this.DllFile); + if (this.disabledFile.Exists) + { + this.Manifest.Disabled = true; + this.disabledFile.Delete(); + } + + // This converts from the ".testing" file feature to the manifest instead. + this.testingFile = LocalPluginManifest.GetTestingFile(this.DllFile); + if (this.testingFile.Exists) + { + this.Manifest.Testing = true; + this.testingFile.Delete(); + } + + var pluginManager = Service.Get(); + this.IsBanned = pluginManager.IsManifestBanned(this.Manifest); + this.BanReason = pluginManager.GetBanReason(this.Manifest); + + this.SaveManifest(); + } + + /// + /// Gets the associated with this plugin. + /// + public DalamudPluginInterface? DalamudInterface { get; private set; } + + /// + /// Gets the path to the plugin DLL. + /// + public FileInfo DllFile { get; } + + /// + /// Gets the plugin manifest, if one exists. + /// + public LocalPluginManifest Manifest { get; private set; } + + /// + /// Gets or sets the current state of the plugin. + /// + public PluginState State { get; protected set; } + + /// + /// Gets the AssemblyName plugin, populated during . + /// + /// Plugin type. + public AssemblyName? AssemblyName { get; private set; } + + /// + /// Gets the plugin name, directly from the plugin or if it is not loaded from the manifest. + /// + public string Name => this.instance?.Name ?? this.Manifest.Name; + + /// + /// Gets an optional reason, if the plugin is banned. + /// + public string BanReason { get; } + + /// + /// Gets a value indicating whether the plugin is loaded and running. + /// + public bool IsLoaded => this.State == PluginState.Loaded; + + /// + /// Gets a value indicating whether the plugin is disabled. + /// + public bool IsDisabled => this.Manifest.Disabled; + + /// + /// Gets a value indicating whether this plugin's API level is out of date. + /// + public bool IsOutdated => this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel; + + /// + /// Gets a value indicating whether the plugin is for testing use only. + /// + public bool IsTesting => this.Manifest.IsTestingExclusive || this.Manifest.Testing; + + /// + /// Gets a value indicating whether this plugin has been banned. + /// + public bool IsBanned { get; } + + /// + /// Gets a value indicating whether this plugin is dev plugin. + /// + public bool IsDev => this is LocalDevPlugin; + + /// + public void Dispose() + { + this.instance?.Dispose(); + this.instance = null; + + this.DalamudInterface?.ExplicitDispose(); + this.DalamudInterface = null; + + this.pluginType = null; + this.pluginAssembly = null; + + this.loader?.Dispose(); + } + + /// + /// Load this plugin. + /// + /// The reason why this plugin is being loaded. + /// Load while reloading. + public void Load(PluginLoadReason reason, bool reloading = false) + { + var startInfo = Service.Get(); + var configuration = Service.Get(); + var pluginManager = Service.Get(); + + // Allowed: Unloaded + switch (this.State) + { + case PluginState.InProgress: + throw new InvalidPluginOperationException($"Unable to load {this.Name}, already working"); + case PluginState.Loaded: + throw new InvalidPluginOperationException($"Unable to load {this.Name}, already loaded"); + case PluginState.LoadError: + throw new InvalidPluginOperationException($"Unable to load {this.Name}, load previously faulted, unload first"); + case PluginState.UnloadError: + throw new InvalidPluginOperationException($"Unable to load {this.Name}, unload previously faulted, restart Dalamud"); + case PluginState.Unloaded: + break; + default: + throw new ArgumentOutOfRangeException(this.State.ToString()); + } + + if (pluginManager.IsManifestBanned(this.Manifest)) + throw new BannedPluginException($"Unable to load {this.Name}, banned"); + + if (this.Manifest.ApplicableVersion < startInfo.GameVersion) + throw new InvalidPluginOperationException($"Unable to load {this.Name}, no applicable version"); + + if (this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel && !configuration.LoadAllApiLevels) + throw new InvalidPluginOperationException($"Unable to load {this.Name}, incompatible API level"); + + if (this.Manifest.Disabled) + throw new InvalidPluginOperationException($"Unable to load {this.Name}, disabled"); + + this.State = PluginState.InProgress; + Log.Information($"Loading {this.DllFile.Name}"); + + if (this.DllFile.DirectoryName != null && File.Exists(Path.Combine(this.DllFile.DirectoryName, "Dalamud.dll"))) + { + Log.Error("==== IMPORTANT MESSAGE TO {0}, THE DEVELOPER OF {1} ====", this.Manifest.Author!, this.Manifest.InternalName); + Log.Error("YOU ARE INCLUDING DALAMUD DEPENDENCIES IN YOUR BUILDS!!!"); + Log.Error("You may not be able to load your plugin. \"False\" needs to be set in your csproj."); + Log.Error("If you are using ILMerge, do not merge anything other than your direct dependencies."); + Log.Error("Do not merge FFXIVClientStructs.Generators.dll."); + Log.Error("Please refer to https://github.com/goatcorp/Dalamud/discussions/603 for more information."); + } + + try + { + this.loader ??= PluginLoader.CreateFromAssemblyFile(this.DllFile.FullName, SetupLoaderConfig); + + if (reloading || this.IsDev) + { + if (this.IsDev) + { + // If a dev plugin is set to not autoload on boot, but we want to reload it at the arbitrary load + // time, we need to essentially "Unload" the plugin, but we can't call plugin.Unload because of the + // load state checks. Null any references to the assembly and types, then proceed with regular reload + // operations. + this.pluginAssembly = null; + this.pluginType = null; + } + + this.loader.Reload(); + + if (this.IsDev) + { + // Reload the manifest in-case there were changes here too. + var manifestDevFile = LocalPluginManifest.GetManifestFile(this.DllFile); + if (manifestDevFile.Exists) + { + this.Manifest = LocalPluginManifest.Load(manifestDevFile); + } + } + } + + // Load the assembly + this.pluginAssembly ??= this.loader.LoadDefaultAssembly(); + + this.AssemblyName = this.pluginAssembly.GetName(); + + // Find the plugin interface implementation. It is guaranteed to exist after checking in the ctor. + this.pluginType ??= this.pluginAssembly.GetTypes().First(type => type.IsAssignableTo(typeof(IDalamudPlugin))); + + // Check for any loaded plugins with the same assembly name + var assemblyName = this.pluginAssembly.GetName().Name; + foreach (var otherPlugin in pluginManager.InstalledPlugins) + { + // During hot-reloading, this plugin will be in the plugin list, and the instance will have been disposed + if (otherPlugin == this || otherPlugin.instance == null) + continue; + + var otherPluginAssemblyName = otherPlugin.instance.GetType().Assembly.GetName().Name; + if (otherPluginAssemblyName == assemblyName && otherPluginAssemblyName != null) + { + this.State = PluginState.Unloaded; + Log.Debug($"Duplicate assembly: {this.Name}"); + + throw new DuplicatePluginException(assemblyName); + } + } + + // Update the location for the Location and CodeBase patches + PluginManager.PluginLocations[this.pluginType.Assembly.FullName] = new PluginPatchData(this.DllFile); + + this.DalamudInterface = new DalamudPluginInterface(this.pluginAssembly.GetName().Name!, this.DllFile, reason, this.IsDev); + + var ioc = Service.Get(); + this.instance = ioc.Create(this.pluginType, this.DalamudInterface) as IDalamudPlugin; + if (this.instance == null) + { + this.State = PluginState.LoadError; + this.DalamudInterface.ExplicitDispose(); + Log.Error($"Error while loading {this.Name}, failed to bind and call the plugin constructor"); + return; + } + + SignatureHelper.Initialise(this.instance); + + // In-case the manifest name was a placeholder. Can occur when no manifest was included. + if (this.instance.Name != this.Manifest.Name) + { + this.Manifest.Name = this.instance.Name; + this.Manifest.Save(this.manifestFile); + } + + this.State = PluginState.Loaded; + Log.Information($"Finished loading {this.DllFile.Name}"); + } + catch (Exception ex) + { + this.State = PluginState.LoadError; + Log.Error(ex, $"Error while loading {this.Name}"); + + throw; + } + } + + /// + /// Unload this plugin. This is the same as dispose, but without the "disposed" connotations. This object should stay + /// in the plugin list until it has been actually disposed. + /// + /// Unload while reloading. + public void Unload(bool reloading = false) + { + // Allowed: Loaded, LoadError(we are cleaning this up while we're at it) + switch (this.State) + { + case PluginState.InProgress: + throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already working"); + case PluginState.Unloaded: + throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already unloaded"); + case PluginState.UnloadError: + throw new InvalidPluginOperationException($"Unable to unload {this.Name}, unload previously faulted, restart Dalamud"); + case PluginState.Loaded: + break; + case PluginState.LoadError: + break; + default: + throw new ArgumentOutOfRangeException(this.State.ToString()); + } + + try + { + this.State = PluginState.InProgress; + Log.Information($"Unloading {this.DllFile.Name}"); + + this.instance?.Dispose(); + this.instance = null; + + this.DalamudInterface?.ExplicitDispose(); + this.DalamudInterface = null; + + this.pluginType = null; + this.pluginAssembly = null; + + if (!reloading) + { + this.loader?.Dispose(); + this.loader = null; + } + + this.State = PluginState.Unloaded; + Log.Information($"Finished unloading {this.DllFile.Name}"); + } + catch (Exception ex) + { + this.State = PluginState.UnloadError; + Log.Error(ex, $"Error while unloading {this.Name}"); + + throw; + } + } + + /// + /// Reload this plugin. + /// + public void Reload() + { + this.Unload(true); + + // We need to handle removed DTR nodes here, as otherwise, plugins will not be able to re-add their bar entries after updates. + var dtr = Service.Get(); + dtr.HandleRemovedNodes(); + + this.Load(PluginLoadReason.Reload, true); + } + + /// + /// Revert a disable. Must be unloaded first, does not load. + /// + public void Enable() + { + // Allowed: Unloaded, UnloadError + switch (this.State) + { + case PluginState.InProgress: + case PluginState.Loaded: + case PluginState.LoadError: + throw new InvalidPluginOperationException($"Unable to enable {this.Name}, still loaded"); + case PluginState.Unloaded: + break; + case PluginState.UnloadError: + break; + default: + throw new ArgumentOutOfRangeException(this.State.ToString()); + } + + if (!this.Manifest.Disabled) + throw new InvalidPluginOperationException($"Unable to enable {this.Name}, not disabled"); + + this.Manifest.Disabled = false; + this.SaveManifest(); + } + + /// + /// Disable this plugin, must be unloaded first. + /// + public void Disable() + { + // Allowed: Unloaded, UnloadError + switch (this.State) + { + case PluginState.InProgress: + case PluginState.Loaded: + case PluginState.LoadError: + throw new InvalidPluginOperationException($"Unable to disable {this.Name}, still loaded"); + case PluginState.Unloaded: + break; + case PluginState.UnloadError: + break; + default: + throw new ArgumentOutOfRangeException(this.State.ToString()); + } + + if (this.Manifest.Disabled) + throw new InvalidPluginOperationException($"Unable to disable {this.Name}, already disabled"); + + this.Manifest.Disabled = true; + this.SaveManifest(); + } + + private static void SetupLoaderConfig(LoaderConfig config) + { + config.IsUnloadable = true; + config.LoadInMemory = true; + config.PreferSharedTypes = false; + config.SharedAssemblies.Add(typeof(Lumina.GameData).Assembly.GetName()); + config.SharedAssemblies.Add(typeof(Lumina.Excel.ExcelSheetImpl).Assembly.GetName()); + } + + private void SaveManifest() => this.Manifest.Save(this.manifestFile); +} diff --git a/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs b/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs index a68418b2f..261f28b0e 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs @@ -1,99 +1,79 @@ using System.IO; -using Dalamud.Utility; using Newtonsoft.Json; -namespace Dalamud.Plugin.Internal.Types +namespace Dalamud.Plugin.Internal.Types; + +/// +/// Information about a plugin, packaged in a json file with the DLL. This variant includes additional information such as +/// if the plugin is disabled and if it was installed from a testing URL. This is designed for use with manifests on disk. +/// +internal record LocalPluginManifest : PluginManifest { /// - /// Information about a plugin, packaged in a json file with the DLL. This variant includes additional information such as - /// if the plugin is disabled and if it was installed from a testing URL. This is designed for use with manifests on disk. + /// Gets or sets a value indicating whether the plugin is disabled and should not be loaded. + /// This value supersedes the ".disabled" file functionality and should not be included in the plugin master. /// - internal record LocalPluginManifest : PluginManifest - { - /// - /// Gets or sets a value indicating whether the plugin is disabled and should not be loaded. - /// This value supercedes the ".disabled" file functionality and should not be included in the plugin master. - /// - public bool Disabled { get; set; } = false; + public bool Disabled { get; set; } - /// - /// Gets or sets a value indicating whether the plugin should only be loaded when testing is enabled. - /// This value supercedes the ".testing" file functionality and should not be included in the plugin master. - /// - public bool Testing { get; set; } = false; + /// + /// Gets or sets a value indicating whether the plugin should only be loaded when testing is enabled. + /// This value supersedes the ".testing" file functionality and should not be included in the plugin master. + /// + public bool Testing { get; set; } - /// - /// Gets or sets the 3rd party repo URL that this plugin was installed from. Used to display where the plugin was - /// sourced from on the installed plugin view. This should not be included in the plugin master. This value is null - /// when installed from the main repo. - /// - public string InstalledFromUrl { get; set; } + /// + /// Gets or sets the 3rd party repo URL that this plugin was installed from. Used to display where the plugin was + /// sourced from on the installed plugin view. This should not be included in the plugin master. This value is null + /// when installed from the main repo. + /// + public string InstalledFromUrl { get; set; } = string.Empty; - /// - /// Gets a value indicating whether this manifest is associated with a plugin that was installed from a third party - /// repo. Unless the manifest has been manually modified, this is determined by the InstalledFromUrl being null. - /// - public bool IsThirdParty => !string.IsNullOrEmpty(this.InstalledFromUrl); + /// + /// Gets a value indicating whether this manifest is associated with a plugin that was installed from a third party + /// repo. Unless the manifest has been manually modified, this is determined by the InstalledFromUrl being null. + /// + public bool IsThirdParty => !string.IsNullOrEmpty(this.InstalledFromUrl); - /// - /// Save a plugin manifest to file. - /// - /// Path to save at. - public void Save(FileInfo manifestFile) => File.WriteAllText(manifestFile.FullName, JsonConvert.SerializeObject(this, Formatting.Indented)); + /// + /// Save a plugin manifest to file. + /// + /// Path to save at. + public void Save(FileInfo manifestFile) => File.WriteAllText(manifestFile.FullName, JsonConvert.SerializeObject(this, Formatting.Indented)); - /// - /// Loads a plugin manifest from file. - /// - /// Path to the manifest. - /// A object. - public static LocalPluginManifest Load(FileInfo manifestFile) => JsonConvert.DeserializeObject(File.ReadAllText(manifestFile.FullName)); + /// + /// Loads a plugin manifest from file. + /// + /// Path to the manifest. + /// A object. + public static LocalPluginManifest Load(FileInfo manifestFile) => JsonConvert.DeserializeObject(File.ReadAllText(manifestFile.FullName))!; - /// - /// A standardized way to get the plugin DLL name that should accompany a manifest file. May not exist. - /// - /// Manifest directory. - /// The manifest. - /// The file. - public static FileInfo GetPluginFile(DirectoryInfo dir, PluginManifest manifest) => new(Path.Combine(dir.FullName, $"{manifest.InternalName}.dll")); + /// + /// A standardized way to get the plugin DLL name that should accompany a manifest file. May not exist. + /// + /// Manifest directory. + /// The manifest. + /// The file. + public static FileInfo GetPluginFile(DirectoryInfo dir, PluginManifest manifest) => new(Path.Combine(dir.FullName, $"{manifest.InternalName}.dll")); - /// - /// A standardized way to get the manifest file that should accompany a plugin DLL. May not exist. - /// - /// The plugin DLL. - /// The file. - public static FileInfo GetManifestFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName, Path.GetFileNameWithoutExtension(dllFile.Name) + ".json")); + /// + /// A standardized way to get the manifest file that should accompany a plugin DLL. May not exist. + /// + /// The plugin DLL. + /// The file. + public static FileInfo GetManifestFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName!, Path.GetFileNameWithoutExtension(dllFile.Name) + ".json")); - /// - /// A standardized way to get the obsolete .disabled file that should accompany a plugin DLL. May not exist. - /// - /// The plugin DLL. - /// The .disabled file. - public static FileInfo GetDisabledFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName, ".disabled")); + /// + /// A standardized way to get the obsolete .disabled file that should accompany a plugin DLL. May not exist. + /// + /// The plugin DLL. + /// The .disabled file. + public static FileInfo GetDisabledFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName!, ".disabled")); - /// - /// A standardized way to get the obsolete .testing file that should accompany a plugin DLL. May not exist. - /// - /// The plugin DLL. - /// The .testing file. - public static FileInfo GetTestingFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName, ".testing")); - - /// - /// Check if this manifest is valid. - /// - /// Whether or not this manifest is valid. - public bool CheckSanity() - { - if (this.InternalName.IsNullOrEmpty()) - return false; - - if (this.Name.IsNullOrEmpty()) - return false; - - if (this.DalamudApiLevel == 0) - return false; - - return true; - } - } + /// + /// A standardized way to get the obsolete .testing file that should accompany a plugin DLL. May not exist. + /// + /// The plugin DLL. + /// The .testing file. + public static FileInfo GetTestingFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName!, ".testing")); } diff --git a/Dalamud/Plugin/Internal/Types/PluginManifest.cs b/Dalamud/Plugin/Internal/Types/PluginManifest.cs index 9d28a1b5c..422c1b0c8 100644 --- a/Dalamud/Plugin/Internal/Types/PluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/PluginManifest.cs @@ -4,177 +4,159 @@ using System.Collections.Generic; using Dalamud.Game; using Newtonsoft.Json; -namespace Dalamud.Plugin.Internal.Types +namespace Dalamud.Plugin.Internal.Types; + +/// +/// Information about a plugin, packaged in a json file with the DLL. +/// +internal record PluginManifest { /// - /// Information about a plugin, packaged in a json file with the DLL. + /// Gets the author/s of the plugin. /// - internal record PluginManifest - { - /// - /// Gets the author/s of the plugin. - /// - [JsonProperty] - public string? Author { get; init; } + [JsonProperty] + public string? Author { get; init; } - /// - /// Gets or sets the public name of the plugin. - /// - [JsonProperty] - public string Name { get; set; } + /// + /// Gets or sets the public name of the plugin. + /// + [JsonProperty] + public string Name { get; set; } = null!; - /// - /// Gets a punchline of the plugins functions. - /// - [JsonProperty] - public string? Punchline { get; init; } + /// + /// Gets a punchline of the plugins functions. + /// + [JsonProperty] + public string? Punchline { get; init; } - /// - /// Gets a description of the plugins functions. - /// - [JsonProperty] - public string? Description { get; init; } + /// + /// Gets a description of the plugins functions. + /// + [JsonProperty] + public string? Description { get; init; } - /// - /// Gets a changelog. - /// - [JsonProperty] - public string? Changelog { get; init; } + /// + /// Gets a changelog. + /// + [JsonProperty] + public string? Changelog { get; init; } - /// - /// Gets a list of tags defined on the plugin. - /// - [JsonProperty] - public List? Tags { get; init; } + /// + /// Gets a list of tags defined on the plugin. + /// + [JsonProperty] + public List? Tags { get; init; } - /// - /// Gets a list of category tags defined on the plugin. - /// - [JsonProperty] - public List? CategoryTags { get; init; } + /// + /// Gets a list of category tags defined on the plugin. + /// + [JsonProperty] + public List? CategoryTags { get; init; } - /// - /// Gets a value indicating whether or not the plugin is hidden in the plugin installer. - /// This value comes from the plugin master and is in addition to the list of hidden names kept by Dalamud. - /// - [JsonProperty] - public bool IsHide { get; init; } + /// + /// Gets a value indicating whether or not the plugin is hidden in the plugin installer. + /// This value comes from the plugin master and is in addition to the list of hidden names kept by Dalamud. + /// + [JsonProperty] + public bool IsHide { get; init; } - /// - /// Gets the internal name of the plugin, which should match the assembly name of the plugin. - /// - [JsonProperty] - public string InternalName { get; init; } + /// + /// Gets the internal name of the plugin, which should match the assembly name of the plugin. + /// + [JsonProperty] + public string InternalName { get; init; } = null!; - /// - /// Gets the current assembly version of the plugin. - /// - [JsonProperty] - public Version AssemblyVersion { get; init; } + /// + /// Gets the current assembly version of the plugin. + /// + [JsonProperty] + public Version AssemblyVersion { get; init; } = null!; - /// - /// Gets the current testing assembly version of the plugin. - /// - [JsonProperty] - public Version? TestingAssemblyVersion { get; init; } + /// + /// Gets the current testing assembly version of the plugin. + /// + [JsonProperty] + public Version? TestingAssemblyVersion { get; init; } - /// - /// Gets a value indicating whether the is not null. - /// - [JsonIgnore] - public bool HasAssemblyVersion => this.AssemblyVersion != null; + /// + /// Gets a value indicating whether the plugin is only available for testing. + /// + [JsonProperty] + public bool IsTestingExclusive { get; init; } - /// - /// Gets a value indicating whether the is not null. - /// - [JsonIgnore] - public bool HasTestingAssemblyVersion => this.TestingAssemblyVersion != null; + /// + /// Gets an URL to the website or source code of the plugin. + /// + [JsonProperty] + public string? RepoUrl { get; init; } - /// - /// Gets a value indicating whether the plugin is only available for testing. - /// - [JsonProperty] - public bool IsTestingExclusive { get; init; } + /// + /// Gets the version of the game this plugin works with. + /// + [JsonProperty] + [JsonConverter(typeof(GameVersionConverter))] + public GameVersion? ApplicableVersion { get; init; } = GameVersion.Any; - /// - /// Gets an URL to the website or source code of the plugin. - /// - [JsonProperty] - public string? RepoUrl { get; init; } + /// + /// Gets the API level of this plugin. For the current API level, please see + /// for the currently used API level. + /// + [JsonProperty] + public int DalamudApiLevel { get; init; } = PluginManager.DalamudApiLevel; - /// - /// Gets the version of the game this plugin works with. - /// - [JsonProperty] - [JsonConverter(typeof(GameVersionConverter))] - public GameVersion? ApplicableVersion { get; init; } = GameVersion.Any; + /// + /// Gets the number of downloads this plugin has. + /// + [JsonProperty] + public long DownloadCount { get; init; } - /// - /// Gets the API level of this plugin. For the current API level, please see - /// for the currently used API level. - /// - [JsonProperty] - public int DalamudApiLevel { get; init; } = PluginManager.DalamudApiLevel; + /// + /// Gets the last time this plugin was updated. + /// + [JsonProperty] + public long LastUpdate { get; init; } - /// - /// Gets the number of downloads this plugin has. - /// - [JsonProperty] - public long DownloadCount { get; init; } + /// + /// Gets the download link used to install the plugin. + /// + [JsonProperty] + public string DownloadLinkInstall { get; init; } = null!; - /// - /// Gets the last time this plugin was updated. - /// - [JsonProperty] - public long LastUpdate { get; init; } + /// + /// Gets the download link used to update the plugin. + /// + [JsonProperty] + public string DownloadLinkUpdate { get; init; } = null!; - /// - /// Gets the download link used to install the plugin. - /// - [JsonProperty] - public string DownloadLinkInstall { get; init; } + /// + /// Gets the download link used to get testing versions of the plugin. + /// + [JsonProperty] + public string DownloadLinkTesting { get; init; } = null!; - /// - /// Gets the download link used to update the plugin. - /// - [JsonProperty] - public string DownloadLinkUpdate { get; init; } + /// + /// Gets the load priority for this plugin. Higher values means higher priority. 0 is default priority. + /// + [JsonProperty] + public int LoadPriority { get; init; } - /// - /// Gets the download link used to get testing versions of the plugin. - /// - [JsonProperty] - public string DownloadLinkTesting { get; init; } + /// + /// Gets a list of screenshot image URLs to show in the plugin installer. + /// + public List? ImageUrls { get; init; } - /// - /// Gets the load priority for this plugin. Higher values means higher priority. 0 is default priority. - /// - [JsonProperty] - public int LoadPriority { get; init; } + /// + /// Gets an URL for the plugin's icon. + /// + public string? IconUrl { get; init; } - /// - /// Gets a list of screenshot image URLs to show in the plugin installer. - /// - public List? ImageUrls { get; init; } + /// + /// Gets a value indicating whether this plugin accepts feedback. + /// + public bool AcceptsFeedback { get; init; } = true; - /// - /// Gets an URL for the plugin's icon. - /// - public string? IconUrl { get; init; } - - /// - /// Gets a value indicating whether this plugin accepts feedback. - /// - public bool AcceptsFeedback { get; init; } = true; - - /// - /// Gets a message that is shown to users when sending feedback. - /// - public string? FeedbackMessage { get; init; } - - /// - /// Gets a value indicating the webhook URL feedback is sent to. - /// - public string? FeedbackWebhook { get; init; } - } + /// + /// Gets a message that is shown to users when sending feedback. + /// + public string? FeedbackMessage { get; init; } } diff --git a/Dalamud/Plugin/Internal/Types/PluginRepository.cs b/Dalamud/Plugin/Internal/Types/PluginRepository.cs new file mode 100644 index 000000000..63ea5c5d4 --- /dev/null +++ b/Dalamud/Plugin/Internal/Types/PluginRepository.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; + +using Dalamud.Logging.Internal; +using Newtonsoft.Json; + +namespace Dalamud.Plugin.Internal.Types; + +/// +/// This class represents a single plugin repository. +/// +internal class PluginRepository +{ + private const string DalamudPluginsMasterUrl = "https://kamori.goats.dev/Plugin/PluginMaster"; + + private static readonly ModuleLog Log = new("PLUGINR"); + + private static readonly HttpClient HttpClient = new() + { + DefaultRequestHeaders = + { + CacheControl = new CacheControlHeaderValue + { + NoCache = true, + }, + }, + }; + + /// + /// Initializes a new instance of the class. + /// + /// The plugin master URL. + /// Whether the plugin repo is enabled. + public PluginRepository(string pluginMasterUrl, bool isEnabled) + { + this.PluginMasterUrl = pluginMasterUrl; + this.IsThirdParty = pluginMasterUrl != DalamudPluginsMasterUrl; + this.IsEnabled = isEnabled; + } + + /// + /// Gets a new instance of the class for the main repo. + /// + public static PluginRepository MainRepo => new(DalamudPluginsMasterUrl, true); + + /// + /// Gets the pluginmaster.json URL. + /// + public string PluginMasterUrl { get; } + + /// + /// Gets a value indicating whether this plugin repository is from a third party. + /// + public bool IsThirdParty { get; } + + /// + /// Gets a value indicating whether this repo is enabled. + /// + public bool IsEnabled { get; } + + /// + /// Gets the plugin master list of available plugins. + /// + public ReadOnlyCollection? PluginMaster { get; private set; } + + /// + /// Gets the initialization state of the plugin repository. + /// + public PluginRepositoryState State { get; private set; } + + /// + /// Reload the plugin master asynchronously in a task. + /// + /// The new state. + public async Task ReloadPluginMasterAsync() + { + this.State = PluginRepositoryState.InProgress; + this.PluginMaster = new List().AsReadOnly(); + + try + { + Log.Information($"Fetching repo: {this.PluginMasterUrl}"); + + using var response = await HttpClient.GetAsync(this.PluginMasterUrl); + response.EnsureSuccessStatusCode(); + + var data = await response.Content.ReadAsStringAsync(); + var pluginMaster = JsonConvert.DeserializeObject>(data); + + if (pluginMaster == null) + { + throw new Exception("Deserialized PluginMaster was null."); + } + + pluginMaster.Sort((pm1, pm2) => string.Compare(pm1.Name, pm2.Name, StringComparison.Ordinal)); + + // Set the source for each remote manifest. Allows for checking if is 3rd party. + foreach (var manifest in pluginMaster) + { + manifest.SourceRepo = this; + } + + this.PluginMaster = pluginMaster.AsReadOnly(); + + Log.Debug($"Successfully fetched repo: {this.PluginMasterUrl}"); + this.State = PluginRepositoryState.Success; + } + catch (Exception ex) + { + Log.Error(ex, $"PluginMaster failed: {this.PluginMasterUrl}"); + this.State = PluginRepositoryState.Fail; + } + } +} diff --git a/Dalamud/Plugin/Internal/Types/PluginRepositoryState.cs b/Dalamud/Plugin/Internal/Types/PluginRepositoryState.cs index 46aa2c351..2909ff981 100644 --- a/Dalamud/Plugin/Internal/Types/PluginRepositoryState.cs +++ b/Dalamud/Plugin/Internal/Types/PluginRepositoryState.cs @@ -1,28 +1,27 @@ -namespace Dalamud.Plugin.Internal.Types +namespace Dalamud.Plugin.Internal.Types; + +/// +/// Values representing plugin repository state. +/// +internal enum PluginRepositoryState { /// - /// Values representing plugin repository state. + /// State is unknown. /// - internal enum PluginRepositoryState - { - /// - /// State is unknown. - /// - Unknown, + Unknown, - /// - /// Currently loading. - /// - InProgress, + /// + /// Currently loading. + /// + InProgress, - /// - /// Load was successful. - /// - Success, + /// + /// Load was successful. + /// + Success, - /// - /// Load failed. - /// - Fail, - } + /// + /// Load failed. + /// + Fail, } diff --git a/Dalamud/Plugin/Internal/Types/PluginState.cs b/Dalamud/Plugin/Internal/Types/PluginState.cs index f32543b39..da5fcf977 100644 --- a/Dalamud/Plugin/Internal/Types/PluginState.cs +++ b/Dalamud/Plugin/Internal/Types/PluginState.cs @@ -1,33 +1,32 @@ -namespace Dalamud.Plugin.Internal.Types +namespace Dalamud.Plugin.Internal.Types; + +/// +/// Values representing plugin load state. +/// +internal enum PluginState { /// - /// Values representing plugin load state. + /// Plugin is defined, but unloaded. /// - internal enum PluginState - { - /// - /// Plugin is defined, but unloaded. - /// - Unloaded, + Unloaded, - /// - /// Plugin has thrown an error during unload. - /// - UnloadError, + /// + /// Plugin has thrown an error during unload. + /// + UnloadError, - /// - /// Currently loading. - /// - InProgress, + /// + /// Currently loading. + /// + InProgress, - /// - /// Load is successful. - /// - Loaded, + /// + /// Load is successful. + /// + Loaded, - /// - /// Plugin has thrown an error during loading. - /// - LoadError, - } + /// + /// Plugin has thrown an error during loading. + /// + LoadError, } diff --git a/Dalamud/Plugin/Internal/Types/PluginUpdateStatus.cs b/Dalamud/Plugin/Internal/Types/PluginUpdateStatus.cs index f0394b9b7..02eba7ea7 100644 --- a/Dalamud/Plugin/Internal/Types/PluginUpdateStatus.cs +++ b/Dalamud/Plugin/Internal/Types/PluginUpdateStatus.cs @@ -1,30 +1,29 @@ using System; -namespace Dalamud.Plugin.Internal.Types +namespace Dalamud.Plugin.Internal.Types; + +/// +/// Plugin update status. +/// +internal class PluginUpdateStatus { /// - /// Plugin update status. + /// Gets the plugin internal name. /// - internal class PluginUpdateStatus - { - /// - /// Gets or sets the plugin internal name. - /// - public string InternalName { get; set; } + public string InternalName { get; init; } = null!; - /// - /// Gets or sets the plugin name. - /// - public string Name { get; set; } + /// + /// Gets the plugin name. + /// + public string Name { get; init; } = null!; - /// - /// Gets or sets the plugin version. - /// - public Version Version { get; set; } + /// + /// Gets the plugin version. + /// + public Version Version { get; init; } = null!; - /// - /// Gets or sets a value indicating whether the plugin was updated. - /// - public bool WasUpdated { get; set; } - } + /// + /// Gets or sets a value indicating whether the plugin was updated. + /// + public bool WasUpdated { get; set; } } diff --git a/Dalamud/Plugin/Internal/Types/RemotePluginManifest.cs b/Dalamud/Plugin/Internal/Types/RemotePluginManifest.cs index cbb989159..09084d569 100644 --- a/Dalamud/Plugin/Internal/Types/RemotePluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/RemotePluginManifest.cs @@ -1,18 +1,19 @@ +using JetBrains.Annotations; using Newtonsoft.Json; -namespace Dalamud.Plugin.Internal.Types +namespace Dalamud.Plugin.Internal.Types; + +/// +/// Information about a plugin, packaged in a json file with the DLL. This variant includes additional information such as +/// if the plugin is disabled and if it was installed from a testing URL. This is designed for use with manifests on disk. +/// +[UsedImplicitly] +internal record RemotePluginManifest : PluginManifest { /// - /// Information about a plugin, packaged in a json file with the DLL. This variant includes additional information such as - /// if the plugin is disabled and if it was installed from a testing URL. This is designed for use with manifests on disk. + /// Gets or sets the plugin repository this manifest came from. Used in reporting which third party repo a manifest + /// may have come from in the plugins available view. This functionality should not be included in the plugin master. /// - internal record RemotePluginManifest : PluginManifest - { - /// - /// Gets or sets the plugin repository this manifest came from. Used in reporting which third party repo a manifest - /// may have come from in the plugins available view. This functionality should not be included in the plugin master. - /// - [JsonIgnore] - public PluginRepository SourceRepo { get; set; } = null; - } + [JsonIgnore] + public PluginRepository SourceRepo { get; set; } = null!; } From bf1a525e4c8b19db7b5577b84be7593751e6ba0f Mon Sep 17 00:00:00 2001 From: kizer Date: Thu, 12 May 2022 17:34:58 +0900 Subject: [PATCH 12/19] Fix --mode= handling on injector arguments (#829) --- Dalamud.Injector/EntryPoint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index ccb093c1d..4bbdad68b 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -454,7 +454,7 @@ namespace Dalamud.Injector else if (args[i] == "-m") mode = args[++i]; else if (args[i].StartsWith("--mode=")) - gamePath = args[i].Split('=', 2)[1]; + mode = args[i].Split('=', 2)[1]; else if (args[i].StartsWith("--handle-owner=")) handleOwner = IntPtr.Parse(args[i].Split('=', 2)[1]); else if (args[i] == "--") From 9413755ee3d34acf2f8922ff91c41af6ba180960 Mon Sep 17 00:00:00 2001 From: kizer Date: Thu, 12 May 2022 17:36:05 +0900 Subject: [PATCH 13/19] Add Service.RunOnTick() (#832) --- Dalamud/Game/Framework.cs | 179 +++++++++++++++++- .../Interface/Internal/Windows/DataWindow.cs | 64 ++++++- 2 files changed, 237 insertions(+), 6 deletions(-) diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index ede9f7cbb..52b9ef020 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -4,7 +4,7 @@ using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Threading; - +using System.Threading.Tasks; using Dalamud.Game.Gui; using Dalamud.Game.Gui.Toast; using Dalamud.Game.Libc; @@ -26,7 +26,9 @@ namespace Dalamud.Game public sealed class Framework : IDisposable { private static Stopwatch statsStopwatch = new(); - private Stopwatch updateStopwatch = new(); + + private readonly List runOnNextTickTaskList = new(); + private readonly Stopwatch updateStopwatch = new(); private bool tier2Initialized = false; private bool tier3Initialized = false; @@ -36,6 +38,8 @@ namespace Dalamud.Game private Hook destroyHook; private Hook realDestroyHook; + private Thread? frameworkUpdateThread; + /// /// Initializes a new instance of the class. /// @@ -113,6 +117,11 @@ namespace Dalamud.Game /// public TimeSpan UpdateDelta { get; private set; } = TimeSpan.Zero; + /// + /// Gets a value indicating whether currently executing code is running in the game's framework update thread. + /// + public bool IsInFrameworkUpdateThread => Thread.CurrentThread == this.frameworkUpdateThread; + /// /// Gets or sets a value indicating whether to dispatch update events. /// @@ -132,6 +141,85 @@ namespace Dalamud.Game this.realDestroyHook.Enable(); } + /// + /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. + /// + /// Return type. + /// Function to call. + /// Task representing the pending or already completed function. + public Task RunOnFrameworkThread(Func func) => this.IsInFrameworkUpdateThread ? Task.FromResult(func()) : this.RunOnTick(func); + + /// + /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. + /// + /// Function to call. + /// Task representing the pending or already completed function. + public Task RunOnFrameworkThread(Action action) + { + if (this.IsInFrameworkUpdateThread) + { + try + { + action(); + return Task.CompletedTask; + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + else + { + return this.RunOnTick(action); + } + } + + /// + /// Run given function in upcoming Framework.Tick call. + /// + /// Return type. + /// Function to call. + /// Wait for given timespan before calling this function. + /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. + /// Cancellation token which will prevent the execution of this function if wait conditions are not met. + /// Task representing the pending function. + public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource(); + this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc() + { + RemainingTicks = delayTicks, + RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds), + CancellationToken = cancellationToken, + TaskCompletionSource = tcs, + Func = func, + }); + return tcs.Task; + } + + /// + /// Run given function in upcoming Framework.Tick call. + /// + /// Return type. + /// Function to call. + /// Wait for given timespan before calling this function. + /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. + /// Cancellation token which will prevent the execution of this function if wait conditions are not met. + /// Task representing the pending function. + public Task RunOnTick(Action action, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource(); + this.runOnNextTickTaskList.Add(new RunOnNextTickTaskAction() + { + RemainingTicks = delayTicks, + RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds), + CancellationToken = cancellationToken, + TaskCompletionSource = tcs, + Action = action, + }); + return tcs.Task; + } + /// /// Dispose of managed and unmanaged resources. /// @@ -179,6 +267,8 @@ namespace Dalamud.Game if (this.tierInitError) goto original; + this.frameworkUpdateThread ??= Thread.CurrentThread; + var dalamud = Service.Get(); // If this is the first time we are running this loop, we need to init Dalamud subsystems synchronously @@ -223,6 +313,8 @@ namespace Dalamud.Game try { + this.runOnNextTickTaskList.RemoveAll(x => x.Run()); + if (StatsEnabled && this.Update != null) { // Stat Tracking for Framework Updates @@ -312,5 +404,88 @@ namespace Dalamud.Game // Return the original trampoline location to cleanly exit return originalPtr; } + + private abstract class RunOnNextTickTaskBase + { + internal int RemainingTicks { get; set; } + + internal long RunAfterTickCount { get; init; } + + internal CancellationToken CancellationToken { get; init; } + + internal bool Run() + { + if (this.CancellationToken.IsCancellationRequested) + { + this.CancelImpl(); + return true; + } + + if (this.RemainingTicks > 0) + this.RemainingTicks -= 1; + if (this.RemainingTicks > 0) + return false; + + if (this.RunAfterTickCount > Environment.TickCount64) + return false; + + this.RunImpl(); + + return true; + } + + protected abstract void RunImpl(); + + protected abstract void CancelImpl(); + } + + private class RunOnNextTickTaskFunc : RunOnNextTickTaskBase + { + internal TaskCompletionSource TaskCompletionSource { get; init; } + + internal Func Func { get; init; } + + protected override void RunImpl() + { + try + { + this.TaskCompletionSource.SetResult(this.Func()); + } + catch (Exception ex) + { + this.TaskCompletionSource.SetException(ex); + } + } + + protected override void CancelImpl() + { + this.TaskCompletionSource.SetCanceled(); + } + } + + private class RunOnNextTickTaskAction : RunOnNextTickTaskBase + { + internal TaskCompletionSource TaskCompletionSource { get; init; } + + internal Action Action { get; init; } + + protected override void RunImpl() + { + try + { + this.Action(); + this.TaskCompletionSource.SetResult(); + } + catch (Exception ex) + { + this.TaskCompletionSource.SetException(ex); + } + } + + protected override void CancelImpl() + { + this.TaskCompletionSource.SetCanceled(); + } + } } } diff --git a/Dalamud/Interface/Internal/Windows/DataWindow.cs b/Dalamud/Interface/Internal/Windows/DataWindow.cs index 2783d00ce..98a5bb63f 100644 --- a/Dalamud/Interface/Internal/Windows/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/DataWindow.cs @@ -114,6 +114,9 @@ namespace Dalamud.Interface.Internal.Windows private DtrBarEntry? dtrTest2; private DtrBarEntry? dtrTest3; + // Task Scheduler + private CancellationTokenSource taskSchedCancelSource = new(); + private uint copyButtonIndex = 0; /// @@ -1359,6 +1362,15 @@ namespace Dalamud.Interface.Internal.Windows ImGuiHelpers.ScaledDummy(10); ImGui.SameLine(); + if (ImGui.Button("Cancel using CancellationTokenSource")) + { + this.taskSchedCancelSource.Cancel(); + this.taskSchedCancelSource = new(); + } + + ImGui.Text("Run in any thread: "); + ImGui.SameLine(); + if (ImGui.Button("Short Task.Run")) { Task.Run(() => { Thread.Sleep(500); }); @@ -1368,7 +1380,8 @@ namespace Dalamud.Interface.Internal.Windows if (ImGui.Button("Task in task(Delay)")) { - Task.Run(async () => await this.TestTaskInTaskDelay()); + var token = this.taskSchedCancelSource.Token; + Task.Run(async () => await this.TestTaskInTaskDelay(token)); } ImGui.SameLine(); @@ -1391,28 +1404,71 @@ namespace Dalamud.Interface.Internal.Windows }); } + ImGui.Text("Run in Framework.Update: "); + ImGui.SameLine(); + + if (ImGui.Button("ASAP")) + { + Task.Run(async () => await Service.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedCancelSource.Token)); + } + + ImGui.SameLine(); + + if (ImGui.Button("In 1s")) + { + Task.Run(async () => await Service.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedCancelSource.Token, delay: TimeSpan.FromSeconds(1))); + } + + ImGui.SameLine(); + + if (ImGui.Button("In 60f")) + { + Task.Run(async () => await Service.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedCancelSource.Token, delayTicks: 60)); + } + + ImGui.SameLine(); + + if (ImGui.Button("Error in 1s")) + { + Task.Run(async () => await Service.Get().RunOnTick(() => throw new Exception("Test Exception"), cancellationToken: this.taskSchedCancelSource.Token, delay: TimeSpan.FromSeconds(1))); + } + + ImGui.SameLine(); + + if (ImGui.Button("As long as it's in Framework Thread")) + { + Task.Run(async () => await Service.Get().RunOnFrameworkThread(() => { Log.Information("Task dispatched from non-framework.update thread"); })); + Service.Get().RunOnFrameworkThread(() => { Log.Information("Task dispatched from framework.update thread"); }).Wait(); + } + if (ImGui.Button("Drown in tasks")) { + var token = this.taskSchedCancelSource.Token; Task.Run(() => { for (var i = 0; i < 100; i++) { + token.ThrowIfCancellationRequested(); Task.Run(() => { for (var i = 0; i < 100; i++) { + token.ThrowIfCancellationRequested(); Task.Run(() => { for (var i = 0; i < 100; i++) { + token.ThrowIfCancellationRequested(); Task.Run(() => { for (var i = 0; i < 100; i++) { - Task.Run(() => + token.ThrowIfCancellationRequested(); + Task.Run(async () => { for (var i = 0; i < 100; i++) { + token.ThrowIfCancellationRequested(); Thread.Sleep(1); } }); @@ -1652,9 +1708,9 @@ namespace Dalamud.Interface.Internal.Windows } } - private async Task TestTaskInTaskDelay() + private async Task TestTaskInTaskDelay(CancellationToken token) { - await Task.Delay(5000); + await Task.Delay(5000, token); } #pragma warning disable 1998 From d68b3a1845575a0608197922c5fcaf782c9b26f6 Mon Sep 17 00:00:00 2001 From: kizer Date: Thu, 12 May 2022 17:46:50 +0900 Subject: [PATCH 14/19] Expose CopyGlyphsAcrossFonts (#824) --- .../Interface/GameFonts/GameFontManager.cs | 199 ++++++------------ .../Interface/Internal/InterfaceManager.cs | 13 +- .../Internal/Windows/SettingsWindow.cs | 3 +- Dalamud/Utility/Util.cs | 94 +++++++++ 4 files changed, 166 insertions(+), 143 deletions(-) diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs index ae45b7226..fec324a73 100644 --- a/Dalamud/Interface/GameFonts/GameFontManager.cs +++ b/Dalamud/Interface/GameFonts/GameFontManager.cs @@ -7,6 +7,7 @@ using System.Text; using Dalamud.Data; using Dalamud.Interface.Internal; +using Dalamud.Utility; using ImGuiNET; using Lumina.Data.Files; using Serilog; @@ -38,6 +39,9 @@ namespace Dalamud.Interface.GameFonts private readonly Dictionary fontUseCounter = new(); private readonly Dictionary>> glyphRectIds = new(); + private bool isBetweenBuildFontsAndAfterBuildFonts = false; + private bool isBuildingAsFallbackFontMode = false; + /// /// Initializes a new instance of the class. /// @@ -110,65 +114,6 @@ namespace Dalamud.Interface.GameFonts }; } - /// - /// Fills missing glyphs in target font from source font, if both are not null. - /// - /// Source font. - /// Target font. - /// Whether to copy missing glyphs only. - /// Whether to call target.BuildLookupTable(). - /// Low codepoint range to copy. - /// High codepoing range to copy. - public static void CopyGlyphsAcrossFonts(ImFontPtr? source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable, int rangeLow = 32, int rangeHigh = 0xFFFE) - { - if (!source.HasValue || !target.HasValue) - return; - - var scale = target.Value!.FontSize / source.Value!.FontSize; - unsafe - { - var glyphs = (ImFontGlyphReal*)source.Value!.Glyphs.Data; - for (int j = 0, j_ = source.Value!.Glyphs.Size; j < j_; j++) - { - var glyph = &glyphs[j]; - if (glyph->Codepoint < rangeLow || glyph->Codepoint > rangeHigh) - continue; - - var prevGlyphPtr = (ImFontGlyphReal*)target.Value!.FindGlyphNoFallback((ushort)glyph->Codepoint).NativePtr; - if ((IntPtr)prevGlyphPtr == IntPtr.Zero) - { - target.Value!.AddGlyph( - target.Value!.ConfigData, - (ushort)glyph->Codepoint, - glyph->X0 * scale, - ((glyph->Y0 - source.Value!.Ascent) * scale) + target.Value!.Ascent, - glyph->X1 * scale, - ((glyph->Y1 - source.Value!.Ascent) * scale) + target.Value!.Ascent, - glyph->U0, - glyph->V0, - glyph->U1, - glyph->V1, - glyph->AdvanceX * scale); - } - else if (!missingOnly) - { - prevGlyphPtr->X0 = glyph->X0 * scale; - prevGlyphPtr->Y0 = ((glyph->Y0 - source.Value!.Ascent) * scale) + target.Value!.Ascent; - prevGlyphPtr->X1 = glyph->X1 * scale; - prevGlyphPtr->Y1 = ((glyph->Y1 - source.Value!.Ascent) * scale) + target.Value!.Ascent; - prevGlyphPtr->U0 = glyph->U0; - prevGlyphPtr->V0 = glyph->V0; - prevGlyphPtr->U1 = glyph->U1; - prevGlyphPtr->V1 = glyph->V1; - prevGlyphPtr->AdvanceX = glyph->AdvanceX * scale; - } - } - } - - if (rebuildLookupTable) - target.Value!.BuildLookupTable(); - } - /// /// Unscales fonts after they have been rendered onto atlas. /// @@ -191,7 +136,7 @@ namespace Dalamud.Interface.GameFonts font->Descent /= fontScale; if (font->ConfigData != null) font->ConfigData->SizePixels /= fontScale; - var glyphs = (ImFontGlyphReal*)font->Glyphs.Data; + var glyphs = (Util.ImFontGlyphReal*)font->Glyphs.Data; for (int i = 0, i_ = font->Glyphs.Size; i < i_; i++) { var glyph = &glyphs[i]; @@ -223,15 +168,22 @@ namespace Dalamud.Interface.GameFonts lock (this.syncRoot) { - var prevValue = this.fontUseCounter.GetValueOrDefault(style, 0); - var newValue = this.fontUseCounter[style] = prevValue + 1; - needRebuild = (prevValue == 0) != (newValue == 0) && !this.fonts.ContainsKey(style); + this.fontUseCounter[style] = this.fontUseCounter.GetValueOrDefault(style, 0) + 1; } + needRebuild = !this.fonts.ContainsKey(style); if (needRebuild) { - Log.Information("[GameFontManager] Calling RebuildFonts because {0} has been requested.", style.ToString()); - this.interfaceManager.RebuildFonts(); + if (Service.Get().IsBuildingFontsBeforeAtlasBuild && this.isBetweenBuildFontsAndAfterBuildFonts) + { + Log.Information("[GameFontManager] NewFontRef: Building {0} right now, as it is called while BuildFonts is already in progress yet atlas build has not been called yet.", style.ToString()); + this.EnsureFont(style); + } + else + { + Log.Information("[GameFontManager] NewFontRef: Calling RebuildFonts because {0} has been requested.", style.ToString()); + this.interfaceManager.RebuildFonts(); + } } return new(this, style); @@ -260,7 +212,7 @@ namespace Dalamud.Interface.GameFonts /// Whether to call target.BuildLookupTable(). public void CopyGlyphsAcrossFonts(ImFontPtr? source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable) { - GameFontManager.CopyGlyphsAcrossFonts(source, this.fonts[target], missingOnly, rebuildLookupTable); + Util.CopyGlyphsAcrossFonts(source, this.fonts[target], missingOnly, rebuildLookupTable); } /// @@ -272,7 +224,7 @@ namespace Dalamud.Interface.GameFonts /// Whether to call target.BuildLookupTable(). public void CopyGlyphsAcrossFonts(GameFontStyle source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable) { - GameFontManager.CopyGlyphsAcrossFonts(this.fonts[source], target, missingOnly, rebuildLookupTable); + Util.CopyGlyphsAcrossFonts(this.fonts[source], target, missingOnly, rebuildLookupTable); } /// @@ -284,7 +236,7 @@ namespace Dalamud.Interface.GameFonts /// Whether to call target.BuildLookupTable(). public void CopyGlyphsAcrossFonts(GameFontStyle source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable) { - GameFontManager.CopyGlyphsAcrossFonts(this.fonts[source], this.fonts[target], missingOnly, rebuildLookupTable); + Util.CopyGlyphsAcrossFonts(this.fonts[source], this.fonts[target], missingOnly, rebuildLookupTable); } /// @@ -293,57 +245,20 @@ namespace Dalamud.Interface.GameFonts /// Whether to load fonts in minimum sizes. public void BuildFonts(bool forceMinSize) { - unsafe - { - ImFontConfigPtr fontConfig = ImGuiNative.ImFontConfig_ImFontConfig(); - fontConfig.OversampleH = 1; - fontConfig.OversampleV = 1; - fontConfig.PixelSnapH = false; + this.isBuildingAsFallbackFontMode = forceMinSize; + this.isBetweenBuildFontsAndAfterBuildFonts = true; - var io = ImGui.GetIO(); + this.glyphRectIds.Clear(); + this.fonts.Clear(); - this.glyphRectIds.Clear(); - this.fonts.Clear(); - - foreach (var style in this.fontUseCounter.Keys) - { - var rectIds = this.glyphRectIds[style] = new(); - - var fdt = this.fdts[(int)(forceMinSize ? style.FamilyWithMinimumSize : style.FamilyAndSize)]; - if (fdt == null) - continue; - - var font = io.Fonts.AddFontDefault(fontConfig); - - this.fonts[style] = font; - foreach (var glyph in fdt.Glyphs) - { - var c = glyph.Char; - if (c < 32 || c >= 0xFFFF) - continue; - - var widthAdjustment = style.CalculateBaseWidthAdjustment(fdt, glyph); - rectIds[c] = Tuple.Create( - io.Fonts.AddCustomRectFontGlyph( - font, - c, - glyph.BoundingWidth + widthAdjustment + 1, - glyph.BoundingHeight + 1, - glyph.AdvanceWidth, - new Vector2(0, glyph.CurrentOffsetY)), - glyph); - } - } - - fontConfig.Destroy(); - } + foreach (var style in this.fontUseCounter.Keys) + this.EnsureFont(style); } /// /// Post-build fonts before plugins do something more. To be called from InterfaceManager. /// - /// Whether to load fonts in minimum sizes. - public unsafe void AfterBuildFonts(bool forceMinSize) + public unsafe void AfterBuildFonts() { var ioFonts = ImGui.GetIO().Fonts; ioFonts.GetTexDataAsRGBA32(out byte* pixels8, out var width, out var height); @@ -352,7 +267,7 @@ namespace Dalamud.Interface.GameFonts foreach (var (style, font) in this.fonts) { - var fdt = this.fdts[(int)(forceMinSize ? style.FamilyWithMinimumSize : style.FamilyAndSize)]; + var fdt = this.fdts[(int)(this.isBuildingAsFallbackFontMode ? style.FamilyWithMinimumSize : style.FamilyAndSize)]; var scale = style.SizePt / fdt.FontHeader.Size; var fontPtr = font.NativePtr; fontPtr->FontSize = fdt.FontHeader.Size * 4 / 3; @@ -449,10 +364,12 @@ namespace Dalamud.Interface.GameFonts } } - CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, font, true, false); + Util.CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, font, true, false); UnscaleFont(font, 1 / scale, false); font.BuildLookupTable(); } + + this.isBetweenBuildFontsAndAfterBuildFonts = false; } /// @@ -471,35 +388,41 @@ namespace Dalamud.Interface.GameFonts } } - private struct ImFontGlyphReal + private unsafe void EnsureFont(GameFontStyle style) { - public uint ColoredVisibleCodepoint; - public float AdvanceX; - public float X0; - public float Y0; - public float X1; - public float Y1; - public float U0; - public float V0; - public float U1; - public float V1; + var rectIds = this.glyphRectIds[style] = new(); - public bool Colored - { - get => ((this.ColoredVisibleCodepoint >> 0) & 1) != 0; - set => this.ColoredVisibleCodepoint = (this.ColoredVisibleCodepoint & 0xFFFFFFFEu) | (value ? 1u : 0u); - } + var fdt = this.fdts[(int)(this.isBuildingAsFallbackFontMode ? style.FamilyWithMinimumSize : style.FamilyAndSize)]; + if (fdt == null) + return; - public bool Visible - { - get => ((this.ColoredVisibleCodepoint >> 1) & 1) != 0; - set => this.ColoredVisibleCodepoint = (this.ColoredVisibleCodepoint & 0xFFFFFFFDu) | (value ? 2u : 0u); - } + ImFontConfigPtr fontConfig = ImGuiNative.ImFontConfig_ImFontConfig(); + fontConfig.OversampleH = 1; + fontConfig.OversampleV = 1; + fontConfig.PixelSnapH = false; - public int Codepoint + var io = ImGui.GetIO(); + var font = io.Fonts.AddFontDefault(fontConfig); + + fontConfig.Destroy(); + + this.fonts[style] = font; + foreach (var glyph in fdt.Glyphs) { - get => (int)(this.ColoredVisibleCodepoint >> 2); - set => this.ColoredVisibleCodepoint = (this.ColoredVisibleCodepoint & 3u) | ((uint)this.Codepoint << 2); + var c = glyph.Char; + if (c < 32 || c >= 0xFFFF) + continue; + + var widthAdjustment = style.CalculateBaseWidthAdjustment(fdt, glyph); + rectIds[c] = Tuple.Create( + io.Fonts.AddCustomRectFontGlyph( + font, + c, + glyph.BoundingWidth + widthAdjustment + 1, + glyph.BoundingHeight + 1, + glyph.AdvanceWidth, + new Vector2(0, glyph.CurrentOffsetY)), + glyph); } } } diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index c2ee00fdc..1d31e4df9 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -248,6 +248,11 @@ namespace Dalamud.Interface.Internal /// public int FontResolutionLevel => this.FontResolutionLevelOverride ?? Service.Get().FontResolutionLevel; + /// + /// Gets a value indicating whether we're building fonts but haven't generated atlas yet. + /// + public bool IsBuildingFontsBeforeAtlasBuild => this.isRebuildingFonts && !this.fontBuildSignal.WaitOne(0); + /// /// Enable this module. /// @@ -900,7 +905,7 @@ namespace Dalamud.Interface.Internal texPixels[i] = (byte)(Math.Pow(texPixels[i] / 255.0f, 1.0f / fontGamma) * 255.0f); } - gameFontManager.AfterBuildFonts(disableBigFonts); + gameFontManager.AfterBuildFonts(); foreach (var (font, mod) in this.loadedFontInfo) { @@ -929,14 +934,14 @@ namespace Dalamud.Interface.Internal font.Descent = mod.SourceAxis.ImFont.Descent; font.FallbackChar = mod.SourceAxis.ImFont.FallbackChar; font.EllipsisChar = mod.SourceAxis.ImFont.EllipsisChar; - GameFontManager.CopyGlyphsAcrossFonts(mod.SourceAxis.ImFont, font, false, false); + Util.CopyGlyphsAcrossFonts(mod.SourceAxis.ImFont, font, false, false); } else if (mod.Axis == TargetFontModification.AxisMode.GameGlyphsOnly) { Log.Verbose("[FONT] {0}: Overwrite game specific glyphs from AXIS of size {1}px", mod.Name, mod.SourceAxis.ImFont.FontSize, font.FontSize); if (!this.UseAxis && font.NativePtr == DefaultFont.NativePtr) mod.SourceAxis.ImFont.FontSize -= 1; - GameFontManager.CopyGlyphsAcrossFonts(mod.SourceAxis.ImFont, font, true, false, 0xE020, 0xE0DB); + Util.CopyGlyphsAcrossFonts(mod.SourceAxis.ImFont, font, true, false, 0xE020, 0xE0DB); if (!this.UseAxis && font.NativePtr == DefaultFont.NativePtr) mod.SourceAxis.ImFont.FontSize += 1; } @@ -946,7 +951,7 @@ namespace Dalamud.Interface.Internal } // Fill missing glyphs in MonoFont from DefaultFont - GameFontManager.CopyGlyphsAcrossFonts(DefaultFont, MonoFont, true, false); + Util.CopyGlyphsAcrossFonts(DefaultFont, MonoFont, true, false); for (int i = 0, i_ = ioFonts.Fonts.Size; i < i_; i++) { diff --git a/Dalamud/Interface/Internal/Windows/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs index 1a34ccd23..72f699840 100644 --- a/Dalamud/Interface/Internal/Windows/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs @@ -176,7 +176,8 @@ namespace Dalamud.Interface.Internal.Windows var configuration = Service.Get(); var interfaceManager = Service.Get(); - var rebuildFont = interfaceManager.FontGamma != configuration.FontGammaLevel + var rebuildFont = ImGui.GetIO().FontGlobalScale != configuration.GlobalUiScale + || interfaceManager.FontGamma != configuration.FontGammaLevel || interfaceManager.FontResolutionLevel != configuration.FontResolutionLevel || interfaceManager.UseAxis != configuration.UseAxisFontsFromGame; diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 9c02efe2c..4eb178854 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -526,6 +526,65 @@ namespace Dalamud.Utility Process.Start(process); } + /// + /// Fills missing glyphs in target font from source font, if both are not null. + /// + /// Source font. + /// Target font. + /// Whether to copy missing glyphs only. + /// Whether to call target.BuildLookupTable(). + /// Low codepoint range to copy. + /// High codepoing range to copy. + public static void CopyGlyphsAcrossFonts(ImFontPtr? source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable, int rangeLow = 32, int rangeHigh = 0xFFFE) + { + if (!source.HasValue || !target.HasValue) + return; + + var scale = target.Value!.FontSize / source.Value!.FontSize; + unsafe + { + var glyphs = (ImFontGlyphReal*)source.Value!.Glyphs.Data; + for (int j = 0, j_ = source.Value!.Glyphs.Size; j < j_; j++) + { + var glyph = &glyphs[j]; + if (glyph->Codepoint < rangeLow || glyph->Codepoint > rangeHigh) + continue; + + var prevGlyphPtr = (ImFontGlyphReal*)target.Value!.FindGlyphNoFallback((ushort)glyph->Codepoint).NativePtr; + if ((IntPtr)prevGlyphPtr == IntPtr.Zero) + { + target.Value!.AddGlyph( + target.Value!.ConfigData, + (ushort)glyph->Codepoint, + glyph->X0 * scale, + ((glyph->Y0 - source.Value!.Ascent) * scale) + target.Value!.Ascent, + glyph->X1 * scale, + ((glyph->Y1 - source.Value!.Ascent) * scale) + target.Value!.Ascent, + glyph->U0, + glyph->V0, + glyph->U1, + glyph->V1, + glyph->AdvanceX * scale); + } + else if (!missingOnly) + { + prevGlyphPtr->X0 = glyph->X0 * scale; + prevGlyphPtr->Y0 = ((glyph->Y0 - source.Value!.Ascent) * scale) + target.Value!.Ascent; + prevGlyphPtr->X1 = glyph->X1 * scale; + prevGlyphPtr->Y1 = ((glyph->Y1 - source.Value!.Ascent) * scale) + target.Value!.Ascent; + prevGlyphPtr->U0 = glyph->U0; + prevGlyphPtr->V0 = glyph->V0; + prevGlyphPtr->U1 = glyph->U1; + prevGlyphPtr->V1 = glyph->V1; + prevGlyphPtr->AdvanceX = glyph->AdvanceX * scale; + } + } + } + + if (rebuildLookupTable) + target.Value!.BuildLookupTable(); + } + /// /// Dispose this object. /// @@ -590,5 +649,40 @@ namespace Dalamud.Utility } } } + + /// + /// ImFontGlyph the correct version. + /// + public struct ImFontGlyphReal + { + public uint ColoredVisibleCodepoint; + public float AdvanceX; + public float X0; + public float Y0; + public float X1; + public float Y1; + public float U0; + public float V0; + public float U1; + public float V1; + + public bool Colored + { + get => ((this.ColoredVisibleCodepoint >> 0) & 1) != 0; + set => this.ColoredVisibleCodepoint = (this.ColoredVisibleCodepoint & 0xFFFFFFFEu) | (value ? 1u : 0u); + } + + public bool Visible + { + get => ((this.ColoredVisibleCodepoint >> 1) & 1) != 0; + set => this.ColoredVisibleCodepoint = (this.ColoredVisibleCodepoint & 0xFFFFFFFDu) | (value ? 2u : 0u); + } + + public int Codepoint + { + get => (int)(this.ColoredVisibleCodepoint >> 2); + set => this.ColoredVisibleCodepoint = (this.ColoredVisibleCodepoint & 3u) | ((uint)this.Codepoint << 2); + } + } } } From 55fae2dc072be7bad6a60137cba41e4f8c7bec35 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Thu, 28 Apr 2022 23:06:02 -0400 Subject: [PATCH 15/19] Check punchlines while searching for plugins --- .../Internal/Windows/PluginInstaller/PluginInstallerWindow.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 6c347b11d..b98e4e5f1 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2013,6 +2013,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller return hasSearchString && !( manifest.Name.ToLowerInvariant().Contains(searchString) || (!manifest.Author.IsNullOrEmpty() && manifest.Author.Equals(this.searchText, StringComparison.InvariantCultureIgnoreCase)) || + (!manifest.Punchline.IsNullOrEmpty() && manifest.Punchline.ToLowerInvariant().Contains(searchString)) || (manifest.Tags != null && manifest.Tags.Contains(searchString, StringComparer.InvariantCultureIgnoreCase))); } From 5e98011a57e1c13e0974d357bf9ce8c03e28cda2 Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 12 May 2022 10:54:11 +0200 Subject: [PATCH 16/19] ci: remove concurrency group from tag build workflow --- .github/workflows/tag-build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/tag-build.yml b/.github/workflows/tag-build.yml index 81ebf4f78..f367c2fc9 100644 --- a/.github/workflows/tag-build.yml +++ b/.github/workflows/tag-build.yml @@ -1,6 +1,5 @@ name: Tag Build on: [push] -concurrency: build_dalamud jobs: tag: From 02d19df0f76cbda80695afe077b830e0fa21f75a Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 12 May 2022 10:59:53 +0200 Subject: [PATCH 17/19] refactor: move CopyGlyphsAcrossFonts into ImGuiHelpers.cs --- .../Interface/GameFonts/GameFontManager.cs | 10 +- Dalamud/Interface/ImGuiHelpers.cs | 100 ++++++++++++++++++ .../Interface/Internal/InterfaceManager.cs | 6 +- Dalamud/Utility/Util.cs | 94 ---------------- 4 files changed, 108 insertions(+), 102 deletions(-) diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs index fec324a73..933558c09 100644 --- a/Dalamud/Interface/GameFonts/GameFontManager.cs +++ b/Dalamud/Interface/GameFonts/GameFontManager.cs @@ -136,7 +136,7 @@ namespace Dalamud.Interface.GameFonts font->Descent /= fontScale; if (font->ConfigData != null) font->ConfigData->SizePixels /= fontScale; - var glyphs = (Util.ImFontGlyphReal*)font->Glyphs.Data; + var glyphs = (ImGuiHelpers.ImFontGlyphReal*)font->Glyphs.Data; for (int i = 0, i_ = font->Glyphs.Size; i < i_; i++) { var glyph = &glyphs[i]; @@ -212,7 +212,7 @@ namespace Dalamud.Interface.GameFonts /// Whether to call target.BuildLookupTable(). public void CopyGlyphsAcrossFonts(ImFontPtr? source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable) { - Util.CopyGlyphsAcrossFonts(source, this.fonts[target], missingOnly, rebuildLookupTable); + ImGuiHelpers.CopyGlyphsAcrossFonts(source, this.fonts[target], missingOnly, rebuildLookupTable); } /// @@ -224,7 +224,7 @@ namespace Dalamud.Interface.GameFonts /// Whether to call target.BuildLookupTable(). public void CopyGlyphsAcrossFonts(GameFontStyle source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable) { - Util.CopyGlyphsAcrossFonts(this.fonts[source], target, missingOnly, rebuildLookupTable); + ImGuiHelpers.CopyGlyphsAcrossFonts(this.fonts[source], target, missingOnly, rebuildLookupTable); } /// @@ -236,7 +236,7 @@ namespace Dalamud.Interface.GameFonts /// Whether to call target.BuildLookupTable(). public void CopyGlyphsAcrossFonts(GameFontStyle source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable) { - Util.CopyGlyphsAcrossFonts(this.fonts[source], this.fonts[target], missingOnly, rebuildLookupTable); + ImGuiHelpers.CopyGlyphsAcrossFonts(this.fonts[source], this.fonts[target], missingOnly, rebuildLookupTable); } /// @@ -364,7 +364,7 @@ namespace Dalamud.Interface.GameFonts } } - Util.CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, font, true, false); + ImGuiHelpers.CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, font, true, false); UnscaleFont(font, 1 / scale, false); font.BuildLookupTable(); } diff --git a/Dalamud/Interface/ImGuiHelpers.cs b/Dalamud/Interface/ImGuiHelpers.cs index b71b7cdd5..99590af18 100644 --- a/Dalamud/Interface/ImGuiHelpers.cs +++ b/Dalamud/Interface/ImGuiHelpers.cs @@ -1,4 +1,7 @@ +using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Numerics; using ImGuiNET; @@ -136,6 +139,67 @@ namespace Dalamud.Interface /// The text to write. public static void SafeTextWrapped(string text) => ImGui.TextWrapped(text.Replace("%", "%%")); + /// + /// Fills missing glyphs in target font from source font, if both are not null. + /// + /// Source font. + /// Target font. + /// Whether to copy missing glyphs only. + /// Whether to call target.BuildLookupTable(). + /// Low codepoint range to copy. + /// High codepoing range to copy. + public static void CopyGlyphsAcrossFonts(ImFontPtr? source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable, int rangeLow = 32, int rangeHigh = 0xFFFE) + { + if (!source.HasValue || !target.HasValue) + return; + + var scale = target.Value!.FontSize / source.Value!.FontSize; + unsafe + { + var glyphs = (ImFontGlyphReal*)source.Value!.Glyphs.Data; + for (int j = 0, k = source.Value!.Glyphs.Size; j < k; j++) + { + Debug.Assert(glyphs != null, nameof(glyphs) + " != null"); + + var glyph = &glyphs[j]; + if (glyph->Codepoint < rangeLow || glyph->Codepoint > rangeHigh) + continue; + + var prevGlyphPtr = (ImFontGlyphReal*)target.Value!.FindGlyphNoFallback((ushort)glyph->Codepoint).NativePtr; + if ((IntPtr)prevGlyphPtr == IntPtr.Zero) + { + target.Value!.AddGlyph( + target.Value!.ConfigData, + (ushort)glyph->Codepoint, + glyph->X0 * scale, + ((glyph->Y0 - source.Value!.Ascent) * scale) + target.Value!.Ascent, + glyph->X1 * scale, + ((glyph->Y1 - source.Value!.Ascent) * scale) + target.Value!.Ascent, + glyph->U0, + glyph->V0, + glyph->U1, + glyph->V1, + glyph->AdvanceX * scale); + } + else if (!missingOnly) + { + prevGlyphPtr->X0 = glyph->X0 * scale; + prevGlyphPtr->Y0 = ((glyph->Y0 - source.Value!.Ascent) * scale) + target.Value!.Ascent; + prevGlyphPtr->X1 = glyph->X1 * scale; + prevGlyphPtr->Y1 = ((glyph->Y1 - source.Value!.Ascent) * scale) + target.Value!.Ascent; + prevGlyphPtr->U0 = glyph->U0; + prevGlyphPtr->V0 = glyph->V0; + prevGlyphPtr->U1 = glyph->U1; + prevGlyphPtr->V1 = glyph->V1; + prevGlyphPtr->AdvanceX = glyph->AdvanceX * scale; + } + } + } + + if (rebuildLookupTable) + target.Value!.BuildLookupTable(); + } + /// /// Get data needed for each new frame. /// @@ -143,5 +207,41 @@ namespace Dalamud.Interface { GlobalScale = ImGui.GetIO().FontGlobalScale; } + + /// + /// ImFontGlyph the correct version. + /// + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "ImGui internals")] + public struct ImFontGlyphReal + { + public uint ColoredVisibleCodepoint; + public float AdvanceX; + public float X0; + public float Y0; + public float X1; + public float Y1; + public float U0; + public float V0; + public float U1; + public float V1; + + public bool Colored + { + get => ((this.ColoredVisibleCodepoint >> 0) & 1) != 0; + set => this.ColoredVisibleCodepoint = (this.ColoredVisibleCodepoint & 0xFFFFFFFEu) | (value ? 1u : 0u); + } + + public bool Visible + { + get => ((this.ColoredVisibleCodepoint >> 1) & 1) != 0; + set => this.ColoredVisibleCodepoint = (this.ColoredVisibleCodepoint & 0xFFFFFFFDu) | (value ? 2u : 0u); + } + + public int Codepoint + { + get => (int)(this.ColoredVisibleCodepoint >> 2); + set => this.ColoredVisibleCodepoint = (this.ColoredVisibleCodepoint & 3u) | ((uint)this.Codepoint << 2); + } + } } } diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 1d31e4df9..6de07e0bd 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -934,14 +934,14 @@ namespace Dalamud.Interface.Internal font.Descent = mod.SourceAxis.ImFont.Descent; font.FallbackChar = mod.SourceAxis.ImFont.FallbackChar; font.EllipsisChar = mod.SourceAxis.ImFont.EllipsisChar; - Util.CopyGlyphsAcrossFonts(mod.SourceAxis.ImFont, font, false, false); + ImGuiHelpers.CopyGlyphsAcrossFonts(mod.SourceAxis.ImFont, font, false, false); } else if (mod.Axis == TargetFontModification.AxisMode.GameGlyphsOnly) { Log.Verbose("[FONT] {0}: Overwrite game specific glyphs from AXIS of size {1}px", mod.Name, mod.SourceAxis.ImFont.FontSize, font.FontSize); if (!this.UseAxis && font.NativePtr == DefaultFont.NativePtr) mod.SourceAxis.ImFont.FontSize -= 1; - Util.CopyGlyphsAcrossFonts(mod.SourceAxis.ImFont, font, true, false, 0xE020, 0xE0DB); + ImGuiHelpers.CopyGlyphsAcrossFonts(mod.SourceAxis.ImFont, font, true, false, 0xE020, 0xE0DB); if (!this.UseAxis && font.NativePtr == DefaultFont.NativePtr) mod.SourceAxis.ImFont.FontSize += 1; } @@ -951,7 +951,7 @@ namespace Dalamud.Interface.Internal } // Fill missing glyphs in MonoFont from DefaultFont - Util.CopyGlyphsAcrossFonts(DefaultFont, MonoFont, true, false); + ImGuiHelpers.CopyGlyphsAcrossFonts(DefaultFont, MonoFont, true, false); for (int i = 0, i_ = ioFonts.Fonts.Size; i < i_; i++) { diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 4eb178854..9c02efe2c 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -526,65 +526,6 @@ namespace Dalamud.Utility Process.Start(process); } - /// - /// Fills missing glyphs in target font from source font, if both are not null. - /// - /// Source font. - /// Target font. - /// Whether to copy missing glyphs only. - /// Whether to call target.BuildLookupTable(). - /// Low codepoint range to copy. - /// High codepoing range to copy. - public static void CopyGlyphsAcrossFonts(ImFontPtr? source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable, int rangeLow = 32, int rangeHigh = 0xFFFE) - { - if (!source.HasValue || !target.HasValue) - return; - - var scale = target.Value!.FontSize / source.Value!.FontSize; - unsafe - { - var glyphs = (ImFontGlyphReal*)source.Value!.Glyphs.Data; - for (int j = 0, j_ = source.Value!.Glyphs.Size; j < j_; j++) - { - var glyph = &glyphs[j]; - if (glyph->Codepoint < rangeLow || glyph->Codepoint > rangeHigh) - continue; - - var prevGlyphPtr = (ImFontGlyphReal*)target.Value!.FindGlyphNoFallback((ushort)glyph->Codepoint).NativePtr; - if ((IntPtr)prevGlyphPtr == IntPtr.Zero) - { - target.Value!.AddGlyph( - target.Value!.ConfigData, - (ushort)glyph->Codepoint, - glyph->X0 * scale, - ((glyph->Y0 - source.Value!.Ascent) * scale) + target.Value!.Ascent, - glyph->X1 * scale, - ((glyph->Y1 - source.Value!.Ascent) * scale) + target.Value!.Ascent, - glyph->U0, - glyph->V0, - glyph->U1, - glyph->V1, - glyph->AdvanceX * scale); - } - else if (!missingOnly) - { - prevGlyphPtr->X0 = glyph->X0 * scale; - prevGlyphPtr->Y0 = ((glyph->Y0 - source.Value!.Ascent) * scale) + target.Value!.Ascent; - prevGlyphPtr->X1 = glyph->X1 * scale; - prevGlyphPtr->Y1 = ((glyph->Y1 - source.Value!.Ascent) * scale) + target.Value!.Ascent; - prevGlyphPtr->U0 = glyph->U0; - prevGlyphPtr->V0 = glyph->V0; - prevGlyphPtr->U1 = glyph->U1; - prevGlyphPtr->V1 = glyph->V1; - prevGlyphPtr->AdvanceX = glyph->AdvanceX * scale; - } - } - } - - if (rebuildLookupTable) - target.Value!.BuildLookupTable(); - } - /// /// Dispose this object. /// @@ -649,40 +590,5 @@ namespace Dalamud.Utility } } } - - /// - /// ImFontGlyph the correct version. - /// - public struct ImFontGlyphReal - { - public uint ColoredVisibleCodepoint; - public float AdvanceX; - public float X0; - public float Y0; - public float X1; - public float Y1; - public float U0; - public float V0; - public float U1; - public float V1; - - public bool Colored - { - get => ((this.ColoredVisibleCodepoint >> 0) & 1) != 0; - set => this.ColoredVisibleCodepoint = (this.ColoredVisibleCodepoint & 0xFFFFFFFEu) | (value ? 1u : 0u); - } - - public bool Visible - { - get => ((this.ColoredVisibleCodepoint >> 1) & 1) != 0; - set => this.ColoredVisibleCodepoint = (this.ColoredVisibleCodepoint & 0xFFFFFFFDu) | (value ? 2u : 0u); - } - - public int Codepoint - { - get => (int)(this.ColoredVisibleCodepoint >> 2); - set => this.ColoredVisibleCodepoint = (this.ColoredVisibleCodepoint & 3u) | ((uint)this.Codepoint << 2); - } - } } } From b1d927ab8f26eeec277558534582568a6374e5cb Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 12 May 2022 11:00:31 +0200 Subject: [PATCH 18/19] chore: fix some warnings --- Dalamud/Game/Framework.cs | 2 +- Dalamud/Interface/Internal/Windows/DataWindow.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index 52b9ef020..810251de2 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; + using Dalamud.Game.Gui; using Dalamud.Game.Gui.Toast; using Dalamud.Game.Libc; @@ -200,7 +201,6 @@ namespace Dalamud.Game /// /// Run given function in upcoming Framework.Tick call. /// - /// Return type. /// Function to call. /// Wait for given timespan before calling this function. /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. diff --git a/Dalamud/Interface/Internal/Windows/DataWindow.cs b/Dalamud/Interface/Internal/Windows/DataWindow.cs index 98a5bb63f..d4f100915 100644 --- a/Dalamud/Interface/Internal/Windows/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/DataWindow.cs @@ -1469,7 +1469,7 @@ namespace Dalamud.Interface.Internal.Windows for (var i = 0; i < 100; i++) { token.ThrowIfCancellationRequested(); - Thread.Sleep(1); + await Task.Delay(1); } }); } From f9eb853a188cb902c15a04e9dfa08eef9f339fa6 Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 12 May 2022 11:05:47 +0200 Subject: [PATCH 19/19] fix: ImFontGlyphReal.Codepoint setter not using value --- Dalamud/Interface/ImGuiHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/ImGuiHelpers.cs b/Dalamud/Interface/ImGuiHelpers.cs index 99590af18..c873210c1 100644 --- a/Dalamud/Interface/ImGuiHelpers.cs +++ b/Dalamud/Interface/ImGuiHelpers.cs @@ -240,7 +240,7 @@ namespace Dalamud.Interface public int Codepoint { get => (int)(this.ColoredVisibleCodepoint >> 2); - set => this.ColoredVisibleCodepoint = (this.ColoredVisibleCodepoint & 3u) | ((uint)this.Codepoint << 2); + set => this.ColoredVisibleCodepoint = (this.ColoredVisibleCodepoint & 3u) | ((uint)value << 2); } } }