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