diff --git a/.editorconfig b/.editorconfig
index a0cd5584f..1b1377fca 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -140,3 +140,8 @@ indent_style = space
indent_size = 4
tab_width = 4
dotnet_style_parentheses_in_other_operators=always_for_clarity:silent
+
+[*.{yaml,yml}]
+indent_style = space
+indent_size = 2
+tab_width = 2
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 000000000..ea155a3f0
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1 @@
+* @goatcorp/dalamud-maintainers
diff --git a/.github/workflows/tag-build.yml b/.github/workflows/tag-build.yml
index f367c2fc9..042630191 100644
--- a/.github/workflows/tag-build.yml
+++ b/.github/workflows/tag-build.yml
@@ -1,5 +1,10 @@
name: Tag Build
-on: [push]
+on:
+ push:
+ branches:
+ - master
+ tags-ignore:
+ - '*' # don't needlessly execute on tags
jobs:
tag:
diff --git a/Dalamud.CorePlugin/GlobalSuppressions.cs b/Dalamud.CorePlugin/GlobalSuppressions.cs
index bfaba20d0..771e6f182 100644
--- a/Dalamud.CorePlugin/GlobalSuppressions.cs
+++ b/Dalamud.CorePlugin/GlobalSuppressions.cs
@@ -6,12 +6,12 @@
using System.Diagnostics.CodeAnalysis;
// General
-[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:Parameter should not span multiple lines", Justification = "Preventing long lines", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
-[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1124:Do not use regions", Justification = "I like regions", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
-[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1123:Do not place regions within elements", Justification = "I like regions in elements too", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
-[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "This is annoying", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
-[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1512:Single-line comments should not be followed by blank line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
-[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1515:Single-line comment should be preceded by blank line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
-[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1127:Generic type constraints should be on their own line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
-[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
+[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:Parameter should not span multiple lines", Justification = "Preventing long lines")]
+[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1124:Do not use regions", Justification = "I like regions")]
+[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1123:Do not place regions within elements", Justification = "I like regions in elements too")]
+[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "This is annoying")]
+[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1512:Single-line comments should not be followed by blank line", Justification = "I like this better")]
+[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1515:Single-line comment should be preceded by blank line", Justification = "I like this better")]
+[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1127:Generic type constraints should be on their own line", Justification = "I like this better")]
+[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "I like this better")]
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File should have header", Justification = "We don't do those yet")]
diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs
index d352ad2c8..9026ea0dd 100644
--- a/Dalamud.CorePlugin/PluginImpl.cs
+++ b/Dalamud.CorePlugin/PluginImpl.cs
@@ -54,6 +54,7 @@ namespace Dalamud.CorePlugin
/// Initializes a new instance of the class.
///
/// Dalamud plugin interface.
+ /// Logging service.
public PluginImpl(DalamudPluginInterface pluginInterface, PluginLog log)
{
try
diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs
index c29fada83..c4c553a47 100644
--- a/Dalamud.Injector/EntryPoint.cs
+++ b/Dalamud.Injector/EntryPoint.cs
@@ -200,15 +200,21 @@ namespace Dalamud.Injector
var logFile = new FileInfo(logPath);
if (!logFile.Exists)
+ {
logFile.Create();
+ }
if (logFile.Length <= cullingFileSize)
+ {
return;
+ }
var amountToCull = logFile.Length - cullingFileSize;
if (amountToCull < bufferSize)
+ {
return;
+ }
using var reader = new BinaryReader(logFile.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite));
using var writer = new BinaryWriter(logFile.Open(FileMode.Open, FileAccess.Write, FileShare.ReadWrite));
@@ -247,7 +253,6 @@ namespace Dalamud.Injector
var workingDirectory = startInfo.WorkingDirectory;
var configurationPath = startInfo.ConfigurationPath;
var pluginDirectory = startInfo.PluginDirectory;
- var defaultPluginDirectory = startInfo.DefaultPluginDirectory;
var assetDirectory = startInfo.AssetDirectory;
var delayInitializeMs = startInfo.DelayInitializeMs;
var logName = startInfo.LogName;
@@ -257,25 +262,41 @@ namespace Dalamud.Injector
for (var i = 2; i < args.Count; i++)
{
if (args[i].StartsWith(key = "--dalamud-working-directory="))
+ {
workingDirectory = args[i][key.Length..];
+ }
else if (args[i].StartsWith(key = "--dalamud-configuration-path="))
+ {
configurationPath = args[i][key.Length..];
+ }
else if (args[i].StartsWith(key = "--dalamud-plugin-directory="))
+ {
pluginDirectory = args[i][key.Length..];
- else if (args[i].StartsWith(key = "--dalamud-dev-plugin-directory="))
- defaultPluginDirectory = args[i][key.Length..];
+ }
else if (args[i].StartsWith(key = "--dalamud-asset-directory="))
+ {
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 if (args[i].StartsWith(key = "--dalamud-tspack-b64="))
+ {
troubleshootingData = Encoding.UTF8.GetString(Convert.FromBase64String(args[i][key.Length..]));
+ }
else if (args[i].StartsWith(key = "--logname="))
+ {
logName = args[i][key.Length..];
+ }
else
+ {
continue;
+ }
args.RemoveAt(i);
i--;
@@ -287,33 +308,49 @@ namespace Dalamud.Injector
workingDirectory ??= Directory.GetCurrentDirectory();
configurationPath ??= Path.Combine(xivlauncherDir, "dalamudConfig.json");
pluginDirectory ??= Path.Combine(xivlauncherDir, "installedPlugins");
- 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 = "deutsch").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.");
+ }
startInfo.WorkingDirectory = workingDirectory;
startInfo.ConfigurationPath = configurationPath;
startInfo.PluginDirectory = pluginDirectory;
- startInfo.DefaultPluginDirectory = defaultPluginDirectory;
startInfo.AssetDirectory = assetDirectory;
startInfo.Language = clientLanguage;
startInfo.DelayInitializeMs = delayInitializeMs;
@@ -350,10 +387,14 @@ namespace Dalamud.Injector
exeSpaces += " ";
if (particularCommand is null or "help")
+ {
Console.WriteLine("{0} help [command]", exeName);
+ }
if (particularCommand is null or "inject")
+ {
Console.WriteLine("{0} inject [-h/--help] [-a/--all] [--warn] [--fix-acl] [--se-debug-privilege] [pid1] [pid2] [pid3] ...", exeName);
+ }
if (particularCommand is null or "launch")
{
@@ -367,7 +408,7 @@ namespace Dalamud.Injector
}
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-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)]");
@@ -431,7 +472,7 @@ namespace Dalamud.Injector
}
else
{
- throw new CommandLineException($"\"{args[i]}\" is not a command line argument.");
+ Log.Warning($"\"{args[i]}\" is not a valid command line argument, ignoring.");
}
}
@@ -505,29 +546,53 @@ 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] == "--no-wait")
+ {
waitForGameWindow = false;
+ }
else if (args[i] == "--no-fix-acl" || args[i] == "--no-acl-fix")
+ {
noFixAcl = 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="))
+ {
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] == "--")
+ {
parsingGameArgument = true;
+ }
else
- throw new CommandLineException($"\"{args[i]}\" is not a command line argument.");
+ {
+ Log.Warning($"\"{args[i]}\" is not a valid command line argument, ignoring.");
+ }
}
var checksumTable = "fX1pGtdS5CAP4_VL";
@@ -536,11 +601,15 @@ namespace Dalamud.Injector
gameArguments = gameArguments.SelectMany(x =>
{
if (!x.StartsWith("//**sqex0003") || !x.EndsWith("**//"))
+ {
return new List() { x };
+ }
var checksum = checksumTable.IndexOf(x[x.Length - 5]);
if (checksum == -1)
+ {
return new List() { x };
+ }
var encData = Convert.FromBase64String(x.Substring(12, x.Length - 12 - 5).Replace('-', '+').Replace('_', '/').Replace('*', '='));
var rawData = new byte[encData.Length];
@@ -554,13 +623,25 @@ namespace Dalamud.Injector
encryptArguments = true;
var args = argDelimiterRegex.Split(rawString).Skip(1).Select(y => string.Join('=', kvDelimiterRegex.Split(y, 2)).Replace(" ", " ")).ToList();
if (!args.Any())
+ {
continue;
+ }
+
if (!args.First().StartsWith("T="))
+ {
continue;
+ }
+
if (!uint.TryParse(args.First().Substring(2), out var tickCount))
+ {
continue;
+ }
+
if (tickCount >> 16 != i)
+ {
continue;
+ }
+
return args.Skip(1);
}
@@ -658,10 +739,12 @@ namespace Dalamud.Injector
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
[System.Runtime.InteropServices.DllImport("c")]
- static extern ulong clock_gettime_nsec_np(int clock_id);
+#pragma warning disable SA1300
+ static extern ulong clock_gettime_nsec_np(int clockId);
+#pragma warning restore SA1300
const int CLOCK_MONOTONIC_RAW = 4;
- var rawTickCountFixed = (clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) / 1000000);
+ var rawTickCountFixed = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) / 1000000;
Log.Information("ArgumentBuilder::DeriveKey() fixing up rawTickCount from {0} to {1} on macOS", rawTickCount, rawTickCountFixed);
rawTickCount = (uint)rawTickCountFixed;
}
@@ -683,21 +766,27 @@ namespace Dalamud.Injector
gameArgumentString = string.Join(" ", gameArguments.Select(x => EncodeParameterArgument(x)));
}
- var process = GameStart.LaunchGame(Path.GetDirectoryName(gamePath), gamePath, gameArgumentString, noFixAcl, (Process p) =>
- {
- if (!withoutDalamud && mode == "entrypoint")
+ var process = GameStart.LaunchGame(
+ Path.GetDirectoryName(gamePath),
+ gamePath,
+ gameArgumentString,
+ noFixAcl,
+ p =>
{
- var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath);
- Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo));
- if (RewriteRemoteEntryPointW(p.Handle, gamePath, JsonConvert.SerializeObject(startInfo)) != 0)
+ if (!withoutDalamud && mode == "entrypoint")
{
- Log.Error("[HOOKS] RewriteRemoteEntryPointW failed");
- throw new Exception("RewriteRemoteEntryPointW failed");
- }
+ var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath);
+ Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo));
+ if (RewriteRemoteEntryPointW(p.Handle, gamePath, JsonConvert.SerializeObject(startInfo)) != 0)
+ {
+ Log.Error("[HOOKS] RewriteRemoteEntryPointW failed");
+ throw new Exception("RewriteRemoteEntryPointW failed");
+ }
- Log.Verbose("RewriteRemoteEntryPointW called!");
- }
- }, waitForGameWindow);
+ Log.Verbose("RewriteRemoteEntryPointW called!");
+ }
+ },
+ waitForGameWindow);
Log.Verbose("Game process started with PID {0}", process.Id);
@@ -712,7 +801,9 @@ namespace Dalamud.Injector
if (handleOwner != IntPtr.Zero)
{
if (!DuplicateHandle(Process.GetCurrentProcess().Handle, process.Handle, handleOwner, out processHandleForOwner, 0, false, DuplicateOptions.SameAccess))
+ {
Log.Warning("Failed to call DuplicateHandle: Win32 error code {0}", Marshal.GetLastWin32Error());
+ }
}
Console.WriteLine($"{{\"pid\": {process.Id}, \"handle\": {processHandleForOwner}}}");
@@ -755,7 +846,9 @@ namespace Dalamud.Injector
helperProcess.BeginErrorReadLine();
helperProcess.WaitForExit();
if (helperProcess.ExitCode != 0)
+ {
return -1;
+ }
var result = JsonSerializer.CreateDefault().Deserialize>(new JsonTextReader(helperProcess.StandardOutput));
var pid = result["pid"];
@@ -812,7 +905,9 @@ namespace Dalamud.Injector
var startInfoAddress = startInfoBuffer.Add(startInfoBytes);
if (startInfoAddress == 0)
+ {
throw new Exception("Unable to allocate start info JSON");
+ }
injector.GetFunctionAddress(bootModule, "Initialize", out var initAddress);
injector.CallRemoteFunction(initAddress, startInfoAddress, out var exitCode);
@@ -847,7 +942,10 @@ namespace Dalamud.Injector
///
private static string EncodeParameterArgument(string argument, bool force = false)
{
- if (argument == null) throw new ArgumentNullException(nameof(argument));
+ if (argument == null)
+ {
+ throw new ArgumentNullException(nameof(argument));
+ }
// Unless we're told otherwise, don't quote unless we actually
// need to do so --- hopefully avoid problems if programs won't
diff --git a/Dalamud.Injector/GameStart.cs b/Dalamud.Injector/GameStart.cs
index 95e963a9a..e34048978 100644
--- a/Dalamud.Injector/GameStart.cs
+++ b/Dalamud.Injector/GameStart.cs
@@ -211,6 +211,9 @@ namespace Dalamud.Injector
}
}
+ ///
+ /// Claim a SE Debug Privilege.
+ ///
public static void ClaimSeDebug()
{
var hToken = PInvoke.INVALID_HANDLE_VALUE;
@@ -345,8 +348,6 @@ namespace Dalamud.Injector
private static class PInvoke
{
#region Constants
- public static readonly IntPtr INVALID_HANDLE_VALUE = new(-1);
-
public const string SE_DEBUG_NAME = "SeDebugPrivilege";
public const UInt32 STANDARD_RIGHTS_ALL = 0x001F0000;
@@ -369,6 +370,8 @@ namespace Dalamud.Injector
public const UInt32 ERROR_NO_TOKEN = 0x000003F0;
+ public static readonly IntPtr INVALID_HANDLE_VALUE = new(-1);
+
public enum MULTIPLE_TRUSTEE_OPERATION
{
NO_MULTIPLE_TRUSTEE,
@@ -431,7 +434,7 @@ namespace Dalamud.Injector
SecurityAnonymous,
SecurityIdentification,
SecurityImpersonation,
- SecurityDelegation
+ SecurityDelegation,
}
#endregion
@@ -485,8 +488,7 @@ namespace Dalamud.Injector
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool ImpersonateSelf(
- SECURITY_IMPERSONATION_LEVEL impersonationLevel
- );
+ SECURITY_IMPERSONATION_LEVEL impersonationLevel);
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool OpenProcessToken(
@@ -496,10 +498,10 @@ namespace Dalamud.Injector
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool OpenThreadToken(
- IntPtr ThreadHandle,
- uint DesiredAccess,
- bool OpenAsSelf,
- out IntPtr TokenHandle);
+ IntPtr threadHandle,
+ uint desiredAccess,
+ bool openAsSelf,
+ out IntPtr tokenHandle);
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool LookupPrivilegeValue(string lpSystemName, string lpName, ref LUID lpLuid);
diff --git a/Dalamud.Injector/GlobalSuppressions.cs b/Dalamud.Injector/GlobalSuppressions.cs
index e1978fae4..57d7f2d28 100644
--- a/Dalamud.Injector/GlobalSuppressions.cs
+++ b/Dalamud.Injector/GlobalSuppressions.cs
@@ -7,7 +7,7 @@ using System.Diagnostics.CodeAnalysis;
// General
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File should have header", Justification = "We don't do those yet")]
-[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "This is annoying", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
-[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1512:Single-line comments should not be followed by blank line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
-[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1515:Single-line comment should be preceded by blank line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
-[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "I'll make what I want static", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
+[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "This is annoying")]
+[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1512:Single-line comments should not be followed by blank line", Justification = "I like this better")]
+[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1515:Single-line comment should be preceded by blank line", Justification = "I like this better")]
+[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "I'll make what I want static")]
diff --git a/Dalamud.Injector/LegacyBlowfish.cs b/Dalamud.Injector/LegacyBlowfish.cs
index 28cc584e4..99c514954 100644
--- a/Dalamud.Injector/LegacyBlowfish.cs
+++ b/Dalamud.Injector/LegacyBlowfish.cs
@@ -1,8 +1,16 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
namespace Dalamud.Injector
{
+ [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1124:Do not use regions", Justification = "Legacy code")]
+ [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1413:Use trailing comma in multi-line initializers", Justification = "Legacy code")]
+ [SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1519:Braces should not be omitted from multi-line child statement", Justification = "Legacy code")]
+ [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1414:Tuple types in signatures should have element names", Justification = "Legacy code")]
+ [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "Legacy code")]
+ [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Legacy code")]
+ [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1204:Static elements should appear before instance elements", Justification = "Legacy code")]
internal class LegacyBlowfish
{
#region P-Array and S-Boxes
@@ -203,10 +211,9 @@ namespace Dalamud.Injector
private static readonly int Rounds = 16;
///
- /// Initialize a new blowfish.
+ /// Initializes a new instance of the class.
///
/// The key to use.
- /// Whether or not a sign confusion should be introduced during key init. This is needed for SE's implementation of blowfish.
public LegacyBlowfish(byte[] key)
{
foreach (var (i, keyFragment) in WrappingUInt32(key, this.p.Length))
@@ -306,7 +313,9 @@ namespace Dalamud.Injector
for (var j = 0; j < 4 && enumerator.MoveNext(); j++)
{
+#pragma warning disable CS0675
n = (uint)((n << 8) | (sbyte)enumerator.Current); // NOTE(goat): THIS IS A BUG! SE's implementation wrongly uses signed numbers for this, so we need to as well.
+#pragma warning restore CS0675
}
yield return (i, n);
diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
index c2ac74a3a..cb33e7070 100644
--- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs
+++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
@@ -6,6 +6,7 @@ using System.Linq;
using Dalamud.Game.Text;
using Dalamud.Interface.Style;
+using Dalamud.Plugin.Internal.Profiles;
using Dalamud.Utility;
using Newtonsoft.Json;
using Serilog;
@@ -271,6 +272,21 @@ internal sealed class DalamudConfiguration : IServiceType
///
public string ChosenStyle { get; set; } = "Dalamud Standard";
+ ///
+ /// Gets or sets a list of saved plugin profiles.
+ ///
+ public List? SavedProfiles { get; set; }
+
+ ///
+ /// Gets or sets the default plugin profile.
+ ///
+ public ProfileModel? DefaultProfile { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether or not profiles are enabled.
+ ///
+ public bool ProfilesEnabled { get; set; } = false;
+
///
/// Gets or sets a value indicating whether or not Dalamud RMT filtering should be disabled.
///
diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs
index 142f653ef..d6cf6a107 100644
--- a/Dalamud/Dalamud.cs
+++ b/Dalamud/Dalamud.cs
@@ -1,7 +1,9 @@
using System;
+using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
@@ -98,6 +100,19 @@ internal sealed class Dalamud : IServiceType
/// Gets location of stored assets.
///
internal DirectoryInfo AssetDirectory => new(Service.Get().AssetDirectory!);
+
+ ///
+ /// Signal to the crash handler process that we should restart the game.
+ ///
+ public static void RestartGame()
+ {
+ [DllImport("kernel32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ static extern void RaiseException(uint dwExceptionCode, uint dwExceptionFlags, uint nNumberOfArguments, IntPtr lpArguments);
+
+ RaiseException(0x12345678, 0, 0, IntPtr.Zero);
+ Process.GetCurrentProcess().Kill();
+ }
///
/// Queue an unload of Dalamud when it gets the chance.
diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj
index 61730b5ca..c52065616 100644
--- a/Dalamud/Dalamud.csproj
+++ b/Dalamud/Dalamud.csproj
@@ -8,7 +8,7 @@
- 7.5.1.0
+ 7.6.5.0
XIV Launcher addon framework
$(DalamudVersion)
$(DalamudVersion)
diff --git a/Dalamud/DalamudStartInfo.cs b/Dalamud/DalamudStartInfo.cs
index 658934005..4c8e7566d 100644
--- a/Dalamud/DalamudStartInfo.cs
+++ b/Dalamud/DalamudStartInfo.cs
@@ -30,7 +30,6 @@ public record DalamudStartInfo : IServiceType
this.ConfigurationPath = other.ConfigurationPath;
this.LogName = other.LogName;
this.PluginDirectory = other.PluginDirectory;
- this.DefaultPluginDirectory = other.DefaultPluginDirectory;
this.AssetDirectory = other.AssetDirectory;
this.Language = other.Language;
this.GameVersion = other.GameVersion;
@@ -72,11 +71,6 @@ public record DalamudStartInfo : IServiceType
///
public string? PluginDirectory { get; set; }
- ///
- /// Gets or sets the path to the directory for developer plugins.
- ///
- public string? DefaultPluginDirectory { get; set; }
-
///
/// Gets or sets the path to core Dalamud assets.
///
diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs
index 1bd77a8e2..33e09e221 100644
--- a/Dalamud/EntryPoint.cs
+++ b/Dalamud/EntryPoint.cs
@@ -68,7 +68,7 @@ public sealed class EntryPoint
{
try
{
- return Marshal.StringToHGlobalUni(Environment.StackTrace);
+ return Marshal.StringToHGlobalUni(new StackTrace(1).ToString());
}
catch (Exception e)
{
diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs
index 4827c0d4f..7cd0869aa 100644
--- a/Dalamud/Game/ChatHandlers.cs
+++ b/Dalamud/Game/ChatHandlers.cs
@@ -248,7 +248,8 @@ public class ChatHandlers : IServiceType
var assemblyVersion = Assembly.GetAssembly(typeof(ChatHandlers)).GetName().Version.ToString();
- if (this.configuration.PrintDalamudWelcomeMsg) {
+ if (this.configuration.PrintDalamudWelcomeMsg)
+ {
chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud vD{0} loaded."), assemblyVersion)
+ string.Format(Loc.Localize("PluginsWelcome", " {0} plugin(s) loaded."), pluginManager.InstalledPlugins.Count(x => x.IsLoaded)));
}
diff --git a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs
index 9f8a62faf..bbd94e505 100644
--- a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs
+++ b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
+using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using Serilog;
@@ -14,7 +15,8 @@ namespace Dalamud.Game.ClientState.Aetherytes;
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
-public sealed unsafe partial class AetheryteList : IServiceType
+[ResolveVia]
+public sealed unsafe partial class AetheryteList : IServiceType, IAetheryteList
{
[ServiceManager.ServiceDependency]
private readonly ClientState clientState = Service.Get();
@@ -27,9 +29,7 @@ public sealed unsafe partial class AetheryteList : IServiceType
Log.Verbose($"Teleport address 0x{((nint)this.telepoInstance).ToInt64():X}");
}
- ///
- /// Gets the amount of Aetherytes the local player has unlocked.
- ///
+ ///
public int Length
{
get
@@ -46,11 +46,7 @@ public sealed unsafe partial class AetheryteList : IServiceType
}
}
- ///
- /// Gets a Aetheryte Entry at the specified index.
- ///
- /// Index.
- /// A at the specified index.
+ ///
public AetheryteEntry? this[int index]
{
get
@@ -80,7 +76,7 @@ public sealed unsafe partial class AetheryteList : IServiceType
///
/// This collection represents the list of available Aetherytes in the Teleport window.
///
-public sealed partial class AetheryteList : IReadOnlyCollection
+public sealed partial class AetheryteList
{
///
public int Count => this.Length;
diff --git a/Dalamud/Game/ClientState/Objects/Enums/BattleNpcSubKind.cs b/Dalamud/Game/ClientState/Objects/Enums/BattleNpcSubKind.cs
index f9019ed77..9c10a84ab 100644
--- a/Dalamud/Game/ClientState/Objects/Enums/BattleNpcSubKind.cs
+++ b/Dalamud/Game/ClientState/Objects/Enums/BattleNpcSubKind.cs
@@ -10,9 +10,9 @@ public enum BattleNpcSubKind : byte
///
None = 0,
- ///
+ ///
/// Weak Spots / Battle NPC parts
- /// Eg: Titan's Heart (Naval), Tioman's left and right wing (Sohm Al), Golem Soulstone (The Sunken Temple of Qarn)
+ /// Eg: Titan's Heart (Naval), Tioman's left and right wing (Sohm Al), Golem Soulstone (The Sunken Temple of Qarn).
///
BattleNpcPart = 1,
diff --git a/Dalamud/Game/ClientState/Objects/Types/BattleChara.cs b/Dalamud/Game/ClientState/Objects/Types/BattleChara.cs
index 651f97834..63a5b828a 100644
--- a/Dalamud/Game/ClientState/Objects/Types/BattleChara.cs
+++ b/Dalamud/Game/ClientState/Objects/Types/BattleChara.cs
@@ -22,42 +22,42 @@ public unsafe class BattleChara : Character
///
/// Gets the current status effects.
///
- public StatusList StatusList => new(&this.Struct->StatusManager);
+ public StatusList StatusList => new(this.Struct->GetStatusManager);
///
/// Gets a value indicating whether the chara is currently casting.
///
- public bool IsCasting => this.Struct->SpellCastInfo.IsCasting > 0;
+ public bool IsCasting => this.Struct->GetCastInfo->IsCasting > 0;
///
/// Gets a value indicating whether the cast is interruptible.
///
- public bool IsCastInterruptible => this.Struct->SpellCastInfo.Interruptible > 0;
+ public bool IsCastInterruptible => this.Struct->GetCastInfo->Interruptible > 0;
///
/// Gets the spell action type of the spell being cast by the actor.
///
- public byte CastActionType => (byte)this.Struct->SpellCastInfo.ActionType;
+ public byte CastActionType => (byte)this.Struct->GetCastInfo->ActionType;
///
/// Gets the spell action ID of the spell being cast by the actor.
///
- public uint CastActionId => this.Struct->SpellCastInfo.ActionID;
+ public uint CastActionId => this.Struct->GetCastInfo->ActionID;
///
/// Gets the object ID of the target currently being cast at by the chara.
///
- public uint CastTargetObjectId => this.Struct->SpellCastInfo.CastTargetID;
+ public uint CastTargetObjectId => this.Struct->GetCastInfo->CastTargetID;
///
/// Gets the current casting time of the spell being cast by the chara.
///
- public float CurrentCastTime => this.Struct->SpellCastInfo.CurrentCastTime;
+ public float CurrentCastTime => this.Struct->GetCastInfo->CurrentCastTime;
///
/// Gets the total casting time of the spell being cast by the chara.
///
- public float TotalCastTime => this.Struct->SpellCastInfo.TotalCastTime;
+ public float TotalCastTime => this.Struct->GetCastInfo->TotalCastTime;
///
/// Gets the underlying structure.
diff --git a/Dalamud/Game/ClientState/Objects/Types/Character.cs b/Dalamud/Game/ClientState/Objects/Types/Character.cs
index afc94bd0f..cdd1515b0 100644
--- a/Dalamud/Game/ClientState/Objects/Types/Character.cs
+++ b/Dalamud/Game/ClientState/Objects/Types/Character.cs
@@ -77,7 +77,7 @@ public unsafe class Character : GameObject
/// Gets a byte array describing the visual appearance of this Chara.
/// Indexed by .
///
- public byte[] Customize => MemoryHelper.Read((IntPtr)this.Struct->CustomizeData, 28);
+ public byte[] Customize => MemoryHelper.Read((IntPtr)this.Struct->DrawData.CustomizeData.Data, 28);
///
/// Gets the Free Company tag of this chara.
diff --git a/Dalamud/Game/Config/GameConfigSection.cs b/Dalamud/Game/Config/GameConfigSection.cs
index 89df468f0..107b0d4a8 100644
--- a/Dalamud/Game/Config/GameConfigSection.cs
+++ b/Dalamud/Game/Config/GameConfigSection.cs
@@ -67,7 +67,8 @@ public class GameConfigSection
/// Name of the config option.
/// The returned value of the config option.
/// A value representing the success.
- public unsafe bool TryGetBool(string name, out bool value) {
+ public unsafe bool TryGetBool(string name, out bool value)
+ {
value = false;
if (!this.TryGetIndex(name, out var index))
{
@@ -97,7 +98,8 @@ public class GameConfigSection
/// Name of the config option.
/// Value of the config option.
/// Thrown if the config option is not found.
- public bool GetBool(string name) {
+ public bool GetBool(string name)
+ {
if (!this.TryGetBool(name, out var value))
{
throw new ConfigOptionNotFoundException(this.SectionName, name);
@@ -114,7 +116,8 @@ public class GameConfigSection
/// New value of the config option.
/// Throw if the config option is not found.
/// Thrown if the name of the config option is found, but the struct was not.
- public unsafe void Set(string name, bool value) {
+ public unsafe void Set(string name, bool value)
+ {
if (!this.TryGetIndex(name, out var index))
{
throw new ConfigOptionNotFoundException(this.SectionName, name);
@@ -139,7 +142,8 @@ public class GameConfigSection
/// Name of the config option.
/// The returned value of the config option.
/// A value representing the success.
- public unsafe bool TryGetUInt(string name, out uint value) {
+ public unsafe bool TryGetUInt(string name, out uint value)
+ {
value = 0;
if (!this.TryGetIndex(name, out var index))
{
@@ -169,7 +173,8 @@ public class GameConfigSection
/// Name of the config option.
/// Value of the config option.
/// Thrown if the config option is not found.
- public uint GetUInt(string name) {
+ public uint GetUInt(string name)
+ {
if (!this.TryGetUInt(name, out var value))
{
throw new ConfigOptionNotFoundException(this.SectionName, name);
@@ -186,8 +191,10 @@ public class GameConfigSection
/// New value of the config option.
/// Throw if the config option is not found.
/// Thrown if the name of the config option is found, but the struct was not.
- public unsafe void Set(string name, uint value) {
- this.framework.RunOnFrameworkThread(() => {
+ public unsafe void Set(string name, uint value)
+ {
+ this.framework.RunOnFrameworkThread(() =>
+ {
if (!this.TryGetIndex(name, out var index))
{
throw new ConfigOptionNotFoundException(this.SectionName, name);
@@ -213,7 +220,8 @@ public class GameConfigSection
/// Name of the config option.
/// The returned value of the config option.
/// A value representing the success.
- public unsafe bool TryGetFloat(string name, out float value) {
+ public unsafe bool TryGetFloat(string name, out float value)
+ {
value = 0;
if (!this.TryGetIndex(name, out var index))
{
@@ -243,7 +251,8 @@ public class GameConfigSection
/// Name of the config option.
/// Value of the config option.
/// Thrown if the config option is not found.
- public float GetFloat(string name) {
+ public float GetFloat(string name)
+ {
if (!this.TryGetFloat(name, out var value))
{
throw new ConfigOptionNotFoundException(this.SectionName, name);
@@ -260,8 +269,10 @@ public class GameConfigSection
/// New value of the config option.
/// Throw if the config option is not found.
/// Thrown if the name of the config option is found, but the struct was not.
- public unsafe void Set(string name, float value) {
- this.framework.RunOnFrameworkThread(() => {
+ public unsafe void Set(string name, float value)
+ {
+ this.framework.RunOnFrameworkThread(() =>
+ {
if (!this.TryGetIndex(name, out var index))
{
throw new ConfigOptionNotFoundException(this.SectionName, name);
@@ -287,7 +298,8 @@ public class GameConfigSection
/// Name of the config option.
/// The returned value of the config option.
/// A value representing the success.
- public unsafe bool TryGetString(string name, out string value) {
+ public unsafe bool TryGetString(string name, out string value)
+ {
value = string.Empty;
if (!this.TryGetIndex(name, out var index))
{
@@ -327,7 +339,8 @@ public class GameConfigSection
/// Name of the config option.
/// Value of the config option.
/// Thrown if the config option is not found.
- public string GetString(string name) {
+ public string GetString(string name)
+ {
if (!this.TryGetString(name, out var value))
{
throw new ConfigOptionNotFoundException(this.SectionName, name);
@@ -344,8 +357,10 @@ public class GameConfigSection
/// New value of the config option.
/// Throw if the config option is not found.
/// Thrown if the name of the config option is found, but the struct was not.
- public unsafe void Set(string name, string value) {
- this.framework.RunOnFrameworkThread(() => {
+ public unsafe void Set(string name, string value)
+ {
+ this.framework.RunOnFrameworkThread(() =>
+ {
if (!this.TryGetIndex(name, out var index))
{
throw new ConfigOptionNotFoundException(this.SectionName, name);
@@ -365,7 +380,8 @@ public class GameConfigSection
});
}
- private unsafe bool TryGetIndex(string name, out uint index) {
+ private unsafe bool TryGetIndex(string name, out uint index)
+ {
if (this.indexMap.TryGetValue(name, out index))
{
return true;
@@ -373,14 +389,16 @@ public class GameConfigSection
var configBase = this.GetConfigBase();
var e = configBase->ConfigEntry;
- for (var i = 0U; i < configBase->ConfigCount; i++, e++) {
+ for (var i = 0U; i < configBase->ConfigCount; i++, e++)
+ {
if (e->Name == null)
{
continue;
}
var eName = MemoryHelper.ReadStringNullTerminated(new IntPtr(e->Name));
- if (eName.Equals(name)) {
+ if (eName.Equals(name))
+ {
this.indexMap.TryAdd(name, i);
this.nameMap.TryAdd(i, name);
index = i;
@@ -392,7 +410,8 @@ public class GameConfigSection
return false;
}
- private unsafe bool TryGetEntry(uint index, out ConfigEntry* entry) {
+ private unsafe bool TryGetEntry(uint index, out ConfigEntry* entry)
+ {
entry = null;
var configBase = this.GetConfigBase();
if (configBase->ConfigEntry == null || index >= configBase->ConfigCount)
diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs
index 21bd9e646..b3083e913 100644
--- a/Dalamud/Game/Framework.cs
+++ b/Dalamud/Game/Framework.cs
@@ -25,25 +25,25 @@ namespace Dalamud.Game;
[ServiceManager.BlockingEarlyLoadedService]
public sealed class Framework : IDisposable, IServiceType
{
+ private static readonly Stopwatch StatsStopwatch = new();
+
private readonly GameLifecycle lifecycle;
- private static Stopwatch statsStopwatch = new();
-
private readonly Stopwatch updateStopwatch = new();
private readonly HitchDetector hitchDetector;
private readonly Hook updateHook;
private readonly Hook destroyHook;
+ [ServiceManager.ServiceDependency]
+ private readonly DalamudConfiguration configuration = Service.Get();
+
private readonly object runOnNextTickTaskListSync = new();
private List runOnNextTickTaskList = new();
private List runOnNextTickTaskList2 = new();
private Thread? frameworkUpdateThread;
- [ServiceManager.ServiceDependency]
- private readonly DalamudConfiguration configuration = Service.Get();
-
[ServiceManager.ServiceConstructor]
private Framework(SigScanner sigScanner, GameLifecycle lifecycle)
{
@@ -346,7 +346,7 @@ public sealed class Framework : IDisposable, IServiceType
this.destroyHook.Dispose();
this.updateStopwatch.Reset();
- statsStopwatch.Reset();
+ StatsStopwatch.Reset();
}
[ServiceManager.CallWhenServicesReady]
@@ -420,11 +420,11 @@ public sealed class Framework : IDisposable, IServiceType
if (StatsEnabled)
{
- statsStopwatch.Restart();
+ StatsStopwatch.Restart();
this.RunPendingTickTasks();
- statsStopwatch.Stop();
+ StatsStopwatch.Stop();
- AddToStats(nameof(this.RunPendingTickTasks), statsStopwatch.Elapsed.TotalMilliseconds);
+ AddToStats(nameof(this.RunPendingTickTasks), StatsStopwatch.Elapsed.TotalMilliseconds);
}
else
{
@@ -440,7 +440,7 @@ public sealed class Framework : IDisposable, IServiceType
// Individually invoke OnUpdate handlers and time them.
foreach (var d in invokeList)
{
- statsStopwatch.Restart();
+ StatsStopwatch.Restart();
try
{
d.Method.Invoke(d.Target, new object[] { this });
@@ -450,13 +450,13 @@ public sealed class Framework : IDisposable, IServiceType
Log.Error(ex, "Exception while dispatching Framework::Update event.");
}
- statsStopwatch.Stop();
+ StatsStopwatch.Stop();
var key = $"{d.Target}::{d.Method.Name}";
if (notUpdated.Contains(key))
notUpdated.Remove(key);
- AddToStats(key, statsStopwatch.Elapsed.TotalMilliseconds);
+ AddToStats(key, StatsStopwatch.Elapsed.TotalMilliseconds);
}
// Cleanup handlers that are no longer being called
diff --git a/Dalamud/Game/Network/GameNetwork.cs b/Dalamud/Game/Network/GameNetwork.cs
index 767c9bc9a..d1fc0bfba 100644
--- a/Dalamud/Game/Network/GameNetwork.cs
+++ b/Dalamud/Game/Network/GameNetwork.cs
@@ -1,13 +1,12 @@
using System;
-using System.Collections.Generic;
using System.Runtime.InteropServices;
+
using Dalamud.Configuration.Internal;
using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Utility;
using Serilog;
-using Serilog.Core;
namespace Dalamud.Game.Network;
@@ -26,11 +25,11 @@ public sealed class GameNetwork : IDisposable, IServiceType
private readonly HitchDetector hitchDetectorUp;
private readonly HitchDetector hitchDetectorDown;
- private IntPtr baseAddress;
-
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service.Get();
-
+
+ private IntPtr baseAddress;
+
[ServiceManager.ServiceConstructor]
private GameNetwork(SigScanner sigScanner)
{
diff --git a/Dalamud/Game/Text/SeStringHandling/BitmapFontIcon.cs b/Dalamud/Game/Text/SeStringHandling/BitmapFontIcon.cs
index 64af2a2a9..ea181238a 100644
--- a/Dalamud/Game/Text/SeStringHandling/BitmapFontIcon.cs
+++ b/Dalamud/Game/Text/SeStringHandling/BitmapFontIcon.cs
@@ -459,4 +459,259 @@ public enum BitmapFontIcon : uint
/// The Island Sanctuary icon.
///
IslandSanctuary = 116,
+
+ ///
+ /// The Physical Damage icon.
+ ///
+ DamagePhysical = 117,
+
+ ///
+ /// The Magical Damage icon.
+ ///
+ DamageMagical = 118,
+
+ ///
+ /// The Special Damage icon.
+ ///
+ DamageSpecial = 119,
+
+ ///
+ /// A gold star icon with an orange exclamation mark.
+ ///
+ GoldStarProblem = 120,
+
+ ///
+ /// A blue star icon.
+ ///
+ BlueStar = 121,
+
+ ///
+ /// A blue star icon with an orange exclamation mark.
+ ///
+ BlueStarProblem = 121,
+
+ ///
+ /// The Disconnecting icon.
+ ///
+ Disconnecting = 124,
+
+ ///
+ /// The Do Not Disturb icon.
+ ///
+ DoNotDisturb = 125,
+
+ ///
+ /// The Meteor icon.
+ ///
+ Meteor = 126,
+
+ ///
+ /// The Role Playing icon.
+ ///
+ RolePlaying = 127,
+
+ ///
+ /// The Gladiator icon.
+ ///
+ Gladiator = 128,
+
+ ///
+ /// The Pugilist icon.
+ ///
+ Pugilist = 129,
+
+ ///
+ /// The Marauder icon.
+ ///
+ Marauder = 130,
+
+ ///
+ /// The Lancer icon.
+ ///
+ Lancer = 131,
+
+ ///
+ /// The Archer icon.
+ ///
+ Archer = 132,
+
+ ///
+ /// The Conjurer icon.
+ ///
+ Conjurer = 133,
+
+ ///
+ /// The Thaumaturge icon.
+ ///
+ Thaumaturge = 134,
+
+ ///
+ /// The Carpenter icon.
+ ///
+ Carpenter = 135,
+
+ ///
+ /// The Blacksmith icon.
+ ///
+ Blacksmith = 136,
+
+ ///
+ /// The Armorer icon.
+ ///
+ Armorer = 137,
+
+ ///
+ /// The Goldsmith icon.
+ ///
+ Goldsmith = 138,
+
+ ///
+ /// The Leatherworker icon.
+ ///
+ Leatherworker = 139,
+
+ ///
+ /// The Weaver icon.
+ ///
+ Weaver = 140,
+
+ ///
+ /// The Alchemist icon.
+ ///
+ Alchemist = 131,
+
+ ///
+ /// The Culinarian icon.
+ ///
+ Culinarian = 132,
+
+ ///
+ /// The Miner icon.
+ ///
+ Miner = 143,
+
+ ///
+ /// The Botanist icon.
+ ///
+ Botanist = 144,
+
+ ///
+ /// The Fisher icon.
+ ///
+ Fisher = 145,
+
+ ///
+ /// The Paladin icon.
+ ///
+ Paladin = 146,
+
+ ///
+ /// The Monk icon.
+ ///
+ Monk = 147,
+
+ ///
+ /// The Warrior icon.
+ ///
+ Warrior = 148,
+
+ ///
+ /// The Dragoon icon.
+ ///
+ Dragoon = 149,
+
+ ///
+ /// The Bard icon.
+ ///
+ Bard = 150,
+
+ ///
+ /// The White Mage icon.
+ ///
+ WhiteMage = 151,
+
+ ///
+ /// The Black Mage icon.
+ ///
+ BlackMage = 152,
+
+ ///
+ /// The Arcanist icon.
+ ///
+ Arcanist = 153,
+
+ ///
+ /// The Summoner icon.
+ ///
+ Summoner = 154,
+
+ ///
+ /// The Scholar icon.
+ ///
+ Scholar = 155,
+
+ ///
+ /// The Rogue icon.
+ ///
+ Rogue = 156,
+
+ ///
+ /// The Ninja icon.
+ ///
+ Ninja = 157,
+
+ ///
+ /// The Machinist icon.
+ ///
+ Machinist = 158,
+
+ ///
+ /// The Dark Knight icon.
+ ///
+ DarkKnight = 159,
+
+ ///
+ /// The Astrologian icon.
+ ///
+ Astrologian = 160,
+
+ ///
+ /// The Samurai icon.
+ ///
+ Samurai = 161,
+
+ ///
+ /// The Red Mage icon.
+ ///
+ RedMage = 162,
+
+ ///
+ /// The Blue Mage icon.
+ ///
+ BlueMage = 163,
+
+ ///
+ /// The Gunbreaker icon.
+ ///
+ Gunbreaker = 164,
+
+ ///
+ /// The Dancer icon.
+ ///
+ Dancer = 165,
+
+ ///
+ /// The Reaper icon.
+ ///
+ Reaper = 166,
+
+ ///
+ /// The Sage icon.
+ ///
+ Sage = 167,
+
+ ///
+ /// The Waiting For Duty Finder icon.
+ ///
+ WaitingForDutyFinder = 168,
}
diff --git a/Dalamud/GlobalSuppressions.cs b/Dalamud/GlobalSuppressions.cs
index 5c3b40ebc..7426ed5c8 100644
--- a/Dalamud/GlobalSuppressions.cs
+++ b/Dalamud/GlobalSuppressions.cs
@@ -6,12 +6,12 @@
using System.Diagnostics.CodeAnalysis;
// General
-[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:Parameter should not span multiple lines", Justification = "Preventing long lines", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
-[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1124:Do not use regions", Justification = "I like regions", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
-[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1123:Do not place regions within elements", Justification = "I like regions in elements too", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
-[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "This is annoying", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
-[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1512:Single-line comments should not be followed by blank line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
-[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1515:Single-line comment should be preceded by blank line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
-[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1127:Generic type constraints should be on their own line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
-[assembly: SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1028:Code should not contain trailing whitespace", Justification = "I don't care anymore", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
+[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:Parameter should not span multiple lines", Justification = "Preventing long lines")]
+[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1124:Do not use regions", Justification = "I like regions")]
+[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1123:Do not place regions within elements", Justification = "I like regions in elements too")]
+[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "This is annoying")]
+[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1512:Single-line comments should not be followed by blank line", Justification = "I like this better")]
+[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1515:Single-line comment should be preceded by blank line", Justification = "I like this better")]
+[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1127:Generic type constraints should be on their own line", Justification = "I like this better")]
+[assembly: SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1028:Code should not contain trailing whitespace", Justification = "I don't care anymore")]
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File should have header", Justification = "We don't do those yet")]
diff --git a/Dalamud/Hooking/AsmHook.cs b/Dalamud/Hooking/AsmHook.cs
index 4c551db04..b05347a0a 100644
--- a/Dalamud/Hooking/AsmHook.cs
+++ b/Dalamud/Hooking/AsmHook.cs
@@ -35,12 +35,8 @@ public sealed class AsmHook : IDisposable, IDalamudHook
{
address = HookManager.FollowJmp(address);
- var hasOtherHooks = HookManager.Originals.ContainsKey(address);
- if (!hasOtherHooks)
- {
- MemoryHelper.ReadRaw(address, 0x32, out var original);
- HookManager.Originals[address] = original;
- }
+ // We cannot call TrimAfterHook here because the hook is activated by the caller.
+ HookManager.RegisterUnhooker(address);
this.address = address;
this.hookImpl = ReloadedHooks.Instance.CreateAsmHook(assembly, address.ToInt64(), (Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour)asmHookBehaviour);
@@ -65,12 +61,8 @@ public sealed class AsmHook : IDisposable, IDalamudHook
{
address = HookManager.FollowJmp(address);
- var hasOtherHooks = HookManager.Originals.ContainsKey(address);
- if (!hasOtherHooks)
- {
- MemoryHelper.ReadRaw(address, 0x32, out var original);
- HookManager.Originals[address] = original;
- }
+ // We cannot call TrimAfterHook here because the hook is activated by the caller.
+ HookManager.RegisterUnhooker(address);
this.address = address;
this.hookImpl = ReloadedHooks.Instance.CreateAsmHook(assembly, address.ToInt64(), (Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour)asmHookBehaviour);
diff --git a/Dalamud/Hooking/Internal/FunctionPointerVariableHook.cs b/Dalamud/Hooking/Internal/FunctionPointerVariableHook.cs
index e75d9c180..e1900a903 100644
--- a/Dalamud/Hooking/Internal/FunctionPointerVariableHook.cs
+++ b/Dalamud/Hooking/Internal/FunctionPointerVariableHook.cs
@@ -41,12 +41,7 @@ internal class FunctionPointerVariableHook : Hook
{
lock (HookManager.HookEnableSyncRoot)
{
- var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address);
- if (!hasOtherHooks)
- {
- MemoryHelper.ReadRaw(this.Address, 0x32, out var original);
- HookManager.Originals[this.Address] = original;
- }
+ var unhooker = HookManager.RegisterUnhooker(this.Address, 8, 8);
if (!HookManager.MultiHookTracker.TryGetValue(this.Address, out var indexList))
{
@@ -104,6 +99,8 @@ internal class FunctionPointerVariableHook : Hook
// Add afterwards, so the hookIdent starts at 0.
indexList.Add(this);
+ unhooker.TrimAfterHook();
+
HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly));
}
}
diff --git a/Dalamud/Hooking/Internal/HookManager.cs b/Dalamud/Hooking/Internal/HookManager.cs
index 303802ff6..9c288a276 100644
--- a/Dalamud/Hooking/Internal/HookManager.cs
+++ b/Dalamud/Hooking/Internal/HookManager.cs
@@ -16,7 +16,10 @@ namespace Dalamud.Hooking.Internal;
[ServiceManager.EarlyLoadedService]
internal class HookManager : IDisposable, IServiceType
{
- private static readonly ModuleLog Log = new("HM");
+ ///
+ /// Logger shared with .
+ ///
+ internal static readonly ModuleLog Log = new("HM");
[ServiceManager.ServiceConstructor]
private HookManager()
@@ -34,21 +37,48 @@ internal class HookManager : IDisposable, IServiceType
internal static ConcurrentDictionary TrackedHooks { get; } = new();
///
- /// Gets a static dictionary of original code for a hooked address.
+ /// Gets a static dictionary of unhookers for a hooked address.
///
- internal static ConcurrentDictionary Originals { get; } = new();
+ internal static ConcurrentDictionary Unhookers { get; } = new();
///
/// Gets a static dictionary of the number of hooks on a given address.
///
internal static ConcurrentDictionary> MultiHookTracker { get; } = new();
+ ///
+ /// Creates a new Unhooker instance for the provided address if no such unhooker was already registered, or returns
+ /// an existing instance if the address was registered previously. By default, the unhooker will restore between 0
+ /// and 0x32 bytes depending on the detected size of the hook. To specify the minimum and maximum bytes restored
+ /// manually, use .
+ ///
+ /// The address of the instruction.
+ /// A new Unhooker instance.
+ public static Unhooker RegisterUnhooker(IntPtr address)
+ {
+ return RegisterUnhooker(address, 0, 0x32);
+ }
+
+ ///
+ /// Creates a new Unhooker instance for the provided address if no such unhooker was already registered, or returns
+ /// an existing instance if the address was registered previously.
+ ///
+ /// The address of the instruction.
+ /// The minimum amount of bytes to restore when unhooking.
+ /// The maximum amount of bytes to restore when unhooking.
+ /// A new Unhooker instance.
+ public static Unhooker RegisterUnhooker(IntPtr address, int minBytes, int maxBytes)
+ {
+ Log.Verbose($"Registering hook at 0x{address.ToInt64():X} (minBytes=0x{minBytes:X}, maxBytes=0x{maxBytes:X})");
+ return Unhookers.GetOrAdd(address, _ => new Unhooker(address, minBytes, maxBytes));
+ }
+
///
public void Dispose()
{
RevertHooks();
TrackedHooks.Clear();
- Originals.Clear();
+ Unhookers.Clear();
}
///
@@ -60,7 +90,7 @@ internal class HookManager : IDisposable, IServiceType
{
while (true)
{
- var hasOtherHooks = HookManager.Originals.ContainsKey(address);
+ var hasOtherHooks = HookManager.Unhookers.ContainsKey(address);
if (hasOtherHooks)
{
// This address has been hooked already. Do not follow a jmp into a trampoline of our own making.
@@ -124,28 +154,11 @@ internal class HookManager : IDisposable, IServiceType
return address;
}
- private static unsafe void RevertHooks()
+ private static void RevertHooks()
{
- foreach (var (address, originalBytes) in Originals)
+ foreach (var unhooker in Unhookers.Values)
{
- var i = 0;
- var current = (byte*)address;
- // Find how many bytes have been modified by comparing to the saved original
- for (; i < originalBytes.Length; i++)
- {
- if (current[i] == originalBytes[i])
- break;
- }
-
- var snippet = originalBytes[0..i];
-
- if (i > 0)
- {
- Log.Verbose($"Reverting hook at 0x{address.ToInt64():X} ({snippet.Length} bytes)");
- MemoryHelper.ChangePermission(address, i, MemoryProtection.ExecuteReadWrite, out var oldPermissions);
- MemoryHelper.WriteRaw(address, snippet);
- MemoryHelper.ChangePermission(address, i, oldPermissions);
- }
+ unhooker.Unhook();
}
}
}
diff --git a/Dalamud/Hooking/Internal/MinHookHook.cs b/Dalamud/Hooking/Internal/MinHookHook.cs
index 0da289371..89a7d9206 100644
--- a/Dalamud/Hooking/Internal/MinHookHook.cs
+++ b/Dalamud/Hooking/Internal/MinHookHook.cs
@@ -1,8 +1,6 @@
using System;
using System.Reflection;
-using Dalamud.Memory;
-
namespace Dalamud.Hooking.Internal;
///
@@ -24,12 +22,7 @@ internal class MinHookHook : Hook where T : Delegate
{
lock (HookManager.HookEnableSyncRoot)
{
- var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address);
- if (!hasOtherHooks)
- {
- MemoryHelper.ReadRaw(this.Address, 0x32, out var original);
- HookManager.Originals[this.Address] = original;
- }
+ var unhooker = HookManager.RegisterUnhooker(this.Address);
if (!HookManager.MultiHookTracker.TryGetValue(this.Address, out var indexList))
indexList = HookManager.MultiHookTracker[this.Address] = new();
@@ -41,6 +34,8 @@ internal class MinHookHook : Hook where T : Delegate
// Add afterwards, so the hookIdent starts at 0.
indexList.Add(this);
+ unhooker.TrimAfterHook();
+
HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly));
}
}
diff --git a/Dalamud/Hooking/Internal/ReloadedHook.cs b/Dalamud/Hooking/Internal/ReloadedHook.cs
index 77c0c9c19..172bd9671 100644
--- a/Dalamud/Hooking/Internal/ReloadedHook.cs
+++ b/Dalamud/Hooking/Internal/ReloadedHook.cs
@@ -1,7 +1,6 @@
using System;
using System.Reflection;
-using Dalamud.Memory;
using Reloaded.Hooks;
namespace Dalamud.Hooking.Internal;
@@ -25,17 +24,13 @@ internal class ReloadedHook : Hook where T : Delegate
{
lock (HookManager.HookEnableSyncRoot)
{
- var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address);
- if (!hasOtherHooks)
- {
- MemoryHelper.ReadRaw(this.Address, 0x32, out var original);
- HookManager.Originals[this.Address] = original;
- }
-
+ var unhooker = HookManager.RegisterUnhooker(address);
this.hookImpl = ReloadedHooks.Instance.CreateHook(detour, address.ToInt64());
this.hookImpl.Activate();
this.hookImpl.Disable();
+ unhooker.TrimAfterHook();
+
HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly));
}
}
diff --git a/Dalamud/Hooking/Internal/Unhooker.cs b/Dalamud/Hooking/Internal/Unhooker.cs
new file mode 100644
index 000000000..09b071ee9
--- /dev/null
+++ b/Dalamud/Hooking/Internal/Unhooker.cs
@@ -0,0 +1,102 @@
+using System;
+
+using Dalamud.Memory;
+
+namespace Dalamud.Hooking.Internal;
+
+///
+/// A class which stores a copy of the bytes at a location which will be hooked in the future, such that those bytes can
+/// be restored later to "unhook" the function.
+///
+public class Unhooker
+{
+ private readonly IntPtr address;
+ private readonly int minBytes;
+ private byte[] originalBytes;
+ private bool trimmed;
+
+ ///
+ /// Initializes a new instance of the class. Upon creation, the Unhooker stores a copy of
+ /// the bytes stored at the provided address, and can be used to restore these bytes when the hook should be
+ /// removed. As such this class should be instantiated before the function is actually hooked.
+ ///
+ /// The address which will be hooked.
+ /// The minimum amount of bytes to restore when unhooking.
+ /// The maximum amount of bytes to restore when unhooking.
+ internal Unhooker(IntPtr address, int minBytes, int maxBytes)
+ {
+ if (minBytes < 0 || minBytes > maxBytes)
+ {
+ throw new ArgumentException($"minBytes ({minBytes}) must be <= maxBytes ({maxBytes}) and nonnegative.");
+ }
+
+ this.address = address;
+ this.minBytes = minBytes;
+ MemoryHelper.ReadRaw(address, maxBytes, out this.originalBytes);
+ }
+
+ ///
+ /// When called after a hook is created, checks the pre-hook original bytes and post-hook modified bytes, trimming
+ /// the original bytes stored and removing unmodified bytes from the end of the byte sequence. Assuming no
+ /// concurrent actions modified the same address space, this should result in storing only the minimum bytes
+ /// required to unhook the function.
+ ///
+ public void TrimAfterHook()
+ {
+ if (this.trimmed)
+ {
+ return;
+ }
+
+ var len = int.Max(this.GetFullHookLength(), this.minBytes);
+
+ this.originalBytes = this.originalBytes[..len];
+ this.trimmed = true;
+ }
+
+ ///
+ /// Attempts to unhook the function by replacing the hooked bytes with the original bytes. If
+ /// was called, the trimmed original bytes stored at that time will be used for
+ /// unhooking. Otherwise, a naive algorithm which only restores bytes until the first unchanged byte will be used in
+ /// order to avoid overwriting adjacent data.
+ ///
+ public void Unhook()
+ {
+ var len = this.trimmed ? this.originalBytes.Length : int.Max(this.GetNaiveHookLength(), this.minBytes);
+ if (len > 0)
+ {
+ HookManager.Log.Verbose($"Reverting hook at 0x{this.address.ToInt64():X} ({len} bytes, trimmed={this.trimmed})");
+ MemoryHelper.ChangePermission(this.address, len, MemoryProtection.ExecuteReadWrite, out var oldPermissions);
+ MemoryHelper.WriteRaw(this.address, this.originalBytes[..len]);
+ MemoryHelper.ChangePermission(this.address, len, oldPermissions);
+ }
+ }
+
+ private unsafe int GetNaiveHookLength()
+ {
+ var current = (byte*)this.address;
+ for (var i = 0; i < this.originalBytes.Length; i++)
+ {
+ if (current[i] == this.originalBytes[i])
+ {
+ return i;
+ }
+ }
+
+ return 0;
+ }
+
+ private unsafe int GetFullHookLength()
+ {
+ var current = (byte*)this.address;
+ for (var i = this.originalBytes.Length - 1; i >= 0; i--)
+ {
+ if (current[i] != this.originalBytes[i])
+ {
+ return int.Max(i + 1, this.minBytes);
+ }
+ }
+
+ return 0;
+ }
+}
diff --git a/Dalamud/Interface/Internal/DalamudCommands.cs b/Dalamud/Interface/Internal/DalamudCommands.cs
index dfcccf79d..307f79436 100644
--- a/Dalamud/Interface/Internal/DalamudCommands.cs
+++ b/Dalamud/Interface/Internal/DalamudCommands.cs
@@ -29,13 +29,19 @@ internal class DalamudCommands : IServiceType
HelpMessage = Loc.Localize("DalamudUnloadHelp", "Unloads XIVLauncher in-game addon."),
ShowInHelp = false,
});
-
+
commandManager.AddHandler("/xlkill", new CommandInfo(this.OnKillCommand)
{
HelpMessage = "Kill the game.",
ShowInHelp = false,
});
+ commandManager.AddHandler("/xlrestart", new CommandInfo(this.OnRestartCommand)
+ {
+ HelpMessage = "Restart the game.",
+ ShowInHelp = false,
+ });
+
commandManager.AddHandler("/xlhelp", new CommandInfo(this.OnHelpCommand)
{
HelpMessage = Loc.Localize("DalamudCmdInfoHelp", "Shows list of commands available. If an argument is provided, shows help for that command."),
@@ -147,12 +153,17 @@ internal class DalamudCommands : IServiceType
Service.Get().Print("Unloading...");
Service.Get().Unload();
}
-
+
private void OnKillCommand(string command, string arguments)
{
Process.GetCurrentProcess().Kill();
}
+ private void OnRestartCommand(string command, string arguments)
+ {
+ Dalamud.RestartGame();
+ }
+
private void OnHelpCommand(string command, string arguments)
{
var chatGui = Service.Get();
diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs
index 07d557967..ca7cbe287 100644
--- a/Dalamud/Interface/Internal/DalamudInterface.cs
+++ b/Dalamud/Interface/Internal/DalamudInterface.cs
@@ -717,12 +717,7 @@ internal class DalamudInterface : IDisposable, IServiceType
if (ImGui.MenuItem("Restart game"))
{
- [DllImport("kernel32.dll")]
- [return: MarshalAs(UnmanagedType.Bool)]
- static extern void RaiseException(uint dwExceptionCode, uint dwExceptionFlags, uint nNumberOfArguments, IntPtr lpArguments);
-
- RaiseException(0x12345678, 0, 0, IntPtr.Zero);
- Process.GetCurrentProcess().Kill();
+ Dalamud.RestartGame();
}
if (ImGui.MenuItem("Kill game"))
@@ -802,6 +797,11 @@ internal class DalamudInterface : IDisposable, IServiceType
ImGui.SetWindowFocus(null);
}
+ if (ImGui.MenuItem("Clear stacks"))
+ {
+ Service.Get().ClearStacks();
+ }
+
if (ImGui.MenuItem("Dump style"))
{
var info = string.Empty;
diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs
index 9a8da773c..5de5f52de 100644
--- a/Dalamud/Interface/Internal/InterfaceManager.cs
+++ b/Dalamud/Interface/Internal/InterfaceManager.cs
@@ -435,6 +435,15 @@ internal class InterfaceManager : IDisposable, IServiceType
return null;
}
+ ///
+ /// Clear font, style, and color stack. Dangerous, only use when you know
+ /// no one else has something pushed they may try to pop.
+ ///
+ public void ClearStacks()
+ {
+ this.scene?.ClearStacksOnContext();
+ }
+
///
/// Toggle Windows 11 immersive mode on the game window.
///
@@ -892,6 +901,7 @@ internal class InterfaceManager : IDisposable, IServiceType
Log.Verbose("[FONT] ImGui.IO.Build will be called.");
ioFonts.Build();
gameFontManager.AfterIoFontsBuild();
+ this.ClearStacks();
Log.Verbose("[FONT] ImGui.IO.Build OK!");
gameFontManager.AfterBuildFonts();
diff --git a/Dalamud/Interface/Internal/PluginCategoryManager.cs b/Dalamud/Interface/Internal/PluginCategoryManager.cs
index 06e306c50..9515a55b5 100644
--- a/Dalamud/Interface/Internal/PluginCategoryManager.cs
+++ b/Dalamud/Interface/Internal/PluginCategoryManager.cs
@@ -28,6 +28,7 @@ internal class PluginCategoryManager
new(11, "special.devIconTester", () => Locs.Category_IconTester),
new(12, "special.dalamud", () => Locs.Category_Dalamud),
new(13, "special.plugins", () => Locs.Category_Plugins),
+ new(14, "special.profiles", () => Locs.Category_PluginProfiles, CategoryInfo.AppearCondition.ProfilesEnabled),
new(FirstTagBasedCategoryId + 0, "other", () => Locs.Category_Other),
new(FirstTagBasedCategoryId + 1, "jobs", () => Locs.Category_Jobs),
new(FirstTagBasedCategoryId + 2, "ui", () => Locs.Category_UI),
@@ -43,7 +44,7 @@ internal class PluginCategoryManager
private GroupInfo[] groupList =
{
new(GroupKind.DevTools, () => Locs.Group_DevTools, 10, 11),
- new(GroupKind.Installed, () => Locs.Group_Installed, 0, 1),
+ new(GroupKind.Installed, () => Locs.Group_Installed, 0, 1, 14),
new(GroupKind.Available, () => Locs.Group_Available, 0),
new(GroupKind.Changelog, () => Locs.Group_Changelog, 0, 12, 13),
@@ -352,6 +353,11 @@ internal class PluginCategoryManager
/// Check if plugin testing is enabled.
///
DoPluginTest,
+
+ ///
+ /// Check if plugin profiles are enabled.
+ ///
+ ProfilesEnabled,
}
///
@@ -430,6 +436,8 @@ internal class PluginCategoryManager
public static string Category_IconTester => "Image/Icon Tester";
+ public static string Category_PluginProfiles => Loc.Localize("InstallerCategoryPluginProfiles", "Plugin Collections");
+
public static string Category_Other => Loc.Localize("InstallerCategoryOther", "Other");
public static string Category_Jobs => Loc.Localize("InstallerCategoryJobs", "Jobs");
diff --git a/Dalamud/Interface/Internal/Windows/DataWindow.cs b/Dalamud/Interface/Internal/Windows/DataWindow.cs
index 2c328d7f1..c46916343 100644
--- a/Dalamud/Interface/Internal/Windows/DataWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/DataWindow.cs
@@ -1792,7 +1792,8 @@ internal class DataWindow : Window
ImGui.TableNextColumn();
ImGui.TextUnformatted(string.Join(", ", share.Users));
}
- } finally
+ }
+ finally
{
ImGui.EndTable();
}
diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogManager.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogManager.cs
index a3a965e80..2dc182e9a 100644
--- a/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogManager.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogManager.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Json;
using System.Threading.Tasks;
+
using Dalamud.Networking.Http;
using Dalamud.Plugin.Internal;
using Dalamud.Utility;
diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
index c548b33fb..fb7714098 100644
--- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
@@ -15,12 +15,14 @@ using Dalamud.Game.Command;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Internal.Notifications;
+using Dalamud.Interface.Raii;
using Dalamud.Interface.Style;
using Dalamud.Interface.Windowing;
using Dalamud.Logging.Internal;
using Dalamud.Plugin;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Internal.Exceptions;
+using Dalamud.Plugin.Internal.Profiles;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Support;
using Dalamud.Utility;
@@ -48,6 +50,8 @@ internal class PluginInstallerWindow : Window, IDisposable
private readonly object listLock = new();
+ private readonly ProfileManagerWidget profileManagerWidget;
+
private DalamudChangelogManager? dalamudChangelogManager;
private Task? dalamudChangelogRefreshTask;
private CancellationTokenSource? dalamudChangelogRefreshTaskCts;
@@ -148,6 +152,8 @@ internal class PluginInstallerWindow : Window, IDisposable
});
this.timeLoaded = DateTime.Now;
+
+ this.profileManagerWidget = new(this);
}
private enum OperationStatus
@@ -166,6 +172,7 @@ internal class PluginInstallerWindow : Window, IDisposable
UpdatingAll,
Installing,
Manager,
+ ProfilesLoading,
}
private enum PluginSortKind
@@ -212,6 +219,8 @@ internal class PluginInstallerWindow : Window, IDisposable
this.updatePluginCount = 0;
this.updatedPlugins = null;
}
+
+ this.profileManagerWidget.Reset();
}
///
@@ -284,17 +293,96 @@ internal class PluginInstallerWindow : Window, IDisposable
this.searchText = text;
}
+ ///
+ /// Start a plugin install and handle errors visually.
+ ///
+ /// The manifest to install.
+ /// Install the testing version.
+ public void StartInstall(RemotePluginManifest manifest, bool useTesting)
+ {
+ var pluginManager = Service.Get();
+ var notifications = Service.Get();
+
+ this.installStatus = OperationStatus.InProgress;
+ this.loadingIndicatorKind = LoadingIndicatorKind.Installing;
+
+ Task.Run(() => pluginManager.InstallPluginAsync(manifest, useTesting || manifest.IsTestingExclusive, PluginLoadReason.Installer))
+ .ContinueWith(task =>
+ {
+ // There is no need to set as Complete for an individual plugin installation
+ this.installStatus = OperationStatus.Idle;
+ if (this.DisplayErrorContinuation(task, Locs.ErrorModal_InstallFail(manifest.Name)))
+ {
+ // Fine as long as we aren't in an error state
+ if (task.Result.State is PluginState.Loaded or PluginState.Unloaded)
+ {
+ notifications.AddNotification(Locs.Notifications_PluginInstalled(manifest.Name), Locs.Notifications_PluginInstalledTitle, NotificationType.Success);
+ }
+ else
+ {
+ notifications.AddNotification(Locs.Notifications_PluginNotInstalled(manifest.Name), Locs.Notifications_PluginNotInstalledTitle, NotificationType.Error);
+ this.ShowErrorModal(Locs.ErrorModal_InstallFail(manifest.Name));
+ }
+ }
+ });
+ }
+
+ ///
+ /// A continuation task that displays any errors received into the error modal.
+ ///
+ /// The previous task.
+ /// An error message to be displayed.
+ /// A value indicating whether to continue with the next task.
+ public bool DisplayErrorContinuation(Task task, object state)
+ {
+ if (task.IsFaulted)
+ {
+ var errorModalMessage = state as string;
+
+ foreach (var ex in task.Exception.InnerExceptions)
+ {
+ if (ex is PluginException)
+ {
+ Log.Error(ex, "Plugin installer threw an error");
+#if DEBUG
+ if (!string.IsNullOrEmpty(ex.Message))
+ errorModalMessage += $"\n\n{ex.Message}";
+#endif
+ }
+ else
+ {
+ Log.Error(ex, "Plugin installer threw an unexpected error");
+#if DEBUG
+ if (!string.IsNullOrEmpty(ex.Message))
+ errorModalMessage += $"\n\n{ex.Message}";
+#endif
+ }
+ }
+
+ this.ShowErrorModal(errorModalMessage);
+
+ return false;
+ }
+
+ return true;
+ }
+
private void DrawProgressOverlay()
{
var pluginManager = Service.Get();
+ var profileManager = Service.Get();
var isWaitingManager = !pluginManager.PluginsReady ||
!pluginManager.ReposReady;
+ var isWaitingProfiles = profileManager.IsBusy;
+
var isLoading = this.AnyOperationInProgress ||
- isWaitingManager;
+ isWaitingManager || isWaitingProfiles;
if (isWaitingManager)
this.loadingIndicatorKind = LoadingIndicatorKind.Manager;
+ else if (isWaitingProfiles)
+ this.loadingIndicatorKind = LoadingIndicatorKind.ProfilesLoading;
if (!isLoading)
return;
@@ -378,6 +466,9 @@ internal class PluginInstallerWindow : Window, IDisposable
}
}
+ break;
+ case LoadingIndicatorKind.ProfilesLoading:
+ ImGuiHelpers.CenteredText("Collections are being applied...");
break;
default:
throw new ArgumentOutOfRangeException();
@@ -386,9 +477,7 @@ internal class PluginInstallerWindow : Window, IDisposable
if (DateTime.Now - this.timeLoaded > TimeSpan.FromSeconds(90) && !pluginManager.PluginsReady)
{
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
- ImGuiHelpers.CenteredText("This is embarrassing, but...");
- ImGuiHelpers.CenteredText("one of your plugins may be blocking the installer.");
- ImGuiHelpers.CenteredText("You should tell us about this, please keep this window open.");
+ ImGuiHelpers.CenteredText("One of your plugins may be blocking the installer.");
ImGui.PopStyleColor();
}
@@ -433,54 +522,57 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.SetCursorPosX(windowSize.X - sortSelectWidth - (style.ItemSpacing.X * 2) - searchInputWidth - searchClearButtonWidth);
- var searchTextChanged = false;
- ImGui.SetNextItemWidth(searchInputWidth);
- searchTextChanged |= ImGui.InputTextWithHint(
- "###XlPluginInstaller_Search",
- Locs.Header_SearchPlaceholder,
- ref this.searchText,
- 100);
-
- ImGui.SameLine();
- ImGui.SetCursorPosY(downShift);
-
- ImGui.SetNextItemWidth(searchClearButtonWidth);
- if (ImGuiComponents.IconButton(FontAwesomeIcon.Times))
+ var isProfileManager =
+ this.categoryManager.CurrentGroupIdx == 1 && this.categoryManager.CurrentCategoryIdx == 2;
+
+ // Disable search if profile editor
+ using (ImRaii.Disabled(isProfileManager))
{
- this.searchText = string.Empty;
- searchTextChanged = true;
- }
+ var searchTextChanged = false;
+ ImGui.SetNextItemWidth(searchInputWidth);
+ searchTextChanged |= ImGui.InputTextWithHint(
+ "###XlPluginInstaller_Search",
+ Locs.Header_SearchPlaceholder,
+ ref this.searchText,
+ 100);
- if (searchTextChanged)
- this.UpdateCategoriesOnSearchChange();
+ ImGui.SameLine();
+ ImGui.SetCursorPosY(downShift);
- // Changelog group
- var isSortDisabled = this.categoryManager.CurrentGroupIdx == 3;
- if (isSortDisabled)
- ImGui.BeginDisabled();
-
- ImGui.SameLine();
- ImGui.SetCursorPosY(downShift);
- ImGui.SetNextItemWidth(selectableWidth);
- if (ImGui.BeginCombo(sortByText, this.filterText, ImGuiComboFlags.NoArrowButton))
- {
- foreach (var selectable in sortSelectables)
+ ImGui.SetNextItemWidth(searchClearButtonWidth);
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.Times))
{
- if (ImGui.Selectable(selectable.Localization))
- {
- this.sortKind = selectable.SortKind;
- this.filterText = selectable.Localization;
-
- lock (this.listLock)
- this.ResortPlugins();
- }
+ this.searchText = string.Empty;
+ searchTextChanged = true;
}
- ImGui.EndCombo();
+ if (searchTextChanged)
+ this.UpdateCategoriesOnSearchChange();
}
- if (isSortDisabled)
- ImGui.EndDisabled();
+ // Disable sort if changelogs or profile editor
+ using (ImRaii.Disabled(this.categoryManager.CurrentGroupIdx == 3 || isProfileManager))
+ {
+ ImGui.SameLine();
+ ImGui.SetCursorPosY(downShift);
+ ImGui.SetNextItemWidth(selectableWidth);
+ if (ImGui.BeginCombo(sortByText, this.filterText, ImGuiComboFlags.NoArrowButton))
+ {
+ foreach (var selectable in sortSelectables)
+ {
+ if (ImGui.Selectable(selectable.Localization))
+ {
+ this.sortKind = selectable.SortKind;
+ this.filterText = selectable.Localization;
+
+ lock (this.listLock)
+ this.ResortPlugins();
+ }
+ }
+
+ ImGui.EndCombo();
+ }
+ }
}
private void DrawFooter()
@@ -1089,6 +1181,10 @@ internal class PluginInstallerWindow : Window, IDisposable
if (!Service.Get().DoPluginTest)
continue;
break;
+ case PluginCategoryManager.CategoryInfo.AppearCondition.ProfilesEnabled:
+ if (!Service.Get().ProfilesEnabled)
+ continue;
+ break;
default:
throw new ArgumentOutOfRangeException();
}
@@ -1194,6 +1290,10 @@ internal class PluginInstallerWindow : Window, IDisposable
case 1:
this.DrawInstalledPluginList(true);
break;
+
+ case 2:
+ this.profileManagerWidget.Draw();
+ break;
}
break;
@@ -1533,7 +1633,7 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.SetCursorPos(startCursor);
- var pluginDisabled = plugin is { IsDisabled: true };
+ var pluginDisabled = plugin is { IsWantedByAnyProfile: false };
var iconSize = ImGuiHelpers.ScaledVector2(64, 64);
var cursorBeforeImage = ImGui.GetCursorPos();
@@ -1831,27 +1931,7 @@ internal class PluginInstallerWindow : Window, IDisposable
var buttonText = Locs.PluginButton_InstallVersion(versionString);
if (ImGui.Button($"{buttonText}##{buttonText}{index}"))
{
- this.installStatus = OperationStatus.InProgress;
- this.loadingIndicatorKind = LoadingIndicatorKind.Installing;
-
- Task.Run(() => pluginManager.InstallPluginAsync(manifest, useTesting || manifest.IsTestingExclusive, PluginLoadReason.Installer))
- .ContinueWith(task =>
- {
- // There is no need to set as Complete for an individual plugin installation
- this.installStatus = OperationStatus.Idle;
- if (this.DisplayErrorContinuation(task, Locs.ErrorModal_InstallFail(manifest.Name)))
- {
- if (task.Result.State == PluginState.Loaded)
- {
- notifications.AddNotification(Locs.Notifications_PluginInstalled(manifest.Name), Locs.Notifications_PluginInstalledTitle, NotificationType.Success);
- }
- else
- {
- notifications.AddNotification(Locs.Notifications_PluginNotInstalled(manifest.Name), Locs.Notifications_PluginNotInstalledTitle, NotificationType.Error);
- this.ShowErrorModal(Locs.ErrorModal_InstallFail(manifest.Name));
- }
- }
- });
+ this.StartInstall(manifest, useTesting);
}
}
@@ -1958,7 +2038,7 @@ internal class PluginInstallerWindow : Window, IDisposable
}
// Disabled
- if (plugin.IsDisabled || !plugin.CheckPolicy())
+ if (!plugin.IsWantedByAnyProfile || !plugin.CheckPolicy())
{
label += Locs.PluginTitleMod_Disabled;
trouble = true;
@@ -2118,7 +2198,7 @@ internal class PluginInstallerWindow : Window, IDisposable
this.DrawSendFeedbackButton(plugin.Manifest, plugin.IsTesting);
}
- if (availablePluginUpdate != default)
+ if (availablePluginUpdate != default && !plugin.IsDev)
this.DrawUpdateSinglePluginButton(availablePluginUpdate);
ImGui.SameLine();
@@ -2240,6 +2320,11 @@ internal class PluginInstallerWindow : Window, IDisposable
{
var notifications = Service.Get();
var pluginManager = Service.Get();
+ var profileManager = Service.Get();
+ var config = Service.Get();
+
+ var applicableForProfiles = plugin.Manifest.SupportsProfiles;
+ var isDefaultPlugin = profileManager.IsInDefaultProfile(plugin.Manifest.InternalName);
// Disable everything if the updater is running or another plugin is operating
var disabled = this.updateStatus == OperationStatus.InProgress || this.installStatus == OperationStatus.InProgress;
@@ -2255,15 +2340,70 @@ internal class PluginInstallerWindow : Window, IDisposable
// Now handled by the first case below
// disabled = disabled || plugin.State == PluginState.LoadError || plugin.State == PluginState.DependencyResolutionFailed;
- // Disable everything if we're working
+ // Disable everything if we're loading plugins
disabled = disabled || plugin.State == PluginState.Loading || plugin.State == PluginState.Unloading;
+ // Disable everything if we're applying profiles
+ disabled = disabled || profileManager.IsBusy;
+
var toggleId = plugin.Manifest.InternalName;
var isLoadedAndUnloadable = plugin.State == PluginState.Loaded ||
plugin.State == PluginState.DependencyResolutionFailed;
StyleModelV1.DalamudStandard.Push();
+ var profileChooserPopupName = $"###pluginProfileChooser{plugin.Manifest.InternalName}";
+ if (ImGui.BeginPopup(profileChooserPopupName))
+ {
+ var didAny = false;
+
+ foreach (var profile in profileManager.Profiles.Where(x => !x.IsDefaultProfile))
+ {
+ var inProfile = profile.WantsPlugin(plugin.Manifest.InternalName) != null;
+ if (ImGui.Checkbox($"###profilePick{profile.Guid}{plugin.Manifest.InternalName}", ref inProfile))
+ {
+ if (inProfile)
+ {
+ Task.Run(() => profile.AddOrUpdate(plugin.Manifest.InternalName, true))
+ .ContinueWith(this.DisplayErrorContinuation, Locs.Profiles_CouldNotAdd);
+ }
+ else
+ {
+ Task.Run(() => profile.Remove(plugin.Manifest.InternalName))
+ .ContinueWith(this.DisplayErrorContinuation, Locs.Profiles_CouldNotRemove);
+ }
+ }
+
+ ImGui.SameLine();
+
+ ImGui.TextUnformatted(profile.Name);
+
+ didAny = true;
+ }
+
+ if (!didAny)
+ ImGui.TextColored(ImGuiColors.DalamudGrey, Locs.Profiles_None);
+
+ ImGui.Separator();
+
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.Times))
+ {
+ profileManager.DefaultProfile.AddOrUpdate(plugin.Manifest.InternalName, plugin.IsLoaded, false);
+ foreach (var profile in profileManager.Profiles.Where(x => !x.IsDefaultProfile && x.Plugins.Any(y => y.InternalName == plugin.Manifest.InternalName)))
+ {
+ profile.Remove(plugin.Manifest.InternalName, false);
+ }
+
+ // TODO error handling
+ Task.Run(() => profileManager.ApplyAllWantStates());
+ }
+
+ ImGui.SameLine();
+ ImGui.Text(Locs.Profiles_RemoveFromAll);
+
+ ImGui.EndPopup();
+ }
+
if (plugin.State is PluginState.UnloadError or PluginState.LoadError or PluginState.DependencyResolutionFailed && !plugin.IsDev)
{
ImGuiComponents.DisabledButton(FontAwesomeIcon.Frown);
@@ -2271,14 +2411,18 @@ internal class PluginInstallerWindow : Window, IDisposable
if (ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.PluginButtonToolTip_UnloadFailed);
}
- else if (disabled)
+ else if (disabled || !isDefaultPlugin)
{
ImGuiComponents.DisabledToggleButton(toggleId, isLoadedAndUnloadable);
+
+ if (!isDefaultPlugin && ImGui.IsItemHovered())
+ ImGui.SetTooltip(Locs.PluginButtonToolTip_NeedsToBeInDefault);
}
else
{
if (ImGuiComponents.ToggleButton(toggleId, ref isLoadedAndUnloadable))
{
+ // TODO: We can technically let profile manager take care of unloading/loading the plugin, but we should figure out error handling first.
if (!isLoadedAndUnloadable)
{
this.enableDisableStatus = OperationStatus.InProgress;
@@ -2301,15 +2445,9 @@ internal class PluginInstallerWindow : Window, IDisposable
return;
}
- var disableTask = Task.Run(() => plugin.Disable())
- .ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_DisableFail(plugin.Name));
-
- disableTask.Wait();
+ profileManager.DefaultProfile.AddOrUpdate(plugin.Manifest.InternalName, false, false);
this.enableDisableStatus = OperationStatus.Complete;
- if (!disableTask.Result)
- return;
-
notifications.AddNotification(Locs.Notifications_PluginDisabled(plugin.Manifest.Name), Locs.Notifications_PluginDisabledTitle, NotificationType.Success);
});
}
@@ -2325,17 +2463,7 @@ internal class PluginInstallerWindow : Window, IDisposable
plugin.ReloadManifest();
}
- var enableTask = Task.Run(plugin.Enable)
- .ContinueWith(
- this.DisplayErrorContinuation,
- Locs.ErrorModal_EnableFail(plugin.Name));
-
- enableTask.Wait();
- if (!enableTask.Result)
- {
- this.enableDisableStatus = OperationStatus.Complete;
- return;
- }
+ profileManager.DefaultProfile.AddOrUpdate(plugin.Manifest.InternalName, true, false);
var loadTask = Task.Run(() => plugin.LoadAsync(PluginLoadReason.Installer))
.ContinueWith(
@@ -2388,6 +2516,29 @@ internal class PluginInstallerWindow : Window, IDisposable
// Only if the plugin isn't broken.
this.DrawOpenPluginSettingsButton(plugin);
}
+
+ if (applicableForProfiles && config.ProfilesEnabled)
+ {
+ ImGui.SameLine();
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.Toolbox))
+ {
+ ImGui.OpenPopup(profileChooserPopupName);
+ }
+
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip(Locs.PluginButtonToolTip_PickProfiles);
+ }
+ else if (!applicableForProfiles && config.ProfilesEnabled)
+ {
+ ImGui.SameLine();
+
+ ImGui.BeginDisabled();
+ ImGuiComponents.IconButton(FontAwesomeIcon.Toolbox);
+ ImGui.EndDisabled();
+
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip(Locs.PluginButtonToolTip_ProfilesNotSupported);
+ }
}
private async Task UpdateSinglePlugin(AvailablePluginUpdate update)
@@ -2474,26 +2625,32 @@ internal class PluginInstallerWindow : Window, IDisposable
if (localPlugin is LocalDevPlugin plugin)
{
+ var isInDefaultProfile =
+ Service.Get().IsInDefaultProfile(localPlugin.Manifest.InternalName);
+
// https://colorswall.com/palette/2868/
var greenColor = new Vector4(0x5C, 0xB8, 0x5C, 0xFF) / 0xFF;
var redColor = new Vector4(0xD9, 0x53, 0x4F, 0xFF) / 0xFF;
// Load on boot
- ImGui.PushStyleColor(ImGuiCol.Button, plugin.StartOnBoot ? greenColor : redColor);
- ImGui.PushStyleColor(ImGuiCol.ButtonHovered, plugin.StartOnBoot ? greenColor : redColor);
-
- ImGui.SameLine();
- if (ImGuiComponents.IconButton(FontAwesomeIcon.PowerOff))
+ using (ImRaii.Disabled(!isInDefaultProfile))
{
- plugin.StartOnBoot ^= true;
- configuration.QueueSave();
- }
+ ImGui.PushStyleColor(ImGuiCol.Button, plugin.StartOnBoot ? greenColor : redColor);
+ ImGui.PushStyleColor(ImGuiCol.ButtonHovered, plugin.StartOnBoot ? greenColor : redColor);
- ImGui.PopStyleColor(2);
+ ImGui.SameLine();
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.PowerOff))
+ {
+ plugin.StartOnBoot ^= true;
+ configuration.QueueSave();
+ }
- if (ImGui.IsItemHovered())
- {
- ImGui.SetTooltip(Locs.PluginButtonToolTip_StartOnBoot);
+ ImGui.PopStyleColor(2);
+
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip(isInDefaultProfile ? Locs.PluginButtonToolTip_StartOnBoot : Locs.PluginButtonToolTip_NeedsToBeInDefault);
+ }
}
// Automatic reload
@@ -2800,46 +2957,6 @@ internal class PluginInstallerWindow : Window, IDisposable
private bool WasPluginSeen(string internalName) =>
Service.Get().SeenPluginInternalName.Contains(internalName);
- ///
- /// A continuation task that displays any errors received into the error modal.
- ///
- /// The previous task.
- /// An error message to be displayed.
- /// A value indicating whether to continue with the next task.
- private bool DisplayErrorContinuation(Task task, object state)
- {
- if (task.IsFaulted)
- {
- var errorModalMessage = state as string;
-
- foreach (var ex in task.Exception.InnerExceptions)
- {
- if (ex is PluginException)
- {
- Log.Error(ex, "Plugin installer threw an error");
-#if DEBUG
- if (!string.IsNullOrEmpty(ex.Message))
- errorModalMessage += $"\n\n{ex.Message}";
-#endif
- }
- else
- {
- Log.Error(ex, "Plugin installer threw an unexpected error");
-#if DEBUG
- if (!string.IsNullOrEmpty(ex.Message))
- errorModalMessage += $"\n\n{ex.Message}";
-#endif
- }
- }
-
- this.ShowErrorModal(errorModalMessage);
-
- return false;
- }
-
- return true;
- }
-
private Task ShowErrorModal(string message)
{
this.errorModalMessage = message;
@@ -2890,7 +3007,7 @@ internal class PluginInstallerWindow : Window, IDisposable
#region Header
- public static string Header_Hint => Loc.Localize("InstallerHint", "This window allows you to install and remove in-game plugins.\nThey are made by third-party developers.");
+ public static string Header_Hint => Loc.Localize("InstallerHint", "This window allows you to install and remove Dalamud plugins.\nThey are made by the community.");
public static string Header_SearchPlaceholder => Loc.Localize("InstallerSearch", "Search");
@@ -3047,6 +3164,10 @@ internal class PluginInstallerWindow : Window, IDisposable
public static string PluginButtonToolTip_OpenConfiguration => Loc.Localize("InstallerOpenConfig", "Open Configuration");
+ public static string PluginButtonToolTip_PickProfiles => Loc.Localize("InstallerPickProfiles", "Pick collections for this plugin");
+
+ public static string PluginButtonToolTip_ProfilesNotSupported => Loc.Localize("InstallerProfilesNotSupported", "This plugin does not support collections");
+
public static string PluginButtonToolTip_StartOnBoot => Loc.Localize("InstallerStartOnBoot", "Start on boot");
public static string PluginButtonToolTip_AutomaticReloading => Loc.Localize("InstallerAutomaticReloading", "Automatic reloading");
@@ -3067,6 +3188,8 @@ internal class PluginInstallerWindow : Window, IDisposable
public static string PluginButtonToolTip_UnloadFailed => Loc.Localize("InstallerLoadUnloadFailedTooltip", "Plugin load/unload failed, please restart your game and try again.");
+ public static string PluginButtonToolTip_NeedsToBeInDefault => Loc.Localize("InstallerUnloadNeedsToBeInDefault", "This plugin is in one or more collections. If you want to enable or disable it, please do so by enabling or disabling the collections it is in.\nIf you want to manage it manually, remove it from all collections.");
+
#endregion
#region Notifications
@@ -3226,5 +3349,20 @@ internal class PluginInstallerWindow : Window, IDisposable
public static string SafeModeDisclaimer => Loc.Localize("SafeModeDisclaimer", "You enabled safe mode, no plugins will be loaded.\nYou may delete plugins from the \"Installed plugins\" tab.\nSimply restart your game to disable safe mode.");
#endregion
+
+ #region Profiles
+
+ public static string Profiles_CouldNotAdd =>
+ Loc.Localize("InstallerProfilesCouldNotAdd", "Couldn't add plugin to this collection.");
+
+ public static string Profiles_CouldNotRemove =>
+ Loc.Localize("InstallerProfilesCouldNotRemove", "Couldn't remove plugin from this collection.");
+
+ public static string Profiles_None => Loc.Localize("InstallerProfilesNone", "No collections! Go add some in \"Plugin Collections\"!");
+
+ public static string Profiles_RemoveFromAll =>
+ Loc.Localize("InstallerProfilesRemoveFromAll", "Remove from all collections");
+
+ #endregion
}
}
diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs
new file mode 100644
index 000000000..4f92cebb8
--- /dev/null
+++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs
@@ -0,0 +1,506 @@
+using System;
+using System.Linq;
+using System.Numerics;
+using System.Threading.Tasks;
+
+using CheapLoc;
+using Dalamud.Interface.Colors;
+using Dalamud.Interface.Components;
+using Dalamud.Interface.Internal.Notifications;
+using Dalamud.Interface.Raii;
+using Dalamud.Plugin.Internal;
+using Dalamud.Plugin.Internal.Profiles;
+using Dalamud.Utility;
+using ImGuiNET;
+using Serilog;
+
+namespace Dalamud.Interface.Internal.Windows.PluginInstaller;
+
+///
+/// ImGui widget used to manage profiles.
+///
+internal class ProfileManagerWidget
+{
+ private readonly PluginInstallerWindow installer;
+ private Mode mode = Mode.Overview;
+ private Guid? editingProfileGuid;
+
+ private string pickerSearch = string.Empty;
+ private string profileNameEdit = string.Empty;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The plugin installer.
+ public ProfileManagerWidget(PluginInstallerWindow installer)
+ {
+ this.installer = installer;
+ }
+
+ private enum Mode
+ {
+ Overview,
+ EditSingleProfile,
+ }
+
+ ///
+ /// Draw this widget's contents.
+ ///
+ public void Draw()
+ {
+ switch (this.mode)
+ {
+ case Mode.Overview:
+ this.DrawOverview();
+ break;
+
+ case Mode.EditSingleProfile:
+ this.DrawEdit();
+ break;
+ }
+ }
+
+ ///
+ /// Reset the widget.
+ ///
+ public void Reset()
+ {
+ this.mode = Mode.Overview;
+ this.editingProfileGuid = null;
+ this.pickerSearch = string.Empty;
+ }
+
+ private void DrawOverview()
+ {
+ var didAny = false;
+ var profman = Service.Get();
+
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.Plus))
+ profman.AddNewProfile();
+
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip(Locs.AddProfile);
+
+ ImGui.SameLine();
+ ImGuiHelpers.ScaledDummy(5);
+ ImGui.SameLine();
+
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.FileImport))
+ {
+ try
+ {
+ profman.ImportProfile(ImGui.GetClipboardText());
+ Service.Get().AddNotification(Locs.NotificationImportSuccess, type: NotificationType.Success);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Could not import profile");
+ Service.Get().AddNotification(Locs.NotificationImportError, type: NotificationType.Error);
+ }
+ }
+
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip(Locs.ImportProfileHint);
+
+ ImGui.Separator();
+ ImGuiHelpers.ScaledDummy(5);
+
+ var windowSize = ImGui.GetWindowSize();
+
+ if (ImGui.BeginChild("###profileChooserScrolling"))
+ {
+ Guid? toCloneGuid = null;
+
+ foreach (var profile in profman.Profiles)
+ {
+ if (profile.IsDefaultProfile)
+ continue;
+
+ var isEnabled = profile.IsEnabled;
+ if (ImGuiComponents.ToggleButton($"###toggleButton{profile.Guid}", ref isEnabled))
+ {
+ Task.Run(() => profile.SetState(isEnabled))
+ .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState);
+ }
+
+ ImGui.SameLine();
+ ImGuiHelpers.ScaledDummy(3);
+ ImGui.SameLine();
+
+ ImGui.Text(profile.Name);
+
+ ImGui.SameLine();
+ ImGui.SetCursorPosX(windowSize.X - (ImGuiHelpers.GlobalScale * 30));
+
+ if (ImGuiComponents.IconButton($"###editButton{profile.Guid}", FontAwesomeIcon.PencilAlt))
+ {
+ this.mode = Mode.EditSingleProfile;
+ this.editingProfileGuid = profile.Guid;
+ this.profileNameEdit = profile.Name;
+ }
+
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip(Locs.EditProfileHint);
+
+ ImGui.SameLine();
+ ImGui.SetCursorPosX(windowSize.X - (ImGuiHelpers.GlobalScale * 30 * 2) - 5);
+
+ if (ImGuiComponents.IconButton($"###cloneButton{profile.Guid}", FontAwesomeIcon.Copy))
+ toCloneGuid = profile.Guid;
+
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip(Locs.CloneProfileHint);
+
+ ImGui.SameLine();
+ ImGui.SetCursorPosX(windowSize.X - (ImGuiHelpers.GlobalScale * 30 * 3) - 5);
+
+ if (ImGuiComponents.IconButton($"###exportButton{profile.Guid}", FontAwesomeIcon.FileExport))
+ {
+ ImGui.SetClipboardText(profile.Model.Serialize());
+ Service.Get().AddNotification(Locs.CopyToClipboardNotification, type: NotificationType.Success);
+ }
+
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip(Locs.CopyToClipboardHint);
+
+ didAny = true;
+
+ ImGuiHelpers.ScaledDummy(2);
+ }
+
+ if (toCloneGuid != null)
+ {
+ profman.CloneProfile(profman.Profiles.First(x => x.Guid == toCloneGuid));
+ }
+
+ if (!didAny)
+ {
+ ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
+ ImGuiHelpers.CenteredText(Locs.AddProfileHint);
+ ImGui.PopStyleColor();
+ }
+
+ ImGui.EndChild();
+ }
+ }
+
+ private void DrawEdit()
+ {
+ if (this.editingProfileGuid == null)
+ {
+ Log.Error("Editing profile guid was null");
+ this.Reset();
+ return;
+ }
+
+ var profman = Service.Get();
+ var pm = Service.Get();
+ var pic = Service.Get();
+ var profile = profman.Profiles.FirstOrDefault(x => x.Guid == this.editingProfileGuid);
+
+ if (profile == null)
+ {
+ Log.Error("Could not find profile {Guid} for edit", this.editingProfileGuid);
+ this.Reset();
+ return;
+ }
+
+ const string addPluginToProfilePopup = "###addPluginToProfile";
+ using (var popup = ImRaii.Popup(addPluginToProfilePopup))
+ {
+ if (popup.Success)
+ {
+ var width = ImGuiHelpers.GlobalScale * 300;
+
+ using var disabled = ImRaii.Disabled(profman.IsBusy);
+
+ ImGui.SetNextItemWidth(width);
+ ImGui.InputTextWithHint("###pluginPickerSearch", Locs.SearchHint, ref this.pickerSearch, 255);
+
+ if (ImGui.BeginListBox("###pluginPicker", new Vector2(width, width - 80)))
+ {
+ // TODO: Plugin searching should be abstracted... installer and this should use the same search
+ foreach (var plugin in pm.InstalledPlugins.Where(x => x.Manifest.SupportsProfiles &&
+ (this.pickerSearch.IsNullOrWhitespace() || x.Manifest.Name.ToLowerInvariant().Contains(this.pickerSearch.ToLowerInvariant()))))
+ {
+ using var disabled2 =
+ ImRaii.Disabled(profile.Plugins.Any(y => y.InternalName == plugin.Manifest.InternalName));
+
+ if (ImGui.Selectable($"{plugin.Manifest.Name}###selector{plugin.Manifest.InternalName}"))
+ {
+ // TODO this sucks
+ profile.AddOrUpdate(plugin.Manifest.InternalName, true, false);
+ Task.Run(() => profman.ApplyAllWantStates())
+ .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState);
+ }
+ }
+
+ ImGui.EndListBox();
+ }
+ }
+ }
+
+ var didAny = false;
+
+ // ======== Top bar ========
+ var windowSize = ImGui.GetWindowSize();
+
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.ArrowLeft))
+ this.Reset();
+
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip(Locs.BackToOverview);
+
+ ImGui.SameLine();
+ ImGuiHelpers.ScaledDummy(5);
+ ImGui.SameLine();
+
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.FileExport))
+ {
+ ImGui.SetClipboardText(profile.Model.Serialize());
+ Service.Get().AddNotification(Locs.CopyToClipboardNotification, type: NotificationType.Success);
+ }
+
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip(Locs.CopyToClipboardHint);
+
+ ImGui.SameLine();
+ ImGuiHelpers.ScaledDummy(5);
+ ImGui.SameLine();
+
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.Trash))
+ {
+ this.Reset();
+
+ // DeleteProfile() is sync, it doesn't apply and we are modifying the plugins collection. Will throw below when iterating
+ profman.DeleteProfile(profile);
+ Task.Run(() => profman.ApplyAllWantStates())
+ .ContinueWith(t =>
+ {
+ this.installer.DisplayErrorContinuation(t, Locs.ErrorCouldNotChangeState);
+ });
+ }
+
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip(Locs.DeleteProfileHint);
+
+ ImGui.SameLine();
+ ImGuiHelpers.ScaledDummy(5);
+ ImGui.SameLine();
+
+ ImGui.SetNextItemWidth(windowSize.X / 3);
+ if (ImGui.InputText("###profileNameInput", ref this.profileNameEdit, 255))
+ {
+ profile.Name = this.profileNameEdit;
+ }
+
+ ImGui.SameLine();
+ ImGui.SetCursorPosX(windowSize.X - (ImGui.GetFrameHeight() * 1.55f * ImGuiHelpers.GlobalScale));
+
+ var isEnabled = profile.IsEnabled;
+ if (ImGuiComponents.ToggleButton($"###toggleButton{profile.Guid}", ref isEnabled))
+ {
+ Task.Run(() => profile.SetState(isEnabled))
+ .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState);
+ }
+
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip(Locs.TooltipEnableDisable);
+
+ ImGui.Separator();
+
+ ImGuiHelpers.ScaledDummy(5);
+
+ var enableAtBoot = profile.AlwaysEnableAtBoot;
+ if (ImGui.Checkbox(Locs.AlwaysEnableAtBoot, ref enableAtBoot))
+ {
+ profile.AlwaysEnableAtBoot = enableAtBoot;
+ }
+
+ ImGuiHelpers.ScaledDummy(5);
+
+ ImGui.Separator();
+ var wantPluginAddPopup = false;
+
+ if (ImGui.BeginChild("###profileEditorPluginList"))
+ {
+ var pluginLineHeight = 32 * ImGuiHelpers.GlobalScale;
+ string? wantRemovePluginInternalName = null;
+
+ foreach (var plugin in profile.Plugins)
+ {
+ didAny = true;
+ var pmPlugin = pm.InstalledPlugins.FirstOrDefault(x => x.Manifest.InternalName == plugin.InternalName);
+ var btnOffset = 2;
+
+ if (pmPlugin != null)
+ {
+ pic.TryGetIcon(pmPlugin, pmPlugin.Manifest, pmPlugin.Manifest.IsThirdParty, out var icon);
+ icon ??= pic.DefaultIcon;
+
+ ImGui.Image(icon.ImGuiHandle, new Vector2(pluginLineHeight));
+ ImGui.SameLine();
+
+ var text = $"{pmPlugin.Name}";
+ var textHeight = ImGui.CalcTextSize(text);
+ var before = ImGui.GetCursorPos();
+
+ ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (textHeight.Y / 2));
+ ImGui.TextUnformatted(text);
+
+ ImGui.SetCursorPos(before);
+ }
+ else
+ {
+ ImGui.Image(pic.DefaultIcon.ImGuiHandle, new Vector2(pluginLineHeight));
+ ImGui.SameLine();
+
+ var text = Locs.NotInstalled(plugin.InternalName);
+ var textHeight = ImGui.CalcTextSize(text);
+ var before = ImGui.GetCursorPos();
+
+ ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (textHeight.Y / 2));
+ ImGui.TextUnformatted(text);
+
+ var available =
+ pm.AvailablePlugins.FirstOrDefault(
+ x => x.InternalName == plugin.InternalName && !x.SourceRepo.IsThirdParty);
+ if (available != null)
+ {
+ ImGui.SameLine();
+ ImGui.SetCursorPosX(windowSize.X - (ImGuiHelpers.GlobalScale * 30 * 2) - 2);
+ ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (ImGui.GetFrameHeight() / 2));
+ btnOffset = 3;
+
+ if (ImGuiComponents.IconButton($"###installMissingPlugin{available.InternalName}", FontAwesomeIcon.Download))
+ {
+ this.installer.StartInstall(available, false);
+ }
+
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip(Locs.InstallPlugin);
+ }
+
+ ImGui.SetCursorPos(before);
+ }
+
+ ImGui.SameLine();
+ ImGui.SetCursorPosX(windowSize.X - (ImGuiHelpers.GlobalScale * 30));
+ ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (ImGui.GetFrameHeight() / 2));
+
+ var enabled = plugin.IsEnabled;
+ if (ImGui.Checkbox($"###{this.editingProfileGuid}-{plugin.InternalName}", ref enabled))
+ {
+ Task.Run(() => profile.AddOrUpdate(plugin.InternalName, enabled))
+ .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState);
+ }
+
+ ImGui.SameLine();
+ ImGui.SetCursorPosX(windowSize.X - (ImGuiHelpers.GlobalScale * 30 * btnOffset) - 5);
+ ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (ImGui.GetFrameHeight() / 2));
+
+ if (ImGuiComponents.IconButton($"###removePlugin{plugin.InternalName}", FontAwesomeIcon.Trash))
+ {
+ wantRemovePluginInternalName = plugin.InternalName;
+ }
+
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip(Locs.RemovePlugin);
+ }
+
+ if (wantRemovePluginInternalName != null)
+ {
+ // TODO: handle error
+ profile.Remove(wantRemovePluginInternalName, false);
+ Task.Run(() => profman.ApplyAllWantStates())
+ .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotRemove);
+ }
+
+ if (!didAny)
+ {
+ ImGui.TextColored(ImGuiColors.DalamudGrey, Locs.NoPluginsInProfile);
+ }
+
+ ImGuiHelpers.ScaledDummy(10);
+
+ var addPluginsText = Locs.AddPlugin;
+ ImGuiHelpers.CenterCursorFor((int)(ImGui.CalcTextSize(addPluginsText).X + 30 + (ImGuiHelpers.GlobalScale * 5)));
+
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.Plus))
+ wantPluginAddPopup = true;
+
+ ImGui.SameLine();
+ ImGuiHelpers.ScaledDummy(5);
+ ImGui.SameLine();
+
+ ImGui.TextUnformatted(addPluginsText);
+
+ ImGuiHelpers.ScaledDummy(10);
+
+ ImGui.EndChild();
+ }
+
+ if (wantPluginAddPopup)
+ {
+ this.pickerSearch = string.Empty;
+ ImGui.OpenPopup(addPluginToProfilePopup);
+ }
+ }
+
+ private static class Locs
+ {
+ public static string TooltipEnableDisable =>
+ Loc.Localize("ProfileManagerEnableDisableHint", "Enable/Disable this collection");
+
+ public static string InstallPlugin => Loc.Localize("ProfileManagerInstall", "Install this plugin");
+
+ public static string RemovePlugin =>
+ Loc.Localize("ProfileManagerRemoveFromProfile", "Remove plugin from this collection");
+
+ public static string AddPlugin => Loc.Localize("ProfileManagerAddPlugin", "Add a plugin!");
+
+ public static string NoPluginsInProfile =>
+ Loc.Localize("ProfileManagerNoPluginsInProfile", "Collection has no plugins!");
+
+ public static string AlwaysEnableAtBoot =>
+ Loc.Localize("ProfileManagerAlwaysEnableAtBoot", "Always enable when game starts");
+
+ public static string DeleteProfileHint => Loc.Localize("ProfileManagerDeleteProfile", "Delete this collection");
+
+ public static string CopyToClipboardHint =>
+ Loc.Localize("ProfileManagerCopyToClipboard", "Copy collection to clipboard for sharing");
+
+ public static string CopyToClipboardNotification =>
+ Loc.Localize("ProfileManagerCopyToClipboardHint", "Copied to clipboard!");
+
+ public static string BackToOverview => Loc.Localize("ProfileManagerBackToOverview", "Back to overview");
+
+ public static string SearchHint => Loc.Localize("ProfileManagerSearchHint", "Search...");
+
+ public static string AddProfileHint => Loc.Localize("ProfileManagerAddProfileHint", "No collections! Add one!");
+
+ public static string CloneProfileHint => Loc.Localize("ProfileManagerCloneProfile", "Clone this collection");
+
+ public static string EditProfileHint => Loc.Localize("ProfileManagerEditProfile", "Edit this collection");
+
+ public static string ImportProfileHint =>
+ Loc.Localize("ProfileManagerImportProfile", "Import a shared collection from your clipboard");
+
+ public static string AddProfile => Loc.Localize("ProfileManagerAddProfile", "Add a new collection");
+
+ public static string NotificationImportSuccess =>
+ Loc.Localize("ProfileManagerNotificationImportSuccess", "Collection successfully imported!");
+
+ public static string NotificationImportError =>
+ Loc.Localize("ProfileManagerNotificationImportError", "Could not import collection.");
+
+ public static string ErrorCouldNotRemove =>
+ Loc.Localize("ProfileManagerCouldNotRemove", "Could not remove plugin.");
+
+ public static string ErrorCouldNotChangeState =>
+ Loc.Localize("ProfileManagerCouldNotChangeState", "Could not change plugin state.");
+
+ public static string NotInstalled(string name) =>
+ Loc.Localize("ProfileManagerNotInstalled", "{0} (Not Installed)").Format(name);
+ }
+}
diff --git a/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs b/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs
index c83df2a4c..44e43fbd3 100644
--- a/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs
@@ -23,6 +23,8 @@ namespace Dalamud.Interface.Internal.Windows;
internal class PluginStatWindow : Window
{
private bool showDalamudHooks;
+ private string drawSearchText = string.Empty;
+ private string frameworkSearchText = string.Empty;
private string hookSearchText = string.Empty;
///
@@ -79,6 +81,12 @@ internal class PluginStatWindow : Window
ImGui.SameLine();
ImGuiComponents.TextWithLabel("Collective Average", $"{(loadedPlugins.Any() ? totalAverage / loadedPlugins.Count() / 10000f : 0):F4}ms", "Average of all average draw times");
+ ImGui.InputTextWithHint(
+ "###PluginStatWindow_DrawSearch",
+ "Search",
+ ref this.drawSearchText,
+ 500);
+
if (ImGui.BeginTable(
"##PluginStatsDrawTimes",
4,
@@ -104,16 +112,22 @@ internal class PluginStatWindow : Window
? loadedPlugins.OrderBy(plugin => plugin.Name)
: loadedPlugins.OrderByDescending(plugin => plugin.Name),
2 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
- ? loadedPlugins.OrderBy(plugin => plugin.DalamudInterface?.UiBuilder.MaxDrawTime)
- : loadedPlugins.OrderByDescending(plugin => plugin.DalamudInterface?.UiBuilder.MaxDrawTime),
+ ? loadedPlugins.OrderBy(plugin => plugin.DalamudInterface?.UiBuilder.MaxDrawTime ?? 0)
+ : loadedPlugins.OrderByDescending(plugin => plugin.DalamudInterface?.UiBuilder.MaxDrawTime ?? 0),
3 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
- ? loadedPlugins.OrderBy(plugin => plugin.DalamudInterface?.UiBuilder.DrawTimeHistory.Average())
- : loadedPlugins.OrderByDescending(plugin => plugin.DalamudInterface?.UiBuilder.DrawTimeHistory.Average()),
+ ? loadedPlugins.OrderBy(plugin => plugin.DalamudInterface?.UiBuilder.DrawTimeHistory.DefaultIfEmpty().Average() ?? 0)
+ : loadedPlugins.OrderByDescending(plugin => plugin.DalamudInterface?.UiBuilder.DrawTimeHistory.DefaultIfEmpty().Average() ?? 0),
_ => loadedPlugins,
};
foreach (var plugin in loadedPlugins)
{
+ if (!this.drawSearchText.IsNullOrEmpty()
+ && !plugin.Manifest.Name.Contains(this.drawSearchText, StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
ImGui.TableNextRow();
ImGui.TableNextColumn();
@@ -168,6 +182,12 @@ internal class PluginStatWindow : Window
ImGui.SameLine();
ImGuiComponents.TextWithLabel("Collective Average", $"{(statsHistory.Any() ? totalAverage / statsHistory.Length : 0):F4}ms", "Average of all average update times");
+ ImGui.InputTextWithHint(
+ "###PluginStatWindow_FrameworkSearch",
+ "Search",
+ ref this.frameworkSearchText,
+ 500);
+
if (ImGui.BeginTable(
"##PluginStatsFrameworkTimes",
4,
@@ -208,6 +228,13 @@ internal class PluginStatWindow : Window
continue;
}
+ if (!this.frameworkSearchText.IsNullOrEmpty()
+ && handlerHistory.Key != null
+ && !handlerHistory.Key.Contains(this.frameworkSearchText, StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
ImGui.TableNextRow();
ImGui.TableNextColumn();
diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs
index ec22ef8d7..62981f4a2 100644
--- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs
+++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs
@@ -46,6 +46,14 @@ public class SettingsTabExperimental : SettingsTab
new GapSettingsEntry(5, true),
new ThirdRepoSettingsEntry(),
+
+ new GapSettingsEntry(5, true),
+
+ new SettingsEntry(
+ Loc.Localize("DalamudSettingsEnableProfiles", "Enable plugin collections"),
+ Loc.Localize("DalamudSettingsEnableProfilesHint", "Enables plugin collections, which lets you create toggleable lists of plugins."),
+ c => c.ProfilesEnabled,
+ (v, c) => c.ProfilesEnabled = v),
};
public override string Title => Loc.Localize("DalamudSettingsExperimental", "Experimental");
diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/DevPluginsSettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/DevPluginsSettingsEntry.cs
index 4e69dcb1a..3e73454f3 100644
--- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/DevPluginsSettingsEntry.cs
+++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/DevPluginsSettingsEntry.cs
@@ -67,7 +67,7 @@ public class DevPluginsSettingsEntry : SettingsEntry
}
}
- ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsDevPluginLocationsHint", "Add additional dev plugin load locations.\nThese can be either the directory or DLL path."));
+ ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsDevPluginLocationsHint", "Add dev plugin load locations.\nThese can be either the directory or DLL path."));
ImGuiHelpers.ScaledDummy(5);
diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/SettingsEntry{T}.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/SettingsEntry{T}.cs
index 6352d7b69..83be6a052 100644
--- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/SettingsEntry{T}.cs
+++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/SettingsEntry{T}.cs
@@ -23,8 +23,16 @@ internal sealed class SettingsEntry : SettingsEntry
private object? valueBacking;
private object? fallbackValue;
- public SettingsEntry(string name, string description, LoadSettingDelegate load, SaveSettingDelegate save, Action? change = null, Func? warning = null, Func? validity = null, Func? visibility = null,
- object? fallbackValue = null)
+ public SettingsEntry(
+ string name,
+ string description,
+ LoadSettingDelegate load,
+ SaveSettingDelegate save,
+ Action? change = null,
+ Func? warning = null,
+ Func? validity = null,
+ Func? visibility = null,
+ object? fallbackValue = null)
{
this.load = load;
this.save = save;
diff --git a/Dalamud/Interface/Internal/Windows/StyleEditor/StyleEditorWindow.cs b/Dalamud/Interface/Internal/Windows/StyleEditor/StyleEditorWindow.cs
index 8fe978ef1..419361b3b 100644
--- a/Dalamud/Interface/Internal/Windows/StyleEditor/StyleEditorWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/StyleEditor/StyleEditorWindow.cs
@@ -11,6 +11,7 @@ using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Style;
using Dalamud.Interface.Windowing;
+using Dalamud.Utility;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets;
using Serilog;
@@ -97,7 +98,7 @@ public class StyleEditorWindow : Window
this.SaveStyle();
var newStyle = StyleModelV1.DalamudStandard;
- newStyle.Name = GetRandomName();
+ newStyle.Name = Util.GetRandomName();
config.SavedStyles.Add(newStyle);
this.currentSel = config.SavedStyles.Count - 1;
@@ -167,11 +168,11 @@ public class StyleEditorWindow : Window
{
var newStyle = StyleModel.Deserialize(styleJson);
- newStyle.Name ??= GetRandomName();
+ newStyle.Name ??= Util.GetRandomName();
if (config.SavedStyles.Any(x => x.Name == newStyle.Name))
{
- newStyle.Name = $"{newStyle.Name} ({GetRandomName()} Mix)";
+ newStyle.Name = $"{newStyle.Name} ({Util.GetRandomName()} Mix)";
}
config.SavedStyles.Add(newStyle);
@@ -375,15 +376,6 @@ public class StyleEditorWindow : Window
}
}
- private static string GetRandomName()
- {
- var data = Service.Get();
- var names = data.GetExcelSheet(ClientLanguage.English);
- var rng = new Random();
-
- return names.ElementAt(rng.Next(0, names.Count() - 1)).Singular.RawString;
- }
-
private void SaveStyle()
{
if (this.currentSel < 2)
diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs
index 3c70c644c..143fda6ab 100644
--- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs
@@ -254,7 +254,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable
var initialCursor = ImGui.GetCursorPos();
-
using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, (float)shadeEasing.Value))
{
ImGui.Image(this.shadeTexture.ImGuiHandle, new Vector2(this.shadeTexture.Width * scale, this.shadeTexture.Height * scale));
diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs
index 5bd317af5..5a757e0e6 100644
--- a/Dalamud/Interface/UiBuilder.cs
+++ b/Dalamud/Interface/UiBuilder.cs
@@ -33,12 +33,12 @@ public sealed class UiBuilder : IDisposable
private readonly GameFontManager gameFontManager = Service.Get();
private readonly DragDropManager dragDropManager = Service.Get();
- private bool hasErrorWindow = false;
- private bool lastFrameUiHideState = false;
-
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service.Get();
+ private bool hasErrorWindow = false;
+ private bool lastFrameUiHideState = false;
+
///
/// Initializes a new instance of the class and registers it.
/// You do not have to call this manually.
diff --git a/Dalamud/IoC/Internal/ResolveViaAttribute.cs b/Dalamud/IoC/Internal/ResolveViaAttribute.cs
new file mode 100644
index 000000000..002878525
--- /dev/null
+++ b/Dalamud/IoC/Internal/ResolveViaAttribute.cs
@@ -0,0 +1,21 @@
+using System;
+
+namespace Dalamud.IoC.Internal;
+
+///
+/// Indicates that an interface a service can implement can be used to resolve that service.
+/// Take care: only one service can implement an interface with this attribute at a time.
+///
+/// The interface that can be used to resolve the service.
+[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
+internal class ResolveViaAttribute : ResolveViaAttribute
+{
+}
+
+///
+/// Helper class used for matching. Use the generic version.
+///
+[AttributeUsage(AttributeTargets.Class)]
+internal class ResolveViaAttribute : Attribute
+{
+}
diff --git a/Dalamud/IoC/Internal/ServiceContainer.cs b/Dalamud/IoC/Internal/ServiceContainer.cs
index 18d294a3e..feac634f3 100644
--- a/Dalamud/IoC/Internal/ServiceContainer.cs
+++ b/Dalamud/IoC/Internal/ServiceContainer.cs
@@ -1,12 +1,12 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Threading.Tasks;
using Dalamud.Logging.Internal;
-using Dalamud.Plugin.Internal.Types;
namespace Dalamud.IoC.Internal;
@@ -18,6 +18,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType
private static readonly ModuleLog Log = new("SERVICECONTAINER");
private readonly Dictionary instances = new();
+ private readonly Dictionary interfaceToTypeMap = new();
///
/// Initializes a new instance of the class.
@@ -39,6 +40,20 @@ internal class ServiceContainer : IServiceProvider, IServiceType
}
this.instances[typeof(T)] = new(instance.ContinueWith(x => new WeakReference(x.Result)), typeof(T));
+
+ var resolveViaTypes = typeof(T)
+ .GetCustomAttributes()
+ .OfType()
+ .Select(x => x.GetType().GetGenericArguments().First());
+ foreach (var resolvableType in resolveViaTypes)
+ {
+ Log.Verbose("=> {InterfaceName} provides for {TName}", resolvableType.FullName ?? "???", typeof(T).FullName ?? "???");
+
+ Debug.Assert(!this.interfaceToTypeMap.ContainsKey(resolvableType), "A service already implements this interface, this is not allowed");
+ Debug.Assert(typeof(T).IsAssignableTo(resolvableType), "Service does not inherit from indicated ResolveVia type");
+
+ this.interfaceToTypeMap[resolvableType] = typeof(T);
+ }
}
///
@@ -233,6 +248,9 @@ internal class ServiceContainer : IServiceProvider, IServiceType
private async Task
+ [Obsolete("This is merely used for migrations now. Use the profile manager to check if a plugin shall be enabled.")]
public bool Disabled { get; set; }
///
diff --git a/Dalamud/Plugin/Internal/Types/PluginManifest.cs b/Dalamud/Plugin/Internal/Types/PluginManifest.cs
index 66b17ed73..71051e666 100644
--- a/Dalamud/Plugin/Internal/Types/PluginManifest.cs
+++ b/Dalamud/Plugin/Internal/Types/PluginManifest.cs
@@ -162,6 +162,12 @@ internal record PluginManifest
[JsonProperty]
public bool CanUnloadAsync { get; init; }
+ ///
+ /// Gets a value indicating whether the plugin supports profiles.
+ ///
+ [JsonProperty]
+ public bool SupportsProfiles { get; init; } = true;
+
///
/// Gets a list of screenshot image URLs to show in the plugin installer.
///
diff --git a/Dalamud/Plugin/PluginLoadReason.cs b/Dalamud/Plugin/PluginLoadReason.cs
index ade95ae67..d4c1a3b26 100644
--- a/Dalamud/Plugin/PluginLoadReason.cs
+++ b/Dalamud/Plugin/PluginLoadReason.cs
@@ -30,3 +30,5 @@ public enum PluginLoadReason
///
Boot,
}
+
+// TODO(api9): This should be a mask, so that we can combine Installer | ProfileLoaded
diff --git a/Dalamud/Plugin/Services/IAetheryteList.cs b/Dalamud/Plugin/Services/IAetheryteList.cs
new file mode 100644
index 000000000..d98e846df
--- /dev/null
+++ b/Dalamud/Plugin/Services/IAetheryteList.cs
@@ -0,0 +1,23 @@
+using System.Collections.Generic;
+
+using Dalamud.Game.ClientState.Aetherytes;
+
+namespace Dalamud.Plugin.Services;
+
+///
+/// This collection represents the list of available Aetherytes in the Teleport window.
+///
+public interface IAetheryteList : IReadOnlyCollection
+{
+ ///
+ /// Gets the amount of Aetherytes the local player has unlocked.
+ ///
+ public int Length { get; }
+
+ ///
+ /// Gets a Aetheryte Entry at the specified index.
+ ///
+ /// Index.
+ /// A at the specified index.
+ public AetheryteEntry? this[int index] { get; }
+}
diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs
index c13d39f4c..41ffe33ca 100644
--- a/Dalamud/ServiceManager.cs
+++ b/Dalamud/ServiceManager.cs
@@ -16,18 +16,15 @@ using JetBrains.Annotations;
namespace Dalamud;
+// TODO:
+// - Unify dependency walking code(load/unload
+// - Visualize/output .dot or imgui thing
+
///
/// Class to initialize Service<T>s.
///
internal static class ServiceManager
{
- /**
- * TODO:
- * - Unify dependency walking code(load/unload
- * - Visualize/output .dot or imgui thing
- */
-
-
///
/// Static log facility for Service{T}, to avoid duplicate instances for different types.
///
diff --git a/Dalamud/Service{T}.cs b/Dalamud/Service{T}.cs
index a1a56a9f0..aa10ead6e 100644
--- a/Dalamud/Service{T}.cs
+++ b/Dalamud/Service{T}.cs
@@ -141,7 +141,6 @@ internal static class Service where T : IServiceType
.OfType()
.Select(x => x.GetType().GetGenericArguments().First()));
-
// HACK: PluginManager needs to depend on ALL plugin exposed services
if (typeof(T) == typeof(PluginManager))
{
diff --git a/Dalamud/Support/BugBait.cs b/Dalamud/Support/BugBait.cs
index ce74c03ec..cdbf94616 100644
--- a/Dalamud/Support/BugBait.cs
+++ b/Dalamud/Support/BugBait.cs
@@ -1,6 +1,7 @@
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
+
using Dalamud.Networking.Http;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Utility;
diff --git a/Dalamud/Support/Troubleshooting.cs b/Dalamud/Support/Troubleshooting.cs
index ad6bacb0f..ef1897eeb 100644
--- a/Dalamud/Support/Troubleshooting.cs
+++ b/Dalamud/Support/Troubleshooting.cs
@@ -68,7 +68,7 @@ public static class Troubleshooting
var payload = new TroubleshootingPayload
{
LoadedPlugins = pluginManager?.InstalledPlugins?.Select(x => x.Manifest)?.OrderByDescending(x => x.InternalName).ToArray(),
- PluginStates = pluginManager?.InstalledPlugins?.ToDictionary(x => x.Manifest.InternalName, x => x.IsBanned ? "Banned" : x.State.ToString()),
+ PluginStates = pluginManager?.InstalledPlugins?.Where(x => !x.IsDev).ToDictionary(x => x.Manifest.InternalName, x => x.IsBanned ? "Banned" : x.State.ToString()),
EverStartedLoadingPlugins = pluginManager?.InstalledPlugins.Where(x => x.HasEverStartedLoad).Select(x => x.InternalName).ToList(),
DalamudVersion = Util.AssemblyVersion,
DalamudGitHash = Util.GetGitHash(),
diff --git a/Dalamud/Utility/AsyncUtils.cs b/Dalamud/Utility/AsyncUtils.cs
index d252bd5d5..d02c90195 100644
--- a/Dalamud/Utility/AsyncUtils.cs
+++ b/Dalamud/Utility/AsyncUtils.cs
@@ -19,7 +19,7 @@ public static class AsyncUtils
/// A list of tasks to race.
/// The return type of all raced tasks.
/// Thrown when all tasks given to this method fail.
- /// Returns the first task that completes, according to .
+ /// Returns the first task that completes, according to .
public static Task FirstSuccessfulTask(ICollection> tasks)
{
var tcs = new TaskCompletionSource();
@@ -51,7 +51,7 @@ public static class AsyncUtils
{
try
{
- await Task.Delay(millisecondsDelay, cancellationToken);
+ await Task.Delay(millisecondsDelay, cancellationToken).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
diff --git a/Dalamud/Utility/Numerics/VectorExtensions.cs b/Dalamud/Utility/Numerics/VectorExtensions.cs
index cd958deb8..910dbdd00 100644
--- a/Dalamud/Utility/Numerics/VectorExtensions.cs
+++ b/Dalamud/Utility/Numerics/VectorExtensions.cs
@@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
+
using FFXIVClientStructs.FFXIV.Client.Graphics;
namespace Dalamud.Utility.Numerics;
diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs
index ecf672caf..4b8fe6822 100644
--- a/Dalamud/Utility/Util.cs
+++ b/Dalamud/Utility/Util.cs
@@ -11,6 +11,7 @@ using System.Runtime.CompilerServices;
using System.Text;
using Dalamud.Configuration.Internal;
+using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Interface;
@@ -18,6 +19,7 @@ using Dalamud.Interface.Colors;
using Dalamud.Logging.Internal;
using Dalamud.Networking.Http;
using ImGuiNET;
+using Lumina.Excel.GeneratedSheets;
using Microsoft.Win32;
using Serilog;
@@ -657,6 +659,19 @@ public static class Util
File.Move(tmpPath, path, true);
}
+ ///
+ /// Gets a random, inoffensive, human-friendly string.
+ ///
+ /// A random human-friendly name.
+ internal static string GetRandomName()
+ {
+ var data = Service.Get();
+ var names = data.GetExcelSheet(ClientLanguage.English)!;
+ var rng = new Random();
+
+ return names.ElementAt(rng.Next(0, names.Count() - 1)).Singular.RawString;
+ }
+
private static unsafe void ShowValue(ulong addr, IEnumerable path, Type type, object value)
{
if (type.IsPointer)
diff --git a/README.md b/README.md
index f98961c3e..97dd4e9dd 100644
--- a/README.md
+++ b/README.md
@@ -26,23 +26,22 @@ Thanks to Mino, whose work has made this possible!
These components are used in order to load Dalamud into a target process.
Dalamud can be loaded via DLL injection, or by rewriting a process' entrypoint.
-| Name | Purpose |
-|---|---|
-| *Dalamud.Injector.Boot* (C++) | Loads the .NET Core runtime into a process via hostfxr and kicks off Dalamud.Injector |
-| *Dalamud.Injector* (C#) | Performs DLL injection on the target process |
-| *Dalamud.Boot* (C++) | Loads the .NET Core runtime into the active process and kicks off Dalamud, or rewrites a target process' entrypoint to do so |
-| *Dalamud* (C#) | Core API, game bindings, plugin framework |
-| *Dalamud.CorePlugin* (C#) | Testbed plugin that can access Dalamud internals, to prototype new Dalamud features |
+| Name | Purpose |
+|-------------------------------|------------------------------------------------------------------------------------------------------------------------------|
+| *Dalamud.Injector.Boot* (C++) | Loads the .NET Core runtime into a process via hostfxr and kicks off Dalamud.Injector |
+| *Dalamud.Injector* (C#) | Performs DLL injection on the target process |
+| *Dalamud.Boot* (C++) | Loads the .NET Core runtime into the active process and kicks off Dalamud, or rewrites a target process' entrypoint to do so |
+| *Dalamud* (C#) | Core API, game bindings, plugin framework |
+| *Dalamud.CorePlugin* (C#) | Testbed plugin that can access Dalamud internals, to prototype new Dalamud features |
## Branches
We are currently working from the following branches.
-| Name | Purpose | .NET Version | Track |
-|---|---|---|---|
-| *master* | Current release branch | .NET 6.0.3 (March 2022) | Release & Staging |
-| *net7* | Upgrade to .NET 7 | .NET 7.0.0 (November 2022) | net7 |
-| *api3* | Legacy version, no longer in active use | .NET Framework 4.7.2 (April 2017) | - |
+| Name | API Level | Purpose | .NET Version | Track |
+|----------|-----------|------------------------------------------------------------|----------------------------|-------------------|
+| *master* | **8** | Current release branch | .NET 7.0.0 (November 2022) | Release & Staging |
+| *v9* | **9** | Next major version, slated for release alongside Patch 6.5 | .NET 7.0.0 (November 2022) | v9 |
diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs
index 010e878fe..e61e4d8fa 160000
--- a/lib/FFXIVClientStructs
+++ b/lib/FFXIVClientStructs
@@ -1 +1 @@
-Subproject commit 010e878febb631c8f3ff5ff90d656f318e35b1de
+Subproject commit e61e4d8fa9e58e37b454314f0f9a9f305f173483
diff --git a/lib/ImGuiScene b/lib/ImGuiScene
index 262d3b066..2f37349ff 160000
--- a/lib/ImGuiScene
+++ b/lib/ImGuiScene
@@ -1 +1 @@
-Subproject commit 262d3b0668196fb236e2191c4a37e9be94e5a7a3
+Subproject commit 2f37349ffd778561a1103a650683116c43edc86c